agentic-documents 0.4.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 +35 -0
  2. package/README.md +112 -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 +23 -6
  13. package/dist/client.js +9 -2
  14. package/dist/{doc-C_8kCsl7.d.ts → doc-mJPajXEQ.d.ts} +5 -1
  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 +6 -5
  19. package/dist/index.js +11 -7
  20. package/dist/live-worker.d.ts +2 -2
  21. package/dist/live-worker.js +6 -4
  22. package/dist/live.d.ts +6 -2
  23. package/dist/live.js +4 -3
  24. package/dist/{mcp-P2C75IIG.js → mcp-ESSEG46C.js} +4 -2
  25. package/dist/{review-BQW2Ci2h.d.ts → review-Hgq8OgvD.d.ts} +2 -16
  26. package/dist/{service-Do2Jwb5U.d.ts → service-CqKE9DKz.d.ts} +38 -5
  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 -2
  36. package/dist/worker.js +6 -4
  37. package/llms.txt +1 -0
  38. package/package.json +19 -3
@@ -1,10 +1,13 @@
1
+ import {
2
+ Suggestions,
3
+ Threads,
4
+ isPerson
5
+ } from "./chunk-UPEGODUV.js";
1
6
  import {
2
7
  AgenticDoc,
3
8
  DocError,
4
- PRINCIPAL_KINDS,
5
- Suggestions,
6
- Threads
7
- } from "./chunk-YBBQNCMC.js";
9
+ PRINCIPAL_KINDS
10
+ } from "./chunk-6QOYS63L.js";
8
11
 
9
12
  // src/service.ts
10
13
  import * as Y from "yjs";
@@ -16,7 +19,10 @@ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
16
19
  var isString = (v) => typeof v === "string";
17
20
  var isWho = (v) => isObject(v) && PRINCIPAL_KINDS.includes(v.kind) && isString(v.name) && v.name !== "";
18
21
  var isAnchor = (v) => isObject(v) && isString(v.text) && isObject(v.start) && isObject(v.end);
