revise 2.2.0 → 3.0.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 +47 -1182
- package/LICENSE +24 -41
- package/README.md +103 -26
- package/agent.d.ts +139 -0
- package/chunks/chunk-U5X47PZ6.js +135 -0
- package/chunks/chunk-U6YKQBKL.js +15 -0
- package/chunks/chunk-ZLQVPMBN.js +1650 -0
- package/chunks/package-5SHL5FOJ.js +14 -0
- package/chunks/render-QD5VWKSV.js +109 -0
- package/cli-output.schema.json +1984 -0
- package/cli.js +3604 -0
- package/document.d.ts +320 -0
- package/docx.d.ts +73 -0
- package/errors.d.ts +14 -0
- package/index.d.ts +13 -975
- package/index.js +31 -1
- package/legacy/editor.d.ts +19 -0
- package/legacy/editor.js +16 -0
- package/legacy/server.d.ts +37 -0
- package/legacy/server.js +25 -0
- package/legacy/style.css +2 -0
- package/package.d.ts +23 -0
- package/package.json +49 -55
- package/render.d.ts +27 -0
- package/FONT-LICENSE-HAN-EXT-B.txt +0 -91
- package/FONT-LICENSE-KATEX.txt +0 -95
- package/FONT-LICENSE-LIBERATION.txt +0 -102
- package/FONT-LICENSE-NOTO-MULTILINGUAL.txt +0 -93
- package/FONT-LICENSE-NOTO-SANS-SC.txt +0 -93
- package/FONT-LICENSE-NOTO.txt +0 -94
- package/FONT-LICENSE-SOURCE-CODE-PRO.txt +0 -94
- package/FONT-LICENSE-SOURCE-SERIF-4.txt +0 -93
- package/THIRD-PARTY-NOTICES.md +0 -3376
- package/assets/historyMaterializeWorker-B51R08XN.js +0 -612
- package/backend.d.ts +0 -161
- package/backend.js +0 -3
- package/chunks/AgentPageRenderer-CH8kSTJn.js +0 -1
- package/chunks/AgentPageRenderer-P23XVguo.js +0 -1
- package/chunks/AttachmentDocumentImporter-B1WOaa7d.js +0 -1
- package/chunks/AttachmentDocumentImporter-CT_PJp_e.js +0 -1
- package/chunks/BidiText-Swm5fl9J.js +0 -160
- package/chunks/ColorParserFull-BKH72fK8.js +0 -446
- package/chunks/DocumentMutator-btiDK57e.js +0 -31
- package/chunks/DocumentScrubber-CMPfm_HF.js +0 -18
- package/chunks/DocumentScrubber-Cm2kH8op.js +0 -1
- package/chunks/DocumentScrubber-bJoKijol.js +0 -1
- package/chunks/DocumentStylesheet-DreoD7sD.js +0 -7
- package/chunks/DocxConverter-BcVG-p5K.js +0 -13
- package/chunks/DocxConverter-D2bO5RE-.js +0 -1125
- package/chunks/DocxConverter-haCfRuWb.js +0 -13
- package/chunks/DocxGraft-BRKh4mHQ.js +0 -1
- package/chunks/DocxGraft-BxSQtl12.js +0 -7
- package/chunks/DocxGraft-C9MBQCnf.js +0 -1
- package/chunks/FileImporter-CszqRfVU.js +0 -1
- package/chunks/FileImporter-DObFoCCn.js +0 -1
- package/chunks/HorizontalRuleOrnaments-gmUZ3R7Q.js +0 -3
- package/chunks/HtmlConverter-DwXB4IDh.js +0 -267
- package/chunks/ListNumbering-Qlu6AuRZ.js +0 -23
- package/chunks/MarkdownConverter-CUC8GgsS.js +0 -57
- package/chunks/MathRenderer-DT6GyJXb.js +0 -68
- package/chunks/OdtConverter-7h9HHMpL.js +0 -125
- package/chunks/OdtConverter-Ct4qypJk.js +0 -6
- package/chunks/OdtConverter-DFl__sJc.js +0 -6
- package/chunks/OfficeImportShared-BY6FdZ8d.js +0 -1
- package/chunks/OfficeImportShared-BrZoDM0j.js +0 -30
- package/chunks/OfficeImportShared-Bvm-mOyj.js +0 -1
- package/chunks/ParagraphDiffAnalysis-DrmStvmz.js +0 -36
- package/chunks/PdfConverter-1wyvoT7X.js +0 -21
- package/chunks/PdfConverter-BAoEHILo.js +0 -21
- package/chunks/PdfConverter-CKEvFj11.js +0 -448
- package/chunks/PdfImportService-BhLPp5uG.js +0 -2
- package/chunks/PdfImportService-xbzrN__L.js +0 -2
- package/chunks/RtfConverter-Bxn-enp8.js +0 -15
- package/chunks/RtfConverter-LZPzBDSr.js +0 -15
- package/chunks/RtfConverter-_bacXqr1.js +0 -186
- package/chunks/TableBorders-CwnF3jvH.js +0 -30
- package/chunks/TableGrid-DbroRucl.js +0 -12
- package/chunks/TextFileDecoder--rsVs3NR.js +0 -9
- package/chunks/TxtConverter-C7FOwHpH.js +0 -14
- package/chunks/UnicodeUtils-CXtusU4g.js +0 -10
- package/chunks/XmlConverter-7oo8oeQB.js +0 -2
- package/chunks/XmlConverter-DGORHJcZ.js +0 -96
- package/chunks/XmlConverter-DeLHReK_.js +0 -2
- package/chunks/__vite-browser-external-l0sNRNKZ.js +0 -0
- package/chunks/_commonjsHelpers-DaMA6jEr.js +0 -2
- package/chunks/_revise-sdk-lazy-harfbuzz-CDWdjF9f.js +0 -1
- package/chunks/_revise-sdk-lazy-harfbuzz-X8gnYTZL.js +0 -1
- package/chunks/ar-ComXvtmh.js +0 -13
- package/chunks/backend-DecaBz-I.js +0 -490
- package/chunks/base64-sSPOzq-u.js +0 -198
- package/chunks/browser-module-CFmqfGkE.js +0 -1791
- package/chunks/browser-module-Dj5o3kuL.js +0 -3
- package/chunks/browser-module-UuM_O36W.js +0 -3
- package/chunks/bundle.min-DRpfgV69.js +0 -782
- package/chunks/capture-1btcWcPF.js +0 -9
- package/chunks/carrier-DQeng6yq.js +0 -6
- package/chunks/de-BkbpIKxx.js +0 -13
- package/chunks/es-CKkXujOF.js +0 -13
- package/chunks/fil-piIHMc8F.js +0 -13
- package/chunks/footnoteFlatten-DUQticYy.js +0 -6
- package/chunks/fr-DuA8qLje.js +0 -13
- package/chunks/graftExport-CYnStOdU.js +0 -82
- package/chunks/html2canvas.esm-CntiHiYW.js +0 -997
- package/chunks/index-2TBhRkX7.js +0 -387
- package/chunks/index-BWdRWxoH.js +0 -1807
- package/chunks/index-Bf14eTEW.js +0 -1096
- package/chunks/index-BwcX_r62.js +0 -387
- package/chunks/index.es-CI0QPo8w.js +0 -780
- package/chunks/index.es-CI7cDExj.js +0 -5
- package/chunks/index.es-DHpGt6Y5.js +0 -5
- package/chunks/ja-DgrSs555.js +0 -13
- package/chunks/jspdf.es.min--GshOuIm.js +0 -77
- package/chunks/jspdf.es.min-DU74zN8b.js +0 -1792
- package/chunks/jspdf.es.min-DurJZ1zW.js +0 -77
- package/chunks/jszip.min-CPW0h1Kx.js +0 -476
- package/chunks/katex-DWj8TvwO.js +0 -1462
- package/chunks/pdf-BLWDCiFF.js +0 -1828
- package/chunks/persistInlineDocumentImages-DQj2iaqg.js +0 -1
- package/chunks/pseudoLocale-BoYTjmiK.js +0 -1
- package/chunks/pseudoLocale-DG2NnZmz.js +0 -1
- package/chunks/purify.es-BwLardii.js +0 -107
- package/chunks/serverDocumentSession-CJ1EH3Tp.js +0 -15
- package/chunks/signatures-Bwt_OGik.js +0 -17
- package/chunks/suggestionAuthorship-B-IuLQlE.js +0 -1
- package/chunks/tree-sitter-D6zWWUMP.js +0 -4
- package/chunks/wasmInline.generated-CBYFkdSM.js +0 -1
- package/chunks/web-tree-sitter-BXGZGnNj.js +0 -2
- package/chunks/zh-9OF3SWWO.js +0 -13
- package/full.js +0 -1
- package/root.d.ts +0 -5
- package/root.js +0 -2
- package/serverToolContract.d.ts +0 -391
- package/serverToolInputs.generated.d.ts +0 -250
- package/style.css +0 -1
|
@@ -0,0 +1,1650 @@
|
|
|
1
|
+
// revise: MIT License. https://revise.io/developer/docs
|
|
2
|
+
import {
|
|
3
|
+
ReviseError
|
|
4
|
+
} from "./chunk-U6YKQBKL.js";
|
|
5
|
+
|
|
6
|
+
// src/embed-sdk/lib/document.ts
|
|
7
|
+
import { engineModules as engineModules2 } from "@reviseio/engine";
|
|
8
|
+
import * as Y from "yjs";
|
|
9
|
+
import {
|
|
10
|
+
createReviseYDoc as createReviseYDoc2,
|
|
11
|
+
createServerDocumentSession,
|
|
12
|
+
decodeYDoc,
|
|
13
|
+
encodeYDoc,
|
|
14
|
+
exportDocument,
|
|
15
|
+
fileToYDoc,
|
|
16
|
+
installDomShims,
|
|
17
|
+
parseDocument as parseDocument2,
|
|
18
|
+
ydocSourceFidelity as ydocSourceFidelity2,
|
|
19
|
+
ydocToDocument as ydocToDocument2
|
|
20
|
+
} from "@reviseio/engine";
|
|
21
|
+
import { seedDocRootWithClaim as seedDocRootWithClaim2, seedYDocFromFile } from "@reviseio/engine";
|
|
22
|
+
|
|
23
|
+
// src/embed-sdk/lib/docx.ts
|
|
24
|
+
import { engineModules } from "@reviseio/engine";
|
|
25
|
+
import {
|
|
26
|
+
createReviseYDoc,
|
|
27
|
+
documentToDocx,
|
|
28
|
+
parseDocument,
|
|
29
|
+
ydocSourceFidelity,
|
|
30
|
+
ydocToDocument
|
|
31
|
+
} from "@reviseio/engine";
|
|
32
|
+
import { createHash } from "node:crypto";
|
|
33
|
+
import { seedDocRootWithClaim } from "@reviseio/engine";
|
|
34
|
+
async function exportSessionDocx(ydoc, pkg, mode = "review") {
|
|
35
|
+
let doc = ydocToDocument(ydoc);
|
|
36
|
+
const grafting = Boolean(pkg && doc.docxSource);
|
|
37
|
+
const regenerated = grafting ? new Set((await ydocSourceFidelity(ydoc)).regeneratedBlockIds) : null;
|
|
38
|
+
if (mode === "final") {
|
|
39
|
+
const { scrubDocument } = await engineModules.scrubber();
|
|
40
|
+
doc = scrubDocument(doc, {
|
|
41
|
+
removeComments: true,
|
|
42
|
+
resolveSuggestions: "accept"
|
|
43
|
+
}).doc;
|
|
44
|
+
}
|
|
45
|
+
doc = stampParaIds(
|
|
46
|
+
doc,
|
|
47
|
+
regenerated,
|
|
48
|
+
grafting ? graftKeptParts(doc, await engineModules.docxGraft()) : void 0
|
|
49
|
+
);
|
|
50
|
+
if (!grafting || !pkg || !doc.docxSource) {
|
|
51
|
+
return {
|
|
52
|
+
bytes: await documentToDocx(doc),
|
|
53
|
+
outcome: {
|
|
54
|
+
method: "regenerated",
|
|
55
|
+
exact: false,
|
|
56
|
+
regeneratedBlocks: doc.children.length,
|
|
57
|
+
fallbackCode: doc.docxSource ? "no-original-package" : "not-from-docx"
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const { hashBytes } = await engineModules.docxGraft();
|
|
62
|
+
doc = {
|
|
63
|
+
...doc,
|
|
64
|
+
docxSource: { ...doc.docxSource, sourceHash: await hashBytes(pkg) }
|
|
65
|
+
};
|
|
66
|
+
let reported = {
|
|
67
|
+
method: "regenerated",
|
|
68
|
+
exact: false,
|
|
69
|
+
regeneratedBlocks: doc.children.length,
|
|
70
|
+
fallbackCode: "no-outcome"
|
|
71
|
+
};
|
|
72
|
+
const bytes = await documentToDocx(doc, {
|
|
73
|
+
originalDocx: pkg,
|
|
74
|
+
onGraftOutcome: (outcome) => {
|
|
75
|
+
reported = outcome.mode === "regenerated" ? {
|
|
76
|
+
method: "regenerated",
|
|
77
|
+
exact: false,
|
|
78
|
+
regeneratedBlocks: doc.children.length,
|
|
79
|
+
fallbackCode: outcome.fallbackCode
|
|
80
|
+
} : {
|
|
81
|
+
method: "graft",
|
|
82
|
+
exact: outcome.mode === "exact",
|
|
83
|
+
regeneratedBlocks: outcome.regeneratedBlockCount ?? 0
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return { bytes, outcome: reported };
|
|
88
|
+
}
|
|
89
|
+
var PARA_ID_KEY = "w14:paraId";
|
|
90
|
+
function paraIdOf(node) {
|
|
91
|
+
if (!node.oxmlAttrs) return void 0;
|
|
92
|
+
try {
|
|
93
|
+
const attrs = JSON.parse(node.oxmlAttrs);
|
|
94
|
+
const value = attrs[PARA_ID_KEY];
|
|
95
|
+
return typeof value === "string" ? value.toUpperCase() : void 0;
|
|
96
|
+
} catch {
|
|
97
|
+
return void 0;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function derivedParaId(blockId, salt = 0) {
|
|
101
|
+
const value = createHash("sha1").update(salt ? `${blockId}:${salt}` : blockId).digest().readUInt32BE(0) & 2147483647;
|
|
102
|
+
return (value || 1).toString(16).toUpperCase().padStart(8, "0");
|
|
103
|
+
}
|
|
104
|
+
function childrenOf(node) {
|
|
105
|
+
return (Array.isArray(node.children) ? node.children : []).filter(
|
|
106
|
+
(child) => child !== null && typeof child === "object" && "type" in child
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
function contentSignature(node) {
|
|
110
|
+
const n = node;
|
|
111
|
+
if (!n || typeof n !== "object") return "";
|
|
112
|
+
if (n.type === "text") return JSON.stringify(n.text ?? "");
|
|
113
|
+
const children = Array.isArray(n.children) ? n.children : [];
|
|
114
|
+
return `${n.type ?? "?"}(${children.map(contentSignature).join(",")})`;
|
|
115
|
+
}
|
|
116
|
+
function walkIds(node, visit) {
|
|
117
|
+
visit(node);
|
|
118
|
+
for (const child of childrenOf(node)) walkIds(child, visit);
|
|
119
|
+
}
|
|
120
|
+
function graftKeptParts(doc, graft) {
|
|
121
|
+
const kept = { whole: /* @__PURE__ */ new Set(), head: /* @__PURE__ */ new Set() };
|
|
122
|
+
const source = doc.docxSource;
|
|
123
|
+
if (!source?.parts) return kept;
|
|
124
|
+
const version = source.signatureVersion === graft.CURRENT_GRAFT_SIGNATURE_VERSION ? graft.CURRENT_GRAFT_SIGNATURE_VERSION : 1;
|
|
125
|
+
for (const block of doc.children) {
|
|
126
|
+
const captured = block.id ? source.parts[block.id] : void 0;
|
|
127
|
+
if (!captured || block.type !== "table" && block.type !== "list") {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
for (const part of block.children) {
|
|
131
|
+
const entry = captured.parts.find((p) => p.id === part.id);
|
|
132
|
+
if (!entry || !part.id) continue;
|
|
133
|
+
if (entry.signature === graft.partSignature(part, version)) {
|
|
134
|
+
kept.whole.add(part.id);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (block.type === "table" && entry.cells && entry.head === graft.partHeadSignature(part, version)) {
|
|
138
|
+
kept.head.add(part.id);
|
|
139
|
+
part.children?.forEach((cell, c) => {
|
|
140
|
+
const cellEntry = entry.cells[c];
|
|
141
|
+
if (cell.id && cellEntry?.id === cell.id && cellEntry.signature === graft.partSignature(cell, version)) {
|
|
142
|
+
kept.whole.add(cell.id);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return kept;
|
|
149
|
+
}
|
|
150
|
+
function stampParaIds(doc, regenerated, kept = { whole: /* @__PURE__ */ new Set(), head: /* @__PURE__ */ new Set() }) {
|
|
151
|
+
const used = /* @__PURE__ */ new Set();
|
|
152
|
+
for (const block of doc.children) {
|
|
153
|
+
walkIds(block, (node) => {
|
|
154
|
+
const id = paraIdOf(node);
|
|
155
|
+
if (id) used.add(id);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const stamp = (node) => {
|
|
159
|
+
if (node.id && kept.whole.has(node.id)) return node;
|
|
160
|
+
const copy = { ...node };
|
|
161
|
+
if ((copy.type === "paragraph" || copy.type === "table-row") && copy.id && !kept.head.has(copy.id) && !paraIdOf(copy)) {
|
|
162
|
+
let salt = 0;
|
|
163
|
+
let candidate = derivedParaId(copy.id);
|
|
164
|
+
while (used.has(candidate)) candidate = derivedParaId(copy.id, ++salt);
|
|
165
|
+
used.add(candidate);
|
|
166
|
+
let attrs = {};
|
|
167
|
+
try {
|
|
168
|
+
attrs = copy.oxmlAttrs ? JSON.parse(copy.oxmlAttrs) : {};
|
|
169
|
+
} catch {
|
|
170
|
+
attrs = {};
|
|
171
|
+
}
|
|
172
|
+
copy.oxmlAttrs = JSON.stringify({ ...attrs, [PARA_ID_KEY]: candidate });
|
|
173
|
+
}
|
|
174
|
+
if (Array.isArray(node.children)) {
|
|
175
|
+
copy.children = node.children.map(
|
|
176
|
+
(child) => child && typeof child === "object" && "type" in child ? stamp(child) : child
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return copy;
|
|
180
|
+
};
|
|
181
|
+
return {
|
|
182
|
+
...doc,
|
|
183
|
+
children: doc.children.map(
|
|
184
|
+
(block) => !regenerated || block.id && regenerated.has(block.id) ? stamp(block) : block
|
|
185
|
+
)
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function lcsIndexPairs(a, b) {
|
|
189
|
+
const n = a.length;
|
|
190
|
+
const m = b.length;
|
|
191
|
+
if (n === 0 || m === 0) return [];
|
|
192
|
+
if (n * m > 4e6) {
|
|
193
|
+
const pairs2 = [];
|
|
194
|
+
let i2 = 0;
|
|
195
|
+
while (i2 < n && i2 < m && a[i2] === b[i2]) pairs2.push([i2, i2++]);
|
|
196
|
+
let k = 1;
|
|
197
|
+
while (k <= n - i2 && k <= m - i2 && a[n - k] === b[m - k]) {
|
|
198
|
+
pairs2.push([n - k, m - k]);
|
|
199
|
+
k += 1;
|
|
200
|
+
}
|
|
201
|
+
return pairs2.sort((x, y) => x[0] - y[0]);
|
|
202
|
+
}
|
|
203
|
+
const table = new Uint32Array((n + 1) * (m + 1));
|
|
204
|
+
for (let i2 = n - 1; i2 >= 0; i2 -= 1) {
|
|
205
|
+
for (let j2 = m - 1; j2 >= 0; j2 -= 1) {
|
|
206
|
+
table[i2 * (m + 1) + j2] = a[i2] === b[j2] ? table[(i2 + 1) * (m + 1) + j2 + 1] + 1 : Math.max(table[(i2 + 1) * (m + 1) + j2], table[i2 * (m + 1) + j2 + 1]);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const pairs = [];
|
|
210
|
+
let i = 0;
|
|
211
|
+
let j = 0;
|
|
212
|
+
while (i < n && j < m) {
|
|
213
|
+
if (a[i] === b[j]) {
|
|
214
|
+
pairs.push([i, j]);
|
|
215
|
+
i += 1;
|
|
216
|
+
j += 1;
|
|
217
|
+
} else if (table[(i + 1) * (m + 1) + j] >= table[i * (m + 1) + j + 1]) {
|
|
218
|
+
i += 1;
|
|
219
|
+
} else {
|
|
220
|
+
j += 1;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return pairs;
|
|
224
|
+
}
|
|
225
|
+
function matchIds(previous, next) {
|
|
226
|
+
const renamed = /* @__PURE__ */ new Map();
|
|
227
|
+
const changed = /* @__PURE__ */ new Set();
|
|
228
|
+
const oldIds = /* @__PURE__ */ new Set();
|
|
229
|
+
for (const block of previous.children) {
|
|
230
|
+
walkIds(block, (node) => node.id && oldIds.add(node.id));
|
|
231
|
+
}
|
|
232
|
+
const byParaId = /* @__PURE__ */ new Map();
|
|
233
|
+
for (const block of previous.children) {
|
|
234
|
+
walkIds(block, (node) => {
|
|
235
|
+
const explicit = paraIdOf(node);
|
|
236
|
+
if (explicit && !byParaId.has(explicit)) byParaId.set(explicit, node);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
for (const block of previous.children) {
|
|
240
|
+
walkIds(block, (node) => {
|
|
241
|
+
if (!node.id) return;
|
|
242
|
+
const derived = derivedParaId(node.id);
|
|
243
|
+
if (!byParaId.has(derived)) byParaId.set(derived, node);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const oldParent = /* @__PURE__ */ new Map();
|
|
247
|
+
const indexParents = (node) => {
|
|
248
|
+
for (const child of childrenOf(node)) {
|
|
249
|
+
oldParent.set(child, node);
|
|
250
|
+
indexParents(child);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
previous.children.forEach((block) => indexParents(block));
|
|
254
|
+
const seenParaIds = /* @__PURE__ */ new Set();
|
|
255
|
+
const duplicated = /* @__PURE__ */ new Set();
|
|
256
|
+
for (const block of next.children) {
|
|
257
|
+
walkIds(block, (node) => {
|
|
258
|
+
const pid = paraIdOf(node);
|
|
259
|
+
if (!pid) return;
|
|
260
|
+
if (seenParaIds.has(pid)) duplicated.add(pid);
|
|
261
|
+
seenParaIds.add(pid);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
265
|
+
let fresh = 0;
|
|
266
|
+
const carrySubtree = (from, to) => {
|
|
267
|
+
if (from.id && to.id) {
|
|
268
|
+
renamed.set(to.id, from.id);
|
|
269
|
+
to.id = from.id;
|
|
270
|
+
}
|
|
271
|
+
claimed.add(from);
|
|
272
|
+
const a = childrenOf(from);
|
|
273
|
+
const b = childrenOf(to);
|
|
274
|
+
b.forEach((child, i) => {
|
|
275
|
+
if (a[i] && a[i].type === child.type) carrySubtree(a[i], child);
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
const matchLists = (olds, news) => {
|
|
279
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
280
|
+
for (const node of news) {
|
|
281
|
+
const pid = paraIdOf(node);
|
|
282
|
+
if (pid && duplicated.has(pid)) continue;
|
|
283
|
+
const old = pid ? byParaId.get(pid) : void 0;
|
|
284
|
+
if (old && !claimed.has(old) && old.type === node.type) {
|
|
285
|
+
pairs.set(node, old);
|
|
286
|
+
claimed.add(old);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const oldSet = new Set(olds);
|
|
290
|
+
for (const node of news) {
|
|
291
|
+
if (pairs.has(node) || paraIdOf(node)) continue;
|
|
292
|
+
let found;
|
|
293
|
+
walkIds(node, (inner) => {
|
|
294
|
+
if (found || inner === node) return;
|
|
295
|
+
const pid = paraIdOf(inner);
|
|
296
|
+
let at = pid && !duplicated.has(pid) ? byParaId.get(pid) : void 0;
|
|
297
|
+
while (at && !oldSet.has(at)) at = oldParent.get(at);
|
|
298
|
+
if (at && !claimed.has(at) && at.type === node.type) found = at;
|
|
299
|
+
});
|
|
300
|
+
if (found) {
|
|
301
|
+
pairs.set(node, found);
|
|
302
|
+
claimed.add(found);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const restOld = olds.filter((node) => !claimed.has(node));
|
|
306
|
+
const restNew = news.filter((node) => !pairs.has(node));
|
|
307
|
+
const lcs = lcsIndexPairs(
|
|
308
|
+
restOld.map(contentSignature),
|
|
309
|
+
restNew.map(contentSignature)
|
|
310
|
+
);
|
|
311
|
+
for (const [i, j] of lcs) {
|
|
312
|
+
if (restOld[i].type === restNew[j].type) {
|
|
313
|
+
pairs.set(restNew[j], restOld[i]);
|
|
314
|
+
claimed.add(restOld[i]);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
for (const node of news) {
|
|
318
|
+
const old = pairs.get(node);
|
|
319
|
+
if (!old) {
|
|
320
|
+
walkIds(node, (n) => {
|
|
321
|
+
if (n.id && n.type !== "text") fresh += 1;
|
|
322
|
+
if (n.id && oldIds.has(n.id)) n.id = void 0;
|
|
323
|
+
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (contentSignature(old) === contentSignature(node)) {
|
|
327
|
+
carrySubtree(old, node);
|
|
328
|
+
} else {
|
|
329
|
+
if (old.id && node.id) {
|
|
330
|
+
renamed.set(node.id, old.id);
|
|
331
|
+
node.id = old.id;
|
|
332
|
+
changed.add(old.id);
|
|
333
|
+
}
|
|
334
|
+
matchLists(childrenOf(old), childrenOf(node));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
matchLists(previous.children, next.children);
|
|
339
|
+
for (const block of next.children) {
|
|
340
|
+
walkIds(block, (node) => {
|
|
341
|
+
if (node.type !== "text" && node.id === void 0 && "id" in node) {
|
|
342
|
+
node.id = freshId(oldIds);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
const source = next.docxSource;
|
|
347
|
+
if (source) {
|
|
348
|
+
source.spans = source.spans.map((span) => ({
|
|
349
|
+
...span,
|
|
350
|
+
blockIds: span.blockIds.map((id) => renamed.get(id) ?? id)
|
|
351
|
+
}));
|
|
352
|
+
}
|
|
353
|
+
const nextIds = /* @__PURE__ */ new Set();
|
|
354
|
+
for (const block of next.children) {
|
|
355
|
+
walkIds(block, (node) => node.id && nextIds.add(node.id));
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
kept: renamed.size,
|
|
359
|
+
fresh,
|
|
360
|
+
changedIds: [...changed],
|
|
361
|
+
removedIds: [...oldIds].filter((id) => !nextIds.has(id))
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function freshId(avoid) {
|
|
365
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
366
|
+
for (; ; ) {
|
|
367
|
+
let id = "";
|
|
368
|
+
for (let i = 0; i < 6; i += 1) {
|
|
369
|
+
id += alphabet[Math.floor(Math.random() * alphabet.length)];
|
|
370
|
+
}
|
|
371
|
+
if (!avoid.has(id)) return id;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async function relearn(ydoc, bytes, filename) {
|
|
375
|
+
let next;
|
|
376
|
+
try {
|
|
377
|
+
next = await parseDocument(bytes, filename);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
throw new ReviseError(
|
|
380
|
+
`${filename} does not open: ${error.message}. Nothing was changed.`,
|
|
381
|
+
"import_failed"
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
const previous = ydocToDocument(ydoc);
|
|
385
|
+
const ids = matchIds(previous, next);
|
|
386
|
+
const { refreshDocxSourceSignatures, storeDocxCarrier } = await engineModules.docxGraft();
|
|
387
|
+
refreshDocxSourceSignatures(next);
|
|
388
|
+
const synced = createReviseYDoc();
|
|
389
|
+
seedDocRootWithClaim(synced, next);
|
|
390
|
+
if (next.docxSource?.sourceHash) {
|
|
391
|
+
await storeDocxCarrier(synced, bytes, {
|
|
392
|
+
hash: next.docxSource.sourceHash
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
const styleMap = (doc) => new Map(
|
|
396
|
+
(doc.stylesheet?.paragraphStyles ?? []).map((style) => [
|
|
397
|
+
style.styleId,
|
|
398
|
+
JSON.stringify(style)
|
|
399
|
+
])
|
|
400
|
+
);
|
|
401
|
+
const stylesBefore = styleMap(previous);
|
|
402
|
+
const changedStyles = [...styleMap(next)].filter(([id, json]) => stylesBefore.get(id) !== json).map(([id]) => id);
|
|
403
|
+
return {
|
|
404
|
+
ydoc: synced,
|
|
405
|
+
pkg: bytes,
|
|
406
|
+
idsKept: ids.kept,
|
|
407
|
+
idsNew: ids.fresh,
|
|
408
|
+
changedIds: ids.changedIds,
|
|
409
|
+
removedIds: ids.removedIds,
|
|
410
|
+
changedBlocks: ids.changedIds.length,
|
|
411
|
+
changedStyles
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/embed-sdk/lib/document.ts
|
|
416
|
+
var READABLE = /\.(docx|md|markdown|html?|odt|rtf|txt)$/i;
|
|
417
|
+
var PLAIN_FORMATS = /\.(md|markdown|html?|txt)$/i;
|
|
418
|
+
var EXTENSION_FORMATS = {
|
|
419
|
+
".docx": "docx",
|
|
420
|
+
".pdf": "pdf",
|
|
421
|
+
".md": "markdown",
|
|
422
|
+
".markdown": "markdown",
|
|
423
|
+
".html": "html",
|
|
424
|
+
".htm": "html",
|
|
425
|
+
".txt": "txt"
|
|
426
|
+
};
|
|
427
|
+
function formatForPath(file) {
|
|
428
|
+
const match = /\.[^./\\]+$/.exec(file);
|
|
429
|
+
return match ? EXTENSION_FORMATS[match[0].toLowerCase()] : void 0;
|
|
430
|
+
}
|
|
431
|
+
async function readSource(source) {
|
|
432
|
+
if (typeof source !== "string") return source;
|
|
433
|
+
const fs = await import("node:fs");
|
|
434
|
+
if (!fs.existsSync(source)) {
|
|
435
|
+
throw new ReviseError(`No such file: ${source}`, "not_found");
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
return { bytes: new Uint8Array(fs.readFileSync(source)), filename: source };
|
|
439
|
+
} catch (error) {
|
|
440
|
+
throw new ReviseError(
|
|
441
|
+
`Could not read ${baseName(source)}: ${error.message}`,
|
|
442
|
+
"read_failed",
|
|
443
|
+
{ cause: error }
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
async function importYDoc(bytes, filename) {
|
|
448
|
+
try {
|
|
449
|
+
return await fileToYDoc(bytes, filename);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
throw new ReviseError(
|
|
452
|
+
`${baseName(filename)} does not open: ${error.message}`,
|
|
453
|
+
"import_failed",
|
|
454
|
+
{ cause: error }
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function assertReadable(filename) {
|
|
459
|
+
if (!READABLE.test(filename)) {
|
|
460
|
+
throw new ReviseError(
|
|
461
|
+
`Cannot read ${baseName(filename)}. Supported: .docx, .md, .html, .odt, .rtf, .txt.`,
|
|
462
|
+
"unsupported_format"
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function baseName(file) {
|
|
467
|
+
return file.split(/[/\\]/).pop() ?? file;
|
|
468
|
+
}
|
|
469
|
+
function stem(file) {
|
|
470
|
+
return baseName(file).replace(/\.[^.]+$/, "");
|
|
471
|
+
}
|
|
472
|
+
async function exportYDoc(ydoc, pkg, { format, mode = "review", title = "Document" }) {
|
|
473
|
+
if (format === "pdf")
|
|
474
|
+
return { bytes: await exportPdf(ydoc, title), outcome: null };
|
|
475
|
+
if (format === "docx") return exportSessionDocx(ydoc, pkg, mode);
|
|
476
|
+
return {
|
|
477
|
+
bytes: await exportDocument(ydoc, { format, mode, title }),
|
|
478
|
+
outcome: null
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
async function exportPdf(ydoc, title) {
|
|
482
|
+
const [{ serializeDocumentFormat }, { DocumentMutator }] = await Promise.all([
|
|
483
|
+
engineModules2.documentFormats(),
|
|
484
|
+
engineModules2.mutator()
|
|
485
|
+
]);
|
|
486
|
+
const doc = DocumentMutator.materializeAcceptedSuggestions(
|
|
487
|
+
ydocToDocument2(ydoc)
|
|
488
|
+
);
|
|
489
|
+
const output = await serializeDocumentFormat(doc, "pdf", { title });
|
|
490
|
+
return typeof output === "string" ? new TextEncoder().encode(output) : new Uint8Array(await output.arrayBuffer());
|
|
491
|
+
}
|
|
492
|
+
var SNAPSHOT_MAGIC = "RVSNAP";
|
|
493
|
+
var SNAPSHOT_SCHEMA = 1;
|
|
494
|
+
function encodeSnapshot2(header, state, pkg) {
|
|
495
|
+
const json = new TextEncoder().encode(
|
|
496
|
+
JSON.stringify({
|
|
497
|
+
...header,
|
|
498
|
+
schema: SNAPSHOT_SCHEMA,
|
|
499
|
+
stateBytes: state.length,
|
|
500
|
+
packageBytes: pkg?.length ?? 0
|
|
501
|
+
})
|
|
502
|
+
);
|
|
503
|
+
const magic = new TextEncoder().encode(SNAPSHOT_MAGIC);
|
|
504
|
+
const out = new Uint8Array(
|
|
505
|
+
magic.length + 4 + json.length + state.length + (pkg?.length ?? 0)
|
|
506
|
+
);
|
|
507
|
+
out.set(magic, 0);
|
|
508
|
+
new DataView(out.buffer).setUint32(magic.length, json.length);
|
|
509
|
+
out.set(json, magic.length + 4);
|
|
510
|
+
out.set(state, magic.length + 4 + json.length);
|
|
511
|
+
if (pkg) out.set(pkg, magic.length + 4 + json.length + state.length);
|
|
512
|
+
return out;
|
|
513
|
+
}
|
|
514
|
+
function decodeSnapshot(bytes) {
|
|
515
|
+
const magic = new TextDecoder().decode(
|
|
516
|
+
bytes.subarray(0, SNAPSHOT_MAGIC.length)
|
|
517
|
+
);
|
|
518
|
+
if (magic !== SNAPSHOT_MAGIC) {
|
|
519
|
+
return { header: null, state: bytes, pkg: null };
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
523
|
+
const jsonLength = view.getUint32(SNAPSHOT_MAGIC.length);
|
|
524
|
+
const start = SNAPSHOT_MAGIC.length + 4;
|
|
525
|
+
const header = JSON.parse(
|
|
526
|
+
new TextDecoder().decode(bytes.subarray(start, start + jsonLength))
|
|
527
|
+
);
|
|
528
|
+
const valid = Number.isInteger(header.schema) && header.schema >= 1 && typeof header.documentId === "string" && (header.mode === "suggesting" || header.mode === "editing") && typeof header.title === "string" && (header.author === void 0 || typeof header.author === "string") && Number.isInteger(header.stateBytes) && header.stateBytes > 0 && Number.isInteger(header.packageBytes) && header.packageBytes >= 0 && start + jsonLength + header.stateBytes + header.packageBytes === bytes.length;
|
|
529
|
+
if (!valid && !(header.schema > SNAPSHOT_SCHEMA)) throw new Error("header");
|
|
530
|
+
if (header.schema > SNAPSHOT_SCHEMA) {
|
|
531
|
+
throw new ReviseError(
|
|
532
|
+
`This snapshot uses schema ${header.schema}; this version of revise reads up to ${SNAPSHOT_SCHEMA}. Upgrade revise.`,
|
|
533
|
+
"snapshot_too_new"
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
const stateStart = start + jsonLength;
|
|
537
|
+
const state = bytes.subarray(stateStart, stateStart + header.stateBytes);
|
|
538
|
+
const pkg = header.packageBytes ? bytes.subarray(
|
|
539
|
+
stateStart + header.stateBytes,
|
|
540
|
+
stateStart + header.stateBytes + header.packageBytes
|
|
541
|
+
) : null;
|
|
542
|
+
if (state.length !== header.stateBytes || pkg && pkg.length !== header.packageBytes) {
|
|
543
|
+
throw new Error("truncated");
|
|
544
|
+
}
|
|
545
|
+
return { header, state, pkg };
|
|
546
|
+
} catch (error) {
|
|
547
|
+
if (error instanceof ReviseError) throw error;
|
|
548
|
+
throw new ReviseError(
|
|
549
|
+
"This snapshot is damaged or incomplete.",
|
|
550
|
+
"snapshot_corrupt",
|
|
551
|
+
{
|
|
552
|
+
cause: error
|
|
553
|
+
}
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
var documentCounter = 0;
|
|
558
|
+
var ReviseDocument = class _ReviseDocument {
|
|
559
|
+
constructor(currentYDoc, currentSession, settings, originalPackage, ownsYDoc) {
|
|
560
|
+
this.currentYDoc = currentYDoc;
|
|
561
|
+
this.currentSession = currentSession;
|
|
562
|
+
this.settings = settings;
|
|
563
|
+
this.originalPackage = originalPackage;
|
|
564
|
+
this.ownsYDoc = ownsYDoc;
|
|
565
|
+
this.disposed = false;
|
|
566
|
+
this.queue = Promise.resolve();
|
|
567
|
+
// ── tools ────────────────────────────────────────────────────────────────
|
|
568
|
+
this.tools = {
|
|
569
|
+
/** Every editing tool's name, description, and JSON input schema. */
|
|
570
|
+
definitions: () => {
|
|
571
|
+
this.assertOpen();
|
|
572
|
+
return this.currentSession.tools.getDefinitions();
|
|
573
|
+
},
|
|
574
|
+
/** Run a known tool with typed input. Expected failures come back as
|
|
575
|
+
* `{ ok: false, error }`; nothing throws for a bad edit. */
|
|
576
|
+
call: (name, ...args) => this.serial(
|
|
577
|
+
async () => this.afterTool(
|
|
578
|
+
name,
|
|
579
|
+
await this.currentSession.tools.execute(name, ...args)
|
|
580
|
+
)
|
|
581
|
+
),
|
|
582
|
+
/** Run a tool by name with untyped input (agent tool calls). Unknown
|
|
583
|
+
* tools and bad input come back as `{ ok: false, error }` too. */
|
|
584
|
+
run: (name, input = {}, options = {}) => this.serial(async () => {
|
|
585
|
+
const definition = this.currentSession.tools.getDefinitions().find((tool) => tool.name === name);
|
|
586
|
+
if (!definition) {
|
|
587
|
+
return {
|
|
588
|
+
ok: false,
|
|
589
|
+
error: {
|
|
590
|
+
code: "unknown_tool",
|
|
591
|
+
message: `Unknown tool "${name}".`
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
const required = definition.inputSchema.required ?? [];
|
|
596
|
+
const filled = required.includes("context_notes") && input.context_notes === void 0 ? { ...input, context_notes: "N/A" } : input;
|
|
597
|
+
return this.afterTool(
|
|
598
|
+
name,
|
|
599
|
+
await this.currentSession.tools.executeDynamic(
|
|
600
|
+
name,
|
|
601
|
+
filled,
|
|
602
|
+
options.direct ? { directMode: true } : void 0
|
|
603
|
+
)
|
|
604
|
+
);
|
|
605
|
+
})
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
/** Open a file (path) or bytes. Word files keep their original package,
|
|
609
|
+
* so untouched content exports byte for byte. */
|
|
610
|
+
static async open(source, options = {}) {
|
|
611
|
+
const { bytes, filename } = await readSource(source);
|
|
612
|
+
assertReadable(filename);
|
|
613
|
+
const ydoc = await importYDoc(bytes, filename);
|
|
614
|
+
return _ReviseDocument.build(ydoc, {
|
|
615
|
+
mode: PLAIN_FORMATS.test(filename) ? "editing" : "suggesting",
|
|
616
|
+
...options,
|
|
617
|
+
title: stem(filename),
|
|
618
|
+
package: /\.docx$/i.test(filename) ? bytes : null,
|
|
619
|
+
owned: true
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
/** Start a document from the HTML the tools read and write (headings,
|
|
623
|
+
* `data-style-id="Title"`, lists, tables), or from Markdown. */
|
|
624
|
+
static async create(content = {}, options = {}) {
|
|
625
|
+
let ydoc;
|
|
626
|
+
if (content.html !== void 0) {
|
|
627
|
+
await installDomShims();
|
|
628
|
+
const { AgentHtmlConverter } = await engineModules2.agentHtml();
|
|
629
|
+
const body = /<body[^>]*>([\s\S]*)<\/body>/i.exec(content.html)?.[1] ?? content.html;
|
|
630
|
+
ydoc = createReviseYDoc2();
|
|
631
|
+
seedDocRootWithClaim2(ydoc, AgentHtmlConverter.fromAgentHtml(body));
|
|
632
|
+
} else {
|
|
633
|
+
const markdown = content.markdown ?? (content.title ? `# ${content.title}
|
|
634
|
+
` : "\n");
|
|
635
|
+
ydoc = await importYDoc(new TextEncoder().encode(markdown), "new.md");
|
|
636
|
+
}
|
|
637
|
+
return _ReviseDocument.build(ydoc, {
|
|
638
|
+
mode: "editing",
|
|
639
|
+
...options,
|
|
640
|
+
title: content.title ?? "Document",
|
|
641
|
+
package: null,
|
|
642
|
+
owned: true
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
/** Restore a document from `snapshot()` (or a bare Y.js update). Options
|
|
646
|
+
* override what the snapshot recorded. */
|
|
647
|
+
static async restore(snapshot2, options = {}) {
|
|
648
|
+
const { header, state, pkg } = decodeSnapshot(snapshot2);
|
|
649
|
+
let ydoc;
|
|
650
|
+
try {
|
|
651
|
+
ydoc = decodeYDoc(state);
|
|
652
|
+
} catch (error) {
|
|
653
|
+
throw new ReviseError("This snapshot is damaged.", "snapshot_corrupt", {
|
|
654
|
+
cause: error
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
return _ReviseDocument.build(ydoc, {
|
|
658
|
+
...header ? {
|
|
659
|
+
documentId: header.documentId,
|
|
660
|
+
mode: header.mode,
|
|
661
|
+
title: header.title,
|
|
662
|
+
...header.author ? { author: header.author } : {}
|
|
663
|
+
} : {},
|
|
664
|
+
...options,
|
|
665
|
+
package: pkg,
|
|
666
|
+
owned: true
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
/** Wrap an existing Revise Y.Doc, e.g. one shared with an editor. The
|
|
670
|
+
* Y.Doc stays yours: `dispose()` leaves it intact, and `reload()` is not
|
|
671
|
+
* available (it would have to replace it). */
|
|
672
|
+
static async fromYDoc(ydoc, options = {}) {
|
|
673
|
+
let pkg = options.package;
|
|
674
|
+
if (pkg === void 0) {
|
|
675
|
+
const { loadDocxCarrier } = await engineModules2.docxGraft();
|
|
676
|
+
pkg = loadDocxCarrier(ydoc)?.bytes ?? null;
|
|
677
|
+
}
|
|
678
|
+
return _ReviseDocument.build(ydoc, {
|
|
679
|
+
...options,
|
|
680
|
+
package: pkg,
|
|
681
|
+
owned: false
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
static async build(ydoc, options) {
|
|
685
|
+
const { package: pkg, owned, ...rest } = options;
|
|
686
|
+
const settings = {
|
|
687
|
+
...rest,
|
|
688
|
+
documentId: rest.documentId ?? `revise-${Date.now()}-${++documentCounter}`,
|
|
689
|
+
mode: rest.mode ?? "suggesting",
|
|
690
|
+
title: rest.title ?? "Document"
|
|
691
|
+
};
|
|
692
|
+
return new _ReviseDocument(
|
|
693
|
+
ydoc,
|
|
694
|
+
await _ReviseDocument.startSession(ydoc, settings),
|
|
695
|
+
settings,
|
|
696
|
+
pkg ?? null,
|
|
697
|
+
owned
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
static async startSession(ydoc, settings) {
|
|
701
|
+
(await engineModules2.serverSession()).requireDocxRoundTrip(ydoc);
|
|
702
|
+
return createServerDocumentSession(ydoc, {
|
|
703
|
+
documentId: settings.documentId,
|
|
704
|
+
mode: settings.mode,
|
|
705
|
+
...settings.author ? { agentName: settings.author } : {},
|
|
706
|
+
...settings.images ? { images: settings.images } : {}
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
assertOpen() {
|
|
710
|
+
if (this.disposed) {
|
|
711
|
+
throw new ReviseError("This document was disposed.", "disposed");
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
/** Run `work` after every operation already queued on this document. */
|
|
715
|
+
serial(work) {
|
|
716
|
+
if (this.disposed) {
|
|
717
|
+
return Promise.reject(
|
|
718
|
+
new ReviseError("This document was disposed.", "disposed")
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
const run = this.queue.then(() => {
|
|
722
|
+
this.assertOpen();
|
|
723
|
+
return work();
|
|
724
|
+
});
|
|
725
|
+
this.queue = run.catch(() => void 0);
|
|
726
|
+
return run;
|
|
727
|
+
}
|
|
728
|
+
/** The live document model. Shared with `@reviseio/editor`. */
|
|
729
|
+
get ydoc() {
|
|
730
|
+
return this.currentYDoc;
|
|
731
|
+
}
|
|
732
|
+
/** The low-level tool session behind `tools`: an escape hatch for what
|
|
733
|
+
* the methods here don't cover. */
|
|
734
|
+
get session() {
|
|
735
|
+
return this.currentSession;
|
|
736
|
+
}
|
|
737
|
+
get mode() {
|
|
738
|
+
return this.currentSession.getMode();
|
|
739
|
+
}
|
|
740
|
+
/** Switch between tracked changes ("suggesting") and direct edits. */
|
|
741
|
+
setMode(mode) {
|
|
742
|
+
return this.serial(() => {
|
|
743
|
+
if (mode === "suggesting") this.currentSession.setSuggestingMode();
|
|
744
|
+
else this.currentSession.setEditingMode();
|
|
745
|
+
this.settings.mode = mode;
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
get author() {
|
|
749
|
+
return this.settings.author;
|
|
750
|
+
}
|
|
751
|
+
get documentId() {
|
|
752
|
+
return this.settings.documentId;
|
|
753
|
+
}
|
|
754
|
+
/** The original Word package that export grafts onto, if any. */
|
|
755
|
+
get package() {
|
|
756
|
+
return this.originalPackage;
|
|
757
|
+
}
|
|
758
|
+
/** The complete document (versioned; includes the original Word package)
|
|
759
|
+
* to store or hand to `@reviseio/editor`, as of every operation queued
|
|
760
|
+
* before this call. Restore with `restore()`. */
|
|
761
|
+
snapshot() {
|
|
762
|
+
return this.serial(
|
|
763
|
+
() => encodeSnapshot2(
|
|
764
|
+
{
|
|
765
|
+
documentId: this.settings.documentId,
|
|
766
|
+
mode: this.mode,
|
|
767
|
+
...this.settings.author ? { author: this.settings.author } : {},
|
|
768
|
+
title: this.settings.title
|
|
769
|
+
},
|
|
770
|
+
encodeYDoc(this.currentYDoc),
|
|
771
|
+
this.originalPackage
|
|
772
|
+
)
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
/** An opaque value that changes whenever the document does (including
|
|
776
|
+
* edits that only delete, which a state vector misses). */
|
|
777
|
+
version() {
|
|
778
|
+
this.assertOpen();
|
|
779
|
+
return Y.encodeSnapshot(Y.snapshot(this.currentYDoc));
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Re-read the document from new bytes (it changed elsewhere). Block ids
|
|
783
|
+
* carry over only where identity is provable (Word's paragraph ids, or
|
|
784
|
+
* identical content); `changedIds` lists kept ids whose content changed,
|
|
785
|
+
* so earlier reads of them are stale. Replaces the Y.Doc, so it is not
|
|
786
|
+
* available on a document made with `fromYDoc`.
|
|
787
|
+
*/
|
|
788
|
+
reload(bytes, filename) {
|
|
789
|
+
return this.serial(async () => {
|
|
790
|
+
if (!this.ownsYDoc) {
|
|
791
|
+
throw new ReviseError(
|
|
792
|
+
"reload() would replace a Y.Doc you own (fromYDoc). Open a new document instead.",
|
|
793
|
+
"borrowed_ydoc"
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
const fresh = await relearn(this.currentYDoc, bytes, filename);
|
|
797
|
+
const session = await _ReviseDocument.startSession(
|
|
798
|
+
fresh.ydoc,
|
|
799
|
+
this.settings
|
|
800
|
+
);
|
|
801
|
+
this.currentSession.dispose();
|
|
802
|
+
this.currentYDoc.destroy();
|
|
803
|
+
this.currentYDoc = fresh.ydoc;
|
|
804
|
+
this.currentSession = session;
|
|
805
|
+
if (/\.docx$/i.test(filename)) this.originalPackage = bytes;
|
|
806
|
+
return {
|
|
807
|
+
idsKept: fresh.idsKept,
|
|
808
|
+
idsNew: fresh.idsNew,
|
|
809
|
+
changedIds: fresh.changedIds,
|
|
810
|
+
removedIds: fresh.removedIds
|
|
811
|
+
};
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
/** Start the tool session over: forget what the agent has read and
|
|
815
|
+
* searched (e.g. after the host compacts its transcript). */
|
|
816
|
+
resetToolContext() {
|
|
817
|
+
return this.serial(async () => {
|
|
818
|
+
const session = await _ReviseDocument.startSession(
|
|
819
|
+
this.currentYDoc,
|
|
820
|
+
this.settings
|
|
821
|
+
);
|
|
822
|
+
this.currentSession.dispose();
|
|
823
|
+
this.currentSession = session;
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
/** Release the tool session. The document can't be used afterwards; a
|
|
827
|
+
* Y.Doc it created is destroyed, one passed to fromYDoc is left alone. */
|
|
828
|
+
dispose() {
|
|
829
|
+
if (this.disposed) return;
|
|
830
|
+
this.disposed = true;
|
|
831
|
+
this.currentSession.dispose();
|
|
832
|
+
if (this.ownsYDoc) this.currentYDoc.destroy();
|
|
833
|
+
}
|
|
834
|
+
/** The document's title for exports: its saved title property (dc:title
|
|
835
|
+
* in a .docx, or set_title's), else the name it was opened under. */
|
|
836
|
+
title() {
|
|
837
|
+
const core = this.currentYDoc.getMap("metadata").get("coreProperties");
|
|
838
|
+
return typeof core?.title === "string" && core.title.trim() ? core.title : this.settings.title;
|
|
839
|
+
}
|
|
840
|
+
/** set_title renames the document in the editor. In a file, the title is
|
|
841
|
+
* the document property (docProps/core.xml in a .docx, the PDF title), so
|
|
842
|
+
* it is saved there too; without this, the tool reported success and the
|
|
843
|
+
* saved file kept its old title. */
|
|
844
|
+
afterTool(name, result) {
|
|
845
|
+
if (name !== "set_title" || !result.ok) return result;
|
|
846
|
+
const metadata = this.currentYDoc.getMap("metadata");
|
|
847
|
+
const title = metadata.get("title");
|
|
848
|
+
if (typeof title !== "string") return result;
|
|
849
|
+
const core = metadata.get("coreProperties");
|
|
850
|
+
this.currentYDoc.transact(() => {
|
|
851
|
+
metadata.set("coreProperties", {
|
|
852
|
+
...core && typeof core === "object" ? core : {},
|
|
853
|
+
title
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
this.settings.title = title;
|
|
857
|
+
return result;
|
|
858
|
+
}
|
|
859
|
+
// ── reading ──────────────────────────────────────────────────────────────
|
|
860
|
+
/** Sections with their block ranges and word counts. */
|
|
861
|
+
outline(options = {}) {
|
|
862
|
+
return this.serial(async () => {
|
|
863
|
+
const measured = await this.currentSession.tools.call(
|
|
864
|
+
"measure_document",
|
|
865
|
+
{
|
|
866
|
+
include_sections: true,
|
|
867
|
+
section_limit: options.limit ?? 200,
|
|
868
|
+
section_offset: options.offset ?? 0
|
|
869
|
+
}
|
|
870
|
+
);
|
|
871
|
+
return measured.data;
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
/** Blocks, words, pending changes, and how faithfully it exports to Word. */
|
|
875
|
+
status() {
|
|
876
|
+
return this.serial(async () => {
|
|
877
|
+
const measured = await this.currentSession.tools.call(
|
|
878
|
+
"measure_document",
|
|
879
|
+
{}
|
|
880
|
+
);
|
|
881
|
+
return {
|
|
882
|
+
blocks: measured.data.totalBlocks,
|
|
883
|
+
words: measured.data.wordCount,
|
|
884
|
+
pendingSuggestions: this.currentSession.listSuggestions().length,
|
|
885
|
+
fidelity: this.originalPackage ? await ydocSourceFidelity2(this.currentYDoc) : null
|
|
886
|
+
};
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
/** Text-bearing blocks matching a pattern (paragraphs, headings, list
|
|
890
|
+
* items, table-cell paragraphs), with their ids. */
|
|
891
|
+
grep(pattern, options = {}) {
|
|
892
|
+
this.assertOpen();
|
|
893
|
+
const matcher = typeof pattern === "string" ? new RegExp(
|
|
894
|
+
pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
|
|
895
|
+
options.ignoreCase ? "gi" : "g"
|
|
896
|
+
) : new RegExp(
|
|
897
|
+
pattern.source,
|
|
898
|
+
pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`
|
|
899
|
+
);
|
|
900
|
+
return textBlocks(ydocToDocument2(this.currentYDoc)).map((block) => ({
|
|
901
|
+
id: block.id,
|
|
902
|
+
blockIndex: block.topIndex,
|
|
903
|
+
count: (block.text.match(matcher) ?? []).length,
|
|
904
|
+
text: block.text
|
|
905
|
+
})).filter((hit) => hit.count > 0);
|
|
906
|
+
}
|
|
907
|
+
/** What the document holds, in one glance: page setup, header and footer
|
|
908
|
+
* text with live fields, title, headings, lists, tables, review state. */
|
|
909
|
+
summary() {
|
|
910
|
+
return this.serial(
|
|
911
|
+
() => documentSummary(this.currentYDoc, this.currentSession)
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
// ── editing ──────────────────────────────────────────────────────────────
|
|
915
|
+
/** Replace every exact occurrence, block by block (tracked changes in
|
|
916
|
+
* suggesting mode). `ids` limits it to those blocks. Replacing text with
|
|
917
|
+
* itself changes nothing and is not a failure. */
|
|
918
|
+
replaceText(find, replace, options = {}) {
|
|
919
|
+
return this.serial(async () => {
|
|
920
|
+
if (!find) {
|
|
921
|
+
throw new ReviseError("Nothing to find: give some text.", "usage");
|
|
922
|
+
}
|
|
923
|
+
const targets = textBlocks(ydocToDocument2(this.currentYDoc)).filter(
|
|
924
|
+
(block) => block.text.includes(find) && (!options.ids || options.ids.includes(block.id))
|
|
925
|
+
);
|
|
926
|
+
if (find === replace) {
|
|
927
|
+
return {
|
|
928
|
+
replaced: 0,
|
|
929
|
+
blocks: targets.map((t) => t.id),
|
|
930
|
+
changeIds: [],
|
|
931
|
+
failures: []
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
const failures = [];
|
|
935
|
+
const changeIds = [];
|
|
936
|
+
let replaced = 0;
|
|
937
|
+
for (const target of targets) {
|
|
938
|
+
await this.currentSession.tools.executeDynamic("read_specific_blocks", {
|
|
939
|
+
block_ids: [target.id],
|
|
940
|
+
context_notes: "N/A"
|
|
941
|
+
});
|
|
942
|
+
const result = await this.currentSession.tools.executeDynamic(
|
|
943
|
+
"replace",
|
|
944
|
+
{
|
|
945
|
+
id: target.id,
|
|
946
|
+
replacements: [{ find, replace, occurrence: "all" }],
|
|
947
|
+
context_notes: "N/A"
|
|
948
|
+
}
|
|
949
|
+
);
|
|
950
|
+
if ("error" in result) {
|
|
951
|
+
if (!/^No changes detected/i.test(result.error.message)) {
|
|
952
|
+
failures.push({
|
|
953
|
+
blockId: target.id,
|
|
954
|
+
code: result.error.code,
|
|
955
|
+
message: result.error.message
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
} else {
|
|
959
|
+
replaced += target.text.split(find).length - 1;
|
|
960
|
+
changeIds.push(...result.value.suggestionIds ?? []);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return {
|
|
964
|
+
replaced,
|
|
965
|
+
blocks: targets.map((t) => t.id),
|
|
966
|
+
changeIds,
|
|
967
|
+
failures
|
|
968
|
+
};
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
// ── review ───────────────────────────────────────────────────────────────
|
|
972
|
+
/** Pending tracked changes with where they are and what they do. */
|
|
973
|
+
changes(filter = {}) {
|
|
974
|
+
return this.serial(() => this.filteredChanges(filter));
|
|
975
|
+
}
|
|
976
|
+
async filteredChanges(filter = {}) {
|
|
977
|
+
return (await describeChanges(this.currentYDoc, this.currentSession)).filter((c) => filter.section ? inSection(c, filter.section) : true).filter(
|
|
978
|
+
(c) => filter.exceptSection ? !inSection(c, filter.exceptSection) : true
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
/** The tracked changes a selection would settle, without settling them. */
|
|
982
|
+
preview(selection) {
|
|
983
|
+
return this.serial(async () => {
|
|
984
|
+
const changes = await this.filteredChanges();
|
|
985
|
+
const ids = await this.selectedIds(selection, changes);
|
|
986
|
+
return ids === "all" ? changes : changes.filter((c) => ids.includes(c.id));
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
accept(selection) {
|
|
990
|
+
return this.serial(() => this.settle(selection, true));
|
|
991
|
+
}
|
|
992
|
+
reject(selection) {
|
|
993
|
+
return this.serial(() => this.settle(selection, false));
|
|
994
|
+
}
|
|
995
|
+
async selectedIds(selection, known) {
|
|
996
|
+
const { section, exceptSection, all } = selection;
|
|
997
|
+
const ids = selection.ids ?? [];
|
|
998
|
+
const kinds = [
|
|
999
|
+
ids.length > 0,
|
|
1000
|
+
Boolean(all),
|
|
1001
|
+
Boolean(section || exceptSection)
|
|
1002
|
+
];
|
|
1003
|
+
if (kinds.filter(Boolean).length !== 1) {
|
|
1004
|
+
throw new ReviseError(
|
|
1005
|
+
"Select tracked changes one way: ids, all, or a section filter (section/exceptSection).",
|
|
1006
|
+
"invalid_selection"
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
if (section || exceptSection) {
|
|
1010
|
+
const changes = known ?? await this.filteredChanges();
|
|
1011
|
+
return changes.filter((c) => section ? inSection(c, section) : true).filter((c) => exceptSection ? !inSection(c, exceptSection) : true).map((c) => c.id);
|
|
1012
|
+
}
|
|
1013
|
+
return all ? "all" : ids;
|
|
1014
|
+
}
|
|
1015
|
+
async settle(selection, accept) {
|
|
1016
|
+
const ids = await this.selectedIds(selection);
|
|
1017
|
+
const session = this.currentSession;
|
|
1018
|
+
if (ids === "all") {
|
|
1019
|
+
return accept ? session.acceptAllSuggestions() : session.rejectAllSuggestions();
|
|
1020
|
+
}
|
|
1021
|
+
if (!ids.length) return { resolved: [], missing: [], unresolved: [] };
|
|
1022
|
+
return accept ? session.acceptSuggestions(ids) : session.rejectSuggestions(ids);
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* What the pending tracked changes do to the text (before → after), or
|
|
1026
|
+
* what changed since `baseline` (a snapshot).
|
|
1027
|
+
*/
|
|
1028
|
+
diff(baseline) {
|
|
1029
|
+
return this.serial(async () => {
|
|
1030
|
+
let before;
|
|
1031
|
+
if (baseline) {
|
|
1032
|
+
before = decodeYDoc(decodeSnapshot(baseline).state);
|
|
1033
|
+
} else {
|
|
1034
|
+
before = decodeYDoc(encodeYDoc(this.currentYDoc));
|
|
1035
|
+
const copy = await createServerDocumentSession(before, {
|
|
1036
|
+
documentId: `${this.settings.documentId}-diff`
|
|
1037
|
+
});
|
|
1038
|
+
copy.rejectAllSuggestions();
|
|
1039
|
+
copy.dispose();
|
|
1040
|
+
}
|
|
1041
|
+
return textDiff(
|
|
1042
|
+
await markdownOf(before),
|
|
1043
|
+
await markdownOf(this.currentYDoc)
|
|
1044
|
+
);
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
// ── output ───────────────────────────────────────────────────────────────
|
|
1048
|
+
/** Write the document in a format. Reports what the output actually
|
|
1049
|
+
* contains: its pending changes, and for .docx how much of the original
|
|
1050
|
+
* file was carried over byte for byte. */
|
|
1051
|
+
export(options) {
|
|
1052
|
+
return this.serial(async () => {
|
|
1053
|
+
options = { ...options, format: exportFormat(options.format) };
|
|
1054
|
+
const mode = options.format === "docx" ? options.mode ?? "review" : "final";
|
|
1055
|
+
const pending = this.currentSession.listSuggestions().length;
|
|
1056
|
+
const { bytes, outcome } = await exportYDoc(
|
|
1057
|
+
this.currentYDoc,
|
|
1058
|
+
this.originalPackage,
|
|
1059
|
+
{ title: this.title(), ...options, mode }
|
|
1060
|
+
);
|
|
1061
|
+
const warnings = [];
|
|
1062
|
+
if (options.format !== "docx" && (options.mode ?? "review") === "review" && pending) {
|
|
1063
|
+
warnings.push(
|
|
1064
|
+
`A .${options.format === "markdown" ? "md" : options.format} file can't hold tracked changes: it shows the ${pending} pending change${pending === 1 ? "" : "s"} accepted.`
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
if (outcome?.method === "regenerated") {
|
|
1068
|
+
if (outcome.fallbackCode === "no-original-package") {
|
|
1069
|
+
warnings.push(
|
|
1070
|
+
"The original Word file isn't available (e.g. restored from a bare Y.js state), so the whole .docx was written fresh."
|
|
1071
|
+
);
|
|
1072
|
+
} else if (outcome.fallbackCode !== "not-from-docx") {
|
|
1073
|
+
warnings.push(
|
|
1074
|
+
`Couldn't carry untouched content over from the original Word file (${outcome.fallbackCode}); the whole .docx was written fresh.`
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return {
|
|
1079
|
+
bytes,
|
|
1080
|
+
format: options.format,
|
|
1081
|
+
mode,
|
|
1082
|
+
pendingTrackedChanges: mode === "final" ? 0 : pending,
|
|
1083
|
+
fidelity: outcome,
|
|
1084
|
+
warnings
|
|
1085
|
+
};
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
/** Pages as images, laid out exactly as the editor lays them out. */
|
|
1089
|
+
renderPages(options = {}) {
|
|
1090
|
+
return this.serial(async () => {
|
|
1091
|
+
try {
|
|
1092
|
+
const { renderPages } = await import("./render-QD5VWKSV.js");
|
|
1093
|
+
return await renderPages(
|
|
1094
|
+
this.currentYDoc,
|
|
1095
|
+
this.settings.documentId,
|
|
1096
|
+
options
|
|
1097
|
+
);
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
if (error instanceof ReviseError) throw error;
|
|
1100
|
+
throw new ReviseError(
|
|
1101
|
+
`Rendering failed: ${error.message}`,
|
|
1102
|
+
"render_failed",
|
|
1103
|
+
{ cause: error }
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
async function seedYDoc(ydoc, source) {
|
|
1110
|
+
const { bytes, filename } = await readSource(source);
|
|
1111
|
+
assertReadable(filename);
|
|
1112
|
+
return seedYDocFromFile(ydoc, bytes, filename);
|
|
1113
|
+
}
|
|
1114
|
+
async function compare(originalSource, revisedSource, options = {}) {
|
|
1115
|
+
const [originalFile, revisedFile] = await Promise.all([
|
|
1116
|
+
readSource(originalSource),
|
|
1117
|
+
readSource(revisedSource)
|
|
1118
|
+
]);
|
|
1119
|
+
assertReadable(originalFile.filename);
|
|
1120
|
+
assertReadable(revisedFile.filename);
|
|
1121
|
+
const [original, revised] = await Promise.all([
|
|
1122
|
+
parseDocument2(originalFile.bytes, originalFile.filename),
|
|
1123
|
+
parseDocument2(revisedFile.bytes, revisedFile.filename)
|
|
1124
|
+
]);
|
|
1125
|
+
const author = options.author ?? baseName(revisedFile.filename);
|
|
1126
|
+
const { compareDocuments } = await engineModules2.compare();
|
|
1127
|
+
const comparison = compareDocuments(original, revised, {
|
|
1128
|
+
authorName: author
|
|
1129
|
+
});
|
|
1130
|
+
if (!comparison) {
|
|
1131
|
+
throw new ReviseError(
|
|
1132
|
+
"These documents are too large to compare.",
|
|
1133
|
+
"too_large"
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
const ydoc = createReviseYDoc2();
|
|
1137
|
+
seedDocRootWithClaim2(ydoc, comparison.doc);
|
|
1138
|
+
const isDocx = /\.docx$/i.test(revisedFile.filename);
|
|
1139
|
+
if (comparison.doc.docxSource?.sourceHash && isDocx) {
|
|
1140
|
+
const { storeDocxCarrier } = await engineModules2.docxGraft();
|
|
1141
|
+
await storeDocxCarrier(ydoc, revisedFile.bytes, {
|
|
1142
|
+
hash: comparison.doc.docxSource.sourceHash
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
const document = await ReviseDocument.restore(
|
|
1146
|
+
encodeSnapshot2(
|
|
1147
|
+
{
|
|
1148
|
+
documentId: options.documentId ?? `revise-compare-${Date.now()}`,
|
|
1149
|
+
mode: options.mode ?? "suggesting",
|
|
1150
|
+
author,
|
|
1151
|
+
title: stem(revisedFile.filename)
|
|
1152
|
+
},
|
|
1153
|
+
encodeYDoc(ydoc),
|
|
1154
|
+
isDocx ? revisedFile.bytes : null
|
|
1155
|
+
),
|
|
1156
|
+
options.images ? { images: options.images } : {}
|
|
1157
|
+
);
|
|
1158
|
+
const plainOf = async (y) => new TextDecoder().decode(await exportDocument(y, { format: "txt", mode: "final" })).replace(/\s+/g, " ").trim();
|
|
1159
|
+
const redline = isDocx ? await fileToYDoc(
|
|
1160
|
+
(await exportSessionDocx(document.ydoc, document.package)).bytes,
|
|
1161
|
+
"redline.docx"
|
|
1162
|
+
) : decodeYDoc(encodeYDoc(document.ydoc));
|
|
1163
|
+
const rejected = decodeYDoc(encodeYDoc(redline));
|
|
1164
|
+
const rejecter = await createServerDocumentSession(rejected, {
|
|
1165
|
+
documentId: `${document.documentId}-verify`
|
|
1166
|
+
});
|
|
1167
|
+
rejecter.rejectAllSuggestions();
|
|
1168
|
+
rejecter.dispose();
|
|
1169
|
+
const [acceptedText, rejectedText, revisedText, originalText] = await Promise.all([
|
|
1170
|
+
plainOf(redline),
|
|
1171
|
+
plainOf(rejected),
|
|
1172
|
+
plainOf(await fileToYDoc(revisedFile.bytes, revisedFile.filename)),
|
|
1173
|
+
plainOf(await fileToYDoc(originalFile.bytes, originalFile.filename))
|
|
1174
|
+
]);
|
|
1175
|
+
return {
|
|
1176
|
+
document,
|
|
1177
|
+
stats: comparison.stats,
|
|
1178
|
+
untrackedRegions: comparison.untrackedRegions,
|
|
1179
|
+
verified: {
|
|
1180
|
+
acceptAllGivesRevised: acceptedText === revisedText,
|
|
1181
|
+
rejectAllGivesOriginal: rejectedText === originalText
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
async function convert(source, format, options = {}) {
|
|
1186
|
+
format = exportFormat(format);
|
|
1187
|
+
const { bytes, filename } = await readSource(source);
|
|
1188
|
+
assertReadable(filename);
|
|
1189
|
+
const ydoc = await importYDoc(bytes, filename);
|
|
1190
|
+
return format === "pdf" ? exportPdf(ydoc, stem(filename)) : exportDocument(ydoc, {
|
|
1191
|
+
format,
|
|
1192
|
+
mode: options.mode ?? "review",
|
|
1193
|
+
title: stem(filename)
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
var EXPORT_FORMATS = ["docx", "pdf", "markdown", "html", "txt"];
|
|
1197
|
+
function exportFormat(format) {
|
|
1198
|
+
const name = String(format).toLowerCase();
|
|
1199
|
+
if (name === "md") return "markdown";
|
|
1200
|
+
if (!EXPORT_FORMATS.includes(name)) {
|
|
1201
|
+
throw new ReviseError(
|
|
1202
|
+
`Unknown export format "${format}". Use one of: ${EXPORT_FORMATS.join(", ")}.`,
|
|
1203
|
+
"unsupported_format"
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
return name;
|
|
1207
|
+
}
|
|
1208
|
+
function textBlocks(doc) {
|
|
1209
|
+
const out = [];
|
|
1210
|
+
const textOf = (node) => (node.children ?? []).map((child) => {
|
|
1211
|
+
const c = child;
|
|
1212
|
+
return c.type === "text" && !c.marks?.deletion ? c.text ?? "" : "";
|
|
1213
|
+
}).join("");
|
|
1214
|
+
doc.children.forEach((block, topIndex) => {
|
|
1215
|
+
const visit = (node) => {
|
|
1216
|
+
const n = node;
|
|
1217
|
+
if (!n || typeof n !== "object") return;
|
|
1218
|
+
if ((n.type === "paragraph" || n.type === "list-item") && n.id) {
|
|
1219
|
+
out.push({ id: n.id, text: textOf(n), topIndex });
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
n.children?.forEach(visit);
|
|
1223
|
+
};
|
|
1224
|
+
visit(block);
|
|
1225
|
+
});
|
|
1226
|
+
return out;
|
|
1227
|
+
}
|
|
1228
|
+
async function describeChanges(ydoc, session) {
|
|
1229
|
+
const records = session.listSuggestions();
|
|
1230
|
+
const doc = ydocToDocument2(ydoc);
|
|
1231
|
+
const measured = await session.tools.call("measure_document", {
|
|
1232
|
+
include_sections: true,
|
|
1233
|
+
section_limit: 1e3
|
|
1234
|
+
});
|
|
1235
|
+
const sections = measured.data.sections ?? [];
|
|
1236
|
+
const positionOf = new Map(
|
|
1237
|
+
doc.children.map((block, index) => [block.id, index])
|
|
1238
|
+
);
|
|
1239
|
+
const starts = sections.map((s) => ({ title: s.title, at: positionOf.get(s.startBlockId) })).filter((s) => s.at !== void 0).sort((a, b) => a.at - b.at);
|
|
1240
|
+
const sectionAt = (index) => {
|
|
1241
|
+
let title;
|
|
1242
|
+
for (const start of starts) {
|
|
1243
|
+
if (start.at > index) break;
|
|
1244
|
+
title = start.title;
|
|
1245
|
+
}
|
|
1246
|
+
return title;
|
|
1247
|
+
};
|
|
1248
|
+
const found = /* @__PURE__ */ new Map();
|
|
1249
|
+
doc.children.forEach((block, blockIndex) => {
|
|
1250
|
+
const visit = (node) => {
|
|
1251
|
+
if (!node || typeof node !== "object") return;
|
|
1252
|
+
const n = node;
|
|
1253
|
+
const id = n.marks?.suggestionId ?? n.suggestionId ?? n.structuralSuggestionId;
|
|
1254
|
+
if (id) {
|
|
1255
|
+
const entry = found.get(id) ?? {
|
|
1256
|
+
blockIndex,
|
|
1257
|
+
blockId: block.id ?? "",
|
|
1258
|
+
deleted: "",
|
|
1259
|
+
inserted: ""
|
|
1260
|
+
};
|
|
1261
|
+
if (n.type === "text") {
|
|
1262
|
+
if (n.marks?.deletion) entry.deleted += n.text ?? "";
|
|
1263
|
+
else if (n.marks?.insertion) entry.inserted += n.text ?? "";
|
|
1264
|
+
}
|
|
1265
|
+
found.set(id, entry);
|
|
1266
|
+
}
|
|
1267
|
+
n.children?.forEach(visit);
|
|
1268
|
+
};
|
|
1269
|
+
visit(block);
|
|
1270
|
+
});
|
|
1271
|
+
return records.map((record) => {
|
|
1272
|
+
const at = found.get(record.id);
|
|
1273
|
+
const blockIndex = at?.blockIndex ?? (record.blockId ? positionOf.get(record.blockId) : void 0) ?? (record.tableId ? positionOf.get(record.tableId) : void 0);
|
|
1274
|
+
return {
|
|
1275
|
+
id: record.id,
|
|
1276
|
+
type: record.type ?? "text",
|
|
1277
|
+
author: record.agentName ?? record.authorType ?? "unknown",
|
|
1278
|
+
...at?.blockId || record.blockId ? { blockId: at?.blockId || record.blockId } : {},
|
|
1279
|
+
...blockIndex !== void 0 ? { blockIndex, section: sectionAt(blockIndex) } : {},
|
|
1280
|
+
deleted: at?.deleted ?? "",
|
|
1281
|
+
inserted: at?.inserted ?? ""
|
|
1282
|
+
};
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
function inSection(entry, query) {
|
|
1286
|
+
if (!entry.section) return false;
|
|
1287
|
+
const title = entry.section.trim().toLowerCase();
|
|
1288
|
+
const q = query.trim().toLowerCase().replace(/\.$/, "");
|
|
1289
|
+
if (/^[\d.]+$/.test(q)) {
|
|
1290
|
+
return new RegExp(`^${q.replace(/\./g, "\\.")}(?:[.\\s)]|$)`).test(title);
|
|
1291
|
+
}
|
|
1292
|
+
return title.includes(q);
|
|
1293
|
+
}
|
|
1294
|
+
async function markdownOf(ydoc) {
|
|
1295
|
+
const bytes = await exportDocument(ydoc, {
|
|
1296
|
+
format: "markdown",
|
|
1297
|
+
mode: "final"
|
|
1298
|
+
});
|
|
1299
|
+
return new TextDecoder().decode(bytes);
|
|
1300
|
+
}
|
|
1301
|
+
function lcsPairs(a, b) {
|
|
1302
|
+
let head = 0;
|
|
1303
|
+
while (head < a.length && head < b.length && a[head] === b[head]) head += 1;
|
|
1304
|
+
let tail = 0;
|
|
1305
|
+
while (tail < a.length - head && tail < b.length - head && a[a.length - 1 - tail] === b[b.length - 1 - tail]) {
|
|
1306
|
+
tail += 1;
|
|
1307
|
+
}
|
|
1308
|
+
const pairs = [];
|
|
1309
|
+
for (let k = 0; k < head; k += 1) pairs.push([k, k]);
|
|
1310
|
+
const midA = a.slice(head, a.length - tail);
|
|
1311
|
+
const midB = b.slice(head, b.length - tail);
|
|
1312
|
+
if (midA.length * midB.length <= 4e6) {
|
|
1313
|
+
for (const [i, j] of lcsTable(midA, midB)) pairs.push([i + head, j + head]);
|
|
1314
|
+
}
|
|
1315
|
+
for (let k = tail; k > 0; k -= 1) {
|
|
1316
|
+
pairs.push([a.length - k, b.length - k]);
|
|
1317
|
+
}
|
|
1318
|
+
return pairs;
|
|
1319
|
+
}
|
|
1320
|
+
function lcsTable(a, b) {
|
|
1321
|
+
const n = a.length;
|
|
1322
|
+
const m = b.length;
|
|
1323
|
+
const table = new Uint32Array((n + 1) * (m + 1));
|
|
1324
|
+
for (let i2 = n - 1; i2 >= 0; i2 -= 1) {
|
|
1325
|
+
for (let j2 = m - 1; j2 >= 0; j2 -= 1) {
|
|
1326
|
+
table[i2 * (m + 1) + j2] = a[i2] === b[j2] ? table[(i2 + 1) * (m + 1) + j2 + 1] + 1 : Math.max(table[(i2 + 1) * (m + 1) + j2], table[i2 * (m + 1) + j2 + 1]);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
const pairs = [];
|
|
1330
|
+
let i = 0;
|
|
1331
|
+
let j = 0;
|
|
1332
|
+
while (i < n && j < m) {
|
|
1333
|
+
if (a[i] === b[j]) {
|
|
1334
|
+
pairs.push([i, j]);
|
|
1335
|
+
i += 1;
|
|
1336
|
+
j += 1;
|
|
1337
|
+
} else if (table[(i + 1) * (m + 1) + j] >= table[i * (m + 1) + j + 1]) {
|
|
1338
|
+
i += 1;
|
|
1339
|
+
} else {
|
|
1340
|
+
j += 1;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return pairs;
|
|
1344
|
+
}
|
|
1345
|
+
function wordDiff(before, after) {
|
|
1346
|
+
const a = before.split(/(\s+)/);
|
|
1347
|
+
const b = after.split(/(\s+)/);
|
|
1348
|
+
if (a.length * b.length > 4e6) return `[-${before}-]{+${after}+}`;
|
|
1349
|
+
const pairs = lcsPairs(a, b);
|
|
1350
|
+
let i = 0;
|
|
1351
|
+
let j = 0;
|
|
1352
|
+
let output = "";
|
|
1353
|
+
const flush = (ai, bj) => {
|
|
1354
|
+
const removed = a.slice(i, ai).join("");
|
|
1355
|
+
const added = b.slice(j, bj).join("");
|
|
1356
|
+
if (removed) output += `[-${removed}-]`;
|
|
1357
|
+
if (added) output += `{+${added}+}`;
|
|
1358
|
+
};
|
|
1359
|
+
for (const [ai, bj] of pairs) {
|
|
1360
|
+
flush(ai, bj);
|
|
1361
|
+
output += a[ai];
|
|
1362
|
+
i = ai + 1;
|
|
1363
|
+
j = bj + 1;
|
|
1364
|
+
}
|
|
1365
|
+
flush(a.length, b.length);
|
|
1366
|
+
return output;
|
|
1367
|
+
}
|
|
1368
|
+
function textDiff(before, after) {
|
|
1369
|
+
const split = (s) => s.split(/\n+/).filter((line) => line.trim());
|
|
1370
|
+
const a = split(before);
|
|
1371
|
+
const b = split(after);
|
|
1372
|
+
const pairs = lcsPairs(a, b);
|
|
1373
|
+
const hunks = [];
|
|
1374
|
+
let changed = 0;
|
|
1375
|
+
let i = 0;
|
|
1376
|
+
let j = 0;
|
|
1377
|
+
const flush = (ai, bj) => {
|
|
1378
|
+
const removed = a.slice(i, ai);
|
|
1379
|
+
const added = b.slice(j, bj);
|
|
1380
|
+
const paired = Math.min(removed.length, added.length);
|
|
1381
|
+
for (let k = 0; k < paired; k += 1) {
|
|
1382
|
+
hunks.push(`~ ${wordDiff(removed[k], added[k])}`);
|
|
1383
|
+
changed += 1;
|
|
1384
|
+
}
|
|
1385
|
+
for (const line of removed.slice(paired)) {
|
|
1386
|
+
hunks.push(`- ${line}`);
|
|
1387
|
+
changed += 1;
|
|
1388
|
+
}
|
|
1389
|
+
for (const line of added.slice(paired)) {
|
|
1390
|
+
hunks.push(`+ ${line}`);
|
|
1391
|
+
changed += 1;
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
for (const [ai, bj] of pairs) {
|
|
1395
|
+
flush(ai, bj);
|
|
1396
|
+
i = ai + 1;
|
|
1397
|
+
j = bj + 1;
|
|
1398
|
+
}
|
|
1399
|
+
flush(a.length, b.length);
|
|
1400
|
+
return { text: hunks.join("\n"), changed };
|
|
1401
|
+
}
|
|
1402
|
+
async function documentSummary(ydoc, session) {
|
|
1403
|
+
const doc = ydocToDocument2(ydoc);
|
|
1404
|
+
const { resolvePageLayoutSettings, getPageDimensionsPx } = await engineModules2.pageLayout();
|
|
1405
|
+
const layout = resolvePageLayoutSettings(doc.pageLayout);
|
|
1406
|
+
const { pageWidth, pageHeight } = getPageDimensionsPx(layout);
|
|
1407
|
+
const inches = (px) => Number((px / 96).toFixed(2));
|
|
1408
|
+
const margins = [
|
|
1409
|
+
layout.marginTopPx,
|
|
1410
|
+
layout.marginRightPx,
|
|
1411
|
+
layout.marginBottomPx,
|
|
1412
|
+
layout.marginLeftPx
|
|
1413
|
+
].map(inches);
|
|
1414
|
+
const measured = await session.tools.call("measure_document", {});
|
|
1415
|
+
const textWithFields = (node) => {
|
|
1416
|
+
const n = node;
|
|
1417
|
+
if (n?.type === "text") {
|
|
1418
|
+
const field = n.marks?.field?.toLowerCase();
|
|
1419
|
+
if (field) {
|
|
1420
|
+
return field === "numpages" || field === "pages" ? "{PAGES}" : `{${field.toUpperCase()}}`;
|
|
1421
|
+
}
|
|
1422
|
+
return n.text ?? "";
|
|
1423
|
+
}
|
|
1424
|
+
return (n?.children ?? []).map(textWithFields).join(n?.type === "table-row" ? " | " : "");
|
|
1425
|
+
};
|
|
1426
|
+
const chrome = (doc.chrome ?? []).filter((block) => block.type === "header-footer").map((block) => {
|
|
1427
|
+
const b = block;
|
|
1428
|
+
return {
|
|
1429
|
+
side: b.side,
|
|
1430
|
+
slot: b.slot ?? "default",
|
|
1431
|
+
text: textWithFields(block).replace(/\s+/g, " ").trim()
|
|
1432
|
+
};
|
|
1433
|
+
}).filter((c) => c.text);
|
|
1434
|
+
let title;
|
|
1435
|
+
const headings = {};
|
|
1436
|
+
const lists = [];
|
|
1437
|
+
const tables = [];
|
|
1438
|
+
let images = 0;
|
|
1439
|
+
for (const block of doc.children) {
|
|
1440
|
+
if (block.type === "paragraph") {
|
|
1441
|
+
if (block.styleId === "Title" && title === void 0) {
|
|
1442
|
+
title = textWithFields(block);
|
|
1443
|
+
}
|
|
1444
|
+
if (block.heading) {
|
|
1445
|
+
headings[`H${block.heading}`] = (headings[`H${block.heading}`] ?? 0) + 1;
|
|
1446
|
+
}
|
|
1447
|
+
} else if (block.type === "list") {
|
|
1448
|
+
lists.push(block.children.length);
|
|
1449
|
+
} else if (block.type === "table") {
|
|
1450
|
+
tables.push(
|
|
1451
|
+
`${block.children.length}\xD7${block.children[0]?.children.length ?? 0}`
|
|
1452
|
+
);
|
|
1453
|
+
} else if (block.type === "image") {
|
|
1454
|
+
images += 1;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
const pending = session.listSuggestions().length;
|
|
1458
|
+
const data = {
|
|
1459
|
+
pages: measured.data.pageCount,
|
|
1460
|
+
words: measured.data.wordCount,
|
|
1461
|
+
pageSize: layout.pageSize,
|
|
1462
|
+
orientation: layout.orientation,
|
|
1463
|
+
pageInches: [inches(pageWidth), inches(pageHeight)],
|
|
1464
|
+
marginsInches: margins,
|
|
1465
|
+
title,
|
|
1466
|
+
headings,
|
|
1467
|
+
lists,
|
|
1468
|
+
tables,
|
|
1469
|
+
images,
|
|
1470
|
+
headersFooters: chrome,
|
|
1471
|
+
pendingTrackedChanges: pending
|
|
1472
|
+
};
|
|
1473
|
+
const marginText = margins.every((m) => m === margins[0]) ? `${margins[0]} in` : `${margins.join("/")} in (top/right/bottom/left)`;
|
|
1474
|
+
const parts = [
|
|
1475
|
+
title ? `title "${title}"` : "",
|
|
1476
|
+
Object.keys(headings).length ? `headings ${Object.entries(headings).map(([level, n]) => `${n}\xD7${level}`).join(", ")}` : "",
|
|
1477
|
+
lists.length ? `${lists.length} list${lists.length > 1 ? "s" : ""} (${lists.join(", ")} items)` : "",
|
|
1478
|
+
tables.length ? `${tables.length} table${tables.length > 1 ? "s" : ""} (${tables.join(", ")})` : "",
|
|
1479
|
+
images ? `${images} image${images > 1 ? "s" : ""}` : ""
|
|
1480
|
+
].filter(Boolean);
|
|
1481
|
+
const text = [
|
|
1482
|
+
`${data.pages ?? "?"} page${data.pages === 1 ? "" : "s"}, ${measured.data.wordCount} words; ${layout.pageSize} ${layout.orientation} (${inches(pageWidth)} \xD7 ${inches(pageHeight)} in), margins ${marginText}.`,
|
|
1483
|
+
...parts.length ? [`Contents: ${parts.join("; ")}.`] : [],
|
|
1484
|
+
...chrome.map(
|
|
1485
|
+
(c) => `${c.side[0].toUpperCase()}${c.side.slice(1)}${c.slot === "default" ? "" : ` (${c.slot})`}: ${c.text}`
|
|
1486
|
+
),
|
|
1487
|
+
...pending ? [`Pending tracked changes: ${pending}.`] : []
|
|
1488
|
+
].join("\n");
|
|
1489
|
+
return { text, data };
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// src/embed-sdk/lib/agent.ts
|
|
1493
|
+
function formatToolResult(result, maxChars = 1e5) {
|
|
1494
|
+
if ("error" in result) {
|
|
1495
|
+
return {
|
|
1496
|
+
text: `error ${result.error.code}: ${result.error.message}`,
|
|
1497
|
+
isError: true
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
const value = result.value;
|
|
1501
|
+
const lines = [];
|
|
1502
|
+
if (value.message) lines.push(value.message);
|
|
1503
|
+
if (value.suggestionIds?.length) {
|
|
1504
|
+
lines.push(`Tracked changes: ${value.suggestionIds.join(", ")}`);
|
|
1505
|
+
}
|
|
1506
|
+
if (value.data !== null && value.data !== void 0) {
|
|
1507
|
+
const data = JSON.stringify(value.data);
|
|
1508
|
+
if (data !== "{}" && data !== "[]") {
|
|
1509
|
+
lines.push(
|
|
1510
|
+
`data: ${data.length <= 4e3 ? data : compactData(value.data)}`
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
if (value.context?.html) lines.push("", value.context.html);
|
|
1515
|
+
let text = lines.join("\n") || "Done.";
|
|
1516
|
+
if (text.length > maxChars) {
|
|
1517
|
+
text = `${text.slice(0, maxChars)}
|
|
1518
|
+
\u2026 (result cut at ${maxChars} characters; read fewer blocks at a time, e.g. a later index or specific ids)`;
|
|
1519
|
+
}
|
|
1520
|
+
return { text, isError: false };
|
|
1521
|
+
}
|
|
1522
|
+
function compactData(data) {
|
|
1523
|
+
const summarize = (value, depth) => {
|
|
1524
|
+
if (Array.isArray(value)) {
|
|
1525
|
+
return value.length <= 5 && depth < 2 ? value.map((item) => summarize(item, depth + 1)) : `[${value.length} items]`;
|
|
1526
|
+
}
|
|
1527
|
+
if (value && typeof value === "object") {
|
|
1528
|
+
if (depth >= 2) return "{\u2026}";
|
|
1529
|
+
return Object.fromEntries(
|
|
1530
|
+
Object.entries(value).map(([key, inner]) => [
|
|
1531
|
+
key,
|
|
1532
|
+
summarize(inner, depth + 1)
|
|
1533
|
+
])
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
return typeof value === "string" && value.length > 200 ? `${value.slice(0, 200)}\u2026` : value;
|
|
1537
|
+
};
|
|
1538
|
+
return `${JSON.stringify(summarize(data, 0))} (summarized; the full data is larger)`;
|
|
1539
|
+
}
|
|
1540
|
+
function agentTools(doc, options) {
|
|
1541
|
+
const definitions = doc.tools.definitions().filter((d) => !options.include || options.include.includes(d.name)).filter((d) => !options.exclude?.includes(d.name));
|
|
1542
|
+
const allowed = new Set(definitions.map((d) => d.name));
|
|
1543
|
+
const tools = definitions.map((d) => {
|
|
1544
|
+
switch (options.provider) {
|
|
1545
|
+
case "anthropic":
|
|
1546
|
+
return {
|
|
1547
|
+
name: d.name,
|
|
1548
|
+
description: d.description,
|
|
1549
|
+
input_schema: { ...d.inputSchema, type: "object" }
|
|
1550
|
+
};
|
|
1551
|
+
case "openai":
|
|
1552
|
+
return {
|
|
1553
|
+
type: "function",
|
|
1554
|
+
function: {
|
|
1555
|
+
name: d.name,
|
|
1556
|
+
description: d.description,
|
|
1557
|
+
parameters: d.inputSchema
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
default:
|
|
1561
|
+
return {
|
|
1562
|
+
type: "function",
|
|
1563
|
+
name: d.name,
|
|
1564
|
+
description: d.description,
|
|
1565
|
+
parameters: d.inputSchema,
|
|
1566
|
+
strict: false
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
const run = async (name, rawInput) => {
|
|
1571
|
+
if (!allowed.has(name)) {
|
|
1572
|
+
return {
|
|
1573
|
+
text: `error unknown_tool: There is no tool named "${name}".`,
|
|
1574
|
+
isError: true
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
let input = rawInput;
|
|
1578
|
+
if (typeof rawInput === "string") {
|
|
1579
|
+
try {
|
|
1580
|
+
input = rawInput.trim() ? JSON.parse(rawInput) : {};
|
|
1581
|
+
} catch (error) {
|
|
1582
|
+
return {
|
|
1583
|
+
text: `error invalid_json: The arguments are not valid JSON (${error.message}).`,
|
|
1584
|
+
isError: true
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
1589
|
+
return {
|
|
1590
|
+
text: "error invalid_arguments: Tool arguments must be a JSON object.",
|
|
1591
|
+
isError: true
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
return formatToolResult(
|
|
1595
|
+
await doc.tools.run(name, input),
|
|
1596
|
+
options.maxResultChars
|
|
1597
|
+
);
|
|
1598
|
+
};
|
|
1599
|
+
const handle = async (call) => {
|
|
1600
|
+
switch (options.provider) {
|
|
1601
|
+
case "anthropic": {
|
|
1602
|
+
const c = call;
|
|
1603
|
+
const { text, isError } = await run(c.name, c.input);
|
|
1604
|
+
return {
|
|
1605
|
+
type: "tool_result",
|
|
1606
|
+
tool_use_id: c.id,
|
|
1607
|
+
content: text,
|
|
1608
|
+
...isError ? { is_error: true } : {}
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
case "openai": {
|
|
1612
|
+
const c = call;
|
|
1613
|
+
const { text } = await run(c.function.name, c.function.arguments);
|
|
1614
|
+
return {
|
|
1615
|
+
role: "tool",
|
|
1616
|
+
tool_call_id: c.id,
|
|
1617
|
+
content: text
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
default: {
|
|
1621
|
+
const c = call;
|
|
1622
|
+
const { text } = await run(c.name, c.arguments);
|
|
1623
|
+
return {
|
|
1624
|
+
type: "function_call_output",
|
|
1625
|
+
call_id: c.call_id,
|
|
1626
|
+
output: text
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1631
|
+
return {
|
|
1632
|
+
tools,
|
|
1633
|
+
handle,
|
|
1634
|
+
version: () => doc.version(),
|
|
1635
|
+
reset: () => doc.resetToolContext()
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
export {
|
|
1640
|
+
relearn,
|
|
1641
|
+
READABLE,
|
|
1642
|
+
formatForPath,
|
|
1643
|
+
exportYDoc,
|
|
1644
|
+
ReviseDocument,
|
|
1645
|
+
seedYDoc,
|
|
1646
|
+
compare,
|
|
1647
|
+
convert,
|
|
1648
|
+
formatToolResult,
|
|
1649
|
+
agentTools
|
|
1650
|
+
};
|