@attentionform/astra-mcp-server 0.4.2 → 0.5.1
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/README.md +26 -3
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1781 -498
- package/dist/lib/mcp/handlers/history.d.ts +5 -0
- package/dist/lib/mcp/handlers/open-inspect.d.ts +17 -0
- package/dist/lib/mcp/handlers/original.d.ts +25 -0
- package/dist/lib/mcp/handlers/parity.d.ts +6 -0
- package/dist/lib/mcp/schemas.d.ts +70 -5
- package/dist/lib/mcp/tool-names.d.ts +1 -1
- package/dist/lib/mcp-tool-adapter.d.ts +1 -0
- package/dist/lib/session-store.d.ts +33 -3
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -45,7 +45,7 @@ __export(index_exports, {
|
|
|
45
45
|
takeDirFlag: () => takeDirFlag
|
|
46
46
|
});
|
|
47
47
|
module.exports = __toCommonJS(index_exports);
|
|
48
|
-
var
|
|
48
|
+
var path24 = __toESM(require("path"));
|
|
49
49
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
50
50
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
51
51
|
var import_types2 = require("@modelcontextprotocol/sdk/types.js");
|
|
@@ -212,6 +212,44 @@ async function refreshAstInPlace(target, parser2, options = {}) {
|
|
|
212
212
|
return notes;
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
// ../engine/src/heading-style.ts
|
|
216
|
+
function headingLevelOfStyle(lowerStyleId) {
|
|
217
|
+
const id = lowerStyleId.replace(/\s+/g, "");
|
|
218
|
+
const numbered = /^heading([1-9])$/.exec(id);
|
|
219
|
+
if (numbered) return Number(numbered[1]);
|
|
220
|
+
if (id === "subtitle") return 2;
|
|
221
|
+
return 1;
|
|
222
|
+
}
|
|
223
|
+
function headingLevelOfStyleId(styleId) {
|
|
224
|
+
if (!styleId) return null;
|
|
225
|
+
const lower = styleId.toLowerCase();
|
|
226
|
+
if (!(lower.includes("heading") || lower.includes("title") || lower.includes("subtitle"))) return null;
|
|
227
|
+
return headingLevelOfStyle(lower);
|
|
228
|
+
}
|
|
229
|
+
function ownParagraphStyleId(paragraph) {
|
|
230
|
+
if (paragraph.tagName !== "w:p") return void 0;
|
|
231
|
+
for (let i = 0; i < paragraph.childNodes.length; i++) {
|
|
232
|
+
const pPr = paragraph.childNodes[i];
|
|
233
|
+
if (pPr.nodeType !== 1 || pPr.tagName !== "w:pPr") continue;
|
|
234
|
+
for (let k = 0; k < pPr.childNodes.length; k++) {
|
|
235
|
+
const el = pPr.childNodes[k];
|
|
236
|
+
if (el.nodeType === 1 && el.tagName === "w:pStyle") return el.getAttribute("w:val") || void 0;
|
|
237
|
+
}
|
|
238
|
+
return void 0;
|
|
239
|
+
}
|
|
240
|
+
return void 0;
|
|
241
|
+
}
|
|
242
|
+
function headingOfNode(node) {
|
|
243
|
+
const style = ownParagraphStyleId(node.xml);
|
|
244
|
+
const level = headingLevelOfStyleId(style);
|
|
245
|
+
return style !== void 0 && level !== null ? { style, level } : null;
|
|
246
|
+
}
|
|
247
|
+
function countHeadings(ast) {
|
|
248
|
+
let n = 0;
|
|
249
|
+
for (const sec of ast.sections) for (const node of sec.nodes) if (headingOfNode(node)) n += 1;
|
|
250
|
+
return n;
|
|
251
|
+
}
|
|
252
|
+
|
|
215
253
|
// ../engine/src/ooxml/revision-ids.ts
|
|
216
254
|
var REVISION_TAGS = [
|
|
217
255
|
"ins",
|
|
@@ -2003,8 +2041,8 @@ function sortChildrenBySchema(element, order) {
|
|
|
2003
2041
|
}
|
|
2004
2042
|
function generateValidParaId(seed, counter = 0) {
|
|
2005
2043
|
if (seed !== void 0 && seed !== null && seed !== "") {
|
|
2006
|
-
const
|
|
2007
|
-
const raw =
|
|
2044
|
+
const hash2 = import_crypto2.default.createHash("sha256").update(`${seed}:${counter}`).digest();
|
|
2045
|
+
const raw = hash2.readUInt32BE(0) & 2147483647;
|
|
2008
2046
|
const val2 = raw === 0 ? 1 : raw;
|
|
2009
2047
|
return val2.toString(16).toUpperCase().padStart(8, "0");
|
|
2010
2048
|
}
|
|
@@ -2905,8 +2943,8 @@ function parserRandomUnit() {
|
|
|
2905
2943
|
if (injectedParserRandom) return injectedParserRandom();
|
|
2906
2944
|
if (resolveReproducibleMode(void 0)) {
|
|
2907
2945
|
parserDeterministicSeq += 1;
|
|
2908
|
-
const
|
|
2909
|
-
return
|
|
2946
|
+
const hash2 = (parserDeterministicSeq * 2654435761 >>> 0) / 4294967296;
|
|
2947
|
+
return hash2 === 0 ? 0.5 : hash2;
|
|
2910
2948
|
}
|
|
2911
2949
|
return Math.random();
|
|
2912
2950
|
}
|
|
@@ -3033,11 +3071,12 @@ function sortContainerChildren(container, order) {
|
|
|
3033
3071
|
const after = next.filter((node) => node.nodeType === 1).map((node) => node.tagName);
|
|
3034
3072
|
return { reordered: true, before, after };
|
|
3035
3073
|
}
|
|
3036
|
-
function passSorts(doc, entries) {
|
|
3074
|
+
function passSorts(doc, entries, reorderedContainers) {
|
|
3037
3075
|
for (const { tag, order } of SORT_CONTAINERS) {
|
|
3038
3076
|
for (const container of snapshotElements(doc, tag)) {
|
|
3039
3077
|
const { reordered, before, after } = sortContainerChildren(container, order);
|
|
3040
3078
|
if (reordered) {
|
|
3079
|
+
reorderedContainers?.push(elementXPath(container));
|
|
3041
3080
|
entries.push(`Reordered ${tag} into schema order${locationSuffix(container)}: [${before.join(",")}] \u2192 [${after.join(",")}]`);
|
|
3042
3081
|
}
|
|
3043
3082
|
}
|
|
@@ -3100,6 +3139,9 @@ function normalizePartXml(partName, xml) {
|
|
|
3100
3139
|
if (entries.length === 0) return { xml, normalized: [] };
|
|
3101
3140
|
return { xml: partSerializer.serializeToString(doc), normalized: entries };
|
|
3102
3141
|
}
|
|
3142
|
+
var ORDERED_CONTAINERS = new Map(
|
|
3143
|
+
SORT_CONTAINERS.map((c) => [c.tag.slice(c.tag.indexOf(":") + 1), new Set(c.order)])
|
|
3144
|
+
);
|
|
3103
3145
|
|
|
3104
3146
|
// ../engine/src/ooxml/omml.ts
|
|
3105
3147
|
var OMML_MAX_DEPTH = 6;
|
|
@@ -4530,9 +4572,9 @@ function ommlToMathmlDisplay(xml) {
|
|
|
4530
4572
|
// ../engine/src/ooxml/validator-backend.ts
|
|
4531
4573
|
var nativeBackend = {
|
|
4532
4574
|
name: "native",
|
|
4533
|
-
async validateFile(
|
|
4575
|
+
async validateFile(path25, opts) {
|
|
4534
4576
|
const native = await import("@attentionform/ooxml-validator");
|
|
4535
|
-
return native.validateFile(
|
|
4577
|
+
return native.validateFile(path25, opts ? { officeVersion: opts.officeVersion } : void 0);
|
|
4536
4578
|
},
|
|
4537
4579
|
async validateBuffer(buf, opts) {
|
|
4538
4580
|
const native = await import("@attentionform/ooxml-validator");
|
|
@@ -4562,14 +4604,16 @@ var OoxmlValidationError = class extends Error {
|
|
|
4562
4604
|
errors;
|
|
4563
4605
|
firstError;
|
|
4564
4606
|
code = "E_VALIDATION";
|
|
4565
|
-
remedy
|
|
4607
|
+
remedy;
|
|
4566
4608
|
retryable = false;
|
|
4567
|
-
|
|
4609
|
+
/** `remedy` replaces the default text for callers that know a better next step (PDF export does). */
|
|
4610
|
+
constructor(errors, context = "Document", remedy) {
|
|
4568
4611
|
const first = errors[0] || {};
|
|
4569
4612
|
const xp = typeof first.xPath === "string" && first.xPath || typeof first.path === "string" && first.path || "unknown";
|
|
4570
4613
|
const hint = tableIndexHintForXPath(first.xPath ?? first.path);
|
|
4571
4614
|
const detail = `${first.description || "Schema error"} @ ${xp}${hint ? ` (${hint})` : ""}`;
|
|
4572
4615
|
super(`[OOXML Schema Violation] ${context} has ${errors.length} validation error(s): ${detail}`);
|
|
4616
|
+
this.remedy = remedy ?? "re-export the file from Word (or pick a valid .docx) and retry";
|
|
4573
4617
|
this.name = "OoxmlValidationError";
|
|
4574
4618
|
this.errors = errors;
|
|
4575
4619
|
this.firstError = first;
|
|
@@ -5494,10 +5538,10 @@ function mergeCorePropertiesXml(existingXml, patch, fallback) {
|
|
|
5494
5538
|
return { xml: serializer2.serializeToString(doc), touched };
|
|
5495
5539
|
}
|
|
5496
5540
|
function ensureContentTypeOverride(adapter) {
|
|
5497
|
-
const
|
|
5541
|
+
const path25 = "[Content_Types].xml";
|
|
5498
5542
|
let raw = null;
|
|
5499
5543
|
try {
|
|
5500
|
-
raw = adapter.readTextOrNull(
|
|
5544
|
+
raw = adapter.readTextOrNull(path25);
|
|
5501
5545
|
} catch {
|
|
5502
5546
|
return;
|
|
5503
5547
|
}
|
|
@@ -5514,13 +5558,13 @@ function ensureContentTypeOverride(adapter) {
|
|
|
5514
5558
|
created.setAttribute("PartName", "/docProps/core.xml");
|
|
5515
5559
|
created.setAttribute("ContentType", CORE_PROPERTIES_CONTENT_TYPE);
|
|
5516
5560
|
root.appendChild(created);
|
|
5517
|
-
adapter.writeText(
|
|
5561
|
+
adapter.writeText(path25, new import_xmldom7.XMLSerializer().serializeToString(doc));
|
|
5518
5562
|
}
|
|
5519
5563
|
function ensurePackageRel(adapter) {
|
|
5520
|
-
const
|
|
5564
|
+
const path25 = "_rels/.rels";
|
|
5521
5565
|
let raw = null;
|
|
5522
5566
|
try {
|
|
5523
|
-
raw = adapter.readTextOrNull(
|
|
5567
|
+
raw = adapter.readTextOrNull(path25);
|
|
5524
5568
|
} catch {
|
|
5525
5569
|
return;
|
|
5526
5570
|
}
|
|
@@ -5553,7 +5597,7 @@ function ensurePackageRel(adapter) {
|
|
|
5553
5597
|
created.setAttribute("Type", CORE_PROPERTIES_REL_TYPE);
|
|
5554
5598
|
created.setAttribute("Target", CORE_PROPERTIES_PART);
|
|
5555
5599
|
root.appendChild(created);
|
|
5556
|
-
adapter.writeText(
|
|
5600
|
+
adapter.writeText(path25, new import_xmldom7.XMLSerializer().serializeToString(doc));
|
|
5557
5601
|
}
|
|
5558
5602
|
function writeCorePropertiesPart(adapter, xml) {
|
|
5559
5603
|
adapter.writeText(CORE_PROPERTIES_PART, xml);
|
|
@@ -7416,20 +7460,20 @@ function sanitizeEmbeddedWorkbookBytes(buf, outerName) {
|
|
|
7416
7460
|
}
|
|
7417
7461
|
var XML_PART_RE = /^(word\/document\.xml|word\/styles\.xml|word\/stylesWithEffects\.xml|word\/numbering\.xml|word\/comments\.xml|word\/commentsExtended\.xml|word\/footnotes\.xml|word\/endnotes\.xml|word\/settings\.xml|word\/header.*\.xml|word\/footer.*\.xml|word\/people\.xml|word\/customXml\/.*\.xml|word\/charts\/.*\.xml|word\/embeddings\/.*\.xml)$/i;
|
|
7418
7462
|
function bufferZipAdapter(files, touched) {
|
|
7419
|
-
const write = (
|
|
7420
|
-
files.set(
|
|
7421
|
-
touched?.add(
|
|
7463
|
+
const write = (path25, content) => {
|
|
7464
|
+
files.set(path25, content);
|
|
7465
|
+
touched?.add(path25);
|
|
7422
7466
|
};
|
|
7423
7467
|
return {
|
|
7424
|
-
hasFile: (
|
|
7425
|
-
readText: (
|
|
7426
|
-
const data = files.get(
|
|
7427
|
-
if (!data) throw new Error(`E_VALIDATION: Part ${
|
|
7468
|
+
hasFile: (path25) => files.has(path25),
|
|
7469
|
+
readText: (path25) => {
|
|
7470
|
+
const data = files.get(path25);
|
|
7471
|
+
if (!data) throw new Error(`E_VALIDATION: Part ${path25} was not found.`);
|
|
7428
7472
|
return data.toString("utf8");
|
|
7429
7473
|
},
|
|
7430
|
-
readTextOrNull: (
|
|
7431
|
-
writeText: (
|
|
7432
|
-
writeBuffer: (
|
|
7474
|
+
readTextOrNull: (path25) => files.get(path25)?.toString("utf8") ?? null,
|
|
7475
|
+
writeText: (path25, content) => write(path25, Buffer.from(content, "utf8")),
|
|
7476
|
+
writeBuffer: (path25, content) => write(path25, Buffer.from(content))
|
|
7433
7477
|
};
|
|
7434
7478
|
}
|
|
7435
7479
|
function deleteRemovedComments(files, ids, touched) {
|
|
@@ -10414,7 +10458,7 @@ var DocxParser = class _DocxParser {
|
|
|
10414
10458
|
* Captures a full package snapshot: a clone of every zip entry buffer.
|
|
10415
10459
|
* Used by OperationEngine for transactional rollback of failed batches.
|
|
10416
10460
|
*/
|
|
10417
|
-
capturePackageSnapshot() {
|
|
10461
|
+
capturePackageSnapshot(options) {
|
|
10418
10462
|
this.purgeRogueZipEntries();
|
|
10419
10463
|
const snapshot3 = /* @__PURE__ */ new Map();
|
|
10420
10464
|
for (const entry of this.zip.getEntries()) {
|
|
@@ -10425,7 +10469,7 @@ var DocxParser = class _DocxParser {
|
|
|
10425
10469
|
snapshot3.set(entry.entryName, Buffer.from(entry.getData()));
|
|
10426
10470
|
}
|
|
10427
10471
|
try {
|
|
10428
|
-
snapshot3.set("word/document.xml", Buffer.from(this.readDocumentXml(), "utf8"));
|
|
10472
|
+
snapshot3.set("word/document.xml", Buffer.from(options?.documentXml ?? this.readDocumentXml(), "utf8"));
|
|
10429
10473
|
} catch {
|
|
10430
10474
|
}
|
|
10431
10475
|
return snapshot3;
|
|
@@ -10912,22 +10956,10 @@ var DocxParser = class _DocxParser {
|
|
|
10912
10956
|
}
|
|
10913
10957
|
let isHeading = false;
|
|
10914
10958
|
let headingLevel = void 0;
|
|
10915
|
-
const
|
|
10916
|
-
if (
|
|
10917
|
-
|
|
10918
|
-
|
|
10919
|
-
if (lowerVal.includes("heading") || lowerVal.includes("title") || lowerVal.includes("subtitle")) {
|
|
10920
|
-
isHeading = true;
|
|
10921
|
-
if (lowerVal.includes("heading 1") || lowerVal === "heading1" || lowerVal === "h1" || lowerVal.includes("title")) {
|
|
10922
|
-
headingLevel = 1;
|
|
10923
|
-
} else if (lowerVal.includes("heading 2") || lowerVal === "heading2" || lowerVal === "h2" || lowerVal.includes("subtitle")) {
|
|
10924
|
-
headingLevel = 2;
|
|
10925
|
-
} else if (lowerVal.includes("heading 3") || lowerVal === "heading3" || lowerVal === "h3") {
|
|
10926
|
-
headingLevel = 3;
|
|
10927
|
-
} else {
|
|
10928
|
-
headingLevel = 1;
|
|
10929
|
-
}
|
|
10930
|
-
}
|
|
10959
|
+
const ownLevel = headingLevelOfStyleId(ownParagraphStyleId(node));
|
|
10960
|
+
if (ownLevel !== null) {
|
|
10961
|
+
isHeading = true;
|
|
10962
|
+
headingLevel = ownLevel;
|
|
10931
10963
|
}
|
|
10932
10964
|
const hasTrackedChanges = nodeHasTrackedChanges(node);
|
|
10933
10965
|
if (hasTrackedChanges) summary.hasTrackedChanges = true;
|
|
@@ -11068,20 +11100,20 @@ var DocxParser = class _DocxParser {
|
|
|
11068
11100
|
}
|
|
11069
11101
|
getZipAdapter() {
|
|
11070
11102
|
return {
|
|
11071
|
-
hasFile: (
|
|
11072
|
-
readText: (
|
|
11073
|
-
const cleanPath = this.sanitizePath(
|
|
11103
|
+
hasFile: (path25) => !!this.zip.getEntry(this.sanitizePath(path25)),
|
|
11104
|
+
readText: (path25) => {
|
|
11105
|
+
const cleanPath = this.sanitizePath(path25);
|
|
11074
11106
|
const entry = this.zip.getEntry(cleanPath);
|
|
11075
11107
|
if (!entry) throw new Error(`File not found: ${cleanPath}`);
|
|
11076
11108
|
return entry.getData().toString("utf8");
|
|
11077
11109
|
},
|
|
11078
|
-
readTextOrNull: (
|
|
11079
|
-
const cleanPath = this.sanitizePath(
|
|
11110
|
+
readTextOrNull: (path25) => {
|
|
11111
|
+
const cleanPath = this.sanitizePath(path25);
|
|
11080
11112
|
const entry = this.zip.getEntry(cleanPath);
|
|
11081
11113
|
return entry ? entry.getData().toString("utf8") : null;
|
|
11082
11114
|
},
|
|
11083
|
-
writeText: (
|
|
11084
|
-
const cleanPath = this.sanitizePath(
|
|
11115
|
+
writeText: (path25, content) => {
|
|
11116
|
+
const cleanPath = this.sanitizePath(path25);
|
|
11085
11117
|
assertNoXxe(content, cleanPath);
|
|
11086
11118
|
assertNoXxeAdjacent(content, cleanPath);
|
|
11087
11119
|
if (this.zip.getEntry(cleanPath)) {
|
|
@@ -11090,8 +11122,8 @@ var DocxParser = class _DocxParser {
|
|
|
11090
11122
|
this.zip.addFile(cleanPath, Buffer.from(content, "utf8"));
|
|
11091
11123
|
}
|
|
11092
11124
|
},
|
|
11093
|
-
writeBuffer: (
|
|
11094
|
-
const cleanPath = this.sanitizePath(
|
|
11125
|
+
writeBuffer: (path25, content) => {
|
|
11126
|
+
const cleanPath = this.sanitizePath(path25);
|
|
11095
11127
|
assertNoXxe(content.toString("utf8"), cleanPath);
|
|
11096
11128
|
assertNoXxeAdjacent(content.toString("utf8"), cleanPath);
|
|
11097
11129
|
if (this.zip.getEntry(cleanPath)) {
|
|
@@ -11223,8 +11255,7 @@ var DocxParser = class _DocxParser {
|
|
|
11223
11255
|
}
|
|
11224
11256
|
}
|
|
11225
11257
|
}
|
|
11226
|
-
const
|
|
11227
|
-
const namedStyle = pStyleEl ? pStyleEl.getAttribute("w:val") || void 0 : void 0;
|
|
11258
|
+
const namedStyle = ownParagraphStyleId(element);
|
|
11228
11259
|
let nodeType = element.tagName === "w:p" ? "paragraph" : "table";
|
|
11229
11260
|
let chartData = void 0;
|
|
11230
11261
|
let detectedChart = void 0;
|
|
@@ -12915,12 +12946,12 @@ ${validation.errors.join("\n")}`, "repair the part XML or re-save the document i
|
|
|
12915
12946
|
if (!comments || /<w:comment[\s>]/.test(comments.getData().toString("utf8"))) return;
|
|
12916
12947
|
const parts = ["comments", "commentsExtended", "commentsIds", "commentsExtensible"].map((n) => `${n}.xml`);
|
|
12917
12948
|
for (const part of parts) if (this.zip.getEntry(`word/${part}`)) this.zip.deleteFile(`word/${part}`);
|
|
12918
|
-
const edit = (
|
|
12919
|
-
const entry = this.zip.getEntry(
|
|
12949
|
+
const edit = (path25, tag, attr, drop) => {
|
|
12950
|
+
const entry = this.zip.getEntry(path25);
|
|
12920
12951
|
if (!entry) return;
|
|
12921
12952
|
const dom = this.parser.parseFromString(entry.getData().toString("utf8"), "text/xml");
|
|
12922
12953
|
for (const el of Array.from(dom.getElementsByTagName(tag))) if (drop(el.getAttribute(attr) ?? "")) el.parentNode?.removeChild(el);
|
|
12923
|
-
this.zip.updateFile(
|
|
12954
|
+
this.zip.updateFile(path25, Buffer.from(this.serializer.serializeToString(dom), "utf8"));
|
|
12924
12955
|
};
|
|
12925
12956
|
edit("word/_rels/document.xml.rels", "Relationship", "Target", (value) => parts.includes(value.replace(/^\/?(word\/)?/, "")));
|
|
12926
12957
|
edit("[Content_Types].xml", "Override", "PartName", (value) => parts.map((p) => `/word/${p}`).includes(value));
|
|
@@ -13658,12 +13689,11 @@ ${validation.errors.join("\n")}`, "repair the part XML or re-save the document i
|
|
|
13658
13689
|
}
|
|
13659
13690
|
};
|
|
13660
13691
|
|
|
13661
|
-
// ../engine/src/
|
|
13662
|
-
var
|
|
13663
|
-
var import_promises2 = __toESM(require("fs/promises"));
|
|
13664
|
-
var
|
|
13665
|
-
var
|
|
13666
|
-
var import_isomorphic_git = __toESM(require("isomorphic-git"));
|
|
13692
|
+
// ../engine/src/original.ts
|
|
13693
|
+
var import_node_fs = __toESM(require("node:fs"));
|
|
13694
|
+
var import_promises2 = __toESM(require("node:fs/promises"));
|
|
13695
|
+
var import_node_path = __toESM(require("node:path"));
|
|
13696
|
+
var import_node_crypto = require("node:crypto");
|
|
13667
13697
|
|
|
13668
13698
|
// ../engine/src/sandbox/paths.ts
|
|
13669
13699
|
var import_fs3 = __toESM(require("fs"));
|
|
@@ -13805,7 +13835,408 @@ function writeFileNoFollowSync(target, data, encoding = "utf8") {
|
|
|
13805
13835
|
assertWriteTargetNoSymlink(import_path2.default.resolve(target));
|
|
13806
13836
|
}
|
|
13807
13837
|
|
|
13838
|
+
// ../engine/src/original.ts
|
|
13839
|
+
var SNAPSHOT_NAME = "original.snapshot";
|
|
13840
|
+
var SNAPSHOT_MAGIC = Buffer.from("ASTRA-ORIGINAL\n");
|
|
13841
|
+
var MAX_SNAPSHOT_HEADER = 4096;
|
|
13842
|
+
function originalSha256(bytes) {
|
|
13843
|
+
return (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
|
|
13844
|
+
}
|
|
13845
|
+
function coded(message, code = "E_IO") {
|
|
13846
|
+
return Object.assign(new Error(message), { code });
|
|
13847
|
+
}
|
|
13848
|
+
function errno(e) {
|
|
13849
|
+
return typeof e === "object" && e !== null && "code" in e ? String(e.code) : "";
|
|
13850
|
+
}
|
|
13851
|
+
function originalDir(docPath) {
|
|
13852
|
+
return import_node_path.default.join(getRepoDir(docPath), ".git", "astra-original");
|
|
13853
|
+
}
|
|
13854
|
+
async function readSafe(file, maxBytes = resolveHistoryMaxSidecarBytes()) {
|
|
13855
|
+
assertWriteTargetNoSymlink(file);
|
|
13856
|
+
const h = await import_promises2.default.open(file, import_node_fs.default.constants.O_RDONLY | import_node_fs.default.constants.O_NOFOLLOW);
|
|
13857
|
+
try {
|
|
13858
|
+
const stat3 = await h.stat();
|
|
13859
|
+
if (!stat3.isFile() || stat3.size > maxBytes) throw coded("Original recovery refused: expected a regular file within the size limit.");
|
|
13860
|
+
return await h.readFile();
|
|
13861
|
+
} finally {
|
|
13862
|
+
await h.close();
|
|
13863
|
+
}
|
|
13864
|
+
}
|
|
13865
|
+
async function exists(file) {
|
|
13866
|
+
try {
|
|
13867
|
+
await import_promises2.default.lstat(file);
|
|
13868
|
+
return true;
|
|
13869
|
+
} catch (e) {
|
|
13870
|
+
if (errno(e) === "ENOENT") return false;
|
|
13871
|
+
throw e;
|
|
13872
|
+
}
|
|
13873
|
+
}
|
|
13874
|
+
async function syncDirectory(dir) {
|
|
13875
|
+
if (process.platform === "win32") return false;
|
|
13876
|
+
let h;
|
|
13877
|
+
let synced = false;
|
|
13878
|
+
try {
|
|
13879
|
+
h = await import_promises2.default.open(dir, import_node_fs.default.constants.O_RDONLY);
|
|
13880
|
+
await h.sync();
|
|
13881
|
+
synced = true;
|
|
13882
|
+
} catch {
|
|
13883
|
+
}
|
|
13884
|
+
try {
|
|
13885
|
+
await h?.close();
|
|
13886
|
+
} catch {
|
|
13887
|
+
synced = false;
|
|
13888
|
+
}
|
|
13889
|
+
return synced;
|
|
13890
|
+
}
|
|
13891
|
+
async function cleanupTemp(file) {
|
|
13892
|
+
try {
|
|
13893
|
+
await import_promises2.default.unlink(file);
|
|
13894
|
+
} catch (e) {
|
|
13895
|
+
if (errno(e) !== "ENOENT") throw Object.assign(coded(`Temporary file cleanup failed: ${file}. ${e instanceof Error ? e.message : String(e)}`), { tempPath: file });
|
|
13896
|
+
}
|
|
13897
|
+
}
|
|
13898
|
+
async function preparedTempParts(target, parts, mode = 384) {
|
|
13899
|
+
assertWriteTargetNoSymlink(target);
|
|
13900
|
+
const dir = import_node_path.default.dirname(target);
|
|
13901
|
+
await import_promises2.default.mkdir(dir, { recursive: true, mode: 448 });
|
|
13902
|
+
assertWriteTargetNoSymlink(target);
|
|
13903
|
+
const tmp = import_node_path.default.join(dir, `.${import_node_path.default.basename(target).slice(0, 60)}.astra-tmp-${(0, import_node_crypto.randomUUID)()}`);
|
|
13904
|
+
const h = await import_promises2.default.open(tmp, import_node_fs.default.constants.O_WRONLY | import_node_fs.default.constants.O_CREAT | import_node_fs.default.constants.O_EXCL | import_node_fs.default.constants.O_NOFOLLOW, mode);
|
|
13905
|
+
let writeError;
|
|
13906
|
+
try {
|
|
13907
|
+
for (const part of parts) {
|
|
13908
|
+
let offset = 0;
|
|
13909
|
+
while (offset < part.length) {
|
|
13910
|
+
const { bytesWritten } = await h.write(part, offset, part.length - offset, null);
|
|
13911
|
+
if (bytesWritten === 0) throw coded("Original recovery refused: temporary file write made no progress.");
|
|
13912
|
+
offset += bytesWritten;
|
|
13913
|
+
}
|
|
13914
|
+
}
|
|
13915
|
+
await h.sync();
|
|
13916
|
+
} catch (error) {
|
|
13917
|
+
writeError = error;
|
|
13918
|
+
}
|
|
13919
|
+
let closeError;
|
|
13920
|
+
try {
|
|
13921
|
+
await h.close();
|
|
13922
|
+
} catch (error) {
|
|
13923
|
+
closeError = error;
|
|
13924
|
+
}
|
|
13925
|
+
if (writeError || closeError) {
|
|
13926
|
+
const primary = writeError ?? closeError;
|
|
13927
|
+
let message = primary instanceof Error ? primary.message : String(primary);
|
|
13928
|
+
if (writeError && closeError) message += `; temporary file close also failed: ${closeError instanceof Error ? closeError.message : String(closeError)}`;
|
|
13929
|
+
let cleanupFailed = false;
|
|
13930
|
+
try {
|
|
13931
|
+
await cleanupTemp(tmp);
|
|
13932
|
+
} catch (cleanup) {
|
|
13933
|
+
cleanupFailed = true;
|
|
13934
|
+
message += `; ${cleanup instanceof Error ? cleanup.message : String(cleanup)}`;
|
|
13935
|
+
}
|
|
13936
|
+
throw Object.assign(coded(message, errno(primary) || "E_IO"), { ...cleanupFailed ? { tempPath: tmp } : {}, cause: primary });
|
|
13937
|
+
}
|
|
13938
|
+
try {
|
|
13939
|
+
const expectedBytes = parts.reduce((total, part) => total + part.length, 0);
|
|
13940
|
+
const expectedHash = (0, import_node_crypto.createHash)("sha256");
|
|
13941
|
+
for (const part of parts) expectedHash.update(part);
|
|
13942
|
+
const check = await import_promises2.default.open(tmp, import_node_fs.default.constants.O_RDONLY | import_node_fs.default.constants.O_NOFOLLOW);
|
|
13943
|
+
try {
|
|
13944
|
+
const stat3 = await check.stat();
|
|
13945
|
+
if (!stat3.isFile() || stat3.size !== expectedBytes) throw coded("Original recovery refused: temporary file integrity check failed.");
|
|
13946
|
+
const actualHash = (0, import_node_crypto.createHash)("sha256");
|
|
13947
|
+
const chunk = Buffer.allocUnsafe(64 * 1024);
|
|
13948
|
+
let position2 = 0;
|
|
13949
|
+
while (position2 < expectedBytes) {
|
|
13950
|
+
const { bytesRead } = await check.read(chunk, 0, Math.min(chunk.length, expectedBytes - position2), position2);
|
|
13951
|
+
if (bytesRead === 0) throw coded("Original recovery refused: temporary file integrity check failed.");
|
|
13952
|
+
actualHash.update(chunk.subarray(0, bytesRead));
|
|
13953
|
+
position2 += bytesRead;
|
|
13954
|
+
}
|
|
13955
|
+
if (actualHash.digest("hex") !== expectedHash.digest("hex")) throw coded("Original recovery refused: temporary file integrity check failed.");
|
|
13956
|
+
} finally {
|
|
13957
|
+
await check.close();
|
|
13958
|
+
}
|
|
13959
|
+
} catch (e) {
|
|
13960
|
+
try {
|
|
13961
|
+
await cleanupTemp(tmp);
|
|
13962
|
+
} catch (cleanup) {
|
|
13963
|
+
throw coded(`${e instanceof Error ? e.message : String(e)}; ${cleanup instanceof Error ? cleanup.message : String(cleanup)}`);
|
|
13964
|
+
}
|
|
13965
|
+
throw e;
|
|
13966
|
+
}
|
|
13967
|
+
return tmp;
|
|
13968
|
+
}
|
|
13969
|
+
async function preparedTemp(target, data, mode = 384) {
|
|
13970
|
+
return preparedTempParts(target, [data], mode);
|
|
13971
|
+
}
|
|
13972
|
+
async function writePartsExclusive(target, parts, mode = 384) {
|
|
13973
|
+
const tmp = await preparedTempParts(target, parts, mode);
|
|
13974
|
+
let failure;
|
|
13975
|
+
try {
|
|
13976
|
+
assertWriteTargetNoSymlink(target);
|
|
13977
|
+
try {
|
|
13978
|
+
await import_promises2.default.link(tmp, target);
|
|
13979
|
+
} catch (e) {
|
|
13980
|
+
if (errno(e) === "EEXIST") throw coded(`Destination exists: ${target}`, "E_CONFLICT");
|
|
13981
|
+
if (["ENOTSUP", "EOPNOTSUPP", "EPERM", "EXDEV", "ENOSYS"].includes(errno(e))) {
|
|
13982
|
+
throw coded("This location does not support safe exclusive publication; export to another directory.");
|
|
13983
|
+
}
|
|
13984
|
+
throw e;
|
|
13985
|
+
}
|
|
13986
|
+
await syncDirectory(import_node_path.default.dirname(target));
|
|
13987
|
+
} catch (error) {
|
|
13988
|
+
failure = error;
|
|
13989
|
+
}
|
|
13990
|
+
try {
|
|
13991
|
+
await cleanupTemp(tmp);
|
|
13992
|
+
} catch (cleanup) {
|
|
13993
|
+
const detail = cleanup instanceof Error ? cleanup.message : String(cleanup);
|
|
13994
|
+
if (failure) {
|
|
13995
|
+
throw Object.assign(coded(`${failure instanceof Error ? failure.message : String(failure)}; ${detail}`, errno(failure) || "E_IO"), { tempPath: tmp });
|
|
13996
|
+
}
|
|
13997
|
+
throw Object.assign(coded(`Destination was published at ${target}, but ${detail}`), { tempPath: tmp, published: true });
|
|
13998
|
+
}
|
|
13999
|
+
if (failure) throw failure;
|
|
14000
|
+
}
|
|
14001
|
+
async function writeFileExclusive(target, data, mode = 384) {
|
|
14002
|
+
return writePartsExclusive(target, [data], mode);
|
|
14003
|
+
}
|
|
14004
|
+
async function createVerifiedBackup(target, bytes) {
|
|
14005
|
+
let ownsTarget = false;
|
|
14006
|
+
try {
|
|
14007
|
+
await writeFileExclusive(target, bytes);
|
|
14008
|
+
ownsTarget = true;
|
|
14009
|
+
assertWriteTargetNoSymlink(target);
|
|
14010
|
+
const written = await import_promises2.default.readFile(target);
|
|
14011
|
+
if (written.length !== bytes.length || originalSha256(written) !== originalSha256(bytes)) {
|
|
14012
|
+
throw coded(`Restore backup verification failed: ${target}`, "E_IO");
|
|
14013
|
+
}
|
|
14014
|
+
} catch (error) {
|
|
14015
|
+
if (error?.published === true) ownsTarget = true;
|
|
14016
|
+
if (ownsTarget) {
|
|
14017
|
+
try {
|
|
14018
|
+
await import_promises2.default.unlink(target);
|
|
14019
|
+
} catch (cleanupError) {
|
|
14020
|
+
if (errno(cleanupError) !== "ENOENT") {
|
|
14021
|
+
throw Object.assign(coded(`${error instanceof Error ? error.message : String(error)}; invalid backup could not be removed: ${target} (${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)})`, "E_IO"), { backupPath: target });
|
|
14022
|
+
}
|
|
14023
|
+
}
|
|
14024
|
+
}
|
|
14025
|
+
throw error;
|
|
14026
|
+
}
|
|
14027
|
+
}
|
|
14028
|
+
function newRestoreBackupPath(target) {
|
|
14029
|
+
return import_node_path.default.join(import_node_path.default.dirname(target), `.astra-restore-backup-${(0, import_node_crypto.randomUUID)()}`);
|
|
14030
|
+
}
|
|
14031
|
+
async function replaceFileAtomic(target, data, newFileMode = 384) {
|
|
14032
|
+
let tmp;
|
|
14033
|
+
let replaced = false;
|
|
14034
|
+
let renameStarted = false;
|
|
14035
|
+
try {
|
|
14036
|
+
assertWriteTargetNoSymlink(target);
|
|
14037
|
+
let mode = newFileMode;
|
|
14038
|
+
try {
|
|
14039
|
+
const st = await import_promises2.default.lstat(target);
|
|
14040
|
+
if (!st.isFile()) throw coded("Atomic replace requires a regular target file.");
|
|
14041
|
+
mode = st.mode & 511;
|
|
14042
|
+
if ((mode & 146) === 0) throw coded("Atomic replace refused: target is read-only.");
|
|
14043
|
+
await import_promises2.default.access(target, import_node_fs.default.constants.W_OK);
|
|
14044
|
+
} catch (e) {
|
|
14045
|
+
if (errno(e) !== "ENOENT") throw e;
|
|
14046
|
+
}
|
|
14047
|
+
tmp = await preparedTemp(target, data, mode);
|
|
14048
|
+
assertWriteTargetNoSymlink(target);
|
|
14049
|
+
renameStarted = true;
|
|
14050
|
+
for (let attempt = 0; ; attempt++) {
|
|
14051
|
+
try {
|
|
14052
|
+
await import_promises2.default.rename(tmp, target);
|
|
14053
|
+
replaced = true;
|
|
14054
|
+
break;
|
|
14055
|
+
} catch (e) {
|
|
14056
|
+
if (process.platform !== "win32" || !["EBUSY", "EPERM", "EACCES"].includes(errno(e)) || attempt >= 3) throw e;
|
|
14057
|
+
await new Promise((resolve10) => setTimeout(resolve10, 25 * (attempt + 1)));
|
|
14058
|
+
}
|
|
14059
|
+
}
|
|
14060
|
+
const durable = await syncDirectory(import_node_path.default.dirname(target));
|
|
14061
|
+
await cleanupTemp(tmp);
|
|
14062
|
+
tmp = void 0;
|
|
14063
|
+
return { outcome: "replaced", ...!durable ? { durabilityUnconfirmed: true } : {} };
|
|
14064
|
+
} catch (e) {
|
|
14065
|
+
let message = e instanceof Error ? e.message : String(e);
|
|
14066
|
+
let outcome = replaced ? "replaced" : renameStarted ? "uncertain" : "not-replaced";
|
|
14067
|
+
if (!replaced && renameStarted && tmp) {
|
|
14068
|
+
try {
|
|
14069
|
+
const tempStat = await import_promises2.default.lstat(tmp);
|
|
14070
|
+
if (tempStat.isFile() && !tempStat.isSymbolicLink()) outcome = "not-replaced";
|
|
14071
|
+
} catch (tempError) {
|
|
14072
|
+
if (errno(tempError) === "ENOENT") {
|
|
14073
|
+
try {
|
|
14074
|
+
assertWriteTargetNoSymlink(target);
|
|
14075
|
+
const targetStat = await import_promises2.default.lstat(target);
|
|
14076
|
+
if (targetStat.isFile() && targetStat.size === data.length && (await import_promises2.default.readFile(target)).equals(data)) {
|
|
14077
|
+
replaced = true;
|
|
14078
|
+
outcome = "replaced";
|
|
14079
|
+
}
|
|
14080
|
+
} catch {
|
|
14081
|
+
}
|
|
14082
|
+
}
|
|
14083
|
+
}
|
|
14084
|
+
}
|
|
14085
|
+
if (tmp) {
|
|
14086
|
+
try {
|
|
14087
|
+
await cleanupTemp(tmp);
|
|
14088
|
+
} catch (cleanup) {
|
|
14089
|
+
message += `; ${cleanup instanceof Error ? cleanup.message : String(cleanup)}`;
|
|
14090
|
+
}
|
|
14091
|
+
}
|
|
14092
|
+
throw Object.assign(coded(message, errno(e) || "E_IO"), {
|
|
14093
|
+
outcome
|
|
14094
|
+
});
|
|
14095
|
+
}
|
|
14096
|
+
}
|
|
14097
|
+
function encodeSnapshot(bytes, sourceName) {
|
|
14098
|
+
const meta = { magic: "ASTRA-ORIGINAL", version: 1, sha256: originalSha256(bytes), bytes: bytes.length, capturedAt: (/* @__PURE__ */ new Date()).toISOString(), sourceName };
|
|
14099
|
+
const json = Buffer.from(JSON.stringify(meta));
|
|
14100
|
+
if (json.length > MAX_SNAPSHOT_HEADER) throw coded("Original snapshot metadata exceeds its format limit.", "E_LIMIT");
|
|
14101
|
+
const prefix = Buffer.alloc(SNAPSHOT_MAGIC.length + 4);
|
|
14102
|
+
SNAPSHOT_MAGIC.copy(prefix);
|
|
14103
|
+
prefix.writeUInt32BE(json.length, SNAPSHOT_MAGIC.length);
|
|
14104
|
+
return [prefix, json, bytes];
|
|
14105
|
+
}
|
|
14106
|
+
function decodeSnapshot(all) {
|
|
14107
|
+
const offset = SNAPSHOT_MAGIC.length;
|
|
14108
|
+
if (all.length < offset + 4 || !all.subarray(0, offset).equals(SNAPSHOT_MAGIC)) throw coded("Original snapshot has an invalid format header.", "E_VALIDATION");
|
|
14109
|
+
const headerBytes = all.readUInt32BE(offset);
|
|
14110
|
+
if (headerBytes < 2 || headerBytes > MAX_SNAPSHOT_HEADER || all.length < offset + 4 + headerBytes) throw coded("Original snapshot has an invalid metadata length.", "E_VALIDATION");
|
|
14111
|
+
let meta;
|
|
14112
|
+
try {
|
|
14113
|
+
meta = JSON.parse(all.subarray(offset + 4, offset + 4 + headerBytes).toString("utf8"));
|
|
14114
|
+
} catch {
|
|
14115
|
+
throw coded("Original snapshot metadata is not valid JSON.", "E_VALIDATION");
|
|
14116
|
+
}
|
|
14117
|
+
const bytes = all.subarray(offset + 4 + headerBytes);
|
|
14118
|
+
if (meta.magic !== "ASTRA-ORIGINAL" || meta.version !== 1 || !/^[a-f0-9]{64}$/.test(meta.sha256) || !Number.isSafeInteger(meta.bytes) || meta.bytes < 1 || meta.bytes !== bytes.length || typeof meta.capturedAt !== "string" || typeof meta.sourceName !== "string") {
|
|
14119
|
+
throw coded("Original snapshot metadata is invalid.", "E_VALIDATION");
|
|
14120
|
+
}
|
|
14121
|
+
if (originalSha256(bytes) !== meta.sha256) throw coded("Original snapshot integrity verification failed.", "E_VALIDATION");
|
|
14122
|
+
return { meta, bytes };
|
|
14123
|
+
}
|
|
14124
|
+
async function verifiedSnapshot(docPath) {
|
|
14125
|
+
const file = import_node_path.default.join(originalDir(docPath), SNAPSHOT_NAME);
|
|
14126
|
+
const raw = await readSafe(file);
|
|
14127
|
+
return decodeSnapshot(raw);
|
|
14128
|
+
}
|
|
14129
|
+
async function getOriginalInfo(docPath) {
|
|
14130
|
+
if (!import_node_path.default.isAbsolute(docPath)) return { available: false, reason: "not-applicable" };
|
|
14131
|
+
const dir = originalDir(docPath);
|
|
14132
|
+
try {
|
|
14133
|
+
if (!await exists(dir)) return { available: false, reason: "predates-feature" };
|
|
14134
|
+
const snapshot3 = import_node_path.default.join(dir, SNAPSHOT_NAME);
|
|
14135
|
+
assertWriteTargetNoSymlink(snapshot3);
|
|
14136
|
+
if (await exists(snapshot3)) {
|
|
14137
|
+
const { meta } = await verifiedSnapshot(docPath);
|
|
14138
|
+
return { available: true, sha256: meta.sha256, bytes: meta.bytes, capturedAt: meta.capturedAt };
|
|
14139
|
+
}
|
|
14140
|
+
try {
|
|
14141
|
+
const status = JSON.parse((await readSafe(import_node_path.default.join(dir, "status.json"), 16 * 1024)).toString("utf8"));
|
|
14142
|
+
if (["capture-failed", "too-large", "not-applicable"].includes(status.reason)) return { available: false, reason: status.reason, ...typeof status.detail === "string" ? { detail: status.detail.slice(0, 1024) } : {} };
|
|
14143
|
+
} catch {
|
|
14144
|
+
}
|
|
14145
|
+
return { available: false, reason: "incomplete" };
|
|
14146
|
+
} catch (e) {
|
|
14147
|
+
return { available: false, reason: "corrupt", detail: e instanceof Error ? e.message : String(e) };
|
|
14148
|
+
}
|
|
14149
|
+
}
|
|
14150
|
+
async function captureOriginal(docPath, bytes) {
|
|
14151
|
+
const dir = originalDir(docPath);
|
|
14152
|
+
let result;
|
|
14153
|
+
try {
|
|
14154
|
+
assertWriteTargetNoSymlink(import_node_path.default.join(dir, SNAPSHOT_NAME));
|
|
14155
|
+
await import_promises2.default.mkdir(dir, { recursive: true, mode: 448 });
|
|
14156
|
+
assertWriteTargetNoSymlink(import_node_path.default.join(dir, SNAPSHOT_NAME));
|
|
14157
|
+
if (await exists(import_node_path.default.join(dir, SNAPSHOT_NAME))) {
|
|
14158
|
+
const info = await getOriginalInfo(docPath);
|
|
14159
|
+
if (info.available && bytes && originalSha256(bytes) !== info.sha256) return { available: false, reason: "capture-failed", detail: "An original is already recorded for this history." };
|
|
14160
|
+
return info;
|
|
14161
|
+
}
|
|
14162
|
+
if (!bytes) result = { available: false, reason: "not-applicable" };
|
|
14163
|
+
else if (!Buffer.isBuffer(bytes) || bytes.length === 0) result = { available: false, reason: "capture-failed", detail: "Original input is empty." };
|
|
14164
|
+
else if (await getSidecarBytes(getRepoDir(docPath)) + bytes.length + 4096 + SNAPSHOT_MAGIC.length > resolveHistoryMaxSidecarBytes()) result = { available: false, reason: "too-large" };
|
|
14165
|
+
else {
|
|
14166
|
+
const digest = originalSha256(bytes);
|
|
14167
|
+
const snapshot3 = import_node_path.default.join(dir, SNAPSHOT_NAME);
|
|
14168
|
+
try {
|
|
14169
|
+
await writePartsExclusive(snapshot3, encodeSnapshot(bytes, import_node_path.default.basename(docPath)));
|
|
14170
|
+
} catch (e) {
|
|
14171
|
+
if (e?.published === true) {
|
|
14172
|
+
const published = await getOriginalInfo(docPath);
|
|
14173
|
+
if (published.available && published.sha256 === digest) return published;
|
|
14174
|
+
throw e;
|
|
14175
|
+
}
|
|
14176
|
+
if (errno(e) !== "E_CONFLICT") throw e;
|
|
14177
|
+
const winner = await verifiedSnapshot(docPath);
|
|
14178
|
+
if (!winner.bytes.equals(bytes)) return { available: false, reason: "capture-failed", detail: "An original is already recorded for this history." };
|
|
14179
|
+
}
|
|
14180
|
+
const info = await getOriginalInfo(docPath);
|
|
14181
|
+
if (info.available && info.sha256 !== digest) return { available: false, reason: "capture-failed", detail: "An original is already recorded for this history." };
|
|
14182
|
+
return info;
|
|
14183
|
+
}
|
|
14184
|
+
} catch (e) {
|
|
14185
|
+
result = { available: false, reason: "capture-failed", detail: e instanceof Error ? e.message : String(e) };
|
|
14186
|
+
}
|
|
14187
|
+
try {
|
|
14188
|
+
await writeFileExclusive(import_node_path.default.join(dir, "status.json"), Buffer.from(JSON.stringify(result)));
|
|
14189
|
+
} catch {
|
|
14190
|
+
}
|
|
14191
|
+
return result;
|
|
14192
|
+
}
|
|
14193
|
+
async function readOriginal(docPath) {
|
|
14194
|
+
const info = await getOriginalInfo(docPath);
|
|
14195
|
+
if (!info.available) {
|
|
14196
|
+
throw Object.assign(coded(`Original unavailable: ${info.reason}${info.detail ? ` (${info.detail})` : ""}`, "E_STATE"), {
|
|
14197
|
+
remedy: "The retained original cannot be trusted, so no substitute is returned. Keep the working file and any separate backup, and check the history status for the reason."
|
|
14198
|
+
});
|
|
14199
|
+
}
|
|
14200
|
+
return (await verifiedSnapshot(docPath)).bytes;
|
|
14201
|
+
}
|
|
14202
|
+
async function exportOriginal(docPath, outPath, opts) {
|
|
14203
|
+
const bytes = await readOriginal(docPath);
|
|
14204
|
+
const source = await import_promises2.default.realpath(docPath).catch((e) => {
|
|
14205
|
+
if (errno(e) === "ENOENT") return import_node_path.default.resolve(docPath);
|
|
14206
|
+
throw e;
|
|
14207
|
+
});
|
|
14208
|
+
const parent = import_node_path.default.dirname(import_node_path.default.resolve(outPath));
|
|
14209
|
+
const destination = import_node_path.default.join(await import_promises2.default.realpath(parent).catch((e) => {
|
|
14210
|
+
if (errno(e) === "ENOENT") return parent;
|
|
14211
|
+
throw e;
|
|
14212
|
+
}), import_node_path.default.basename(outPath));
|
|
14213
|
+
const samePath = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
14214
|
+
let sameFile = samePath(source, destination);
|
|
14215
|
+
try {
|
|
14216
|
+
const [a, b] = await Promise.all([import_promises2.default.stat(source), import_promises2.default.stat(destination)]);
|
|
14217
|
+
sameFile ||= a.ino !== 0 && b.ino !== 0 && a.dev === b.dev && a.ino === b.ino;
|
|
14218
|
+
} catch (e) {
|
|
14219
|
+
if (errno(e) !== "ENOENT") throw e;
|
|
14220
|
+
}
|
|
14221
|
+
if (sameFile) throw coded("Use restore_original_document to replace the working file.", "E_CONFLICT");
|
|
14222
|
+
const repo = await import_promises2.default.realpath(getRepoDir(docPath));
|
|
14223
|
+
const normalizedDest = process.platform === "win32" ? destination.toLowerCase() : destination;
|
|
14224
|
+
const normalizedRepo = process.platform === "win32" ? repo.toLowerCase() : repo;
|
|
14225
|
+
if (normalizedDest === normalizedRepo || normalizedDest.startsWith(`${normalizedRepo}${import_node_path.default.sep}`)) {
|
|
14226
|
+
throw coded("Original export cannot write inside the document history store.", "E_CONFLICT");
|
|
14227
|
+
}
|
|
14228
|
+
if (opts?.overwrite) await replaceFileAtomic(outPath, bytes, 438);
|
|
14229
|
+
else await writeFileExclusive(outPath, bytes, 438);
|
|
14230
|
+
return getOriginalInfo(docPath);
|
|
14231
|
+
}
|
|
14232
|
+
|
|
13808
14233
|
// ../engine/src/git.ts
|
|
14234
|
+
var import_fs4 = __toESM(require("fs"));
|
|
14235
|
+
var import_os = __toESM(require("os"));
|
|
14236
|
+
var import_promises3 = __toESM(require("fs/promises"));
|
|
14237
|
+
var import_path3 = __toESM(require("path"));
|
|
14238
|
+
var import_crypto4 = __toESM(require("crypto"));
|
|
14239
|
+
var import_isomorphic_git = __toESM(require("isomorphic-git"));
|
|
13809
14240
|
var HISTORY_DIR_ENV = "ASTRA_HISTORY_DIR";
|
|
13810
14241
|
function historyRoot() {
|
|
13811
14242
|
const raw = process.env[HISTORY_DIR_ENV];
|
|
@@ -13916,7 +14347,7 @@ async function writeAndAdd(repoDir, relPath, content) {
|
|
|
13916
14347
|
const fullPath = import_path3.default.join(repoDir, safeRel);
|
|
13917
14348
|
const dirName = import_path3.default.dirname(fullPath);
|
|
13918
14349
|
assertWriteTargetNoSymlink(fullPath);
|
|
13919
|
-
await
|
|
14350
|
+
await import_promises3.default.mkdir(dirName, { recursive: true, mode: 448 });
|
|
13920
14351
|
await secureDirChain0700(repoDir, dirName);
|
|
13921
14352
|
assertWriteTargetNoSymlink(fullPath);
|
|
13922
14353
|
await writeFileNoFollow(fullPath, content);
|
|
@@ -13945,6 +14376,40 @@ async function indexedPartPaths(repoDir) {
|
|
|
13945
14376
|
const tracked = await import_isomorphic_git.default.listFiles({ fs: import_fs4.default, dir: repoDir });
|
|
13946
14377
|
return new Set(tracked.map((t) => toRepoRelative(t)).filter(isRepoPartPath));
|
|
13947
14378
|
}
|
|
14379
|
+
async function clearUncommittedPackageParts(repoDir) {
|
|
14380
|
+
const repoRoot = import_path3.default.resolve(repoDir);
|
|
14381
|
+
for (const rel of await indexedPartPaths(repoDir)) {
|
|
14382
|
+
if (isTraversalName(rel)) {
|
|
14383
|
+
throw Object.assign(new Error(`History repair refused an unsafe indexed path: ${rel}`), { code: "E_SANDBOX" });
|
|
14384
|
+
}
|
|
14385
|
+
const fullPath = import_path3.default.resolve(repoRoot, rel);
|
|
14386
|
+
const relative3 = import_path3.default.relative(repoRoot, fullPath);
|
|
14387
|
+
if (relative3 === "" || relative3 === ".." || relative3.startsWith(`..${import_path3.default.sep}`) || import_path3.default.isAbsolute(relative3)) {
|
|
14388
|
+
throw Object.assign(new Error(`History repair refused an indexed path outside its repository: ${rel}`), { code: "E_SANDBOX" });
|
|
14389
|
+
}
|
|
14390
|
+
assertWriteTargetNoSymlink(fullPath);
|
|
14391
|
+
try {
|
|
14392
|
+
await import_promises3.default.unlink(fullPath);
|
|
14393
|
+
} catch (error) {
|
|
14394
|
+
if (error?.code !== "ENOENT") throw error;
|
|
14395
|
+
}
|
|
14396
|
+
await import_isomorphic_git.default.remove({ fs: import_fs4.default, dir: repoDir, filepath: rel });
|
|
14397
|
+
}
|
|
14398
|
+
}
|
|
14399
|
+
async function isUnbornHistory(repoDir) {
|
|
14400
|
+
let head;
|
|
14401
|
+
try {
|
|
14402
|
+
head = await import_promises3.default.readFile(import_path3.default.join(repoDir, ".git", "HEAD"), "utf8");
|
|
14403
|
+
} catch {
|
|
14404
|
+
return false;
|
|
14405
|
+
}
|
|
14406
|
+
if (!/^ref:\s*refs\/heads\/[^\s]+\s*$/.test(head)) return false;
|
|
14407
|
+
const [branches, tags] = await Promise.all([
|
|
14408
|
+
import_isomorphic_git.default.listBranches({ fs: import_fs4.default, dir: repoDir }),
|
|
14409
|
+
import_isomorphic_git.default.listTags({ fs: import_fs4.default, dir: repoDir })
|
|
14410
|
+
]);
|
|
14411
|
+
return branches.length === 0 && tags.length === 0;
|
|
14412
|
+
}
|
|
13948
14413
|
async function snapshotIsProvenComplete(repoDir, extraFiles, written) {
|
|
13949
14414
|
if (!extraFiles) return false;
|
|
13950
14415
|
try {
|
|
@@ -13963,7 +14428,7 @@ async function applySnapshotMarker(repoDir, complete) {
|
|
|
13963
14428
|
return;
|
|
13964
14429
|
}
|
|
13965
14430
|
try {
|
|
13966
|
-
await
|
|
14431
|
+
await import_promises3.default.unlink(import_path3.default.join(repoDir, SNAPSHOT_FORMAT_MARKER));
|
|
13967
14432
|
} catch {
|
|
13968
14433
|
}
|
|
13969
14434
|
try {
|
|
@@ -13978,7 +14443,7 @@ async function applySnapshotMarker(repoDir, complete) {
|
|
|
13978
14443
|
async function chmod0700BestEffort(dir) {
|
|
13979
14444
|
if (process.platform === "win32") return;
|
|
13980
14445
|
try {
|
|
13981
|
-
await
|
|
14446
|
+
await import_promises3.default.chmod(dir, 448);
|
|
13982
14447
|
} catch {
|
|
13983
14448
|
}
|
|
13984
14449
|
}
|
|
@@ -14011,7 +14476,7 @@ async function migrateExistingTree0700(repoDir) {
|
|
|
14011
14476
|
await chmod0700BestEffort(dir);
|
|
14012
14477
|
let entries = [];
|
|
14013
14478
|
try {
|
|
14014
|
-
entries = await
|
|
14479
|
+
entries = await import_promises3.default.readdir(dir, { withFileTypes: true });
|
|
14015
14480
|
} catch {
|
|
14016
14481
|
continue;
|
|
14017
14482
|
}
|
|
@@ -14061,7 +14526,7 @@ async function getSidecarBytes(repoDir) {
|
|
|
14061
14526
|
const dir = stack.pop();
|
|
14062
14527
|
let entries = [];
|
|
14063
14528
|
try {
|
|
14064
|
-
entries = await
|
|
14529
|
+
entries = await import_promises3.default.readdir(dir, { withFileTypes: true });
|
|
14065
14530
|
} catch {
|
|
14066
14531
|
continue;
|
|
14067
14532
|
}
|
|
@@ -14074,7 +14539,7 @@ async function getSidecarBytes(repoDir) {
|
|
|
14074
14539
|
continue;
|
|
14075
14540
|
}
|
|
14076
14541
|
if (typeof entry?.isFile === "function" && !entry.isFile()) continue;
|
|
14077
|
-
const st = await
|
|
14542
|
+
const st = await import_promises3.default.stat(full);
|
|
14078
14543
|
if (st.isDirectory()) stack.push(full);
|
|
14079
14544
|
else if (st.isFile()) total += st.size;
|
|
14080
14545
|
} catch {
|
|
@@ -14092,6 +14557,116 @@ async function getHistoryCommitCount(repoDir) {
|
|
|
14092
14557
|
return 0;
|
|
14093
14558
|
}
|
|
14094
14559
|
}
|
|
14560
|
+
var GIT_INITIALIZATION_LOCK = ".astra-init.lock";
|
|
14561
|
+
function gitInitializationLockPath(repoDir) {
|
|
14562
|
+
return import_path3.default.join(repoDir, GIT_INITIALIZATION_LOCK);
|
|
14563
|
+
}
|
|
14564
|
+
function gitInitializationIncompleteError(repoDir) {
|
|
14565
|
+
return Object.assign(new Error(`History initialization is incomplete at ${repoDir}.`), {
|
|
14566
|
+
code: "E_STATE",
|
|
14567
|
+
remedy: "Stop other ASTra processes using this document, inspect the history directory, then run `astra history <file> --init` to record a baseline."
|
|
14568
|
+
});
|
|
14569
|
+
}
|
|
14570
|
+
async function gitInitializationLockExists(repoDir) {
|
|
14571
|
+
const file = gitInitializationLockPath(repoDir);
|
|
14572
|
+
try {
|
|
14573
|
+
const st = await import_promises3.default.lstat(file);
|
|
14574
|
+
if (!st.isFile() || st.isSymbolicLink()) throw Object.assign(new Error(`Unsafe history initialization lock: ${file}`), { code: "E_SANDBOX" });
|
|
14575
|
+
return true;
|
|
14576
|
+
} catch (error) {
|
|
14577
|
+
if (error?.code === "ENOENT") return false;
|
|
14578
|
+
throw error;
|
|
14579
|
+
}
|
|
14580
|
+
}
|
|
14581
|
+
async function describeInitializationLock(file) {
|
|
14582
|
+
let record = {};
|
|
14583
|
+
try {
|
|
14584
|
+
const st = await import_promises3.default.lstat(file);
|
|
14585
|
+
if (st.isFile() && !st.isSymbolicLink() && st.size <= 4096) record = JSON.parse(await import_promises3.default.readFile(file, "utf8"));
|
|
14586
|
+
} catch {
|
|
14587
|
+
}
|
|
14588
|
+
const pid = typeof record.pid === "number" && Number.isInteger(record.pid) && record.pid > 0 ? record.pid : void 0;
|
|
14589
|
+
const hostname = typeof record.hostname === "string" ? record.hostname.slice(0, 255) : void 0;
|
|
14590
|
+
const started = typeof record.startedAt === "string" ? ` since ${record.startedAt.slice(0, 40)}` : "";
|
|
14591
|
+
if (pid === void 0) return `the lock does not record its owner${started}; cannot tell whether an initializer is still running`;
|
|
14592
|
+
if (hostname === void 0) return `owner pid ${pid}, host not recorded${started}; cannot tell whether it is still running`;
|
|
14593
|
+
if (hostname !== import_os.default.hostname()) return `owner pid ${pid} on host ${hostname}${started}; this host is ${import_os.default.hostname()}, cannot tell whether it is still running`;
|
|
14594
|
+
try {
|
|
14595
|
+
process.kill(pid, 0);
|
|
14596
|
+
return `owner pid ${pid} on this host${started}; that process is running`;
|
|
14597
|
+
} catch (error) {
|
|
14598
|
+
if (error?.code === "EPERM") return `owner pid ${pid} on this host${started}; that process is running`;
|
|
14599
|
+
if (error?.code === "ESRCH") return `owner pid ${pid} on this host${started}; that process was not found (this alone does not prove the lock is stale)`;
|
|
14600
|
+
return `owner pid ${pid} on this host${started}; cannot tell whether it is still running`;
|
|
14601
|
+
}
|
|
14602
|
+
}
|
|
14603
|
+
async function gitInitializationConflictError(file) {
|
|
14604
|
+
return Object.assign(new Error(`Another or interrupted ASTra history initialization is marked at ${file} (${await describeInitializationLock(file)}).`), {
|
|
14605
|
+
code: "E_CONFLICT",
|
|
14606
|
+
remedy: `Do not delete the lock while any ASTra process (an agent, a pipeline or a CLI run) may be using this document. Once you have made sure none is, delete ${file}; if the history then has no commits, run \`astra history <file> --init\`. ASTra never removes the lock itself.`
|
|
14607
|
+
});
|
|
14608
|
+
}
|
|
14609
|
+
async function countCommitsStrict(repoDir) {
|
|
14610
|
+
await assertUsableSidecarGit(repoDir);
|
|
14611
|
+
try {
|
|
14612
|
+
return (await import_isomorphic_git.default.log({ fs: import_fs4.default, dir: repoDir })).length;
|
|
14613
|
+
} catch (error) {
|
|
14614
|
+
if (await isUnbornHistory(repoDir)) return 0;
|
|
14615
|
+
throw error;
|
|
14616
|
+
}
|
|
14617
|
+
}
|
|
14618
|
+
async function assertGitHistoryReady(repoDir) {
|
|
14619
|
+
await assertNoGitInitializationInProgress(repoDir);
|
|
14620
|
+
if (await countCommitsStrict(repoDir) === 0) throw gitInitializationIncompleteError(repoDir);
|
|
14621
|
+
}
|
|
14622
|
+
async function assertNoGitInitializationInProgress(repoDir) {
|
|
14623
|
+
if (await gitInitializationLockExists(repoDir)) throw await gitInitializationConflictError(gitInitializationLockPath(repoDir));
|
|
14624
|
+
}
|
|
14625
|
+
async function assertHistoryReadyForWrite(docPath) {
|
|
14626
|
+
const repoDir = getRepoDir(docPath);
|
|
14627
|
+
await assertNoGitInitializationInProgress(repoDir);
|
|
14628
|
+
if (import_fs4.default.existsSync(import_path3.default.join(repoDir, ".git"))) await assertGitHistoryReady(repoDir);
|
|
14629
|
+
}
|
|
14630
|
+
async function withGitInitializationLock(repoDir, run) {
|
|
14631
|
+
const file = gitInitializationLockPath(repoDir);
|
|
14632
|
+
assertWriteTargetNoSymlink(file);
|
|
14633
|
+
const token = import_crypto4.default.randomUUID();
|
|
14634
|
+
let handle;
|
|
14635
|
+
try {
|
|
14636
|
+
handle = await import_promises3.default.open(file, import_fs4.default.constants.O_WRONLY | import_fs4.default.constants.O_CREAT | import_fs4.default.constants.O_EXCL | import_fs4.default.constants.O_NOFOLLOW, 384);
|
|
14637
|
+
} catch (error) {
|
|
14638
|
+
if (error?.code === "EEXIST") throw await gitInitializationConflictError(file);
|
|
14639
|
+
throw error;
|
|
14640
|
+
}
|
|
14641
|
+
let primaryError;
|
|
14642
|
+
let result;
|
|
14643
|
+
try {
|
|
14644
|
+
await handle.writeFile(JSON.stringify({ version: 2, pid: process.pid, hostname: import_os.default.hostname(), token, startedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
14645
|
+
await handle.sync();
|
|
14646
|
+
result = await run();
|
|
14647
|
+
} catch (error) {
|
|
14648
|
+
primaryError = error;
|
|
14649
|
+
}
|
|
14650
|
+
await handle.close().catch(() => void 0);
|
|
14651
|
+
let cleanupError;
|
|
14652
|
+
try {
|
|
14653
|
+
const current = JSON.parse(await import_promises3.default.readFile(file, "utf8"));
|
|
14654
|
+
if (current.token !== token) throw Object.assign(new Error(`History initialization lock ownership changed; left in place: ${file}`), { code: "E_CONFLICT" });
|
|
14655
|
+
await import_promises3.default.unlink(file);
|
|
14656
|
+
} catch (error) {
|
|
14657
|
+
cleanupError = error;
|
|
14658
|
+
}
|
|
14659
|
+
if (cleanupError && cleanupError?.code !== "ENOENT") {
|
|
14660
|
+
if (primaryError) {
|
|
14661
|
+
const cause = primaryError instanceof Error ? primaryError.message : String(primaryError);
|
|
14662
|
+
throw Object.assign(new Error(`${cause}; initialization lock cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`), { code: "E_IO", lockPath: file });
|
|
14663
|
+
}
|
|
14664
|
+
throw Object.assign(new Error(`History initialization lock cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`), { code: "E_IO", lockPath: file });
|
|
14665
|
+
}
|
|
14666
|
+
if (cleanupError && !primaryError) throw Object.assign(new Error(`History initialization finished, but its lock file disappeared while it ran: ${file}. Another process or a cleaner may have removed it, so exclusivity is not guaranteed for this run. Inspect the history with \`astra history <file>\` before relying on it.`), { code: "E_CONFLICT" });
|
|
14667
|
+
if (primaryError) throw primaryError;
|
|
14668
|
+
return result;
|
|
14669
|
+
}
|
|
14095
14670
|
var SIDECAR_TOMBSTONE_FILENAME = ".astra-tombstones.json";
|
|
14096
14671
|
var sidecarRegenWarnings = /* @__PURE__ */ new Map();
|
|
14097
14672
|
function resolveDocIdentity(target) {
|
|
@@ -14211,7 +14786,7 @@ async function checkSidecarTombstone(target, repoDir) {
|
|
|
14211
14786
|
const key = import_path3.default.resolve(repoDir);
|
|
14212
14787
|
let hasGit = false;
|
|
14213
14788
|
try {
|
|
14214
|
-
hasGit = (await
|
|
14789
|
+
hasGit = (await import_promises3.default.stat(import_path3.default.join(repoDir, ".git"))).isDirectory();
|
|
14215
14790
|
} catch {
|
|
14216
14791
|
hasGit = false;
|
|
14217
14792
|
}
|
|
@@ -14305,14 +14880,14 @@ async function assertUsableSidecarGit(repoDir) {
|
|
|
14305
14880
|
const gitPath = import_path3.default.join(repoDir, ".git");
|
|
14306
14881
|
let entry;
|
|
14307
14882
|
try {
|
|
14308
|
-
entry = await
|
|
14883
|
+
entry = await import_promises3.default.lstat(gitPath);
|
|
14309
14884
|
} catch {
|
|
14310
14885
|
return;
|
|
14311
14886
|
}
|
|
14312
14887
|
let usable = false;
|
|
14313
14888
|
if (entry.isSymbolicLink()) {
|
|
14314
14889
|
try {
|
|
14315
|
-
usable = (await
|
|
14890
|
+
usable = (await import_promises3.default.stat(gitPath)).isDirectory();
|
|
14316
14891
|
} catch {
|
|
14317
14892
|
usable = false;
|
|
14318
14893
|
}
|
|
@@ -14330,7 +14905,7 @@ async function assertUsableSidecarGit(repoDir) {
|
|
|
14330
14905
|
async function ensureAstraGitRepo(target) {
|
|
14331
14906
|
const repoDir = isRepoDirTarget(target) ? target : getRepoDir(target);
|
|
14332
14907
|
await assertUsableSidecarGit(repoDir);
|
|
14333
|
-
await
|
|
14908
|
+
await import_promises3.default.mkdir(repoDir, { recursive: true, mode: 448 });
|
|
14334
14909
|
if (process.platform !== "win32") {
|
|
14335
14910
|
await chmod0700BestEffort(repoDir);
|
|
14336
14911
|
const parent = import_path3.default.dirname(repoDir);
|
|
@@ -14346,17 +14921,44 @@ async function ensureAstraGitRepo(target) {
|
|
|
14346
14921
|
}
|
|
14347
14922
|
return repoDir;
|
|
14348
14923
|
}
|
|
14349
|
-
async function initAstraRepo(docPath, initialXml, extraFiles) {
|
|
14924
|
+
async function initAstraRepo(docPath, initialXml, extraFiles, originalBytes) {
|
|
14350
14925
|
const repoDir = await ensureAstraGitRepo(docPath);
|
|
14351
|
-
await
|
|
14352
|
-
|
|
14353
|
-
|
|
14354
|
-
|
|
14355
|
-
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
|
|
14926
|
+
await withGitInitializationLock(repoDir, async () => {
|
|
14927
|
+
await import_isomorphic_git.default.init({ fs: import_fs4.default, dir: repoDir });
|
|
14928
|
+
let commits;
|
|
14929
|
+
try {
|
|
14930
|
+
commits = await import_isomorphic_git.default.log({ fs: import_fs4.default, dir: repoDir });
|
|
14931
|
+
} catch (error) {
|
|
14932
|
+
if (!await isUnbornHistory(repoDir)) throw error;
|
|
14933
|
+
commits = [];
|
|
14934
|
+
}
|
|
14935
|
+
if (commits.length > 0) {
|
|
14936
|
+
throw Object.assign(new Error(`History is already initialized at ${repoDir}.`), { code: "E_CONFLICT" });
|
|
14937
|
+
}
|
|
14938
|
+
await clearUncommittedPackageParts(repoDir);
|
|
14939
|
+
if (originalBytes) {
|
|
14940
|
+
const existing = await getOriginalInfo(import_path3.default.resolve(docPath));
|
|
14941
|
+
if (existing.available && existing.sha256 !== originalSha256(originalBytes)) {
|
|
14942
|
+
const snapshotPath = import_path3.default.join(repoDir, ".git", "astra-original", "original.snapshot");
|
|
14943
|
+
throw Object.assign(new Error(
|
|
14944
|
+
`History initialization refused: an interrupted earlier initialization left a retained original (sha256 ${existing.sha256}) that differs from this file (sha256 ${originalSha256(originalBytes)}), and no history was recorded.`
|
|
14945
|
+
), {
|
|
14946
|
+
code: "E_CONFLICT",
|
|
14947
|
+
reason: "original-mismatch",
|
|
14948
|
+
remedy: `Nothing was changed. The retained original may be the only copy of the earlier document: keep ${snapshotPath} (copy or rename it, do not delete it; \`astra history <file> --export-original <out>\` can still write its bytes to a new file), then run \`astra history <file> --init\` again once it is out of the way.`
|
|
14949
|
+
});
|
|
14950
|
+
}
|
|
14951
|
+
}
|
|
14952
|
+
await captureOriginal(import_path3.default.resolve(docPath), originalBytes);
|
|
14953
|
+
await writeAndAdd(repoDir, "document.xml", initialXml);
|
|
14954
|
+
const writtenParts = await writeSnapshotParts(repoDir, extraFiles);
|
|
14955
|
+
await applySnapshotMarker(repoDir, await snapshotIsProvenComplete(repoDir, extraFiles, writtenParts));
|
|
14956
|
+
await import_isomorphic_git.default.commit({
|
|
14957
|
+
fs: import_fs4.default,
|
|
14958
|
+
dir: repoDir,
|
|
14959
|
+
author: { name: "ASTra", email: "astra@attentionform.com" },
|
|
14960
|
+
message: "Initial state"
|
|
14961
|
+
});
|
|
14360
14962
|
});
|
|
14361
14963
|
try {
|
|
14362
14964
|
await checkSidecarTombstone(docPath, repoDir);
|
|
@@ -14368,6 +14970,7 @@ async function commitChange(docPath, xmlContent, message, extraFiles, deletedFil
|
|
|
14368
14970
|
const maxCommits = resolveHistoryMaxCommits(limits?.maxCommits);
|
|
14369
14971
|
const maxSidecarBytes = resolveHistoryMaxSidecarBytes(limits?.maxSidecarBytes);
|
|
14370
14972
|
const repoDir = await ensureAstraGitRepo(docPath);
|
|
14973
|
+
await assertGitHistoryReady(repoDir);
|
|
14371
14974
|
const existingCommits = await getHistoryCommitCount(repoDir);
|
|
14372
14975
|
if (existingCommits + 1 > maxCommits) {
|
|
14373
14976
|
throw Object.assign(
|
|
@@ -14446,7 +15049,7 @@ async function commitChange(docPath, xmlContent, message, extraFiles, deletedFil
|
|
|
14446
15049
|
if (isTraversalName(rel)) continue;
|
|
14447
15050
|
const fullPath = import_path3.default.join(repoDir, rel);
|
|
14448
15051
|
try {
|
|
14449
|
-
await
|
|
15052
|
+
await import_promises3.default.unlink(fullPath);
|
|
14450
15053
|
} catch {
|
|
14451
15054
|
}
|
|
14452
15055
|
try {
|
|
@@ -14469,8 +15072,27 @@ async function commitChange(docPath, xmlContent, message, extraFiles, deletedFil
|
|
|
14469
15072
|
}
|
|
14470
15073
|
return sha;
|
|
14471
15074
|
}
|
|
15075
|
+
async function getHistoryInitializationState(docPath) {
|
|
15076
|
+
const repoDir = getRepoDir(docPath);
|
|
15077
|
+
try {
|
|
15078
|
+
if (await gitInitializationLockExists(repoDir)) {
|
|
15079
|
+
return { state: "in-progress", detail: "Another ASTra process holds the history initialization lock (or it was interrupted)." };
|
|
15080
|
+
}
|
|
15081
|
+
if (!import_fs4.default.existsSync(import_path3.default.join(repoDir, ".git"))) return { state: "absent" };
|
|
15082
|
+
await assertUsableSidecarGit(repoDir);
|
|
15083
|
+
const commits = await import_isomorphic_git.default.log({ fs: import_fs4.default, dir: repoDir });
|
|
15084
|
+
if (commits.length === 0) return { state: "incomplete", detail: "History initialization is incomplete: no commit was recorded." };
|
|
15085
|
+
const root = commits[commits.length - 1];
|
|
15086
|
+
const initial = await checkoutCommit(docPath, root.oid);
|
|
15087
|
+
if (!initial.complete) return { state: "incomplete", detail: "The first history commit is not a complete package snapshot." };
|
|
15088
|
+
return { state: "complete" };
|
|
15089
|
+
} catch (error) {
|
|
15090
|
+
return { state: "incomplete", detail: error instanceof Error ? error.message : String(error) };
|
|
15091
|
+
}
|
|
15092
|
+
}
|
|
14472
15093
|
async function getHistory(docPath) {
|
|
14473
15094
|
const repoDir = getRepoDir(docPath);
|
|
15095
|
+
await assertNoGitInitializationInProgress(repoDir);
|
|
14474
15096
|
await assertUsableSidecarGit(repoDir);
|
|
14475
15097
|
let commits;
|
|
14476
15098
|
try {
|
|
@@ -14513,7 +15135,7 @@ async function listRepoPaths(repoDir) {
|
|
|
14513
15135
|
async function walk(dir, relBase) {
|
|
14514
15136
|
let entries = [];
|
|
14515
15137
|
try {
|
|
14516
|
-
entries = await
|
|
15138
|
+
entries = await import_promises3.default.readdir(dir, { withFileTypes: true });
|
|
14517
15139
|
} catch {
|
|
14518
15140
|
return;
|
|
14519
15141
|
}
|
|
@@ -14944,6 +15566,89 @@ function assertNoCellAddress(action, fields, ast) {
|
|
|
14944
15566
|
// ../engine/src/operations/table.ts
|
|
14945
15567
|
var import_xmldom12 = require("@xmldom/xmldom");
|
|
14946
15568
|
var crypto3 = __toESM(require("crypto"));
|
|
15569
|
+
|
|
15570
|
+
// ../engine/src/operations/colour.ts
|
|
15571
|
+
var COLOUR_NAMES = {
|
|
15572
|
+
red: "FF0000",
|
|
15573
|
+
green: "008000",
|
|
15574
|
+
blue: "0000FF",
|
|
15575
|
+
black: "000000",
|
|
15576
|
+
white: "FFFFFF",
|
|
15577
|
+
yellow: "FFFF00",
|
|
15578
|
+
orange: "FFA500",
|
|
15579
|
+
gray: "808080",
|
|
15580
|
+
grey: "808080",
|
|
15581
|
+
purple: "800080"
|
|
15582
|
+
};
|
|
15583
|
+
var HEX = /^#?([0-9A-Fa-f]{6})$/;
|
|
15584
|
+
function normalizeColour(input, options = {}) {
|
|
15585
|
+
if (typeof input !== "string") return void 0;
|
|
15586
|
+
const text2 = input.trim();
|
|
15587
|
+
const hex = HEX.exec(text2);
|
|
15588
|
+
if (hex) return hex[1];
|
|
15589
|
+
const lower = text2.toLowerCase();
|
|
15590
|
+
if (options.allowAuto && lower === "auto") return "auto";
|
|
15591
|
+
return Object.prototype.hasOwnProperty.call(COLOUR_NAMES, lower) ? COLOUR_NAMES[lower] : void 0;
|
|
15592
|
+
}
|
|
15593
|
+
function resolveColourInput(input, options = {}) {
|
|
15594
|
+
const value = normalizeColour(input, options);
|
|
15595
|
+
return value === void 0 ? input : value;
|
|
15596
|
+
}
|
|
15597
|
+
function colourForms(options = {}) {
|
|
15598
|
+
const names = Object.keys(COLOUR_NAMES).join(", ");
|
|
15599
|
+
return `6 hex digits such as FF0000 (optionally with #), ${options.allowAuto ? '"auto", ' : ""}or one of: ${names}`;
|
|
15600
|
+
}
|
|
15601
|
+
function colourError(field, value, options = {}) {
|
|
15602
|
+
return new Error(
|
|
15603
|
+
`E_VALIDATION: ${field} must be ${colourForms(options)}. Got ${JSON.stringify(value)}. Remedy: pass ${field} as hex, for example "FF0000", or a colour name such as "red".`
|
|
15604
|
+
);
|
|
15605
|
+
}
|
|
15606
|
+
function requireColour(field, input, options = {}) {
|
|
15607
|
+
const value = normalizeColour(input, options);
|
|
15608
|
+
if (value === void 0) throw colourError(field, input, options);
|
|
15609
|
+
return value;
|
|
15610
|
+
}
|
|
15611
|
+
var BORDER_WIDTH_MIN = 1;
|
|
15612
|
+
var BORDER_WIDTH_MAX = 96;
|
|
15613
|
+
function requireBorderSize(field, input, fallback) {
|
|
15614
|
+
if (input === void 0) return fallback;
|
|
15615
|
+
const n = typeof input === "number" ? input : typeof input === "string" && input.trim() !== "" ? Number(input) : Number.NaN;
|
|
15616
|
+
if (!Number.isInteger(n) || n < BORDER_WIDTH_MIN || n > BORDER_WIDTH_MAX) {
|
|
15617
|
+
throw new Error(
|
|
15618
|
+
`E_VALIDATION: ${field}: border width must be a whole number of eighths of a point from ${BORDER_WIDTH_MIN} to ${BORDER_WIDTH_MAX} (got ${JSON.stringify(input)}). Remedy: pass sz:4 for a 0.5 pt line, sz:8 for 1 pt, sz:96 for 12 pt (the widest).`
|
|
15619
|
+
);
|
|
15620
|
+
}
|
|
15621
|
+
return n;
|
|
15622
|
+
}
|
|
15623
|
+
function colourPattern(options = {}) {
|
|
15624
|
+
const ci = (word) => [...word].map((c) => `[${c.toLowerCase()}${c.toUpperCase()}]`).join("");
|
|
15625
|
+
const words = [...Object.keys(COLOUR_NAMES), ...options.allowAuto ? ["auto"] : []].map(ci);
|
|
15626
|
+
return `^\\s*(#?[0-9A-Fa-f]{6}|${words.join("|")})\\s*$`;
|
|
15627
|
+
}
|
|
15628
|
+
function colourDescription(options = {}) {
|
|
15629
|
+
return `A colour: ${colourForms(options)}.`;
|
|
15630
|
+
}
|
|
15631
|
+
var HIGHLIGHT_COLOURS = [
|
|
15632
|
+
"yellow",
|
|
15633
|
+
"green",
|
|
15634
|
+
"cyan",
|
|
15635
|
+
"magenta",
|
|
15636
|
+
"blue",
|
|
15637
|
+
"red",
|
|
15638
|
+
"darkBlue",
|
|
15639
|
+
"darkCyan",
|
|
15640
|
+
"darkGreen",
|
|
15641
|
+
"darkMagenta",
|
|
15642
|
+
"darkRed",
|
|
15643
|
+
"darkYellow",
|
|
15644
|
+
"darkGray",
|
|
15645
|
+
"lightGray",
|
|
15646
|
+
"black",
|
|
15647
|
+
"white",
|
|
15648
|
+
"none"
|
|
15649
|
+
];
|
|
15650
|
+
|
|
15651
|
+
// ../engine/src/operations/table.ts
|
|
14947
15652
|
function isTableReproducible() {
|
|
14948
15653
|
try {
|
|
14949
15654
|
return resolveReproducibleMode(void 0);
|
|
@@ -15343,13 +16048,13 @@ async function executeTable(opIn, ast, guide) {
|
|
|
15343
16048
|
afterIndex: Number(pick("afterIndex", op.afterIndex) ?? rows.length - 1),
|
|
15344
16049
|
cells: cellsArray,
|
|
15345
16050
|
index: Number(pick("index", op.index) ?? 0),
|
|
15346
|
-
fill: String(pick("fill", op.fill) ?? "F7F9FC").replace(/^#/, ""),
|
|
15347
|
-
headerFill: pick("headerFill", op.headerFill)?.replace(/^#/, ""),
|
|
16051
|
+
fill: String(resolveColourInput(pick("fill", op.fill) ?? "F7F9FC")).replace(/^#/, ""),
|
|
16052
|
+
headerFill: resolveColourInput(pick("headerFill", op.headerFill))?.replace(/^#/, ""),
|
|
15348
16053
|
zebra: pick("zebra", op.zebra),
|
|
15349
16054
|
// Explicit header/single-cell text color for setShading/setCellShading
|
|
15350
16055
|
// (flat or nested in params; wins over the guide per the precedence
|
|
15351
16056
|
// invariant). Validated below before any mutation.
|
|
15352
|
-
textColor: pick("textColor", op.textColor)
|
|
16057
|
+
textColor: resolveColourInput(pick("textColor", op.textColor))
|
|
15353
16058
|
};
|
|
15354
16059
|
const warnings = [];
|
|
15355
16060
|
const revisionIds = [];
|
|
@@ -15363,7 +16068,7 @@ async function executeTable(opIn, ast, guide) {
|
|
|
15363
16068
|
const normText = String(P.textColor).replace(/^#/, "");
|
|
15364
16069
|
if (!/^[0-9A-Fa-f]{6}$/.test(normText)) {
|
|
15365
16070
|
throw new Error(
|
|
15366
|
-
`E_VALIDATION: ${resolvedOp} requires textColor as
|
|
16071
|
+
`E_VALIDATION: ${resolvedOp} requires textColor as ${colourForms()} (got textColor='${String(P.textColor)}'). Remedy: pass textColor as 6 hex digits, e.g. textColor:'FFFFFF', or a colour name such as 'white'.`
|
|
15367
16072
|
);
|
|
15368
16073
|
}
|
|
15369
16074
|
explicitTextColor = normText.toUpperCase();
|
|
@@ -15945,16 +16650,8 @@ async function executeTable(opIn, ast, guide) {
|
|
|
15945
16650
|
}
|
|
15946
16651
|
case "setBorder": {
|
|
15947
16652
|
const borderType = String(pick("border", void 0) || "all");
|
|
15948
|
-
const color =
|
|
15949
|
-
const
|
|
15950
|
-
const szNum = Number(rawSz);
|
|
15951
|
-
if (!Number.isInteger(szNum) || szNum < 1) {
|
|
15952
|
-
throw toError(
|
|
15953
|
-
"E_VALIDATION",
|
|
15954
|
-
`setBorder sz must be a whole number of eighths of a point (got ${String(rawSz)}).`,
|
|
15955
|
-
"Pass sz:4 for a 0.5 pt line or sz:8 for 1 pt."
|
|
15956
|
-
);
|
|
15957
|
-
}
|
|
16653
|
+
const color = requireColour("color", pick("color", void 0) || "D1D5DB", { allowAuto: true });
|
|
16654
|
+
const szNum = requireBorderSize("setBorder sz", pick("sz", void 0) ?? raw.size, 4);
|
|
15958
16655
|
const sz = String(szNum);
|
|
15959
16656
|
const val = String(raw.val || "single");
|
|
15960
16657
|
let tblPr = directChildren5(tblEl, "w:tblPr")[0];
|
|
@@ -16212,7 +16909,7 @@ async function executeTable(opIn, ast, guide) {
|
|
|
16212
16909
|
);
|
|
16213
16910
|
}
|
|
16214
16911
|
const tc = resolveTableCell(tr, colIndex, "setCellShading");
|
|
16215
|
-
const fillInput = raw.fill ?? op.fill;
|
|
16912
|
+
const fillInput = resolveColourInput(raw.fill ?? op.fill);
|
|
16216
16913
|
if (fillInput === void 0) {
|
|
16217
16914
|
throw new Error(
|
|
16218
16915
|
`E_VALIDATION: setCellShading requires fill (got no fill). Remedy: pass fill as 6 hex digits without '#', e.g. fill:'FF0000'.`
|
|
@@ -16221,7 +16918,7 @@ async function executeTable(opIn, ast, guide) {
|
|
|
16221
16918
|
const fill = String(fillInput).replace(/^#/, "");
|
|
16222
16919
|
if (!/^[0-9A-Fa-f]{6}$/.test(fill)) {
|
|
16223
16920
|
throw new Error(
|
|
16224
|
-
`E_VALIDATION: setCellShading requires a
|
|
16921
|
+
`E_VALIDATION: setCellShading requires a fill that is ${colourForms()} (got fill='${String(fillInput)}'). Remedy: pass fill as 6 hex digits, e.g. fill:'FF0000' or fill:'#FF0000', or a colour name such as 'red'.`
|
|
16225
16922
|
);
|
|
16226
16923
|
}
|
|
16227
16924
|
const normFill = fill.toUpperCase();
|
|
@@ -16530,19 +17227,20 @@ async function executeTable(opIn, ast, guide) {
|
|
|
16530
17227
|
break;
|
|
16531
17228
|
}
|
|
16532
17229
|
case "setCellTextColor": {
|
|
16533
|
-
const colorInput = raw.color ?? op.color ?? raw.textColor ?? op.textColor;
|
|
17230
|
+
const colorInput = resolveColourInput(raw.color ?? op.color ?? raw.textColor ?? op.textColor, { allowAuto: true });
|
|
16534
17231
|
if (colorInput === void 0 || colorInput === null || String(colorInput).trim() === "") {
|
|
16535
17232
|
throw new Error(
|
|
16536
17233
|
`E_VALIDATION: setCellTextColor requires color (got no color). Remedy: pass color as 6 hex digits without '#', e.g. color:'FFFFFF' for white header text.`
|
|
16537
17234
|
);
|
|
16538
17235
|
}
|
|
16539
17236
|
const color = String(colorInput).replace(/^#/, "");
|
|
16540
|
-
|
|
17237
|
+
const autoColor = color === "auto";
|
|
17238
|
+
if (!autoColor && !/^[0-9A-Fa-f]{6}$/.test(color)) {
|
|
16541
17239
|
throw new Error(
|
|
16542
|
-
`E_VALIDATION: setCellTextColor requires
|
|
17240
|
+
`E_VALIDATION: setCellTextColor requires ${colourForms({ allowAuto: true })} (got color='${String(colorInput)}'). Remedy: pass color as 6 hex digits, e.g. color:'FFFFFF' or color:'#FFFFFF', or a colour name such as 'white'.`
|
|
16543
17241
|
);
|
|
16544
17242
|
}
|
|
16545
|
-
const normColor = color.toUpperCase();
|
|
17243
|
+
const normColor = autoColor ? "auto" : color.toUpperCase();
|
|
16546
17244
|
const cellFillOf2 = (tc) => {
|
|
16547
17245
|
const tcPr = directChildren5(tc, "w:tcPr")[0];
|
|
16548
17246
|
const shd = tcPr ? directChildren5(tcPr, "w:shd")[0] : void 0;
|
|
@@ -16581,7 +17279,7 @@ async function executeTable(opIn, ast, guide) {
|
|
|
16581
17279
|
}
|
|
16582
17280
|
}
|
|
16583
17281
|
}
|
|
16584
|
-
warnIfLowContrast(warnings, warnedContrastPairs, cellFillOf2(tc), normColor, `cell (${rIdx + 1},${cIdx + 1}) of table '${tableNode.id}'`);
|
|
17282
|
+
if (!autoColor) warnIfLowContrast(warnings, warnedContrastPairs, cellFillOf2(tc), normColor, `cell (${rIdx + 1},${cIdx + 1}) of table '${tableNode.id}'`);
|
|
16585
17283
|
};
|
|
16586
17284
|
const tRowSrc = rowIndexAlias(raw, op);
|
|
16587
17285
|
const tColSrc = colIndexAlias(raw, op);
|
|
@@ -17764,6 +18462,8 @@ function generateEmbeddedWorkbook(seriesList, categories, scatterSeriesXValues)
|
|
|
17764
18462
|
var DEFAULT_SERIES_PALETTE = ["0066CC", "10B981", "F59E0B", "EF4444", "8B5CF6", "06B6D4", "EC4899", "84CC16"];
|
|
17765
18463
|
function defaultSeriesColor(color, seriesIdx) {
|
|
17766
18464
|
if (color) {
|
|
18465
|
+
const named = normalizeColour(color);
|
|
18466
|
+
if (named !== void 0) return named;
|
|
17767
18467
|
const raw = color.replace(/^#/, "");
|
|
17768
18468
|
if (isShorthandHex(raw)) return expandShorthandHex(raw);
|
|
17769
18469
|
return raw;
|
|
@@ -18994,7 +19694,7 @@ function sameStrings(a, b) {
|
|
|
18994
19694
|
}
|
|
18995
19695
|
function normalizeHex(value) {
|
|
18996
19696
|
if (typeof value !== "string") return void 0;
|
|
18997
|
-
const raw = value.replace(/^#/, "").trim();
|
|
19697
|
+
const raw = (normalizeColour(value) ?? value.replace(/^#/, "")).trim();
|
|
18998
19698
|
return raw.length > 0 ? raw.toUpperCase() : void 0;
|
|
18999
19699
|
}
|
|
19000
19700
|
function isNumericText(value) {
|
|
@@ -19237,7 +19937,7 @@ function patchSeriesList(xml, req, patches, scope) {
|
|
|
19237
19937
|
const haveColor = normalizeHex(existing?.color);
|
|
19238
19938
|
if (wantColor !== void 0 && wantColor !== haveColor) {
|
|
19239
19939
|
const lineSeries = combo ? (patch.chartType ?? existing?.chartType) === "line" : baseLineSeries;
|
|
19240
|
-
serXml = patchSeriesColor(serXml, patch.color.replace(/^#/, ""), lineSeries, scope);
|
|
19940
|
+
serXml = patchSeriesColor(serXml, normalizeColour(patch.color) ?? patch.color.replace(/^#/, ""), lineSeries, scope);
|
|
19241
19941
|
}
|
|
19242
19942
|
pieces.push(serXml);
|
|
19243
19943
|
cursor = spans[i].end;
|
|
@@ -19746,7 +20446,7 @@ function withGuideSeriesColors(series, palette) {
|
|
|
19746
20446
|
return series.map((s, i) => {
|
|
19747
20447
|
const raw = typeof s?.color === "string" ? s.color.replace(/^#/, "") : "";
|
|
19748
20448
|
const existing = isShorthandHex(raw) ? expandShorthandHex(raw) : raw;
|
|
19749
|
-
if (/^[0-9A-Fa-f]{6}$/.test(existing)) return s;
|
|
20449
|
+
if (/^[0-9A-Fa-f]{6}$/.test(existing) || typeof s?.color === "string" && normalizeColour(s.color) !== void 0) return s;
|
|
19750
20450
|
return { ...s, color: guidePaletteColor(palette, i, taken) };
|
|
19751
20451
|
});
|
|
19752
20452
|
}
|
|
@@ -21023,10 +21723,15 @@ var twipsOrPoints = import_zod2.z.union([
|
|
|
21023
21723
|
import_zod2.z.number().finite(),
|
|
21024
21724
|
import_zod2.z.string().regex(FONT_SIZE_PT_REGEX, 'must be a number of twips (20 per point) or a string such as "12pt"')
|
|
21025
21725
|
]).describe('Twips (20 per point, so 240 is 12 pt), or a string in points such as "12pt".');
|
|
21726
|
+
var MAX_FONT_HALF_POINTS = 3276;
|
|
21026
21727
|
var fontSizeOrSzSchema = import_zod2.z.union([
|
|
21027
|
-
import_zod2.z.number().finite().nonnegative(),
|
|
21028
|
-
import_zod2.z.string().regex(FONT_SIZE_PT_REGEX, 'Font size string must match format like "12pt" or "10.5pt"')
|
|
21029
|
-
]);
|
|
21728
|
+
import_zod2.z.number().finite().nonnegative().max(MAX_FONT_HALF_POINTS, "font size must be at most 1638 pt (3276 half-points)"),
|
|
21729
|
+
import_zod2.z.string().regex(FONT_SIZE_PT_REGEX, 'Font size string must match format like "12pt" or "10.5pt"').refine((v) => (parseFontSize(v) ?? 0) <= MAX_FONT_HALF_POINTS, "font size must be at most 1638 pt (3276 half-points)")
|
|
21730
|
+
]).describe('Font size: a number of half-points (24 is 12 pt) or a string in points such as "12pt"; at most 1638 pt.');
|
|
21731
|
+
var borderSizeSchema = import_zod2.z.number("border width is a whole number of eighths of a point, e.g. 8 for 1 pt").int("border width is a whole number of eighths of a point, e.g. 8 for 1 pt").min(BORDER_WIDTH_MIN, "border width must be at least 1 (an eighth of a point)").max(BORDER_WIDTH_MAX, "border width must be at most 96 (12 pt)").describe("Border width in eighths of a point (8 is 1 pt), a whole number from 1 to 96.");
|
|
21732
|
+
function colourField(options = {}, max = 32) {
|
|
21733
|
+
return import_zod2.z.string().max(max).regex(new RegExp(colourPattern(options)), `color must be ${colourForms(options)}`).describe(colourDescription(options));
|
|
21734
|
+
}
|
|
21030
21735
|
var styleProperties = import_zod2.z.object({
|
|
21031
21736
|
bold: import_zod2.z.boolean().optional(),
|
|
21032
21737
|
italic: import_zod2.z.boolean().optional(),
|
|
@@ -21035,11 +21740,11 @@ var styleProperties = import_zod2.z.object({
|
|
|
21035
21740
|
doubleStrikethrough: import_zod2.z.boolean().optional(),
|
|
21036
21741
|
vertAlign: import_zod2.z.enum(["superscript", "subscript", "baseline"]).optional(),
|
|
21037
21742
|
alignment: import_zod2.z.enum(["left", "center", "right", "both", "justify"]).optional(),
|
|
21038
|
-
color:
|
|
21743
|
+
color: colourField({ allowAuto: true }).optional(),
|
|
21039
21744
|
fontSize: fontSizeOrSzSchema.optional(),
|
|
21040
21745
|
sz: fontSizeOrSzSchema.optional(),
|
|
21041
21746
|
fontFamily: import_zod2.z.string().max(128).optional(),
|
|
21042
|
-
highlight: import_zod2.z.
|
|
21747
|
+
highlight: import_zod2.z.enum(HIGHLIGHT_COLOURS).optional(),
|
|
21043
21748
|
lineSpacing: import_zod2.z.number().optional(),
|
|
21044
21749
|
spacingBefore: twipsOrPoints.optional(),
|
|
21045
21750
|
spacingAfter: twipsOrPoints.optional(),
|
|
@@ -21057,7 +21762,7 @@ var styleProperties = import_zod2.z.object({
|
|
|
21057
21762
|
var insertChartSeries = import_zod2.z.object({
|
|
21058
21763
|
name: import_zod2.z.string().max(500),
|
|
21059
21764
|
values: import_zod2.z.array(import_zod2.z.number()).max(1e3),
|
|
21060
|
-
color: import_zod2.z.string().max(16).optional()
|
|
21765
|
+
color: import_zod2.z.string().max(16).describe("Series colour: 6 hex digits, optionally with #, or one of: red, green, blue, black, white, yellow, orange, gray, grey, purple. Any other value is replaced by the style guide palette colour when there is one.").optional()
|
|
21061
21766
|
}).catchall(import_zod2.z.unknown());
|
|
21062
21767
|
var insertWrapMode = import_zod2.z.enum(["inline", "square", "tight", "through", "topAndBottom", "behindText", "inFrontOfText"]);
|
|
21063
21768
|
var tableCellPadding = import_zod2.z.object({
|
|
@@ -21094,7 +21799,7 @@ var insertOp = import_zod2.z.object({
|
|
|
21094
21799
|
headers: import_zod2.z.array(import_zod2.z.string().max(1e4)).max(100).optional(),
|
|
21095
21800
|
rows: import_zod2.z.array(import_zod2.z.array(import_zod2.z.string().max(1e4)).max(100)).max(1e3).optional(),
|
|
21096
21801
|
cellPadding: import_zod2.z.union([import_zod2.z.number().int().nonnegative(), tableCellPadding]).optional(),
|
|
21097
|
-
color:
|
|
21802
|
+
color: colourField({ allowAuto: true }).optional(),
|
|
21098
21803
|
thickness: import_zod2.z.number().optional(),
|
|
21099
21804
|
lineStyle: import_zod2.z.string().max(32).optional(),
|
|
21100
21805
|
spacingBefore: twipsOrPoints.optional(),
|
|
@@ -21181,11 +21886,11 @@ var styleOp = import_zod2.z.object({
|
|
|
21181
21886
|
doubleStrikethrough: import_zod2.z.boolean().optional(),
|
|
21182
21887
|
vertAlign: import_zod2.z.enum(["superscript", "subscript", "baseline"]).optional(),
|
|
21183
21888
|
alignment: import_zod2.z.enum(["left", "center", "right", "both", "justify"]).optional(),
|
|
21184
|
-
color:
|
|
21889
|
+
color: colourField({ allowAuto: true }).optional(),
|
|
21185
21890
|
fontSize: fontSizeOrSzSchema.optional(),
|
|
21186
21891
|
sz: fontSizeOrSzSchema.optional(),
|
|
21187
21892
|
fontFamily: import_zod2.z.string().max(128).optional(),
|
|
21188
|
-
highlight: import_zod2.z.
|
|
21893
|
+
highlight: import_zod2.z.enum(HIGHLIGHT_COLOURS).optional(),
|
|
21189
21894
|
lineSpacing: import_zod2.z.number().optional(),
|
|
21190
21895
|
spacingBefore: twipsOrPoints.optional(),
|
|
21191
21896
|
spacingAfter: twipsOrPoints.optional(),
|
|
@@ -21262,20 +21967,20 @@ var tableOp = import_zod2.z.object({
|
|
|
21262
21967
|
cells: import_zod2.z.array(import_zod2.z.string().max(1e5)).max(100).describe("addRow: the text of the new cells, one entry per column from the first (also accepted inside params).").optional(),
|
|
21263
21968
|
afterIndex: import_zod2.z.number().int().describe("addRow: insert after this row number; addColumn: after this column number (counting from 1; default the last).").optional(),
|
|
21264
21969
|
index: import_zod2.z.number().int().describe("deleteRow/deleteColumn: the row or column number to remove (counting from 1).").optional(),
|
|
21265
|
-
fill:
|
|
21266
|
-
headerFill:
|
|
21970
|
+
fill: colourField({}, 32).optional(),
|
|
21971
|
+
headerFill: colourField({}, 32).optional(),
|
|
21267
21972
|
zebra: import_zod2.z.boolean().optional(),
|
|
21268
21973
|
// Explicit header/single-cell text color for setShading/setCellShading
|
|
21269
21974
|
// (flat or params-nested; wins over the guide per the precedence invariant).
|
|
21270
|
-
textColor:
|
|
21975
|
+
textColor: colourField({}, 32).optional(),
|
|
21271
21976
|
widths: import_zod2.z.array(import_zod2.z.number()).max(100).optional(),
|
|
21272
21977
|
columnWidths: import_zod2.z.array(import_zod2.z.number()).max(100).optional(),
|
|
21273
21978
|
width: import_zod2.z.number().int().positive().optional(),
|
|
21274
21979
|
height: import_zod2.z.number().int().positive().optional(),
|
|
21275
21980
|
rule: import_zod2.z.enum(["atLeast", "exact", "auto"]).optional(),
|
|
21276
21981
|
border: import_zod2.z.string().max(32).optional(),
|
|
21277
|
-
color:
|
|
21278
|
-
sz:
|
|
21982
|
+
color: colourField({ allowAuto: true }).optional(),
|
|
21983
|
+
sz: borderSizeSchema.optional(),
|
|
21279
21984
|
vAlign: import_zod2.z.string().max(16).optional(),
|
|
21280
21985
|
align: tableAlignmentSchema.optional(),
|
|
21281
21986
|
alignment: tableAlignmentSchema.optional(),
|
|
@@ -21395,7 +22100,7 @@ function chartHoleSizeSchemaRemedy(detail) {
|
|
|
21395
22100
|
var chartSeries = import_zod2.z.object({
|
|
21396
22101
|
name: import_zod2.z.string().max(500),
|
|
21397
22102
|
values: import_zod2.z.array(import_zod2.z.number()).max(1e3),
|
|
21398
|
-
color: import_zod2.z.string().max(16).optional(),
|
|
22103
|
+
color: import_zod2.z.string().max(16).describe("Series colour: 6 hex digits, optionally with #, or one of: red, green, blue, black, white, yellow, orange, gray, grey, purple. Any other value is replaced by the style guide palette colour when there is one.").optional(),
|
|
21399
22104
|
chartType: import_zod2.z.enum(["column", "line"]).optional(),
|
|
21400
22105
|
axis: import_zod2.z.enum(["primary", "secondary"]).optional()
|
|
21401
22106
|
}).catchall(import_zod2.z.unknown());
|
|
@@ -21521,7 +22226,7 @@ var watermarkSizeSchema = import_zod2.z.union([import_zod2.z.number().finite().n
|
|
|
21521
22226
|
var watermarkOp = import_zod2.z.object({
|
|
21522
22227
|
action: import_zod2.z.literal("watermark"),
|
|
21523
22228
|
text: import_zod2.z.string().max(500).optional(),
|
|
21524
|
-
color: import_zod2.z.string().max(16).optional(),
|
|
22229
|
+
color: import_zod2.z.string().max(16).describe("Watermark colour: 6 hex digits, optionally with #, or one of: red, green, blue, black, white, yellow, orange, gray, grey, purple. Any other value falls back to D1D5DB.").optional(),
|
|
21525
22230
|
fontSize: watermarkSizeSchema.optional(),
|
|
21526
22231
|
sz: watermarkSizeSchema.optional(),
|
|
21527
22232
|
opacity: import_zod2.z.number().min(0).max(1).optional(),
|
|
@@ -21531,9 +22236,9 @@ var watermarkOp = import_zod2.z.object({
|
|
|
21531
22236
|
var pageBorderOp = import_zod2.z.object({
|
|
21532
22237
|
action: import_zod2.z.literal("pageBorder"),
|
|
21533
22238
|
style: import_zod2.z.string().max(32).optional(),
|
|
21534
|
-
sz:
|
|
22239
|
+
sz: borderSizeSchema.optional(),
|
|
21535
22240
|
fontSize: fontSizeOrSzSchema.optional(),
|
|
21536
|
-
color:
|
|
22241
|
+
color: colourField({ allowAuto: true }).optional(),
|
|
21537
22242
|
marginOffset: import_zod2.z.enum(["page", "text"]).optional(),
|
|
21538
22243
|
sectionIndex: sectionNumber.optional(),
|
|
21539
22244
|
expectedVersion
|
|
@@ -21871,6 +22576,32 @@ var KEY_WORDS = {
|
|
|
21871
22576
|
};
|
|
21872
22577
|
var MAX_KEYS_LISTED = 60;
|
|
21873
22578
|
var cachedShapes;
|
|
22579
|
+
function subOperationValues(shape) {
|
|
22580
|
+
for (const key of ["operation", "op"]) {
|
|
22581
|
+
let field = shape[key];
|
|
22582
|
+
for (let depth = 0; depth < 4 && field?.def; depth++) {
|
|
22583
|
+
const values = field.def.entries;
|
|
22584
|
+
if (values && typeof values === "object") return Object.keys(values);
|
|
22585
|
+
field = field.def["innerType"] ?? field.def["in"] ?? field.def["out"];
|
|
22586
|
+
}
|
|
22587
|
+
}
|
|
22588
|
+
return [];
|
|
22589
|
+
}
|
|
22590
|
+
var ACTION_VERBS = ["set", "add", "insert", "update", "edit", "create", "apply", "remove", "delete", "change", "make"];
|
|
22591
|
+
function verbPrefixCandidates(given, actions) {
|
|
22592
|
+
const key = squash(given);
|
|
22593
|
+
for (const verb of ACTION_VERBS) {
|
|
22594
|
+
if (!key.startsWith(verb) || key.length - verb.length < 3) continue;
|
|
22595
|
+
const rest = key.slice(verb.length);
|
|
22596
|
+
const target = actions.find((a) => squash(a) === rest);
|
|
22597
|
+
if (!target) continue;
|
|
22598
|
+
const out = [target];
|
|
22599
|
+
const verbAction = actions.find((a) => squash(a) === verb) ?? ACTION_WORDS[verb];
|
|
22600
|
+
if (verbAction && actions.includes(verbAction) && verbAction !== target) out.push(verbAction);
|
|
22601
|
+
return out;
|
|
22602
|
+
}
|
|
22603
|
+
return [];
|
|
22604
|
+
}
|
|
21874
22605
|
function actionShapes() {
|
|
21875
22606
|
if (cachedShapes) return cachedShapes;
|
|
21876
22607
|
const shapes = [];
|
|
@@ -21878,7 +22609,10 @@ function actionShapes() {
|
|
|
21878
22609
|
for (const option of options) {
|
|
21879
22610
|
const literal = option.shape["action"]?.def?.values?.[0];
|
|
21880
22611
|
if (typeof literal !== "string") continue;
|
|
21881
|
-
|
|
22612
|
+
const shape = { action: literal, keys: Object.keys(option.shape).filter((k) => k !== "action") };
|
|
22613
|
+
const sub = subOperationValues(option.shape);
|
|
22614
|
+
if (sub.length > 0) shape.subOperations = sub;
|
|
22615
|
+
shapes.push(shape);
|
|
21882
22616
|
}
|
|
21883
22617
|
cachedShapes = shapes;
|
|
21884
22618
|
return shapes;
|
|
@@ -21923,9 +22657,17 @@ function hintFor(operations, issue) {
|
|
|
21923
22657
|
const given = op["action"];
|
|
21924
22658
|
if (typeof given !== "string") return void 0;
|
|
21925
22659
|
const actions = shapes.map((s) => s.action);
|
|
21926
|
-
|
|
22660
|
+
let near = closestNames(given, actions, ACTION_WORDS);
|
|
22661
|
+
if (near.length === 0) near = verbPrefixCandidates(given, actions);
|
|
21927
22662
|
const parts = [`${label}: "${given}" is not an action.`];
|
|
21928
|
-
if (near.length > 0)
|
|
22663
|
+
if (near.length > 0) {
|
|
22664
|
+
parts.push(`Did you mean ${near.map((n) => `"${n}"`).join(" or ")}?`);
|
|
22665
|
+
const candidate = shapes.find((s) => s.action === near[0]);
|
|
22666
|
+
if (candidate) {
|
|
22667
|
+
parts.push(`Keys of "${candidate.action}": ${candidate.keys.slice(0, MAX_KEYS_LISTED).join(", ")}${candidate.keys.length > MAX_KEYS_LISTED ? ", ..." : ""}.`);
|
|
22668
|
+
if (candidate.subOperations) parts.push(`Operations of "${candidate.action}": ${candidate.subOperations.join(", ")}.`);
|
|
22669
|
+
}
|
|
22670
|
+
}
|
|
21929
22671
|
parts.push(`Actions: ${actions.join(", ")}.`);
|
|
21930
22672
|
return parts.join(" ");
|
|
21931
22673
|
}
|
|
@@ -22751,16 +23493,14 @@ function applyRunProps(doc, run, props) {
|
|
|
22751
23493
|
vaEl.setAttribute("w:val", props.vertAlign);
|
|
22752
23494
|
}
|
|
22753
23495
|
}
|
|
22754
|
-
if (props.color !== void 0 && props.color !== null) {
|
|
23496
|
+
if (props.color !== void 0 && props.color !== null && String(props.color).trim() !== "") {
|
|
23497
|
+
const hex = requireColour("color", props.color, { allowAuto: true });
|
|
22755
23498
|
let colorEl = rPr.getElementsByTagName("w:color")[0];
|
|
22756
23499
|
if (!colorEl) {
|
|
22757
23500
|
colorEl = createWordElement(doc, "w:color");
|
|
22758
23501
|
rPr.appendChild(colorEl);
|
|
22759
23502
|
}
|
|
22760
|
-
|
|
22761
|
-
if (hex) {
|
|
22762
|
-
colorEl.setAttribute("w:val", hex);
|
|
22763
|
-
}
|
|
23503
|
+
colorEl.setAttribute("w:val", hex);
|
|
22764
23504
|
}
|
|
22765
23505
|
const rawSz = props.sz !== void 0 ? props.sz : props.fontSize;
|
|
22766
23506
|
if (rawSz !== void 0 && rawSz !== null) {
|
|
@@ -23276,9 +24016,9 @@ function executeStyle(op, ast) {
|
|
|
23276
24016
|
pBdr.appendChild(el);
|
|
23277
24017
|
}
|
|
23278
24018
|
el.setAttribute("w:val", config.style || "single");
|
|
23279
|
-
el.setAttribute("w:sz", String(config.sz
|
|
24019
|
+
el.setAttribute("w:sz", String(requireBorderSize(`${side} border sz`, config.sz, 12)));
|
|
23280
24020
|
el.setAttribute("w:space", "1");
|
|
23281
|
-
el.setAttribute("w:color",
|
|
24021
|
+
el.setAttribute("w:color", config.color ? requireColour("color", config.color, { allowAuto: true }) : "CBD5E1");
|
|
23282
24022
|
};
|
|
23283
24023
|
if (props.borderBox) {
|
|
23284
24024
|
applyBdr("top", props.borderBox);
|
|
@@ -26277,7 +27017,7 @@ async function executeInsert(op, ast, parser2, guide, paraIdCache) {
|
|
|
26277
27017
|
const pBdr = createWordElement(doc, "w:pBdr");
|
|
26278
27018
|
const bottom = createWordElement(doc, "w:bottom");
|
|
26279
27019
|
const lineStyle = op.lineStyle || "single";
|
|
26280
|
-
const color =
|
|
27020
|
+
const color = op.color ? requireColour("color", op.color, { allowAuto: true }) : "CBD5E1";
|
|
26281
27021
|
const thickness = op.thickness !== void 0 ? String(op.thickness) : "12";
|
|
26282
27022
|
bottom.setAttribute("w:val", lineStyle);
|
|
26283
27023
|
bottom.setAttribute("w:sz", thickness);
|
|
@@ -31025,7 +31765,7 @@ function executeEndnote(op, ast, parser2) {
|
|
|
31025
31765
|
function executeWatermark(op, ast, parser2) {
|
|
31026
31766
|
const rawText = (op.text || "CONFIDENTIAL").trim();
|
|
31027
31767
|
const text2 = rawText.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
31028
|
-
const color =
|
|
31768
|
+
const color = normalizeColour(op.color) ?? "D1D5DB";
|
|
31029
31769
|
const fontSize = op.fontSize || 60;
|
|
31030
31770
|
const rotation = op.rotation !== void 0 ? op.rotation : 315;
|
|
31031
31771
|
const watermarkXml = `<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
|
@@ -31143,8 +31883,8 @@ function executePageBorder(op, ast, parser2) {
|
|
|
31143
31883
|
const pgBorders = upsertSectPrChild(sectPr, "w:pgBorders");
|
|
31144
31884
|
pgBorders.setAttribute("w:offsetFrom", op.marginOffset || "page");
|
|
31145
31885
|
const style = op.style || "single";
|
|
31146
|
-
const sz = String(op.sz
|
|
31147
|
-
const color =
|
|
31886
|
+
const sz = String(requireBorderSize("pageBorder sz", op.sz, 12));
|
|
31887
|
+
const color = op.color ? requireColour("color", op.color, { allowAuto: true }) : "CBD5E1";
|
|
31148
31888
|
["top", "left", "bottom", "right"].forEach((side) => {
|
|
31149
31889
|
let sideEl = pgBorders.getElementsByTagName(`w:${side}`)[0];
|
|
31150
31890
|
if (!sideEl) {
|
|
@@ -31666,6 +32406,7 @@ async function executeMetadata(op, ast, parser2) {
|
|
|
31666
32406
|
}
|
|
31667
32407
|
|
|
31668
32408
|
// ../engine/src/operation-engine.ts
|
|
32409
|
+
var COMPOSE_ID_IN_MESSAGE = /\b(md\d*_\d{4,})\b/;
|
|
31669
32410
|
var ENGINE_MAX_OPS = 500;
|
|
31670
32411
|
var ENGINE_MAX_BYTES = 200 * 1024 * 1024;
|
|
31671
32412
|
var EngineQuotaError = class extends Error {
|
|
@@ -32465,6 +33206,10 @@ var OperationEngine = class _OperationEngine {
|
|
|
32465
33206
|
}
|
|
32466
33207
|
}
|
|
32467
33208
|
generateHint(op, errorMessage) {
|
|
33209
|
+
const missing = errorMessage.includes("not found") && !errorMessage.includes("not found in node") ? COMPOSE_ID_IN_MESSAGE.exec(errorMessage) : null;
|
|
33210
|
+
if (missing) {
|
|
33211
|
+
return `"${missing[1]}" looks like an id from compose_document. It may be an id of a session that was closed and reopened, a mistyped id, or a node that was deleted. If the document was reopened, use the paragraph id from persistentIds in the compose response, find the text with search_content, or re-read ids with inspect_document. Table ids are renumbered by position (t_001...) after reopen.`;
|
|
33212
|
+
}
|
|
32468
33213
|
if (errorMessage.includes("not found") && !errorMessage.includes("not found in node")) {
|
|
32469
33214
|
return `Check if the target node ID is correct. Node ids are session-scoped and go stale after reopen/edit; re-read ids via inspect_document; locate current text with search_content, or append without an id via anchor:end.`;
|
|
32470
33215
|
}
|
|
@@ -34088,12 +34833,12 @@ $$` : `[Equation]
|
|
|
34088
34833
|
// ../engine/src/toon.ts
|
|
34089
34834
|
var TOON_DEFAULT_FIDELITY = "standard";
|
|
34090
34835
|
function isToonFidelity(value) {
|
|
34091
|
-
return value === "compact" || value === "standard" || value === "full";
|
|
34836
|
+
return value === "outline" || value === "compact" || value === "standard" || value === "full";
|
|
34092
34837
|
}
|
|
34093
34838
|
function assertToonFidelity(value) {
|
|
34094
34839
|
if (!isToonFidelity(value)) {
|
|
34095
34840
|
throw new Error(
|
|
34096
|
-
`Unknown TOON fidelity '${String(value)}'. Supported: compact|standard|full. Remedy: call toon({ fidelity: 'standard' }) for the default enriched outline, 'compact' for the minimal outline, or 'full' with a sectionId for rPr/pPr detail.`
|
|
34841
|
+
`Unknown TOON fidelity '${String(value)}'. Supported: outline|compact|standard|full. Remedy: call toon({ fidelity: 'standard' }) for the default enriched outline, 'compact' for the minimal table, 'outline' for headings only, or 'full' with a sectionId for rPr/pPr detail.`
|
|
34097
34842
|
);
|
|
34098
34843
|
}
|
|
34099
34844
|
}
|
|
@@ -34605,6 +35350,15 @@ function resolveSectionScope(sections, sectionId) {
|
|
|
34605
35350
|
if (sectionId.length >= MIN_NODE_ID_PREFIX_LENGTH) return pick((id) => id.startsWith(sectionId));
|
|
34606
35351
|
return [];
|
|
34607
35352
|
}
|
|
35353
|
+
var OUTLINE_HEADERS = ["ID", "L", "Style", "Text", "Section"];
|
|
35354
|
+
var OUTLINE_DEFAULT_TEXT_LENGTH = 80;
|
|
35355
|
+
function escapeOutlineCell(value) {
|
|
35356
|
+
return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
35357
|
+
}
|
|
35358
|
+
function escapeOutlinePathPart(value) {
|
|
35359
|
+
return escapeOutlineCell(value).replace(/>/g, "\\>");
|
|
35360
|
+
}
|
|
35361
|
+
var OUTLINE_PATH_PART_LENGTH = 60;
|
|
34608
35362
|
var ToonGenerator = class _ToonGenerator {
|
|
34609
35363
|
cache = /* @__PURE__ */ new Map();
|
|
34610
35364
|
checksumCache = /* @__PURE__ */ new Map();
|
|
@@ -34722,7 +35476,10 @@ var ToonGenerator = class _ToonGenerator {
|
|
|
34722
35476
|
}
|
|
34723
35477
|
const sectionPaths = this.buildSectionPaths(ast.sections);
|
|
34724
35478
|
const maxLen = options?.maxTextLength;
|
|
34725
|
-
|
|
35479
|
+
if (fidelity === "outline") {
|
|
35480
|
+
rows.push(...this.buildOutlineRows(ast, sectionsToProcess, maxLen ?? OUTLINE_DEFAULT_TEXT_LENGTH));
|
|
35481
|
+
}
|
|
35482
|
+
for (const section of fidelity === "outline" ? [] : sectionsToProcess) {
|
|
34726
35483
|
const sectionPath = sectionPaths.get(section.id) || "Root";
|
|
34727
35484
|
const sectionHeader = section.heading?.text || "";
|
|
34728
35485
|
for (const node of section.nodes) {
|
|
@@ -34762,7 +35519,7 @@ var ToonGenerator = class _ToonGenerator {
|
|
|
34762
35519
|
rows.push(row);
|
|
34763
35520
|
}
|
|
34764
35521
|
}
|
|
34765
|
-
const headers = fidelity === "full" ? ["ID", "V", "Path", "Header", "Style", "Text", "Flags", "Detail"] : ["ID", "V", "Path", "Header", "Style", "Text", "Flags"];
|
|
35522
|
+
const headers = fidelity === "outline" ? [...OUTLINE_HEADERS] : fidelity === "full" ? ["ID", "V", "Path", "Header", "Style", "Text", "Flags", "Detail"] : ["ID", "V", "Path", "Header", "Style", "Text", "Flags"];
|
|
34766
35523
|
const table = {
|
|
34767
35524
|
headers,
|
|
34768
35525
|
rows,
|
|
@@ -34821,6 +35578,20 @@ var ToonGenerator = class _ToonGenerator {
|
|
|
34821
35578
|
if (fidelity !== "compact") {
|
|
34822
35579
|
combined += `${fingerprintFmt(n)};`;
|
|
34823
35580
|
}
|
|
35581
|
+
if (fidelity === "outline") {
|
|
35582
|
+
combined += `os${n.type === "paragraph" ? ownParagraphStyleId(n.xml) ?? "-" : "-"};`;
|
|
35583
|
+
}
|
|
35584
|
+
}
|
|
35585
|
+
}
|
|
35586
|
+
if (fidelity === "outline") {
|
|
35587
|
+
combined += "oh:";
|
|
35588
|
+
for (const sec of ast.sections) {
|
|
35589
|
+
for (const n of sec.nodes) {
|
|
35590
|
+
const h = headingOfNode(n);
|
|
35591
|
+
if (!h) continue;
|
|
35592
|
+
const t = (n.metadata.text ?? "").trim();
|
|
35593
|
+
combined += `${n.id}:${h.level}:${h.style}:${t.length}:${t.slice(0, OUTLINE_PATH_PART_LENGTH + 4)};`;
|
|
35594
|
+
}
|
|
34824
35595
|
}
|
|
34825
35596
|
}
|
|
34826
35597
|
try {
|
|
@@ -34852,6 +35623,42 @@ var ToonGenerator = class _ToonGenerator {
|
|
|
34852
35623
|
}
|
|
34853
35624
|
return combined;
|
|
34854
35625
|
}
|
|
35626
|
+
/**
|
|
35627
|
+
* The `outline` tier: one row per heading, in document order. A heading is a top-level body paragraph whose own
|
|
35628
|
+
* `w:pStyle` is a heading style (`headingLevelOfStyleId`), the rule the structural summary uses. Section is the
|
|
35629
|
+
* chain of headings above it, nested by level, and is built over the whole document so a `sectionId` scope still
|
|
35630
|
+
* shows the real parents. `scope` limits which headings are listed.
|
|
35631
|
+
*/
|
|
35632
|
+
buildOutlineRows(ast, scope, maxLen) {
|
|
35633
|
+
const inScope = /* @__PURE__ */ new Set();
|
|
35634
|
+
for (const sec of scope) for (const n of sec.nodes) inScope.add(n.id);
|
|
35635
|
+
const cut = (text2, max) => max > 0 && text2.length > max ? `${text2.substring(0, max)}...` : text2;
|
|
35636
|
+
const stack = [];
|
|
35637
|
+
const rows = [];
|
|
35638
|
+
for (const sec of ast.sections) {
|
|
35639
|
+
for (const node of sec.nodes) {
|
|
35640
|
+
const heading = headingOfNode(node);
|
|
35641
|
+
if (!heading) continue;
|
|
35642
|
+
const { style, level } = heading;
|
|
35643
|
+
while (stack.length > 0 && stack[stack.length - 1].level >= level) stack.pop();
|
|
35644
|
+
const text2 = node.metadata.text.trim() || this.generatePlaceholderText(node);
|
|
35645
|
+
if (inScope.has(node.id)) {
|
|
35646
|
+
rows.push({
|
|
35647
|
+
id: node.id,
|
|
35648
|
+
version: node.version,
|
|
35649
|
+
path: stack.map((h) => escapeOutlinePathPart(cut(h.text, OUTLINE_PATH_PART_LENGTH))).join(" > "),
|
|
35650
|
+
header: "",
|
|
35651
|
+
style,
|
|
35652
|
+
text: cut(text2, maxLen),
|
|
35653
|
+
flags: "",
|
|
35654
|
+
level
|
|
35655
|
+
});
|
|
35656
|
+
}
|
|
35657
|
+
stack.push({ level, text: text2 });
|
|
35658
|
+
}
|
|
35659
|
+
}
|
|
35660
|
+
return rows;
|
|
35661
|
+
}
|
|
34855
35662
|
buildSectionPaths(sections) {
|
|
34856
35663
|
const paths = /* @__PURE__ */ new Map();
|
|
34857
35664
|
const hierarchy = [];
|
|
@@ -34988,7 +35795,58 @@ var ToonGenerator = class _ToonGenerator {
|
|
|
34988
35795
|
if (node.metadata.listLevel !== void 0) flags.push(`[L${node.metadata.listLevel + 1}]`);
|
|
34989
35796
|
return flags.join(" ");
|
|
34990
35797
|
}
|
|
35798
|
+
/**
|
|
35799
|
+
* One rendered row, wrapped to `rawWidth` when it is a usable width. Text is the cell at `textPos`; only it may
|
|
35800
|
+
* wrap: the prefix (all columns before Text, trailing separator included) and the suffix (the columns after it,
|
|
35801
|
+
* leading separator included) stay whole. The suffix rides on the LAST line so folding continuations back recovers
|
|
35802
|
+
* the default line byte-identically; a row that fits emits exactly the default line.
|
|
35803
|
+
*/
|
|
35804
|
+
renderRowLine(rowParts, textPos, sep5, textVal, rawWidth) {
|
|
35805
|
+
const width = typeof rawWidth === "number" && Number.isFinite(rawWidth) && rawWidth >= 1 ? Math.floor(rawWidth) : void 0;
|
|
35806
|
+
if (width === void 0) return `${rowParts.join(sep5)}
|
|
35807
|
+
`;
|
|
35808
|
+
const prefix = `${rowParts.slice(0, textPos).join(sep5)}${sep5}`;
|
|
35809
|
+
const tail = rowParts.slice(textPos + 1);
|
|
35810
|
+
const suffix = tail.length > 0 ? `${sep5}${tail.join(sep5)}` : "";
|
|
35811
|
+
const prefixLen = displayLen(prefix);
|
|
35812
|
+
const budget = width - prefixLen - displayLen(suffix);
|
|
35813
|
+
if (budget <= 0) return `${rowParts.join(sep5)}
|
|
35814
|
+
`;
|
|
35815
|
+
const segments = wrapTextCell(textVal, budget);
|
|
35816
|
+
if (segments.length <= 1) return `${rowParts.join(sep5)}
|
|
35817
|
+
`;
|
|
35818
|
+
const indent = " ".repeat(prefixLen);
|
|
35819
|
+
let out = `${prefix}${segments[0]}
|
|
35820
|
+
`;
|
|
35821
|
+
for (let k = 1; k < segments.length - 1; k++) out += `${indent}${segments[k]}
|
|
35822
|
+
`;
|
|
35823
|
+
out += `${indent}${segments[segments.length - 1]}${suffix}
|
|
35824
|
+
`;
|
|
35825
|
+
return out;
|
|
35826
|
+
}
|
|
35827
|
+
renderOutlineText(table, options) {
|
|
35828
|
+
const compact = options?.compact ?? false;
|
|
35829
|
+
const sep5 = compact ? "|" : " | ";
|
|
35830
|
+
let output = `> fidelity: ${table.fidelity || "lossy"} tier: outline
|
|
35831
|
+
`;
|
|
35832
|
+
if (options?.includeHeaderRow !== false) {
|
|
35833
|
+
output += `${OUTLINE_HEADERS.join(sep5)}
|
|
35834
|
+
`;
|
|
35835
|
+
if (!compact) output += `${OUTLINE_HEADERS.map(() => "---").join(" | ")}
|
|
35836
|
+
`;
|
|
35837
|
+
}
|
|
35838
|
+
for (const row of table.rows) {
|
|
35839
|
+
let text2 = row.text;
|
|
35840
|
+
if (options?.maxTextLength && options.maxTextLength > 0 && text2.length > options.maxTextLength) {
|
|
35841
|
+
text2 = `${text2.substring(0, options.maxTextLength)}...`;
|
|
35842
|
+
}
|
|
35843
|
+
const cells = [row.id, String(row.level ?? ""), escapeOutlineCell(row.style), escapeOutlineCell(text2), row.path];
|
|
35844
|
+
output += this.renderRowLine(cells, 3, sep5, cells[3], options?.width);
|
|
35845
|
+
}
|
|
35846
|
+
return output;
|
|
35847
|
+
}
|
|
34991
35848
|
renderToonText(table, options) {
|
|
35849
|
+
if (table.tier === "outline") return this.renderOutlineText(table, options);
|
|
34992
35850
|
const omits = new Set(options?.omitColumns ?? []);
|
|
34993
35851
|
const compact = options?.compact ?? false;
|
|
34994
35852
|
const tier = table.tier ?? "compact";
|
|
@@ -35036,40 +35894,8 @@ var ToonGenerator = class _ToonGenerator {
|
|
|
35036
35894
|
rowParts.push(row.detail ?? "");
|
|
35037
35895
|
}
|
|
35038
35896
|
}
|
|
35039
|
-
const sep5 = compact ? "|" : " | ";
|
|
35040
|
-
const rawWidth = options?.width;
|
|
35041
|
-
const width = typeof rawWidth === "number" && Number.isFinite(rawWidth) && rawWidth >= 1 ? Math.floor(rawWidth) : void 0;
|
|
35042
|
-
if (width === void 0) {
|
|
35043
|
-
output += `${rowParts.join(sep5)}
|
|
35044
|
-
`;
|
|
35045
|
-
continue;
|
|
35046
|
-
}
|
|
35047
35897
|
const textPos = 2 + (omits.has("path") ? 0 : 1) + (omits.has("header") ? 0 : 1) + (omits.has("style") ? 0 : 1);
|
|
35048
|
-
|
|
35049
|
-
const tail = rowParts.slice(textPos + 1);
|
|
35050
|
-
const suffix = tail.length > 0 ? `${sep5}${tail.join(sep5)}` : "";
|
|
35051
|
-
const prefixLen = displayLen(prefix);
|
|
35052
|
-
const budget = width - prefixLen - displayLen(suffix);
|
|
35053
|
-
if (budget <= 0) {
|
|
35054
|
-
output += `${rowParts.join(sep5)}
|
|
35055
|
-
`;
|
|
35056
|
-
continue;
|
|
35057
|
-
}
|
|
35058
|
-
const segments = wrapTextCell(textVal, budget);
|
|
35059
|
-
if (segments.length <= 1) {
|
|
35060
|
-
output += `${rowParts.join(sep5)}
|
|
35061
|
-
`;
|
|
35062
|
-
continue;
|
|
35063
|
-
}
|
|
35064
|
-
const indent = " ".repeat(prefixLen);
|
|
35065
|
-
output += `${prefix}${segments[0]}
|
|
35066
|
-
`;
|
|
35067
|
-
for (let k = 1; k < segments.length - 1; k++) {
|
|
35068
|
-
output += `${indent}${segments[k]}
|
|
35069
|
-
`;
|
|
35070
|
-
}
|
|
35071
|
-
output += `${indent}${segments[segments.length - 1]}${suffix}
|
|
35072
|
-
`;
|
|
35898
|
+
output += this.renderRowLine(rowParts, textPos, compact ? "|" : " | ", textVal, options?.width);
|
|
35073
35899
|
}
|
|
35074
35900
|
return output;
|
|
35075
35901
|
}
|
|
@@ -35195,7 +36021,7 @@ var ToonGenerator = class _ToonGenerator {
|
|
|
35195
36021
|
// ../engine/src/tmp.ts
|
|
35196
36022
|
var import_crypto5 = __toESM(require("crypto"));
|
|
35197
36023
|
var import_fs7 = __toESM(require("fs"));
|
|
35198
|
-
var
|
|
36024
|
+
var import_os2 = __toESM(require("os"));
|
|
35199
36025
|
var import_path6 = __toESM(require("path"));
|
|
35200
36026
|
var ASTRA_TMPDIR_ENV = "ASTRA_TMPDIR";
|
|
35201
36027
|
var TMP_SYMLINK_REMEDY = "Point $ASTRA_TMPDIR at a real directory (not a symlink), or unset it and retry.";
|
|
@@ -35251,7 +36077,7 @@ function resolveAstraTmpBase() {
|
|
|
35251
36077
|
try {
|
|
35252
36078
|
import_fs7.default.mkdirSync(dir, { recursive: true });
|
|
35253
36079
|
} catch {
|
|
35254
|
-
return
|
|
36080
|
+
return import_os2.default.tmpdir();
|
|
35255
36081
|
}
|
|
35256
36082
|
refuseSymlinkBase(dir);
|
|
35257
36083
|
refuseForeignOwner(dir);
|
|
@@ -35261,7 +36087,7 @@ function resolveAstraTmpBase() {
|
|
|
35261
36087
|
}
|
|
35262
36088
|
return dir;
|
|
35263
36089
|
}
|
|
35264
|
-
const fallback =
|
|
36090
|
+
const fallback = import_os2.default.tmpdir();
|
|
35265
36091
|
const injected = [process.env.TMPDIR, process.env.TEMP, process.env.TMP].filter((v) => typeof v === "string" && v.trim() !== "").map((v) => import_path6.default.resolve(v.trim()));
|
|
35266
36092
|
if (injected.includes(import_path6.default.resolve(fallback))) {
|
|
35267
36093
|
const dir = import_path6.default.resolve(fallback);
|
|
@@ -35301,12 +36127,12 @@ function logForensics(p, reason) {
|
|
|
35301
36127
|
try {
|
|
35302
36128
|
const rawName = import_path6.default.basename(p);
|
|
35303
36129
|
const name2 = rawName !== "" ? rawName : "artifact";
|
|
35304
|
-
let
|
|
36130
|
+
let hash2 = "unknown";
|
|
35305
36131
|
try {
|
|
35306
|
-
|
|
36132
|
+
hash2 = import_crypto5.default.createHash("sha256").update(p).digest("hex").slice(0, 16);
|
|
35307
36133
|
} catch {
|
|
35308
36134
|
}
|
|
35309
|
-
process.stderr.write(`[ASTra] tmp artifact kept for forensics: name="${name2}" sha256="${
|
|
36135
|
+
process.stderr.write(`[ASTra] tmp artifact kept for forensics: name="${name2}" sha256="${hash2}" (${reason}). Base honors $ASTRA_TMPDIR; remove manually after inspection.
|
|
35310
36136
|
`);
|
|
35311
36137
|
} catch {
|
|
35312
36138
|
}
|
|
@@ -35370,12 +36196,12 @@ function logForensicPrune(target, reason) {
|
|
|
35370
36196
|
try {
|
|
35371
36197
|
const rawName = import_path6.default.basename(target);
|
|
35372
36198
|
const name2 = rawName !== "" ? rawName : "artifact";
|
|
35373
|
-
let
|
|
36199
|
+
let hash2 = "unknown";
|
|
35374
36200
|
try {
|
|
35375
|
-
|
|
36201
|
+
hash2 = import_crypto5.default.createHash("sha256").update(target).digest("hex").slice(0, 16);
|
|
35376
36202
|
} catch {
|
|
35377
36203
|
}
|
|
35378
|
-
process.stderr.write(`[ASTra] tmp forensics prune: name="${name2}" sha256="${
|
|
36204
|
+
process.stderr.write(`[ASTra] tmp forensics prune: name="${name2}" sha256="${hash2}" (${reason}).
|
|
35379
36205
|
`);
|
|
35380
36206
|
} catch {
|
|
35381
36207
|
}
|
|
@@ -37611,9 +38437,9 @@ function escapeHtml(str) {
|
|
|
37611
38437
|
|
|
37612
38438
|
// ../engine/src/export/pdf.ts
|
|
37613
38439
|
var import_child_process = require("child_process");
|
|
37614
|
-
var
|
|
37615
|
-
var
|
|
37616
|
-
var
|
|
38440
|
+
var fs10 = __toESM(require("fs"));
|
|
38441
|
+
var fsp4 = __toESM(require("fs/promises"));
|
|
38442
|
+
var path9 = __toESM(require("path"));
|
|
37617
38443
|
var import_url = require("url");
|
|
37618
38444
|
|
|
37619
38445
|
// ../engine/src/export/preflight.ts
|
|
@@ -37954,6 +38780,136 @@ function runPreflightChecks(docx) {
|
|
|
37954
38780
|
return { blockers, warnings };
|
|
37955
38781
|
}
|
|
37956
38782
|
|
|
38783
|
+
// ../engine/src/visual/backend.ts
|
|
38784
|
+
var fs9 = __toESM(require("fs"));
|
|
38785
|
+
var path8 = __toESM(require("path"));
|
|
38786
|
+
var VISUAL_DEPENDENCY_REMEDY = "PDF render backend missing. Install LibreOffice and poppler-utils (pdftoppm): Linux: sudo apt install libreoffice poppler-utils; macOS: brew install --cask libreoffice && brew install poppler; Windows: winget install TheDocumentFoundation.LibreOffice, then install Poppler for Windows (for example scoop install poppler, or choco install poppler) and make sure pdftoppm.exe is on PATH. Alternatively inspect the XML/DOM without images (telemetry-only mode).";
|
|
38787
|
+
function createDependencyError(missing) {
|
|
38788
|
+
const which = missing.length > 0 ? missing.join(", ") : "soffice, pdftoppm";
|
|
38789
|
+
return Object.assign(new Error(`E_DEPENDENCY: headless visual backend unavailable (missing: ${which})`), {
|
|
38790
|
+
code: "E_DEPENDENCY",
|
|
38791
|
+
remedy: VISUAL_DEPENDENCY_REMEDY
|
|
38792
|
+
});
|
|
38793
|
+
}
|
|
38794
|
+
function windowsExtensions(pathExt) {
|
|
38795
|
+
const requested = (pathExt ?? ".EXE;.COM").split(";").map((e) => e.trim().toLowerCase()).filter((e) => e === ".exe" || e === ".com");
|
|
38796
|
+
const ordered = [".exe", ".com"].filter((e) => requested.includes(e));
|
|
38797
|
+
return ordered.length > 0 ? ordered : [".exe", ".com"];
|
|
38798
|
+
}
|
|
38799
|
+
function isExecutableFile(candidate) {
|
|
38800
|
+
try {
|
|
38801
|
+
fs9.accessSync(candidate, fs9.constants.X_OK);
|
|
38802
|
+
return fs9.statSync(candidate).isFile();
|
|
38803
|
+
} catch {
|
|
38804
|
+
return false;
|
|
38805
|
+
}
|
|
38806
|
+
}
|
|
38807
|
+
function candidateNames(name2, platform, pathExt) {
|
|
38808
|
+
if (platform !== "win32") return [name2];
|
|
38809
|
+
const lower = name2.toLowerCase();
|
|
38810
|
+
if (lower.endsWith(".exe") || lower.endsWith(".com")) return [name2];
|
|
38811
|
+
return windowsExtensions(pathExt).map((ext) => name2 + ext);
|
|
38812
|
+
}
|
|
38813
|
+
function installFolders(name2, platform = process.platform, env = process.env) {
|
|
38814
|
+
const bare = name2.toLowerCase().replace(/\.(exe|com)$/, "");
|
|
38815
|
+
const isOffice = bare === "soffice" || bare === "libreoffice";
|
|
38816
|
+
const isPoppler = bare === "pdftoppm";
|
|
38817
|
+
const dirs = [];
|
|
38818
|
+
if (platform === "win32") {
|
|
38819
|
+
const programFiles = env.ProgramFiles ?? "C:\\Program Files";
|
|
38820
|
+
const programFilesX86 = env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
|
38821
|
+
const localAppData = env.LOCALAPPDATA;
|
|
38822
|
+
const userProfile = env.USERPROFILE;
|
|
38823
|
+
if (isOffice) {
|
|
38824
|
+
dirs.push(path8.join(programFiles, "LibreOffice", "program"));
|
|
38825
|
+
dirs.push(path8.join(programFilesX86, "LibreOffice", "program"));
|
|
38826
|
+
if (localAppData) dirs.push(path8.join(localAppData, "Programs", "LibreOffice", "program"));
|
|
38827
|
+
}
|
|
38828
|
+
if (isPoppler) {
|
|
38829
|
+
if (userProfile) dirs.push(path8.join(userProfile, "scoop", "shims"));
|
|
38830
|
+
dirs.push(path8.join(env.ProgramData ?? "C:\\ProgramData", "chocolatey", "bin"));
|
|
38831
|
+
if (localAppData) dirs.push(path8.join(localAppData, "Microsoft", "WinGet", "Links"));
|
|
38832
|
+
}
|
|
38833
|
+
} else if (platform === "darwin") {
|
|
38834
|
+
const home = env.HOME;
|
|
38835
|
+
if (isOffice) {
|
|
38836
|
+
dirs.push("/Applications/LibreOffice.app/Contents/MacOS");
|
|
38837
|
+
if (home) dirs.push(path8.join(home, "Applications", "LibreOffice.app", "Contents", "MacOS"));
|
|
38838
|
+
}
|
|
38839
|
+
dirs.push("/opt/homebrew/bin", "/usr/local/bin", "/opt/local/bin");
|
|
38840
|
+
}
|
|
38841
|
+
return dirs;
|
|
38842
|
+
}
|
|
38843
|
+
function findOnPath(name2, pathEnv, options = {}) {
|
|
38844
|
+
try {
|
|
38845
|
+
if (!name2 || name2.trim() === "") return null;
|
|
38846
|
+
const trimmed = name2.trim();
|
|
38847
|
+
const platform = options.platform ?? process.platform;
|
|
38848
|
+
const env = options.env ?? process.env;
|
|
38849
|
+
const pathExt = options.pathExt ?? (platform === "win32" ? env.PATHEXT : void 0);
|
|
38850
|
+
const names = candidateNames(trimmed, platform, pathExt);
|
|
38851
|
+
if (trimmed.includes("/") || trimmed.includes("\\")) {
|
|
38852
|
+
for (const n of names) {
|
|
38853
|
+
const abs = path8.resolve(n);
|
|
38854
|
+
if (isExecutableFile(abs)) return abs;
|
|
38855
|
+
}
|
|
38856
|
+
return null;
|
|
38857
|
+
}
|
|
38858
|
+
const pathValue = pathEnv !== void 0 ? pathEnv : env.PATH ?? env.Path ?? "";
|
|
38859
|
+
const dirs = pathValue.split(path8.delimiter).map((d) => d.trim().replace(/^"(.*)"$/, "$1")).filter((d) => d.length > 0);
|
|
38860
|
+
for (const dir of dirs) {
|
|
38861
|
+
for (const n of names) {
|
|
38862
|
+
const candidate = path8.resolve(dir, n);
|
|
38863
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
38864
|
+
}
|
|
38865
|
+
}
|
|
38866
|
+
const useFallback = options.fallbackDirs ?? (pathEnv === void 0 && env.ASTRA_VISUAL_NO_FALLBACK_DIRS !== "1");
|
|
38867
|
+
if (useFallback) {
|
|
38868
|
+
for (const dir of options.installDirs ?? installFolders(trimmed, platform, env)) {
|
|
38869
|
+
for (const n of names) {
|
|
38870
|
+
const candidate = path8.resolve(dir, n);
|
|
38871
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
38872
|
+
}
|
|
38873
|
+
}
|
|
38874
|
+
}
|
|
38875
|
+
return null;
|
|
38876
|
+
} catch {
|
|
38877
|
+
return null;
|
|
38878
|
+
}
|
|
38879
|
+
}
|
|
38880
|
+
function resolveSoffice(override, pathEnv) {
|
|
38881
|
+
if (override && override.trim()) {
|
|
38882
|
+
const direct = findOnPath(override.trim(), pathEnv);
|
|
38883
|
+
if (direct) return direct;
|
|
38884
|
+
return null;
|
|
38885
|
+
}
|
|
38886
|
+
return findOnPath("soffice", pathEnv) ?? findOnPath("libreoffice", pathEnv);
|
|
38887
|
+
}
|
|
38888
|
+
function hasPdfBackend(override) {
|
|
38889
|
+
const pathEnv = override?.pathEnv;
|
|
38890
|
+
const sofficePath = resolveSoffice(override?.sofficeBin, pathEnv);
|
|
38891
|
+
let pdftoppmPath = null;
|
|
38892
|
+
if (override?.pdftoppmBin && override.pdftoppmBin.trim()) {
|
|
38893
|
+
pdftoppmPath = findOnPath(override.pdftoppmBin.trim(), pathEnv);
|
|
38894
|
+
} else {
|
|
38895
|
+
pdftoppmPath = findOnPath("pdftoppm", pathEnv);
|
|
38896
|
+
}
|
|
38897
|
+
const missing = [];
|
|
38898
|
+
if (!sofficePath) missing.push("soffice");
|
|
38899
|
+
if (!pdftoppmPath) missing.push("pdftoppm");
|
|
38900
|
+
if (missing.length === 0) {
|
|
38901
|
+
return { available: true, backend: "libreoffice", sofficePath, pdftoppmPath, missing: [] };
|
|
38902
|
+
}
|
|
38903
|
+
return { available: false, backend: "none", missing };
|
|
38904
|
+
}
|
|
38905
|
+
function assertPdfBackend(override) {
|
|
38906
|
+
const status = hasPdfBackend(override);
|
|
38907
|
+
if (!status.available) {
|
|
38908
|
+
throw createDependencyError(status.missing);
|
|
38909
|
+
}
|
|
38910
|
+
return { sofficePath: status.sofficePath, pdftoppmPath: status.pdftoppmPath };
|
|
38911
|
+
}
|
|
38912
|
+
|
|
37957
38913
|
// ../engine/src/export/pdf.ts
|
|
37958
38914
|
var DependencyError = class extends Error {
|
|
37959
38915
|
code = "E_DEPENDENCY";
|
|
@@ -37999,19 +38955,15 @@ function getPdfBackendBinary() {
|
|
|
37999
38955
|
if (cachedBackendBinary !== void 0) {
|
|
38000
38956
|
return cachedBackendBinary;
|
|
38001
38957
|
}
|
|
38002
|
-
const
|
|
38003
|
-
|
|
38004
|
-
|
|
38005
|
-
|
|
38006
|
-
cachedBackendBinary = bin;
|
|
38007
|
-
return bin;
|
|
38008
|
-
} catch {
|
|
38009
|
-
}
|
|
38958
|
+
const found = findOnPath("soffice") ?? findOnPath("libreoffice");
|
|
38959
|
+
if (found) {
|
|
38960
|
+
cachedBackendBinary = found;
|
|
38961
|
+
return found;
|
|
38010
38962
|
}
|
|
38011
38963
|
cachedBackendBinary = null;
|
|
38012
38964
|
return null;
|
|
38013
38965
|
}
|
|
38014
|
-
function
|
|
38966
|
+
function hasPdfBackend2() {
|
|
38015
38967
|
return getPdfBackendBinary() !== null;
|
|
38016
38968
|
}
|
|
38017
38969
|
async function convertDocxToPdf(docxBuffer, outPdfPath, options) {
|
|
@@ -38022,7 +38974,8 @@ async function convertDocxToPdf(docxBuffer, outPdfPath, options) {
|
|
|
38022
38974
|
Debian/Ubuntu: sudo apt install libreoffice
|
|
38023
38975
|
Fedora/RHEL: sudo dnf install libreoffice
|
|
38024
38976
|
macOS: brew install --cask libreoffice
|
|
38025
|
-
Windows: winget install TheDocumentFoundation.LibreOffice
|
|
38977
|
+
Windows: winget install TheDocumentFoundation.LibreOffice
|
|
38978
|
+
ASTra also looks in the standard install folders (for example C:\\Program Files\\LibreOffice\\program and /Applications/LibreOffice.app/Contents/MacOS).`
|
|
38026
38979
|
);
|
|
38027
38980
|
}
|
|
38028
38981
|
const preflight = runPreflightChecks(docxBuffer);
|
|
@@ -38072,7 +39025,7 @@ var MAX_CONVERT_ATTEMPTS = 3;
|
|
|
38072
39025
|
async function removeStagedDir(dir) {
|
|
38073
39026
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
38074
39027
|
try {
|
|
38075
|
-
await
|
|
39028
|
+
await fsp4.rm(dir, { recursive: true, force: true });
|
|
38076
39029
|
return;
|
|
38077
39030
|
} catch {
|
|
38078
39031
|
if (attempt < 2) {
|
|
@@ -38084,10 +39037,10 @@ async function removeStagedDir(dir) {
|
|
|
38084
39037
|
}
|
|
38085
39038
|
async function convertWithFilter(docxBuffer, outPdfPath, filterSpec, options, binary, stagedForensics) {
|
|
38086
39039
|
const tmpDir = secureMkdtemp("astra-pdf-");
|
|
38087
|
-
const tmpDocxPath =
|
|
39040
|
+
const tmpDocxPath = path9.join(tmpDir, `doc_${Date.now()}.docx`);
|
|
38088
39041
|
let succeeded = false;
|
|
38089
39042
|
try {
|
|
38090
|
-
await
|
|
39043
|
+
await fsp4.writeFile(tmpDocxPath, docxBuffer);
|
|
38091
39044
|
secureFile(tmpDocxPath);
|
|
38092
39045
|
let converted = false;
|
|
38093
39046
|
let lastError = null;
|
|
@@ -38096,7 +39049,7 @@ async function convertWithFilter(docxBuffer, outPdfPath, filterSpec, options, bi
|
|
|
38096
39049
|
await new Promise((resolve10, reject) => {
|
|
38097
39050
|
const child = (0, import_child_process.spawn)(
|
|
38098
39051
|
binary,
|
|
38099
|
-
[`-env:UserInstallation=${(0, import_url.pathToFileURL)(
|
|
39052
|
+
[`-env:UserInstallation=${(0, import_url.pathToFileURL)(path9.join(tmpDir, "lo_profile")).href}`, "--headless", "--convert-to", filterSpec, tmpDocxPath, "--outdir", tmpDir],
|
|
38100
39053
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
38101
39054
|
);
|
|
38102
39055
|
let stderr = "";
|
|
@@ -38132,23 +39085,23 @@ async function convertWithFilter(docxBuffer, outPdfPath, filterSpec, options, bi
|
|
|
38132
39085
|
if (!converted && lastError) {
|
|
38133
39086
|
throw lastError;
|
|
38134
39087
|
}
|
|
38135
|
-
const expectedPdfName =
|
|
38136
|
-
const generatedPdfPath =
|
|
38137
|
-
if (!
|
|
39088
|
+
const expectedPdfName = path9.basename(tmpDocxPath).replace(/\.docx$/i, ".pdf");
|
|
39089
|
+
const generatedPdfPath = path9.join(tmpDir, expectedPdfName);
|
|
39090
|
+
if (!fs10.existsSync(generatedPdfPath)) {
|
|
38138
39091
|
throw new Error(`PDF conversion failed: output file ${generatedPdfPath} was not created`);
|
|
38139
39092
|
}
|
|
38140
|
-
const targetDir =
|
|
38141
|
-
assertWriteTargetNoSymlink(
|
|
38142
|
-
await
|
|
38143
|
-
assertWriteTargetNoSymlink(
|
|
38144
|
-
await writeFileNoFollow(
|
|
38145
|
-
|
|
39093
|
+
const targetDir = path9.dirname(path9.resolve(outPdfPath));
|
|
39094
|
+
assertWriteTargetNoSymlink(path9.resolve(outPdfPath));
|
|
39095
|
+
await fsp4.mkdir(targetDir, { recursive: true });
|
|
39096
|
+
assertWriteTargetNoSymlink(path9.resolve(outPdfPath));
|
|
39097
|
+
await writeFileNoFollow(path9.resolve(outPdfPath), await fsp4.readFile(generatedPdfPath));
|
|
39098
|
+
fs10.chmodSync(path9.resolve(outPdfPath), 420);
|
|
38146
39099
|
succeeded = true;
|
|
38147
|
-
return
|
|
39100
|
+
return path9.resolve(outPdfPath);
|
|
38148
39101
|
} finally {
|
|
38149
39102
|
if (succeeded) {
|
|
38150
39103
|
try {
|
|
38151
|
-
await
|
|
39104
|
+
await fsp4.rm(tmpDir, { recursive: true, force: true });
|
|
38152
39105
|
} catch {
|
|
38153
39106
|
keepForForensics(tmpDir, "could not remove tmp dir after success");
|
|
38154
39107
|
}
|
|
@@ -38160,86 +39113,12 @@ async function convertWithFilter(docxBuffer, outPdfPath, filterSpec, options, bi
|
|
|
38160
39113
|
}
|
|
38161
39114
|
}
|
|
38162
39115
|
|
|
38163
|
-
// ../engine/src/
|
|
38164
|
-
var
|
|
38165
|
-
var path8 = __toESM(require("path"));
|
|
38166
|
-
var VISUAL_DEPENDENCY_REMEDY = "PDF render backend missing. Install LibreOffice and poppler-utils (pdftoppm): sudo apt install libreoffice poppler-utils, or brew install libreoffice poppler. Alternatively inspect the XML/DOM without images (telemetry-only mode).";
|
|
38167
|
-
function createDependencyError(missing) {
|
|
38168
|
-
const which = missing.length > 0 ? missing.join(", ") : "soffice, pdftoppm";
|
|
38169
|
-
return Object.assign(new Error(`E_DEPENDENCY: headless visual backend unavailable (missing: ${which})`), {
|
|
38170
|
-
code: "E_DEPENDENCY",
|
|
38171
|
-
remedy: VISUAL_DEPENDENCY_REMEDY
|
|
38172
|
-
});
|
|
38173
|
-
}
|
|
38174
|
-
function findOnPath(name2, pathEnv) {
|
|
38175
|
-
try {
|
|
38176
|
-
if (!name2 || name2.trim() === "") return null;
|
|
38177
|
-
const trimmed = name2.trim();
|
|
38178
|
-
if (trimmed.includes("/") || trimmed.includes("\\")) {
|
|
38179
|
-
try {
|
|
38180
|
-
const abs = path8.resolve(trimmed);
|
|
38181
|
-
fs9.accessSync(abs, fs9.constants.X_OK);
|
|
38182
|
-
const stat3 = fs9.statSync(abs);
|
|
38183
|
-
if (stat3.isFile()) return abs;
|
|
38184
|
-
return null;
|
|
38185
|
-
} catch {
|
|
38186
|
-
return null;
|
|
38187
|
-
}
|
|
38188
|
-
}
|
|
38189
|
-
const pathValue = pathEnv !== void 0 ? pathEnv : process.env.PATH ?? "";
|
|
38190
|
-
if (!pathValue) return null;
|
|
38191
|
-
const dirs = pathValue.split(path8.delimiter).filter((d) => d.length > 0);
|
|
38192
|
-
for (const dir of dirs) {
|
|
38193
|
-
try {
|
|
38194
|
-
const candidate = path8.join(dir, trimmed);
|
|
38195
|
-
fs9.accessSync(candidate, fs9.constants.X_OK);
|
|
38196
|
-
const stat3 = fs9.statSync(candidate);
|
|
38197
|
-
if (stat3.isFile()) return candidate;
|
|
38198
|
-
} catch {
|
|
38199
|
-
continue;
|
|
38200
|
-
}
|
|
38201
|
-
}
|
|
38202
|
-
return null;
|
|
38203
|
-
} catch {
|
|
38204
|
-
return null;
|
|
38205
|
-
}
|
|
38206
|
-
}
|
|
38207
|
-
function resolveSoffice(override, pathEnv) {
|
|
38208
|
-
if (override && override.trim()) {
|
|
38209
|
-
const direct = findOnPath(override.trim(), pathEnv);
|
|
38210
|
-
if (direct) return direct;
|
|
38211
|
-
return null;
|
|
38212
|
-
}
|
|
38213
|
-
return findOnPath("soffice", pathEnv) ?? findOnPath("libreoffice", pathEnv);
|
|
38214
|
-
}
|
|
38215
|
-
function hasPdfBackend2(override) {
|
|
38216
|
-
const pathEnv = override?.pathEnv;
|
|
38217
|
-
const sofficePath = resolveSoffice(override?.sofficeBin, pathEnv);
|
|
38218
|
-
let pdftoppmPath = null;
|
|
38219
|
-
if (override?.pdftoppmBin && override.pdftoppmBin.trim()) {
|
|
38220
|
-
pdftoppmPath = findOnPath(override.pdftoppmBin.trim(), pathEnv);
|
|
38221
|
-
} else {
|
|
38222
|
-
pdftoppmPath = findOnPath("pdftoppm", pathEnv);
|
|
38223
|
-
}
|
|
38224
|
-
const missing = [];
|
|
38225
|
-
if (!sofficePath) missing.push("soffice");
|
|
38226
|
-
if (!pdftoppmPath) missing.push("pdftoppm");
|
|
38227
|
-
if (missing.length === 0) {
|
|
38228
|
-
return { available: true, backend: "libreoffice", sofficePath, pdftoppmPath, missing: [] };
|
|
38229
|
-
}
|
|
38230
|
-
return { available: false, backend: "none", missing };
|
|
38231
|
-
}
|
|
38232
|
-
function assertPdfBackend(override) {
|
|
38233
|
-
const status = hasPdfBackend2(override);
|
|
38234
|
-
if (!status.available) {
|
|
38235
|
-
throw createDependencyError(status.missing);
|
|
38236
|
-
}
|
|
38237
|
-
return { sofficePath: status.sofficePath, pdftoppmPath: status.pdftoppmPath };
|
|
38238
|
-
}
|
|
39116
|
+
// ../engine/src/export/pdf-source.ts
|
|
39117
|
+
var STRICT_UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
38239
39118
|
|
|
38240
39119
|
// ../engine/src/visual/renderer.ts
|
|
38241
|
-
var
|
|
38242
|
-
var
|
|
39120
|
+
var fs11 = __toESM(require("fs"));
|
|
39121
|
+
var path10 = __toESM(require("path"));
|
|
38243
39122
|
var import_url2 = require("url");
|
|
38244
39123
|
var import_child_process2 = require("child_process");
|
|
38245
39124
|
var import_util = require("util");
|
|
@@ -38250,13 +39129,13 @@ var VISUAL_DEFAULT_MAX_PAGES = 4;
|
|
|
38250
39129
|
var VISUAL_MIN_DPI = 72;
|
|
38251
39130
|
var VISUAL_MAX_DPI = 300;
|
|
38252
39131
|
function getVisualArtifactsBaseDir() {
|
|
38253
|
-
const dir =
|
|
39132
|
+
const dir = path10.join(resolveAstraTmpBase(), "ASTra", "visual");
|
|
38254
39133
|
try {
|
|
38255
|
-
|
|
39134
|
+
fs11.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
38256
39135
|
} catch {
|
|
38257
39136
|
}
|
|
38258
39137
|
try {
|
|
38259
|
-
secureDir(
|
|
39138
|
+
secureDir(path10.join(resolveAstraTmpBase(), "ASTra"));
|
|
38260
39139
|
} catch {
|
|
38261
39140
|
}
|
|
38262
39141
|
secureDir(dir);
|
|
@@ -38266,9 +39145,9 @@ function getVisualArtifactsDir(subDir) {
|
|
|
38266
39145
|
const base = getVisualArtifactsBaseDir();
|
|
38267
39146
|
if (!subDir || subDir.trim() === "") return base;
|
|
38268
39147
|
const safe = subDir.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 64) || "run";
|
|
38269
|
-
const dir =
|
|
39148
|
+
const dir = path10.join(base, safe);
|
|
38270
39149
|
try {
|
|
38271
|
-
|
|
39150
|
+
fs11.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
38272
39151
|
} catch {
|
|
38273
39152
|
}
|
|
38274
39153
|
secureDir(dir);
|
|
@@ -38302,7 +39181,7 @@ function parsePageNumber(fileName) {
|
|
|
38302
39181
|
function listRenderedPages(workDir, prefixBase) {
|
|
38303
39182
|
let entries = [];
|
|
38304
39183
|
try {
|
|
38305
|
-
entries =
|
|
39184
|
+
entries = fs11.readdirSync(workDir);
|
|
38306
39185
|
} catch {
|
|
38307
39186
|
return [];
|
|
38308
39187
|
}
|
|
@@ -38311,7 +39190,7 @@ function listRenderedPages(workDir, prefixBase) {
|
|
|
38311
39190
|
if (!name2.startsWith(prefixBase)) continue;
|
|
38312
39191
|
const page = parsePageNumber(name2);
|
|
38313
39192
|
if (page === null) continue;
|
|
38314
|
-
out.push({ page, file:
|
|
39193
|
+
out.push({ page, file: path10.join(workDir, name2) });
|
|
38315
39194
|
}
|
|
38316
39195
|
out.sort((a, b) => a.page - b.page);
|
|
38317
39196
|
return out;
|
|
@@ -38325,7 +39204,7 @@ function sandboxError(message, remedy) {
|
|
|
38325
39204
|
function canonicalVisualBaseDir() {
|
|
38326
39205
|
const base = getVisualArtifactsBaseDir();
|
|
38327
39206
|
try {
|
|
38328
|
-
return
|
|
39207
|
+
return fs11.realpathSync(base);
|
|
38329
39208
|
} catch {
|
|
38330
39209
|
return base;
|
|
38331
39210
|
}
|
|
@@ -38349,7 +39228,7 @@ function resolveJailedVisualOutDir(rawOutDir) {
|
|
|
38349
39228
|
if (trimmed.includes("\n") || trimmed.includes("\r")) {
|
|
38350
39229
|
throw sandboxError("E_SANDBOX: visual outDir contains a forbidden line break.", "Remove line breaks from the path and retry.");
|
|
38351
39230
|
}
|
|
38352
|
-
const resolved =
|
|
39231
|
+
const resolved = path10.resolve(trimmed);
|
|
38353
39232
|
const canonical = canonicalizeForAsset(resolved);
|
|
38354
39233
|
const canonicalBase = canonicalVisualBaseDir();
|
|
38355
39234
|
assertVisualInsideBase(canonical, trimmed, canonicalBase);
|
|
@@ -38381,7 +39260,7 @@ async function renderDocxToImages(docxBuffer, opts = {}) {
|
|
|
38381
39260
|
outDir = getVisualArtifactsDir(runId);
|
|
38382
39261
|
}
|
|
38383
39262
|
try {
|
|
38384
|
-
|
|
39263
|
+
fs11.mkdirSync(outDir, { recursive: true });
|
|
38385
39264
|
} catch (err) {
|
|
38386
39265
|
const msg = err instanceof Error ? err.message : String(err);
|
|
38387
39266
|
throw validationError(`E_VALIDATION: cannot create artifacts dir "${outDir}": ${msg}`, "Pass a writable outDir and retry.");
|
|
@@ -38392,16 +39271,16 @@ async function renderDocxToImages(docxBuffer, opts = {}) {
|
|
|
38392
39271
|
outDir = live;
|
|
38393
39272
|
}
|
|
38394
39273
|
const workDir = secureMkdtemp("astra-visual-");
|
|
38395
|
-
const inputDocx =
|
|
39274
|
+
const inputDocx = path10.join(workDir, "input.docx");
|
|
38396
39275
|
let succeeded = false;
|
|
38397
39276
|
try {
|
|
38398
|
-
|
|
39277
|
+
fs11.writeFileSync(inputDocx, docxBuffer);
|
|
38399
39278
|
secureFile(inputDocx);
|
|
38400
39279
|
let lastErr;
|
|
38401
39280
|
let converted = false;
|
|
38402
39281
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
38403
39282
|
try {
|
|
38404
|
-
await execFileAsync(sofficePath, [`-env:UserInstallation=${(0, import_url2.pathToFileURL)(
|
|
39283
|
+
await execFileAsync(sofficePath, [`-env:UserInstallation=${(0, import_url2.pathToFileURL)(path10.join(workDir, "lo_profile")).href}`, "--headless", "--convert-to", "pdf", "--outdir", workDir, inputDocx], {
|
|
38405
39284
|
timeout: timeoutMs
|
|
38406
39285
|
});
|
|
38407
39286
|
converted = true;
|
|
@@ -38419,17 +39298,17 @@ async function renderDocxToImages(docxBuffer, opts = {}) {
|
|
|
38419
39298
|
}
|
|
38420
39299
|
let pdfPath = null;
|
|
38421
39300
|
try {
|
|
38422
|
-
const names =
|
|
39301
|
+
const names = fs11.readdirSync(workDir).filter((n) => n.toLowerCase().endsWith(".pdf"));
|
|
38423
39302
|
names.sort((a, b) => a === "input.pdf" ? -1 : b === "input.pdf" ? 1 : a.localeCompare(b));
|
|
38424
|
-
if (names.length > 0) pdfPath =
|
|
39303
|
+
if (names.length > 0) pdfPath = path10.join(workDir, names[0]);
|
|
38425
39304
|
} catch {
|
|
38426
39305
|
pdfPath = null;
|
|
38427
39306
|
}
|
|
38428
|
-
if (!pdfPath || !
|
|
39307
|
+
if (!pdfPath || !fs11.existsSync(pdfPath)) {
|
|
38429
39308
|
throw validationError("E_VALIDATION: libreoffice produced no PDF output.", "Verify the .docx opens in Word/LibreOffice and retry.");
|
|
38430
39309
|
}
|
|
38431
39310
|
const prefixBase = "page";
|
|
38432
|
-
const prefix =
|
|
39311
|
+
const prefix = path10.join(workDir, prefixBase);
|
|
38433
39312
|
const ppmArgs = format === "png" ? ["-png", "-r", String(dpi), pdfPath, prefix] : ["-jpeg", "-jpegopt", `quality=${jpegQuality}`, "-r", String(dpi), pdfPath, prefix];
|
|
38434
39313
|
try {
|
|
38435
39314
|
await execFileAsync(pdftoppmPath, ppmArgs, { timeout: Math.min(timeoutMs, 6e4) });
|
|
@@ -38448,15 +39327,15 @@ async function renderDocxToImages(docxBuffer, opts = {}) {
|
|
|
38448
39327
|
const ext = format === "png" ? "png" : "jpg";
|
|
38449
39328
|
for (const entry of selected) {
|
|
38450
39329
|
const destName = `page-${entry.page}.${ext}`;
|
|
38451
|
-
const dest =
|
|
38452
|
-
if (!isPathInsideDir(
|
|
39330
|
+
const dest = path10.join(outDir, destName);
|
|
39331
|
+
if (!isPathInsideDir(path10.resolve(dest), path10.resolve(outDir))) {
|
|
38453
39332
|
throw sandboxError(
|
|
38454
39333
|
`E_SANDBOX: visual artifact destination escapes outDir: "${dest}".`,
|
|
38455
39334
|
"Pass an outDir inside the visual artifacts dir (use getVisualArtifactsDir(subDir) for a run-scoped dir) and retry."
|
|
38456
39335
|
);
|
|
38457
39336
|
}
|
|
38458
39337
|
try {
|
|
38459
|
-
|
|
39338
|
+
fs11.copyFileSync(entry.file, dest);
|
|
38460
39339
|
secureFile(dest);
|
|
38461
39340
|
} catch (err) {
|
|
38462
39341
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -38465,7 +39344,7 @@ async function renderDocxToImages(docxBuffer, opts = {}) {
|
|
|
38465
39344
|
const canonicalDest = canonicalizeForAsset(dest);
|
|
38466
39345
|
if (!isPathInsideDir(canonicalDest, canonicalizeForAsset(outDir))) {
|
|
38467
39346
|
try {
|
|
38468
|
-
|
|
39347
|
+
fs11.unlinkSync(dest);
|
|
38469
39348
|
} catch {
|
|
38470
39349
|
}
|
|
38471
39350
|
throw sandboxError(
|
|
@@ -38475,7 +39354,7 @@ async function renderDocxToImages(docxBuffer, opts = {}) {
|
|
|
38475
39354
|
}
|
|
38476
39355
|
let bytes = 0;
|
|
38477
39356
|
try {
|
|
38478
|
-
bytes =
|
|
39357
|
+
bytes = fs11.statSync(dest).size;
|
|
38479
39358
|
} catch {
|
|
38480
39359
|
bytes = 0;
|
|
38481
39360
|
}
|
|
@@ -38625,8 +39504,18 @@ function computeDocStats(ast) {
|
|
|
38625
39504
|
if (hits) words += hits.length;
|
|
38626
39505
|
}
|
|
38627
39506
|
}
|
|
38628
|
-
const
|
|
38629
|
-
|
|
39507
|
+
const pageTerms = {
|
|
39508
|
+
words: words / 450,
|
|
39509
|
+
tables: (tables + callouts) * 0.45,
|
|
39510
|
+
images: images * 0.5,
|
|
39511
|
+
pageBreaks
|
|
39512
|
+
};
|
|
39513
|
+
const pageBudget = pageTerms.words + pageTerms.tables + pageTerms.images + pageTerms.pageBreaks;
|
|
39514
|
+
return { words, paragraphs, pages: Math.max(1, Math.ceil(pageBudget)), pageTerms, tables, callouts, images };
|
|
39515
|
+
}
|
|
39516
|
+
function roundedPageTerms(terms) {
|
|
39517
|
+
const r = (n) => Math.round(n * 100) / 100;
|
|
39518
|
+
return { words: r(terms.words), tables: r(terms.tables), images: r(terms.images), pageBreaks: r(terms.pageBreaks) };
|
|
38630
39519
|
}
|
|
38631
39520
|
|
|
38632
39521
|
// ../engine/src/visual/telemetry.ts
|
|
@@ -40219,7 +41108,7 @@ function assertTemplateSeed(parser2, template) {
|
|
|
40219
41108
|
}
|
|
40220
41109
|
if (!matched) {
|
|
40221
41110
|
throw new Error(
|
|
40222
|
-
`E_VALIDATION: line 1: open document does not hold the ${template} seed ("${marker}"). Remedy:
|
|
41111
|
+
`E_VALIDATION: line 1: open document does not hold the ${template} seed ("${marker}"). Remedy: to add to this document, call compose_document again without template; to start from the ${template} template, call new_document with template "${template}" first and compose into that.`
|
|
40223
41112
|
);
|
|
40224
41113
|
}
|
|
40225
41114
|
}
|
|
@@ -40791,9 +41680,39 @@ function summarizeStyleGuide(guide) {
|
|
|
40791
41680
|
return out;
|
|
40792
41681
|
}
|
|
40793
41682
|
|
|
41683
|
+
// ../engine/src/restore-original.ts
|
|
41684
|
+
var import_node_fs2 = __toESM(require("node:fs"));
|
|
41685
|
+
var import_promises4 = __toESM(require("node:fs/promises"));
|
|
41686
|
+
var import_node_path2 = __toESM(require("node:path"));
|
|
41687
|
+
var import_isomorphic_git2 = __toESM(require("isomorphic-git"));
|
|
41688
|
+
async function captureHistoryState(docPath) {
|
|
41689
|
+
const target = import_node_path2.default.resolve(docPath);
|
|
41690
|
+
const [head, index] = await Promise.all([
|
|
41691
|
+
getHistory(target).then((entries) => entries[0]?.oid, () => void 0),
|
|
41692
|
+
import_isomorphic_git2.default.statusMatrix({ fs: import_node_fs2.default, dir: getRepoDir(target) }).then((matrix) => JSON.stringify(matrix), () => void 0)
|
|
41693
|
+
]);
|
|
41694
|
+
return { head, index };
|
|
41695
|
+
}
|
|
41696
|
+
async function historyChangedSince(docPath, before) {
|
|
41697
|
+
const after = await captureHistoryState(docPath);
|
|
41698
|
+
if (before.head === void 0 || after.head === void 0 || before.index === void 0 || after.index === void 0) return true;
|
|
41699
|
+
return after.head !== before.head || after.index !== before.index;
|
|
41700
|
+
}
|
|
41701
|
+
async function rollbackReplacedFile(docPath, previous, failure) {
|
|
41702
|
+
const outcome = failure.error?.outcome;
|
|
41703
|
+
if (!failure.replaced && outcome !== "replaced" && outcome !== "uncertain") return { recovered: true };
|
|
41704
|
+
try {
|
|
41705
|
+
await replaceFileAtomic(docPath, previous);
|
|
41706
|
+
if (originalSha256(await import_promises4.default.readFile(docPath)) === originalSha256(previous)) return { recovered: true };
|
|
41707
|
+
return { recovered: false, failure: "the file does not match the previous bytes after rollback" };
|
|
41708
|
+
} catch (error) {
|
|
41709
|
+
return { recovered: false, failure: error instanceof Error ? error.message : String(error) };
|
|
41710
|
+
}
|
|
41711
|
+
}
|
|
41712
|
+
|
|
40794
41713
|
// src/lib/mcp/baseline-gate.ts
|
|
40795
41714
|
var crypto13 = __toESM(require("crypto"));
|
|
40796
|
-
var
|
|
41715
|
+
var fsp6 = __toESM(require("fs/promises"));
|
|
40797
41716
|
|
|
40798
41717
|
// src/lib/mcp/errors.ts
|
|
40799
41718
|
function mcpError(code, message, remedy) {
|
|
@@ -40873,8 +41792,8 @@ function invalidInput(message) {
|
|
|
40873
41792
|
}
|
|
40874
41793
|
|
|
40875
41794
|
// src/lib/mcp/diff.ts
|
|
40876
|
-
var
|
|
40877
|
-
var
|
|
41795
|
+
var fs14 = __toESM(require("fs"));
|
|
41796
|
+
var path13 = __toESM(require("path"));
|
|
40878
41797
|
var DISCARDED_NODE_IDS_MAX = 20;
|
|
40879
41798
|
var AUDIT_NODE_IDS_MAX = 100;
|
|
40880
41799
|
var AUDIT_FILES_MAX = 100;
|
|
@@ -41241,7 +42160,7 @@ function pathDirname(p) {
|
|
|
41241
42160
|
}
|
|
41242
42161
|
function sidecarRepoExists(docPath) {
|
|
41243
42162
|
try {
|
|
41244
|
-
return
|
|
42163
|
+
return fs14.existsSync(path13.join(getRepoDir(docPath), ".git"));
|
|
41245
42164
|
} catch {
|
|
41246
42165
|
return false;
|
|
41247
42166
|
}
|
|
@@ -41280,7 +42199,7 @@ async function resolveBaseline(handle) {
|
|
|
41280
42199
|
if (handle.validationBaseline !== void 0) return handle.validationBaseline;
|
|
41281
42200
|
if (!handle.openSha256) return null;
|
|
41282
42201
|
try {
|
|
41283
|
-
const bytes = await
|
|
42202
|
+
const bytes = await fsp6.readFile(handle.filePath);
|
|
41284
42203
|
if (sha256Hex(bytes) !== handle.openSha256) return null;
|
|
41285
42204
|
const baseline = buildBaseline(await validateForBaseline(bytes));
|
|
41286
42205
|
handle.validationBaseline = baseline;
|
|
@@ -41364,7 +42283,7 @@ function withToleratedNote(message, preExisting) {
|
|
|
41364
42283
|
async function validateAtOpen(handle, timeoutMs) {
|
|
41365
42284
|
let bytes;
|
|
41366
42285
|
try {
|
|
41367
|
-
bytes = await
|
|
42286
|
+
bytes = await fsp6.readFile(handle.filePath);
|
|
41368
42287
|
} catch (err) {
|
|
41369
42288
|
return { status: "skipped", note: `the file could not be read for validation: ${err instanceof Error ? err.message : String(err)}` };
|
|
41370
42289
|
}
|
|
@@ -41408,10 +42327,10 @@ async function validateAtOpen(handle, timeoutMs) {
|
|
|
41408
42327
|
}
|
|
41409
42328
|
|
|
41410
42329
|
// src/lib/session-store.ts
|
|
41411
|
-
var
|
|
41412
|
-
var
|
|
41413
|
-
var
|
|
41414
|
-
var
|
|
42330
|
+
var fs15 = __toESM(require("fs"));
|
|
42331
|
+
var fsp7 = __toESM(require("fs/promises"));
|
|
42332
|
+
var os3 = __toESM(require("os"));
|
|
42333
|
+
var path14 = __toESM(require("path"));
|
|
41415
42334
|
var import_crypto6 = require("crypto");
|
|
41416
42335
|
var MCP_SESSION_MAX_DOCS = 10;
|
|
41417
42336
|
var MCP_SESSION_MAX_BYTES = 200 * 1024 * 1024;
|
|
@@ -41609,19 +42528,19 @@ function getMcpAllowedRoots() {
|
|
|
41609
42528
|
if (named.length > 0) return [...new Set(named)];
|
|
41610
42529
|
const roots = [];
|
|
41611
42530
|
try {
|
|
41612
|
-
roots.push(
|
|
42531
|
+
roots.push(path14.resolve(process.cwd(), "documents"));
|
|
41613
42532
|
} catch {
|
|
41614
42533
|
}
|
|
41615
42534
|
try {
|
|
41616
|
-
roots.push(
|
|
42535
|
+
roots.push(path14.resolve(os3.tmpdir()));
|
|
41617
42536
|
} catch {
|
|
41618
42537
|
}
|
|
41619
42538
|
try {
|
|
41620
|
-
roots.push(
|
|
42539
|
+
roots.push(path14.resolve(process.cwd(), "documents", "Drafts"));
|
|
41621
42540
|
} catch {
|
|
41622
42541
|
}
|
|
41623
42542
|
try {
|
|
41624
|
-
roots.push(
|
|
42543
|
+
roots.push(path14.resolve(os3.tmpdir(), "ASTra"));
|
|
41625
42544
|
} catch {
|
|
41626
42545
|
}
|
|
41627
42546
|
return [...new Set(roots)];
|
|
@@ -41638,12 +42557,12 @@ function getExistingExtraRoots() {
|
|
|
41638
42557
|
const raw = (process.env.ASTRA_EXTRA_DOC_ROOTS || "").trim();
|
|
41639
42558
|
if (!raw) return [];
|
|
41640
42559
|
const out = [];
|
|
41641
|
-
for (const part of raw.split(
|
|
42560
|
+
for (const part of raw.split(path14.delimiter)) {
|
|
41642
42561
|
const p = part.trim();
|
|
41643
42562
|
if (!p) continue;
|
|
41644
42563
|
try {
|
|
41645
|
-
const resolved =
|
|
41646
|
-
const stat3 =
|
|
42564
|
+
const resolved = path14.resolve(p);
|
|
42565
|
+
const stat3 = fs15.statSync(resolved);
|
|
41647
42566
|
if (!stat3.isDirectory()) continue;
|
|
41648
42567
|
out.push(resolved);
|
|
41649
42568
|
} catch {
|
|
@@ -41655,7 +42574,7 @@ function findMcpContainingRoot(canonical) {
|
|
|
41655
42574
|
for (const root of getMcpAllowedRoots()) {
|
|
41656
42575
|
let canonicalRoot = root;
|
|
41657
42576
|
try {
|
|
41658
|
-
canonicalRoot =
|
|
42577
|
+
canonicalRoot = fs15.realpathSync(root);
|
|
41659
42578
|
} catch {
|
|
41660
42579
|
}
|
|
41661
42580
|
if (isPathInsideDir(canonical, canonicalRoot) || isPathInsideDir(canonical, root)) {
|
|
@@ -41665,8 +42584,8 @@ function findMcpContainingRoot(canonical) {
|
|
|
41665
42584
|
return void 0;
|
|
41666
42585
|
}
|
|
41667
42586
|
function assertNoDotSegments(canonical, root) {
|
|
41668
|
-
const rel =
|
|
41669
|
-
const segs = rel.split(
|
|
42587
|
+
const rel = path14.relative(root, canonical);
|
|
42588
|
+
const segs = rel.split(path14.sep).filter((s) => s && s !== "." && s !== "..");
|
|
41670
42589
|
for (const seg of segs) {
|
|
41671
42590
|
if (seg.startsWith(".")) {
|
|
41672
42591
|
throw Object.assign(new Error(`Document path traverses a hidden directory "${seg}"`), {
|
|
@@ -41675,7 +42594,7 @@ function assertNoDotSegments(canonical, root) {
|
|
|
41675
42594
|
});
|
|
41676
42595
|
}
|
|
41677
42596
|
}
|
|
41678
|
-
const base =
|
|
42597
|
+
const base = path14.basename(canonical);
|
|
41679
42598
|
if (base.startsWith(".")) {
|
|
41680
42599
|
throw Object.assign(new Error("Document path must not use a hidden (dot-prefixed) file name"), {
|
|
41681
42600
|
code: "E_SANDBOX",
|
|
@@ -41716,8 +42635,8 @@ function validateDocPath(rawPath) {
|
|
|
41716
42635
|
remedy: `Use a path ${describeAllowedFolders()}.`
|
|
41717
42636
|
});
|
|
41718
42637
|
}
|
|
41719
|
-
const resolved =
|
|
41720
|
-
const resolvedSegments = resolved.split(
|
|
42638
|
+
const resolved = path14.resolve(trimmed);
|
|
42639
|
+
const resolvedSegments = resolved.split(path14.sep);
|
|
41721
42640
|
if (resolvedSegments.includes("..")) {
|
|
41722
42641
|
throw Object.assign(new Error("Document path resolves outside its directory"), {
|
|
41723
42642
|
code: "E_SANDBOX",
|
|
@@ -41736,7 +42655,7 @@ function validateDocPath(rawPath) {
|
|
|
41736
42655
|
);
|
|
41737
42656
|
}
|
|
41738
42657
|
assertNoDotSegments(canonical, containingRoot);
|
|
41739
|
-
const ext =
|
|
42658
|
+
const ext = path14.extname(trimmed).toLowerCase();
|
|
41740
42659
|
if (ext !== ".docx") {
|
|
41741
42660
|
throw Object.assign(new Error(`Document path must use a .docx extension (got "${ext || "(none)"}")`), {
|
|
41742
42661
|
code: "E_VALIDATION",
|
|
@@ -41758,9 +42677,9 @@ function validateOpenBufferLabel(rawLabel) {
|
|
|
41758
42677
|
remedy: "Pass a plain memory label such as memory.docx."
|
|
41759
42678
|
});
|
|
41760
42679
|
}
|
|
41761
|
-
if (
|
|
42680
|
+
if (path14.isAbsolute(trimmed)) return validateDocPath(trimmed);
|
|
41762
42681
|
if (trimmed.includes("\n") || trimmed.includes("\r")) return fallback;
|
|
41763
|
-
const base =
|
|
42682
|
+
const base = path14.basename(trimmed.replace(/\\/g, "/"));
|
|
41764
42683
|
if (!base || base === "." || base === "..") return fallback;
|
|
41765
42684
|
const clean = base.slice(0, 128);
|
|
41766
42685
|
return clean.toLowerCase().endsWith(".docx") ? clean : fallback;
|
|
@@ -41774,29 +42693,141 @@ function collectTrackedExtrasForGit(parser2) {
|
|
|
41774
42693
|
}
|
|
41775
42694
|
return extras;
|
|
41776
42695
|
} catch (err) {
|
|
41777
|
-
|
|
41778
|
-
console.error(`[collectTrackedExtrasForGit] snapshot failed: ${msg}`);
|
|
41779
|
-
return {};
|
|
42696
|
+
throw snapshotCollectionError(err);
|
|
41780
42697
|
}
|
|
41781
42698
|
}
|
|
41782
|
-
|
|
42699
|
+
function snapshotCollectionError(cause) {
|
|
42700
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
42701
|
+
return Object.assign(new Error(`The document's package parts could not be collected for history (${detail}). Nothing was recorded.`), {
|
|
42702
|
+
code: "E_STATE",
|
|
42703
|
+
remedy: "Nothing was recorded in history. Retry; if it keeps failing, the document package may be damaged."
|
|
42704
|
+
});
|
|
42705
|
+
}
|
|
42706
|
+
async function ensureGitRepo(docPath, xml, extras, originalBytes) {
|
|
42707
|
+
return (await ensureGitRepoWith(docPath, () => ({ xml, extras }), originalBytes)).existed;
|
|
42708
|
+
}
|
|
42709
|
+
async function ensureGitRepoWith(docPath, baseline, originalBytes, options) {
|
|
41783
42710
|
const repoDir = getRepoDir(docPath);
|
|
41784
42711
|
let hasGit = false;
|
|
41785
42712
|
try {
|
|
41786
|
-
hasGit =
|
|
42713
|
+
hasGit = fs15.existsSync(path14.join(repoDir, ".git"));
|
|
41787
42714
|
} catch {
|
|
41788
42715
|
hasGit = false;
|
|
41789
42716
|
}
|
|
41790
|
-
if (hasGit)
|
|
41791
|
-
|
|
41792
|
-
|
|
42717
|
+
if (hasGit) {
|
|
42718
|
+
if (!options?.alreadyReady) await assertGitHistoryReady(repoDir);
|
|
42719
|
+
return { existed: true };
|
|
42720
|
+
}
|
|
42721
|
+
try {
|
|
42722
|
+
const initial = baseline();
|
|
42723
|
+
await initAstraRepo(docPath, initial.xml, initial.extras, originalBytes);
|
|
42724
|
+
return { existed: false, baseline: initial };
|
|
42725
|
+
} catch (error) {
|
|
42726
|
+
if (fs15.existsSync(path14.join(repoDir, ".git"))) {
|
|
42727
|
+
await assertGitHistoryReady(repoDir);
|
|
42728
|
+
return { existed: true };
|
|
42729
|
+
}
|
|
42730
|
+
throw error;
|
|
42731
|
+
}
|
|
41793
42732
|
}
|
|
41794
|
-
|
|
42733
|
+
function collectBaselineForGit(parser2) {
|
|
42734
|
+
const xml = parser2.readDocumentXml();
|
|
41795
42735
|
try {
|
|
41796
|
-
|
|
41797
|
-
const
|
|
41798
|
-
|
|
41799
|
-
|
|
42736
|
+
const snapshot3 = parser2.capturePackageSnapshot({ documentXml: xml });
|
|
42737
|
+
const extras = {};
|
|
42738
|
+
for (const [name2, buf] of snapshot3) if (isTrackedPackageFile(name2)) extras[name2] = buf;
|
|
42739
|
+
return { xml, extras };
|
|
42740
|
+
} catch (err) {
|
|
42741
|
+
throw snapshotCollectionError(err);
|
|
42742
|
+
}
|
|
42743
|
+
}
|
|
42744
|
+
var sha256Of = (bytes) => (0, import_crypto6.createHash)("sha256").update(bytes).digest("hex");
|
|
42745
|
+
async function resolveSessionOriginal(handle) {
|
|
42746
|
+
const fallback = handle.originalInfo ?? { available: false, reason: "not-applicable" };
|
|
42747
|
+
const pin = handle.originalPin;
|
|
42748
|
+
if (!pin || !path14.isAbsolute(handle.filePath)) return fallback;
|
|
42749
|
+
const current = await getOriginalInfo(handle.filePath);
|
|
42750
|
+
if (pin.kind === "retained") {
|
|
42751
|
+
if (!current.available) return current;
|
|
42752
|
+
if (current.sha256 !== pin.sha256) {
|
|
42753
|
+
return { available: false, reason: "capture-failed", detail: "The retained original no longer matches the one this session opened." };
|
|
42754
|
+
}
|
|
42755
|
+
return current;
|
|
42756
|
+
}
|
|
42757
|
+
if (current.available && current.sha256 !== pin.sha256) {
|
|
42758
|
+
return { available: false, reason: "capture-failed", detail: "Another opener recorded a different original for this history." };
|
|
42759
|
+
}
|
|
42760
|
+
const initialization = await getHistoryInitializationState(handle.filePath);
|
|
42761
|
+
if (initialization.state !== "complete") {
|
|
42762
|
+
return { available: false, reason: "initializing", detail: initialization.detail ?? "History initialization has not completed. Check get_document_history again later." };
|
|
42763
|
+
}
|
|
42764
|
+
if (!current.available) return current;
|
|
42765
|
+
handle.originalInfo = current;
|
|
42766
|
+
handle.originalPin = { kind: "retained", sha256: current.sha256 };
|
|
42767
|
+
handle.originalWarning = void 0;
|
|
42768
|
+
return current;
|
|
42769
|
+
}
|
|
42770
|
+
var INITIALIZING_WARNING = "History initialization was not complete when this document opened (another ASTra process may be creating it). Editing is available. Exact-original recovery becomes available if that initialization finishes for this same file; check get_document_history.";
|
|
42771
|
+
async function initGitForHandleBestEffort(handle, originalBytes) {
|
|
42772
|
+
try {
|
|
42773
|
+
if (!path14.isAbsolute(handle.filePath)) return;
|
|
42774
|
+
const repoDir = getRepoDir(handle.filePath);
|
|
42775
|
+
let completeBeforeRead = false;
|
|
42776
|
+
if (fs15.existsSync(path14.join(repoDir, ".git"))) {
|
|
42777
|
+
try {
|
|
42778
|
+
await assertGitHistoryReady(repoDir);
|
|
42779
|
+
completeBeforeRead = true;
|
|
42780
|
+
} catch {
|
|
42781
|
+
}
|
|
42782
|
+
}
|
|
42783
|
+
const availabilityAtOpen = await getOriginalInfo(handle.filePath);
|
|
42784
|
+
const ensured = await ensureGitRepoWith(handle.filePath, () => collectBaselineForGit(handle.parser), originalBytes, { alreadyReady: completeBeforeRead });
|
|
42785
|
+
if (ensured.existed) {
|
|
42786
|
+
const preExisting = completeBeforeRead && availabilityAtOpen.available;
|
|
42787
|
+
const now = preExisting ? availabilityAtOpen : await getOriginalInfo(handle.filePath);
|
|
42788
|
+
handle.originalInfo = now;
|
|
42789
|
+
if (now.available) {
|
|
42790
|
+
if (preExisting || originalBytes && sha256Of(originalBytes) === now.sha256) {
|
|
42791
|
+
handle.originalPin = { kind: "retained", sha256: now.sha256 };
|
|
42792
|
+
} else {
|
|
42793
|
+
handle.originalInfo = { available: false, reason: "capture-failed", detail: "Another opener recorded a different original for this history." };
|
|
42794
|
+
}
|
|
42795
|
+
} else if (originalBytes && now.reason === "incomplete") {
|
|
42796
|
+
handle.originalInfo = { available: false, reason: "initializing", detail: "The history was still being initialized when this session opened." };
|
|
42797
|
+
handle.originalPin = { kind: "input", sha256: sha256Of(originalBytes) };
|
|
42798
|
+
handle.originalWarning = INITIALIZING_WARNING;
|
|
42799
|
+
}
|
|
42800
|
+
} else {
|
|
42801
|
+
handle.originalInfo = await getOriginalInfo(handle.filePath);
|
|
42802
|
+
const recorded = ensured.baseline;
|
|
42803
|
+
const history = await getHistory(handle.filePath);
|
|
42804
|
+
const head = history[0] ? await checkoutCommit(handle.filePath, history[0].oid) : void 0;
|
|
42805
|
+
const names = Object.keys(recorded.extras).sort();
|
|
42806
|
+
const headNames = head ? [...head.extra.keys()].sort() : [];
|
|
42807
|
+
const initialStateMatches = !!head?.complete && recorded.xml === head.xml && JSON.stringify(names) === JSON.stringify(headNames) && names.every((name2) => recorded.extras[name2].equals(head.extra.get(name2)));
|
|
42808
|
+
if (!initialStateMatches && handle.originalInfo.available) {
|
|
42809
|
+
handle.originalInfo = { available: false, reason: "capture-failed", detail: "Concurrent history initialization produced a different initial document state. Close and reopen after inspecting history." };
|
|
42810
|
+
}
|
|
42811
|
+
if (originalBytes && handle.originalInfo.available && handle.originalInfo.sha256 !== sha256Of(originalBytes)) {
|
|
42812
|
+
handle.originalInfo = { available: false, reason: "capture-failed", detail: "Another opener recorded a different original for this history." };
|
|
42813
|
+
}
|
|
42814
|
+
if (handle.originalInfo.available) handle.originalPin = { kind: "retained", sha256: handle.originalInfo.sha256 };
|
|
42815
|
+
}
|
|
42816
|
+
if (!handle.originalInfo.available && !["predates-feature", "not-applicable"].includes(handle.originalInfo.reason)) {
|
|
42817
|
+
handle.originalWarning = handle.originalInfo.reason === "initializing" ? INITIALIZING_WARNING : `Exact-original recovery is unavailable: ${handle.originalInfo.reason}. Keep a separate copy of the input file.`;
|
|
42818
|
+
}
|
|
42819
|
+
} catch (error) {
|
|
42820
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
42821
|
+
const mismatch = error?.reason === "original-mismatch";
|
|
42822
|
+
const initialization = originalBytes && !mismatch && path14.isAbsolute(handle.filePath) ? await getHistoryInitializationState(handle.filePath) : void 0;
|
|
42823
|
+
if (initialization && originalBytes && initialization.state !== "absent") {
|
|
42824
|
+
handle.originalInfo = { available: false, reason: "initializing", detail: initialization.detail ?? detail };
|
|
42825
|
+
handle.originalPin = { kind: "input", sha256: sha256Of(originalBytes) };
|
|
42826
|
+
handle.originalWarning = INITIALIZING_WARNING;
|
|
42827
|
+
} else {
|
|
42828
|
+
handle.originalInfo = mismatch ? { available: false, reason: "capture-failed", detail } : { available: false, reason: "incomplete", detail };
|
|
42829
|
+
handle.originalWarning = `History initialization or original-file capture did not complete. Editing is available, but exact-original recovery is not. ${detail} Keep a separate copy of the input file.`;
|
|
42830
|
+
}
|
|
41800
42831
|
}
|
|
41801
42832
|
}
|
|
41802
42833
|
var McpSessionStore = class {
|
|
@@ -42046,12 +43077,12 @@ var McpSessionStore = class {
|
|
|
42046
43077
|
* Macro/active content refuses by default (E_VALIDATION naming the parts);
|
|
42047
43078
|
* pass {allowMacros:true} to preserve or {stripMacros:true} to strip.
|
|
42048
43079
|
*/
|
|
42049
|
-
async openDocument(filePath, sessionId, macroOpts) {
|
|
43080
|
+
async openDocument(filePath, sessionId, macroOpts, captureInput = true) {
|
|
42050
43081
|
const cleanPath = validateDocPath(filePath);
|
|
42051
43082
|
const expiredEvicted = this.evictExpired();
|
|
42052
43083
|
let buf;
|
|
42053
43084
|
try {
|
|
42054
|
-
buf = await
|
|
43085
|
+
buf = await fsp7.readFile(cleanPath);
|
|
42055
43086
|
} catch (err) {
|
|
42056
43087
|
const msg = err instanceof Error ? err.message : String(err);
|
|
42057
43088
|
throw Object.assign(new Error(`open_document: cannot read "${cleanPath}": ${msg}`), {
|
|
@@ -42086,10 +43117,12 @@ var McpSessionStore = class {
|
|
|
42086
43117
|
lastAccess: Date.now(),
|
|
42087
43118
|
mutex: new SessionMutex(),
|
|
42088
43119
|
redoStack: this.createRedoStack(),
|
|
42089
|
-
pendingAudit: { ops: 0, nodeIds: [] }
|
|
43120
|
+
pendingAudit: { ops: 0, nodeIds: [] },
|
|
43121
|
+
lastWrittenSha256: (0, import_crypto6.createHash)("sha256").update(buf).digest("hex"),
|
|
43122
|
+
macroPolicy: macroOpts
|
|
42090
43123
|
};
|
|
42091
43124
|
this.sessions.set(id, handle);
|
|
42092
|
-
await initGitForHandleBestEffort(handle);
|
|
43125
|
+
await initGitForHandleBestEffort(handle, captureInput ? buf : void 0);
|
|
42093
43126
|
this.lastOpenEvicted = [...expiredEvicted, ...this.evictToFit(id)];
|
|
42094
43127
|
const live = this.sessions.get(id);
|
|
42095
43128
|
if (!live) {
|
|
@@ -42156,10 +43189,13 @@ var McpSessionStore = class {
|
|
|
42156
43189
|
lastAccess: Date.now(),
|
|
42157
43190
|
mutex: new SessionMutex(),
|
|
42158
43191
|
redoStack: this.createRedoStack(),
|
|
42159
|
-
pendingAudit: { ops: 0, nodeIds: [] }
|
|
43192
|
+
pendingAudit: { ops: 0, nodeIds: [] },
|
|
43193
|
+
lastWrittenSha256: (0, import_crypto6.createHash)("sha256").update(buf).digest("hex"),
|
|
43194
|
+
macroPolicy: macroOpts
|
|
42160
43195
|
};
|
|
42161
43196
|
this.sessions.set(id, handle);
|
|
42162
|
-
await initGitForHandleBestEffort(handle);
|
|
43197
|
+
await initGitForHandleBestEffort(handle, buf);
|
|
43198
|
+
if (!path14.isAbsolute(handle.filePath)) handle.originalInfo = { available: false, reason: "not-applicable" };
|
|
42163
43199
|
this.lastOpenEvicted = [...expiredEvicted, ...this.evictToFit(id)];
|
|
42164
43200
|
const live = this.sessions.get(id);
|
|
42165
43201
|
if (!live) {
|
|
@@ -42334,6 +43370,8 @@ var LEAN6_TOOL_NAMES = [
|
|
|
42334
43370
|
];
|
|
42335
43371
|
var GIT_HISTORY_TOOL_NAMES = [
|
|
42336
43372
|
"get_document_history",
|
|
43373
|
+
"export_original_document",
|
|
43374
|
+
"restore_original_document",
|
|
42337
43375
|
"preview_revert",
|
|
42338
43376
|
"revert_document",
|
|
42339
43377
|
"redo_document"
|
|
@@ -42363,7 +43401,7 @@ var LEAN6_DESCRIPTIONS = {
|
|
|
42363
43401
|
{ workflow: true }
|
|
42364
43402
|
),
|
|
42365
43403
|
inspect_document: withSharedDocs(
|
|
42366
|
-
'Inspect a session: standard TOON outline (fidelity compact|standard|full, default standard) plus style palette passthrough {availableStyles,brandColors,primaryFont} parsed from word/styles.xml, plus document core properties `metadata` {title,creator,subject,description,keywords,category,lastModifiedBy,revision,created,modified} read from docProps/core.xml (absent when the docx has no core part). Use include:["comments","revisions"] for review records paged by reviewOffset/reviewLimit (max 50; offset/limit page nodes only); commentId or revisionId reads full detail and needs its include. Renders the in-memory session state (includes unsaved execute_batch edits); `dirty:true` means the session differs from the last saved file, `revision` is the in-memory revision. Node ids are verbatim: paragraphs carry 8-hex w14:paraId (e.g. 3F2A9C1B), tables t_001.., sections sec_root, sec_2, sec_3.. by position; use them as-is for targets/anchors. A repeat paragraph sharing one native paraId reports a `<paraId>_dup` session id instead (e.g. 3F2A9C1B_dup): saving refuses until the repeat is deleted and its text reinserted for a fresh id. Tables render one row per table with grid cells as rNcM coordinates (r1c1 value | r1c2 value, rows joined by ;); merged spans are marked (spanN); merge covers are tagged hidden-cover (not directly editable, address the restart cell). Cell edits: the replace, style, namedStyle, clearFormat, hyperlink, bookmark and comment actions accept target "<tableId>:r<row>c<col>" (1-based, as the grid prints it; add ":p<n>" to pick one paragraph of a multi-paragraph cell). Other actions, including table, take the table id. Every row and column number counts from 1 (cell addresses, rowIndex, colIndex, columnIndex, row, col, index, afterIndex, fromRow/toRow/fromCol/toCol), so r3c2 and {rowIndex:3,colIndex:2} are the same cell. Chart nodes render style chart|chart3d with title + categories + series in the text ([Chart column] Title | cats: Q1,Q2 | ARR: 10,25), read from the chart part. Flag legend: [TBL] table [IMG] image/drawing [EQ] equation [CITE] citation [CMT] comment [TRK] tracked [LNK] link [BKMK] bookmark [L#] list level (1 is the top level; list ops count the same way) [+ADDED]/[~MODIFIED] diff; standard tier adds [B] bold [I] italic [C:hex] color [SZ:half-points] size [RS:name] run style [J:just] [N:num] numbering [HDR]/[ZEBRA] table look [CH:type]/[RID:id] chart. Palette lists every style id verbatim with no trim (large templates return the full list). 3-tier workflow for large docs: (1) macro-overview inspect (compact, first window) -> (2) search_content to find a target node id -> (3) local zoom via {aroundNode:"<id>",window:5} or page with {offset,limit}. Windowing opts: offset (min 0, default 0), limit (1-200, default 50), aroundNode + window (1-20, default 5) returning [node-w,node+w], maxTokens (200-8000, cuts on a clean row boundary, always reported via truncated/hasMore). aroundNode and offset/limit are mutually exclusive (use one or the other). Windowed renders preserve the requested tier; the TOON fidelity marker stays lossy by design (TOON is a lossy projection). Docs under ~2000 tokens return the full TOON (unchanged); larger docs default to the first ~50-node window {totalNodes,returnedNodes,offset,limit,hasMore,nextOffset,toonText} with a navigation remedy. Full fidelity requires sectionId; sectionId is a section id or a node id (a node id prefix needs 4 characters or more), and an id that matches nothing is an error that names the available section ids. Example {"sessionId":"sess_abc","fidelity":"standard"} -> TOON text + palette. Use the palette style names verbatim (e.g. Corporate Callout); never invent styles. When the session carries a style guide, the envelope also includes its compact styleGuide summary {name,headerFill,headerText,chartPalette,calloutTheme}. IDs are session-scoped: valid only in the issuing session, must not be stored across sessions, and duplicate client-supplied ids are rejected.'
|
|
43404
|
+
'Inspect a session: standard TOON outline (fidelity outline|compact|standard|full, default standard) plus style palette passthrough fidelity outline lists headings only (ID, level L, Style, Text, Section = the headings above); there rowCount/totalNodes count heading rows, `outline` {headings,nodes,tables,images,words} counts the whole document, and aroundNode on a body node shows the heading above it (`aroundResolvedTo`). {availableStyles,brandColors,primaryFont} parsed from word/styles.xml, plus document core properties `metadata` {title,creator,subject,description,keywords,category,lastModifiedBy,revision,created,modified} read from docProps/core.xml (absent when the docx has no core part). Use include:["comments","revisions"] for review records paged by reviewOffset/reviewLimit (max 50; offset/limit page nodes only); commentId or revisionId reads full detail and needs its include. Renders the in-memory session state (includes unsaved execute_batch edits); `dirty:true` means the session differs from the last saved file, `revision` is the in-memory revision. Node ids are verbatim: paragraphs carry 8-hex w14:paraId (e.g. 3F2A9C1B), tables t_001.., sections sec_root, sec_2, sec_3.. by position; use them as-is for targets/anchors. A repeat paragraph sharing one native paraId reports a `<paraId>_dup` session id instead (e.g. 3F2A9C1B_dup): saving refuses until the repeat is deleted and its text reinserted for a fresh id. Tables render one row per table with grid cells as rNcM coordinates (r1c1 value | r1c2 value, rows joined by ;); merged spans are marked (spanN); merge covers are tagged hidden-cover (not directly editable, address the restart cell). Cell edits: the replace, style, namedStyle, clearFormat, hyperlink, bookmark and comment actions accept target "<tableId>:r<row>c<col>" (1-based, as the grid prints it; add ":p<n>" to pick one paragraph of a multi-paragraph cell). Other actions, including table, take the table id. Every row and column number counts from 1 (cell addresses, rowIndex, colIndex, columnIndex, row, col, index, afterIndex, fromRow/toRow/fromCol/toCol), so r3c2 and {rowIndex:3,colIndex:2} are the same cell. Chart nodes render style chart|chart3d with title + categories + series in the text ([Chart column] Title | cats: Q1,Q2 | ARR: 10,25), read from the chart part. Flag legend: [TBL] table [IMG] image/drawing [EQ] equation [CITE] citation [CMT] comment [TRK] tracked [LNK] link [BKMK] bookmark [L#] list level (1 is the top level; list ops count the same way) [+ADDED]/[~MODIFIED] diff; standard tier adds [B] bold [I] italic [C:hex] color [SZ:half-points] size [RS:name] run style [J:just] [N:num] numbering [HDR]/[ZEBRA] table look [CH:type]/[RID:id] chart. Palette lists every style id verbatim with no trim (large templates return the full list). 3-tier workflow for large docs: (1) macro-overview inspect (compact, first window) -> (2) search_content to find a target node id -> (3) local zoom via {aroundNode:"<id>",window:5} or page with {offset,limit}. Windowing opts: offset (min 0, default 0), limit (1-200, default 50), aroundNode + window (1-20, default 5) returning [node-w,node+w], maxTokens (200-8000, cuts on a clean row boundary, always reported via truncated/hasMore). aroundNode and offset/limit are mutually exclusive (use one or the other). Windowed renders preserve the requested tier; the TOON fidelity marker stays lossy by design (TOON is a lossy projection). Docs under ~2000 tokens return the full TOON (unchanged); larger docs default to the first ~50-node window {totalNodes,returnedNodes,offset,limit,hasMore,nextOffset,toonText} with a navigation remedy. Full fidelity requires sectionId; sectionId is a section id or a node id (a node id prefix needs 4 characters or more), and an id that matches nothing is an error that names the available section ids. Example {"sessionId":"sess_abc","fidelity":"standard"} -> TOON text + palette. Use the palette style names verbatim (e.g. Corporate Callout); never invent styles. When the session carries a style guide, the envelope also includes its compact styleGuide summary {name,headerFill,headerText,chartPalette,calloutTheme}. IDs are session-scoped: valid only in the issuing session, must not be stored across sessions, and duplicate client-supplied ids are rejected.'
|
|
42367
43405
|
),
|
|
42368
43406
|
search_content: withSharedDocs(
|
|
42369
43407
|
'Search live paragraph/table w:t text; returns [{id,preview}] node hits for anchors/targets. Use scope:"comments" or scope:"all" to search comment text; the default scope is body. At most 50 matches come back; truncated:true says more exist. A table hit also lists `cells` (up to 5 addresses such as t_001:r3c1) for the cells that hold the match; pass one as the target of replace/style/namedStyle/clearFormat/hyperlink/bookmark/comment. Chart titles/series are NOT matched by search (they live in the chart part, not w:t); discover them in inspect TOON chart rows, then anchor by the chart node id. Image alt text and footnote/endnote bodies are NOT matched either (search covers body w:t text only); find images via inspect [IMG] rows and footnotes via their anchor ids. Search extension to those parts is a later-program candidate, not this surface. Default is an exact literal substring (case-insensitive); pass {"regex":true} to interpret the query as a regular expression (bounded: pattern <=200 chars, ~500ms evaluation budget, case-insensitive). Caps: first 50 hits, 120-char previews. Literal queries containing regex metacharacters return literal results plus a hint that regex:true exists. Example {"sessionId":"sess_abc","query":"Executive Summary"} -> {"ok":true,"matches":[{"id":"3F2A9C1B","preview":"Executive ..."}]}. Search before editing when the anchor id is unknown.'
|
|
@@ -42381,8 +43419,10 @@ var LEAN6_DESCRIPTIONS = {
|
|
|
42381
43419
|
)
|
|
42382
43420
|
};
|
|
42383
43421
|
var GIT_HISTORY_DESCRIPTIONS = {
|
|
43422
|
+
export_original_document: "Export the exact original DOCX captured when this history was created. Requires sessionId and outPath; overwrite defaults to false. Verifies the retained SHA-256 and never serializes the original. Does not change the live session. Refuses overwriting the active document: use restore_original_document instead. Old histories and memory-only sessions may have no original. The original can contain content removed by later sanitization; history retains it until the history store is deleted.",
|
|
43423
|
+
restore_original_document: "Replace the working file with the exact captured original, verified by SHA-256, and record a new history commit. Requires sessionId. Unsaved edits refuse unless discard:true explicitly discards them. Refuses an externally changed file or history that does not match the saved state. Close other programs editing this file before restoring; an external write racing the final replacement cannot be excluded. Reloads the session under its opening policy. Normal revert/redo restore document state, not necessarily the original ZIP bytes. The next normal save may normalize the file again. Use export_original_document for recovery to a different path.",
|
|
42384
43424
|
get_document_history: withSharedDocs(
|
|
42385
|
-
'List the saved git history for a session (newest first, read-only: never mutates, never flips dirty). Paginate with limit (1-100, default 20) and offset (default 0). Returns {ok:true,sessionId,documentPath,currentCommit,totalCommits,history:[{oid,message,author,timestamp,timestampMs,opCount,affectedNodeIds,resultingHeadOid}]} (timestampMs = ms-precision epoch millis; commit-time clock for tracked commits, git seconds for older ones; opCount is always present (0 for pre-tracking commits and heals-only saves: it folds operations, not normalizer rewrites); affectedNodeIds is always present ([] when untracked); resultingHeadOid always equals oid (append-only; refs never move). Example {"sessionId":"sess_abc","limit":10} -> history slice. Use the oids with revert_document (or preview_revert first) to restore an earlier saved state. '
|
|
43425
|
+
'List the saved git history and original-file availability for a session (newest first, read-only: never mutates, never flips dirty). Paginate with limit (1-100, default 20) and offset (default 0). Returns {ok:true,sessionId,documentPath,currentCommit,totalCommits,history:[{oid,message,author,timestamp,timestampMs,opCount,affectedNodeIds,resultingHeadOid}]} (timestampMs = ms-precision epoch millis; commit-time clock for tracked commits, git seconds for older ones; opCount is always present (0 for pre-tracking commits and heals-only saves: it folds operations, not normalizer rewrites); affectedNodeIds is always present ([] when untracked); resultingHeadOid always equals oid (append-only; refs never move). Example {"sessionId":"sess_abc","limit":10} -> history slice. Use the oids with revert_document (or preview_revert first) to restore an earlier saved state. '
|
|
42386
43426
|
),
|
|
42387
43427
|
preview_revert: withSharedDocs(
|
|
42388
43428
|
`Read-only WOULD-restore preview for revert_document (never mutates: no revision bump, no disk write, no redo-stack change). Target one commit: {steps:N} walks back N saved commits from the head (1 = the previous saved commit, must be < totalCommits) OR {commitOid:"<40-hex>"} jumps to that commit (mutually exclusive). Returns {ok:true,sessionId,targetOid,targetMessage,targetAuthor,stepsBack,affectedFiles,changedValues,changedValuesTruncated,changedValuesPositional,dirty,dirtyHandling,currentHeadOid,totalCommits} where affectedFiles lists package paths that differ from the target (word/document.xml plus tracked extras). The preview also carries advisory changedValues [{nodeId,before,after}] (live current text -> target restore text, bounded: at most ${CHANGED_VALUES_MAX} entries, each side at most ${CHANGED_VALUE_TEXT_MAX} chars, truncated flag honest; changedValuesPositional:true means either side held orphan ids (no shared id across the boundary, common under duplicate native ids) so before/after pairing is positional, not identity; empty + changedValuesReason when the target range is not computable). Example {"sessionId":"sess_abc","steps":1} -> preview summary. Preview first when a revert would discard unsaved edits.`
|
|
@@ -42402,7 +43442,7 @@ var PARITY_DESCRIPTIONS = {
|
|
|
42402
43442
|
'Preflight read-only OOXML audit for a session: runs the same validator gate save_document enforces, before anything is written. Detects schema violations while they are still cheap to fix. Strictly read-only: zero session mutation (revision and dirty flag untouched), zero disk writes, zero history commits. Returns {ok:true,sessionId,revision,dirty,validation:{valid,errorCount,errors}} where errors carry description + xPath (first 10, same shape as the save-blocked envelope). The verdict is on the document as a save would write it (children put in schema order first); when that save would repair something the result also carries pendingNormalizations (a list of repairs, absent when none), so a valid verdict is not mistaken for "valid exactly as it stands". Use before save_document when the package may be non-conforming; save_document re-gates before writing. Example {"sessionId":"sess_abc"} -> {"ok":true,"validation":{"valid":true,"errorCount":0,"errors":[]}}. '
|
|
42403
43443
|
),
|
|
42404
43444
|
get_stats: withSharedDocs(
|
|
42405
|
-
'Token-cheap document census for a session: words, paragraphs, tables, callouts, images, and an estimated editorial page budget (layout-weighted by words, tables, images, and explicit page breaks) in a single small envelope. Counters are the exact engine counters behind CLI stats, so both surfaces always agree. Physical pagination is finalized when exporting to PDF via LibreOffice. Read-only: never mutates, never bumps the revision. Returns {ok:true,sessionId,revision,dirty,words,paragraphs,tables,callouts,images,pages}. A callout (a "> [!NOTE]" alert or a callout op: a one-cell table with a fill) is counted in callouts and not in tables. Use instead of inspect_document when only counts are needed (a full TOON dump costs thousands of tokens for the same question). Example {"sessionId":"sess_abc"} -> {"ok":true,"words":42,"paragraphs":5}. '
|
|
43445
|
+
'Token-cheap document census for a session: words, paragraphs, tables, callouts, images, and an estimated editorial page budget (layout-weighted by words, tables, images, and explicit page breaks) in a single small envelope. Counters are the exact engine counters behind CLI stats, so both surfaces always agree. Physical pagination is finalized when exporting to PDF via LibreOffice. Read-only: never mutates, never bumps the revision. Returns {ok:true,sessionId,revision,dirty,words,paragraphs,tables,callouts,images,pages,pageTerms}. pages is a rough estimate, max(1, ceil(words/450 + 0.45 per table or callout + 0.5 per image + 1 per page break)); pageTerms gives those four terms in pages, so a caller can see how much of it is counted from tables, callouts, images and page breaks (`pageTerms.tables` covers tables and callouts), which the estimate cannot size. Use visual_inspect for the renderer\'s page count. A callout (a "> [!NOTE]" alert or a callout op: a one-cell table with a fill) is counted in callouts and not in tables. Use instead of inspect_document when only counts are needed (a full TOON dump costs thousands of tokens for the same question). Example {"sessionId":"sess_abc"} -> {"ok":true,"words":42,"paragraphs":5}. '
|
|
42406
43446
|
),
|
|
42407
43447
|
sanitize_document: withSharedDocs(
|
|
42408
43448
|
`Scrub an open session document. By default it removes revision ids, authorship, embedded EXIF and hidden text PII. Tracked changes and comments are kept, with the text of every tracked deletion, unless you ask: acceptAllRevisions:true accepts all tracked changes (deleted text is removed) and stripComments:true removes comments. The CLI presets (bare sanitize, --client-safe, --metadata-only, --redact-pii) turn both on; this tool does not. Macro/active parts are removed on request. It uses the same engine scrub path as CLI sanitize. Body text is kept. Options mirror the engine scrub keys 1:1 (zeroRetention, purgeRSIDs, purgeAuthors, purgeExif, purgePII, acceptAllRevisions, stripComments, replacementAuthor, allowMacros, stripMacros); omitted options take engine defaults. With acceptAllRevisions it returns E_VALIDATION and no file when revision markup remains that cannot be resolved. allowMacros and stripMacros together fail with E_VALIDATION. ${piiCoverageSentence()} Returns {ok:true,sessionId,revision,dirty,removedRSIDs,purgedAuthors,strippedExifBytes,purgedPII,bodyPII,piiCoverage,acceptedRevisions,strippedComments,strippedMacros,macroParts,filesTouched,droppedRogue,...} plus the standard redoCleared signal when a redo chain is dropped. Example {"sessionId":"sess_abc","stripMacros":true} -> scrubbed session at a new revision (save after). `
|
|
@@ -42440,7 +43480,7 @@ var OpenDocumentSchema = import_zod4.z.object({
|
|
|
42440
43480
|
}).strict();
|
|
42441
43481
|
var InspectDocumentSchema = import_zod4.z.object({
|
|
42442
43482
|
sessionId: import_zod4.z.string().min(1).max(128),
|
|
42443
|
-
fidelity: import_zod4.z.enum(["compact", "standard", "full"]).optional(),
|
|
43483
|
+
fidelity: import_zod4.z.enum(["outline", "compact", "standard", "full"]).optional(),
|
|
42444
43484
|
sectionId: import_zod4.z.string().min(1).max(256).optional(),
|
|
42445
43485
|
maxTextLength: import_zod4.z.number().int().min(1).max(1e4).optional(),
|
|
42446
43486
|
// Phase C-2 smart windowing: offset/limit page the node rows; aroundNode +
|
|
@@ -42571,6 +43611,15 @@ var ImportDocumentSchema = import_zod4.z.object({
|
|
|
42571
43611
|
// Read $...$ as inline math (off by default: prices and shell text can look like math).
|
|
42572
43612
|
dollarMath: import_zod4.z.boolean().optional()
|
|
42573
43613
|
}).strict();
|
|
43614
|
+
var ExportOriginalDocumentSchema = import_zod4.z.object({
|
|
43615
|
+
sessionId: import_zod4.z.string().min(1).max(128),
|
|
43616
|
+
outPath: import_zod4.z.string().min(1).max(1024),
|
|
43617
|
+
overwrite: import_zod4.z.boolean().optional()
|
|
43618
|
+
}).strict();
|
|
43619
|
+
var RestoreOriginalDocumentSchema = import_zod4.z.object({
|
|
43620
|
+
sessionId: import_zod4.z.string().min(1).max(128),
|
|
43621
|
+
discard: import_zod4.z.boolean().optional()
|
|
43622
|
+
}).strict();
|
|
42574
43623
|
|
|
42575
43624
|
// src/lib/mcp/handlers/open-inspect.ts
|
|
42576
43625
|
function extractStylePalette2(parser2) {
|
|
@@ -42664,7 +43713,7 @@ async function handleOpenDocument(store, input) {
|
|
|
42664
43713
|
const evicted = store.drainLastOpenEvicted();
|
|
42665
43714
|
const pendingNormalizations = readPendingNormalizations(handle);
|
|
42666
43715
|
const validation = await validateAtOpen(handle, parsed.data.validationTimeoutMs ?? openValidationTimeoutMs());
|
|
42667
|
-
return { ok: true, sessionId: handle.sessionId, revision: handle.revision, summary, evicted, pendingNormalizations, validation };
|
|
43716
|
+
return { ok: true, sessionId: handle.sessionId, revision: handle.revision, summary, evicted, pendingNormalizations, validation, original: handle.originalInfo, ...handle.originalWarning ? { originalWarning: handle.originalWarning } : {} };
|
|
42668
43717
|
} catch (err) {
|
|
42669
43718
|
return mcpErrorFrom(err, "E_IO", "Check the .docx path is readable and retry open_document.");
|
|
42670
43719
|
}
|
|
@@ -42678,6 +43727,12 @@ function readPendingNormalizations(handle) {
|
|
|
42678
43727
|
return [];
|
|
42679
43728
|
}
|
|
42680
43729
|
}
|
|
43730
|
+
function outlineCountsOf(ast) {
|
|
43731
|
+
const stats = computeDocStats(ast);
|
|
43732
|
+
let nodes = 0;
|
|
43733
|
+
for (const sec of ast.sections) nodes += sec.nodes.length;
|
|
43734
|
+
return { headings: countHeadings(ast), nodes, tables: stats.tables + stats.callouts, images: stats.images, words: stats.words };
|
|
43735
|
+
}
|
|
42681
43736
|
async function handleInspectDocument(store, input) {
|
|
42682
43737
|
const parsed = InspectDocumentSchema.safeParse(input);
|
|
42683
43738
|
if (!parsed.success) {
|
|
@@ -42777,6 +43832,7 @@ async function handleInspectDocument(store, input) {
|
|
|
42777
43832
|
pendingNormalizations: readPendingNormalizations(handle),
|
|
42778
43833
|
...handle.styleGuide !== void 0 ? { styleGuide: summarizeStyleGuide(handle.styleGuide) } : {},
|
|
42779
43834
|
toonText: fullText,
|
|
43835
|
+
...fidelity === "outline" ? { outline: outlineCountsOf(handle.ast) } : {},
|
|
42780
43836
|
rowCount: totalNodes,
|
|
42781
43837
|
totalNodes,
|
|
42782
43838
|
returnedNodes: totalNodes,
|
|
@@ -42790,11 +43846,40 @@ async function handleInspectDocument(store, input) {
|
|
|
42790
43846
|
let start;
|
|
42791
43847
|
let end;
|
|
42792
43848
|
let effectiveLimit = limit;
|
|
43849
|
+
let aroundResolvedTo;
|
|
43850
|
+
let note;
|
|
42793
43851
|
if (aroundNode !== void 0) {
|
|
42794
|
-
|
|
43852
|
+
let idx = table.rows.findIndex((r) => r.id === aroundNode);
|
|
43853
|
+
if (idx < 0 && fidelity === "outline") {
|
|
43854
|
+
const scoped = sectionId ? resolveSectionScope(handle.ast.sections, sectionId) : handle.ast.sections;
|
|
43855
|
+
const order = scoped.flatMap((sec) => sec.nodes.map((n) => n.id));
|
|
43856
|
+
const at = order.indexOf(aroundNode);
|
|
43857
|
+
if (at < 0) {
|
|
43858
|
+
const anywhere = handle.ast.sections.some((sec) => sec.nodes.some((n) => n.id === aroundNode));
|
|
43859
|
+
if (anywhere) {
|
|
43860
|
+
throw Object.assign(
|
|
43861
|
+
new Error(`inspect_document: aroundNode "${aroundNode}" is not in section "${sectionId}".`),
|
|
43862
|
+
{ code: "E_VALIDATION", remedy: "Drop sectionId, or pass a node id that belongs to that section." }
|
|
43863
|
+
);
|
|
43864
|
+
}
|
|
43865
|
+
} else {
|
|
43866
|
+
const headingIds = new Set(table.rows.map((r) => r.id));
|
|
43867
|
+
let above = -1;
|
|
43868
|
+
for (let k = at; k >= 0 && above < 0; k--) if (headingIds.has(order[k])) above = k;
|
|
43869
|
+
if (above >= 0) {
|
|
43870
|
+
idx = table.rows.findIndex((r) => r.id === order[above]);
|
|
43871
|
+
aroundResolvedTo = order[above];
|
|
43872
|
+
note = `Node ${aroundNode} is not a heading; showing the heading above it (${order[above]}).`;
|
|
43873
|
+
} else {
|
|
43874
|
+
aroundResolvedTo = null;
|
|
43875
|
+
note = totalNodes === 0 ? `No headings${sectionId ? " in this scope" : " in the document"}; the outline is empty.` : `Node ${aroundNode} has no heading above it${sectionId ? " in this scope" : ""}; showing the first headings.`;
|
|
43876
|
+
idx = 0;
|
|
43877
|
+
}
|
|
43878
|
+
}
|
|
43879
|
+
}
|
|
42795
43880
|
if (idx < 0) {
|
|
42796
43881
|
throw Object.assign(
|
|
42797
|
-
new Error(`inspect_document: aroundNode "${aroundNode}" not found (${totalNodes} nodes in scope).`),
|
|
43882
|
+
new Error(`inspect_document: aroundNode "${aroundNode}" not found (${totalNodes} ${fidelity === "outline" ? "headings" : "nodes"} in scope).`),
|
|
42798
43883
|
{
|
|
42799
43884
|
code: "E_VALIDATION",
|
|
42800
43885
|
remedy: 'Call search_content to find a valid target node id, then retry inspect_document with {aroundNode:"<id>", window:5}.'
|
|
@@ -42803,6 +43888,10 @@ async function handleInspectDocument(store, input) {
|
|
|
42803
43888
|
}
|
|
42804
43889
|
start = Math.max(0, idx - window);
|
|
42805
43890
|
end = Math.min(totalNodes, idx + window + 1);
|
|
43891
|
+
if (aroundResolvedTo === null) {
|
|
43892
|
+
start = 0;
|
|
43893
|
+
end = Math.min(totalNodes, window * 2 + 1);
|
|
43894
|
+
}
|
|
42806
43895
|
effectiveLimit = end - start;
|
|
42807
43896
|
} else {
|
|
42808
43897
|
if (offset >= totalNodes && totalNodes > 0) {
|
|
@@ -42869,6 +43958,7 @@ async function handleInspectDocument(store, input) {
|
|
|
42869
43958
|
pendingNormalizations: readPendingNormalizations(handle),
|
|
42870
43959
|
...handle.styleGuide !== void 0 ? { styleGuide: summarizeStyleGuide(handle.styleGuide) } : {},
|
|
42871
43960
|
toonText,
|
|
43961
|
+
...fidelity === "outline" ? { outline: outlineCountsOf(handle.ast) } : {},
|
|
42872
43962
|
rowCount: returnedNodes,
|
|
42873
43963
|
totalNodes,
|
|
42874
43964
|
returnedNodes,
|
|
@@ -42878,6 +43968,8 @@ async function handleInspectDocument(store, input) {
|
|
|
42878
43968
|
nextOffset: hasMore ? start + returnedNodes : null,
|
|
42879
43969
|
truncated
|
|
42880
43970
|
};
|
|
43971
|
+
if (aroundResolvedTo !== void 0) out.aroundResolvedTo = aroundResolvedTo;
|
|
43972
|
+
if (note !== void 0) out.note = note;
|
|
42881
43973
|
if (hasMore || truncated) {
|
|
42882
43974
|
out.remedy = `Large document: showing ${returnedNodes}/${totalNodes} TOON nodes${truncated ? ` (cut to fit maxTokens ${maxTokens})` : ""}. Navigate: 1) search_content to find a target node id, 2) inspect_document {aroundNode:"<id>", window:5} to zoom, 3) page with {offset:${hasMore ? start + returnedNodes : start}, limit:${effectiveLimit}} to continue.`;
|
|
42883
43975
|
}
|
|
@@ -42889,11 +43981,11 @@ async function handleInspectDocument(store, input) {
|
|
|
42889
43981
|
}
|
|
42890
43982
|
|
|
42891
43983
|
// src/lib/mcp/handlers/search-batch.ts
|
|
42892
|
-
var
|
|
43984
|
+
var path16 = __toESM(require("path"));
|
|
42893
43985
|
|
|
42894
43986
|
// src/tools/compose-document.ts
|
|
42895
|
-
var
|
|
42896
|
-
var
|
|
43987
|
+
var fs16 = __toESM(require("fs"));
|
|
43988
|
+
var path15 = __toESM(require("path"));
|
|
42897
43989
|
var import_zod5 = require("zod");
|
|
42898
43990
|
var COMPOSE_MARKDOWN_MAX = 5e5;
|
|
42899
43991
|
var COMPOSE_VALIDATION_ERRORS_MAX = 3;
|
|
@@ -42908,8 +44000,8 @@ function resolveMcpStyleGuide(ref, jail) {
|
|
|
42908
44000
|
let anchor;
|
|
42909
44001
|
if (jail?.allowedDir !== void 0) {
|
|
42910
44002
|
anchor = jail.allowedDir;
|
|
42911
|
-
} else if (jail?.sessionFilePath !== void 0 &&
|
|
42912
|
-
anchor = canonicalizeForAsset(
|
|
44003
|
+
} else if (jail?.sessionFilePath !== void 0 && path15.isAbsolute(jail.sessionFilePath)) {
|
|
44004
|
+
anchor = canonicalizeForAsset(path15.resolve(path15.dirname(jail.sessionFilePath)));
|
|
42913
44005
|
}
|
|
42914
44006
|
if (anchor === void 0 && !hasGuideScheme(trimmed) && isFileGuideRef(trimmed)) {
|
|
42915
44007
|
throw guideSandboxError(
|
|
@@ -42942,7 +44034,7 @@ var ComposeDocumentSchema = import_zod5.z.object({
|
|
|
42942
44034
|
// Side of anchor the first block goes on (default after). Needs anchor.
|
|
42943
44035
|
position: import_zod5.z.enum(["before", "after"]).optional()
|
|
42944
44036
|
}).strict();
|
|
42945
|
-
var COMPOSE_DOCUMENT_DESCRIPTION = 'Compose a Markdown string into the open session document (appends after existing content, or at anchor with position before|after; an unchanged built-in template created in this session has its sample content replaced). A document can be built in several compose calls: the first call of a session mints ids md_0001..., later calls mint md2_0001..., md3_0001..., so ids never collide; pass anchor (a node id, start or end) and position to add a section in the middle without another tool. Runs the engine compose path, then applies the resulting operations through the same execute_batch path (strict schema gate, per-chunk quotas, structural checks, per-chunk atomic rollback, then inline-span run splitting), so every gate a batch meets applies here unchanged. Supported input: #/##/### headings (real Heading1..3 styles), blank-line paragraphs (single newlines join with a space), flat bullet (-, *, +) and ordered (1., 1)) lists with real numbering, GitHub-style tables with a header separator row (header row shaded and repeated), --- dividers, fenced or indented code blocks (monospace, line breaks kept), [^label] footnotes with definitions, inline spans (**bold**, *italic*, `code`, [text](url)) applied as runs with real hyperlink targets, plain `> quote` blocks (written in the Quote paragraph style: blank > lines start a new paragraph, > > nests up to 3 levels, flat lists and fenced code are kept inside, a table, heading or image inside a quote fails), and standalone block images/charts () resolved from allowedDir. Out-of-scope input fails closed with E_VALIDATION plus a 1-based line number and writes nothing: raw HTML (except [!NOTE]-style alert callouts, which compile to callout containers and keep bold, italic, code and links), custom styles, deeper headings, nested lists or tables, task items. Block image/chart refs need allowedDir pointing at the folder holding the files (refs stay jailed inside it; without allowedDir they fail closed); remote URL refs fail closed; inline data-URL block images bypass allowedDir by design (bytes decode in memory through the same image gates). Sources compiling past 500 operations apply as sequential 500-operation chunks; the compose is atomic as a whole: when any chunk fails, every chunk before it is rewound and the document stays as it was. Citation numbers and the References list restart in each compose call. Optional template (report|memo|blank) is accepted on the first compose only and requires the open document to still hold that template seed, otherwise composition fails before any write (for a fresh templated document call new_document first, then compose_document with template), and placement is append after existing content except for an unchanged built-in template created in this session. Reopened documents and external templates keep existing content. Returns {ok:true,opCount,appliedCount,revision,createdNodeIds,results,validation:{valid,errorCount,errors}} where createdNodeIds lists new node ids in document order (use them as anchors without re-inspecting) and validation proves the post-compose package state via a read-only OOXML check (save_document re-gates before writing). A compose that drops a non-empty redo stack signals it (redoCleared:true + redoClearedWarning naming the depth): a fresh edit invalidates redo. Optional styleGuide (bundled guide name or guide .md path, jailed like image refs: explicit allowedDir else the session anchor; remote URLs fail closed) replaces the session guide (not a one-batch override: later batches inherit it until another compose replaces it) and skins this compose (table/header-fill, chart-palette, callout, and code defaults apply where ops omit params; explicit params still win). Guides cannot be detached: open a new session to work without a guide. With no guide, compose uses the built-in Default Corporate look (navy table header, bold white header text, striped rows). styleGuide "legacy" gives the older grey header for this call only and is not stored on the session. An italic-only line directly under the opening # title becomes the Subtitle; a [!NOTE]-style line outside a blockquote stays plain text and comes back in warnings. Later batches inherit it, inspect_document shows its compact summary, save_document reports advisory styleWarnings. Unknown names fail with E_VALIDATION naming the available set. Math: $$ ... $$ or \\[ ... \\] blocks become display equations and \\(...\\) becomes inline math (one line, up to 500 characters; refused inside links and table cells). Display blocks are top level only (inside a list item or alert body they fail closed). Optional dollarMath (default false) treats $...$ as inline math; off by default because prices and shell text can look like math. Workflow: open_document -> compose_document -> save_document. Example {"sessionId":"sess_abc","markdown":"# Plan\\n\\n- a\\n- b\\n"} (add "allowedDir":"<dir>" for image/chart refs, "template":"report|memo|blank" to verify the seed). IDs are session-scoped: valid only in the issuing session, must not be stored across sessions. When dc:title is still a template placeholder it is set from the first Title or Heading 1 (titleSetFrom says so); a title already set is kept. persistentIds maps each created paragraph id (md_0003) to the paragraph id the saved file carries, so a reopened session can be addressed with it (tables are renumbered t_001.. by position on reopen).';
|
|
44037
|
+
var COMPOSE_DOCUMENT_DESCRIPTION = 'Compose a Markdown string into the open session document (appends after existing content, or at anchor with position before|after; an unchanged built-in template created in this session has its sample content replaced). A document can be built in several compose calls: the first call of a session mints ids md_0001..., later calls mint md2_0001..., md3_0001..., so ids never collide; pass anchor (a node id, start or end) and position to add a section in the middle without another tool. Runs the engine compose path, then applies the resulting operations through the same execute_batch path (strict schema gate, per-chunk quotas, structural checks, per-chunk atomic rollback, then inline-span run splitting), so every gate a batch meets applies here unchanged. Supported input: #/##/### headings (real Heading1..3 styles), blank-line paragraphs (single newlines join with a space), flat bullet (-, *, +) and ordered (1., 1)) lists with real numbering, GitHub-style tables with a header separator row (header row shaded and repeated), --- dividers, fenced or indented code blocks (monospace, line breaks kept), [^label] footnotes with definitions, inline spans (**bold**, *italic*, `code`, [text](url)) applied as runs with real hyperlink targets, plain `> quote` blocks (written in the Quote paragraph style: blank > lines start a new paragraph, > > nests up to 3 levels, flat lists and fenced code are kept inside, a table, heading or image inside a quote fails), and standalone block images/charts () resolved from allowedDir. Out-of-scope input fails closed with E_VALIDATION plus a 1-based line number and writes nothing: raw HTML (except [!NOTE]-style alert callouts, which compile to callout containers and keep bold, italic, code and links), custom styles, deeper headings, nested lists or tables, task items. Block image/chart refs need allowedDir pointing at the folder holding the files (refs stay jailed inside it; without allowedDir they fail closed); remote URL refs fail closed; inline data-URL block images bypass allowedDir by design (bytes decode in memory through the same image gates). Sources compiling past 500 operations apply as sequential 500-operation chunks; the compose is atomic as a whole: when any chunk fails, every chunk before it is rewound and the document stays as it was. Citation numbers and the References list restart in each compose call. Optional template (report|memo|blank) is accepted on the first compose only and requires the open document to still hold that template seed, otherwise composition fails before any write (to append to an existing document omit template; for a fresh templated document call new_document first, then compose_document with template), and placement is append after existing content except for an unchanged built-in template created in this session. Reopened documents and external templates keep existing content. Returns {ok:true,opCount,appliedCount,revision,createdNodeIds,results,validation:{valid,errorCount,errors}} where createdNodeIds lists new node ids in document order (use them as anchors without re-inspecting) and validation proves the post-compose package state via a read-only OOXML check (save_document re-gates before writing). A compose that drops a non-empty redo stack signals it (redoCleared:true + redoClearedWarning naming the depth): a fresh edit invalidates redo. Optional styleGuide (bundled guide name or guide .md path, jailed like image refs: explicit allowedDir else the session anchor; remote URLs fail closed) replaces the session guide (not a one-batch override: later batches inherit it until another compose replaces it) and skins this compose (table/header-fill, chart-palette, callout, and code defaults apply where ops omit params; explicit params still win). Guides cannot be detached: open a new session to work without a guide. With no guide, compose uses the built-in Default Corporate look (navy table header, bold white header text, striped rows). styleGuide "legacy" gives the older grey header for this call only and is not stored on the session. An italic-only line directly under the opening # title becomes the Subtitle; a [!NOTE]-style line outside a blockquote stays plain text and comes back in warnings. Later batches inherit it, inspect_document shows its compact summary, save_document reports advisory styleWarnings. Unknown names fail with E_VALIDATION naming the available set. Math: $$ ... $$ or \\[ ... \\] blocks become display equations and \\(...\\) becomes inline math (one line, up to 500 characters; refused inside links and table cells). Display blocks are top level only (inside a list item or alert body they fail closed). Optional dollarMath (default false) treats $...$ as inline math; off by default because prices and shell text can look like math. Workflow: open_document -> compose_document -> save_document. Example {"sessionId":"sess_abc","markdown":"# Plan\\n\\n- a\\n- b\\n"} (add "allowedDir":"<dir>" for image/chart refs, "template":"report|memo|blank" to verify the seed). IDs are session-scoped: valid only in the issuing session, must not be stored across sessions. When dc:title is still a template placeholder it is set from the first Title or Heading 1 (titleSetFrom says so); a title already set is kept. persistentIds maps each created paragraph id (md_0003) to the paragraph id the saved file carries, so a reopened session can be addressed with it (tables are renumbered t_001.. by position on reopen).';
|
|
42946
44038
|
function persistentParagraphIds(ast, createdIds) {
|
|
42947
44039
|
const wanted = new Set(createdIds);
|
|
42948
44040
|
const out = {};
|
|
@@ -43008,22 +44100,22 @@ function assertPathAllowed(allowedDir, roots) {
|
|
|
43008
44100
|
remedy: `Use a folder ${describeAllowedFolders()}.`
|
|
43009
44101
|
});
|
|
43010
44102
|
}
|
|
43011
|
-
const resolved =
|
|
44103
|
+
const resolved = path15.resolve(trimmed);
|
|
43012
44104
|
const canonical = canonicalizeForAsset(resolved);
|
|
43013
44105
|
const inside = roots.some((root) => {
|
|
43014
44106
|
let canonicalRoot = root;
|
|
43015
44107
|
try {
|
|
43016
|
-
canonicalRoot =
|
|
44108
|
+
canonicalRoot = fs16.realpathSync(root);
|
|
43017
44109
|
} catch {
|
|
43018
44110
|
}
|
|
43019
|
-
const normC =
|
|
43020
|
-
const normR =
|
|
44111
|
+
const normC = path15.normalize(canonical);
|
|
44112
|
+
const normR = path15.normalize(canonicalRoot);
|
|
43021
44113
|
if (process.platform === "win32") {
|
|
43022
44114
|
const lc = normC.toLowerCase();
|
|
43023
44115
|
const lr = normR.toLowerCase();
|
|
43024
|
-
return lc === lr || lc.startsWith(lr +
|
|
44116
|
+
return lc === lr || lc.startsWith(lr + path15.sep.toLowerCase());
|
|
43025
44117
|
}
|
|
43026
|
-
return normC === normR || normC.startsWith(normR +
|
|
44118
|
+
return normC === normR || normC.startsWith(normR + path15.sep);
|
|
43027
44119
|
});
|
|
43028
44120
|
if (!inside) {
|
|
43029
44121
|
throw Object.assign(
|
|
@@ -43401,8 +44493,8 @@ async function handleExecuteBatch(store, input) {
|
|
|
43401
44493
|
const explicit = parsed.data.allowedDir;
|
|
43402
44494
|
if (explicit !== void 0) {
|
|
43403
44495
|
batchAllowedDir = assertPathAllowed(explicit, getMcpAllowedRoots());
|
|
43404
|
-
} else if (
|
|
43405
|
-
batchAllowedDir = canonicalizeForAsset(
|
|
44496
|
+
} else if (path16.isAbsolute(handle.filePath)) {
|
|
44497
|
+
batchAllowedDir = canonicalizeForAsset(path16.resolve(path16.dirname(handle.filePath)));
|
|
43406
44498
|
} else {
|
|
43407
44499
|
throw Object.assign(
|
|
43408
44500
|
new Error(
|
|
@@ -43551,8 +44643,9 @@ async function handleExecuteBatch(store, input) {
|
|
|
43551
44643
|
}
|
|
43552
44644
|
|
|
43553
44645
|
// src/lib/mcp/handlers/save-close.ts
|
|
43554
|
-
var
|
|
43555
|
-
var
|
|
44646
|
+
var import_crypto7 = require("crypto");
|
|
44647
|
+
var fsp8 = __toESM(require("fs/promises"));
|
|
44648
|
+
var path17 = __toESM(require("path"));
|
|
43556
44649
|
async function executeSaveDocument(store, input) {
|
|
43557
44650
|
const parsed = SaveDocumentSchema.safeParse(input);
|
|
43558
44651
|
if (!parsed.success) {
|
|
@@ -43598,18 +44691,18 @@ async function executeSaveDocument(store, input) {
|
|
|
43598
44691
|
),
|
|
43599
44692
|
{
|
|
43600
44693
|
code: "E_VALIDATION",
|
|
43601
|
-
remedy: `name the output "${
|
|
44694
|
+
remedy: `name the output "${path17.basename(target)}" with a .docm extension, or add stripMacros: true to remove the macro parts`
|
|
43602
44695
|
}
|
|
43603
44696
|
);
|
|
43604
44697
|
}
|
|
43605
44698
|
if (isStripped && macroParts.length === 0 && /\.(docm|dotm)$/i.test(target)) {
|
|
43606
44699
|
throw Object.assign(
|
|
43607
44700
|
new Error(
|
|
43608
|
-
`E_VALIDATION: save_document: stripMacros removes the macro parts, so the result is a plain document and cannot be written to "${
|
|
44701
|
+
`E_VALIDATION: save_document: stripMacros removes the macro parts, so the result is a plain document and cannot be written to "${path17.basename(target)}", a macro-enabled name`
|
|
43609
44702
|
),
|
|
43610
44703
|
{
|
|
43611
44704
|
code: "E_VALIDATION",
|
|
43612
|
-
remedy: `write the result to a .docx with outPath: "${
|
|
44705
|
+
remedy: `write the result to a .docx with outPath: "${path17.basename(target).replace(/\.docm$/i, ".docx")}", or use allowMacros: true to keep the macro parts in the .docm`
|
|
43613
44706
|
}
|
|
43614
44707
|
);
|
|
43615
44708
|
}
|
|
@@ -43650,19 +44743,22 @@ async function executeSaveDocument(store, input) {
|
|
|
43650
44743
|
const saveGateNormalized = readParserNormalizations(handle.parser);
|
|
43651
44744
|
const buf = handle.parser.rezip();
|
|
43652
44745
|
const saveNormalized = mergeNormalized(saveGateNormalized, readParserNormalizations(handle.parser));
|
|
44746
|
+
const savedXml = handle.parser.readDocumentXml();
|
|
44747
|
+
const extras = collectTrackedExtrasForGit(handle.parser);
|
|
43653
44748
|
const prevRevision = handle.revision;
|
|
43654
44749
|
const prevSavedRevision = handle.savedRevision;
|
|
43655
44750
|
const prevByteSize = handle.byteSize;
|
|
43656
44751
|
const prevFilePath = handle.filePath;
|
|
43657
44752
|
let prevBytes = null;
|
|
43658
44753
|
try {
|
|
43659
|
-
prevBytes = await
|
|
44754
|
+
prevBytes = await fsp8.readFile(target);
|
|
43660
44755
|
} catch {
|
|
43661
44756
|
prevBytes = null;
|
|
43662
44757
|
}
|
|
44758
|
+
await assertHistoryReadyForWrite(target);
|
|
43663
44759
|
try {
|
|
43664
44760
|
assertWriteTargetNoSymlink(target);
|
|
43665
|
-
await
|
|
44761
|
+
await fsp8.mkdir(pathDirname(target), { recursive: true });
|
|
43666
44762
|
assertWriteTargetNoSymlink(target);
|
|
43667
44763
|
await writeFileNoFollow(target, buf);
|
|
43668
44764
|
} catch (err) {
|
|
@@ -43673,7 +44769,7 @@ async function executeSaveDocument(store, input) {
|
|
|
43673
44769
|
if (prevBytes !== null) {
|
|
43674
44770
|
await writeFileNoFollow(target, prevBytes);
|
|
43675
44771
|
} else {
|
|
43676
|
-
await
|
|
44772
|
+
await fsp8.unlink(target);
|
|
43677
44773
|
}
|
|
43678
44774
|
} catch (restoreErr) {
|
|
43679
44775
|
diskOk = false;
|
|
@@ -43685,8 +44781,6 @@ async function executeSaveDocument(store, input) {
|
|
|
43685
44781
|
remedy
|
|
43686
44782
|
});
|
|
43687
44783
|
}
|
|
43688
|
-
const savedXml = handle.parser.readDocumentXml();
|
|
43689
|
-
const extras = collectTrackedExtrasForGit(handle.parser);
|
|
43690
44784
|
const opsSinceSave = handle.revision - handle.savedRevision;
|
|
43691
44785
|
const fallbackMessage = opsSinceSave <= 0 ? `save_document: rev ${handle.revision} (initial baseline)` : `save_document: rev ${handle.revision} (${opsSinceSave} ops since last save)`;
|
|
43692
44786
|
const baseMessage = sanitizeCommitMessage(parsed.data.message, fallbackMessage);
|
|
@@ -43694,7 +44788,7 @@ async function executeSaveDocument(store, input) {
|
|
|
43694
44788
|
const authorName = sanitizeCommitAuthor(parsed.data.author);
|
|
43695
44789
|
let commitOid;
|
|
43696
44790
|
try {
|
|
43697
|
-
await ensureGitRepo(target, savedXml, extras);
|
|
44791
|
+
await ensureGitRepo(target, savedXml, extras, prevBytes ?? void 0);
|
|
43698
44792
|
commitOid = await commitChange(target, savedXml, commitMessage, extras, void 0, authorName);
|
|
43699
44793
|
} catch (err) {
|
|
43700
44794
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -43703,7 +44797,7 @@ async function executeSaveDocument(store, input) {
|
|
|
43703
44797
|
if (prevBytes !== null) {
|
|
43704
44798
|
await writeFileNoFollow(target, prevBytes);
|
|
43705
44799
|
} else {
|
|
43706
|
-
await
|
|
44800
|
+
await fsp8.unlink(target);
|
|
43707
44801
|
}
|
|
43708
44802
|
} catch (restoreErr) {
|
|
43709
44803
|
diskOk = false;
|
|
@@ -43724,6 +44818,15 @@ async function executeSaveDocument(store, input) {
|
|
|
43724
44818
|
}
|
|
43725
44819
|
try {
|
|
43726
44820
|
handle.filePath = target;
|
|
44821
|
+
if (prevFilePath !== target || !handle.originalInfo) {
|
|
44822
|
+
handle.originalInfo = await getOriginalInfo(target);
|
|
44823
|
+
handle.originalPin = handle.originalInfo.available ? { kind: "retained", sha256: handle.originalInfo.sha256 } : void 0;
|
|
44824
|
+
}
|
|
44825
|
+
if (prevFilePath !== target) handle.originalWarning = void 0;
|
|
44826
|
+
if (!handle.originalInfo.available && !["predates-feature", "not-applicable"].includes(handle.originalInfo.reason)) {
|
|
44827
|
+
handle.originalWarning = `Exact-original recovery is unavailable: ${handle.originalInfo.reason}. Keep a separate copy of the input file.`;
|
|
44828
|
+
}
|
|
44829
|
+
handle.lastWrittenSha256 = (0, import_crypto7.createHash)("sha256").update(buf).digest("hex");
|
|
43727
44830
|
handle.byteSize = buf.length;
|
|
43728
44831
|
const auditNodeIds = [...handle.pendingAudit.nodeIds];
|
|
43729
44832
|
const auditOps = handle.pendingAudit.ops;
|
|
@@ -43761,6 +44864,7 @@ async function executeSaveDocument(store, input) {
|
|
|
43761
44864
|
ok: true,
|
|
43762
44865
|
sessionId: handle.sessionId,
|
|
43763
44866
|
path: target,
|
|
44867
|
+
...handle.originalWarning ? { originalWarning: handle.originalWarning } : {},
|
|
43764
44868
|
revision: handle.revision,
|
|
43765
44869
|
commitOid,
|
|
43766
44870
|
commitMessage,
|
|
@@ -43830,8 +44934,151 @@ async function handleCloseDocument(store, input) {
|
|
|
43830
44934
|
}
|
|
43831
44935
|
}
|
|
43832
44936
|
|
|
44937
|
+
// src/lib/mcp/handlers/original.ts
|
|
44938
|
+
var fsp9 = __toESM(require("fs/promises"));
|
|
44939
|
+
var path18 = __toESM(require("path"));
|
|
44940
|
+
var import_crypto8 = require("crypto");
|
|
44941
|
+
var hash = (buf) => (0, import_crypto8.createHash)("sha256").update(buf).digest("hex");
|
|
44942
|
+
function refuse2(code, message, remedy = "Inspect the file and its history before retrying. Export the original to a new path for recovery without changing this document.") {
|
|
44943
|
+
throw Object.assign(new Error(message), { code, remedy });
|
|
44944
|
+
}
|
|
44945
|
+
async function requireAvailableOriginal(handle) {
|
|
44946
|
+
const info = await resolveSessionOriginal(handle);
|
|
44947
|
+
if (info.available) return;
|
|
44948
|
+
const detail = info.detail ?? "Close and reopen after checking history.";
|
|
44949
|
+
const remedy = info.reason === "initializing" ? "History initialization for this document has not finished. Call get_document_history again later; the original becomes available once it completes for this same file." : "The retained original cannot be trusted, so no substitute is returned. Keep the working file and any separate backup, and call get_document_history to see the reason.";
|
|
44950
|
+
refuse2("E_CONFLICT", `Exact-original recovery is unavailable to this session (${info.reason}). ${detail}`, remedy);
|
|
44951
|
+
}
|
|
44952
|
+
async function handleExportOriginalDocument(store, input) {
|
|
44953
|
+
const parsed = ExportOriginalDocumentSchema.safeParse(input);
|
|
44954
|
+
if (!parsed.success) return invalidInput(`export_original_document: ${parsed.error.message}`);
|
|
44955
|
+
try {
|
|
44956
|
+
return await store.withSession(parsed.data.sessionId, async (handle) => {
|
|
44957
|
+
if (!path18.isAbsolute(handle.filePath)) refuse2("E_VALIDATION", "This memory session has no retained original.");
|
|
44958
|
+
await requireAvailableOriginal(handle);
|
|
44959
|
+
const outPath = validateSaveTarget(parsed.data.outPath);
|
|
44960
|
+
if (outPath === handle.filePath) refuse2("E_CONFLICT", "Use restore_original_document to replace the active document.");
|
|
44961
|
+
const original = await exportOriginal(handle.filePath, outPath, { overwrite: parsed.data.overwrite === true });
|
|
44962
|
+
return { ok: true, sessionId: handle.sessionId, path: outPath, original };
|
|
44963
|
+
});
|
|
44964
|
+
} catch (err) {
|
|
44965
|
+
return mcpErrorFrom(err, "E_IO", "Choose a writable output path inside the allowed directory.");
|
|
44966
|
+
}
|
|
44967
|
+
}
|
|
44968
|
+
async function confirmHistoryCurrent(handle, bytes) {
|
|
44969
|
+
const history = await getHistory(handle.filePath);
|
|
44970
|
+
if (!history[0]) refuse2("E_CONFLICT", "Cannot confirm history is current: there is no recorded head.");
|
|
44971
|
+
const head = await checkoutCommit(handle.filePath, history[0].oid);
|
|
44972
|
+
if (!head.complete) refuse2("E_CONFLICT", "Cannot confirm history is current: the head is not a complete package snapshot. Save the document first.");
|
|
44973
|
+
const parser2 = new DocxParser(bytes, handle.macroPolicy);
|
|
44974
|
+
await parser2.getAST();
|
|
44975
|
+
const snapshot3 = parser2.capturePackageSnapshot();
|
|
44976
|
+
if (parser2.readDocumentXml() !== head.xml) refuse2("E_CONFLICT", "History differs from the current file. Save or revert before restoring the original.");
|
|
44977
|
+
const names = [...snapshot3.keys()].filter(isTrackedPackageFile).sort();
|
|
44978
|
+
const recorded = [...head.extra.keys()].sort();
|
|
44979
|
+
if (JSON.stringify(names) !== JSON.stringify(recorded) || names.some((name2) => !snapshot3.get(name2).equals(head.extra.get(name2)))) {
|
|
44980
|
+
refuse2("E_CONFLICT", "History is behind the file or the file was modified outside ASTra. Save or revert before restoring the original.");
|
|
44981
|
+
}
|
|
44982
|
+
return { oid: history[0].oid, parser: parser2 };
|
|
44983
|
+
}
|
|
44984
|
+
async function handleRestoreOriginalDocument(store, input) {
|
|
44985
|
+
const parsed = RestoreOriginalDocumentSchema.safeParse(input);
|
|
44986
|
+
if (!parsed.success) return invalidInput(`restore_original_document: ${parsed.error.message}`);
|
|
44987
|
+
try {
|
|
44988
|
+
return await store.withSession(parsed.data.sessionId, async (handle) => {
|
|
44989
|
+
if (!path18.isAbsolute(handle.filePath)) refuse2("E_VALIDATION", "This memory session has no retained original.");
|
|
44990
|
+
await requireAvailableOriginal(handle);
|
|
44991
|
+
if (handle.revision !== handle.savedRevision && !parsed.data.discard) refuse2("E_CONFLICT", "The session has unsaved edits. Save first, or explicitly pass discard:true.");
|
|
44992
|
+
const source = validateSaveTarget(handle.filePath);
|
|
44993
|
+
await assertHistoryReadyForWrite(source);
|
|
44994
|
+
const original = await getOriginalInfo(source);
|
|
44995
|
+
if (!original.available) refuse2("E_NOT_FOUND", `The original is unavailable: ${original.reason}.`);
|
|
44996
|
+
const bytes = await readOriginal(source);
|
|
44997
|
+
const previous = await fsp9.readFile(source);
|
|
44998
|
+
if (!handle.lastWrittenSha256 || hash(previous) !== handle.lastWrittenSha256) refuse2("E_CONFLICT", "The file changed outside ASTra. Reopen it before restoring.");
|
|
44999
|
+
const checked = await confirmHistoryCurrent(handle, previous);
|
|
45000
|
+
const parser2 = new DocxParser(bytes, handle.macroPolicy);
|
|
45001
|
+
const ast = await parser2.getAST();
|
|
45002
|
+
const engine = new OperationEngine(ast, parser2);
|
|
45003
|
+
if (handle.styleGuide) engine.setStyleGuide(handle.styleGuide);
|
|
45004
|
+
const validation = await validateForBaseline(bytes);
|
|
45005
|
+
const backup = newRestoreBackupPath(source);
|
|
45006
|
+
await createVerifiedBackup(backup, previous);
|
|
45007
|
+
const historyBefore = await captureHistoryState(source);
|
|
45008
|
+
const oldRedo = Array.from(handle.redoStack);
|
|
45009
|
+
const previousSession = {
|
|
45010
|
+
parser: handle.parser,
|
|
45011
|
+
ast: handle.ast,
|
|
45012
|
+
engine: handle.engine,
|
|
45013
|
+
toon: handle.toon,
|
|
45014
|
+
revision: handle.revision,
|
|
45015
|
+
savedRevision: handle.savedRevision,
|
|
45016
|
+
byteSize: handle.byteSize,
|
|
45017
|
+
lastWrittenSha256: handle.lastWrittenSha256,
|
|
45018
|
+
validationBaseline: handle.validationBaseline,
|
|
45019
|
+
pendingAudit: { ops: handle.pendingAudit.ops, nodeIds: [...handle.pendingAudit.nodeIds] }
|
|
45020
|
+
};
|
|
45021
|
+
let replaced = false;
|
|
45022
|
+
let commitAttempted = false;
|
|
45023
|
+
let commitOid;
|
|
45024
|
+
let durabilityUnconfirmed = false;
|
|
45025
|
+
try {
|
|
45026
|
+
handle.redoStack.length = 0;
|
|
45027
|
+
try {
|
|
45028
|
+
handle.redoStack.push((parsed.data.discard ? checked.parser : handle.parser).capturePackageSnapshot());
|
|
45029
|
+
} catch (quota) {
|
|
45030
|
+
if (quota?.code !== "E_QUOTA") throw quota;
|
|
45031
|
+
}
|
|
45032
|
+
if (hash(await fsp9.readFile(source)) !== hash(previous)) refuse2("E_CONFLICT", "The file changed while restore was being prepared.");
|
|
45033
|
+
const write = await replaceFileAtomic(source, bytes);
|
|
45034
|
+
replaced = true;
|
|
45035
|
+
durabilityUnconfirmed = write.durabilityUnconfirmed === true;
|
|
45036
|
+
commitAttempted = true;
|
|
45037
|
+
commitOid = await commitChange(source, parser2.readDocumentXml(), `Restore original file (sha256 ${hash(bytes)})`, collectTrackedExtrasForGit(parser2));
|
|
45038
|
+
handle.parser = parser2;
|
|
45039
|
+
handle.ast = ast;
|
|
45040
|
+
handle.engine = engine;
|
|
45041
|
+
handle.toon = new ToonGenerator();
|
|
45042
|
+
handle.byteSize = bytes.length;
|
|
45043
|
+
store.bumpRevision(handle);
|
|
45044
|
+
store.markSaved(handle);
|
|
45045
|
+
handle.lastWrittenSha256 = hash(bytes);
|
|
45046
|
+
adoptBaseline(handle, validation);
|
|
45047
|
+
} catch (err) {
|
|
45048
|
+
Object.assign(handle, previousSession);
|
|
45049
|
+
handle.redoStack.length = 0;
|
|
45050
|
+
Array.prototype.push.apply(handle.redoStack, oldRedo);
|
|
45051
|
+
const { recovered: diskRecovered, failure: rollbackFailure } = await rollbackReplacedFile(source, previous, { replaced, error: err });
|
|
45052
|
+
const rollbackError = rollbackFailure ?? "";
|
|
45053
|
+
const historyChanged = commitAttempted && await historyChangedSince(source, historyBefore);
|
|
45054
|
+
if (diskRecovered) await fsp9.unlink(backup).catch(() => void 0);
|
|
45055
|
+
const detail = `${String(err instanceof Error ? err.message : err)}${historyChanged ? " History may have changed despite the failed commit; inspect history before continuing." : ""}${diskRecovered ? "" : ` Rollback failed (${rollbackError}); recovery bytes are retained at ${backup}.`}`;
|
|
45056
|
+
throw Object.assign(new Error(detail), {
|
|
45057
|
+
code: err?.code ?? "E_IO",
|
|
45058
|
+
remedy: diskRecovered ? "Inspect the file and history before retrying." : `Recover from ${backup} before editing further.`
|
|
45059
|
+
});
|
|
45060
|
+
}
|
|
45061
|
+
const cleanupFailed = await fsp9.unlink(backup).then(() => false, () => true);
|
|
45062
|
+
return {
|
|
45063
|
+
ok: true,
|
|
45064
|
+
sessionId: handle.sessionId,
|
|
45065
|
+
path: source,
|
|
45066
|
+
revision: handle.revision,
|
|
45067
|
+
dirty: false,
|
|
45068
|
+
commitOid,
|
|
45069
|
+
original,
|
|
45070
|
+
...durabilityUnconfirmed ? { durabilityUnconfirmed: true } : {},
|
|
45071
|
+
...cleanupFailed ? { warning: `Restore succeeded but the temporary recovery backup could not be removed: ${backup}` } : {}
|
|
45072
|
+
};
|
|
45073
|
+
});
|
|
45074
|
+
} catch (err) {
|
|
45075
|
+
return mcpErrorFrom(err, "E_IO", "Inspect the document and history; export the original to a new path if in-place restore is unavailable.");
|
|
45076
|
+
}
|
|
45077
|
+
}
|
|
45078
|
+
|
|
43833
45079
|
// src/lib/mcp/handlers/history.ts
|
|
43834
|
-
var
|
|
45080
|
+
var import_crypto9 = require("crypto");
|
|
45081
|
+
var fsp10 = __toESM(require("fs/promises"));
|
|
43835
45082
|
async function handleGetDocumentHistory(store, input) {
|
|
43836
45083
|
const parsed = GetDocumentHistorySchema.safeParse(input);
|
|
43837
45084
|
if (!parsed.success) {
|
|
@@ -43858,7 +45105,21 @@ async function handleGetDocumentHistory(store, input) {
|
|
|
43858
45105
|
currentCommit: null,
|
|
43859
45106
|
totalCommits: 0,
|
|
43860
45107
|
history: [],
|
|
43861
|
-
warning: emptyWarning
|
|
45108
|
+
warning: emptyWarning,
|
|
45109
|
+
original: await resolveSessionOriginal(handle)
|
|
45110
|
+
};
|
|
45111
|
+
}
|
|
45112
|
+
const original = await resolveSessionOriginal(handle);
|
|
45113
|
+
if (!original.available && original.reason === "initializing") {
|
|
45114
|
+
return {
|
|
45115
|
+
ok: true,
|
|
45116
|
+
sessionId,
|
|
45117
|
+
documentPath: filePath,
|
|
45118
|
+
currentCommit: null,
|
|
45119
|
+
totalCommits: 0,
|
|
45120
|
+
history: [],
|
|
45121
|
+
warning: original.detail ?? "History initialization has not completed.",
|
|
45122
|
+
original
|
|
43862
45123
|
};
|
|
43863
45124
|
}
|
|
43864
45125
|
const full = await getHistory(filePath);
|
|
@@ -43892,7 +45153,10 @@ async function handleGetDocumentHistory(store, input) {
|
|
|
43892
45153
|
currentCommit: full.length > 0 ? full[0].oid : null,
|
|
43893
45154
|
totalCommits: full.length,
|
|
43894
45155
|
history: slice,
|
|
43895
|
-
warning
|
|
45156
|
+
warning,
|
|
45157
|
+
// Recovery status was re-verified above against what this session pinned at open, so a corrupted or
|
|
45158
|
+
// replaced original is not reported as available and another history's original is never adopted.
|
|
45159
|
+
original
|
|
43896
45160
|
};
|
|
43897
45161
|
});
|
|
43898
45162
|
} catch (err) {
|
|
@@ -43994,7 +45258,7 @@ async function handleRevertDocument(store, input) {
|
|
|
43994
45258
|
const prevByteSize = handle.byteSize;
|
|
43995
45259
|
let prevFileBytes = null;
|
|
43996
45260
|
try {
|
|
43997
|
-
prevFileBytes = await
|
|
45261
|
+
prevFileBytes = await fsp10.readFile(handle.filePath);
|
|
43998
45262
|
} catch {
|
|
43999
45263
|
prevFileBytes = null;
|
|
44000
45264
|
}
|
|
@@ -44017,6 +45281,7 @@ async function handleRevertDocument(store, input) {
|
|
|
44017
45281
|
truncated: false
|
|
44018
45282
|
};
|
|
44019
45283
|
}
|
|
45284
|
+
await assertHistoryReadyForWrite(handle.filePath);
|
|
44020
45285
|
let target;
|
|
44021
45286
|
try {
|
|
44022
45287
|
target = await checkoutCommit(handle.filePath, targetOid);
|
|
@@ -44046,7 +45311,7 @@ async function handleRevertDocument(store, input) {
|
|
|
44046
45311
|
}
|
|
44047
45312
|
assertWriteTargetNoSymlink(handle.filePath);
|
|
44048
45313
|
writeAttempted = true;
|
|
44049
|
-
await
|
|
45314
|
+
await fsp10.mkdir(pathDirname(handle.filePath), { recursive: true });
|
|
44050
45315
|
assertWriteTargetNoSymlink(handle.filePath);
|
|
44051
45316
|
await writeFileNoFollow(handle.filePath, buf);
|
|
44052
45317
|
handle.byteSize = buf.length;
|
|
@@ -44075,6 +45340,7 @@ async function handleRevertDocument(store, input) {
|
|
|
44075
45340
|
affectedNodeIds: diffIdTexts(liveBefore.texts, liveAfter.order, liveAfter.texts)
|
|
44076
45341
|
});
|
|
44077
45342
|
store.markSaved(handle);
|
|
45343
|
+
handle.lastWrittenSha256 = (0, import_crypto9.createHash)("sha256").update(buf).digest("hex");
|
|
44078
45344
|
adoptBaseline(handle, candidate);
|
|
44079
45345
|
return {
|
|
44080
45346
|
ok: true,
|
|
@@ -44110,7 +45376,7 @@ async function handleRevertDocument(store, input) {
|
|
|
44110
45376
|
if (prevFileBytes !== null) {
|
|
44111
45377
|
await writeFileNoFollow(handle.filePath, prevFileBytes);
|
|
44112
45378
|
} else {
|
|
44113
|
-
await
|
|
45379
|
+
await fsp10.unlink(handle.filePath);
|
|
44114
45380
|
}
|
|
44115
45381
|
} catch (restoreErr) {
|
|
44116
45382
|
diskOk = false;
|
|
@@ -44167,6 +45433,7 @@ async function handleRedoDocument(store, input) {
|
|
|
44167
45433
|
const redoReasonProvided = "reason" in rawRedo && parsed.data.reason !== void 0;
|
|
44168
45434
|
const redoReasonText = redoReasonProvided ? sanitizeCommitMessage(parsed.data.reason, "") : "";
|
|
44169
45435
|
const authorName = sanitizeCommitAuthor(parsed.data.author);
|
|
45436
|
+
await assertHistoryReadyForWrite(handle.filePath);
|
|
44170
45437
|
const redoBefore = collectAstIdTexts(handle.ast);
|
|
44171
45438
|
const prevRevision = handle.revision;
|
|
44172
45439
|
const prevSavedRevision = handle.savedRevision;
|
|
@@ -44174,7 +45441,7 @@ async function handleRedoDocument(store, input) {
|
|
|
44174
45441
|
const preRedoSnap = handle.parser.capturePackageSnapshot();
|
|
44175
45442
|
let prevFileBytes = null;
|
|
44176
45443
|
try {
|
|
44177
|
-
prevFileBytes = await
|
|
45444
|
+
prevFileBytes = await fsp10.readFile(handle.filePath);
|
|
44178
45445
|
} catch {
|
|
44179
45446
|
prevFileBytes = null;
|
|
44180
45447
|
}
|
|
@@ -44194,7 +45461,7 @@ async function handleRedoDocument(store, input) {
|
|
|
44194
45461
|
didPop = true;
|
|
44195
45462
|
assertWriteTargetNoSymlink(handle.filePath);
|
|
44196
45463
|
writeAttempted = true;
|
|
44197
|
-
await
|
|
45464
|
+
await fsp10.mkdir(pathDirname(handle.filePath), { recursive: true });
|
|
44198
45465
|
assertWriteTargetNoSymlink(handle.filePath);
|
|
44199
45466
|
await writeFileNoFollow(handle.filePath, buf);
|
|
44200
45467
|
handle.byteSize = buf.length;
|
|
@@ -44227,6 +45494,7 @@ async function handleRedoDocument(store, input) {
|
|
|
44227
45494
|
affectedNodeIds: diffIdTexts(redoBefore.texts, redoAfter.order, redoAfter.texts)
|
|
44228
45495
|
});
|
|
44229
45496
|
store.markSaved(handle);
|
|
45497
|
+
handle.lastWrittenSha256 = (0, import_crypto9.createHash)("sha256").update(buf).digest("hex");
|
|
44230
45498
|
adoptBaseline(handle, candidate);
|
|
44231
45499
|
return {
|
|
44232
45500
|
ok: true,
|
|
@@ -44261,7 +45529,7 @@ async function handleRedoDocument(store, input) {
|
|
|
44261
45529
|
if (prevFileBytes !== null) {
|
|
44262
45530
|
await writeFileNoFollow(handle.filePath, prevFileBytes);
|
|
44263
45531
|
} else {
|
|
44264
|
-
await
|
|
45532
|
+
await fsp10.unlink(handle.filePath);
|
|
44265
45533
|
}
|
|
44266
45534
|
} catch (restoreErr) {
|
|
44267
45535
|
diskOk = false;
|
|
@@ -44455,6 +45723,8 @@ async function handlePreviewRevert(store, input) {
|
|
|
44455
45723
|
}
|
|
44456
45724
|
}
|
|
44457
45725
|
var GIT_HISTORY_HANDLERS = {
|
|
45726
|
+
export_original_document: handleExportOriginalDocument,
|
|
45727
|
+
restore_original_document: handleRestoreOriginalDocument,
|
|
44458
45728
|
get_document_history: handleGetDocumentHistory,
|
|
44459
45729
|
preview_revert: handlePreviewRevert,
|
|
44460
45730
|
revert_document: handleRevertDocument,
|
|
@@ -44502,9 +45772,9 @@ function adoptForkNumbering(fork, ast) {
|
|
|
44502
45772
|
}
|
|
44503
45773
|
|
|
44504
45774
|
// src/tools/export.ts
|
|
44505
|
-
var
|
|
44506
|
-
var
|
|
44507
|
-
var
|
|
45775
|
+
var fs17 = __toESM(require("fs"));
|
|
45776
|
+
var fsp11 = __toESM(require("fs/promises"));
|
|
45777
|
+
var path19 = __toESM(require("path"));
|
|
44508
45778
|
var ExportDocumentSchema = import_zod6.z.object({
|
|
44509
45779
|
sessionId: import_zod6.z.string().describe("Active document session ID returned by open_document"),
|
|
44510
45780
|
format: import_zod6.z.enum(["html", "markdown", "md", "pdf"]).describe('Target export format: "html", "markdown", "md", or "pdf"'),
|
|
@@ -44560,18 +45830,18 @@ function validateExportTarget(rawPath, expectedFormat) {
|
|
|
44560
45830
|
remedy: "Use a path inside the workspace documents directory."
|
|
44561
45831
|
});
|
|
44562
45832
|
}
|
|
44563
|
-
const resolved =
|
|
45833
|
+
const resolved = path19.resolve(trimmed);
|
|
44564
45834
|
const canonical = canonicalizeForAsset(resolved);
|
|
44565
45835
|
const allowedRoots = getMcpAllowedRoots();
|
|
44566
45836
|
const isInside2 = allowedRoots.some((root) => {
|
|
44567
45837
|
let canonicalRoot = root;
|
|
44568
45838
|
try {
|
|
44569
|
-
canonicalRoot =
|
|
45839
|
+
canonicalRoot = fs17.realpathSync(root);
|
|
44570
45840
|
} catch {
|
|
44571
45841
|
}
|
|
44572
|
-
const normC =
|
|
44573
|
-
const normR =
|
|
44574
|
-
return normC === normR || normC.startsWith(normR +
|
|
45842
|
+
const normC = path19.normalize(canonical);
|
|
45843
|
+
const normR = path19.normalize(canonicalRoot);
|
|
45844
|
+
return normC === normR || normC.startsWith(normR + path19.sep);
|
|
44575
45845
|
});
|
|
44576
45846
|
if (!isInside2) {
|
|
44577
45847
|
throw Object.assign(
|
|
@@ -44582,7 +45852,7 @@ function validateExportTarget(rawPath, expectedFormat) {
|
|
|
44582
45852
|
}
|
|
44583
45853
|
);
|
|
44584
45854
|
}
|
|
44585
|
-
const ext =
|
|
45855
|
+
const ext = path19.extname(trimmed).toLowerCase();
|
|
44586
45856
|
const validExts = {
|
|
44587
45857
|
html: [".html", ".htm"],
|
|
44588
45858
|
markdown: [".md", ".markdown"],
|
|
@@ -44626,18 +45896,18 @@ function validateAssetTarget(rawPath) {
|
|
|
44626
45896
|
remedy: "Use a path inside the workspace documents directory."
|
|
44627
45897
|
});
|
|
44628
45898
|
}
|
|
44629
|
-
const resolved =
|
|
45899
|
+
const resolved = path19.resolve(trimmed);
|
|
44630
45900
|
const canonical = canonicalizeForAsset(resolved);
|
|
44631
45901
|
const allowedRoots = getMcpAllowedRoots();
|
|
44632
45902
|
const isInside2 = allowedRoots.some((root) => {
|
|
44633
45903
|
let canonicalRoot = root;
|
|
44634
45904
|
try {
|
|
44635
|
-
canonicalRoot =
|
|
45905
|
+
canonicalRoot = fs17.realpathSync(root);
|
|
44636
45906
|
} catch {
|
|
44637
45907
|
}
|
|
44638
|
-
const normC =
|
|
44639
|
-
const normR =
|
|
44640
|
-
return normC === normR || normC.startsWith(normR +
|
|
45908
|
+
const normC = path19.normalize(canonical);
|
|
45909
|
+
const normR = path19.normalize(canonicalRoot);
|
|
45910
|
+
return normC === normR || normC.startsWith(normR + path19.sep);
|
|
44641
45911
|
});
|
|
44642
45912
|
if (!isInside2) {
|
|
44643
45913
|
throw Object.assign(
|
|
@@ -44737,7 +46007,7 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44737
46007
|
}
|
|
44738
46008
|
let validatedPdfPath;
|
|
44739
46009
|
if (format === "pdf") {
|
|
44740
|
-
if (!
|
|
46010
|
+
if (!hasPdfBackend2()) {
|
|
44741
46011
|
return {
|
|
44742
46012
|
ok: false,
|
|
44743
46013
|
code: "E_DEPENDENCY",
|
|
@@ -44745,7 +46015,7 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44745
46015
|
remedy: "Install LibreOffice on the host system: sudo apt install libreoffice (Linux) or brew install --cask libreoffice (macOS)."
|
|
44746
46016
|
};
|
|
44747
46017
|
}
|
|
44748
|
-
const defaultPdf = targetPath ||
|
|
46018
|
+
const defaultPdf = targetPath || path19.join(path19.dirname(session.filePath), `${path19.basename(session.filePath, path19.extname(session.filePath))}.pdf`);
|
|
44749
46019
|
try {
|
|
44750
46020
|
validatedPdfPath = validateExportTarget(defaultPdf, "pdf");
|
|
44751
46021
|
} catch (err) {
|
|
@@ -44784,7 +46054,7 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44784
46054
|
}
|
|
44785
46055
|
throw original;
|
|
44786
46056
|
}
|
|
44787
|
-
const stat3 = await
|
|
46057
|
+
const stat3 = await fsp11.stat(validatedPdfPath);
|
|
44788
46058
|
return {
|
|
44789
46059
|
ok: true,
|
|
44790
46060
|
format: "pdf",
|
|
@@ -44798,22 +46068,22 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44798
46068
|
}
|
|
44799
46069
|
const ast = session.ast;
|
|
44800
46070
|
if (format === "html") {
|
|
44801
|
-
const title = htmlDocumentTitle(ast,
|
|
46071
|
+
const title = htmlDocumentTitle(ast, path19.basename(session.filePath, path19.extname(session.filePath)));
|
|
44802
46072
|
const html = convertAstToHtml(ast, title, void 0, session.parser);
|
|
44803
46073
|
if (targetPath) {
|
|
44804
46074
|
assertWriteTargetNoSymlink(targetPath);
|
|
44805
|
-
await
|
|
46075
|
+
await fsp11.mkdir(path19.dirname(targetPath), { recursive: true });
|
|
44806
46076
|
assertWriteTargetNoSymlink(targetPath);
|
|
44807
46077
|
await writeFileNoFollow(targetPath, html, "utf-8");
|
|
44808
|
-
const stat3 = await
|
|
46078
|
+
const stat3 = await fsp11.stat(targetPath);
|
|
44809
46079
|
return { ok: true, format: "html", path: targetPath, bytes: stat3.size, dirty, revision };
|
|
44810
46080
|
}
|
|
44811
46081
|
return { ok: true, format: "html", content: html, bytes: Buffer.byteLength(html, "utf-8"), dirty, revision };
|
|
44812
46082
|
} else {
|
|
44813
46083
|
const effectiveImageMode = parsed.data.imageMode ?? (targetPath ? "file" : "embed");
|
|
44814
|
-
const docBase =
|
|
46084
|
+
const docBase = path19.basename(
|
|
44815
46085
|
targetPath || session.filePath || "document.docx",
|
|
44816
|
-
|
|
46086
|
+
path19.extname(targetPath || session.filePath || "document.docx")
|
|
44817
46087
|
) || "document";
|
|
44818
46088
|
const safeDocBase = docBase.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^\.+/, "").slice(0, 120) || "document";
|
|
44819
46089
|
const defaultAssetPrefix = `${safeDocBase}_assets`;
|
|
@@ -44821,39 +46091,39 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44821
46091
|
if (effectiveImageMode === "file") {
|
|
44822
46092
|
const initialAssets = collectMarkdownImageAssets(ast, effectiveAssetPrefix);
|
|
44823
46093
|
if (!targetPath) throw new Error("Markdown file image mode requires an output path.");
|
|
44824
|
-
const destDir =
|
|
46094
|
+
const destDir = path19.dirname(targetPath);
|
|
44825
46095
|
let generation;
|
|
44826
46096
|
let staging;
|
|
44827
46097
|
let published = false;
|
|
44828
46098
|
try {
|
|
44829
46099
|
assertWriteTargetNoSymlink(targetPath);
|
|
44830
|
-
await
|
|
46100
|
+
await fsp11.mkdir(destDir, { recursive: true });
|
|
44831
46101
|
assertWriteTargetNoSymlink(targetPath);
|
|
44832
46102
|
let outputPrefix = effectiveAssetPrefix;
|
|
44833
46103
|
if (initialAssets.length > 0) {
|
|
44834
|
-
const prefixDir = validateAssetTarget(
|
|
46104
|
+
const prefixDir = validateAssetTarget(path19.resolve(destDir, effectiveAssetPrefix));
|
|
44835
46105
|
assertWriteTargetNoSymlink(prefixDir);
|
|
44836
|
-
await
|
|
46106
|
+
await fsp11.mkdir(prefixDir, { recursive: true });
|
|
44837
46107
|
assertWriteTargetNoSymlink(prefixDir);
|
|
44838
|
-
generation = await
|
|
46108
|
+
generation = await fsp11.mkdtemp(path19.join(prefixDir, "export-"));
|
|
44839
46109
|
assertWriteTargetNoSymlink(generation);
|
|
44840
|
-
outputPrefix =
|
|
46110
|
+
outputPrefix = path19.relative(destDir, generation).split(path19.sep).join("/");
|
|
44841
46111
|
}
|
|
44842
46112
|
const md = convertAstToMarkdown(ast, session.parser, { imageMode: "file", assetPrefix: outputPrefix });
|
|
44843
46113
|
const imageAssets = collectMarkdownImageAssets(ast, outputPrefix);
|
|
44844
46114
|
const writtenAssets = [];
|
|
44845
46115
|
for (const asset of imageAssets) {
|
|
44846
|
-
const assetPath = validateAssetTarget(
|
|
46116
|
+
const assetPath = validateAssetTarget(path19.resolve(destDir, asset.name));
|
|
44847
46117
|
assertWriteTargetNoSymlink(assetPath);
|
|
44848
46118
|
await writeFileNoFollow(assetPath, asset.data);
|
|
44849
46119
|
writtenAssets.push(assetPath);
|
|
44850
46120
|
}
|
|
44851
|
-
staging = await
|
|
44852
|
-
const stagedFile =
|
|
46121
|
+
staging = await fsp11.mkdtemp(path19.join(destDir, ".astra-export-"));
|
|
46122
|
+
const stagedFile = path19.join(staging, "document.md");
|
|
44853
46123
|
assertWriteTargetNoSymlink(stagedFile);
|
|
44854
46124
|
await writeFileNoFollow(stagedFile, md, "utf-8");
|
|
44855
46125
|
assertWriteTargetNoSymlink(targetPath);
|
|
44856
|
-
await
|
|
46126
|
+
await fsp11.rename(stagedFile, targetPath);
|
|
44857
46127
|
published = true;
|
|
44858
46128
|
return {
|
|
44859
46129
|
ok: true,
|
|
@@ -44868,17 +46138,17 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44868
46138
|
assetCount: writtenAssets.length
|
|
44869
46139
|
};
|
|
44870
46140
|
} finally {
|
|
44871
|
-
if (staging) await
|
|
44872
|
-
if (!published && generation) await
|
|
46141
|
+
if (staging) await fsp11.rm(staging, { recursive: true, force: true }).catch(() => void 0);
|
|
46142
|
+
if (!published && generation) await fsp11.rm(generation, { recursive: true, force: true }).catch(() => void 0);
|
|
44873
46143
|
}
|
|
44874
46144
|
} else {
|
|
44875
46145
|
const md = convertAstToMarkdown(ast, session.parser, { imageMode: "embed" });
|
|
44876
46146
|
if (targetPath) {
|
|
44877
46147
|
assertWriteTargetNoSymlink(targetPath);
|
|
44878
|
-
await
|
|
46148
|
+
await fsp11.mkdir(path19.dirname(targetPath), { recursive: true });
|
|
44879
46149
|
assertWriteTargetNoSymlink(targetPath);
|
|
44880
46150
|
await writeFileNoFollow(targetPath, md, "utf-8");
|
|
44881
|
-
const stat3 = await
|
|
46151
|
+
const stat3 = await fsp11.stat(targetPath);
|
|
44882
46152
|
return {
|
|
44883
46153
|
ok: true,
|
|
44884
46154
|
format: "markdown",
|
|
@@ -44916,8 +46186,8 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44916
46186
|
}
|
|
44917
46187
|
|
|
44918
46188
|
// src/tools/visual.ts
|
|
44919
|
-
var
|
|
44920
|
-
var
|
|
46189
|
+
var fs18 = __toESM(require("fs"));
|
|
46190
|
+
var path20 = __toESM(require("path"));
|
|
44921
46191
|
var import_zod7 = require("zod");
|
|
44922
46192
|
function visualError(code, message, remedy) {
|
|
44923
46193
|
return { ok: false, code, message, remedy };
|
|
@@ -45065,14 +46335,14 @@ function serializeSessionCopy2(parser2, ast) {
|
|
|
45065
46335
|
return fork.rezip();
|
|
45066
46336
|
}
|
|
45067
46337
|
function placeImages(images, dir, docPath) {
|
|
45068
|
-
|
|
45069
|
-
const base =
|
|
46338
|
+
fs18.mkdirSync(dir, { recursive: true });
|
|
46339
|
+
const base = path20.basename(docPath || "document", path20.extname(docPath || "")).replace(/[^a-zA-Z0-9._-]+/g, "-") || "document";
|
|
45070
46340
|
return images.map((img) => {
|
|
45071
|
-
const ext =
|
|
45072
|
-
const dest =
|
|
45073
|
-
writeFileNoFollowSync(dest,
|
|
46341
|
+
const ext = path20.extname(img.path) || (img.format === "jpeg" ? ".jpg" : ".png");
|
|
46342
|
+
const dest = path20.join(dir, `${base}-page-${img.page}${ext}`);
|
|
46343
|
+
writeFileNoFollowSync(dest, fs18.readFileSync(img.path));
|
|
45074
46344
|
try {
|
|
45075
|
-
|
|
46345
|
+
fs18.rmSync(img.path, { force: true });
|
|
45076
46346
|
} catch {
|
|
45077
46347
|
}
|
|
45078
46348
|
return { ...img, path: dest };
|
|
@@ -45199,8 +46469,8 @@ async function handleVisualInspect(store, input) {
|
|
|
45199
46469
|
|
|
45200
46470
|
// src/tools/new-document.ts
|
|
45201
46471
|
var import_zod8 = require("zod");
|
|
45202
|
-
var
|
|
45203
|
-
var
|
|
46472
|
+
var fsp12 = __toESM(require("fs/promises"));
|
|
46473
|
+
var path21 = __toESM(require("path"));
|
|
45204
46474
|
var NEW_DOCUMENT_TEMPLATES = ["report", "memo", "blank"];
|
|
45205
46475
|
var NewDocumentSchema = import_zod8.z.object({
|
|
45206
46476
|
outPath: import_zod8.z.string().min(1).max(1024),
|
|
@@ -45276,10 +46546,10 @@ async function handleNewDocument(store, rawArgs) {
|
|
|
45276
46546
|
}
|
|
45277
46547
|
if (!force) {
|
|
45278
46548
|
try {
|
|
45279
|
-
await
|
|
46549
|
+
await fsp12.access(target);
|
|
45280
46550
|
return newDocError(
|
|
45281
46551
|
"E_CONFLICT",
|
|
45282
|
-
`new_document: destination "${
|
|
46552
|
+
`new_document: destination "${path21.basename(target)}" already exists (refusing to overwrite without force)`,
|
|
45283
46553
|
'Retry with {"force":true} to overwrite, or choose a different outPath.'
|
|
45284
46554
|
);
|
|
45285
46555
|
} catch {
|
|
@@ -45330,7 +46600,7 @@ async function handleNewDocument(store, rawArgs) {
|
|
|
45330
46600
|
}
|
|
45331
46601
|
try {
|
|
45332
46602
|
assertWriteTargetNoSymlink(target);
|
|
45333
|
-
await
|
|
46603
|
+
await fsp12.mkdir(path21.dirname(target), { recursive: true });
|
|
45334
46604
|
assertWriteTargetNoSymlink(target);
|
|
45335
46605
|
await writeFileNoFollow(target, buf);
|
|
45336
46606
|
} catch (err) {
|
|
@@ -45342,12 +46612,12 @@ async function handleNewDocument(store, rawArgs) {
|
|
|
45342
46612
|
);
|
|
45343
46613
|
}
|
|
45344
46614
|
try {
|
|
45345
|
-
const handle = await store.openDocument(target, parsed.data.sessionId);
|
|
46615
|
+
const handle = await store.openDocument(target, parsed.data.sessionId, void 0, false);
|
|
45346
46616
|
handle.parser.adoptBuiltinSeedFrom(parser2);
|
|
45347
46617
|
if (parsed.data.author !== void 0) handle.ast.settings.defaultAuthor = parsed.data.author;
|
|
45348
46618
|
let bytes = buf.length;
|
|
45349
46619
|
try {
|
|
45350
|
-
bytes = (await
|
|
46620
|
+
bytes = (await fsp12.stat(target)).size;
|
|
45351
46621
|
} catch {
|
|
45352
46622
|
}
|
|
45353
46623
|
if (guideRef !== void 0 && resolvedGuide !== void 0) {
|
|
@@ -45377,8 +46647,8 @@ async function handleNewDocument(store, rawArgs) {
|
|
|
45377
46647
|
}
|
|
45378
46648
|
|
|
45379
46649
|
// src/lib/mcp/handlers/parity.ts
|
|
45380
|
-
var
|
|
45381
|
-
var
|
|
46650
|
+
var fs19 = __toESM(require("fs"));
|
|
46651
|
+
var path22 = __toESM(require("path"));
|
|
45382
46652
|
var IMPORT_FILE_MAX_BYTES = COMPOSE_MARKDOWN_MAX;
|
|
45383
46653
|
async function refreshSessionAst(handle) {
|
|
45384
46654
|
await refreshAstInPlace(handle.ast, handle.parser);
|
|
@@ -45463,7 +46733,8 @@ async function handleGetStats(store, input) {
|
|
|
45463
46733
|
tables: stats.tables,
|
|
45464
46734
|
callouts: stats.callouts,
|
|
45465
46735
|
images: stats.images,
|
|
45466
|
-
pages: stats.pages
|
|
46736
|
+
pages: stats.pages,
|
|
46737
|
+
pageTerms: roundedPageTerms(stats.pageTerms)
|
|
45467
46738
|
};
|
|
45468
46739
|
});
|
|
45469
46740
|
} catch (err) {
|
|
@@ -45633,17 +46904,17 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
|
|
|
45633
46904
|
if (segments.includes("..")) {
|
|
45634
46905
|
throw toError("E_SANDBOX", 'import path contains forbidden ".." traversal', "Use a path inside the workspace documents directory.");
|
|
45635
46906
|
}
|
|
45636
|
-
const resolved =
|
|
46907
|
+
const resolved = path22.resolve(trimmed);
|
|
45637
46908
|
const canonical = canonicalizeForAsset(resolved);
|
|
45638
46909
|
const inside = roots.some((root) => {
|
|
45639
46910
|
let canonicalRoot = root;
|
|
45640
46911
|
try {
|
|
45641
|
-
canonicalRoot =
|
|
46912
|
+
canonicalRoot = fs19.realpathSync(root);
|
|
45642
46913
|
} catch {
|
|
45643
46914
|
}
|
|
45644
|
-
const normC =
|
|
45645
|
-
const normR =
|
|
45646
|
-
return normC === normR || normC.startsWith(normR +
|
|
46915
|
+
const normC = path22.normalize(canonical);
|
|
46916
|
+
const normR = path22.normalize(canonicalRoot);
|
|
46917
|
+
return normC === normR || normC.startsWith(normR + path22.sep);
|
|
45647
46918
|
});
|
|
45648
46919
|
if (!inside) {
|
|
45649
46920
|
throw toError(
|
|
@@ -45652,12 +46923,12 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
|
|
|
45652
46923
|
"Place the Markdown file inside one of the allowed directories, or name its folder with --allowed-dir or ASTRA_EXTRA_DOC_ROOTS."
|
|
45653
46924
|
);
|
|
45654
46925
|
}
|
|
45655
|
-
const base =
|
|
45656
|
-
const nofollow =
|
|
45657
|
-
const flags = typeof nofollow === "number" && nofollow !== 0 ?
|
|
46926
|
+
const base = path22.basename(canonical);
|
|
46927
|
+
const nofollow = fs19.constants.O_NOFOLLOW;
|
|
46928
|
+
const flags = typeof nofollow === "number" && nofollow !== 0 ? fs19.constants.O_RDONLY | nofollow : "r";
|
|
45658
46929
|
let fd;
|
|
45659
46930
|
try {
|
|
45660
|
-
fd =
|
|
46931
|
+
fd = fs19.openSync(canonical, flags);
|
|
45661
46932
|
} catch (err) {
|
|
45662
46933
|
const code = err?.code;
|
|
45663
46934
|
if (code === "ENOENT") {
|
|
@@ -45669,7 +46940,7 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
|
|
|
45669
46940
|
throw toError("E_IO", `import file cannot be read (${base})`, "Check the file exists and is readable, then retry.");
|
|
45670
46941
|
}
|
|
45671
46942
|
try {
|
|
45672
|
-
const st =
|
|
46943
|
+
const st = fs19.fstatSync(fd);
|
|
45673
46944
|
if (!st.isFile()) {
|
|
45674
46945
|
throw toError("E_IO", `import reference is not a file (${base})`, "Point the import at a regular Markdown file.");
|
|
45675
46946
|
}
|
|
@@ -45683,7 +46954,7 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
|
|
|
45683
46954
|
"Split the Markdown source into smaller files and import each in its own call."
|
|
45684
46955
|
);
|
|
45685
46956
|
}
|
|
45686
|
-
const buf =
|
|
46957
|
+
const buf = fs19.readFileSync(fd);
|
|
45687
46958
|
if (buf.length > IMPORT_FILE_MAX_BYTES) {
|
|
45688
46959
|
throw toError(
|
|
45689
46960
|
"E_LIMIT",
|
|
@@ -45693,18 +46964,18 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
|
|
|
45693
46964
|
}
|
|
45694
46965
|
let post = canonical;
|
|
45695
46966
|
try {
|
|
45696
|
-
post =
|
|
46967
|
+
post = fs19.realpathSync(canonical);
|
|
45697
46968
|
} catch {
|
|
45698
46969
|
}
|
|
45699
46970
|
const stillInside = roots.some((root) => {
|
|
45700
46971
|
let canonicalRoot = root;
|
|
45701
46972
|
try {
|
|
45702
|
-
canonicalRoot =
|
|
46973
|
+
canonicalRoot = fs19.realpathSync(root);
|
|
45703
46974
|
} catch {
|
|
45704
46975
|
}
|
|
45705
|
-
const normC =
|
|
45706
|
-
const normR =
|
|
45707
|
-
return normC === normR || normC.startsWith(normR +
|
|
46976
|
+
const normC = path22.normalize(post);
|
|
46977
|
+
const normR = path22.normalize(canonicalRoot);
|
|
46978
|
+
return normC === normR || normC.startsWith(normR + path22.sep);
|
|
45708
46979
|
});
|
|
45709
46980
|
if (!stillInside) {
|
|
45710
46981
|
throw toError("E_SANDBOX", "import path points outside the allowed directory", `Keep Markdown files ${describeAllowedFolders()}.`);
|
|
@@ -45712,7 +46983,7 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
|
|
|
45712
46983
|
return buf.toString("utf8");
|
|
45713
46984
|
} finally {
|
|
45714
46985
|
try {
|
|
45715
|
-
if (fd !== void 0)
|
|
46986
|
+
if (fd !== void 0) fs19.closeSync(fd);
|
|
45716
46987
|
} catch {
|
|
45717
46988
|
}
|
|
45718
46989
|
}
|
|
@@ -45728,12 +46999,12 @@ async function handleImportDocument(store, input) {
|
|
|
45728
46999
|
remedy: 'Retry with {"sessionId":"<id from open_document>","path":"<allowed-dir>/file.md"} (optional allowedDir for image refs, optional template report|memo|blank, optional styleGuide).'
|
|
45729
47000
|
};
|
|
45730
47001
|
}
|
|
45731
|
-
const srcExt =
|
|
47002
|
+
const srcExt = path22.extname(parsed.data.path).toLowerCase();
|
|
45732
47003
|
if (srcExt === ".html" || srcExt === ".htm") {
|
|
45733
47004
|
return {
|
|
45734
47005
|
ok: false,
|
|
45735
47006
|
code: "E_VALIDATION",
|
|
45736
|
-
message: `HTML import is not supported directly ("${
|
|
47007
|
+
message: `HTML import is not supported directly ("${path22.basename(parsed.data.path)}")`,
|
|
45737
47008
|
remedy: "ASTra imports Markdown (.md). Convert the HTML file to Markdown or supply a .md document"
|
|
45738
47009
|
};
|
|
45739
47010
|
}
|
|
@@ -45741,7 +47012,7 @@ async function handleImportDocument(store, input) {
|
|
|
45741
47012
|
return {
|
|
45742
47013
|
ok: false,
|
|
45743
47014
|
code: "E_VALIDATION",
|
|
45744
|
-
message: `import_document cannot ingest .docx files ("${
|
|
47015
|
+
message: `import_document cannot ingest .docx files ("${path22.basename(parsed.data.path)}"): open .docx files directly with open_document instead`,
|
|
45745
47016
|
remedy: "Call open_document with the .docx path to work with it, or convert it to Markdown first and import the .md file."
|
|
45746
47017
|
};
|
|
45747
47018
|
}
|
|
@@ -45777,15 +47048,15 @@ async function handleImportDocument(store, input) {
|
|
|
45777
47048
|
}
|
|
45778
47049
|
|
|
45779
47050
|
// src/lib/mcp/schema-dump.ts
|
|
45780
|
-
var
|
|
45781
|
-
var
|
|
47051
|
+
var fs20 = __toESM(require("fs"));
|
|
47052
|
+
var path23 = __toESM(require("path"));
|
|
45782
47053
|
var import_zod9 = require("zod");
|
|
45783
47054
|
|
|
45784
47055
|
// package.json
|
|
45785
47056
|
var package_default = {
|
|
45786
47057
|
name: "@attentionform/astra-mcp-server",
|
|
45787
47058
|
mcpName: "com.attentionform/astra-mcp-server",
|
|
45788
|
-
version: "0.
|
|
47059
|
+
version: "0.5.1",
|
|
45789
47060
|
description: "Official Model Context Protocol (MCP) server for ASTra document IDE",
|
|
45790
47061
|
main: "dist/index.js",
|
|
45791
47062
|
types: "dist/index.d.ts",
|
|
@@ -45826,8 +47097,8 @@ var package_default = {
|
|
|
45826
47097
|
"adm-zip-gate": "node ../../scripts/check-adm-zip-imports.mjs"
|
|
45827
47098
|
},
|
|
45828
47099
|
dependencies: {
|
|
45829
|
-
"@attentionform/astra-engine": "^0.
|
|
45830
|
-
"@attentionform/ooxml-validator": "^0.
|
|
47100
|
+
"@attentionform/astra-engine": "^0.5.1",
|
|
47101
|
+
"@attentionform/ooxml-validator": "^0.5.1",
|
|
45831
47102
|
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
45832
47103
|
"@xmldom/xmldom": "0.8.15",
|
|
45833
47104
|
"adm-zip": "0.6.1",
|
|
@@ -45872,6 +47143,8 @@ function getToolDefinitions() {
|
|
|
45872
47143
|
{ name: "compose_document", description: COMPOSE_DOCUMENT_DESCRIPTION, schema: ComposeDocumentSchema },
|
|
45873
47144
|
{ name: "export_document", description: EXPORT_DOCUMENT_DESCRIPTION, schema: ExportDocumentSchema },
|
|
45874
47145
|
{ name: "visual_inspect", description: VISUAL_INSPECT_DESCRIPTION, schema: VisualInspectSchema },
|
|
47146
|
+
{ name: "export_original_document", description: GIT_HISTORY_DESCRIPTIONS.export_original_document, schema: ExportOriginalDocumentSchema },
|
|
47147
|
+
{ name: "restore_original_document", description: GIT_HISTORY_DESCRIPTIONS.restore_original_document, schema: RestoreOriginalDocumentSchema },
|
|
45875
47148
|
{ name: "get_document_history", description: GIT_HISTORY_DESCRIPTIONS.get_document_history, schema: GetDocumentHistorySchema },
|
|
45876
47149
|
{ name: "preview_revert", description: GIT_HISTORY_DESCRIPTIONS.preview_revert, schema: PreviewRevertSchema },
|
|
45877
47150
|
{ name: "revert_document", description: GIT_HISTORY_DESCRIPTIONS.revert_document, schema: RevertDocumentSchema },
|
|
@@ -45892,8 +47165,8 @@ function compactToolSchema(schema) {
|
|
|
45892
47165
|
return raw;
|
|
45893
47166
|
}
|
|
45894
47167
|
function generateSchemasToDir(targetDir, version = getServerVersion()) {
|
|
45895
|
-
if (!
|
|
45896
|
-
|
|
47168
|
+
if (!fs20.existsSync(targetDir)) {
|
|
47169
|
+
fs20.mkdirSync(targetDir, { recursive: true });
|
|
45897
47170
|
}
|
|
45898
47171
|
const stamp2 = buildStamp(version);
|
|
45899
47172
|
const written = [];
|
|
@@ -45905,8 +47178,8 @@ function generateSchemasToDir(targetDir, version = getServerVersion()) {
|
|
|
45905
47178
|
parameters: raw,
|
|
45906
47179
|
[SCHEMA_STAMP_KEY]: stamp2
|
|
45907
47180
|
};
|
|
45908
|
-
const filePath =
|
|
45909
|
-
|
|
47181
|
+
const filePath = path23.join(targetDir, `${tool.name}.json`);
|
|
47182
|
+
fs20.writeFileSync(filePath, JSON.stringify(payload, null, 2) + "\n", "utf8");
|
|
45910
47183
|
written.push(filePath);
|
|
45911
47184
|
}
|
|
45912
47185
|
return { written, count: written.length };
|
|
@@ -45916,11 +47189,11 @@ function checkSchemasInDir(dir, expectedVersion2 = getServerVersion()) {
|
|
|
45916
47189
|
const mismatches = [];
|
|
45917
47190
|
let checked = 0;
|
|
45918
47191
|
for (const tool of getToolDefinitions()) {
|
|
45919
|
-
const file =
|
|
47192
|
+
const file = path23.join(dir, `${tool.name}.json`);
|
|
45920
47193
|
checked += 1;
|
|
45921
47194
|
let stamped = "<missing>";
|
|
45922
47195
|
try {
|
|
45923
|
-
const raw = JSON.parse(
|
|
47196
|
+
const raw = JSON.parse(fs20.readFileSync(file, "utf8"));
|
|
45924
47197
|
const value = raw[SCHEMA_STAMP_KEY];
|
|
45925
47198
|
stamped = typeof value === "string" ? value : "<unstamped>";
|
|
45926
47199
|
} catch {
|
|
@@ -45933,7 +47206,7 @@ function checkSchemasInDir(dir, expectedVersion2 = getServerVersion()) {
|
|
|
45933
47206
|
return { ok: mismatches.length === 0, mismatches, checked };
|
|
45934
47207
|
}
|
|
45935
47208
|
function formatMismatchWarning(mismatches) {
|
|
45936
|
-
const lines = mismatches.map((m) => ` ${
|
|
47209
|
+
const lines = mismatches.map((m) => ` ${path23.basename(m.file)}: stamped ${m.stamped}, installed ${m.expected}`);
|
|
45937
47210
|
return "Schema cache is stale: regenerated wire schemas differ from the installed server. Refresh file-based client copies with `astra-mcp-server --dump-schemas <dir>` and restart memory-cached clients.\n" + lines.join("\n");
|
|
45938
47211
|
}
|
|
45939
47212
|
|
|
@@ -46000,20 +47273,20 @@ function buildUsageText() {
|
|
|
46000
47273
|
var CLI_USAGE_ERROR = 2;
|
|
46001
47274
|
function initAllowedDirs(argv = process.argv) {
|
|
46002
47275
|
const currentRoots = new Set(
|
|
46003
|
-
(process.env.ASTRA_EXTRA_DOC_ROOTS || "").split(
|
|
47276
|
+
(process.env.ASTRA_EXTRA_DOC_ROOTS || "").split(path24.delimiter).map((p) => p.trim()).filter(Boolean)
|
|
46004
47277
|
);
|
|
46005
47278
|
for (let i = 0; i < argv.length; i++) {
|
|
46006
47279
|
if (argv[i] === "--") break;
|
|
46007
47280
|
if (argv[i] === "--allowed-dir" && argv[i + 1] && !argv[i + 1].startsWith("-")) {
|
|
46008
|
-
currentRoots.add(
|
|
47281
|
+
currentRoots.add(path24.resolve(argv[i + 1]));
|
|
46009
47282
|
i++;
|
|
46010
47283
|
} else if (argv[i].startsWith("--allowed-dir=")) {
|
|
46011
47284
|
const value = argv[i].slice("--allowed-dir=".length).trim();
|
|
46012
|
-
if (value && !value.startsWith("-")) currentRoots.add(
|
|
47285
|
+
if (value && !value.startsWith("-")) currentRoots.add(path24.resolve(value));
|
|
46013
47286
|
}
|
|
46014
47287
|
}
|
|
46015
47288
|
if (currentRoots.size > 0) {
|
|
46016
|
-
process.env.ASTRA_EXTRA_DOC_ROOTS = Array.from(currentRoots).join(
|
|
47289
|
+
process.env.ASTRA_EXTRA_DOC_ROOTS = Array.from(currentRoots).join(path24.delimiter);
|
|
46017
47290
|
}
|
|
46018
47291
|
}
|
|
46019
47292
|
function takeDirFlag(argv, flag) {
|
|
@@ -46079,6 +47352,16 @@ function createAstraMcpServer(store = createMcpSessionStore()) {
|
|
|
46079
47352
|
},
|
|
46080
47353
|
async (args) => toTextResult(await handleVisualInspect(store, args))
|
|
46081
47354
|
);
|
|
47355
|
+
server.registerTool(
|
|
47356
|
+
"export_original_document",
|
|
47357
|
+
{ description: GIT_HISTORY_DESCRIPTIONS.export_original_document, inputSchema: ExportOriginalDocumentSchema },
|
|
47358
|
+
async (args) => toTextResult(await GIT_HISTORY_HANDLERS.export_original_document(store, args))
|
|
47359
|
+
);
|
|
47360
|
+
server.registerTool(
|
|
47361
|
+
"restore_original_document",
|
|
47362
|
+
{ description: GIT_HISTORY_DESCRIPTIONS.restore_original_document, inputSchema: RestoreOriginalDocumentSchema },
|
|
47363
|
+
async (args) => toTextResult(await GIT_HISTORY_HANDLERS.restore_original_document(store, args))
|
|
47364
|
+
);
|
|
46082
47365
|
server.registerTool(
|
|
46083
47366
|
"get_document_history",
|
|
46084
47367
|
{ description: GIT_HISTORY_DESCRIPTIONS.get_document_history, inputSchema: GetDocumentHistorySchema },
|
|
@@ -46206,7 +47489,7 @@ async function main(argv = process.argv) {
|
|
|
46206
47489
|
if (openTimeout !== void 0) setOpenValidationTimeoutMs(Number(openTimeout));
|
|
46207
47490
|
const dumpDir = takeDirFlag(argv, "--dump-schemas");
|
|
46208
47491
|
if (dumpDir !== void 0) {
|
|
46209
|
-
const resolved =
|
|
47492
|
+
const resolved = path24.resolve(dumpDir);
|
|
46210
47493
|
const result = generateSchemasToDir(resolved, SERVER_VERSION);
|
|
46211
47494
|
process.stdout.write(`[${SERVER_NAME} v${SERVER_VERSION}] wrote ${result.count} schemas to ${resolved}
|
|
46212
47495
|
`);
|
|
@@ -46214,7 +47497,7 @@ async function main(argv = process.argv) {
|
|
|
46214
47497
|
}
|
|
46215
47498
|
const checkDir = takeDirFlag(argv, "--check-schemas");
|
|
46216
47499
|
if (checkDir !== void 0) {
|
|
46217
|
-
const resolved =
|
|
47500
|
+
const resolved = path24.resolve(checkDir);
|
|
46218
47501
|
const verdict = checkSchemasInDir(resolved, SERVER_VERSION);
|
|
46219
47502
|
if (verdict.ok) {
|
|
46220
47503
|
process.stdout.write(`[${SERVER_NAME} v${SERVER_VERSION}] schemas fresh (${verdict.checked} tools)
|