19
- var ROOTS = /* @__PURE__ */ new Set(["default", "edits", "suggestions", "threads", "applied"]);
22
+ var ROOTS = /* @__PURE__ */ new Set(["default", "edits", "suggestions", "threads", "applied", "__tiptap_position_migrations:root:default"]);
23
+ var MAX_TITLE = 120;
24
+ var REMOVE_SCOPE = "docs:remove";
25
+ var MAX_LISTED = 500;
20
26
  var DocsService = class {
21
27
  constructor(ports, options = {}) {
22
28
  this.options = options;
@@ -47,7 +53,7 @@ var DocsService = class {
47
53
  * has none. A merge is safe to repeat and to run in any order. The save is conditional, so two
48
54
  * checkpoints at once both land.
49
55
  */
50
- async checkpoint(id, update, author) {
56
+ async checkpoint(id, update, author, options = {}) {
51
57
  const key = this.key(id);
52
58
  const scratch = new Y.Doc();
53
59
  try {
@@ -62,6 +68,7 @@ var DocsService = class {
62
68
  for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
63
69
  const found = await this.blobs.get(key);
64
70
  if (!found) {
71
+ if (options.create === false) throw new DocError("NO_DOCUMENT", `no document with id "${id}"`);
65
72
  const created = AgenticDoc.fromUpdate(update, this.options);
66
73
  const fresh = this.checked(id, created);
67
74
  this.fits(id, fresh);
@@ -73,7 +80,10 @@ var DocsService = class {
73
80
  doc.applyUpdate(update);
74
81
  const merged = this.checked(id, doc);
75
82
  this.fits(id, merged);
76
- if (await this.blobs.put(key, merged, { ifMatch: found.version })) return;
83
+ if (await this.blobs.put(key, merged, { ifMatch: found.version })) {
84
+ await this.touch(id, author);
85
+ return;
86
+ }
77
87
  }
78
88
  throw new DocError("CONFLICT", `document "${id}" kept changing; try again`);
79
89
  }
@@ -86,7 +96,7 @@ var DocsService = class {
86
96
  const refuse = (what) => new DocError("INVALID_INPUT", `the update would leave document "${id}" unreadable: ${what}`);
87
97
  try {
88
98
  doc.toMarkdown();
89
- if (!doc.edits().every((e) => isObject(e) && isWho(e.author) && isString(e.at) && isString(e.op))) throw refuse("an edit log entry is malformed");
99
+ if (!doc.edits().every((e) => isObject(e) && isWho(e.author) && isString(e.at) && isString(e.op) && (e.client === void 0 || Number.isSafeInteger(e.client) && e.client >= 0))) throw refuse("an edit log entry is malformed");
90
100
  for (const [, t] of doc.ydoc.getMap("threads")) {
91
101
  const messages = t instanceof Y.Map ? t.get("messages") : void 0;
92
102
  const ok = t instanceof Y.Map && isAnchor(t.get("anchor")) && ["open", "resolved"].includes(t.get("status")) && messages instanceof Y.Array && messages.length > 0 && messages.toArray().every((m) => isObject(m) && isString(m.id) && isString(m.at) && isString(m.body) && isWho(m.author));
@@ -108,9 +118,71 @@ var DocsService = class {
108
118
  }
109
119
  async catalog(id, author) {
110
120
  await this.store.transaction(async (tx) => {
111
- if (await tx.get(CATALOG, id) === null) await tx.put(CATALOG, id, { createdBy: { kind: author.kind, name: author.name }, createdAt: Date.now() });
121
+ if (await tx.get(CATALOG, id) === null) {
122
+ const now = Date.now();
123
+ const who = { kind: author.kind, name: author.name };
124
+ await tx.put(CATALOG, id, { createdBy: who, createdAt: now, updatedAt: now, updatedBy: who });
125
+ }
112
126
  });
113
127
  }
128
+ /**
129
+ * Record a save. The catalog is a side record that follows the blob, so a failure here never fails a save that worked:
130
+ * the caller would repeat a change that is not safe to repeat. A document with no catalog entry stays without one.
131
+ */
132
+ async touch(id, author) {
133
+ if (!author) return;
134
+ try {
135
+ await this.store.transaction(async (tx) => {
136
+ const entry = await tx.get(CATALOG, id);
137
+ if (entry !== null) await tx.put(CATALOG, id, { ...entry, updatedAt: Date.now(), updatedBy: { kind: author.kind, name: author.name } });
138
+ });
139
+ } catch {
140
+ }
141
+ }
142
+ /** Every cataloged document, newest first, up to ${MAX_LISTED}. A damaged entry is skipped, so one bad entry never hides the list. */
143
+ async list() {
144
+ const found = [];
145
+ let cursor;
146
+ do {
147
+ const page = await this.store.query(CATALOG, { orderBy: { field: "createdAt", direction: "desc" }, limit: 100, cursor });
148
+ for (const { id, data } of page.items) {
149
+ const entry = isObject(data) ? data : {};
150
+ const { createdBy, createdAt } = entry;
151
+ if (!isWho(createdBy) || typeof createdAt !== "number") continue;
152
+ const by = entry.updatedBy;
153
+ if (by !== void 0 && !isWho(by)) continue;
154
+ found.push({ id, title: typeof entry.title === "string" && entry.title !== "" ? entry.title : id, createdBy, createdAt, updatedAt: typeof entry.updatedAt === "number" ? entry.updatedAt : createdAt, updatedBy: by ?? createdBy });
155
+ }
156
+ cursor = page.nextCursor;
157
+ } while (cursor !== void 0 && found.length < MAX_LISTED);
158
+ return found.slice(0, MAX_LISTED);
159
+ }
160
+ /** Give the document a name. It does not change the text. */
161
+ async rename(id, title, author) {
162
+ const key = this.key(id);
163
+ const clean = title.trim();
164
+ if (clean === "" || [...clean].length > MAX_TITLE || /[\u0000-\u001f\u007f-\u009f\u00ad\u200b-\u200f\u2028-\u202e\u2066-\u2069\ufeff]/.test(clean)) {
165
+ throw new DocError("INVALID_INPUT", `a title is 1 to ${MAX_TITLE} characters of plain text`);
166
+ }
167
+ const found = await this.blobs.get(key);
168
+ if (!found) throw new DocError("NO_DOCUMENT", `no document with id "${id}"`);
169
+ const who = { kind: author.kind, name: author.name };
170
+ const first = AgenticDoc.fromUpdate(found.data, this.options).edits()[0];
171
+ const created = first ? { createdBy: { kind: first.author.kind, name: first.author.name }, createdAt: Date.parse(first.at) } : { createdBy: who, createdAt: Date.now() };
172
+ await this.store.transaction(async (tx) => {
173
+ const entry = await tx.get(CATALOG, id);
174
+ const now = Date.now();
175
+ await tx.put(CATALOG, id, entry === null ? { ...created, createdAt: Number.isFinite(created.createdAt) ? created.createdAt : now, title: clean, updatedAt: now, updatedBy: who } : { ...entry, title: clean, updatedAt: now, updatedBy: who });
176
+ });
177
+ }
178
+ /** Delete the document and its catalog entry. An agent needs the remove scope. A live room keeps its own copy of the text. */
179
+ async remove(id, author) {
180
+ const key = this.key(id);
181
+ if (!isPerson(author) && author.scopes?.includes(REMOVE_SCOPE) !== true) throw new DocError("FORBIDDEN", `an agent needs the ${REMOVE_SCOPE} scope to remove a document`);
182
+ if (!await this.blobs.head(key)) throw new DocError("NO_DOCUMENT", `no document with id "${id}"`);
183
+ await this.store.delete(CATALOG, id);
184
+ await this.blobs.delete(key);
185
+ }
114
186
  /** Who created the document and when, or null when it has no catalog entry (made before the catalog, or a failed catalog write). */
115
187
  async info(id) {
116
188
  this.key(id);
@@ -131,14 +203,17 @@ var DocsService = class {
131
203
  * Load, run the change, and save only if nobody saved in between. When someone did, load again and
132
204
  * run the change again, so a check such as "is it still pending" sees the new state.
133
205
  */
134
- async write(id, run) {
206
+ async write(id, run, author) {
135
207
  const key = this.key(id);
136
208
  for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
137
209
  const found = await this.blobs.get(key);
138
210
  if (!found) throw new DocError("NO_DOCUMENT", `no document with id "${id}"`);
139
211
  const doc = AgenticDoc.fromUpdate(found.data, this.options);
140
212
  const result = run(doc);
141
- if (await this.blobs.put(key, doc.encode(), { ifMatch: found.version })) return result;
213
+ if (await this.blobs.put(key, doc.encode(), { ifMatch: found.version })) {
214
+ await this.touch(id, author);
215
+ return result;
216
+ }
142
217
  }
143
218
  throw new DocError("CONFLICT", `document "${id}" kept changing; try again`);
144
219
  }
package/dist/cli.js CHANGED
@@ -2,24 +2,26 @@
2
2
  import {
3
3
  createFileBlobs,
4
4
  createFileStore
5
- } from "./chunk-TFCRPEEM.js";
5
+ } from "./chunk-7R7ZPPRF.js";
6
6
  import {
7
7
  LiveService,
8
8
  whoAmI
9
- } from "./chunk-R5XUTBC3.js";
9
+ } from "./chunk-E7B6T5WN.js";
10
10
  import {
11
11
  DocsService
12
- } from "./chunk-FZA5LQV3.js";
12
+ } from "./chunk-YNHZCIGO.js";
13
13
  import {
14
14
  VERSION
15
15
  } from "./chunk-5Y66UCTE.js";
16
16
  import {
17
17
  commands
18
- } from "./chunk-GYQFTF2E.js";
18
+ } from "./chunk-S6MQDWFK.js";
19
+ import "./chunk-L6W2AQUF.js";
20
+ import "./chunk-UPEGODUV.js";
19
21
  import {
20
22
  DocError,
21
23
  parseAuthor
22
- } from "./chunk-YBBQNCMC.js";
24
+ } from "./chunk-6QOYS63L.js";
23
25
 
24
26
  // src/cli.ts
25
27
  import { readFileSync, writeFileSync } from "fs";
@@ -118,7 +120,7 @@ async function main(argv, env = process.env) {
118
120
  author = named && scopes?.length ? { ...named, scopes } : named;
119
121
  }
120
122
  if (name === "mcp") {
121
- const { startMcp } = await import("./mcp-P2C75IIG.js");
123
+ const { startMcp } = await import("./mcp-ESSEG46C.js");
122
124
  await startMcp(service, author);
123
125
  return 0;
124
126
  }
package/dist/client.d.ts CHANGED
@@ -1,7 +1,10 @@
1
- import { P as PrincipalKind, S as Section, E as EditEntry } from './doc-C_8kCsl7.js';
2
- export { A as Anchor } from './doc-C_8kCsl7.js';
3
- import { D as DocErrorCode, S as Suggestion, T as Thread, E as ExportFormat } from './review-BQW2Ci2h.js';
1
+ import { P as PrincipalKind, S as Section, E as EditEntry } from './doc-mJPajXEQ.js';
2
+ export { A as Anchor } from './doc-mJPajXEQ.js';
3
+ import { E as ExportFormat } from './export-Cwx276UP.js';
4
+ import { D as DocErrorCode, S as Suggestion, T as Thread } from './review-Hgq8OgvD.js';
5
+ import { D as DocSummary } from './service-CqKE9DKz.js';
4
6
  import 'yjs';
7
+ import 'agentic-identity';
5
8
 
6
9
  /** The place an edit goes: a section id, or exact text. */
7
10
  type Where = {
@@ -69,6 +72,18 @@ declare function createClient(options: ClientOptions): {
69
72
  }, call?: CallOptions) => Promise<{
70
73
  id: string;
71
74
  }>;
75
+ /** `GET /api/v1/docs`: documents with titles, creators, and last edits. */
76
+ list: (call?: CallOptions) => Promise<DocSummary[]>;
77
+ /** `POST /rename`: give a document a title. */
78
+ rename: (id: string, input: {
79
+ title: string;
80
+ }, call?: CallOptions) => Promise<{
81
+ ok: true;
82
+ }>;
83
+ /** `POST /remove`: delete a document. An agent needs the `docs:remove` scope. */
84
+ remove: (id: string, call?: CallOptions) => Promise<{
85
+ ok: true;
86
+ }>;
72
87
  /** `GET /read`: the whole document, or one section, as Markdown. */
73
88
  read(id: string, input?: {
74
89
  section?: string;
@@ -161,11 +176,13 @@ declare function createClient(options: ClientOptions): {
161
176
  }>;
162
177
  /** `GET /state`: the stored document as a Yjs update. */
163
178
  state(id: string, call?: CallOptions): Promise<Uint8Array>;
164
- /** `POST /checkpoint`: merge a Yjs update into the stored document. */
165
- checkpoint(id: string, update: Uint8Array, call?: CallOptions): Promise<{
179
+ /** `POST /checkpoint`: merge a Yjs update into the stored document. It makes the document when there is none, unless `create` is false: then it fails with `NO_DOCUMENT`. */
180
+ checkpoint(id: string, update: Uint8Array, call?: CallOptions & {
181
+ create?: boolean;
182
+ }): Promise<{
166
183
  ok: true;
167
184
  }>;
168
185
  };
169
186
  type DocsClient = ReturnType<typeof createClient>;
170
187
 
171
- export { type CallOptions, type ClientOptions, type Contributor, DocsApiError, type DocsApiErrorCode, type DocsClient, EditEntry, ExportFormat, type Me, Section, Suggestion, Thread, type Where, createClient };
188
+ export { type CallOptions, type ClientOptions, type Contributor, DocSummary, DocsApiError, type DocsApiErrorCode, type DocsClient, EditEntry, ExportFormat, type Me, Section, Suggestion, Thread, type Where, createClient };
package/dist/client.js CHANGED
@@ -72,6 +72,12 @@ function createClient(options) {
72
72
  me: (call) => get("/api/v1/me", {}, (v) => isObject(v) && typeof v.kind === "string" && typeof v.name === "string" && Array.isArray(v.scopes) && (v.role === "editor" || v.role === "viewer"), "the token identity", call),
73
73
  /** `POST /api/v1/docs`. */
74
74
  create: (input, call) => withKey("/api/v1/docs", input, "id", call),
75
+ /** `GET /api/v1/docs`: documents with titles, creators, and last edits. */
76
+ list: (call) => list("/api/v1/docs", {}, call),
77
+ /** `POST /rename`: give a document a title. */
78
+ rename: (id, input, call) => ok(`${doc(id)}/rename`, input, call),
79
+ /** `POST /remove`: delete a document. An agent needs the `docs:remove` scope. */
80
+ remove: (id, call) => ok(`${doc(id)}/remove`, {}, call),
75
81
  /** `GET /read`: the whole document, or one section, as Markdown. */
76
82
  async read(id, input = {}, call) {
77
83
  return (await send(`${doc(id)}/read`, { method: "GET", query: input }, call, "text/markdown")).text();
@@ -102,9 +108,10 @@ function createClient(options) {
102
108
  async state(id, call) {
103
109
  return new Uint8Array(await (await send(`${doc(id)}/state`, { method: "GET" }, call, "application/octet-stream")).arrayBuffer());
104
110
  },
105
- /** `POST /checkpoint`: merge a Yjs update into the stored document. */
111
+ /** `POST /checkpoint`: merge a Yjs update into the stored document. It makes the document when there is none, unless `create` is false: then it fails with `NO_DOCUMENT`. */
106
112
  async checkpoint(id, update, call) {
107
- return await json(await send(`${doc(id)}/checkpoint`, { method: "POST", body: update }, call), isOk, "{ ok: true }");
113
+ const query = call?.create === false ? { create: false } : void 0;
114
+ return await json(await send(`${doc(id)}/checkpoint`, { method: "POST", body: update, query }, call), isOk, "{ ok: true }");
108
115
  }
109
116
  };
110
117
  }
@@ -58,6 +58,8 @@ interface EditEntry {
58
58
  inserted: number;
59
59
  deleted: number;
60
60
  section?: string;
61
+ /** The Yjs client id of the replica that made the edit. Text it wrote carries this id, so a UI can mark text by author kind. Written by the client, like the rest of the log. */
62
+ client?: number;
61
63
  /** Set when an accepted suggestion caused the edit. `author` is who suggested it. */
62
64
  suggestion?: {
63
65
  id: string;
@@ -87,6 +89,8 @@ declare class AgenticDoc {
87
89
  readonly now: () => Date;
88
90
  constructor(ydoc?: Y.Doc, options?: DocOptions);
89
91
  static fromMarkdown(markdown: string, author: Principal, options?: DocOptions): AgenticDoc;
92
+ /** Make a document from blocks in ProseMirror JSON form. Blocks that the schema does not allow fail with INVALID_INPUT. */
93
+ static fromBlocks(content: PmNode[], author: Principal, options?: DocOptions): AgenticDoc;
90
94
  static fromUpdate(update: Uint8Array, options?: DocOptions): AgenticDoc;
91
95
  encode(): Uint8Array;
92
96
  applyUpdate(update: Uint8Array): void;
@@ -123,4 +127,4 @@ declare class AgenticDoc {
123
127
  append(markdown: string, author: Principal): void;
124
128
  }
125
129
 
126
- export { type Anchor as A, type DocOptions as D, type EditEntry as E, type PrincipalKind as P, type Section as S, type Target as T, type Principal as a, type PmNode as b, AgenticDoc as c, type AnchorTarget as d, PRINCIPAL_KINDS as e, type SectionTarget as f, type TextTarget as g, blocksToMarkdown as h, markdownToBlocks as m, parseAuthor as p };
130
+ export { type Anchor as A, type DocOptions as D, type EditEntry as E, type PrincipalKind as P, type Section as S, type Target as T, AgenticDoc as a, type PmNode as b, type Principal as c, type AnchorTarget as d, PRINCIPAL_KINDS as e, type SectionTarget as f, type TextTarget as g, blocksToMarkdown as h, markdownToBlocks as m, parseAuthor as p };
@@ -0,0 +1,17 @@
1
+ import { a as AgenticDoc } from './doc-mJPajXEQ.js';
2
+
3
+ declare const EXPORT_FORMATS: readonly ["md", "html", "odt"];
4
+ type ExportFormat = (typeof EXPORT_FORMATS)[number];
5
+ interface ExportOptions {
6
+ /** Keep `<img>` for web and relative sources. Opening the page then asks those hosts for the image. Off by default. */
7
+ remoteImages?: boolean;
8
+ }
9
+ interface ExportResult {
10
+ format: ExportFormat;
11
+ contentType: string;
12
+ encoding: "utf8" | "base64";
13
+ data: string;
14
+ }
15
+ declare function exportDoc(doc: AgenticDoc, format: ExportFormat, options?: ExportOptions): ExportResult;
16
+
17
+ export { type ExportFormat as E, type ExportOptions as a, EXPORT_FORMATS as b, type ExportResult as c, exportDoc as e };
@@ -0,0 +1,49 @@
1
+ import { Limits, TextDoc } from 'agentic-formats';
2
+ import { D as DocOptions, a as AgenticDoc, b as PmNode, c as Principal } from './doc-mJPajXEQ.js';
3
+ import { a as ExportOptions } from './export-Cwx276UP.js';
4
+ import 'yjs';
5
+
6
+ declare const IMPORT_FORMATS: readonly ["md", "odt", "docx"];
7
+ type ImportFormat = (typeof IMPORT_FORMATS)[number];
8
+ declare const FILE_FORMATS: readonly ["md", "html", "odt", "docx"];
9
+ type FileFormat = (typeof FILE_FORMATS)[number];
10
+ declare const DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
11
+ interface ImportOptions {
12
+ /** Caps for the file. A file that goes past one is refused, never trimmed. The defaults are those of agentic-formats. */
13
+ limits?: Partial<Limits>;
14
+ doc?: DocOptions;
15
+ }
16
+ interface ImportResult {
17
+ doc: AgenticDoc;
18
+ /** The title the file holds, if any. */
19
+ title?: string;
20
+ /** One line for each part of the file that the document cannot hold. Empty when nothing was lost. */
21
+ warnings: string[];
22
+ }
23
+ interface FileResult {
24
+ format: FileFormat;
25
+ contentType: string;
26
+ /** Text for `md` and `html`. File bytes for `odt` and `docx`. */
27
+ data: string | Uint8Array;
28
+ /** One line for each part of the document that the format cannot hold. Empty when nothing was lost. */
29
+ warnings: string[];
30
+ }
31
+ /** Map document blocks to the neutral text model. A part the model cannot hold keeps its text and adds a warning. */
32
+ declare function toTextDoc(blocks: PmNode[], title?: string): {
33
+ model: TextDoc;
34
+ warnings: string[];
35
+ };
36
+ /** Map the neutral text model to document blocks. A part the editor cannot hold keeps its text and adds a warning. */
37
+ declare function fromTextDoc(model: TextDoc): {
38
+ blocks: PmNode[];
39
+ title?: string;
40
+ warnings: string[];
41
+ };
42
+ /** Read a file into a new document. The author makes the one `create` edit, so text from an agent stays labeled. */
43
+ declare function importFile(input: string | Uint8Array, format: ImportFormat, author: Principal, options?: ImportOptions): ImportResult;
44
+ /** Write a document as a file. `title` goes into the ODT and DOCX metadata. */
45
+ declare function exportFile(doc: AgenticDoc, format: FileFormat, options?: ExportOptions & {
46
+ title?: string;
47
+ }): FileResult;
48
+
49
+ export { DOCX_MIME, FILE_FORMATS, type FileFormat, type FileResult, IMPORT_FORMATS, type ImportFormat, type ImportOptions, type ImportResult, exportFile, fromTextDoc, importFile, toTextDoc };
@@ -0,0 +1,234 @@
1
+ import {
2
+ DATA_IMAGE,
3
+ exportDoc,
4
+ fromBase64,
5
+ safeHref,
6
+ toBase64
7
+ } from "./chunk-L6W2AQUF.js";
8
+ import {
9
+ AgenticDoc,
10
+ DocError,
11
+ checkAuthor
12
+ } from "./chunk-6QOYS63L.js";
13
+
14
+ // src/formats.ts
15
+ import { DEFAULT_LIMITS, FormatError, readDocx, readOdt, writeDocx, writeOdt } from "agentic-formats";
16
+ var IMPORT_FORMATS = ["md", "odt", "docx"];
17
+ var FILE_FORMATS = ["md", "html", "odt", "docx"];
18
+ var DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
19
+ var CONTENT_TYPES = {
20
+ md: "text/markdown; charset=utf-8",
21
+ html: "text/html; charset=utf-8",
22
+ odt: "application/vnd.oasis.opendocument.text",
23
+ docx: DOCX_MIME
24
+ };
25
+ var PM_MARKS = { bold: "bold", italic: "italic", strike: "strike", code: "code" };
26
+ var MODEL_IMAGE = /^image\/(png|jpeg|gif)$/;
27
+ var TASK = ["\u2610 ", "\u2611 "];
28
+ var warner = () => {
29
+ const seen = /* @__PURE__ */ new Set();
30
+ return { warn: (line) => void seen.add(line), list: () => [...seen] };
31
+ };
32
+ function toTextDoc(blocks, title) {
33
+ const { warn, list } = warner();
34
+ const inlines = (nodes = [], extra = []) => nodes.flatMap((node) => {
35
+ if (node.type === "hardBreak") return [{ type: "break" }];
36
+ if (node.type === "image") {
37
+ const alt = typeof node.attrs?.alt === "string" ? node.attrs.alt : "";
38
+ const found = DATA_IMAGE.exec(String(node.attrs?.src ?? "").trim());
39
+ if (found && MODEL_IMAGE.test(found[1].toLowerCase())) {
40
+ try {
41
+ return [{ type: "image", data: fromBase64(found[2]), mime: found[1].toLowerCase(), ...alt ? { alt } : {} }];
42
+ } catch {
43
+ }
44
+ }
45
+ warn("an image that is not an embedded PNG, JPEG, or GIF is kept as its alt text");
46
+ return [{ type: "text", text: `[image${alt ? `: ${alt}` : ""}]` }];
47
+ }
48
+ if (!node.text) return [];
49
+ const marks = [...extra, ...(node.marks ?? []).flatMap((m) => PM_MARKS[m.type] ? [PM_MARKS[m.type]] : [])];
50
+ const href = safeHref((node.marks ?? []).find((m) => m.type === "link")?.attrs?.href);
51
+ return [{ type: "text", text: node.text, ...marks.length ? { marks } : {}, ...href ? { href } : {} }];
52
+ });
53
+ const listOf = (node) => {
54
+ const items = (node.content ?? []).map((item) => {
55
+ const [first, ...rest] = item.content ?? [];
56
+ const content = inlines(first?.content);
57
+ if (item.type === "taskItem") content.unshift({ type: "text", text: TASK[item.attrs?.checked ? 1 : 0] });
58
+ let sublist;
59
+ for (const child of rest) {
60
+ if (!sublist && /^(bulletList|orderedList|taskList)$/.test(child.type)) sublist = listOf(child);
61
+ else {
62
+ warn("a list item with more than one paragraph is kept as one paragraph with line breaks");
63
+ for (const b of toBlocks(child)) content.push({ type: "break" }, ...textOfBlock(b));
64
+ }
65
+ }
66
+ return { content, ...sublist ? { sublist } : {} };
67
+ });
68
+ if (node.type === "orderedList" && Number(node.attrs?.start ?? 1) !== 1) warn("a numbered list that does not start at 1 starts at 1");
69
+ return { type: "list", ordered: node.type === "orderedList", items };
70
+ };
71
+ 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;
72
+ const toBlocks = (node) => {
73
+ switch (node.type) {
74
+ case "paragraph":
75
+ return [{ type: "paragraph", content: inlines(node.content) }];
76
+ case "heading": {
77
+ const level = Math.min(6, Math.max(1, Math.trunc(Number(node.attrs?.level)) || 1));
78
+ return [{ type: "heading", level, content: inlines(node.content) }];
79
+ }
80
+ case "blockquote":
81
+ warn("a quote is kept as plain paragraphs");
82
+ return (node.content ?? []).flatMap(toBlocks);
83
+ case "codeBlock": {
84
+ warn("a code block is kept as a paragraph of code text");
85
+ const lines = (node.content ?? []).map((t) => t.text ?? "").join("").split("\n");
86
+ return [{ type: "paragraph", content: lines.flatMap((line, i) => [...i ? [{ type: "break" }] : [], ...line ? [{ type: "text", text: line, marks: ["code"] }] : []]) }];
87
+ }
88
+ case "horizontalRule":
89
+ warn("a horizontal rule is left out");
90
+ return [];
91
+ case "bulletList":
92
+ case "orderedList":
93
+ case "taskList":
94
+ return [listOf(node)];
95
+ case "table": {
96
+ const rows = node.content ?? [];
97
+ const header = rows.length > 0 && (rows[0].content ?? []).length > 0 && (rows[0].content ?? []).every((c) => c.type === "tableHeader");
98
+ return [{ type: "table", ...header ? { header: true } : {}, rows: rows.map((r) => (r.content ?? []).map((c) => (c.content ?? []).flatMap(toBlocks))) }];
99
+ }
100
+ default:
101
+ warn(`a ${node.type} block is left out`);
102
+ return [];
103
+ }
104
+ };
105
+ const model = { ...title ? { title } : {}, blocks: blocks.flatMap(toBlocks) };
106
+ return { model, warnings: list() };
107
+ }
108
+ function fromTextDoc(model) {
109
+ const { warn, list } = warner();
110
+ const text = (value, marks = []) => value ? [{ type: "text", text: value, ...marks.length ? { marks } : {} }] : [];
111
+ const inlines = (nodes) => nodes.flatMap((node) => {
112
+ switch (node.type) {
113
+ case "break":
114
+ return [{ type: "hardBreak" }];
115
+ case "image":
116
+ if (!MODEL_IMAGE.test(node.mime)) {
117
+ warn("an image that is not a PNG, JPEG, or GIF is kept as its alt text");
118
+ return text(`[image${node.alt ? `: ${node.alt}` : ""}]`);
119
+ }
120
+ return [{ type: "image", attrs: { src: `data:${node.mime};base64,${toBase64(node.data)}`, alt: node.alt ?? null, title: null } }];
121
+ case "footnote":
122
+ warn("a footnote is kept as text in brackets");
123
+ return [...text(" ["), ...inlines(node.content), ...text("]")];
124
+ case "text": {
125
+ const marks = [];
126
+ for (const mark of node.marks ?? []) {
127
+ if (mark === "bold" || mark === "italic" || mark === "strike" || mark === "code") marks.push({ type: mark });
128
+ else warn(`the ${mark} mark is left out; the text stays`);
129
+ }
130
+ const href = node.href === void 0 ? void 0 : safeHref(node.href);
131
+ if (node.href !== void 0 && !href) warn("a link that is not a web, mail, or relative address is left out; the text stays");
132
+ if (href) marks.push({ type: "link", attrs: { href } });
133
+ if (marks.length > 1 && marks.some((m) => m.type === "code")) {
134
+ warn("a mark or a link on code text is left out; the text stays as code");
135
+ return text(node.text, [{ type: "code" }]);
136
+ }
137
+ return text(node.text, marks);
138
+ }
139
+ default:
140
+ return [];
141
+ }
142
+ });
143
+ const paragraph = (content) => ({ type: "paragraph", ...content.length ? { content: inlines(content) } : {} });
144
+ const toNodes = (block) => {
145
+ switch (block.type) {
146
+ case "heading":
147
+ return [{ type: "heading", attrs: { level: block.level }, content: inlines(block.content) }];
148
+ case "paragraph":
149
+ return [paragraph(block.content)];
150
+ case "list":
151
+ return [
152
+ {
153
+ type: block.ordered ? "orderedList" : "bulletList",
154
+ ...block.ordered ? { attrs: { start: 1, type: null } } : {},
155
+ content: block.items.map((item) => ({ type: "listItem", content: [paragraph(item.content), ...item.sublist ? toNodes(item.sublist) : []] }))
156
+ }
157
+ ];
158
+ case "table": {
159
+ const rows = block.rows.filter((r) => r.length > 0);
160
+ if (rows.length === 0) return [];
161
+ return [
162
+ {
163
+ type: "table",
164
+ content: rows.map((row, i) => ({
165
+ type: "tableRow",
166
+ content: row.map((cell) => {
167
+ const content = cell.flatMap(toNodes);
168
+ return { type: block.header && i === 0 ? "tableHeader" : "tableCell", attrs: { colspan: 1, rowspan: 1, colwidth: null }, content: content.length ? content : [{ type: "paragraph" }] };
169
+ })
170
+ }))
171
+ }
172
+ ];
173
+ }
174
+ default:
175
+ return [];
176
+ }
177
+ };
178
+ const prune = (node) => node.content ? { ...node, content: node.content.filter((n) => n.type !== "text" || n.text).map(prune) } : node;
179
+ const blocks = model.blocks.flatMap(toNodes).map(prune);
180
+ return { blocks, ...model.title ? { title: model.title } : {}, warnings: list() };
181
+ }
182
+ var formatFailure = (error) => {
183
+ if (error instanceof FormatError) throw new DocError("INVALID_INPUT", `the file was refused (${error.code}): ${error.message}`);
184
+ throw error;
185
+ };
186
+ function importFile(input, format, author, options = {}) {
187
+ checkAuthor(author);
188
+ if (!IMPORT_FORMATS.includes(format)) throw new DocError("INVALID_INPUT", `import format must be one of ${IMPORT_FORMATS.join(", ")}`);
189
+ const maxBytes = options.limits?.maxInputBytes ?? DEFAULT_LIMITS.maxInputBytes;
190
+ if (format === "md") {
191
+ let markdown;
192
+ try {
193
+ markdown = typeof input === "string" ? input : new TextDecoder("utf-8", { fatal: true }).decode(input);
194
+ } catch {
195
+ throw new DocError("INVALID_INPUT", "a md file must be UTF-8 text");
196
+ }
197
+ if (new TextEncoder().encode(markdown).length > maxBytes) throw new DocError("INVALID_INPUT", `the file is larger than ${maxBytes} bytes`);
198
+ return { doc: AgenticDoc.fromMarkdown(markdown, author, options.doc), warnings: [] };
199
+ }
200
+ if (!(input instanceof Uint8Array)) throw new DocError("INVALID_INPUT", `a ${format} file must be bytes, not text`);
201
+ let read;
202
+ try {
203
+ read = format === "odt" ? readOdt(input, options.limits) : readDocx(input, options.limits);
204
+ } catch (error) {
205
+ return formatFailure(error);
206
+ }
207
+ const mapped = fromTextDoc(read.model);
208
+ return {
209
+ doc: AgenticDoc.fromBlocks(mapped.blocks, author, options.doc),
210
+ ...mapped.title ? { title: mapped.title } : {},
211
+ warnings: [...read.warnings, ...mapped.warnings]
212
+ };
213
+ }
214
+ function exportFile(doc, format, options = {}) {
215
+ if (!FILE_FORMATS.includes(format)) throw new DocError("INVALID_INPUT", `export format must be one of ${FILE_FORMATS.join(", ")}`);
216
+ if (format === "md" || format === "html") return { format, contentType: CONTENT_TYPES[format], data: exportDoc(doc, format, options).data, warnings: [] };
217
+ const { model, warnings } = toTextDoc(doc.toJSON().content ?? [], options.title);
218
+ let written;
219
+ try {
220
+ written = format === "odt" ? writeOdt(model) : writeDocx(model);
221
+ } catch (error) {
222
+ return formatFailure(error);
223
+ }
224
+ return { format, contentType: CONTENT_TYPES[format], data: written.bytes, warnings: [...warnings, ...written.warnings] };
225
+ }
226
+ export {
227
+ DOCX_MIME,
228
+ FILE_FORMATS,
229
+ IMPORT_FORMATS,
230
+ exportFile,
231
+ fromTextDoc,
232
+ importFile,
233
+ toTextDoc
234
+ };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
- import { a as Principal, b as PmNode } from './doc-C_8kCsl7.js';
2
- export { c as AgenticDoc, A as Anchor, d as AnchorTarget, D as DocOptions, E as EditEntry, e as PRINCIPAL_KINDS, P as PrincipalKind, S as Section, f as SectionTarget, T as Target, g as TextTarget, h as blocksToMarkdown, m as markdownToBlocks, p as parseAuthor } from './doc-C_8kCsl7.js';
1
+ import { c as Principal, b as PmNode } from './doc-mJPajXEQ.js';
2
+ export { a as AgenticDoc, A as Anchor, d as AnchorTarget, D as DocOptions, E as EditEntry, e as PRINCIPAL_KINDS, P as PrincipalKind, S as Section, f as SectionTarget, T as Target, g as TextTarget, h as blocksToMarkdown, m as markdownToBlocks, p as parseAuthor } from './doc-mJPajXEQ.js';
3
3
  import { Schema } from 'prosemirror-model';
4
- export { a as DocError, D as DocErrorCode, b as EXPORT_FORMATS, E as ExportFormat, c as ExportOptions, d as ExportResult, M as Message, S as Suggestion, e as Suggestions, T as Thread, f as Threads, g as exportDoc } from './review-BQW2Ci2h.js';
4
+ export { a as DocError, D as DocErrorCode, M as Message, S as Suggestion, b as Suggestions, T as Thread, c as Threads } from './review-Hgq8OgvD.js';
5
5
  import { z } from 'zod';
6
- import { D as DocAccess } from './service-Do2Jwb5U.js';
7
- export { a as DocInfo, b as DocsPorts, c as DocsService } from './service-Do2Jwb5U.js';
6
+ import { a as DocAccess } from './service-CqKE9DKz.js';
7
+ export { b as DocInfo, c as DocsPorts, d as DocsService } from './service-CqKE9DKz.js';
8
+ export { b as EXPORT_FORMATS, E as ExportFormat, a as ExportOptions, c as ExportResult, e as exportDoc } from './export-Cwx276UP.js';
8
9
  import { AdapterOptions, Blobs, Store } from 'agentic-identity';
9
10
  import 'yjs';
10
11
 
package/dist/index.js CHANGED
@@ -1,28 +1,32 @@
1
1
  import {
2
2
  createFileBlobs,
3
3
  createFileStore
4
- } from "./chunk-TFCRPEEM.js";
4
+ } from "./chunk-7R7ZPPRF.js";
5
5
  import {
6
6
  DocsService
7
- } from "./chunk-FZA5LQV3.js";
7
+ } from "./chunk-YNHZCIGO.js";
8
+ import {
9
+ commands
10
+ } from "./chunk-S6MQDWFK.js";
8
11
  import {
9
12
  EXPORT_FORMATS,
10
13
  ODT_MIME,
11
- commands,
12
14
  exportDoc,
13
15
  exportOdt
14
- } from "./chunk-GYQFTF2E.js";
16
+ } from "./chunk-L6W2AQUF.js";
17
+ import {
18
+ Suggestions,
19
+ Threads
20
+ } from "./chunk-UPEGODUV.js";
15
21
  import {
16
22
  AgenticDoc,
17
23
  DocError,
18
24
  PRINCIPAL_KINDS,
19
- Suggestions,
20
- Threads,
21
25
  blocksToMarkdown,
22
26
  markdownToBlocks,
23
27
  parseAuthor,
24
28
  schema
25
- } from "./chunk-YBBQNCMC.js";
29
+ } from "./chunk-6QOYS63L.js";
26
30
  export {
27
31
  AgenticDoc,
28
32
  DocError,