agentic-documents 0.0.0-stage → 0.2.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.
- package/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +237 -2
- package/SECURITY.md +23 -0
- package/SKILL.md +35 -0
- package/dist/chunk-5Y66UCTE.js +7 -0
- package/dist/chunk-FZA5LQV3.js +150 -0
- package/dist/chunk-GYQFTF2E.js +389 -0
- package/dist/chunk-IU3YIQ4Y.js +232 -0
- package/dist/chunk-R5XUTBC3.js +177 -0
- package/dist/chunk-TFCRPEEM.js +224 -0
- package/dist/chunk-YBBQNCMC.js +744 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +165 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.js +44 -0
- package/dist/live-worker.d.ts +20 -0
- package/dist/live-worker.js +27 -0
- package/dist/live.d.ts +73 -0
- package/dist/live.js +12 -0
- package/dist/mcp-P2C75IIG.js +40 -0
- package/dist/service-BKkxEnaJ.d.ts +181 -0
- package/dist/worker.d.ts +50 -0
- package/dist/worker.js +15 -0
- package/llms.txt +15 -0
- package/package.json +104 -4
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DocError,
|
|
3
|
+
Suggestions,
|
|
4
|
+
Threads
|
|
5
|
+
} from "./chunk-YBBQNCMC.js";
|
|
6
|
+
|
|
7
|
+
// src/odt.ts
|
|
8
|
+
import { strToU8, zipSync } from "fflate";
|
|
9
|
+
|
|
10
|
+
// src/package.ts
|
|
11
|
+
var IMAGE_TYPES = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp" };
|
|
12
|
+
var DATA_IMAGE = /^data:(image\/(?:png|jpeg|gif|webp));base64,([a-z0-9+/=]+)$/i;
|
|
13
|
+
var FIXED_DATE = new Date(2e3, 0, 1);
|
|
14
|
+
function toBase64(bytes) {
|
|
15
|
+
let text2 = "";
|
|
16
|
+
for (let i = 0; i < bytes.length; i += 32768) text2 += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
17
|
+
return btoa(text2);
|
|
18
|
+
}
|
|
19
|
+
var fromBase64 = (value) => Uint8Array.from(atob(value), (c) => c.charCodeAt(0));
|
|
20
|
+
function imageSize(b) {
|
|
21
|
+
const v = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
22
|
+
if (b.length > 24 && v.getUint32(0) === 2303741511) return [v.getUint32(16), v.getUint32(20)];
|
|
23
|
+
if (b.length > 10 && v.getUint32(0) === 1195984440) return [v.getUint16(6, true), v.getUint16(8, true)];
|
|
24
|
+
if (b.length > 4 && v.getUint16(0) === 65496) {
|
|
25
|
+
for (let i = 2; i + 9 < b.length && b[i] === 255; i += 2 + v.getUint16(i + 2)) {
|
|
26
|
+
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)];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return void 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/xml.ts
|
|
33
|
+
var xmlEscape = (value) => String(value ?? "").replace(/[^\u0009\u000A\u000D --�\u{10000}-\u{10FFFF}]/gu, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
34
|
+
var safeHref = (value) => {
|
|
35
|
+
const href = String(value ?? "").trim();
|
|
36
|
+
return /^(https?:|mailto:|#|[^:]*$)/i.test(href) ? href : void 0;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/odt.ts
|
|
40
|
+
var ODT_MIME = "application/vnd.oasis.opendocument.text";
|
|
41
|
+
var NS = 'xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" office:version="1.3"';
|
|
42
|
+
var XML = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
43
|
+
var SPANS = { bold: "B", italic: "I", strike: "S", code: "Source_20_Text" };
|
|
44
|
+
var TASK = ["\u2610 ", "\u2611 "];
|
|
45
|
+
var LEVELS = (kind) => Array.from(
|
|
46
|
+
{ length: 10 },
|
|
47
|
+
(_, 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>`
|
|
48
|
+
).join("");
|
|
49
|
+
var SIZES = [24, 18, 14, 12, 11, 10];
|
|
50
|
+
var STYLES = `${XML}<office:document-styles ${NS}><office:styles>
|
|
51
|
+
<style:style style:name="Standard" style:family="paragraph" style:class="text"/>
|
|
52
|
+
${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")}
|
|
53
|
+
<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>
|
|
54
|
+
<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>
|
|
55
|
+
<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>
|
|
56
|
+
<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>
|
|
57
|
+
</office:styles></office:document-styles>`;
|
|
58
|
+
var AUTO = `<style:style style:name="B" style:family="text"><style:text-properties fo:font-weight="bold"/></style:style>
|
|
59
|
+
<style:style style:name="I" style:family="text"><style:text-properties fo:font-style="italic"/></style:style>
|
|
60
|
+
<style:style style:name="S" style:family="text"><style:text-properties style:text-line-through-style="solid"/></style:style>
|
|
61
|
+
<text:list-style style:name="Bullets">${LEVELS("bullet")}</text:list-style>
|
|
62
|
+
<text:list-style style:name="Numbering">${LEVELS("number")}</text:list-style>`;
|
|
63
|
+
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);
|
|
64
|
+
function image(node, out) {
|
|
65
|
+
const alt = String(node.attrs?.alt ?? "");
|
|
66
|
+
const found = DATA_IMAGE.exec(String(node.attrs?.src ?? "").trim());
|
|
67
|
+
let data;
|
|
68
|
+
try {
|
|
69
|
+
data = found ? fromBase64(found[2]) : void 0;
|
|
70
|
+
} catch {
|
|
71
|
+
data = void 0;
|
|
72
|
+
}
|
|
73
|
+
if (!found || !data) return spaced(`[image${alt ? `: ${alt}` : ""}]`);
|
|
74
|
+
const ext = Object.keys(IMAGE_TYPES).find((k) => IMAGE_TYPES[k] === found[1].toLowerCase());
|
|
75
|
+
const name = `Image${out.images.length + 1}`;
|
|
76
|
+
out.images.push({ name: `Pictures/${name}.${ext}`, type: found[1].toLowerCase(), data });
|
|
77
|
+
const [w, h] = imageSize(data) ?? [320, 240];
|
|
78
|
+
const scale = Math.min(1, 16 / (w * 2.54 / 96));
|
|
79
|
+
const cm = (px) => (px * 2.54 * scale / 96).toFixed(2);
|
|
80
|
+
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>`;
|
|
81
|
+
}
|
|
82
|
+
function inline(nodes, out) {
|
|
83
|
+
return nodes.map((node) => {
|
|
84
|
+
if (node.type === "hardBreak") return "<text:line-break/>";
|
|
85
|
+
if (node.type === "image") return image(node, out);
|
|
86
|
+
return (node.marks ?? []).reduce((xml, mark) => {
|
|
87
|
+
if (mark.type === "link") {
|
|
88
|
+
const href = safeHref(mark.attrs?.href);
|
|
89
|
+
return href ? `<text:a xlink:type="simple" xlink:href="${xmlEscape(href)}">${xml}</text:a>` : xml;
|
|
90
|
+
}
|
|
91
|
+
return SPANS[mark.type] ? `<text:span text:style-name="${SPANS[mark.type]}">${xml}</text:span>` : xml;
|
|
92
|
+
}, spaced(node.text ?? ""));
|
|
93
|
+
}).join("");
|
|
94
|
+
}
|
|
95
|
+
function block(node, out, style = "Standard") {
|
|
96
|
+
const kids = node.content ?? [];
|
|
97
|
+
const p = (name, inner) => `<text:p text:style-name="${name}">${inner}</text:p>`;
|
|
98
|
+
switch (node.type) {
|
|
99
|
+
case "paragraph":
|
|
100
|
+
return p(style, inline(kids, out));
|
|
101
|
+
case "heading": {
|
|
102
|
+
const level = Math.min(6, Math.max(1, Math.trunc(Number(node.attrs?.level)) || 1));
|
|
103
|
+
return `<text:h text:style-name="Heading_20_${level}" text:outline-level="${level}">${inline(kids, out)}</text:h>`;
|
|
104
|
+
}
|
|
105
|
+
case "blockquote":
|
|
106
|
+
return kids.map((k) => block(k, out, "Quotations")).join("");
|
|
107
|
+
case "codeBlock":
|
|
108
|
+
return p("Preformatted_20_Text", spaced(kids.map((k) => k.text ?? "").join("")));
|
|
109
|
+
case "horizontalRule":
|
|
110
|
+
return `<text:p text:style-name="Horizontal_20_Line"/>`;
|
|
111
|
+
case "bulletList":
|
|
112
|
+
case "orderedList":
|
|
113
|
+
case "taskList": {
|
|
114
|
+
const ordered = node.type === "orderedList";
|
|
115
|
+
const start = Number(node.attrs?.start ?? 1);
|
|
116
|
+
const items = kids.map((item, i) => {
|
|
117
|
+
const [first, ...rest] = item.content ?? [];
|
|
118
|
+
const mark = item.type === "taskItem" && first ? { ...first, content: [{ type: "text", text: TASK[item.attrs?.checked ? 1 : 0] }, ...first.content ?? []] } : first;
|
|
119
|
+
const attr = ordered && i === 0 && start !== 1 ? ` text:start-value="${Math.trunc(start) || 1}"` : "";
|
|
120
|
+
return `<text:list-item${attr}>${[mark, ...rest].filter(Boolean).map((k) => block(k, out)).join("")}</text:list-item>`;
|
|
121
|
+
});
|
|
122
|
+
return `<text:list text:style-name="${ordered ? "Numbering" : "Bullets"}">${items.join("")}</text:list>`;
|
|
123
|
+
}
|
|
124
|
+
case "table": {
|
|
125
|
+
const cols = Math.max(0, ...kids.map((r) => r.content?.length ?? 0));
|
|
126
|
+
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>`;
|
|
127
|
+
const header = kids[0]?.content?.length && kids[0].content.every((c) => c.type === "tableHeader");
|
|
128
|
+
const rows = header ? `<table:table-header-rows>${row(kids[0])}</table:table-header-rows>${kids.slice(1).map(row).join("")}` : kids.map(row).join("");
|
|
129
|
+
return `<table:table table:name="Table${++out.tables}">${cols ? `<table:table-column table:number-columns-repeated="${cols}"/>` : ""}${rows}</table:table>`;
|
|
130
|
+
}
|
|
131
|
+
default:
|
|
132
|
+
return "";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function exportOdt(blocks) {
|
|
136
|
+
const out = { images: [], tables: 0 };
|
|
137
|
+
const body = blocks.map((b) => block(b, out)).join("\n");
|
|
138
|
+
const content2 = `${XML}<office:document-content ${NS}><office:automatic-styles>${AUTO}</office:automatic-styles><office:body><office:text>${body}</office:text></office:body></office:document-content>`;
|
|
139
|
+
const entries = [
|
|
140
|
+
`<manifest:file-entry manifest:full-path="/" manifest:version="1.3" manifest:media-type="${ODT_MIME}"/>`,
|
|
141
|
+
...["content.xml", "styles.xml", "meta.xml"].map((n) => `<manifest:file-entry manifest:full-path="${n}" manifest:media-type="text/xml"/>`),
|
|
142
|
+
...out.images.map((i) => `<manifest:file-entry manifest:full-path="${i.name}" manifest:media-type="${i.type}"/>`)
|
|
143
|
+
];
|
|
144
|
+
const meta = `${XML}<office:document-meta ${NS}><office:meta><meta:generator>agentic-documents</meta:generator></office:meta></office:document-meta>`;
|
|
145
|
+
const part = (text2) => [strToU8(text2), { mtime: FIXED_DATE }];
|
|
146
|
+
return zipSync({
|
|
147
|
+
mimetype: [strToU8(ODT_MIME), { level: 0, mtime: FIXED_DATE }],
|
|
148
|
+
// first and stored, so file sniffers can read it
|
|
149
|
+
"META-INF/manifest.xml": part(`${XML}<manifest:manifest ${NS}>${entries.join("")}</manifest:manifest>`),
|
|
150
|
+
"content.xml": part(content2),
|
|
151
|
+
"styles.xml": part(STYLES),
|
|
152
|
+
"meta.xml": part(meta),
|
|
153
|
+
...Object.fromEntries(out.images.map((i) => [i.name, [i.data, { mtime: FIXED_DATE }]]))
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/export.ts
|
|
158
|
+
var EXPORT_FORMATS = ["md", "html", "odt"];
|
|
159
|
+
var esc = (s) => String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
160
|
+
var url = (value) => /^(https?:|mailto:|[^:]*$)/i.test(String(value ?? "").trim()) ? esc(String(value).trim()) : "#";
|
|
161
|
+
var integer = (value, fallback) => Number.isFinite(Number(value)) ? Math.trunc(Number(value)) : fallback;
|
|
162
|
+
var INLINE_IMAGE = /^data:image\/(png|jpeg|gif|webp);base64,[a-z0-9+/=]+$/i;
|
|
163
|
+
var textOf = (node) => node.text ?? (node.content ?? []).map(textOf).join("");
|
|
164
|
+
var MARK_TAGS = { bold: "strong", italic: "em", strike: "s", code: "code" };
|
|
165
|
+
function image2(node, remote) {
|
|
166
|
+
const src = String(node.attrs?.src ?? "").trim();
|
|
167
|
+
const alt = esc(node.attrs?.alt);
|
|
168
|
+
if (INLINE_IMAGE.test(src)) return `<img src="${src}" alt="${alt}">`;
|
|
169
|
+
if (remote && url(src) !== "#") return `<img src="${url(src)}" alt="${alt}">`;
|
|
170
|
+
return `<span class="image">[image${alt ? `: ${alt}` : ""}]</span>`;
|
|
171
|
+
}
|
|
172
|
+
function inline2(node, remote) {
|
|
173
|
+
if (node.type === "hardBreak") return "<br>";
|
|
174
|
+
if (node.type === "image") return image2(node, remote);
|
|
175
|
+
return (node.marks ?? []).reduce((html, mark) => {
|
|
176
|
+
if (mark.type === "link") return `<a href="${url(mark.attrs?.href)}">${html}</a>`;
|
|
177
|
+
const tag = MARK_TAGS[mark.type];
|
|
178
|
+
return tag ? `<${tag}>${html}</${tag}>` : html;
|
|
179
|
+
}, esc(node.text));
|
|
180
|
+
}
|
|
181
|
+
function block2(node, remote) {
|
|
182
|
+
const kids = (node.content ?? []).map((n) => block2(n, remote)).join("");
|
|
183
|
+
const text2 = (node.content ?? []).map((n) => inline2(n, remote)).join("");
|
|
184
|
+
switch (node.type) {
|
|
185
|
+
case "paragraph":
|
|
186
|
+
return `<p>${text2}</p>
|
|
187
|
+
`;
|
|
188
|
+
case "heading": {
|
|
189
|
+
const level = Math.min(6, Math.max(1, integer(node.attrs?.level, 1)));
|
|
190
|
+
return `<h${level}>${text2}</h${level}>
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
193
|
+
case "blockquote":
|
|
194
|
+
return `<blockquote>
|
|
195
|
+
${kids}</blockquote>
|
|
196
|
+
`;
|
|
197
|
+
case "codeBlock": {
|
|
198
|
+
const lang = node.attrs?.language ? ` class="language-${esc(node.attrs.language)}"` : "";
|
|
199
|
+
return `<pre><code${lang}>${esc(textOf(node))}</code></pre>
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
case "horizontalRule":
|
|
203
|
+
return "<hr>\n";
|
|
204
|
+
case "bulletList":
|
|
205
|
+
case "taskList":
|
|
206
|
+
return `<ul>
|
|
207
|
+
${kids}</ul>
|
|
208
|
+
`;
|
|
209
|
+
case "orderedList":
|
|
210
|
+
return `<ol start="${integer(node.attrs?.start, 1)}">
|
|
211
|
+
${kids}</ol>
|
|
212
|
+
`;
|
|
213
|
+
case "listItem":
|
|
214
|
+
return `<li>${kids}</li>
|
|
215
|
+
`;
|
|
216
|
+
case "taskItem":
|
|
217
|
+
return `<li><input type="checkbox" disabled${node.attrs?.checked ? " checked" : ""}> ${kids}</li>
|
|
218
|
+
`;
|
|
219
|
+
case "table":
|
|
220
|
+
return `<table>
|
|
221
|
+
${kids}</table>
|
|
222
|
+
`;
|
|
223
|
+
case "tableRow":
|
|
224
|
+
return `<tr>${kids}</tr>
|
|
225
|
+
`;
|
|
226
|
+
case "tableHeader":
|
|
227
|
+
return `<th>${kids}</th>`;
|
|
228
|
+
case "tableCell":
|
|
229
|
+
return `<td>${kids}</td>`;
|
|
230
|
+
default:
|
|
231
|
+
return "";
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function toHtml(blocks, remote) {
|
|
235
|
+
const title = textOf(blocks.find((b) => b.type === "heading") ?? { type: "doc" }) || "Document";
|
|
236
|
+
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}";
|
|
237
|
+
return `<!doctype html>
|
|
238
|
+
<html lang="en">
|
|
239
|
+
<head>
|
|
240
|
+
<meta charset="utf-8">
|
|
241
|
+
<title>${esc(title)}</title>
|
|
242
|
+
<style>${css}</style>
|
|
243
|
+
</head>
|
|
244
|
+
<body>
|
|
245
|
+
${blocks.map((b) => block2(b, remote)).join("")}</body>
|
|
246
|
+
</html>
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
function exportDoc(doc, format, options = {}) {
|
|
250
|
+
if (format === "html") {
|
|
251
|
+
return { format, contentType: "text/html; charset=utf-8", encoding: "utf8", data: toHtml(doc.toJSON().content ?? [], options.remoteImages === true) };
|
|
252
|
+
}
|
|
253
|
+
if (format === "odt") return { format, contentType: ODT_MIME, encoding: "base64", data: toBase64(exportOdt(doc.toJSON().content ?? [])) };
|
|
254
|
+
return { format, contentType: "text/markdown; charset=utf-8", encoding: "utf8", data: doc.toMarkdown() };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/commands.ts
|
|
258
|
+
import { z } from "zod";
|
|
259
|
+
var id = z.string().describe("Document id.");
|
|
260
|
+
var content = z.string().describe("Markdown for a section target. Plain text for a text target.");
|
|
261
|
+
var text = z.string().describe("Exact text to match. It must sit inside one block.");
|
|
262
|
+
var occurrence = z.coerce.number().int().min(1).optional().describe("Which match, counting from 1, when the text matches more than once.");
|
|
263
|
+
var side = z.enum(["before", "after"]).optional().describe("Put the new text before or after the match.");
|
|
264
|
+
var thread = z.string().describe("Thread id.");
|
|
265
|
+
var where = {
|
|
266
|
+
section: z.string().optional().describe("Section id from the outline. Use this or text."),
|
|
267
|
+
text: text.optional(),
|
|
268
|
+
occurrence,
|
|
269
|
+
at: z.enum(["start", "end", "before", "after"]).optional().describe("Insert at the start or end of a section, or before or after matched text.")
|
|
270
|
+
};
|
|
271
|
+
function target(input) {
|
|
272
|
+
const { section, text: text2, occurrence: occurrence2, at } = input;
|
|
273
|
+
if (section !== void 0) {
|
|
274
|
+
if (at !== void 0 && at !== "start" && at !== "end") throw new DocError("INVALID_INPUT", `at "${at}" does not fit a section; use start or end`);
|
|
275
|
+
return { section, at };
|
|
276
|
+
}
|
|
277
|
+
if (text2 !== void 0) {
|
|
278
|
+
if (at === "start" || at === "end") throw new DocError("INVALID_INPUT", `at "${at}" does not fit text; use before or after`);
|
|
279
|
+
return { text: text2, occurrence: occurrence2, at };
|
|
280
|
+
}
|
|
281
|
+
throw new DocError("INVALID_INPUT", "pass section or text");
|
|
282
|
+
}
|
|
283
|
+
var textTarget = (input) => ({
|
|
284
|
+
text: input.text,
|
|
285
|
+
occurrence: input.occurrence,
|
|
286
|
+
at: input.at
|
|
287
|
+
});
|
|
288
|
+
function command(name, description, readOnly, shape, run) {
|
|
289
|
+
return {
|
|
290
|
+
name,
|
|
291
|
+
description,
|
|
292
|
+
shape,
|
|
293
|
+
readOnly,
|
|
294
|
+
async run(service, raw, author) {
|
|
295
|
+
const parsed = z.object(shape).strict().safeParse(raw);
|
|
296
|
+
if (!parsed.success) {
|
|
297
|
+
throw new DocError("INVALID_INPUT", parsed.error.issues.map((i) => `${i.path.join(".") || "input"}: ${i.message}`).join("; "));
|
|
298
|
+
}
|
|
299
|
+
return run(service, parsed.data, author);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
var ok = () => ({ ok: true });
|
|
304
|
+
var commands = [
|
|
305
|
+
command("new", "Create a document, from Markdown if given.", false, { id, markdown: z.string().optional() }, async (s, i, a) => {
|
|
306
|
+
await s.create(i.id, i.markdown ?? "", a);
|
|
307
|
+
return { id: i.id };
|
|
308
|
+
}),
|
|
309
|
+
command(
|
|
310
|
+
"read",
|
|
311
|
+
"Read a document, or one section, as Markdown.",
|
|
312
|
+
true,
|
|
313
|
+
{ id, section: where.section },
|
|
314
|
+
(s, i) => s.read(i.id, (d) => i.section ? d.readSection(i.section) : d.toMarkdown())
|
|
315
|
+
),
|
|
316
|
+
command("outline", "List sections with their ids, levels, and titles.", true, { id }, (s, i) => s.read(i.id, (d) => d.outline())),
|
|
317
|
+
command("edits", "Read the edit log. It names each author kind and holds no content.", true, { id }, (s, i) => s.read(i.id, (d) => d.edits())),
|
|
318
|
+
command("insert", "Insert Markdown into a section, or plain text next to matched text.", false, { id, ...where, content }, async (s, i, a) => {
|
|
319
|
+
await s.write(i.id, (d) => d.insert(target(i), i.content, a));
|
|
320
|
+
return ok();
|
|
321
|
+
}),
|
|
322
|
+
command("replace", "Replace a section body (the heading stays), or the matched text.", false, { id, ...where, content }, async (s, i, a) => {
|
|
323
|
+
await s.write(i.id, (d) => d.replace(target(i), i.content, a));
|
|
324
|
+
return ok();
|
|
325
|
+
}),
|
|
326
|
+
command("delete", "Delete a section with its subsections, or the matched text.", false, { id, ...where }, async (s, i, a) => {
|
|
327
|
+
await s.write(i.id, (d) => d.delete(target(i), a));
|
|
328
|
+
return ok();
|
|
329
|
+
}),
|
|
330
|
+
command("append", "Add Markdown at the end of the document.", false, { id, content: z.string() }, async (s, i, a) => {
|
|
331
|
+
await s.write(i.id, (d) => d.append(i.content, a));
|
|
332
|
+
return ok();
|
|
333
|
+
}),
|
|
334
|
+
command("suggest", "Suggest an edit to matched text. An empty replacement suggests a delete.", false, { id, text, occurrence, at: side, replacement: z.string() }, async (s, i, a) => ({
|
|
335
|
+
suggestion: await s.write(i.id, (d) => new Suggestions(d).add(textTarget(i), i.replacement, a))
|
|
336
|
+
})),
|
|
337
|
+
command(
|
|
338
|
+
"suggestions",
|
|
339
|
+
"List suggested edits.",
|
|
340
|
+
true,
|
|
341
|
+
{ id, status: z.enum(["pending", "accepted", "rejected"]).optional() },
|
|
342
|
+
(s, i) => s.read(i.id, (d) => new Suggestions(d).list({ status: i.status }))
|
|
343
|
+
),
|
|
344
|
+
command("accept", "Accept a suggestion. The text changes now.", false, { id, suggestion: z.string() }, async (s, i, a) => {
|
|
345
|
+
await s.write(i.id, (d) => new Suggestions(d).accept(i.suggestion, a));
|
|
346
|
+
return ok();
|
|
347
|
+
}),
|
|
348
|
+
command("reject", "Reject a suggestion. The text stays.", false, { id, suggestion: z.string() }, async (s, i, a) => {
|
|
349
|
+
await s.write(i.id, (d) => new Suggestions(d).reject(i.suggestion, a));
|
|
350
|
+
return ok();
|
|
351
|
+
}),
|
|
352
|
+
command("comment", "Start a comment thread on matched text.", false, { id, text, occurrence, body: z.string() }, async (s, i, a) => ({
|
|
353
|
+
thread: await s.write(i.id, (d) => new Threads(d).add(textTarget(i), i.body, a))
|
|
354
|
+
})),
|
|
355
|
+
command("reply", "Reply in a comment thread.", false, { id, thread, body: z.string() }, async (s, i, a) => {
|
|
356
|
+
await s.write(i.id, (d) => new Threads(d).reply(i.thread, i.body, a));
|
|
357
|
+
return ok();
|
|
358
|
+
}),
|
|
359
|
+
command("resolve", "Resolve a comment thread.", false, { id, thread }, async (s, i, a) => {
|
|
360
|
+
await s.write(i.id, (d) => new Threads(d).resolve(i.thread, a));
|
|
361
|
+
return ok();
|
|
362
|
+
}),
|
|
363
|
+
command("reopen", "Reopen a comment thread.", false, { id, thread }, async (s, i, a) => {
|
|
364
|
+
await s.write(i.id, (d) => new Threads(d).reopen(i.thread, a));
|
|
365
|
+
return ok();
|
|
366
|
+
}),
|
|
367
|
+
command(
|
|
368
|
+
"threads",
|
|
369
|
+
"List comment threads with their messages.",
|
|
370
|
+
true,
|
|
371
|
+
{ id, status: z.enum(["open", "resolved"]).optional() },
|
|
372
|
+
(s, i) => s.read(i.id, (d) => new Threads(d).list({ status: i.status }))
|
|
373
|
+
),
|
|
374
|
+
command(
|
|
375
|
+
"export",
|
|
376
|
+
"Export the document as Markdown (md), self-contained HTML (html), or OpenDocument text (odt, base64). HTML shows no remote image unless remoteImages is true.",
|
|
377
|
+
true,
|
|
378
|
+
{ id, format: z.enum(EXPORT_FORMATS), remoteImages: z.union([z.boolean(), z.enum(["true", "false"])]).optional() },
|
|
379
|
+
(s, i) => s.read(i.id, (d) => exportDoc(d, i.format, { remoteImages: i.remoteImages === true || i.remoteImages === "true" }))
|
|
380
|
+
)
|
|
381
|
+
];
|
|
382
|
+
|
|
383
|
+
export {
|
|
384
|
+
ODT_MIME,
|
|
385
|
+
exportOdt,
|
|
386
|
+
EXPORT_FORMATS,
|
|
387
|
+
exportDoc,
|
|
388
|
+
commands
|
|
389
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DocsService,
|
|
3
|
+
MAX_LIVE_BYTES
|
|
4
|
+
} from "./chunk-FZA5LQV3.js";
|
|
5
|
+
import {
|
|
6
|
+
commands
|
|
7
|
+
} from "./chunk-GYQFTF2E.js";
|
|
8
|
+
import {
|
|
9
|
+
DocError,
|
|
10
|
+
PRINCIPAL_KINDS,
|
|
11
|
+
parseAuthor
|
|
12
|
+
} from "./chunk-YBBQNCMC.js";
|
|
13
|
+
|
|
14
|
+
// src/worker.ts
|
|
15
|
+
import { Hono } from "hono";
|
|
16
|
+
import { assertRegion, parseRegion } from "agentic-identity";
|
|
17
|
+
import { D1_SCHEMA, createD1Store, createR2Blobs } from "agentic-identity/cloudflare";
|
|
18
|
+
var STATUS = {
|
|
19
|
+
NO_MATCH: 404,
|
|
20
|
+
NO_SECTION: 404,
|
|
21
|
+
NO_THREAD: 404,
|
|
22
|
+
NO_SUGGESTION: 404,
|
|
23
|
+
NO_DOCUMENT: 404,
|
|
24
|
+
AMBIGUOUS_MATCH: 409,
|
|
25
|
+
STALE_ANCHOR: 409,
|
|
26
|
+
ALREADY_DECIDED: 409,
|
|
27
|
+
DOCUMENT_EXISTS: 409,
|
|
28
|
+
CONFLICT: 409,
|
|
29
|
+
FORBIDDEN: 403,
|
|
30
|
+
LOCKED: 503,
|
|
31
|
+
MISCONFIGURED: 500
|
|
32
|
+
};
|
|
33
|
+
function same(a, b) {
|
|
34
|
+
let diff = a.length ^ b.length;
|
|
35
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
|
|
36
|
+
return diff === 0;
|
|
37
|
+
}
|
|
38
|
+
var MIN_TOKEN_BYTES = 32;
|
|
39
|
+
var warned = /* @__PURE__ */ new Set();
|
|
40
|
+
function refuse(reason) {
|
|
41
|
+
if (!warned.has(reason)) {
|
|
42
|
+
warned.add(reason);
|
|
43
|
+
console.error(`AGENTIC_DOCS_TOKENS is refused: ${reason}`);
|
|
44
|
+
}
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
function identityOf(entry) {
|
|
48
|
+
const object = typeof entry === "object" && entry !== null && !Array.isArray(entry) ? entry : void 0;
|
|
49
|
+
if (object?.role !== void 0 && object.role !== "editor" && object.role !== "viewer") return void 0;
|
|
50
|
+
const role = object?.role ?? "editor";
|
|
51
|
+
const principal = principalOf(entry, object);
|
|
52
|
+
return principal && { principal, role };
|
|
53
|
+
}
|
|
54
|
+
function principalOf(entry, object) {
|
|
55
|
+
const who = typeof entry === "string" ? entry : object?.as;
|
|
56
|
+
const author = typeof who === "string" ? parseAuthor(who) : void 0;
|
|
57
|
+
if (!author || !PRINCIPAL_KINDS.includes(author.kind) || !author.name) return void 0;
|
|
58
|
+
if (object?.scopes === void 0) return author;
|
|
59
|
+
const scopes = object.scopes;
|
|
60
|
+
return Array.isArray(scopes) && scopes.every((s) => typeof s === "string" && s !== "") ? { ...author, scopes } : void 0;
|
|
61
|
+
}
|
|
62
|
+
function readTokens(raw) {
|
|
63
|
+
let table;
|
|
64
|
+
try {
|
|
65
|
+
table = JSON.parse(raw ?? "{}");
|
|
66
|
+
} catch {
|
|
67
|
+
return refuse("not valid JSON");
|
|
68
|
+
}
|
|
69
|
+
if (typeof table !== "object" || table === null || Array.isArray(table)) return refuse("not a JSON object");
|
|
70
|
+
const found = /* @__PURE__ */ new Map();
|
|
71
|
+
for (const [token, entry] of Object.entries(table)) {
|
|
72
|
+
if (new TextEncoder().encode(token).length < MIN_TOKEN_BYTES) return refuse(`a token is shorter than ${MIN_TOKEN_BYTES} bytes`);
|
|
73
|
+
const identity = identityOf(entry);
|
|
74
|
+
if (!identity) return refuse("an entry is not a valid kind:name or { as, scopes, role }");
|
|
75
|
+
found.set(token, identity);
|
|
76
|
+
}
|
|
77
|
+
return found;
|
|
78
|
+
}
|
|
79
|
+
function identityFor(tokens, bearer) {
|
|
80
|
+
let found;
|
|
81
|
+
for (const [token, identity] of readTokens(tokens) ?? []) if (same(token, bearer)) found = identity;
|
|
82
|
+
return found;
|
|
83
|
+
}
|
|
84
|
+
var fail = (code, message, status, matches = []) => Response.json({ error: { code, message, matches } }, { status });
|
|
85
|
+
async function readCapped(request, cap) {
|
|
86
|
+
const tooBig = () => new DocError("INVALID_INPUT", `the update is over ${cap} bytes`);
|
|
87
|
+
if (Number(request.headers.get("content-length") ?? 0) > cap) throw tooBig();
|
|
88
|
+
const chunks = [];
|
|
89
|
+
let size = 0;
|
|
90
|
+
const reader = request.body?.getReader();
|
|
91
|
+
for (let part = await reader?.read(); part && !part.done; part = await reader?.read()) {
|
|
92
|
+
size += part.value.length;
|
|
93
|
+
if (size > cap) {
|
|
94
|
+
await reader?.cancel();
|
|
95
|
+
throw tooBig();
|
|
96
|
+
}
|
|
97
|
+
chunks.push(part.value);
|
|
98
|
+
}
|
|
99
|
+
const whole = new Uint8Array(size);
|
|
100
|
+
let at = 0;
|
|
101
|
+
for (const chunk of chunks) {
|
|
102
|
+
whole.set(chunk, at);
|
|
103
|
+
at += chunk.length;
|
|
104
|
+
}
|
|
105
|
+
return whole;
|
|
106
|
+
}
|
|
107
|
+
var DOC_ID = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
108
|
+
function createApp(source, options = {}) {
|
|
109
|
+
const app2 = new Hono();
|
|
110
|
+
app2.use("*", async (c, next) => {
|
|
111
|
+
await next();
|
|
112
|
+
c.header("x-content-type-options", "nosniff");
|
|
113
|
+
});
|
|
114
|
+
app2.get("/health", (c) => c.json({ ok: true, pack: "agentic-documents" }));
|
|
115
|
+
app2.use("/api/v1/*", async (c, next) => {
|
|
116
|
+
const bearer = /^Bearer (.+)$/.exec(c.req.header("authorization") ?? "")?.[1];
|
|
117
|
+
const identity = bearer === void 0 ? void 0 : identityFor(c.env?.AGENTIC_DOCS_TOKENS, bearer);
|
|
118
|
+
if (!identity) return fail("UNAUTHENTICATED", "send a valid bearer token", 401);
|
|
119
|
+
c.set("author", identity.principal);
|
|
120
|
+
c.set("role", identity.role);
|
|
121
|
+
await next();
|
|
122
|
+
});
|
|
123
|
+
app2.get("/api/v1/me", (c) => {
|
|
124
|
+
const { kind, name, scopes } = c.get("author");
|
|
125
|
+
return c.json({ kind, name, scopes: scopes ?? [], role: c.get("role") });
|
|
126
|
+
});
|
|
127
|
+
app2.get("/api/v1/docs/:id/room", async (c) => {
|
|
128
|
+
if (!options.live) return fail("NOT_FOUND", "this server hosts no live rooms", 404);
|
|
129
|
+
if (!DOC_ID.test(c.req.param("id"))) return fail("INVALID_INPUT", `invalid document id "${c.req.param("id")}"`, 400);
|
|
130
|
+
const { contributors } = await options.live(c.env ?? {}).meta(c.req.param("id"));
|
|
131
|
+
return c.json({ contributors: contributors.map(({ id, kind, machine }) => ({ name: id, kind, machine })) });
|
|
132
|
+
});
|
|
133
|
+
app2.get("/api/v1/docs/:id/ws", (c) => {
|
|
134
|
+
const id = c.req.param("id");
|
|
135
|
+
if (!options.live) return fail("NOT_FOUND", "this server hosts no live rooms", 404);
|
|
136
|
+
if (c.req.header("upgrade") !== "websocket") return fail("UPGRADE_REQUIRED", "open this route as a WebSocket", 426);
|
|
137
|
+
if (!DOC_ID.test(id)) return fail("INVALID_INPUT", `invalid document id "${id}"`, 400);
|
|
138
|
+
const { kind, name } = c.get("author");
|
|
139
|
+
return options.live(c.env ?? {}).connect(id, c.req.raw, { principal: { id: name, kind }, role: c.get("role"), name });
|
|
140
|
+
});
|
|
141
|
+
async function run(c, command, input) {
|
|
142
|
+
try {
|
|
143
|
+
if (!command.readOnly && c.get("role") === "viewer") throw new DocError("FORBIDDEN", "this token is a viewer; it cannot change a document");
|
|
144
|
+
const result = await command.run(await (typeof source === "function" ? source(c.env ?? {}) : source), input, c.get("author"));
|
|
145
|
+
if (command.name === "export") {
|
|
146
|
+
const { data, contentType, encoding, format } = result;
|
|
147
|
+
const images = input.remoteImages === true || input.remoteImages === "true" ? "data: https:" : "data:";
|
|
148
|
+
const disposition = format === "odt" ? { "content-disposition": `attachment; filename="${input.id}.odt"` } : {};
|
|
149
|
+
const headers = { ...disposition, "content-type": contentType, "content-security-policy": `default-src 'none'; style-src 'unsafe-inline'; img-src ${images}; sandbox` };
|
|
150
|
+
return encoding === "base64" ? c.body(Uint8Array.from(atob(data), (ch) => ch.charCodeAt(0)), 200, headers) : c.body(data, 200, headers);
|
|
151
|
+
}
|
|
152
|
+
if (typeof result === "string") return c.body(result, 200, { "content-type": "text/markdown; charset=utf-8" });
|
|
153
|
+
return c.json(result);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (!(error instanceof DocError)) throw error;
|
|
156
|
+
return fail(error.code, error.message, STATUS[error.code] ?? 400, error.matches);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function body(c) {
|
|
160
|
+
const parsed = await c.req.json().catch(() => void 0);
|
|
161
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
162
|
+
throw new DocError("INVALID_INPUT", "the body must be a JSON object");
|
|
163
|
+
}
|
|
164
|
+
return parsed;
|
|
165
|
+
}
|
|
166
|
+
const serviceOf = (c) => typeof source === "function" ? source(c.env ?? {}) : Promise.resolve(source);
|
|
167
|
+
app2.get("/api/v1/docs/:id/state", async (c) => {
|
|
168
|
+
const state = await (await serviceOf(c)).state(c.req.param("id"));
|
|
169
|
+
return c.body(new Uint8Array(state), 200, { "content-type": "application/octet-stream" });
|
|
170
|
+
});
|
|
171
|
+
app2.post("/api/v1/docs/:id/checkpoint", async (c) => {
|
|
172
|
+
if (c.get("role") === "viewer") throw new DocError("FORBIDDEN", "this token is a viewer; it cannot change a document");
|
|
173
|
+
const update = await readCapped(c.req.raw, MAX_LIVE_BYTES);
|
|
174
|
+
await (await serviceOf(c)).checkpoint(c.req.param("id"), update, c.get("author"));
|
|
175
|
+
return c.json({ ok: true });
|
|
176
|
+
});
|
|
177
|
+
const create = commands.find((c) => c.name === "new");
|
|
178
|
+
app2.post("/api/v1/docs", async (c) => run(c, create, await body(c)));
|
|
179
|
+
for (const command of commands.filter((c) => c !== create)) {
|
|
180
|
+
const path = `/api/v1/docs/:id/${command.name}`;
|
|
181
|
+
if (command.readOnly) app2.get(path, (c) => run(c, command, { ...c.req.query(), id: c.req.param("id") }));
|
|
182
|
+
else app2.post(path, async (c) => run(c, command, { ...await body(c), id: c.req.param("id") }));
|
|
183
|
+
}
|
|
184
|
+
app2.notFound(() => fail("NOT_FOUND", "no such route", 404));
|
|
185
|
+
app2.onError((error) => {
|
|
186
|
+
if (error instanceof DocError) return fail(error.code, error.message, STATUS[error.code] ?? 400, error.matches);
|
|
187
|
+
console.error(error instanceof Error ? error.name : "error");
|
|
188
|
+
return fail("INTERNAL", "the server could not finish the request", 500);
|
|
189
|
+
});
|
|
190
|
+
return app2;
|
|
191
|
+
}
|
|
192
|
+
var schemas = /* @__PURE__ */ new WeakMap();
|
|
193
|
+
var urlSecret;
|
|
194
|
+
function regionOf(env) {
|
|
195
|
+
let region;
|
|
196
|
+
try {
|
|
197
|
+
region = parseRegion(env.AGENTIC_DOCS_REGION);
|
|
198
|
+
} catch {
|
|
199
|
+
throw new DocError("MISCONFIGURED", 'set AGENTIC_DOCS_REGION to "eu" or "us" ("unknown" only for local development)');
|
|
200
|
+
}
|
|
201
|
+
if (region === "unknown" && env.AGENTIC_DOCS_UNSAFE_REGION !== "1") {
|
|
202
|
+
throw new DocError("MISCONFIGURED", 'the region "unknown" turns the residency check off; set AGENTIC_DOCS_UNSAFE_REGION=1 to accept that for local development');
|
|
203
|
+
}
|
|
204
|
+
return region;
|
|
205
|
+
}
|
|
206
|
+
async function serviceFor(env) {
|
|
207
|
+
if (!env.DB || !env.DOCS) throw new DocError("MISCONFIGURED", "bind a D1 database as DB and an R2 bucket as DOCS");
|
|
208
|
+
const region = regionOf(env);
|
|
209
|
+
const db = env.DB;
|
|
210
|
+
let ready = schemas.get(db);
|
|
211
|
+
if (!ready) {
|
|
212
|
+
ready = db.prepare(D1_SCHEMA).run().catch((error) => {
|
|
213
|
+
schemas.delete(db);
|
|
214
|
+
throw error;
|
|
215
|
+
});
|
|
216
|
+
schemas.set(db, ready);
|
|
217
|
+
}
|
|
218
|
+
await ready;
|
|
219
|
+
const store = createD1Store(db, { region });
|
|
220
|
+
const blobs = createR2Blobs(env.DOCS, { region, urlBase: "https://invalid.example/unused", urlSecret: urlSecret ??= `${crypto.randomUUID()}${crypto.randomUUID()}` });
|
|
221
|
+
assertRegion(region, store, blobs);
|
|
222
|
+
return new DocsService({ store, blobs });
|
|
223
|
+
}
|
|
224
|
+
var app = createApp(serviceFor);
|
|
225
|
+
var worker_default = { fetch: (request, env, ctx) => app.fetch(request, env, ctx) };
|
|
226
|
+
|
|
227
|
+
export {
|
|
228
|
+
createApp,
|
|
229
|
+
regionOf,
|
|
230
|
+
serviceFor,
|
|
231
|
+
worker_default
|
|
232
|
+
};
|