@attentionform/astra-mcp-server 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -3
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1405 -338
- package/dist/lib/mcp/handlers/history.d.ts +13 -0
- package/dist/lib/mcp/handlers/open-inspect.d.ts +2 -0
- package/dist/lib/mcp/handlers/original.d.ts +25 -0
- package/dist/lib/mcp/schemas.d.ts +10 -0
- 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");
|
|
@@ -2003,8 +2003,8 @@ function sortChildrenBySchema(element, order) {
|
|
|
2003
2003
|
}
|
|
2004
2004
|
function generateValidParaId(seed, counter = 0) {
|
|
2005
2005
|
if (seed !== void 0 && seed !== null && seed !== "") {
|
|
2006
|
-
const
|
|
2007
|
-
const raw =
|
|
2006
|
+
const hash2 = import_crypto2.default.createHash("sha256").update(`${seed}:${counter}`).digest();
|
|
2007
|
+
const raw = hash2.readUInt32BE(0) & 2147483647;
|
|
2008
2008
|
const val2 = raw === 0 ? 1 : raw;
|
|
2009
2009
|
return val2.toString(16).toUpperCase().padStart(8, "0");
|
|
2010
2010
|
}
|
|
@@ -2905,8 +2905,8 @@ function parserRandomUnit() {
|
|
|
2905
2905
|
if (injectedParserRandom) return injectedParserRandom();
|
|
2906
2906
|
if (resolveReproducibleMode(void 0)) {
|
|
2907
2907
|
parserDeterministicSeq += 1;
|
|
2908
|
-
const
|
|
2909
|
-
return
|
|
2908
|
+
const hash2 = (parserDeterministicSeq * 2654435761 >>> 0) / 4294967296;
|
|
2909
|
+
return hash2 === 0 ? 0.5 : hash2;
|
|
2910
2910
|
}
|
|
2911
2911
|
return Math.random();
|
|
2912
2912
|
}
|
|
@@ -4530,9 +4530,9 @@ function ommlToMathmlDisplay(xml) {
|
|
|
4530
4530
|
// ../engine/src/ooxml/validator-backend.ts
|
|
4531
4531
|
var nativeBackend = {
|
|
4532
4532
|
name: "native",
|
|
4533
|
-
async validateFile(
|
|
4533
|
+
async validateFile(path25, opts) {
|
|
4534
4534
|
const native = await import("@attentionform/ooxml-validator");
|
|
4535
|
-
return native.validateFile(
|
|
4535
|
+
return native.validateFile(path25, opts ? { officeVersion: opts.officeVersion } : void 0);
|
|
4536
4536
|
},
|
|
4537
4537
|
async validateBuffer(buf, opts) {
|
|
4538
4538
|
const native = await import("@attentionform/ooxml-validator");
|
|
@@ -5494,10 +5494,10 @@ function mergeCorePropertiesXml(existingXml, patch, fallback) {
|
|
|
5494
5494
|
return { xml: serializer2.serializeToString(doc), touched };
|
|
5495
5495
|
}
|
|
5496
5496
|
function ensureContentTypeOverride(adapter) {
|
|
5497
|
-
const
|
|
5497
|
+
const path25 = "[Content_Types].xml";
|
|
5498
5498
|
let raw = null;
|
|
5499
5499
|
try {
|
|
5500
|
-
raw = adapter.readTextOrNull(
|
|
5500
|
+
raw = adapter.readTextOrNull(path25);
|
|
5501
5501
|
} catch {
|
|
5502
5502
|
return;
|
|
5503
5503
|
}
|
|
@@ -5514,13 +5514,13 @@ function ensureContentTypeOverride(adapter) {
|
|
|
5514
5514
|
created.setAttribute("PartName", "/docProps/core.xml");
|
|
5515
5515
|
created.setAttribute("ContentType", CORE_PROPERTIES_CONTENT_TYPE);
|
|
5516
5516
|
root.appendChild(created);
|
|
5517
|
-
adapter.writeText(
|
|
5517
|
+
adapter.writeText(path25, new import_xmldom7.XMLSerializer().serializeToString(doc));
|
|
5518
5518
|
}
|
|
5519
5519
|
function ensurePackageRel(adapter) {
|
|
5520
|
-
const
|
|
5520
|
+
const path25 = "_rels/.rels";
|
|
5521
5521
|
let raw = null;
|
|
5522
5522
|
try {
|
|
5523
|
-
raw = adapter.readTextOrNull(
|
|
5523
|
+
raw = adapter.readTextOrNull(path25);
|
|
5524
5524
|
} catch {
|
|
5525
5525
|
return;
|
|
5526
5526
|
}
|
|
@@ -5553,7 +5553,7 @@ function ensurePackageRel(adapter) {
|
|
|
5553
5553
|
created.setAttribute("Type", CORE_PROPERTIES_REL_TYPE);
|
|
5554
5554
|
created.setAttribute("Target", CORE_PROPERTIES_PART);
|
|
5555
5555
|
root.appendChild(created);
|
|
5556
|
-
adapter.writeText(
|
|
5556
|
+
adapter.writeText(path25, new import_xmldom7.XMLSerializer().serializeToString(doc));
|
|
5557
5557
|
}
|
|
5558
5558
|
function writeCorePropertiesPart(adapter, xml) {
|
|
5559
5559
|
adapter.writeText(CORE_PROPERTIES_PART, xml);
|
|
@@ -6082,6 +6082,9 @@ var RSIDS_EMPTY_RE = /<w:rsids>\s*<\/w:rsids>/g;
|
|
|
6082
6082
|
var RSIDS_SELF_CLOSING_RE = /<w:rsids\s*\/>/g;
|
|
6083
6083
|
var AUTHOR_ATTR_RE = /\s+(w|w15):author="[^"]*"/g;
|
|
6084
6084
|
var INITIALS_ATTR_RE = /\s+w:initials="[^"]*"/g;
|
|
6085
|
+
var PRESENCE_INFO_TAG_RE = /<w15:presenceInfo\b(?:[^>"']|"[^"]*"|'[^']*')*>/g;
|
|
6086
|
+
var PRESENCE_PROVIDER_NONE_RE = /\sw15:providerId=(["'])None\1/;
|
|
6087
|
+
var PRESENCE_USERID_RE = /(\sw15:userId=)(?:"[^"]*"|'[^']*')/;
|
|
6085
6088
|
var EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
6086
6089
|
var PHONE_RE = /(\+\d[\d\s\-().]{6,}\d|\(\d{3}\)\s*\d{3}[\s\-.]\d{4}|\b\d{3}[\s\-.]\d{3}[\s\-.]\d{4}\b)/g;
|
|
6087
6090
|
var CLAUSE_ACRONYM_RE = /\b(dfars|far|cfr|nist)\b/gi;
|
|
@@ -6853,6 +6856,15 @@ function sanitizeDocumentXml(xml, opts, partName = "word XML part", hooks) {
|
|
|
6853
6856
|
o.replacementAuthor.split(/[\s._-]+/).filter(Boolean).map((s) => s[0]).join("").slice(0, 4).toUpperCase() || "A"
|
|
6854
6857
|
);
|
|
6855
6858
|
out = out.replace(INITIALS_ATTR_RE, ` w:initials="${initials}"`);
|
|
6859
|
+
if (partName === "word/people.xml") {
|
|
6860
|
+
out = out.replace(PRESENCE_INFO_TAG_RE, (tag) => {
|
|
6861
|
+
if (!PRESENCE_PROVIDER_NONE_RE.test(tag)) return tag;
|
|
6862
|
+
return tag.replace(PRESENCE_USERID_RE, (_m, open) => {
|
|
6863
|
+
purgedAuthors += 1;
|
|
6864
|
+
return `${open}"${replacement === "" ? "Anonymous" : replacement}"`;
|
|
6865
|
+
});
|
|
6866
|
+
});
|
|
6867
|
+
}
|
|
6856
6868
|
}
|
|
6857
6869
|
let acceptedRevisions = 0;
|
|
6858
6870
|
let unresolvedRevisions = [];
|
|
@@ -7404,20 +7416,20 @@ function sanitizeEmbeddedWorkbookBytes(buf, outerName) {
|
|
|
7404
7416
|
}
|
|
7405
7417
|
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;
|
|
7406
7418
|
function bufferZipAdapter(files, touched) {
|
|
7407
|
-
const write = (
|
|
7408
|
-
files.set(
|
|
7409
|
-
touched?.add(
|
|
7419
|
+
const write = (path25, content) => {
|
|
7420
|
+
files.set(path25, content);
|
|
7421
|
+
touched?.add(path25);
|
|
7410
7422
|
};
|
|
7411
7423
|
return {
|
|
7412
|
-
hasFile: (
|
|
7413
|
-
readText: (
|
|
7414
|
-
const data = files.get(
|
|
7415
|
-
if (!data) throw new Error(`E_VALIDATION: Part ${
|
|
7424
|
+
hasFile: (path25) => files.has(path25),
|
|
7425
|
+
readText: (path25) => {
|
|
7426
|
+
const data = files.get(path25);
|
|
7427
|
+
if (!data) throw new Error(`E_VALIDATION: Part ${path25} was not found.`);
|
|
7416
7428
|
return data.toString("utf8");
|
|
7417
7429
|
},
|
|
7418
|
-
readTextOrNull: (
|
|
7419
|
-
writeText: (
|
|
7420
|
-
writeBuffer: (
|
|
7430
|
+
readTextOrNull: (path25) => files.get(path25)?.toString("utf8") ?? null,
|
|
7431
|
+
writeText: (path25, content) => write(path25, Buffer.from(content, "utf8")),
|
|
7432
|
+
writeBuffer: (path25, content) => write(path25, Buffer.from(content))
|
|
7421
7433
|
};
|
|
7422
7434
|
}
|
|
7423
7435
|
function deleteRemovedComments(files, ids, touched) {
|
|
@@ -10402,7 +10414,7 @@ var DocxParser = class _DocxParser {
|
|
|
10402
10414
|
* Captures a full package snapshot: a clone of every zip entry buffer.
|
|
10403
10415
|
* Used by OperationEngine for transactional rollback of failed batches.
|
|
10404
10416
|
*/
|
|
10405
|
-
capturePackageSnapshot() {
|
|
10417
|
+
capturePackageSnapshot(options) {
|
|
10406
10418
|
this.purgeRogueZipEntries();
|
|
10407
10419
|
const snapshot3 = /* @__PURE__ */ new Map();
|
|
10408
10420
|
for (const entry of this.zip.getEntries()) {
|
|
@@ -10413,7 +10425,7 @@ var DocxParser = class _DocxParser {
|
|
|
10413
10425
|
snapshot3.set(entry.entryName, Buffer.from(entry.getData()));
|
|
10414
10426
|
}
|
|
10415
10427
|
try {
|
|
10416
|
-
snapshot3.set("word/document.xml", Buffer.from(this.readDocumentXml(), "utf8"));
|
|
10428
|
+
snapshot3.set("word/document.xml", Buffer.from(options?.documentXml ?? this.readDocumentXml(), "utf8"));
|
|
10417
10429
|
} catch {
|
|
10418
10430
|
}
|
|
10419
10431
|
return snapshot3;
|
|
@@ -10480,9 +10492,10 @@ var DocxParser = class _DocxParser {
|
|
|
10480
10492
|
/**
|
|
10481
10493
|
* Restores the package to a previously captured snapshot: every entry is
|
|
10482
10494
|
* restored (entries added after the snapshot are removed), then documentDom
|
|
10483
|
-
* and stylesDom are reloaded and stable IDs re-assigned.
|
|
10495
|
+
* and stylesDom are reloaded and stable IDs re-assigned. The restored package is exactly the snapshot;
|
|
10496
|
+
* only a restore that mixes a pre-0.4.2 commit with live parts passes `registerCoreProperties`.
|
|
10484
10497
|
*/
|
|
10485
|
-
restorePackageSnapshot(snapshot3) {
|
|
10498
|
+
restorePackageSnapshot(snapshot3, options) {
|
|
10486
10499
|
for (const entry of [...this.zip.getEntries()]) {
|
|
10487
10500
|
if (!snapshot3.has(entry.entryName)) {
|
|
10488
10501
|
this.zip.deleteFile(entry);
|
|
@@ -10501,8 +10514,15 @@ var DocxParser = class _DocxParser {
|
|
|
10501
10514
|
continue;
|
|
10502
10515
|
}
|
|
10503
10516
|
const buffer = Buffer.from(data);
|
|
10504
|
-
|
|
10505
|
-
|
|
10517
|
+
const liveEntry = this.zip.getEntry(entryName);
|
|
10518
|
+
if (liveEntry) {
|
|
10519
|
+
let unchanged = false;
|
|
10520
|
+
try {
|
|
10521
|
+
unchanged = !liveEntry.isDirectory && liveEntry.header.size === buffer.length && readPartData(liveEntry, entryName).equals(buffer);
|
|
10522
|
+
} catch {
|
|
10523
|
+
unchanged = false;
|
|
10524
|
+
}
|
|
10525
|
+
if (!unchanged) this.zip.updateFile(entryName, buffer);
|
|
10506
10526
|
} else {
|
|
10507
10527
|
this.zip.addFile(entryName, buffer);
|
|
10508
10528
|
}
|
|
@@ -10527,7 +10547,7 @@ var DocxParser = class _DocxParser {
|
|
|
10527
10547
|
} else {
|
|
10528
10548
|
this.stylesDom = null;
|
|
10529
10549
|
}
|
|
10530
|
-
registerExistingCorePropertiesPart(this.getZipAdapter());
|
|
10550
|
+
if (options?.registerCoreProperties) registerExistingCorePropertiesPart(this.getZipAdapter());
|
|
10531
10551
|
this.invalidateMediaCache();
|
|
10532
10552
|
this.assignStableIds();
|
|
10533
10553
|
this._bumpNodePagesRevision();
|
|
@@ -11048,20 +11068,20 @@ var DocxParser = class _DocxParser {
|
|
|
11048
11068
|
}
|
|
11049
11069
|
getZipAdapter() {
|
|
11050
11070
|
return {
|
|
11051
|
-
hasFile: (
|
|
11052
|
-
readText: (
|
|
11053
|
-
const cleanPath = this.sanitizePath(
|
|
11071
|
+
hasFile: (path25) => !!this.zip.getEntry(this.sanitizePath(path25)),
|
|
11072
|
+
readText: (path25) => {
|
|
11073
|
+
const cleanPath = this.sanitizePath(path25);
|
|
11054
11074
|
const entry = this.zip.getEntry(cleanPath);
|
|
11055
11075
|
if (!entry) throw new Error(`File not found: ${cleanPath}`);
|
|
11056
11076
|
return entry.getData().toString("utf8");
|
|
11057
11077
|
},
|
|
11058
|
-
readTextOrNull: (
|
|
11059
|
-
const cleanPath = this.sanitizePath(
|
|
11078
|
+
readTextOrNull: (path25) => {
|
|
11079
|
+
const cleanPath = this.sanitizePath(path25);
|
|
11060
11080
|
const entry = this.zip.getEntry(cleanPath);
|
|
11061
11081
|
return entry ? entry.getData().toString("utf8") : null;
|
|
11062
11082
|
},
|
|
11063
|
-
writeText: (
|
|
11064
|
-
const cleanPath = this.sanitizePath(
|
|
11083
|
+
writeText: (path25, content) => {
|
|
11084
|
+
const cleanPath = this.sanitizePath(path25);
|
|
11065
11085
|
assertNoXxe(content, cleanPath);
|
|
11066
11086
|
assertNoXxeAdjacent(content, cleanPath);
|
|
11067
11087
|
if (this.zip.getEntry(cleanPath)) {
|
|
@@ -11070,8 +11090,8 @@ var DocxParser = class _DocxParser {
|
|
|
11070
11090
|
this.zip.addFile(cleanPath, Buffer.from(content, "utf8"));
|
|
11071
11091
|
}
|
|
11072
11092
|
},
|
|
11073
|
-
writeBuffer: (
|
|
11074
|
-
const cleanPath = this.sanitizePath(
|
|
11093
|
+
writeBuffer: (path25, content) => {
|
|
11094
|
+
const cleanPath = this.sanitizePath(path25);
|
|
11075
11095
|
assertNoXxe(content.toString("utf8"), cleanPath);
|
|
11076
11096
|
assertNoXxeAdjacent(content.toString("utf8"), cleanPath);
|
|
11077
11097
|
if (this.zip.getEntry(cleanPath)) {
|
|
@@ -12895,12 +12915,12 @@ ${validation.errors.join("\n")}`, "repair the part XML or re-save the document i
|
|
|
12895
12915
|
if (!comments || /<w:comment[\s>]/.test(comments.getData().toString("utf8"))) return;
|
|
12896
12916
|
const parts = ["comments", "commentsExtended", "commentsIds", "commentsExtensible"].map((n) => `${n}.xml`);
|
|
12897
12917
|
for (const part of parts) if (this.zip.getEntry(`word/${part}`)) this.zip.deleteFile(`word/${part}`);
|
|
12898
|
-
const edit = (
|
|
12899
|
-
const entry = this.zip.getEntry(
|
|
12918
|
+
const edit = (path25, tag, attr, drop) => {
|
|
12919
|
+
const entry = this.zip.getEntry(path25);
|
|
12900
12920
|
if (!entry) return;
|
|
12901
12921
|
const dom = this.parser.parseFromString(entry.getData().toString("utf8"), "text/xml");
|
|
12902
12922
|
for (const el of Array.from(dom.getElementsByTagName(tag))) if (drop(el.getAttribute(attr) ?? "")) el.parentNode?.removeChild(el);
|
|
12903
|
-
this.zip.updateFile(
|
|
12923
|
+
this.zip.updateFile(path25, Buffer.from(this.serializer.serializeToString(dom), "utf8"));
|
|
12904
12924
|
};
|
|
12905
12925
|
edit("word/_rels/document.xml.rels", "Relationship", "Target", (value) => parts.includes(value.replace(/^\/?(word\/)?/, "")));
|
|
12906
12926
|
edit("[Content_Types].xml", "Override", "PartName", (value) => parts.map((p) => `/word/${p}`).includes(value));
|
|
@@ -13638,12 +13658,11 @@ ${validation.errors.join("\n")}`, "repair the part XML or re-save the document i
|
|
|
13638
13658
|
}
|
|
13639
13659
|
};
|
|
13640
13660
|
|
|
13641
|
-
// ../engine/src/
|
|
13642
|
-
var
|
|
13643
|
-
var import_promises2 = __toESM(require("fs/promises"));
|
|
13644
|
-
var
|
|
13645
|
-
var
|
|
13646
|
-
var import_isomorphic_git = __toESM(require("isomorphic-git"));
|
|
13661
|
+
// ../engine/src/original.ts
|
|
13662
|
+
var import_node_fs = __toESM(require("node:fs"));
|
|
13663
|
+
var import_promises2 = __toESM(require("node:fs/promises"));
|
|
13664
|
+
var import_node_path = __toESM(require("node:path"));
|
|
13665
|
+
var import_node_crypto = require("node:crypto");
|
|
13647
13666
|
|
|
13648
13667
|
// ../engine/src/sandbox/paths.ts
|
|
13649
13668
|
var import_fs3 = __toESM(require("fs"));
|
|
@@ -13785,7 +13804,408 @@ function writeFileNoFollowSync(target, data, encoding = "utf8") {
|
|
|
13785
13804
|
assertWriteTargetNoSymlink(import_path2.default.resolve(target));
|
|
13786
13805
|
}
|
|
13787
13806
|
|
|
13807
|
+
// ../engine/src/original.ts
|
|
13808
|
+
var SNAPSHOT_NAME = "original.snapshot";
|
|
13809
|
+
var SNAPSHOT_MAGIC = Buffer.from("ASTRA-ORIGINAL\n");
|
|
13810
|
+
var MAX_SNAPSHOT_HEADER = 4096;
|
|
13811
|
+
function originalSha256(bytes) {
|
|
13812
|
+
return (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
|
|
13813
|
+
}
|
|
13814
|
+
function coded(message, code = "E_IO") {
|
|
13815
|
+
return Object.assign(new Error(message), { code });
|
|
13816
|
+
}
|
|
13817
|
+
function errno(e) {
|
|
13818
|
+
return typeof e === "object" && e !== null && "code" in e ? String(e.code) : "";
|
|
13819
|
+
}
|
|
13820
|
+
function originalDir(docPath) {
|
|
13821
|
+
return import_node_path.default.join(getRepoDir(docPath), ".git", "astra-original");
|
|
13822
|
+
}
|
|
13823
|
+
async function readSafe(file, maxBytes = resolveHistoryMaxSidecarBytes()) {
|
|
13824
|
+
assertWriteTargetNoSymlink(file);
|
|
13825
|
+
const h = await import_promises2.default.open(file, import_node_fs.default.constants.O_RDONLY | import_node_fs.default.constants.O_NOFOLLOW);
|
|
13826
|
+
try {
|
|
13827
|
+
const stat3 = await h.stat();
|
|
13828
|
+
if (!stat3.isFile() || stat3.size > maxBytes) throw coded("Original recovery refused: expected a regular file within the size limit.");
|
|
13829
|
+
return await h.readFile();
|
|
13830
|
+
} finally {
|
|
13831
|
+
await h.close();
|
|
13832
|
+
}
|
|
13833
|
+
}
|
|
13834
|
+
async function exists(file) {
|
|
13835
|
+
try {
|
|
13836
|
+
await import_promises2.default.lstat(file);
|
|
13837
|
+
return true;
|
|
13838
|
+
} catch (e) {
|
|
13839
|
+
if (errno(e) === "ENOENT") return false;
|
|
13840
|
+
throw e;
|
|
13841
|
+
}
|
|
13842
|
+
}
|
|
13843
|
+
async function syncDirectory(dir) {
|
|
13844
|
+
if (process.platform === "win32") return false;
|
|
13845
|
+
let h;
|
|
13846
|
+
let synced = false;
|
|
13847
|
+
try {
|
|
13848
|
+
h = await import_promises2.default.open(dir, import_node_fs.default.constants.O_RDONLY);
|
|
13849
|
+
await h.sync();
|
|
13850
|
+
synced = true;
|
|
13851
|
+
} catch {
|
|
13852
|
+
}
|
|
13853
|
+
try {
|
|
13854
|
+
await h?.close();
|
|
13855
|
+
} catch {
|
|
13856
|
+
synced = false;
|
|
13857
|
+
}
|
|
13858
|
+
return synced;
|
|
13859
|
+
}
|
|
13860
|
+
async function cleanupTemp(file) {
|
|
13861
|
+
try {
|
|
13862
|
+
await import_promises2.default.unlink(file);
|
|
13863
|
+
} catch (e) {
|
|
13864
|
+
if (errno(e) !== "ENOENT") throw Object.assign(coded(`Temporary file cleanup failed: ${file}. ${e instanceof Error ? e.message : String(e)}`), { tempPath: file });
|
|
13865
|
+
}
|
|
13866
|
+
}
|
|
13867
|
+
async function preparedTempParts(target, parts, mode = 384) {
|
|
13868
|
+
assertWriteTargetNoSymlink(target);
|
|
13869
|
+
const dir = import_node_path.default.dirname(target);
|
|
13870
|
+
await import_promises2.default.mkdir(dir, { recursive: true, mode: 448 });
|
|
13871
|
+
assertWriteTargetNoSymlink(target);
|
|
13872
|
+
const tmp = import_node_path.default.join(dir, `.${import_node_path.default.basename(target).slice(0, 60)}.astra-tmp-${(0, import_node_crypto.randomUUID)()}`);
|
|
13873
|
+
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);
|
|
13874
|
+
let writeError;
|
|
13875
|
+
try {
|
|
13876
|
+
for (const part of parts) {
|
|
13877
|
+
let offset = 0;
|
|
13878
|
+
while (offset < part.length) {
|
|
13879
|
+
const { bytesWritten } = await h.write(part, offset, part.length - offset, null);
|
|
13880
|
+
if (bytesWritten === 0) throw coded("Original recovery refused: temporary file write made no progress.");
|
|
13881
|
+
offset += bytesWritten;
|
|
13882
|
+
}
|
|
13883
|
+
}
|
|
13884
|
+
await h.sync();
|
|
13885
|
+
} catch (error) {
|
|
13886
|
+
writeError = error;
|
|
13887
|
+
}
|
|
13888
|
+
let closeError;
|
|
13889
|
+
try {
|
|
13890
|
+
await h.close();
|
|
13891
|
+
} catch (error) {
|
|
13892
|
+
closeError = error;
|
|
13893
|
+
}
|
|
13894
|
+
if (writeError || closeError) {
|
|
13895
|
+
const primary = writeError ?? closeError;
|
|
13896
|
+
let message = primary instanceof Error ? primary.message : String(primary);
|
|
13897
|
+
if (writeError && closeError) message += `; temporary file close also failed: ${closeError instanceof Error ? closeError.message : String(closeError)}`;
|
|
13898
|
+
let cleanupFailed = false;
|
|
13899
|
+
try {
|
|
13900
|
+
await cleanupTemp(tmp);
|
|
13901
|
+
} catch (cleanup) {
|
|
13902
|
+
cleanupFailed = true;
|
|
13903
|
+
message += `; ${cleanup instanceof Error ? cleanup.message : String(cleanup)}`;
|
|
13904
|
+
}
|
|
13905
|
+
throw Object.assign(coded(message, errno(primary) || "E_IO"), { ...cleanupFailed ? { tempPath: tmp } : {}, cause: primary });
|
|
13906
|
+
}
|
|
13907
|
+
try {
|
|
13908
|
+
const expectedBytes = parts.reduce((total, part) => total + part.length, 0);
|
|
13909
|
+
const expectedHash = (0, import_node_crypto.createHash)("sha256");
|
|
13910
|
+
for (const part of parts) expectedHash.update(part);
|
|
13911
|
+
const check = await import_promises2.default.open(tmp, import_node_fs.default.constants.O_RDONLY | import_node_fs.default.constants.O_NOFOLLOW);
|
|
13912
|
+
try {
|
|
13913
|
+
const stat3 = await check.stat();
|
|
13914
|
+
if (!stat3.isFile() || stat3.size !== expectedBytes) throw coded("Original recovery refused: temporary file integrity check failed.");
|
|
13915
|
+
const actualHash = (0, import_node_crypto.createHash)("sha256");
|
|
13916
|
+
const chunk = Buffer.allocUnsafe(64 * 1024);
|
|
13917
|
+
let position2 = 0;
|
|
13918
|
+
while (position2 < expectedBytes) {
|
|
13919
|
+
const { bytesRead } = await check.read(chunk, 0, Math.min(chunk.length, expectedBytes - position2), position2);
|
|
13920
|
+
if (bytesRead === 0) throw coded("Original recovery refused: temporary file integrity check failed.");
|
|
13921
|
+
actualHash.update(chunk.subarray(0, bytesRead));
|
|
13922
|
+
position2 += bytesRead;
|
|
13923
|
+
}
|
|
13924
|
+
if (actualHash.digest("hex") !== expectedHash.digest("hex")) throw coded("Original recovery refused: temporary file integrity check failed.");
|
|
13925
|
+
} finally {
|
|
13926
|
+
await check.close();
|
|
13927
|
+
}
|
|
13928
|
+
} catch (e) {
|
|
13929
|
+
try {
|
|
13930
|
+
await cleanupTemp(tmp);
|
|
13931
|
+
} catch (cleanup) {
|
|
13932
|
+
throw coded(`${e instanceof Error ? e.message : String(e)}; ${cleanup instanceof Error ? cleanup.message : String(cleanup)}`);
|
|
13933
|
+
}
|
|
13934
|
+
throw e;
|
|
13935
|
+
}
|
|
13936
|
+
return tmp;
|
|
13937
|
+
}
|
|
13938
|
+
async function preparedTemp(target, data, mode = 384) {
|
|
13939
|
+
return preparedTempParts(target, [data], mode);
|
|
13940
|
+
}
|
|
13941
|
+
async function writePartsExclusive(target, parts, mode = 384) {
|
|
13942
|
+
const tmp = await preparedTempParts(target, parts, mode);
|
|
13943
|
+
let failure;
|
|
13944
|
+
try {
|
|
13945
|
+
assertWriteTargetNoSymlink(target);
|
|
13946
|
+
try {
|
|
13947
|
+
await import_promises2.default.link(tmp, target);
|
|
13948
|
+
} catch (e) {
|
|
13949
|
+
if (errno(e) === "EEXIST") throw coded(`Destination exists: ${target}`, "E_CONFLICT");
|
|
13950
|
+
if (["ENOTSUP", "EOPNOTSUPP", "EPERM", "EXDEV", "ENOSYS"].includes(errno(e))) {
|
|
13951
|
+
throw coded("This location does not support safe exclusive publication; export to another directory.");
|
|
13952
|
+
}
|
|
13953
|
+
throw e;
|
|
13954
|
+
}
|
|
13955
|
+
await syncDirectory(import_node_path.default.dirname(target));
|
|
13956
|
+
} catch (error) {
|
|
13957
|
+
failure = error;
|
|
13958
|
+
}
|
|
13959
|
+
try {
|
|
13960
|
+
await cleanupTemp(tmp);
|
|
13961
|
+
} catch (cleanup) {
|
|
13962
|
+
const detail = cleanup instanceof Error ? cleanup.message : String(cleanup);
|
|
13963
|
+
if (failure) {
|
|
13964
|
+
throw Object.assign(coded(`${failure instanceof Error ? failure.message : String(failure)}; ${detail}`, errno(failure) || "E_IO"), { tempPath: tmp });
|
|
13965
|
+
}
|
|
13966
|
+
throw Object.assign(coded(`Destination was published at ${target}, but ${detail}`), { tempPath: tmp, published: true });
|
|
13967
|
+
}
|
|
13968
|
+
if (failure) throw failure;
|
|
13969
|
+
}
|
|
13970
|
+
async function writeFileExclusive(target, data, mode = 384) {
|
|
13971
|
+
return writePartsExclusive(target, [data], mode);
|
|
13972
|
+
}
|
|
13973
|
+
async function createVerifiedBackup(target, bytes) {
|
|
13974
|
+
let ownsTarget = false;
|
|
13975
|
+
try {
|
|
13976
|
+
await writeFileExclusive(target, bytes);
|
|
13977
|
+
ownsTarget = true;
|
|
13978
|
+
assertWriteTargetNoSymlink(target);
|
|
13979
|
+
const written = await import_promises2.default.readFile(target);
|
|
13980
|
+
if (written.length !== bytes.length || originalSha256(written) !== originalSha256(bytes)) {
|
|
13981
|
+
throw coded(`Restore backup verification failed: ${target}`, "E_IO");
|
|
13982
|
+
}
|
|
13983
|
+
} catch (error) {
|
|
13984
|
+
if (error?.published === true) ownsTarget = true;
|
|
13985
|
+
if (ownsTarget) {
|
|
13986
|
+
try {
|
|
13987
|
+
await import_promises2.default.unlink(target);
|
|
13988
|
+
} catch (cleanupError) {
|
|
13989
|
+
if (errno(cleanupError) !== "ENOENT") {
|
|
13990
|
+
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 });
|
|
13991
|
+
}
|
|
13992
|
+
}
|
|
13993
|
+
}
|
|
13994
|
+
throw error;
|
|
13995
|
+
}
|
|
13996
|
+
}
|
|
13997
|
+
function newRestoreBackupPath(target) {
|
|
13998
|
+
return import_node_path.default.join(import_node_path.default.dirname(target), `.astra-restore-backup-${(0, import_node_crypto.randomUUID)()}`);
|
|
13999
|
+
}
|
|
14000
|
+
async function replaceFileAtomic(target, data, newFileMode = 384) {
|
|
14001
|
+
let tmp;
|
|
14002
|
+
let replaced = false;
|
|
14003
|
+
let renameStarted = false;
|
|
14004
|
+
try {
|
|
14005
|
+
assertWriteTargetNoSymlink(target);
|
|
14006
|
+
let mode = newFileMode;
|
|
14007
|
+
try {
|
|
14008
|
+
const st = await import_promises2.default.lstat(target);
|
|
14009
|
+
if (!st.isFile()) throw coded("Atomic replace requires a regular target file.");
|
|
14010
|
+
mode = st.mode & 511;
|
|
14011
|
+
if ((mode & 146) === 0) throw coded("Atomic replace refused: target is read-only.");
|
|
14012
|
+
await import_promises2.default.access(target, import_node_fs.default.constants.W_OK);
|
|
14013
|
+
} catch (e) {
|
|
14014
|
+
if (errno(e) !== "ENOENT") throw e;
|
|
14015
|
+
}
|
|
14016
|
+
tmp = await preparedTemp(target, data, mode);
|
|
14017
|
+
assertWriteTargetNoSymlink(target);
|
|
14018
|
+
renameStarted = true;
|
|
14019
|
+
for (let attempt = 0; ; attempt++) {
|
|
14020
|
+
try {
|
|
14021
|
+
await import_promises2.default.rename(tmp, target);
|
|
14022
|
+
replaced = true;
|
|
14023
|
+
break;
|
|
14024
|
+
} catch (e) {
|
|
14025
|
+
if (process.platform !== "win32" || !["EBUSY", "EPERM", "EACCES"].includes(errno(e)) || attempt >= 3) throw e;
|
|
14026
|
+
await new Promise((resolve10) => setTimeout(resolve10, 25 * (attempt + 1)));
|
|
14027
|
+
}
|
|
14028
|
+
}
|
|
14029
|
+
const durable = await syncDirectory(import_node_path.default.dirname(target));
|
|
14030
|
+
await cleanupTemp(tmp);
|
|
14031
|
+
tmp = void 0;
|
|
14032
|
+
return { outcome: "replaced", ...!durable ? { durabilityUnconfirmed: true } : {} };
|
|
14033
|
+
} catch (e) {
|
|
14034
|
+
let message = e instanceof Error ? e.message : String(e);
|
|
14035
|
+
let outcome = replaced ? "replaced" : renameStarted ? "uncertain" : "not-replaced";
|
|
14036
|
+
if (!replaced && renameStarted && tmp) {
|
|
14037
|
+
try {
|
|
14038
|
+
const tempStat = await import_promises2.default.lstat(tmp);
|
|
14039
|
+
if (tempStat.isFile() && !tempStat.isSymbolicLink()) outcome = "not-replaced";
|
|
14040
|
+
} catch (tempError) {
|
|
14041
|
+
if (errno(tempError) === "ENOENT") {
|
|
14042
|
+
try {
|
|
14043
|
+
assertWriteTargetNoSymlink(target);
|
|
14044
|
+
const targetStat = await import_promises2.default.lstat(target);
|
|
14045
|
+
if (targetStat.isFile() && targetStat.size === data.length && (await import_promises2.default.readFile(target)).equals(data)) {
|
|
14046
|
+
replaced = true;
|
|
14047
|
+
outcome = "replaced";
|
|
14048
|
+
}
|
|
14049
|
+
} catch {
|
|
14050
|
+
}
|
|
14051
|
+
}
|
|
14052
|
+
}
|
|
14053
|
+
}
|
|
14054
|
+
if (tmp) {
|
|
14055
|
+
try {
|
|
14056
|
+
await cleanupTemp(tmp);
|
|
14057
|
+
} catch (cleanup) {
|
|
14058
|
+
message += `; ${cleanup instanceof Error ? cleanup.message : String(cleanup)}`;
|
|
14059
|
+
}
|
|
14060
|
+
}
|
|
14061
|
+
throw Object.assign(coded(message, errno(e) || "E_IO"), {
|
|
14062
|
+
outcome
|
|
14063
|
+
});
|
|
14064
|
+
}
|
|
14065
|
+
}
|
|
14066
|
+
function encodeSnapshot(bytes, sourceName) {
|
|
14067
|
+
const meta = { magic: "ASTRA-ORIGINAL", version: 1, sha256: originalSha256(bytes), bytes: bytes.length, capturedAt: (/* @__PURE__ */ new Date()).toISOString(), sourceName };
|
|
14068
|
+
const json = Buffer.from(JSON.stringify(meta));
|
|
14069
|
+
if (json.length > MAX_SNAPSHOT_HEADER) throw coded("Original snapshot metadata exceeds its format limit.", "E_LIMIT");
|
|
14070
|
+
const prefix = Buffer.alloc(SNAPSHOT_MAGIC.length + 4);
|
|
14071
|
+
SNAPSHOT_MAGIC.copy(prefix);
|
|
14072
|
+
prefix.writeUInt32BE(json.length, SNAPSHOT_MAGIC.length);
|
|
14073
|
+
return [prefix, json, bytes];
|
|
14074
|
+
}
|
|
14075
|
+
function decodeSnapshot(all) {
|
|
14076
|
+
const offset = SNAPSHOT_MAGIC.length;
|
|
14077
|
+
if (all.length < offset + 4 || !all.subarray(0, offset).equals(SNAPSHOT_MAGIC)) throw coded("Original snapshot has an invalid format header.", "E_VALIDATION");
|
|
14078
|
+
const headerBytes = all.readUInt32BE(offset);
|
|
14079
|
+
if (headerBytes < 2 || headerBytes > MAX_SNAPSHOT_HEADER || all.length < offset + 4 + headerBytes) throw coded("Original snapshot has an invalid metadata length.", "E_VALIDATION");
|
|
14080
|
+
let meta;
|
|
14081
|
+
try {
|
|
14082
|
+
meta = JSON.parse(all.subarray(offset + 4, offset + 4 + headerBytes).toString("utf8"));
|
|
14083
|
+
} catch {
|
|
14084
|
+
throw coded("Original snapshot metadata is not valid JSON.", "E_VALIDATION");
|
|
14085
|
+
}
|
|
14086
|
+
const bytes = all.subarray(offset + 4 + headerBytes);
|
|
14087
|
+
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") {
|
|
14088
|
+
throw coded("Original snapshot metadata is invalid.", "E_VALIDATION");
|
|
14089
|
+
}
|
|
14090
|
+
if (originalSha256(bytes) !== meta.sha256) throw coded("Original snapshot integrity verification failed.", "E_VALIDATION");
|
|
14091
|
+
return { meta, bytes };
|
|
14092
|
+
}
|
|
14093
|
+
async function verifiedSnapshot(docPath) {
|
|
14094
|
+
const file = import_node_path.default.join(originalDir(docPath), SNAPSHOT_NAME);
|
|
14095
|
+
const raw = await readSafe(file);
|
|
14096
|
+
return decodeSnapshot(raw);
|
|
14097
|
+
}
|
|
14098
|
+
async function getOriginalInfo(docPath) {
|
|
14099
|
+
if (!import_node_path.default.isAbsolute(docPath)) return { available: false, reason: "not-applicable" };
|
|
14100
|
+
const dir = originalDir(docPath);
|
|
14101
|
+
try {
|
|
14102
|
+
if (!await exists(dir)) return { available: false, reason: "predates-feature" };
|
|
14103
|
+
const snapshot3 = import_node_path.default.join(dir, SNAPSHOT_NAME);
|
|
14104
|
+
assertWriteTargetNoSymlink(snapshot3);
|
|
14105
|
+
if (await exists(snapshot3)) {
|
|
14106
|
+
const { meta } = await verifiedSnapshot(docPath);
|
|
14107
|
+
return { available: true, sha256: meta.sha256, bytes: meta.bytes, capturedAt: meta.capturedAt };
|
|
14108
|
+
}
|
|
14109
|
+
try {
|
|
14110
|
+
const status = JSON.parse((await readSafe(import_node_path.default.join(dir, "status.json"), 16 * 1024)).toString("utf8"));
|
|
14111
|
+
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) } : {} };
|
|
14112
|
+
} catch {
|
|
14113
|
+
}
|
|
14114
|
+
return { available: false, reason: "incomplete" };
|
|
14115
|
+
} catch (e) {
|
|
14116
|
+
return { available: false, reason: "corrupt", detail: e instanceof Error ? e.message : String(e) };
|
|
14117
|
+
}
|
|
14118
|
+
}
|
|
14119
|
+
async function captureOriginal(docPath, bytes) {
|
|
14120
|
+
const dir = originalDir(docPath);
|
|
14121
|
+
let result;
|
|
14122
|
+
try {
|
|
14123
|
+
assertWriteTargetNoSymlink(import_node_path.default.join(dir, SNAPSHOT_NAME));
|
|
14124
|
+
await import_promises2.default.mkdir(dir, { recursive: true, mode: 448 });
|
|
14125
|
+
assertWriteTargetNoSymlink(import_node_path.default.join(dir, SNAPSHOT_NAME));
|
|
14126
|
+
if (await exists(import_node_path.default.join(dir, SNAPSHOT_NAME))) {
|
|
14127
|
+
const info = await getOriginalInfo(docPath);
|
|
14128
|
+
if (info.available && bytes && originalSha256(bytes) !== info.sha256) return { available: false, reason: "capture-failed", detail: "An original is already recorded for this history." };
|
|
14129
|
+
return info;
|
|
14130
|
+
}
|
|
14131
|
+
if (!bytes) result = { available: false, reason: "not-applicable" };
|
|
14132
|
+
else if (!Buffer.isBuffer(bytes) || bytes.length === 0) result = { available: false, reason: "capture-failed", detail: "Original input is empty." };
|
|
14133
|
+
else if (await getSidecarBytes(getRepoDir(docPath)) + bytes.length + 4096 + SNAPSHOT_MAGIC.length > resolveHistoryMaxSidecarBytes()) result = { available: false, reason: "too-large" };
|
|
14134
|
+
else {
|
|
14135
|
+
const digest = originalSha256(bytes);
|
|
14136
|
+
const snapshot3 = import_node_path.default.join(dir, SNAPSHOT_NAME);
|
|
14137
|
+
try {
|
|
14138
|
+
await writePartsExclusive(snapshot3, encodeSnapshot(bytes, import_node_path.default.basename(docPath)));
|
|
14139
|
+
} catch (e) {
|
|
14140
|
+
if (e?.published === true) {
|
|
14141
|
+
const published = await getOriginalInfo(docPath);
|
|
14142
|
+
if (published.available && published.sha256 === digest) return published;
|
|
14143
|
+
throw e;
|
|
14144
|
+
}
|
|
14145
|
+
if (errno(e) !== "E_CONFLICT") throw e;
|
|
14146
|
+
const winner = await verifiedSnapshot(docPath);
|
|
14147
|
+
if (!winner.bytes.equals(bytes)) return { available: false, reason: "capture-failed", detail: "An original is already recorded for this history." };
|
|
14148
|
+
}
|
|
14149
|
+
const info = await getOriginalInfo(docPath);
|
|
14150
|
+
if (info.available && info.sha256 !== digest) return { available: false, reason: "capture-failed", detail: "An original is already recorded for this history." };
|
|
14151
|
+
return info;
|
|
14152
|
+
}
|
|
14153
|
+
} catch (e) {
|
|
14154
|
+
result = { available: false, reason: "capture-failed", detail: e instanceof Error ? e.message : String(e) };
|
|
14155
|
+
}
|
|
14156
|
+
try {
|
|
14157
|
+
await writeFileExclusive(import_node_path.default.join(dir, "status.json"), Buffer.from(JSON.stringify(result)));
|
|
14158
|
+
} catch {
|
|
14159
|
+
}
|
|
14160
|
+
return result;
|
|
14161
|
+
}
|
|
14162
|
+
async function readOriginal(docPath) {
|
|
14163
|
+
const info = await getOriginalInfo(docPath);
|
|
14164
|
+
if (!info.available) {
|
|
14165
|
+
throw Object.assign(coded(`Original unavailable: ${info.reason}${info.detail ? ` (${info.detail})` : ""}`, "E_STATE"), {
|
|
14166
|
+
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."
|
|
14167
|
+
});
|
|
14168
|
+
}
|
|
14169
|
+
return (await verifiedSnapshot(docPath)).bytes;
|
|
14170
|
+
}
|
|
14171
|
+
async function exportOriginal(docPath, outPath, opts) {
|
|
14172
|
+
const bytes = await readOriginal(docPath);
|
|
14173
|
+
const source = await import_promises2.default.realpath(docPath).catch((e) => {
|
|
14174
|
+
if (errno(e) === "ENOENT") return import_node_path.default.resolve(docPath);
|
|
14175
|
+
throw e;
|
|
14176
|
+
});
|
|
14177
|
+
const parent = import_node_path.default.dirname(import_node_path.default.resolve(outPath));
|
|
14178
|
+
const destination = import_node_path.default.join(await import_promises2.default.realpath(parent).catch((e) => {
|
|
14179
|
+
if (errno(e) === "ENOENT") return parent;
|
|
14180
|
+
throw e;
|
|
14181
|
+
}), import_node_path.default.basename(outPath));
|
|
14182
|
+
const samePath = (a, b) => process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
14183
|
+
let sameFile = samePath(source, destination);
|
|
14184
|
+
try {
|
|
14185
|
+
const [a, b] = await Promise.all([import_promises2.default.stat(source), import_promises2.default.stat(destination)]);
|
|
14186
|
+
sameFile ||= a.ino !== 0 && b.ino !== 0 && a.dev === b.dev && a.ino === b.ino;
|
|
14187
|
+
} catch (e) {
|
|
14188
|
+
if (errno(e) !== "ENOENT") throw e;
|
|
14189
|
+
}
|
|
14190
|
+
if (sameFile) throw coded("Use restore_original_document to replace the working file.", "E_CONFLICT");
|
|
14191
|
+
const repo = await import_promises2.default.realpath(getRepoDir(docPath));
|
|
14192
|
+
const normalizedDest = process.platform === "win32" ? destination.toLowerCase() : destination;
|
|
14193
|
+
const normalizedRepo = process.platform === "win32" ? repo.toLowerCase() : repo;
|
|
14194
|
+
if (normalizedDest === normalizedRepo || normalizedDest.startsWith(`${normalizedRepo}${import_node_path.default.sep}`)) {
|
|
14195
|
+
throw coded("Original export cannot write inside the document history store.", "E_CONFLICT");
|
|
14196
|
+
}
|
|
14197
|
+
if (opts?.overwrite) await replaceFileAtomic(outPath, bytes, 438);
|
|
14198
|
+
else await writeFileExclusive(outPath, bytes, 438);
|
|
14199
|
+
return getOriginalInfo(docPath);
|
|
14200
|
+
}
|
|
14201
|
+
|
|
13788
14202
|
// ../engine/src/git.ts
|
|
14203
|
+
var import_fs4 = __toESM(require("fs"));
|
|
14204
|
+
var import_os = __toESM(require("os"));
|
|
14205
|
+
var import_promises3 = __toESM(require("fs/promises"));
|
|
14206
|
+
var import_path3 = __toESM(require("path"));
|
|
14207
|
+
var import_crypto4 = __toESM(require("crypto"));
|
|
14208
|
+
var import_isomorphic_git = __toESM(require("isomorphic-git"));
|
|
13789
14209
|
var HISTORY_DIR_ENV = "ASTRA_HISTORY_DIR";
|
|
13790
14210
|
function historyRoot() {
|
|
13791
14211
|
const raw = process.env[HISTORY_DIR_ENV];
|
|
@@ -13818,7 +14238,55 @@ function getRepoDir(docPath) {
|
|
|
13818
14238
|
if (root !== null) return import_path3.default.join(root, historyKey(import_path3.default.dirname(docPath)), import_path3.default.basename(docPath));
|
|
13819
14239
|
return import_path3.default.join(import_path3.default.dirname(docPath), ".astra", import_path3.default.basename(docPath));
|
|
13820
14240
|
}
|
|
14241
|
+
var SNAPSHOT_FORMAT_MARKER = ".astra-snapshot-format";
|
|
14242
|
+
var SNAPSHOT_FORMAT_VERSION = "2";
|
|
14243
|
+
function isCompleteSnapshotMarker(content) {
|
|
14244
|
+
if (content === void 0) return false;
|
|
14245
|
+
return Buffer.from(content).toString("utf8").trim() === SNAPSHOT_FORMAT_VERSION;
|
|
14246
|
+
}
|
|
14247
|
+
var ESCAPED_DIR = "__astra_escaped__";
|
|
14248
|
+
var ESCAPED_NAME_MAX = 255;
|
|
14249
|
+
function needsHistoryEscape(entryName) {
|
|
14250
|
+
const name2 = entryName.replace(/\\/g, "/");
|
|
14251
|
+
if (name2 === SNAPSHOT_FORMAT_MARKER || name2 === "document.xml") return true;
|
|
14252
|
+
if (name2 === ESCAPED_DIR || name2.startsWith(`${ESCAPED_DIR}/`)) return true;
|
|
14253
|
+
return name2.split("/").some((segment) => segment.toLowerCase().startsWith(".git"));
|
|
14254
|
+
}
|
|
14255
|
+
function isUnrecordableName(entryName) {
|
|
14256
|
+
const name2 = entryName.replace(/\\/g, "/");
|
|
14257
|
+
if (name2 === "" || name2.endsWith("/") || name2.startsWith("/") || /^[a-zA-Z]:/.test(name2)) return true;
|
|
14258
|
+
return name2.split("/").some((segment) => segment === "" || segment === "." || segment === "..");
|
|
14259
|
+
}
|
|
13821
14260
|
function isTrackedPackageFile(entryName) {
|
|
14261
|
+
const name2 = entryName.replace(/\\/g, "/");
|
|
14262
|
+
if (name2 === "word/document.xml") return false;
|
|
14263
|
+
return !isUnrecordableName(name2);
|
|
14264
|
+
}
|
|
14265
|
+
function historyPathFor(entryName) {
|
|
14266
|
+
const name2 = entryName.replace(/\\/g, "/");
|
|
14267
|
+
if (!needsHistoryEscape(name2)) return name2;
|
|
14268
|
+
const encoded = Buffer.from(name2, "utf8").toString("base64url");
|
|
14269
|
+
if (encoded.length > ESCAPED_NAME_MAX) {
|
|
14270
|
+
throw Object.assign(
|
|
14271
|
+
new Error(`E_VALIDATION: the package entry name is too long to record in history (${name2.length} characters).`),
|
|
14272
|
+
{
|
|
14273
|
+
code: "E_VALIDATION",
|
|
14274
|
+
remedy: "Rename or remove that package part (for example with sanitize_document), then save again."
|
|
14275
|
+
}
|
|
14276
|
+
);
|
|
14277
|
+
}
|
|
14278
|
+
return `${ESCAPED_DIR}/${encoded}`;
|
|
14279
|
+
}
|
|
14280
|
+
function entryNameFor(repoPath) {
|
|
14281
|
+
const rel = repoPath.replace(/\\/g, "/");
|
|
14282
|
+
if (!rel.startsWith(`${ESCAPED_DIR}/`)) return rel;
|
|
14283
|
+
return Buffer.from(rel.slice(ESCAPED_DIR.length + 1), "base64url").toString("utf8");
|
|
14284
|
+
}
|
|
14285
|
+
function isRepoPartPath(repoPath) {
|
|
14286
|
+
const rel = repoPath.replace(/\\/g, "/");
|
|
14287
|
+
return rel !== "document.xml" && rel !== SNAPSHOT_FORMAT_MARKER && rel !== "" && !rel.startsWith(".git/");
|
|
14288
|
+
}
|
|
14289
|
+
function isLegacyTrackedPackageFile(entryName) {
|
|
13822
14290
|
const name2 = entryName.replace(/\\/g, "/");
|
|
13823
14291
|
if (name2 === "word/document.xml") return false;
|
|
13824
14292
|
if (name2 === "word/styles.xml") return true;
|
|
@@ -13842,22 +14310,109 @@ function toRepoRelative(entryName) {
|
|
|
13842
14310
|
}
|
|
13843
14311
|
async function writeAndAdd(repoDir, relPath, content) {
|
|
13844
14312
|
const safeRel = toRepoRelative(relPath);
|
|
13845
|
-
if (
|
|
14313
|
+
if (isTraversalName(safeRel)) {
|
|
13846
14314
|
throw new Error(`Security Violation: refusing to track package path '${relPath}'`);
|
|
13847
14315
|
}
|
|
13848
14316
|
const fullPath = import_path3.default.join(repoDir, safeRel);
|
|
13849
14317
|
const dirName = import_path3.default.dirname(fullPath);
|
|
13850
14318
|
assertWriteTargetNoSymlink(fullPath);
|
|
13851
|
-
await
|
|
14319
|
+
await import_promises3.default.mkdir(dirName, { recursive: true, mode: 448 });
|
|
13852
14320
|
await secureDirChain0700(repoDir, dirName);
|
|
13853
14321
|
assertWriteTargetNoSymlink(fullPath);
|
|
13854
14322
|
await writeFileNoFollow(fullPath, content);
|
|
13855
14323
|
await import_isomorphic_git.default.add({ fs: import_fs4.default, dir: repoDir, filepath: safeRel });
|
|
13856
14324
|
}
|
|
14325
|
+
function isTraversalName(entryName) {
|
|
14326
|
+
const name2 = toRepoRelative(entryName);
|
|
14327
|
+
return name2.split("/").includes("..") || import_path3.default.isAbsolute(name2) || /^[a-zA-Z]:/.test(name2);
|
|
14328
|
+
}
|
|
14329
|
+
async function writeSnapshotParts(repoDir, extraFiles) {
|
|
14330
|
+
const written = /* @__PURE__ */ new Set();
|
|
14331
|
+
if (!extraFiles) return written;
|
|
14332
|
+
for (const [name2, content] of Object.entries(extraFiles)) {
|
|
14333
|
+
if (isTraversalName(name2)) {
|
|
14334
|
+
await writeAndAdd(repoDir, name2, content);
|
|
14335
|
+
continue;
|
|
14336
|
+
}
|
|
14337
|
+
if (!isTrackedPackageFile(name2)) continue;
|
|
14338
|
+
const repoPath = historyPathFor(name2);
|
|
14339
|
+
await writeAndAdd(repoDir, repoPath, content);
|
|
14340
|
+
written.add(repoPath);
|
|
14341
|
+
}
|
|
14342
|
+
return written;
|
|
14343
|
+
}
|
|
14344
|
+
async function indexedPartPaths(repoDir) {
|
|
14345
|
+
const tracked = await import_isomorphic_git.default.listFiles({ fs: import_fs4.default, dir: repoDir });
|
|
14346
|
+
return new Set(tracked.map((t) => toRepoRelative(t)).filter(isRepoPartPath));
|
|
14347
|
+
}
|
|
14348
|
+
async function clearUncommittedPackageParts(repoDir) {
|
|
14349
|
+
const repoRoot = import_path3.default.resolve(repoDir);
|
|
14350
|
+
for (const rel of await indexedPartPaths(repoDir)) {
|
|
14351
|
+
if (isTraversalName(rel)) {
|
|
14352
|
+
throw Object.assign(new Error(`History repair refused an unsafe indexed path: ${rel}`), { code: "E_SANDBOX" });
|
|
14353
|
+
}
|
|
14354
|
+
const fullPath = import_path3.default.resolve(repoRoot, rel);
|
|
14355
|
+
const relative3 = import_path3.default.relative(repoRoot, fullPath);
|
|
14356
|
+
if (relative3 === "" || relative3 === ".." || relative3.startsWith(`..${import_path3.default.sep}`) || import_path3.default.isAbsolute(relative3)) {
|
|
14357
|
+
throw Object.assign(new Error(`History repair refused an indexed path outside its repository: ${rel}`), { code: "E_SANDBOX" });
|
|
14358
|
+
}
|
|
14359
|
+
assertWriteTargetNoSymlink(fullPath);
|
|
14360
|
+
try {
|
|
14361
|
+
await import_promises3.default.unlink(fullPath);
|
|
14362
|
+
} catch (error) {
|
|
14363
|
+
if (error?.code !== "ENOENT") throw error;
|
|
14364
|
+
}
|
|
14365
|
+
await import_isomorphic_git.default.remove({ fs: import_fs4.default, dir: repoDir, filepath: rel });
|
|
14366
|
+
}
|
|
14367
|
+
}
|
|
14368
|
+
async function isUnbornHistory(repoDir) {
|
|
14369
|
+
let head;
|
|
14370
|
+
try {
|
|
14371
|
+
head = await import_promises3.default.readFile(import_path3.default.join(repoDir, ".git", "HEAD"), "utf8");
|
|
14372
|
+
} catch {
|
|
14373
|
+
return false;
|
|
14374
|
+
}
|
|
14375
|
+
if (!/^ref:\s*refs\/heads\/[^\s]+\s*$/.test(head)) return false;
|
|
14376
|
+
const [branches, tags] = await Promise.all([
|
|
14377
|
+
import_isomorphic_git.default.listBranches({ fs: import_fs4.default, dir: repoDir }),
|
|
14378
|
+
import_isomorphic_git.default.listTags({ fs: import_fs4.default, dir: repoDir })
|
|
14379
|
+
]);
|
|
14380
|
+
return branches.length === 0 && tags.length === 0;
|
|
14381
|
+
}
|
|
14382
|
+
async function snapshotIsProvenComplete(repoDir, extraFiles, written) {
|
|
14383
|
+
if (!extraFiles) return false;
|
|
14384
|
+
try {
|
|
14385
|
+
const indexed = await indexedPartPaths(repoDir);
|
|
14386
|
+
if (indexed.size !== written.size) return false;
|
|
14387
|
+
for (const p of written) if (!indexed.has(p)) return false;
|
|
14388
|
+
return true;
|
|
14389
|
+
} catch {
|
|
14390
|
+
return false;
|
|
14391
|
+
}
|
|
14392
|
+
}
|
|
14393
|
+
async function applySnapshotMarker(repoDir, complete) {
|
|
14394
|
+
if (complete) {
|
|
14395
|
+
await writeAndAdd(repoDir, SNAPSHOT_FORMAT_MARKER, `${SNAPSHOT_FORMAT_VERSION}
|
|
14396
|
+
`);
|
|
14397
|
+
return;
|
|
14398
|
+
}
|
|
14399
|
+
try {
|
|
14400
|
+
await import_promises3.default.unlink(import_path3.default.join(repoDir, SNAPSHOT_FORMAT_MARKER));
|
|
14401
|
+
} catch {
|
|
14402
|
+
}
|
|
14403
|
+
try {
|
|
14404
|
+
await import_isomorphic_git.default.remove({ fs: import_fs4.default, dir: repoDir, filepath: SNAPSHOT_FORMAT_MARKER });
|
|
14405
|
+
} catch {
|
|
14406
|
+
}
|
|
14407
|
+
const stillIndexed = (await import_isomorphic_git.default.listFiles({ fs: import_fs4.default, dir: repoDir })).some((t) => toRepoRelative(t) === SNAPSHOT_FORMAT_MARKER);
|
|
14408
|
+
if (stillIndexed) {
|
|
14409
|
+
throw new Error("History commit refused: could not clear the snapshot marker of an earlier commit.");
|
|
14410
|
+
}
|
|
14411
|
+
}
|
|
13857
14412
|
async function chmod0700BestEffort(dir) {
|
|
13858
14413
|
if (process.platform === "win32") return;
|
|
13859
14414
|
try {
|
|
13860
|
-
await
|
|
14415
|
+
await import_promises3.default.chmod(dir, 448);
|
|
13861
14416
|
} catch {
|
|
13862
14417
|
}
|
|
13863
14418
|
}
|
|
@@ -13890,7 +14445,7 @@ async function migrateExistingTree0700(repoDir) {
|
|
|
13890
14445
|
await chmod0700BestEffort(dir);
|
|
13891
14446
|
let entries = [];
|
|
13892
14447
|
try {
|
|
13893
|
-
entries = await
|
|
14448
|
+
entries = await import_promises3.default.readdir(dir, { withFileTypes: true });
|
|
13894
14449
|
} catch {
|
|
13895
14450
|
continue;
|
|
13896
14451
|
}
|
|
@@ -13940,7 +14495,7 @@ async function getSidecarBytes(repoDir) {
|
|
|
13940
14495
|
const dir = stack.pop();
|
|
13941
14496
|
let entries = [];
|
|
13942
14497
|
try {
|
|
13943
|
-
entries = await
|
|
14498
|
+
entries = await import_promises3.default.readdir(dir, { withFileTypes: true });
|
|
13944
14499
|
} catch {
|
|
13945
14500
|
continue;
|
|
13946
14501
|
}
|
|
@@ -13953,7 +14508,7 @@ async function getSidecarBytes(repoDir) {
|
|
|
13953
14508
|
continue;
|
|
13954
14509
|
}
|
|
13955
14510
|
if (typeof entry?.isFile === "function" && !entry.isFile()) continue;
|
|
13956
|
-
const st = await
|
|
14511
|
+
const st = await import_promises3.default.stat(full);
|
|
13957
14512
|
if (st.isDirectory()) stack.push(full);
|
|
13958
14513
|
else if (st.isFile()) total += st.size;
|
|
13959
14514
|
} catch {
|
|
@@ -13971,6 +14526,116 @@ async function getHistoryCommitCount(repoDir) {
|
|
|
13971
14526
|
return 0;
|
|
13972
14527
|
}
|
|
13973
14528
|
}
|
|
14529
|
+
var GIT_INITIALIZATION_LOCK = ".astra-init.lock";
|
|
14530
|
+
function gitInitializationLockPath(repoDir) {
|
|
14531
|
+
return import_path3.default.join(repoDir, GIT_INITIALIZATION_LOCK);
|
|
14532
|
+
}
|
|
14533
|
+
function gitInitializationIncompleteError(repoDir) {
|
|
14534
|
+
return Object.assign(new Error(`History initialization is incomplete at ${repoDir}.`), {
|
|
14535
|
+
code: "E_STATE",
|
|
14536
|
+
remedy: "Stop other ASTra processes using this document, inspect the history directory, then run `astra history <file> --init` to record a baseline."
|
|
14537
|
+
});
|
|
14538
|
+
}
|
|
14539
|
+
async function gitInitializationLockExists(repoDir) {
|
|
14540
|
+
const file = gitInitializationLockPath(repoDir);
|
|
14541
|
+
try {
|
|
14542
|
+
const st = await import_promises3.default.lstat(file);
|
|
14543
|
+
if (!st.isFile() || st.isSymbolicLink()) throw Object.assign(new Error(`Unsafe history initialization lock: ${file}`), { code: "E_SANDBOX" });
|
|
14544
|
+
return true;
|
|
14545
|
+
} catch (error) {
|
|
14546
|
+
if (error?.code === "ENOENT") return false;
|
|
14547
|
+
throw error;
|
|
14548
|
+
}
|
|
14549
|
+
}
|
|
14550
|
+
async function describeInitializationLock(file) {
|
|
14551
|
+
let record = {};
|
|
14552
|
+
try {
|
|
14553
|
+
const st = await import_promises3.default.lstat(file);
|
|
14554
|
+
if (st.isFile() && !st.isSymbolicLink() && st.size <= 4096) record = JSON.parse(await import_promises3.default.readFile(file, "utf8"));
|
|
14555
|
+
} catch {
|
|
14556
|
+
}
|
|
14557
|
+
const pid = typeof record.pid === "number" && Number.isInteger(record.pid) && record.pid > 0 ? record.pid : void 0;
|
|
14558
|
+
const hostname = typeof record.hostname === "string" ? record.hostname.slice(0, 255) : void 0;
|
|
14559
|
+
const started = typeof record.startedAt === "string" ? ` since ${record.startedAt.slice(0, 40)}` : "";
|
|
14560
|
+
if (pid === void 0) return `the lock does not record its owner${started}; cannot tell whether an initializer is still running`;
|
|
14561
|
+
if (hostname === void 0) return `owner pid ${pid}, host not recorded${started}; cannot tell whether it is still running`;
|
|
14562
|
+
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`;
|
|
14563
|
+
try {
|
|
14564
|
+
process.kill(pid, 0);
|
|
14565
|
+
return `owner pid ${pid} on this host${started}; that process is running`;
|
|
14566
|
+
} catch (error) {
|
|
14567
|
+
if (error?.code === "EPERM") return `owner pid ${pid} on this host${started}; that process is running`;
|
|
14568
|
+
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)`;
|
|
14569
|
+
return `owner pid ${pid} on this host${started}; cannot tell whether it is still running`;
|
|
14570
|
+
}
|
|
14571
|
+
}
|
|
14572
|
+
async function gitInitializationConflictError(file) {
|
|
14573
|
+
return Object.assign(new Error(`Another or interrupted ASTra history initialization is marked at ${file} (${await describeInitializationLock(file)}).`), {
|
|
14574
|
+
code: "E_CONFLICT",
|
|
14575
|
+
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.`
|
|
14576
|
+
});
|
|
14577
|
+
}
|
|
14578
|
+
async function countCommitsStrict(repoDir) {
|
|
14579
|
+
await assertUsableSidecarGit(repoDir);
|
|
14580
|
+
try {
|
|
14581
|
+
return (await import_isomorphic_git.default.log({ fs: import_fs4.default, dir: repoDir })).length;
|
|
14582
|
+
} catch (error) {
|
|
14583
|
+
if (await isUnbornHistory(repoDir)) return 0;
|
|
14584
|
+
throw error;
|
|
14585
|
+
}
|
|
14586
|
+
}
|
|
14587
|
+
async function assertGitHistoryReady(repoDir) {
|
|
14588
|
+
await assertNoGitInitializationInProgress(repoDir);
|
|
14589
|
+
if (await countCommitsStrict(repoDir) === 0) throw gitInitializationIncompleteError(repoDir);
|
|
14590
|
+
}
|
|
14591
|
+
async function assertNoGitInitializationInProgress(repoDir) {
|
|
14592
|
+
if (await gitInitializationLockExists(repoDir)) throw await gitInitializationConflictError(gitInitializationLockPath(repoDir));
|
|
14593
|
+
}
|
|
14594
|
+
async function assertHistoryReadyForWrite(docPath) {
|
|
14595
|
+
const repoDir = getRepoDir(docPath);
|
|
14596
|
+
await assertNoGitInitializationInProgress(repoDir);
|
|
14597
|
+
if (import_fs4.default.existsSync(import_path3.default.join(repoDir, ".git"))) await assertGitHistoryReady(repoDir);
|
|
14598
|
+
}
|
|
14599
|
+
async function withGitInitializationLock(repoDir, run) {
|
|
14600
|
+
const file = gitInitializationLockPath(repoDir);
|
|
14601
|
+
assertWriteTargetNoSymlink(file);
|
|
14602
|
+
const token = import_crypto4.default.randomUUID();
|
|
14603
|
+
let handle;
|
|
14604
|
+
try {
|
|
14605
|
+
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);
|
|
14606
|
+
} catch (error) {
|
|
14607
|
+
if (error?.code === "EEXIST") throw await gitInitializationConflictError(file);
|
|
14608
|
+
throw error;
|
|
14609
|
+
}
|
|
14610
|
+
let primaryError;
|
|
14611
|
+
let result;
|
|
14612
|
+
try {
|
|
14613
|
+
await handle.writeFile(JSON.stringify({ version: 2, pid: process.pid, hostname: import_os.default.hostname(), token, startedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
14614
|
+
await handle.sync();
|
|
14615
|
+
result = await run();
|
|
14616
|
+
} catch (error) {
|
|
14617
|
+
primaryError = error;
|
|
14618
|
+
}
|
|
14619
|
+
await handle.close().catch(() => void 0);
|
|
14620
|
+
let cleanupError;
|
|
14621
|
+
try {
|
|
14622
|
+
const current = JSON.parse(await import_promises3.default.readFile(file, "utf8"));
|
|
14623
|
+
if (current.token !== token) throw Object.assign(new Error(`History initialization lock ownership changed; left in place: ${file}`), { code: "E_CONFLICT" });
|
|
14624
|
+
await import_promises3.default.unlink(file);
|
|
14625
|
+
} catch (error) {
|
|
14626
|
+
cleanupError = error;
|
|
14627
|
+
}
|
|
14628
|
+
if (cleanupError && cleanupError?.code !== "ENOENT") {
|
|
14629
|
+
if (primaryError) {
|
|
14630
|
+
const cause = primaryError instanceof Error ? primaryError.message : String(primaryError);
|
|
14631
|
+
throw Object.assign(new Error(`${cause}; initialization lock cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`), { code: "E_IO", lockPath: file });
|
|
14632
|
+
}
|
|
14633
|
+
throw Object.assign(new Error(`History initialization lock cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`), { code: "E_IO", lockPath: file });
|
|
14634
|
+
}
|
|
14635
|
+
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" });
|
|
14636
|
+
if (primaryError) throw primaryError;
|
|
14637
|
+
return result;
|
|
14638
|
+
}
|
|
13974
14639
|
var SIDECAR_TOMBSTONE_FILENAME = ".astra-tombstones.json";
|
|
13975
14640
|
var sidecarRegenWarnings = /* @__PURE__ */ new Map();
|
|
13976
14641
|
function resolveDocIdentity(target) {
|
|
@@ -14090,7 +14755,7 @@ async function checkSidecarTombstone(target, repoDir) {
|
|
|
14090
14755
|
const key = import_path3.default.resolve(repoDir);
|
|
14091
14756
|
let hasGit = false;
|
|
14092
14757
|
try {
|
|
14093
|
-
hasGit = (await
|
|
14758
|
+
hasGit = (await import_promises3.default.stat(import_path3.default.join(repoDir, ".git"))).isDirectory();
|
|
14094
14759
|
} catch {
|
|
14095
14760
|
hasGit = false;
|
|
14096
14761
|
}
|
|
@@ -14184,14 +14849,14 @@ async function assertUsableSidecarGit(repoDir) {
|
|
|
14184
14849
|
const gitPath = import_path3.default.join(repoDir, ".git");
|
|
14185
14850
|
let entry;
|
|
14186
14851
|
try {
|
|
14187
|
-
entry = await
|
|
14852
|
+
entry = await import_promises3.default.lstat(gitPath);
|
|
14188
14853
|
} catch {
|
|
14189
14854
|
return;
|
|
14190
14855
|
}
|
|
14191
14856
|
let usable = false;
|
|
14192
14857
|
if (entry.isSymbolicLink()) {
|
|
14193
14858
|
try {
|
|
14194
|
-
usable = (await
|
|
14859
|
+
usable = (await import_promises3.default.stat(gitPath)).isDirectory();
|
|
14195
14860
|
} catch {
|
|
14196
14861
|
usable = false;
|
|
14197
14862
|
}
|
|
@@ -14209,7 +14874,7 @@ async function assertUsableSidecarGit(repoDir) {
|
|
|
14209
14874
|
async function ensureAstraGitRepo(target) {
|
|
14210
14875
|
const repoDir = isRepoDirTarget(target) ? target : getRepoDir(target);
|
|
14211
14876
|
await assertUsableSidecarGit(repoDir);
|
|
14212
|
-
await
|
|
14877
|
+
await import_promises3.default.mkdir(repoDir, { recursive: true, mode: 448 });
|
|
14213
14878
|
if (process.platform !== "win32") {
|
|
14214
14879
|
await chmod0700BestEffort(repoDir);
|
|
14215
14880
|
const parent = import_path3.default.dirname(repoDir);
|
|
@@ -14225,20 +14890,44 @@ async function ensureAstraGitRepo(target) {
|
|
|
14225
14890
|
}
|
|
14226
14891
|
return repoDir;
|
|
14227
14892
|
}
|
|
14228
|
-
async function initAstraRepo(docPath, initialXml, extraFiles) {
|
|
14893
|
+
async function initAstraRepo(docPath, initialXml, extraFiles, originalBytes) {
|
|
14229
14894
|
const repoDir = await ensureAstraGitRepo(docPath);
|
|
14230
|
-
await
|
|
14231
|
-
|
|
14232
|
-
|
|
14233
|
-
|
|
14234
|
-
await
|
|
14895
|
+
await withGitInitializationLock(repoDir, async () => {
|
|
14896
|
+
await import_isomorphic_git.default.init({ fs: import_fs4.default, dir: repoDir });
|
|
14897
|
+
let commits;
|
|
14898
|
+
try {
|
|
14899
|
+
commits = await import_isomorphic_git.default.log({ fs: import_fs4.default, dir: repoDir });
|
|
14900
|
+
} catch (error) {
|
|
14901
|
+
if (!await isUnbornHistory(repoDir)) throw error;
|
|
14902
|
+
commits = [];
|
|
14903
|
+
}
|
|
14904
|
+
if (commits.length > 0) {
|
|
14905
|
+
throw Object.assign(new Error(`History is already initialized at ${repoDir}.`), { code: "E_CONFLICT" });
|
|
14906
|
+
}
|
|
14907
|
+
await clearUncommittedPackageParts(repoDir);
|
|
14908
|
+
if (originalBytes) {
|
|
14909
|
+
const existing = await getOriginalInfo(import_path3.default.resolve(docPath));
|
|
14910
|
+
if (existing.available && existing.sha256 !== originalSha256(originalBytes)) {
|
|
14911
|
+
const snapshotPath = import_path3.default.join(repoDir, ".git", "astra-original", "original.snapshot");
|
|
14912
|
+
throw Object.assign(new Error(
|
|
14913
|
+
`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.`
|
|
14914
|
+
), {
|
|
14915
|
+
code: "E_CONFLICT",
|
|
14916
|
+
reason: "original-mismatch",
|
|
14917
|
+
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.`
|
|
14918
|
+
});
|
|
14919
|
+
}
|
|
14235
14920
|
}
|
|
14236
|
-
|
|
14237
|
-
|
|
14238
|
-
|
|
14239
|
-
|
|
14240
|
-
|
|
14241
|
-
|
|
14921
|
+
await captureOriginal(import_path3.default.resolve(docPath), originalBytes);
|
|
14922
|
+
await writeAndAdd(repoDir, "document.xml", initialXml);
|
|
14923
|
+
const writtenParts = await writeSnapshotParts(repoDir, extraFiles);
|
|
14924
|
+
await applySnapshotMarker(repoDir, await snapshotIsProvenComplete(repoDir, extraFiles, writtenParts));
|
|
14925
|
+
await import_isomorphic_git.default.commit({
|
|
14926
|
+
fs: import_fs4.default,
|
|
14927
|
+
dir: repoDir,
|
|
14928
|
+
author: { name: "ASTra", email: "astra@attentionform.com" },
|
|
14929
|
+
message: "Initial state"
|
|
14930
|
+
});
|
|
14242
14931
|
});
|
|
14243
14932
|
try {
|
|
14244
14933
|
await checkSidecarTombstone(docPath, repoDir);
|
|
@@ -14250,6 +14939,7 @@ async function commitChange(docPath, xmlContent, message, extraFiles, deletedFil
|
|
|
14250
14939
|
const maxCommits = resolveHistoryMaxCommits(limits?.maxCommits);
|
|
14251
14940
|
const maxSidecarBytes = resolveHistoryMaxSidecarBytes(limits?.maxSidecarBytes);
|
|
14252
14941
|
const repoDir = await ensureAstraGitRepo(docPath);
|
|
14942
|
+
await assertGitHistoryReady(repoDir);
|
|
14253
14943
|
const existingCommits = await getHistoryCommitCount(repoDir);
|
|
14254
14944
|
if (existingCommits + 1 > maxCommits) {
|
|
14255
14945
|
throw Object.assign(
|
|
@@ -14289,41 +14979,35 @@ async function commitChange(docPath, xmlContent, message, extraFiles, deletedFil
|
|
|
14289
14979
|
} catch (err) {
|
|
14290
14980
|
if (err?.code === HISTORY_QUOTA_CODE) throw err;
|
|
14291
14981
|
}
|
|
14292
|
-
await writeAndAdd(repoDir, "document.xml", xmlContent);
|
|
14293
14982
|
if (extraFiles) {
|
|
14294
|
-
for (const
|
|
14295
|
-
await writeAndAdd(repoDir, name2, content);
|
|
14296
|
-
}
|
|
14983
|
+
for (const name2 of Object.keys(extraFiles)) if (!isTraversalName(name2) && isTrackedPackageFile(name2)) historyPathFor(name2);
|
|
14297
14984
|
}
|
|
14985
|
+
await writeAndAdd(repoDir, "document.xml", xmlContent);
|
|
14986
|
+
const writtenParts = await writeSnapshotParts(repoDir, extraFiles);
|
|
14298
14987
|
try {
|
|
14299
14988
|
const toDelete = /* @__PURE__ */ new Set();
|
|
14300
14989
|
if (deletedFiles) {
|
|
14301
14990
|
for (const name2 of deletedFiles) {
|
|
14302
|
-
|
|
14303
|
-
|
|
14304
|
-
toDelete.add(rel);
|
|
14991
|
+
if (!isTrackedPackageFile(name2) || isTraversalName(name2)) continue;
|
|
14992
|
+
toDelete.add(historyPathFor(name2));
|
|
14305
14993
|
}
|
|
14306
14994
|
}
|
|
14307
14995
|
if (extraFiles) {
|
|
14308
|
-
const newTracked =
|
|
14309
|
-
Object.keys(extraFiles).map((k) => toRepoRelative(k))
|
|
14310
|
-
);
|
|
14996
|
+
const newTracked = writtenParts;
|
|
14311
14997
|
try {
|
|
14312
14998
|
const tracked = await import_isomorphic_git.default.listFiles({ fs: import_fs4.default, dir: repoDir });
|
|
14313
14999
|
for (const t of tracked) {
|
|
14314
15000
|
const rel = toRepoRelative(t);
|
|
14315
|
-
if (rel
|
|
14316
|
-
if (isTrackedPackageFile(rel) && !newTracked.has(rel)) {
|
|
15001
|
+
if (isRepoPartPath(rel) && !newTracked.has(rel)) {
|
|
14317
15002
|
toDelete.add(rel);
|
|
14318
15003
|
}
|
|
14319
15004
|
}
|
|
14320
15005
|
} catch {
|
|
14321
15006
|
}
|
|
14322
15007
|
try {
|
|
14323
|
-
const onDisk = await
|
|
14324
|
-
for (const rel of onDisk
|
|
14325
|
-
if (rel
|
|
14326
|
-
if (isTrackedPackageFile(rel) && !newTracked.has(rel)) {
|
|
15008
|
+
const onDisk = await listRepoPaths(repoDir);
|
|
15009
|
+
for (const rel of onDisk) {
|
|
15010
|
+
if (isRepoPartPath(rel) && !newTracked.has(rel)) {
|
|
14327
15011
|
toDelete.add(rel);
|
|
14328
15012
|
}
|
|
14329
15013
|
}
|
|
@@ -14331,10 +15015,10 @@ async function commitChange(docPath, xmlContent, message, extraFiles, deletedFil
|
|
|
14331
15015
|
}
|
|
14332
15016
|
}
|
|
14333
15017
|
for (const rel of toDelete) {
|
|
14334
|
-
if (
|
|
15018
|
+
if (isTraversalName(rel)) continue;
|
|
14335
15019
|
const fullPath = import_path3.default.join(repoDir, rel);
|
|
14336
15020
|
try {
|
|
14337
|
-
await
|
|
15021
|
+
await import_promises3.default.unlink(fullPath);
|
|
14338
15022
|
} catch {
|
|
14339
15023
|
}
|
|
14340
15024
|
try {
|
|
@@ -14344,6 +15028,7 @@ async function commitChange(docPath, xmlContent, message, extraFiles, deletedFil
|
|
|
14344
15028
|
}
|
|
14345
15029
|
} catch {
|
|
14346
15030
|
}
|
|
15031
|
+
await applySnapshotMarker(repoDir, await snapshotIsProvenComplete(repoDir, extraFiles, writtenParts));
|
|
14347
15032
|
const sha = await import_isomorphic_git.default.commit({
|
|
14348
15033
|
fs: import_fs4.default,
|
|
14349
15034
|
dir: repoDir,
|
|
@@ -14356,8 +15041,27 @@ async function commitChange(docPath, xmlContent, message, extraFiles, deletedFil
|
|
|
14356
15041
|
}
|
|
14357
15042
|
return sha;
|
|
14358
15043
|
}
|
|
15044
|
+
async function getHistoryInitializationState(docPath) {
|
|
15045
|
+
const repoDir = getRepoDir(docPath);
|
|
15046
|
+
try {
|
|
15047
|
+
if (await gitInitializationLockExists(repoDir)) {
|
|
15048
|
+
return { state: "in-progress", detail: "Another ASTra process holds the history initialization lock (or it was interrupted)." };
|
|
15049
|
+
}
|
|
15050
|
+
if (!import_fs4.default.existsSync(import_path3.default.join(repoDir, ".git"))) return { state: "absent" };
|
|
15051
|
+
await assertUsableSidecarGit(repoDir);
|
|
15052
|
+
const commits = await import_isomorphic_git.default.log({ fs: import_fs4.default, dir: repoDir });
|
|
15053
|
+
if (commits.length === 0) return { state: "incomplete", detail: "History initialization is incomplete: no commit was recorded." };
|
|
15054
|
+
const root = commits[commits.length - 1];
|
|
15055
|
+
const initial = await checkoutCommit(docPath, root.oid);
|
|
15056
|
+
if (!initial.complete) return { state: "incomplete", detail: "The first history commit is not a complete package snapshot." };
|
|
15057
|
+
return { state: "complete" };
|
|
15058
|
+
} catch (error) {
|
|
15059
|
+
return { state: "incomplete", detail: error instanceof Error ? error.message : String(error) };
|
|
15060
|
+
}
|
|
15061
|
+
}
|
|
14359
15062
|
async function getHistory(docPath) {
|
|
14360
15063
|
const repoDir = getRepoDir(docPath);
|
|
15064
|
+
await assertNoGitInitializationInProgress(repoDir);
|
|
14361
15065
|
await assertUsableSidecarGit(repoDir);
|
|
14362
15066
|
let commits;
|
|
14363
15067
|
try {
|
|
@@ -14395,32 +15099,29 @@ async function getHistory(docPath) {
|
|
|
14395
15099
|
});
|
|
14396
15100
|
return entries;
|
|
14397
15101
|
}
|
|
14398
|
-
async function
|
|
14399
|
-
const out =
|
|
15102
|
+
async function listRepoPaths(repoDir) {
|
|
15103
|
+
const out = [];
|
|
14400
15104
|
async function walk(dir, relBase) {
|
|
14401
15105
|
let entries = [];
|
|
14402
15106
|
try {
|
|
14403
|
-
entries = await
|
|
15107
|
+
entries = await import_promises3.default.readdir(dir, { withFileTypes: true });
|
|
14404
15108
|
} catch {
|
|
14405
15109
|
return;
|
|
14406
15110
|
}
|
|
14407
15111
|
for (const entry of entries) {
|
|
14408
15112
|
if (entry.name === ".git") continue;
|
|
14409
|
-
const full = import_path3.default.join(dir, entry.name);
|
|
14410
15113
|
const rel = relBase ? `${relBase}/${entry.name}` : entry.name;
|
|
14411
|
-
if (entry.isDirectory())
|
|
14412
|
-
|
|
14413
|
-
} else if (entry.isFile()) {
|
|
14414
|
-
try {
|
|
14415
|
-
out.set(rel, await import_promises2.default.readFile(full));
|
|
14416
|
-
} catch {
|
|
14417
|
-
}
|
|
14418
|
-
}
|
|
15114
|
+
if (entry.isDirectory()) await walk(import_path3.default.join(dir, entry.name), rel);
|
|
15115
|
+
else if (entry.isFile()) out.push(rel);
|
|
14419
15116
|
}
|
|
14420
15117
|
}
|
|
14421
15118
|
await walk(repoDir, "");
|
|
14422
15119
|
return out;
|
|
14423
15120
|
}
|
|
15121
|
+
function trackedPredicateFor(target) {
|
|
15122
|
+
return target.complete ? isTrackedPackageFile : isLegacyTrackedPackageFile;
|
|
15123
|
+
}
|
|
15124
|
+
var INCOMPLETE_SNAPSHOT_WARNING = "This commit was recorded by an older ASTra version and predates complete snapshots: parts it never recorded (for example comment metadata, document properties, custom XML) keep their current content in the file, so the restore is not exact: those parts can be left without the content-type or relationship entries they had before. Restores to versions saved from now on are exact.";
|
|
14424
15125
|
async function checkoutCommit(docPath, commitOid) {
|
|
14425
15126
|
if (typeof commitOid !== "string" || commitOid.length === 0) {
|
|
14426
15127
|
throw new Error(`Cannot checkout: commit ${commitOid} not found in history.`);
|
|
@@ -14484,8 +15185,18 @@ async function checkoutCommit(docPath, commitOid) {
|
|
|
14484
15185
|
);
|
|
14485
15186
|
}
|
|
14486
15187
|
const xml = Buffer.from(docBlob.blob).toString("utf8");
|
|
15188
|
+
let complete = false;
|
|
15189
|
+
const markerOid = blobs.get(SNAPSHOT_FORMAT_MARKER);
|
|
15190
|
+
if (markerOid) {
|
|
15191
|
+
try {
|
|
15192
|
+
const marker = await import_isomorphic_git.default.readBlob({ fs: import_fs4.default, dir: repoDir, oid: markerOid });
|
|
15193
|
+
complete = isCompleteSnapshotMarker(marker.blob);
|
|
15194
|
+
} catch {
|
|
15195
|
+
complete = false;
|
|
15196
|
+
}
|
|
15197
|
+
}
|
|
14487
15198
|
const trackedEntries = [...blobs.entries()].filter(
|
|
14488
|
-
([rel]) => rel
|
|
15199
|
+
([rel]) => isRepoPartPath(rel) && (complete || isLegacyTrackedPackageFile(entryNameFor(rel)))
|
|
14489
15200
|
);
|
|
14490
15201
|
const extraBlobs = await Promise.all(
|
|
14491
15202
|
trackedEntries.map(async ([rel, blobOid]) => {
|
|
@@ -14498,11 +15209,11 @@ async function checkoutCommit(docPath, commitOid) {
|
|
|
14498
15209
|
`Checkout failed: cannot read blob '${rel}' (${blobOid}) at commit ${commitOid}: ${cause}`
|
|
14499
15210
|
);
|
|
14500
15211
|
}
|
|
14501
|
-
return [rel, Buffer.from(blobResult.blob)];
|
|
15212
|
+
return [entryNameFor(rel), Buffer.from(blobResult.blob)];
|
|
14502
15213
|
})
|
|
14503
15214
|
);
|
|
14504
15215
|
const extra = new Map(extraBlobs);
|
|
14505
|
-
return { xml, extra };
|
|
15216
|
+
return { xml, extra, complete };
|
|
14506
15217
|
}
|
|
14507
15218
|
async function commitStateIfDiffersFromHead(docPath, xmlContent, message, extraFiles = {}, author) {
|
|
14508
15219
|
try {
|
|
@@ -35075,7 +35786,7 @@ var ToonGenerator = class _ToonGenerator {
|
|
|
35075
35786
|
// ../engine/src/tmp.ts
|
|
35076
35787
|
var import_crypto5 = __toESM(require("crypto"));
|
|
35077
35788
|
var import_fs7 = __toESM(require("fs"));
|
|
35078
|
-
var
|
|
35789
|
+
var import_os2 = __toESM(require("os"));
|
|
35079
35790
|
var import_path6 = __toESM(require("path"));
|
|
35080
35791
|
var ASTRA_TMPDIR_ENV = "ASTRA_TMPDIR";
|
|
35081
35792
|
var TMP_SYMLINK_REMEDY = "Point $ASTRA_TMPDIR at a real directory (not a symlink), or unset it and retry.";
|
|
@@ -35131,7 +35842,7 @@ function resolveAstraTmpBase() {
|
|
|
35131
35842
|
try {
|
|
35132
35843
|
import_fs7.default.mkdirSync(dir, { recursive: true });
|
|
35133
35844
|
} catch {
|
|
35134
|
-
return
|
|
35845
|
+
return import_os2.default.tmpdir();
|
|
35135
35846
|
}
|
|
35136
35847
|
refuseSymlinkBase(dir);
|
|
35137
35848
|
refuseForeignOwner(dir);
|
|
@@ -35141,7 +35852,7 @@ function resolveAstraTmpBase() {
|
|
|
35141
35852
|
}
|
|
35142
35853
|
return dir;
|
|
35143
35854
|
}
|
|
35144
|
-
const fallback =
|
|
35855
|
+
const fallback = import_os2.default.tmpdir();
|
|
35145
35856
|
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()));
|
|
35146
35857
|
if (injected.includes(import_path6.default.resolve(fallback))) {
|
|
35147
35858
|
const dir = import_path6.default.resolve(fallback);
|
|
@@ -35181,12 +35892,12 @@ function logForensics(p, reason) {
|
|
|
35181
35892
|
try {
|
|
35182
35893
|
const rawName = import_path6.default.basename(p);
|
|
35183
35894
|
const name2 = rawName !== "" ? rawName : "artifact";
|
|
35184
|
-
let
|
|
35895
|
+
let hash2 = "unknown";
|
|
35185
35896
|
try {
|
|
35186
|
-
|
|
35897
|
+
hash2 = import_crypto5.default.createHash("sha256").update(p).digest("hex").slice(0, 16);
|
|
35187
35898
|
} catch {
|
|
35188
35899
|
}
|
|
35189
|
-
process.stderr.write(`[ASTra] tmp artifact kept for forensics: name="${name2}" sha256="${
|
|
35900
|
+
process.stderr.write(`[ASTra] tmp artifact kept for forensics: name="${name2}" sha256="${hash2}" (${reason}). Base honors $ASTRA_TMPDIR; remove manually after inspection.
|
|
35190
35901
|
`);
|
|
35191
35902
|
} catch {
|
|
35192
35903
|
}
|
|
@@ -35250,12 +35961,12 @@ function logForensicPrune(target, reason) {
|
|
|
35250
35961
|
try {
|
|
35251
35962
|
const rawName = import_path6.default.basename(target);
|
|
35252
35963
|
const name2 = rawName !== "" ? rawName : "artifact";
|
|
35253
|
-
let
|
|
35964
|
+
let hash2 = "unknown";
|
|
35254
35965
|
try {
|
|
35255
|
-
|
|
35966
|
+
hash2 = import_crypto5.default.createHash("sha256").update(target).digest("hex").slice(0, 16);
|
|
35256
35967
|
} catch {
|
|
35257
35968
|
}
|
|
35258
|
-
process.stderr.write(`[ASTra] tmp forensics prune: name="${name2}" sha256="${
|
|
35969
|
+
process.stderr.write(`[ASTra] tmp forensics prune: name="${name2}" sha256="${hash2}" (${reason}).
|
|
35259
35970
|
`);
|
|
35260
35971
|
} catch {
|
|
35261
35972
|
}
|
|
@@ -37491,9 +38202,9 @@ function escapeHtml(str) {
|
|
|
37491
38202
|
|
|
37492
38203
|
// ../engine/src/export/pdf.ts
|
|
37493
38204
|
var import_child_process = require("child_process");
|
|
37494
|
-
var
|
|
37495
|
-
var
|
|
37496
|
-
var
|
|
38205
|
+
var fs9 = __toESM(require("fs"));
|
|
38206
|
+
var fsp4 = __toESM(require("fs/promises"));
|
|
38207
|
+
var path8 = __toESM(require("path"));
|
|
37497
38208
|
var import_url = require("url");
|
|
37498
38209
|
|
|
37499
38210
|
// ../engine/src/export/preflight.ts
|
|
@@ -37952,7 +38663,7 @@ var MAX_CONVERT_ATTEMPTS = 3;
|
|
|
37952
38663
|
async function removeStagedDir(dir) {
|
|
37953
38664
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
37954
38665
|
try {
|
|
37955
|
-
await
|
|
38666
|
+
await fsp4.rm(dir, { recursive: true, force: true });
|
|
37956
38667
|
return;
|
|
37957
38668
|
} catch {
|
|
37958
38669
|
if (attempt < 2) {
|
|
@@ -37964,10 +38675,10 @@ async function removeStagedDir(dir) {
|
|
|
37964
38675
|
}
|
|
37965
38676
|
async function convertWithFilter(docxBuffer, outPdfPath, filterSpec, options, binary, stagedForensics) {
|
|
37966
38677
|
const tmpDir = secureMkdtemp("astra-pdf-");
|
|
37967
|
-
const tmpDocxPath =
|
|
38678
|
+
const tmpDocxPath = path8.join(tmpDir, `doc_${Date.now()}.docx`);
|
|
37968
38679
|
let succeeded = false;
|
|
37969
38680
|
try {
|
|
37970
|
-
await
|
|
38681
|
+
await fsp4.writeFile(tmpDocxPath, docxBuffer);
|
|
37971
38682
|
secureFile(tmpDocxPath);
|
|
37972
38683
|
let converted = false;
|
|
37973
38684
|
let lastError = null;
|
|
@@ -37976,7 +38687,7 @@ async function convertWithFilter(docxBuffer, outPdfPath, filterSpec, options, bi
|
|
|
37976
38687
|
await new Promise((resolve10, reject) => {
|
|
37977
38688
|
const child = (0, import_child_process.spawn)(
|
|
37978
38689
|
binary,
|
|
37979
|
-
[`-env:UserInstallation=${(0, import_url.pathToFileURL)(
|
|
38690
|
+
[`-env:UserInstallation=${(0, import_url.pathToFileURL)(path8.join(tmpDir, "lo_profile")).href}`, "--headless", "--convert-to", filterSpec, tmpDocxPath, "--outdir", tmpDir],
|
|
37980
38691
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
37981
38692
|
);
|
|
37982
38693
|
let stderr = "";
|
|
@@ -38012,23 +38723,23 @@ async function convertWithFilter(docxBuffer, outPdfPath, filterSpec, options, bi
|
|
|
38012
38723
|
if (!converted && lastError) {
|
|
38013
38724
|
throw lastError;
|
|
38014
38725
|
}
|
|
38015
|
-
const expectedPdfName =
|
|
38016
|
-
const generatedPdfPath =
|
|
38017
|
-
if (!
|
|
38726
|
+
const expectedPdfName = path8.basename(tmpDocxPath).replace(/\.docx$/i, ".pdf");
|
|
38727
|
+
const generatedPdfPath = path8.join(tmpDir, expectedPdfName);
|
|
38728
|
+
if (!fs9.existsSync(generatedPdfPath)) {
|
|
38018
38729
|
throw new Error(`PDF conversion failed: output file ${generatedPdfPath} was not created`);
|
|
38019
38730
|
}
|
|
38020
|
-
const targetDir =
|
|
38021
|
-
assertWriteTargetNoSymlink(
|
|
38022
|
-
await
|
|
38023
|
-
assertWriteTargetNoSymlink(
|
|
38024
|
-
await writeFileNoFollow(
|
|
38025
|
-
|
|
38731
|
+
const targetDir = path8.dirname(path8.resolve(outPdfPath));
|
|
38732
|
+
assertWriteTargetNoSymlink(path8.resolve(outPdfPath));
|
|
38733
|
+
await fsp4.mkdir(targetDir, { recursive: true });
|
|
38734
|
+
assertWriteTargetNoSymlink(path8.resolve(outPdfPath));
|
|
38735
|
+
await writeFileNoFollow(path8.resolve(outPdfPath), await fsp4.readFile(generatedPdfPath));
|
|
38736
|
+
fs9.chmodSync(path8.resolve(outPdfPath), 420);
|
|
38026
38737
|
succeeded = true;
|
|
38027
|
-
return
|
|
38738
|
+
return path8.resolve(outPdfPath);
|
|
38028
38739
|
} finally {
|
|
38029
38740
|
if (succeeded) {
|
|
38030
38741
|
try {
|
|
38031
|
-
await
|
|
38742
|
+
await fsp4.rm(tmpDir, { recursive: true, force: true });
|
|
38032
38743
|
} catch {
|
|
38033
38744
|
keepForForensics(tmpDir, "could not remove tmp dir after success");
|
|
38034
38745
|
}
|
|
@@ -38041,8 +38752,8 @@ async function convertWithFilter(docxBuffer, outPdfPath, filterSpec, options, bi
|
|
|
38041
38752
|
}
|
|
38042
38753
|
|
|
38043
38754
|
// ../engine/src/visual/backend.ts
|
|
38044
|
-
var
|
|
38045
|
-
var
|
|
38755
|
+
var fs10 = __toESM(require("fs"));
|
|
38756
|
+
var path9 = __toESM(require("path"));
|
|
38046
38757
|
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).";
|
|
38047
38758
|
function createDependencyError(missing) {
|
|
38048
38759
|
const which = missing.length > 0 ? missing.join(", ") : "soffice, pdftoppm";
|
|
@@ -38057,9 +38768,9 @@ function findOnPath(name2, pathEnv) {
|
|
|
38057
38768
|
const trimmed = name2.trim();
|
|
38058
38769
|
if (trimmed.includes("/") || trimmed.includes("\\")) {
|
|
38059
38770
|
try {
|
|
38060
|
-
const abs =
|
|
38061
|
-
|
|
38062
|
-
const stat3 =
|
|
38771
|
+
const abs = path9.resolve(trimmed);
|
|
38772
|
+
fs10.accessSync(abs, fs10.constants.X_OK);
|
|
38773
|
+
const stat3 = fs10.statSync(abs);
|
|
38063
38774
|
if (stat3.isFile()) return abs;
|
|
38064
38775
|
return null;
|
|
38065
38776
|
} catch {
|
|
@@ -38068,12 +38779,12 @@ function findOnPath(name2, pathEnv) {
|
|
|
38068
38779
|
}
|
|
38069
38780
|
const pathValue = pathEnv !== void 0 ? pathEnv : process.env.PATH ?? "";
|
|
38070
38781
|
if (!pathValue) return null;
|
|
38071
|
-
const dirs = pathValue.split(
|
|
38782
|
+
const dirs = pathValue.split(path9.delimiter).filter((d) => d.length > 0);
|
|
38072
38783
|
for (const dir of dirs) {
|
|
38073
38784
|
try {
|
|
38074
|
-
const candidate =
|
|
38075
|
-
|
|
38076
|
-
const stat3 =
|
|
38785
|
+
const candidate = path9.join(dir, trimmed);
|
|
38786
|
+
fs10.accessSync(candidate, fs10.constants.X_OK);
|
|
38787
|
+
const stat3 = fs10.statSync(candidate);
|
|
38077
38788
|
if (stat3.isFile()) return candidate;
|
|
38078
38789
|
} catch {
|
|
38079
38790
|
continue;
|
|
@@ -38118,8 +38829,8 @@ function assertPdfBackend(override) {
|
|
|
38118
38829
|
}
|
|
38119
38830
|
|
|
38120
38831
|
// ../engine/src/visual/renderer.ts
|
|
38121
|
-
var
|
|
38122
|
-
var
|
|
38832
|
+
var fs11 = __toESM(require("fs"));
|
|
38833
|
+
var path10 = __toESM(require("path"));
|
|
38123
38834
|
var import_url2 = require("url");
|
|
38124
38835
|
var import_child_process2 = require("child_process");
|
|
38125
38836
|
var import_util = require("util");
|
|
@@ -38130,13 +38841,13 @@ var VISUAL_DEFAULT_MAX_PAGES = 4;
|
|
|
38130
38841
|
var VISUAL_MIN_DPI = 72;
|
|
38131
38842
|
var VISUAL_MAX_DPI = 300;
|
|
38132
38843
|
function getVisualArtifactsBaseDir() {
|
|
38133
|
-
const dir =
|
|
38844
|
+
const dir = path10.join(resolveAstraTmpBase(), "ASTra", "visual");
|
|
38134
38845
|
try {
|
|
38135
|
-
|
|
38846
|
+
fs11.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
38136
38847
|
} catch {
|
|
38137
38848
|
}
|
|
38138
38849
|
try {
|
|
38139
|
-
secureDir(
|
|
38850
|
+
secureDir(path10.join(resolveAstraTmpBase(), "ASTra"));
|
|
38140
38851
|
} catch {
|
|
38141
38852
|
}
|
|
38142
38853
|
secureDir(dir);
|
|
@@ -38146,9 +38857,9 @@ function getVisualArtifactsDir(subDir) {
|
|
|
38146
38857
|
const base = getVisualArtifactsBaseDir();
|
|
38147
38858
|
if (!subDir || subDir.trim() === "") return base;
|
|
38148
38859
|
const safe = subDir.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 64) || "run";
|
|
38149
|
-
const dir =
|
|
38860
|
+
const dir = path10.join(base, safe);
|
|
38150
38861
|
try {
|
|
38151
|
-
|
|
38862
|
+
fs11.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
38152
38863
|
} catch {
|
|
38153
38864
|
}
|
|
38154
38865
|
secureDir(dir);
|
|
@@ -38182,7 +38893,7 @@ function parsePageNumber(fileName) {
|
|
|
38182
38893
|
function listRenderedPages(workDir, prefixBase) {
|
|
38183
38894
|
let entries = [];
|
|
38184
38895
|
try {
|
|
38185
|
-
entries =
|
|
38896
|
+
entries = fs11.readdirSync(workDir);
|
|
38186
38897
|
} catch {
|
|
38187
38898
|
return [];
|
|
38188
38899
|
}
|
|
@@ -38191,7 +38902,7 @@ function listRenderedPages(workDir, prefixBase) {
|
|
|
38191
38902
|
if (!name2.startsWith(prefixBase)) continue;
|
|
38192
38903
|
const page = parsePageNumber(name2);
|
|
38193
38904
|
if (page === null) continue;
|
|
38194
|
-
out.push({ page, file:
|
|
38905
|
+
out.push({ page, file: path10.join(workDir, name2) });
|
|
38195
38906
|
}
|
|
38196
38907
|
out.sort((a, b) => a.page - b.page);
|
|
38197
38908
|
return out;
|
|
@@ -38205,7 +38916,7 @@ function sandboxError(message, remedy) {
|
|
|
38205
38916
|
function canonicalVisualBaseDir() {
|
|
38206
38917
|
const base = getVisualArtifactsBaseDir();
|
|
38207
38918
|
try {
|
|
38208
|
-
return
|
|
38919
|
+
return fs11.realpathSync(base);
|
|
38209
38920
|
} catch {
|
|
38210
38921
|
return base;
|
|
38211
38922
|
}
|
|
@@ -38229,7 +38940,7 @@ function resolveJailedVisualOutDir(rawOutDir) {
|
|
|
38229
38940
|
if (trimmed.includes("\n") || trimmed.includes("\r")) {
|
|
38230
38941
|
throw sandboxError("E_SANDBOX: visual outDir contains a forbidden line break.", "Remove line breaks from the path and retry.");
|
|
38231
38942
|
}
|
|
38232
|
-
const resolved =
|
|
38943
|
+
const resolved = path10.resolve(trimmed);
|
|
38233
38944
|
const canonical = canonicalizeForAsset(resolved);
|
|
38234
38945
|
const canonicalBase = canonicalVisualBaseDir();
|
|
38235
38946
|
assertVisualInsideBase(canonical, trimmed, canonicalBase);
|
|
@@ -38261,7 +38972,7 @@ async function renderDocxToImages(docxBuffer, opts = {}) {
|
|
|
38261
38972
|
outDir = getVisualArtifactsDir(runId);
|
|
38262
38973
|
}
|
|
38263
38974
|
try {
|
|
38264
|
-
|
|
38975
|
+
fs11.mkdirSync(outDir, { recursive: true });
|
|
38265
38976
|
} catch (err) {
|
|
38266
38977
|
const msg = err instanceof Error ? err.message : String(err);
|
|
38267
38978
|
throw validationError(`E_VALIDATION: cannot create artifacts dir "${outDir}": ${msg}`, "Pass a writable outDir and retry.");
|
|
@@ -38272,16 +38983,16 @@ async function renderDocxToImages(docxBuffer, opts = {}) {
|
|
|
38272
38983
|
outDir = live;
|
|
38273
38984
|
}
|
|
38274
38985
|
const workDir = secureMkdtemp("astra-visual-");
|
|
38275
|
-
const inputDocx =
|
|
38986
|
+
const inputDocx = path10.join(workDir, "input.docx");
|
|
38276
38987
|
let succeeded = false;
|
|
38277
38988
|
try {
|
|
38278
|
-
|
|
38989
|
+
fs11.writeFileSync(inputDocx, docxBuffer);
|
|
38279
38990
|
secureFile(inputDocx);
|
|
38280
38991
|
let lastErr;
|
|
38281
38992
|
let converted = false;
|
|
38282
38993
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
38283
38994
|
try {
|
|
38284
|
-
await execFileAsync(sofficePath, [`-env:UserInstallation=${(0, import_url2.pathToFileURL)(
|
|
38995
|
+
await execFileAsync(sofficePath, [`-env:UserInstallation=${(0, import_url2.pathToFileURL)(path10.join(workDir, "lo_profile")).href}`, "--headless", "--convert-to", "pdf", "--outdir", workDir, inputDocx], {
|
|
38285
38996
|
timeout: timeoutMs
|
|
38286
38997
|
});
|
|
38287
38998
|
converted = true;
|
|
@@ -38299,17 +39010,17 @@ async function renderDocxToImages(docxBuffer, opts = {}) {
|
|
|
38299
39010
|
}
|
|
38300
39011
|
let pdfPath = null;
|
|
38301
39012
|
try {
|
|
38302
|
-
const names =
|
|
39013
|
+
const names = fs11.readdirSync(workDir).filter((n) => n.toLowerCase().endsWith(".pdf"));
|
|
38303
39014
|
names.sort((a, b) => a === "input.pdf" ? -1 : b === "input.pdf" ? 1 : a.localeCompare(b));
|
|
38304
|
-
if (names.length > 0) pdfPath =
|
|
39015
|
+
if (names.length > 0) pdfPath = path10.join(workDir, names[0]);
|
|
38305
39016
|
} catch {
|
|
38306
39017
|
pdfPath = null;
|
|
38307
39018
|
}
|
|
38308
|
-
if (!pdfPath || !
|
|
39019
|
+
if (!pdfPath || !fs11.existsSync(pdfPath)) {
|
|
38309
39020
|
throw validationError("E_VALIDATION: libreoffice produced no PDF output.", "Verify the .docx opens in Word/LibreOffice and retry.");
|
|
38310
39021
|
}
|
|
38311
39022
|
const prefixBase = "page";
|
|
38312
|
-
const prefix =
|
|
39023
|
+
const prefix = path10.join(workDir, prefixBase);
|
|
38313
39024
|
const ppmArgs = format === "png" ? ["-png", "-r", String(dpi), pdfPath, prefix] : ["-jpeg", "-jpegopt", `quality=${jpegQuality}`, "-r", String(dpi), pdfPath, prefix];
|
|
38314
39025
|
try {
|
|
38315
39026
|
await execFileAsync(pdftoppmPath, ppmArgs, { timeout: Math.min(timeoutMs, 6e4) });
|
|
@@ -38328,15 +39039,15 @@ async function renderDocxToImages(docxBuffer, opts = {}) {
|
|
|
38328
39039
|
const ext = format === "png" ? "png" : "jpg";
|
|
38329
39040
|
for (const entry of selected) {
|
|
38330
39041
|
const destName = `page-${entry.page}.${ext}`;
|
|
38331
|
-
const dest =
|
|
38332
|
-
if (!isPathInsideDir(
|
|
39042
|
+
const dest = path10.join(outDir, destName);
|
|
39043
|
+
if (!isPathInsideDir(path10.resolve(dest), path10.resolve(outDir))) {
|
|
38333
39044
|
throw sandboxError(
|
|
38334
39045
|
`E_SANDBOX: visual artifact destination escapes outDir: "${dest}".`,
|
|
38335
39046
|
"Pass an outDir inside the visual artifacts dir (use getVisualArtifactsDir(subDir) for a run-scoped dir) and retry."
|
|
38336
39047
|
);
|
|
38337
39048
|
}
|
|
38338
39049
|
try {
|
|
38339
|
-
|
|
39050
|
+
fs11.copyFileSync(entry.file, dest);
|
|
38340
39051
|
secureFile(dest);
|
|
38341
39052
|
} catch (err) {
|
|
38342
39053
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -38345,7 +39056,7 @@ async function renderDocxToImages(docxBuffer, opts = {}) {
|
|
|
38345
39056
|
const canonicalDest = canonicalizeForAsset(dest);
|
|
38346
39057
|
if (!isPathInsideDir(canonicalDest, canonicalizeForAsset(outDir))) {
|
|
38347
39058
|
try {
|
|
38348
|
-
|
|
39059
|
+
fs11.unlinkSync(dest);
|
|
38349
39060
|
} catch {
|
|
38350
39061
|
}
|
|
38351
39062
|
throw sandboxError(
|
|
@@ -38355,7 +39066,7 @@ async function renderDocxToImages(docxBuffer, opts = {}) {
|
|
|
38355
39066
|
}
|
|
38356
39067
|
let bytes = 0;
|
|
38357
39068
|
try {
|
|
38358
|
-
bytes =
|
|
39069
|
+
bytes = fs11.statSync(dest).size;
|
|
38359
39070
|
} catch {
|
|
38360
39071
|
bytes = 0;
|
|
38361
39072
|
}
|
|
@@ -40671,9 +41382,39 @@ function summarizeStyleGuide(guide) {
|
|
|
40671
41382
|
return out;
|
|
40672
41383
|
}
|
|
40673
41384
|
|
|
41385
|
+
// ../engine/src/restore-original.ts
|
|
41386
|
+
var import_node_fs2 = __toESM(require("node:fs"));
|
|
41387
|
+
var import_promises4 = __toESM(require("node:fs/promises"));
|
|
41388
|
+
var import_node_path2 = __toESM(require("node:path"));
|
|
41389
|
+
var import_isomorphic_git2 = __toESM(require("isomorphic-git"));
|
|
41390
|
+
async function captureHistoryState(docPath) {
|
|
41391
|
+
const target = import_node_path2.default.resolve(docPath);
|
|
41392
|
+
const [head, index] = await Promise.all([
|
|
41393
|
+
getHistory(target).then((entries) => entries[0]?.oid, () => void 0),
|
|
41394
|
+
import_isomorphic_git2.default.statusMatrix({ fs: import_node_fs2.default, dir: getRepoDir(target) }).then((matrix) => JSON.stringify(matrix), () => void 0)
|
|
41395
|
+
]);
|
|
41396
|
+
return { head, index };
|
|
41397
|
+
}
|
|
41398
|
+
async function historyChangedSince(docPath, before) {
|
|
41399
|
+
const after = await captureHistoryState(docPath);
|
|
41400
|
+
if (before.head === void 0 || after.head === void 0 || before.index === void 0 || after.index === void 0) return true;
|
|
41401
|
+
return after.head !== before.head || after.index !== before.index;
|
|
41402
|
+
}
|
|
41403
|
+
async function rollbackReplacedFile(docPath, previous, failure) {
|
|
41404
|
+
const outcome = failure.error?.outcome;
|
|
41405
|
+
if (!failure.replaced && outcome !== "replaced" && outcome !== "uncertain") return { recovered: true };
|
|
41406
|
+
try {
|
|
41407
|
+
await replaceFileAtomic(docPath, previous);
|
|
41408
|
+
if (originalSha256(await import_promises4.default.readFile(docPath)) === originalSha256(previous)) return { recovered: true };
|
|
41409
|
+
return { recovered: false, failure: "the file does not match the previous bytes after rollback" };
|
|
41410
|
+
} catch (error) {
|
|
41411
|
+
return { recovered: false, failure: error instanceof Error ? error.message : String(error) };
|
|
41412
|
+
}
|
|
41413
|
+
}
|
|
41414
|
+
|
|
40674
41415
|
// src/lib/mcp/baseline-gate.ts
|
|
40675
41416
|
var crypto13 = __toESM(require("crypto"));
|
|
40676
|
-
var
|
|
41417
|
+
var fsp6 = __toESM(require("fs/promises"));
|
|
40677
41418
|
|
|
40678
41419
|
// src/lib/mcp/errors.ts
|
|
40679
41420
|
function mcpError(code, message, remedy) {
|
|
@@ -40753,8 +41494,8 @@ function invalidInput(message) {
|
|
|
40753
41494
|
}
|
|
40754
41495
|
|
|
40755
41496
|
// src/lib/mcp/diff.ts
|
|
40756
|
-
var
|
|
40757
|
-
var
|
|
41497
|
+
var fs14 = __toESM(require("fs"));
|
|
41498
|
+
var path13 = __toESM(require("path"));
|
|
40758
41499
|
var DISCARDED_NODE_IDS_MAX = 20;
|
|
40759
41500
|
var AUDIT_NODE_IDS_MAX = 100;
|
|
40760
41501
|
var AUDIT_FILES_MAX = 100;
|
|
@@ -41121,7 +41862,7 @@ function pathDirname(p) {
|
|
|
41121
41862
|
}
|
|
41122
41863
|
function sidecarRepoExists(docPath) {
|
|
41123
41864
|
try {
|
|
41124
|
-
return
|
|
41865
|
+
return fs14.existsSync(path13.join(getRepoDir(docPath), ".git"));
|
|
41125
41866
|
} catch {
|
|
41126
41867
|
return false;
|
|
41127
41868
|
}
|
|
@@ -41160,7 +41901,7 @@ async function resolveBaseline(handle) {
|
|
|
41160
41901
|
if (handle.validationBaseline !== void 0) return handle.validationBaseline;
|
|
41161
41902
|
if (!handle.openSha256) return null;
|
|
41162
41903
|
try {
|
|
41163
|
-
const bytes = await
|
|
41904
|
+
const bytes = await fsp6.readFile(handle.filePath);
|
|
41164
41905
|
if (sha256Hex(bytes) !== handle.openSha256) return null;
|
|
41165
41906
|
const baseline = buildBaseline(await validateForBaseline(bytes));
|
|
41166
41907
|
handle.validationBaseline = baseline;
|
|
@@ -41244,7 +41985,7 @@ function withToleratedNote(message, preExisting) {
|
|
|
41244
41985
|
async function validateAtOpen(handle, timeoutMs) {
|
|
41245
41986
|
let bytes;
|
|
41246
41987
|
try {
|
|
41247
|
-
bytes = await
|
|
41988
|
+
bytes = await fsp6.readFile(handle.filePath);
|
|
41248
41989
|
} catch (err) {
|
|
41249
41990
|
return { status: "skipped", note: `the file could not be read for validation: ${err instanceof Error ? err.message : String(err)}` };
|
|
41250
41991
|
}
|
|
@@ -41288,10 +42029,10 @@ async function validateAtOpen(handle, timeoutMs) {
|
|
|
41288
42029
|
}
|
|
41289
42030
|
|
|
41290
42031
|
// src/lib/session-store.ts
|
|
41291
|
-
var
|
|
41292
|
-
var
|
|
41293
|
-
var
|
|
41294
|
-
var
|
|
42032
|
+
var fs15 = __toESM(require("fs"));
|
|
42033
|
+
var fsp7 = __toESM(require("fs/promises"));
|
|
42034
|
+
var os3 = __toESM(require("os"));
|
|
42035
|
+
var path14 = __toESM(require("path"));
|
|
41295
42036
|
var import_crypto6 = require("crypto");
|
|
41296
42037
|
var MCP_SESSION_MAX_DOCS = 10;
|
|
41297
42038
|
var MCP_SESSION_MAX_BYTES = 200 * 1024 * 1024;
|
|
@@ -41489,19 +42230,19 @@ function getMcpAllowedRoots() {
|
|
|
41489
42230
|
if (named.length > 0) return [...new Set(named)];
|
|
41490
42231
|
const roots = [];
|
|
41491
42232
|
try {
|
|
41492
|
-
roots.push(
|
|
42233
|
+
roots.push(path14.resolve(process.cwd(), "documents"));
|
|
41493
42234
|
} catch {
|
|
41494
42235
|
}
|
|
41495
42236
|
try {
|
|
41496
|
-
roots.push(
|
|
42237
|
+
roots.push(path14.resolve(os3.tmpdir()));
|
|
41497
42238
|
} catch {
|
|
41498
42239
|
}
|
|
41499
42240
|
try {
|
|
41500
|
-
roots.push(
|
|
42241
|
+
roots.push(path14.resolve(process.cwd(), "documents", "Drafts"));
|
|
41501
42242
|
} catch {
|
|
41502
42243
|
}
|
|
41503
42244
|
try {
|
|
41504
|
-
roots.push(
|
|
42245
|
+
roots.push(path14.resolve(os3.tmpdir(), "ASTra"));
|
|
41505
42246
|
} catch {
|
|
41506
42247
|
}
|
|
41507
42248
|
return [...new Set(roots)];
|
|
@@ -41518,12 +42259,12 @@ function getExistingExtraRoots() {
|
|
|
41518
42259
|
const raw = (process.env.ASTRA_EXTRA_DOC_ROOTS || "").trim();
|
|
41519
42260
|
if (!raw) return [];
|
|
41520
42261
|
const out = [];
|
|
41521
|
-
for (const part of raw.split(
|
|
42262
|
+
for (const part of raw.split(path14.delimiter)) {
|
|
41522
42263
|
const p = part.trim();
|
|
41523
42264
|
if (!p) continue;
|
|
41524
42265
|
try {
|
|
41525
|
-
const resolved =
|
|
41526
|
-
const stat3 =
|
|
42266
|
+
const resolved = path14.resolve(p);
|
|
42267
|
+
const stat3 = fs15.statSync(resolved);
|
|
41527
42268
|
if (!stat3.isDirectory()) continue;
|
|
41528
42269
|
out.push(resolved);
|
|
41529
42270
|
} catch {
|
|
@@ -41535,7 +42276,7 @@ function findMcpContainingRoot(canonical) {
|
|
|
41535
42276
|
for (const root of getMcpAllowedRoots()) {
|
|
41536
42277
|
let canonicalRoot = root;
|
|
41537
42278
|
try {
|
|
41538
|
-
canonicalRoot =
|
|
42279
|
+
canonicalRoot = fs15.realpathSync(root);
|
|
41539
42280
|
} catch {
|
|
41540
42281
|
}
|
|
41541
42282
|
if (isPathInsideDir(canonical, canonicalRoot) || isPathInsideDir(canonical, root)) {
|
|
@@ -41545,8 +42286,8 @@ function findMcpContainingRoot(canonical) {
|
|
|
41545
42286
|
return void 0;
|
|
41546
42287
|
}
|
|
41547
42288
|
function assertNoDotSegments(canonical, root) {
|
|
41548
|
-
const rel =
|
|
41549
|
-
const segs = rel.split(
|
|
42289
|
+
const rel = path14.relative(root, canonical);
|
|
42290
|
+
const segs = rel.split(path14.sep).filter((s) => s && s !== "." && s !== "..");
|
|
41550
42291
|
for (const seg of segs) {
|
|
41551
42292
|
if (seg.startsWith(".")) {
|
|
41552
42293
|
throw Object.assign(new Error(`Document path traverses a hidden directory "${seg}"`), {
|
|
@@ -41555,7 +42296,7 @@ function assertNoDotSegments(canonical, root) {
|
|
|
41555
42296
|
});
|
|
41556
42297
|
}
|
|
41557
42298
|
}
|
|
41558
|
-
const base =
|
|
42299
|
+
const base = path14.basename(canonical);
|
|
41559
42300
|
if (base.startsWith(".")) {
|
|
41560
42301
|
throw Object.assign(new Error("Document path must not use a hidden (dot-prefixed) file name"), {
|
|
41561
42302
|
code: "E_SANDBOX",
|
|
@@ -41596,8 +42337,8 @@ function validateDocPath(rawPath) {
|
|
|
41596
42337
|
remedy: `Use a path ${describeAllowedFolders()}.`
|
|
41597
42338
|
});
|
|
41598
42339
|
}
|
|
41599
|
-
const resolved =
|
|
41600
|
-
const resolvedSegments = resolved.split(
|
|
42340
|
+
const resolved = path14.resolve(trimmed);
|
|
42341
|
+
const resolvedSegments = resolved.split(path14.sep);
|
|
41601
42342
|
if (resolvedSegments.includes("..")) {
|
|
41602
42343
|
throw Object.assign(new Error("Document path resolves outside its directory"), {
|
|
41603
42344
|
code: "E_SANDBOX",
|
|
@@ -41616,7 +42357,7 @@ function validateDocPath(rawPath) {
|
|
|
41616
42357
|
);
|
|
41617
42358
|
}
|
|
41618
42359
|
assertNoDotSegments(canonical, containingRoot);
|
|
41619
|
-
const ext =
|
|
42360
|
+
const ext = path14.extname(trimmed).toLowerCase();
|
|
41620
42361
|
if (ext !== ".docx") {
|
|
41621
42362
|
throw Object.assign(new Error(`Document path must use a .docx extension (got "${ext || "(none)"}")`), {
|
|
41622
42363
|
code: "E_VALIDATION",
|
|
@@ -41638,9 +42379,9 @@ function validateOpenBufferLabel(rawLabel) {
|
|
|
41638
42379
|
remedy: "Pass a plain memory label such as memory.docx."
|
|
41639
42380
|
});
|
|
41640
42381
|
}
|
|
41641
|
-
if (
|
|
42382
|
+
if (path14.isAbsolute(trimmed)) return validateDocPath(trimmed);
|
|
41642
42383
|
if (trimmed.includes("\n") || trimmed.includes("\r")) return fallback;
|
|
41643
|
-
const base =
|
|
42384
|
+
const base = path14.basename(trimmed.replace(/\\/g, "/"));
|
|
41644
42385
|
if (!base || base === "." || base === "..") return fallback;
|
|
41645
42386
|
const clean = base.slice(0, 128);
|
|
41646
42387
|
return clean.toLowerCase().endsWith(".docx") ? clean : fallback;
|
|
@@ -41654,29 +42395,141 @@ function collectTrackedExtrasForGit(parser2) {
|
|
|
41654
42395
|
}
|
|
41655
42396
|
return extras;
|
|
41656
42397
|
} catch (err) {
|
|
41657
|
-
|
|
41658
|
-
console.error(`[collectTrackedExtrasForGit] snapshot failed: ${msg}`);
|
|
41659
|
-
return {};
|
|
42398
|
+
throw snapshotCollectionError(err);
|
|
41660
42399
|
}
|
|
41661
42400
|
}
|
|
41662
|
-
|
|
42401
|
+
function snapshotCollectionError(cause) {
|
|
42402
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
42403
|
+
return Object.assign(new Error(`The document's package parts could not be collected for history (${detail}). Nothing was recorded.`), {
|
|
42404
|
+
code: "E_STATE",
|
|
42405
|
+
remedy: "Nothing was recorded in history. Retry; if it keeps failing, the document package may be damaged."
|
|
42406
|
+
});
|
|
42407
|
+
}
|
|
42408
|
+
async function ensureGitRepo(docPath, xml, extras, originalBytes) {
|
|
42409
|
+
return (await ensureGitRepoWith(docPath, () => ({ xml, extras }), originalBytes)).existed;
|
|
42410
|
+
}
|
|
42411
|
+
async function ensureGitRepoWith(docPath, baseline, originalBytes, options) {
|
|
41663
42412
|
const repoDir = getRepoDir(docPath);
|
|
41664
42413
|
let hasGit = false;
|
|
41665
42414
|
try {
|
|
41666
|
-
hasGit =
|
|
42415
|
+
hasGit = fs15.existsSync(path14.join(repoDir, ".git"));
|
|
41667
42416
|
} catch {
|
|
41668
42417
|
hasGit = false;
|
|
41669
42418
|
}
|
|
41670
|
-
if (hasGit)
|
|
41671
|
-
|
|
41672
|
-
|
|
42419
|
+
if (hasGit) {
|
|
42420
|
+
if (!options?.alreadyReady) await assertGitHistoryReady(repoDir);
|
|
42421
|
+
return { existed: true };
|
|
42422
|
+
}
|
|
42423
|
+
try {
|
|
42424
|
+
const initial = baseline();
|
|
42425
|
+
await initAstraRepo(docPath, initial.xml, initial.extras, originalBytes);
|
|
42426
|
+
return { existed: false, baseline: initial };
|
|
42427
|
+
} catch (error) {
|
|
42428
|
+
if (fs15.existsSync(path14.join(repoDir, ".git"))) {
|
|
42429
|
+
await assertGitHistoryReady(repoDir);
|
|
42430
|
+
return { existed: true };
|
|
42431
|
+
}
|
|
42432
|
+
throw error;
|
|
42433
|
+
}
|
|
41673
42434
|
}
|
|
41674
|
-
|
|
42435
|
+
function collectBaselineForGit(parser2) {
|
|
42436
|
+
const xml = parser2.readDocumentXml();
|
|
41675
42437
|
try {
|
|
41676
|
-
|
|
41677
|
-
const
|
|
41678
|
-
|
|
41679
|
-
|
|
42438
|
+
const snapshot3 = parser2.capturePackageSnapshot({ documentXml: xml });
|
|
42439
|
+
const extras = {};
|
|
42440
|
+
for (const [name2, buf] of snapshot3) if (isTrackedPackageFile(name2)) extras[name2] = buf;
|
|
42441
|
+
return { xml, extras };
|
|
42442
|
+
} catch (err) {
|
|
42443
|
+
throw snapshotCollectionError(err);
|
|
42444
|
+
}
|
|
42445
|
+
}
|
|
42446
|
+
var sha256Of = (bytes) => (0, import_crypto6.createHash)("sha256").update(bytes).digest("hex");
|
|
42447
|
+
async function resolveSessionOriginal(handle) {
|
|
42448
|
+
const fallback = handle.originalInfo ?? { available: false, reason: "not-applicable" };
|
|
42449
|
+
const pin = handle.originalPin;
|
|
42450
|
+
if (!pin || !path14.isAbsolute(handle.filePath)) return fallback;
|
|
42451
|
+
const current = await getOriginalInfo(handle.filePath);
|
|
42452
|
+
if (pin.kind === "retained") {
|
|
42453
|
+
if (!current.available) return current;
|
|
42454
|
+
if (current.sha256 !== pin.sha256) {
|
|
42455
|
+
return { available: false, reason: "capture-failed", detail: "The retained original no longer matches the one this session opened." };
|
|
42456
|
+
}
|
|
42457
|
+
return current;
|
|
42458
|
+
}
|
|
42459
|
+
if (current.available && current.sha256 !== pin.sha256) {
|
|
42460
|
+
return { available: false, reason: "capture-failed", detail: "Another opener recorded a different original for this history." };
|
|
42461
|
+
}
|
|
42462
|
+
const initialization = await getHistoryInitializationState(handle.filePath);
|
|
42463
|
+
if (initialization.state !== "complete") {
|
|
42464
|
+
return { available: false, reason: "initializing", detail: initialization.detail ?? "History initialization has not completed. Check get_document_history again later." };
|
|
42465
|
+
}
|
|
42466
|
+
if (!current.available) return current;
|
|
42467
|
+
handle.originalInfo = current;
|
|
42468
|
+
handle.originalPin = { kind: "retained", sha256: current.sha256 };
|
|
42469
|
+
handle.originalWarning = void 0;
|
|
42470
|
+
return current;
|
|
42471
|
+
}
|
|
42472
|
+
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.";
|
|
42473
|
+
async function initGitForHandleBestEffort(handle, originalBytes) {
|
|
42474
|
+
try {
|
|
42475
|
+
if (!path14.isAbsolute(handle.filePath)) return;
|
|
42476
|
+
const repoDir = getRepoDir(handle.filePath);
|
|
42477
|
+
let completeBeforeRead = false;
|
|
42478
|
+
if (fs15.existsSync(path14.join(repoDir, ".git"))) {
|
|
42479
|
+
try {
|
|
42480
|
+
await assertGitHistoryReady(repoDir);
|
|
42481
|
+
completeBeforeRead = true;
|
|
42482
|
+
} catch {
|
|
42483
|
+
}
|
|
42484
|
+
}
|
|
42485
|
+
const availabilityAtOpen = await getOriginalInfo(handle.filePath);
|
|
42486
|
+
const ensured = await ensureGitRepoWith(handle.filePath, () => collectBaselineForGit(handle.parser), originalBytes, { alreadyReady: completeBeforeRead });
|
|
42487
|
+
if (ensured.existed) {
|
|
42488
|
+
const preExisting = completeBeforeRead && availabilityAtOpen.available;
|
|
42489
|
+
const now = preExisting ? availabilityAtOpen : await getOriginalInfo(handle.filePath);
|
|
42490
|
+
handle.originalInfo = now;
|
|
42491
|
+
if (now.available) {
|
|
42492
|
+
if (preExisting || originalBytes && sha256Of(originalBytes) === now.sha256) {
|
|
42493
|
+
handle.originalPin = { kind: "retained", sha256: now.sha256 };
|
|
42494
|
+
} else {
|
|
42495
|
+
handle.originalInfo = { available: false, reason: "capture-failed", detail: "Another opener recorded a different original for this history." };
|
|
42496
|
+
}
|
|
42497
|
+
} else if (originalBytes && now.reason === "incomplete") {
|
|
42498
|
+
handle.originalInfo = { available: false, reason: "initializing", detail: "The history was still being initialized when this session opened." };
|
|
42499
|
+
handle.originalPin = { kind: "input", sha256: sha256Of(originalBytes) };
|
|
42500
|
+
handle.originalWarning = INITIALIZING_WARNING;
|
|
42501
|
+
}
|
|
42502
|
+
} else {
|
|
42503
|
+
handle.originalInfo = await getOriginalInfo(handle.filePath);
|
|
42504
|
+
const recorded = ensured.baseline;
|
|
42505
|
+
const history = await getHistory(handle.filePath);
|
|
42506
|
+
const head = history[0] ? await checkoutCommit(handle.filePath, history[0].oid) : void 0;
|
|
42507
|
+
const names = Object.keys(recorded.extras).sort();
|
|
42508
|
+
const headNames = head ? [...head.extra.keys()].sort() : [];
|
|
42509
|
+
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)));
|
|
42510
|
+
if (!initialStateMatches && handle.originalInfo.available) {
|
|
42511
|
+
handle.originalInfo = { available: false, reason: "capture-failed", detail: "Concurrent history initialization produced a different initial document state. Close and reopen after inspecting history." };
|
|
42512
|
+
}
|
|
42513
|
+
if (originalBytes && handle.originalInfo.available && handle.originalInfo.sha256 !== sha256Of(originalBytes)) {
|
|
42514
|
+
handle.originalInfo = { available: false, reason: "capture-failed", detail: "Another opener recorded a different original for this history." };
|
|
42515
|
+
}
|
|
42516
|
+
if (handle.originalInfo.available) handle.originalPin = { kind: "retained", sha256: handle.originalInfo.sha256 };
|
|
42517
|
+
}
|
|
42518
|
+
if (!handle.originalInfo.available && !["predates-feature", "not-applicable"].includes(handle.originalInfo.reason)) {
|
|
42519
|
+
handle.originalWarning = handle.originalInfo.reason === "initializing" ? INITIALIZING_WARNING : `Exact-original recovery is unavailable: ${handle.originalInfo.reason}. Keep a separate copy of the input file.`;
|
|
42520
|
+
}
|
|
42521
|
+
} catch (error) {
|
|
42522
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
42523
|
+
const mismatch = error?.reason === "original-mismatch";
|
|
42524
|
+
const initialization = originalBytes && !mismatch && path14.isAbsolute(handle.filePath) ? await getHistoryInitializationState(handle.filePath) : void 0;
|
|
42525
|
+
if (initialization && originalBytes && initialization.state !== "absent") {
|
|
42526
|
+
handle.originalInfo = { available: false, reason: "initializing", detail: initialization.detail ?? detail };
|
|
42527
|
+
handle.originalPin = { kind: "input", sha256: sha256Of(originalBytes) };
|
|
42528
|
+
handle.originalWarning = INITIALIZING_WARNING;
|
|
42529
|
+
} else {
|
|
42530
|
+
handle.originalInfo = mismatch ? { available: false, reason: "capture-failed", detail } : { available: false, reason: "incomplete", detail };
|
|
42531
|
+
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.`;
|
|
42532
|
+
}
|
|
41680
42533
|
}
|
|
41681
42534
|
}
|
|
41682
42535
|
var McpSessionStore = class {
|
|
@@ -41926,12 +42779,12 @@ var McpSessionStore = class {
|
|
|
41926
42779
|
* Macro/active content refuses by default (E_VALIDATION naming the parts);
|
|
41927
42780
|
* pass {allowMacros:true} to preserve or {stripMacros:true} to strip.
|
|
41928
42781
|
*/
|
|
41929
|
-
async openDocument(filePath, sessionId, macroOpts) {
|
|
42782
|
+
async openDocument(filePath, sessionId, macroOpts, captureInput = true) {
|
|
41930
42783
|
const cleanPath = validateDocPath(filePath);
|
|
41931
42784
|
const expiredEvicted = this.evictExpired();
|
|
41932
42785
|
let buf;
|
|
41933
42786
|
try {
|
|
41934
|
-
buf = await
|
|
42787
|
+
buf = await fsp7.readFile(cleanPath);
|
|
41935
42788
|
} catch (err) {
|
|
41936
42789
|
const msg = err instanceof Error ? err.message : String(err);
|
|
41937
42790
|
throw Object.assign(new Error(`open_document: cannot read "${cleanPath}": ${msg}`), {
|
|
@@ -41966,10 +42819,12 @@ var McpSessionStore = class {
|
|
|
41966
42819
|
lastAccess: Date.now(),
|
|
41967
42820
|
mutex: new SessionMutex(),
|
|
41968
42821
|
redoStack: this.createRedoStack(),
|
|
41969
|
-
pendingAudit: { ops: 0, nodeIds: [] }
|
|
42822
|
+
pendingAudit: { ops: 0, nodeIds: [] },
|
|
42823
|
+
lastWrittenSha256: (0, import_crypto6.createHash)("sha256").update(buf).digest("hex"),
|
|
42824
|
+
macroPolicy: macroOpts
|
|
41970
42825
|
};
|
|
41971
42826
|
this.sessions.set(id, handle);
|
|
41972
|
-
await initGitForHandleBestEffort(handle);
|
|
42827
|
+
await initGitForHandleBestEffort(handle, captureInput ? buf : void 0);
|
|
41973
42828
|
this.lastOpenEvicted = [...expiredEvicted, ...this.evictToFit(id)];
|
|
41974
42829
|
const live = this.sessions.get(id);
|
|
41975
42830
|
if (!live) {
|
|
@@ -42036,10 +42891,13 @@ var McpSessionStore = class {
|
|
|
42036
42891
|
lastAccess: Date.now(),
|
|
42037
42892
|
mutex: new SessionMutex(),
|
|
42038
42893
|
redoStack: this.createRedoStack(),
|
|
42039
|
-
pendingAudit: { ops: 0, nodeIds: [] }
|
|
42894
|
+
pendingAudit: { ops: 0, nodeIds: [] },
|
|
42895
|
+
lastWrittenSha256: (0, import_crypto6.createHash)("sha256").update(buf).digest("hex"),
|
|
42896
|
+
macroPolicy: macroOpts
|
|
42040
42897
|
};
|
|
42041
42898
|
this.sessions.set(id, handle);
|
|
42042
|
-
await initGitForHandleBestEffort(handle);
|
|
42899
|
+
await initGitForHandleBestEffort(handle, buf);
|
|
42900
|
+
if (!path14.isAbsolute(handle.filePath)) handle.originalInfo = { available: false, reason: "not-applicable" };
|
|
42043
42901
|
this.lastOpenEvicted = [...expiredEvicted, ...this.evictToFit(id)];
|
|
42044
42902
|
const live = this.sessions.get(id);
|
|
42045
42903
|
if (!live) {
|
|
@@ -42214,6 +43072,8 @@ var LEAN6_TOOL_NAMES = [
|
|
|
42214
43072
|
];
|
|
42215
43073
|
var GIT_HISTORY_TOOL_NAMES = [
|
|
42216
43074
|
"get_document_history",
|
|
43075
|
+
"export_original_document",
|
|
43076
|
+
"restore_original_document",
|
|
42217
43077
|
"preview_revert",
|
|
42218
43078
|
"revert_document",
|
|
42219
43079
|
"redo_document"
|
|
@@ -42261,8 +43121,10 @@ var LEAN6_DESCRIPTIONS = {
|
|
|
42261
43121
|
)
|
|
42262
43122
|
};
|
|
42263
43123
|
var GIT_HISTORY_DESCRIPTIONS = {
|
|
43124
|
+
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.",
|
|
43125
|
+
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.",
|
|
42264
43126
|
get_document_history: withSharedDocs(
|
|
42265
|
-
'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. '
|
|
43127
|
+
'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. '
|
|
42266
43128
|
),
|
|
42267
43129
|
preview_revert: withSharedDocs(
|
|
42268
43130
|
`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.`
|
|
@@ -42451,6 +43313,15 @@ var ImportDocumentSchema = import_zod4.z.object({
|
|
|
42451
43313
|
// Read $...$ as inline math (off by default: prices and shell text can look like math).
|
|
42452
43314
|
dollarMath: import_zod4.z.boolean().optional()
|
|
42453
43315
|
}).strict();
|
|
43316
|
+
var ExportOriginalDocumentSchema = import_zod4.z.object({
|
|
43317
|
+
sessionId: import_zod4.z.string().min(1).max(128),
|
|
43318
|
+
outPath: import_zod4.z.string().min(1).max(1024),
|
|
43319
|
+
overwrite: import_zod4.z.boolean().optional()
|
|
43320
|
+
}).strict();
|
|
43321
|
+
var RestoreOriginalDocumentSchema = import_zod4.z.object({
|
|
43322
|
+
sessionId: import_zod4.z.string().min(1).max(128),
|
|
43323
|
+
discard: import_zod4.z.boolean().optional()
|
|
43324
|
+
}).strict();
|
|
42454
43325
|
|
|
42455
43326
|
// src/lib/mcp/handlers/open-inspect.ts
|
|
42456
43327
|
function extractStylePalette2(parser2) {
|
|
@@ -42544,7 +43415,7 @@ async function handleOpenDocument(store, input) {
|
|
|
42544
43415
|
const evicted = store.drainLastOpenEvicted();
|
|
42545
43416
|
const pendingNormalizations = readPendingNormalizations(handle);
|
|
42546
43417
|
const validation = await validateAtOpen(handle, parsed.data.validationTimeoutMs ?? openValidationTimeoutMs());
|
|
42547
|
-
return { ok: true, sessionId: handle.sessionId, revision: handle.revision, summary, evicted, pendingNormalizations, validation };
|
|
43418
|
+
return { ok: true, sessionId: handle.sessionId, revision: handle.revision, summary, evicted, pendingNormalizations, validation, original: handle.originalInfo, ...handle.originalWarning ? { originalWarning: handle.originalWarning } : {} };
|
|
42548
43419
|
} catch (err) {
|
|
42549
43420
|
return mcpErrorFrom(err, "E_IO", "Check the .docx path is readable and retry open_document.");
|
|
42550
43421
|
}
|
|
@@ -42769,11 +43640,11 @@ async function handleInspectDocument(store, input) {
|
|
|
42769
43640
|
}
|
|
42770
43641
|
|
|
42771
43642
|
// src/lib/mcp/handlers/search-batch.ts
|
|
42772
|
-
var
|
|
43643
|
+
var path16 = __toESM(require("path"));
|
|
42773
43644
|
|
|
42774
43645
|
// src/tools/compose-document.ts
|
|
42775
|
-
var
|
|
42776
|
-
var
|
|
43646
|
+
var fs16 = __toESM(require("fs"));
|
|
43647
|
+
var path15 = __toESM(require("path"));
|
|
42777
43648
|
var import_zod5 = require("zod");
|
|
42778
43649
|
var COMPOSE_MARKDOWN_MAX = 5e5;
|
|
42779
43650
|
var COMPOSE_VALIDATION_ERRORS_MAX = 3;
|
|
@@ -42788,8 +43659,8 @@ function resolveMcpStyleGuide(ref, jail) {
|
|
|
42788
43659
|
let anchor;
|
|
42789
43660
|
if (jail?.allowedDir !== void 0) {
|
|
42790
43661
|
anchor = jail.allowedDir;
|
|
42791
|
-
} else if (jail?.sessionFilePath !== void 0 &&
|
|
42792
|
-
anchor = canonicalizeForAsset(
|
|
43662
|
+
} else if (jail?.sessionFilePath !== void 0 && path15.isAbsolute(jail.sessionFilePath)) {
|
|
43663
|
+
anchor = canonicalizeForAsset(path15.resolve(path15.dirname(jail.sessionFilePath)));
|
|
42793
43664
|
}
|
|
42794
43665
|
if (anchor === void 0 && !hasGuideScheme(trimmed) && isFileGuideRef(trimmed)) {
|
|
42795
43666
|
throw guideSandboxError(
|
|
@@ -42888,22 +43759,22 @@ function assertPathAllowed(allowedDir, roots) {
|
|
|
42888
43759
|
remedy: `Use a folder ${describeAllowedFolders()}.`
|
|
42889
43760
|
});
|
|
42890
43761
|
}
|
|
42891
|
-
const resolved =
|
|
43762
|
+
const resolved = path15.resolve(trimmed);
|
|
42892
43763
|
const canonical = canonicalizeForAsset(resolved);
|
|
42893
43764
|
const inside = roots.some((root) => {
|
|
42894
43765
|
let canonicalRoot = root;
|
|
42895
43766
|
try {
|
|
42896
|
-
canonicalRoot =
|
|
43767
|
+
canonicalRoot = fs16.realpathSync(root);
|
|
42897
43768
|
} catch {
|
|
42898
43769
|
}
|
|
42899
|
-
const normC =
|
|
42900
|
-
const normR =
|
|
43770
|
+
const normC = path15.normalize(canonical);
|
|
43771
|
+
const normR = path15.normalize(canonicalRoot);
|
|
42901
43772
|
if (process.platform === "win32") {
|
|
42902
43773
|
const lc = normC.toLowerCase();
|
|
42903
43774
|
const lr = normR.toLowerCase();
|
|
42904
|
-
return lc === lr || lc.startsWith(lr +
|
|
43775
|
+
return lc === lr || lc.startsWith(lr + path15.sep.toLowerCase());
|
|
42905
43776
|
}
|
|
42906
|
-
return normC === normR || normC.startsWith(normR +
|
|
43777
|
+
return normC === normR || normC.startsWith(normR + path15.sep);
|
|
42907
43778
|
});
|
|
42908
43779
|
if (!inside) {
|
|
42909
43780
|
throw Object.assign(
|
|
@@ -43281,8 +44152,8 @@ async function handleExecuteBatch(store, input) {
|
|
|
43281
44152
|
const explicit = parsed.data.allowedDir;
|
|
43282
44153
|
if (explicit !== void 0) {
|
|
43283
44154
|
batchAllowedDir = assertPathAllowed(explicit, getMcpAllowedRoots());
|
|
43284
|
-
} else if (
|
|
43285
|
-
batchAllowedDir = canonicalizeForAsset(
|
|
44155
|
+
} else if (path16.isAbsolute(handle.filePath)) {
|
|
44156
|
+
batchAllowedDir = canonicalizeForAsset(path16.resolve(path16.dirname(handle.filePath)));
|
|
43286
44157
|
} else {
|
|
43287
44158
|
throw Object.assign(
|
|
43288
44159
|
new Error(
|
|
@@ -43431,8 +44302,9 @@ async function handleExecuteBatch(store, input) {
|
|
|
43431
44302
|
}
|
|
43432
44303
|
|
|
43433
44304
|
// src/lib/mcp/handlers/save-close.ts
|
|
43434
|
-
var
|
|
43435
|
-
var
|
|
44305
|
+
var import_crypto7 = require("crypto");
|
|
44306
|
+
var fsp8 = __toESM(require("fs/promises"));
|
|
44307
|
+
var path17 = __toESM(require("path"));
|
|
43436
44308
|
async function executeSaveDocument(store, input) {
|
|
43437
44309
|
const parsed = SaveDocumentSchema.safeParse(input);
|
|
43438
44310
|
if (!parsed.success) {
|
|
@@ -43478,18 +44350,18 @@ async function executeSaveDocument(store, input) {
|
|
|
43478
44350
|
),
|
|
43479
44351
|
{
|
|
43480
44352
|
code: "E_VALIDATION",
|
|
43481
|
-
remedy: `name the output "${
|
|
44353
|
+
remedy: `name the output "${path17.basename(target)}" with a .docm extension, or add stripMacros: true to remove the macro parts`
|
|
43482
44354
|
}
|
|
43483
44355
|
);
|
|
43484
44356
|
}
|
|
43485
44357
|
if (isStripped && macroParts.length === 0 && /\.(docm|dotm)$/i.test(target)) {
|
|
43486
44358
|
throw Object.assign(
|
|
43487
44359
|
new Error(
|
|
43488
|
-
`E_VALIDATION: save_document: stripMacros removes the macro parts, so the result is a plain document and cannot be written to "${
|
|
44360
|
+
`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`
|
|
43489
44361
|
),
|
|
43490
44362
|
{
|
|
43491
44363
|
code: "E_VALIDATION",
|
|
43492
|
-
remedy: `write the result to a .docx with outPath: "${
|
|
44364
|
+
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`
|
|
43493
44365
|
}
|
|
43494
44366
|
);
|
|
43495
44367
|
}
|
|
@@ -43530,19 +44402,22 @@ async function executeSaveDocument(store, input) {
|
|
|
43530
44402
|
const saveGateNormalized = readParserNormalizations(handle.parser);
|
|
43531
44403
|
const buf = handle.parser.rezip();
|
|
43532
44404
|
const saveNormalized = mergeNormalized(saveGateNormalized, readParserNormalizations(handle.parser));
|
|
44405
|
+
const savedXml = handle.parser.readDocumentXml();
|
|
44406
|
+
const extras = collectTrackedExtrasForGit(handle.parser);
|
|
43533
44407
|
const prevRevision = handle.revision;
|
|
43534
44408
|
const prevSavedRevision = handle.savedRevision;
|
|
43535
44409
|
const prevByteSize = handle.byteSize;
|
|
43536
44410
|
const prevFilePath = handle.filePath;
|
|
43537
44411
|
let prevBytes = null;
|
|
43538
44412
|
try {
|
|
43539
|
-
prevBytes = await
|
|
44413
|
+
prevBytes = await fsp8.readFile(target);
|
|
43540
44414
|
} catch {
|
|
43541
44415
|
prevBytes = null;
|
|
43542
44416
|
}
|
|
44417
|
+
await assertHistoryReadyForWrite(target);
|
|
43543
44418
|
try {
|
|
43544
44419
|
assertWriteTargetNoSymlink(target);
|
|
43545
|
-
await
|
|
44420
|
+
await fsp8.mkdir(pathDirname(target), { recursive: true });
|
|
43546
44421
|
assertWriteTargetNoSymlink(target);
|
|
43547
44422
|
await writeFileNoFollow(target, buf);
|
|
43548
44423
|
} catch (err) {
|
|
@@ -43553,7 +44428,7 @@ async function executeSaveDocument(store, input) {
|
|
|
43553
44428
|
if (prevBytes !== null) {
|
|
43554
44429
|
await writeFileNoFollow(target, prevBytes);
|
|
43555
44430
|
} else {
|
|
43556
|
-
await
|
|
44431
|
+
await fsp8.unlink(target);
|
|
43557
44432
|
}
|
|
43558
44433
|
} catch (restoreErr) {
|
|
43559
44434
|
diskOk = false;
|
|
@@ -43565,8 +44440,6 @@ async function executeSaveDocument(store, input) {
|
|
|
43565
44440
|
remedy
|
|
43566
44441
|
});
|
|
43567
44442
|
}
|
|
43568
|
-
const savedXml = handle.parser.readDocumentXml();
|
|
43569
|
-
const extras = collectTrackedExtrasForGit(handle.parser);
|
|
43570
44443
|
const opsSinceSave = handle.revision - handle.savedRevision;
|
|
43571
44444
|
const fallbackMessage = opsSinceSave <= 0 ? `save_document: rev ${handle.revision} (initial baseline)` : `save_document: rev ${handle.revision} (${opsSinceSave} ops since last save)`;
|
|
43572
44445
|
const baseMessage = sanitizeCommitMessage(parsed.data.message, fallbackMessage);
|
|
@@ -43574,7 +44447,7 @@ async function executeSaveDocument(store, input) {
|
|
|
43574
44447
|
const authorName = sanitizeCommitAuthor(parsed.data.author);
|
|
43575
44448
|
let commitOid;
|
|
43576
44449
|
try {
|
|
43577
|
-
await ensureGitRepo(target, savedXml, extras);
|
|
44450
|
+
await ensureGitRepo(target, savedXml, extras, prevBytes ?? void 0);
|
|
43578
44451
|
commitOid = await commitChange(target, savedXml, commitMessage, extras, void 0, authorName);
|
|
43579
44452
|
} catch (err) {
|
|
43580
44453
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -43583,7 +44456,7 @@ async function executeSaveDocument(store, input) {
|
|
|
43583
44456
|
if (prevBytes !== null) {
|
|
43584
44457
|
await writeFileNoFollow(target, prevBytes);
|
|
43585
44458
|
} else {
|
|
43586
|
-
await
|
|
44459
|
+
await fsp8.unlink(target);
|
|
43587
44460
|
}
|
|
43588
44461
|
} catch (restoreErr) {
|
|
43589
44462
|
diskOk = false;
|
|
@@ -43604,6 +44477,15 @@ async function executeSaveDocument(store, input) {
|
|
|
43604
44477
|
}
|
|
43605
44478
|
try {
|
|
43606
44479
|
handle.filePath = target;
|
|
44480
|
+
if (prevFilePath !== target || !handle.originalInfo) {
|
|
44481
|
+
handle.originalInfo = await getOriginalInfo(target);
|
|
44482
|
+
handle.originalPin = handle.originalInfo.available ? { kind: "retained", sha256: handle.originalInfo.sha256 } : void 0;
|
|
44483
|
+
}
|
|
44484
|
+
if (prevFilePath !== target) handle.originalWarning = void 0;
|
|
44485
|
+
if (!handle.originalInfo.available && !["predates-feature", "not-applicable"].includes(handle.originalInfo.reason)) {
|
|
44486
|
+
handle.originalWarning = `Exact-original recovery is unavailable: ${handle.originalInfo.reason}. Keep a separate copy of the input file.`;
|
|
44487
|
+
}
|
|
44488
|
+
handle.lastWrittenSha256 = (0, import_crypto7.createHash)("sha256").update(buf).digest("hex");
|
|
43607
44489
|
handle.byteSize = buf.length;
|
|
43608
44490
|
const auditNodeIds = [...handle.pendingAudit.nodeIds];
|
|
43609
44491
|
const auditOps = handle.pendingAudit.ops;
|
|
@@ -43641,6 +44523,7 @@ async function executeSaveDocument(store, input) {
|
|
|
43641
44523
|
ok: true,
|
|
43642
44524
|
sessionId: handle.sessionId,
|
|
43643
44525
|
path: target,
|
|
44526
|
+
...handle.originalWarning ? { originalWarning: handle.originalWarning } : {},
|
|
43644
44527
|
revision: handle.revision,
|
|
43645
44528
|
commitOid,
|
|
43646
44529
|
commitMessage,
|
|
@@ -43710,8 +44593,151 @@ async function handleCloseDocument(store, input) {
|
|
|
43710
44593
|
}
|
|
43711
44594
|
}
|
|
43712
44595
|
|
|
44596
|
+
// src/lib/mcp/handlers/original.ts
|
|
44597
|
+
var fsp9 = __toESM(require("fs/promises"));
|
|
44598
|
+
var path18 = __toESM(require("path"));
|
|
44599
|
+
var import_crypto8 = require("crypto");
|
|
44600
|
+
var hash = (buf) => (0, import_crypto8.createHash)("sha256").update(buf).digest("hex");
|
|
44601
|
+
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.") {
|
|
44602
|
+
throw Object.assign(new Error(message), { code, remedy });
|
|
44603
|
+
}
|
|
44604
|
+
async function requireAvailableOriginal(handle) {
|
|
44605
|
+
const info = await resolveSessionOriginal(handle);
|
|
44606
|
+
if (info.available) return;
|
|
44607
|
+
const detail = info.detail ?? "Close and reopen after checking history.";
|
|
44608
|
+
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.";
|
|
44609
|
+
refuse2("E_CONFLICT", `Exact-original recovery is unavailable to this session (${info.reason}). ${detail}`, remedy);
|
|
44610
|
+
}
|
|
44611
|
+
async function handleExportOriginalDocument(store, input) {
|
|
44612
|
+
const parsed = ExportOriginalDocumentSchema.safeParse(input);
|
|
44613
|
+
if (!parsed.success) return invalidInput(`export_original_document: ${parsed.error.message}`);
|
|
44614
|
+
try {
|
|
44615
|
+
return await store.withSession(parsed.data.sessionId, async (handle) => {
|
|
44616
|
+
if (!path18.isAbsolute(handle.filePath)) refuse2("E_VALIDATION", "This memory session has no retained original.");
|
|
44617
|
+
await requireAvailableOriginal(handle);
|
|
44618
|
+
const outPath = validateSaveTarget(parsed.data.outPath);
|
|
44619
|
+
if (outPath === handle.filePath) refuse2("E_CONFLICT", "Use restore_original_document to replace the active document.");
|
|
44620
|
+
const original = await exportOriginal(handle.filePath, outPath, { overwrite: parsed.data.overwrite === true });
|
|
44621
|
+
return { ok: true, sessionId: handle.sessionId, path: outPath, original };
|
|
44622
|
+
});
|
|
44623
|
+
} catch (err) {
|
|
44624
|
+
return mcpErrorFrom(err, "E_IO", "Choose a writable output path inside the allowed directory.");
|
|
44625
|
+
}
|
|
44626
|
+
}
|
|
44627
|
+
async function confirmHistoryCurrent(handle, bytes) {
|
|
44628
|
+
const history = await getHistory(handle.filePath);
|
|
44629
|
+
if (!history[0]) refuse2("E_CONFLICT", "Cannot confirm history is current: there is no recorded head.");
|
|
44630
|
+
const head = await checkoutCommit(handle.filePath, history[0].oid);
|
|
44631
|
+
if (!head.complete) refuse2("E_CONFLICT", "Cannot confirm history is current: the head is not a complete package snapshot. Save the document first.");
|
|
44632
|
+
const parser2 = new DocxParser(bytes, handle.macroPolicy);
|
|
44633
|
+
await parser2.getAST();
|
|
44634
|
+
const snapshot3 = parser2.capturePackageSnapshot();
|
|
44635
|
+
if (parser2.readDocumentXml() !== head.xml) refuse2("E_CONFLICT", "History differs from the current file. Save or revert before restoring the original.");
|
|
44636
|
+
const names = [...snapshot3.keys()].filter(isTrackedPackageFile).sort();
|
|
44637
|
+
const recorded = [...head.extra.keys()].sort();
|
|
44638
|
+
if (JSON.stringify(names) !== JSON.stringify(recorded) || names.some((name2) => !snapshot3.get(name2).equals(head.extra.get(name2)))) {
|
|
44639
|
+
refuse2("E_CONFLICT", "History is behind the file or the file was modified outside ASTra. Save or revert before restoring the original.");
|
|
44640
|
+
}
|
|
44641
|
+
return { oid: history[0].oid, parser: parser2 };
|
|
44642
|
+
}
|
|
44643
|
+
async function handleRestoreOriginalDocument(store, input) {
|
|
44644
|
+
const parsed = RestoreOriginalDocumentSchema.safeParse(input);
|
|
44645
|
+
if (!parsed.success) return invalidInput(`restore_original_document: ${parsed.error.message}`);
|
|
44646
|
+
try {
|
|
44647
|
+
return await store.withSession(parsed.data.sessionId, async (handle) => {
|
|
44648
|
+
if (!path18.isAbsolute(handle.filePath)) refuse2("E_VALIDATION", "This memory session has no retained original.");
|
|
44649
|
+
await requireAvailableOriginal(handle);
|
|
44650
|
+
if (handle.revision !== handle.savedRevision && !parsed.data.discard) refuse2("E_CONFLICT", "The session has unsaved edits. Save first, or explicitly pass discard:true.");
|
|
44651
|
+
const source = validateSaveTarget(handle.filePath);
|
|
44652
|
+
await assertHistoryReadyForWrite(source);
|
|
44653
|
+
const original = await getOriginalInfo(source);
|
|
44654
|
+
if (!original.available) refuse2("E_NOT_FOUND", `The original is unavailable: ${original.reason}.`);
|
|
44655
|
+
const bytes = await readOriginal(source);
|
|
44656
|
+
const previous = await fsp9.readFile(source);
|
|
44657
|
+
if (!handle.lastWrittenSha256 || hash(previous) !== handle.lastWrittenSha256) refuse2("E_CONFLICT", "The file changed outside ASTra. Reopen it before restoring.");
|
|
44658
|
+
const checked = await confirmHistoryCurrent(handle, previous);
|
|
44659
|
+
const parser2 = new DocxParser(bytes, handle.macroPolicy);
|
|
44660
|
+
const ast = await parser2.getAST();
|
|
44661
|
+
const engine = new OperationEngine(ast, parser2);
|
|
44662
|
+
if (handle.styleGuide) engine.setStyleGuide(handle.styleGuide);
|
|
44663
|
+
const validation = await validateForBaseline(bytes);
|
|
44664
|
+
const backup = newRestoreBackupPath(source);
|
|
44665
|
+
await createVerifiedBackup(backup, previous);
|
|
44666
|
+
const historyBefore = await captureHistoryState(source);
|
|
44667
|
+
const oldRedo = Array.from(handle.redoStack);
|
|
44668
|
+
const previousSession = {
|
|
44669
|
+
parser: handle.parser,
|
|
44670
|
+
ast: handle.ast,
|
|
44671
|
+
engine: handle.engine,
|
|
44672
|
+
toon: handle.toon,
|
|
44673
|
+
revision: handle.revision,
|
|
44674
|
+
savedRevision: handle.savedRevision,
|
|
44675
|
+
byteSize: handle.byteSize,
|
|
44676
|
+
lastWrittenSha256: handle.lastWrittenSha256,
|
|
44677
|
+
validationBaseline: handle.validationBaseline,
|
|
44678
|
+
pendingAudit: { ops: handle.pendingAudit.ops, nodeIds: [...handle.pendingAudit.nodeIds] }
|
|
44679
|
+
};
|
|
44680
|
+
let replaced = false;
|
|
44681
|
+
let commitAttempted = false;
|
|
44682
|
+
let commitOid;
|
|
44683
|
+
let durabilityUnconfirmed = false;
|
|
44684
|
+
try {
|
|
44685
|
+
handle.redoStack.length = 0;
|
|
44686
|
+
try {
|
|
44687
|
+
handle.redoStack.push((parsed.data.discard ? checked.parser : handle.parser).capturePackageSnapshot());
|
|
44688
|
+
} catch (quota) {
|
|
44689
|
+
if (quota?.code !== "E_QUOTA") throw quota;
|
|
44690
|
+
}
|
|
44691
|
+
if (hash(await fsp9.readFile(source)) !== hash(previous)) refuse2("E_CONFLICT", "The file changed while restore was being prepared.");
|
|
44692
|
+
const write = await replaceFileAtomic(source, bytes);
|
|
44693
|
+
replaced = true;
|
|
44694
|
+
durabilityUnconfirmed = write.durabilityUnconfirmed === true;
|
|
44695
|
+
commitAttempted = true;
|
|
44696
|
+
commitOid = await commitChange(source, parser2.readDocumentXml(), `Restore original file (sha256 ${hash(bytes)})`, collectTrackedExtrasForGit(parser2));
|
|
44697
|
+
handle.parser = parser2;
|
|
44698
|
+
handle.ast = ast;
|
|
44699
|
+
handle.engine = engine;
|
|
44700
|
+
handle.toon = new ToonGenerator();
|
|
44701
|
+
handle.byteSize = bytes.length;
|
|
44702
|
+
store.bumpRevision(handle);
|
|
44703
|
+
store.markSaved(handle);
|
|
44704
|
+
handle.lastWrittenSha256 = hash(bytes);
|
|
44705
|
+
adoptBaseline(handle, validation);
|
|
44706
|
+
} catch (err) {
|
|
44707
|
+
Object.assign(handle, previousSession);
|
|
44708
|
+
handle.redoStack.length = 0;
|
|
44709
|
+
Array.prototype.push.apply(handle.redoStack, oldRedo);
|
|
44710
|
+
const { recovered: diskRecovered, failure: rollbackFailure } = await rollbackReplacedFile(source, previous, { replaced, error: err });
|
|
44711
|
+
const rollbackError = rollbackFailure ?? "";
|
|
44712
|
+
const historyChanged = commitAttempted && await historyChangedSince(source, historyBefore);
|
|
44713
|
+
if (diskRecovered) await fsp9.unlink(backup).catch(() => void 0);
|
|
44714
|
+
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}.`}`;
|
|
44715
|
+
throw Object.assign(new Error(detail), {
|
|
44716
|
+
code: err?.code ?? "E_IO",
|
|
44717
|
+
remedy: diskRecovered ? "Inspect the file and history before retrying." : `Recover from ${backup} before editing further.`
|
|
44718
|
+
});
|
|
44719
|
+
}
|
|
44720
|
+
const cleanupFailed = await fsp9.unlink(backup).then(() => false, () => true);
|
|
44721
|
+
return {
|
|
44722
|
+
ok: true,
|
|
44723
|
+
sessionId: handle.sessionId,
|
|
44724
|
+
path: source,
|
|
44725
|
+
revision: handle.revision,
|
|
44726
|
+
dirty: false,
|
|
44727
|
+
commitOid,
|
|
44728
|
+
original,
|
|
44729
|
+
...durabilityUnconfirmed ? { durabilityUnconfirmed: true } : {},
|
|
44730
|
+
...cleanupFailed ? { warning: `Restore succeeded but the temporary recovery backup could not be removed: ${backup}` } : {}
|
|
44731
|
+
};
|
|
44732
|
+
});
|
|
44733
|
+
} catch (err) {
|
|
44734
|
+
return mcpErrorFrom(err, "E_IO", "Inspect the document and history; export the original to a new path if in-place restore is unavailable.");
|
|
44735
|
+
}
|
|
44736
|
+
}
|
|
44737
|
+
|
|
43713
44738
|
// src/lib/mcp/handlers/history.ts
|
|
43714
|
-
var
|
|
44739
|
+
var import_crypto9 = require("crypto");
|
|
44740
|
+
var fsp10 = __toESM(require("fs/promises"));
|
|
43715
44741
|
async function handleGetDocumentHistory(store, input) {
|
|
43716
44742
|
const parsed = GetDocumentHistorySchema.safeParse(input);
|
|
43717
44743
|
if (!parsed.success) {
|
|
@@ -43738,7 +44764,21 @@ async function handleGetDocumentHistory(store, input) {
|
|
|
43738
44764
|
currentCommit: null,
|
|
43739
44765
|
totalCommits: 0,
|
|
43740
44766
|
history: [],
|
|
43741
|
-
warning: emptyWarning
|
|
44767
|
+
warning: emptyWarning,
|
|
44768
|
+
original: await resolveSessionOriginal(handle)
|
|
44769
|
+
};
|
|
44770
|
+
}
|
|
44771
|
+
const original = await resolveSessionOriginal(handle);
|
|
44772
|
+
if (!original.available && original.reason === "initializing") {
|
|
44773
|
+
return {
|
|
44774
|
+
ok: true,
|
|
44775
|
+
sessionId,
|
|
44776
|
+
documentPath: filePath,
|
|
44777
|
+
currentCommit: null,
|
|
44778
|
+
totalCommits: 0,
|
|
44779
|
+
history: [],
|
|
44780
|
+
warning: original.detail ?? "History initialization has not completed.",
|
|
44781
|
+
original
|
|
43742
44782
|
};
|
|
43743
44783
|
}
|
|
43744
44784
|
const full = await getHistory(filePath);
|
|
@@ -43772,7 +44812,10 @@ async function handleGetDocumentHistory(store, input) {
|
|
|
43772
44812
|
currentCommit: full.length > 0 ? full[0].oid : null,
|
|
43773
44813
|
totalCommits: full.length,
|
|
43774
44814
|
history: slice,
|
|
43775
|
-
warning
|
|
44815
|
+
warning,
|
|
44816
|
+
// Recovery status was re-verified above against what this session pinned at open, so a corrupted or
|
|
44817
|
+
// replaced original is not reported as available and another history's original is never adopted.
|
|
44818
|
+
original
|
|
43776
44819
|
};
|
|
43777
44820
|
});
|
|
43778
44821
|
} catch (err) {
|
|
@@ -43874,7 +44917,7 @@ async function handleRevertDocument(store, input) {
|
|
|
43874
44917
|
const prevByteSize = handle.byteSize;
|
|
43875
44918
|
let prevFileBytes = null;
|
|
43876
44919
|
try {
|
|
43877
|
-
prevFileBytes = await
|
|
44920
|
+
prevFileBytes = await fsp10.readFile(handle.filePath);
|
|
43878
44921
|
} catch {
|
|
43879
44922
|
prevFileBytes = null;
|
|
43880
44923
|
}
|
|
@@ -43897,6 +44940,7 @@ async function handleRevertDocument(store, input) {
|
|
|
43897
44940
|
truncated: false
|
|
43898
44941
|
};
|
|
43899
44942
|
}
|
|
44943
|
+
await assertHistoryReadyForWrite(handle.filePath);
|
|
43900
44944
|
let target;
|
|
43901
44945
|
try {
|
|
43902
44946
|
target = await checkoutCommit(handle.filePath, targetOid);
|
|
@@ -43908,15 +44952,16 @@ async function handleRevertDocument(store, input) {
|
|
|
43908
44952
|
});
|
|
43909
44953
|
}
|
|
43910
44954
|
const merged = new Map(preRevertSnap);
|
|
44955
|
+
const isTrackedForTarget = trackedPredicateFor(target);
|
|
43911
44956
|
for (const key of [...merged.keys()]) {
|
|
43912
|
-
if (key !== "word/document.xml" &&
|
|
44957
|
+
if (key !== "word/document.xml" && isTrackedForTarget(key)) merged.delete(key);
|
|
43913
44958
|
}
|
|
43914
44959
|
for (const [k, v] of target.extra) merged.set(k, v);
|
|
43915
44960
|
merged.set("word/document.xml", Buffer.from(target.xml, "utf8"));
|
|
43916
44961
|
let writeAttempted = false;
|
|
43917
44962
|
let commitPhase = false;
|
|
43918
44963
|
try {
|
|
43919
|
-
handle.parser.restorePackageSnapshot(merged);
|
|
44964
|
+
handle.parser.restorePackageSnapshot(merged, { registerCoreProperties: !target.complete });
|
|
43920
44965
|
await refreshHandleAstInPlace(handle);
|
|
43921
44966
|
const buf = handle.parser.rezip();
|
|
43922
44967
|
const candidate = await validateForBaseline(buf);
|
|
@@ -43925,7 +44970,7 @@ async function handleRevertDocument(store, input) {
|
|
|
43925
44970
|
}
|
|
43926
44971
|
assertWriteTargetNoSymlink(handle.filePath);
|
|
43927
44972
|
writeAttempted = true;
|
|
43928
|
-
await
|
|
44973
|
+
await fsp10.mkdir(pathDirname(handle.filePath), { recursive: true });
|
|
43929
44974
|
assertWriteTargetNoSymlink(handle.filePath);
|
|
43930
44975
|
await writeFileNoFollow(handle.filePath, buf);
|
|
43931
44976
|
handle.byteSize = buf.length;
|
|
@@ -43954,6 +44999,7 @@ async function handleRevertDocument(store, input) {
|
|
|
43954
44999
|
affectedNodeIds: diffIdTexts(liveBefore.texts, liveAfter.order, liveAfter.texts)
|
|
43955
45000
|
});
|
|
43956
45001
|
store.markSaved(handle);
|
|
45002
|
+
handle.lastWrittenSha256 = (0, import_crypto9.createHash)("sha256").update(buf).digest("hex");
|
|
43957
45003
|
adoptBaseline(handle, candidate);
|
|
43958
45004
|
return {
|
|
43959
45005
|
ok: true,
|
|
@@ -43964,7 +45010,9 @@ async function handleRevertDocument(store, input) {
|
|
|
43964
45010
|
dirty: false,
|
|
43965
45011
|
discardedUnsaved,
|
|
43966
45012
|
commitOid,
|
|
43967
|
-
commitMessage
|
|
45013
|
+
commitMessage,
|
|
45014
|
+
snapshotComplete: target.complete,
|
|
45015
|
+
...target.complete ? {} : { restoreWarning: INCOMPLETE_SNAPSHOT_WARNING }
|
|
43968
45016
|
};
|
|
43969
45017
|
} catch (stageErr) {
|
|
43970
45018
|
const preserved = getEngineErrorCode(stageErr);
|
|
@@ -43987,7 +45035,7 @@ async function handleRevertDocument(store, input) {
|
|
|
43987
45035
|
if (prevFileBytes !== null) {
|
|
43988
45036
|
await writeFileNoFollow(handle.filePath, prevFileBytes);
|
|
43989
45037
|
} else {
|
|
43990
|
-
await
|
|
45038
|
+
await fsp10.unlink(handle.filePath);
|
|
43991
45039
|
}
|
|
43992
45040
|
} catch (restoreErr) {
|
|
43993
45041
|
diskOk = false;
|
|
@@ -44044,6 +45092,7 @@ async function handleRedoDocument(store, input) {
|
|
|
44044
45092
|
const redoReasonProvided = "reason" in rawRedo && parsed.data.reason !== void 0;
|
|
44045
45093
|
const redoReasonText = redoReasonProvided ? sanitizeCommitMessage(parsed.data.reason, "") : "";
|
|
44046
45094
|
const authorName = sanitizeCommitAuthor(parsed.data.author);
|
|
45095
|
+
await assertHistoryReadyForWrite(handle.filePath);
|
|
44047
45096
|
const redoBefore = collectAstIdTexts(handle.ast);
|
|
44048
45097
|
const prevRevision = handle.revision;
|
|
44049
45098
|
const prevSavedRevision = handle.savedRevision;
|
|
@@ -44051,7 +45100,7 @@ async function handleRedoDocument(store, input) {
|
|
|
44051
45100
|
const preRedoSnap = handle.parser.capturePackageSnapshot();
|
|
44052
45101
|
let prevFileBytes = null;
|
|
44053
45102
|
try {
|
|
44054
|
-
prevFileBytes = await
|
|
45103
|
+
prevFileBytes = await fsp10.readFile(handle.filePath);
|
|
44055
45104
|
} catch {
|
|
44056
45105
|
prevFileBytes = null;
|
|
44057
45106
|
}
|
|
@@ -44071,7 +45120,7 @@ async function handleRedoDocument(store, input) {
|
|
|
44071
45120
|
didPop = true;
|
|
44072
45121
|
assertWriteTargetNoSymlink(handle.filePath);
|
|
44073
45122
|
writeAttempted = true;
|
|
44074
|
-
await
|
|
45123
|
+
await fsp10.mkdir(pathDirname(handle.filePath), { recursive: true });
|
|
44075
45124
|
assertWriteTargetNoSymlink(handle.filePath);
|
|
44076
45125
|
await writeFileNoFollow(handle.filePath, buf);
|
|
44077
45126
|
handle.byteSize = buf.length;
|
|
@@ -44104,6 +45153,7 @@ async function handleRedoDocument(store, input) {
|
|
|
44104
45153
|
affectedNodeIds: diffIdTexts(redoBefore.texts, redoAfter.order, redoAfter.texts)
|
|
44105
45154
|
});
|
|
44106
45155
|
store.markSaved(handle);
|
|
45156
|
+
handle.lastWrittenSha256 = (0, import_crypto9.createHash)("sha256").update(buf).digest("hex");
|
|
44107
45157
|
adoptBaseline(handle, candidate);
|
|
44108
45158
|
return {
|
|
44109
45159
|
ok: true,
|
|
@@ -44138,7 +45188,7 @@ async function handleRedoDocument(store, input) {
|
|
|
44138
45188
|
if (prevFileBytes !== null) {
|
|
44139
45189
|
await writeFileNoFollow(handle.filePath, prevFileBytes);
|
|
44140
45190
|
} else {
|
|
44141
|
-
await
|
|
45191
|
+
await fsp10.unlink(handle.filePath);
|
|
44142
45192
|
}
|
|
44143
45193
|
} catch (restoreErr) {
|
|
44144
45194
|
diskOk = false;
|
|
@@ -44266,7 +45316,8 @@ async function handlePreviewRevert(store, input) {
|
|
|
44266
45316
|
const targetXmlBytes = Buffer.from(target.xml, "utf8");
|
|
44267
45317
|
if (!liveXml || Buffer.compare(liveXml, targetXmlBytes) !== 0) affectedFiles.push("word/document.xml");
|
|
44268
45318
|
const names = /* @__PURE__ */ new Set();
|
|
44269
|
-
|
|
45319
|
+
const isTrackedForTarget = trackedPredicateFor(target);
|
|
45320
|
+
for (const k of liveSnap.keys()) if (k !== "word/document.xml" && isTrackedForTarget(k)) names.add(k);
|
|
44270
45321
|
for (const k of target.extra.keys()) names.add(k);
|
|
44271
45322
|
for (const name2 of [...names].sort()) {
|
|
44272
45323
|
if (affectedFiles.length >= AUDIT_FILES_MAX) break;
|
|
@@ -44314,6 +45365,8 @@ async function handlePreviewRevert(store, input) {
|
|
|
44314
45365
|
changedValuesTruncated,
|
|
44315
45366
|
changedValuesPositional,
|
|
44316
45367
|
...changedValuesReason ? { changedValuesReason } : {},
|
|
45368
|
+
snapshotComplete: target.complete,
|
|
45369
|
+
...target.complete ? {} : { restoreWarning: INCOMPLETE_SNAPSHOT_WARNING },
|
|
44317
45370
|
dirty,
|
|
44318
45371
|
dirtyHandling: dirty ? `Session holds ${unsavedOps} revision(s) of unsaved edits: a bare real revert would fail closed with E_CONFLICT; a real revert with discardUnsaved:true WOULD discard them (echoed back bounded). Net-zero edits still count as dirty (revisions pending, not a content diff). This preview discarded nothing.` : "Session is clean: a real revert would discard nothing.",
|
|
44319
45372
|
currentHeadOid: full[0].oid,
|
|
@@ -44329,6 +45382,8 @@ async function handlePreviewRevert(store, input) {
|
|
|
44329
45382
|
}
|
|
44330
45383
|
}
|
|
44331
45384
|
var GIT_HISTORY_HANDLERS = {
|
|
45385
|
+
export_original_document: handleExportOriginalDocument,
|
|
45386
|
+
restore_original_document: handleRestoreOriginalDocument,
|
|
44332
45387
|
get_document_history: handleGetDocumentHistory,
|
|
44333
45388
|
preview_revert: handlePreviewRevert,
|
|
44334
45389
|
revert_document: handleRevertDocument,
|
|
@@ -44376,9 +45431,9 @@ function adoptForkNumbering(fork, ast) {
|
|
|
44376
45431
|
}
|
|
44377
45432
|
|
|
44378
45433
|
// src/tools/export.ts
|
|
44379
|
-
var
|
|
44380
|
-
var
|
|
44381
|
-
var
|
|
45434
|
+
var fs17 = __toESM(require("fs"));
|
|
45435
|
+
var fsp11 = __toESM(require("fs/promises"));
|
|
45436
|
+
var path19 = __toESM(require("path"));
|
|
44382
45437
|
var ExportDocumentSchema = import_zod6.z.object({
|
|
44383
45438
|
sessionId: import_zod6.z.string().describe("Active document session ID returned by open_document"),
|
|
44384
45439
|
format: import_zod6.z.enum(["html", "markdown", "md", "pdf"]).describe('Target export format: "html", "markdown", "md", or "pdf"'),
|
|
@@ -44434,18 +45489,18 @@ function validateExportTarget(rawPath, expectedFormat) {
|
|
|
44434
45489
|
remedy: "Use a path inside the workspace documents directory."
|
|
44435
45490
|
});
|
|
44436
45491
|
}
|
|
44437
|
-
const resolved =
|
|
45492
|
+
const resolved = path19.resolve(trimmed);
|
|
44438
45493
|
const canonical = canonicalizeForAsset(resolved);
|
|
44439
45494
|
const allowedRoots = getMcpAllowedRoots();
|
|
44440
45495
|
const isInside2 = allowedRoots.some((root) => {
|
|
44441
45496
|
let canonicalRoot = root;
|
|
44442
45497
|
try {
|
|
44443
|
-
canonicalRoot =
|
|
45498
|
+
canonicalRoot = fs17.realpathSync(root);
|
|
44444
45499
|
} catch {
|
|
44445
45500
|
}
|
|
44446
|
-
const normC =
|
|
44447
|
-
const normR =
|
|
44448
|
-
return normC === normR || normC.startsWith(normR +
|
|
45501
|
+
const normC = path19.normalize(canonical);
|
|
45502
|
+
const normR = path19.normalize(canonicalRoot);
|
|
45503
|
+
return normC === normR || normC.startsWith(normR + path19.sep);
|
|
44449
45504
|
});
|
|
44450
45505
|
if (!isInside2) {
|
|
44451
45506
|
throw Object.assign(
|
|
@@ -44456,7 +45511,7 @@ function validateExportTarget(rawPath, expectedFormat) {
|
|
|
44456
45511
|
}
|
|
44457
45512
|
);
|
|
44458
45513
|
}
|
|
44459
|
-
const ext =
|
|
45514
|
+
const ext = path19.extname(trimmed).toLowerCase();
|
|
44460
45515
|
const validExts = {
|
|
44461
45516
|
html: [".html", ".htm"],
|
|
44462
45517
|
markdown: [".md", ".markdown"],
|
|
@@ -44500,18 +45555,18 @@ function validateAssetTarget(rawPath) {
|
|
|
44500
45555
|
remedy: "Use a path inside the workspace documents directory."
|
|
44501
45556
|
});
|
|
44502
45557
|
}
|
|
44503
|
-
const resolved =
|
|
45558
|
+
const resolved = path19.resolve(trimmed);
|
|
44504
45559
|
const canonical = canonicalizeForAsset(resolved);
|
|
44505
45560
|
const allowedRoots = getMcpAllowedRoots();
|
|
44506
45561
|
const isInside2 = allowedRoots.some((root) => {
|
|
44507
45562
|
let canonicalRoot = root;
|
|
44508
45563
|
try {
|
|
44509
|
-
canonicalRoot =
|
|
45564
|
+
canonicalRoot = fs17.realpathSync(root);
|
|
44510
45565
|
} catch {
|
|
44511
45566
|
}
|
|
44512
|
-
const normC =
|
|
44513
|
-
const normR =
|
|
44514
|
-
return normC === normR || normC.startsWith(normR +
|
|
45567
|
+
const normC = path19.normalize(canonical);
|
|
45568
|
+
const normR = path19.normalize(canonicalRoot);
|
|
45569
|
+
return normC === normR || normC.startsWith(normR + path19.sep);
|
|
44515
45570
|
});
|
|
44516
45571
|
if (!isInside2) {
|
|
44517
45572
|
throw Object.assign(
|
|
@@ -44619,7 +45674,7 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44619
45674
|
remedy: "Install LibreOffice on the host system: sudo apt install libreoffice (Linux) or brew install --cask libreoffice (macOS)."
|
|
44620
45675
|
};
|
|
44621
45676
|
}
|
|
44622
|
-
const defaultPdf = targetPath ||
|
|
45677
|
+
const defaultPdf = targetPath || path19.join(path19.dirname(session.filePath), `${path19.basename(session.filePath, path19.extname(session.filePath))}.pdf`);
|
|
44623
45678
|
try {
|
|
44624
45679
|
validatedPdfPath = validateExportTarget(defaultPdf, "pdf");
|
|
44625
45680
|
} catch (err) {
|
|
@@ -44658,7 +45713,7 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44658
45713
|
}
|
|
44659
45714
|
throw original;
|
|
44660
45715
|
}
|
|
44661
|
-
const stat3 = await
|
|
45716
|
+
const stat3 = await fsp11.stat(validatedPdfPath);
|
|
44662
45717
|
return {
|
|
44663
45718
|
ok: true,
|
|
44664
45719
|
format: "pdf",
|
|
@@ -44672,22 +45727,22 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44672
45727
|
}
|
|
44673
45728
|
const ast = session.ast;
|
|
44674
45729
|
if (format === "html") {
|
|
44675
|
-
const title = htmlDocumentTitle(ast,
|
|
45730
|
+
const title = htmlDocumentTitle(ast, path19.basename(session.filePath, path19.extname(session.filePath)));
|
|
44676
45731
|
const html = convertAstToHtml(ast, title, void 0, session.parser);
|
|
44677
45732
|
if (targetPath) {
|
|
44678
45733
|
assertWriteTargetNoSymlink(targetPath);
|
|
44679
|
-
await
|
|
45734
|
+
await fsp11.mkdir(path19.dirname(targetPath), { recursive: true });
|
|
44680
45735
|
assertWriteTargetNoSymlink(targetPath);
|
|
44681
45736
|
await writeFileNoFollow(targetPath, html, "utf-8");
|
|
44682
|
-
const stat3 = await
|
|
45737
|
+
const stat3 = await fsp11.stat(targetPath);
|
|
44683
45738
|
return { ok: true, format: "html", path: targetPath, bytes: stat3.size, dirty, revision };
|
|
44684
45739
|
}
|
|
44685
45740
|
return { ok: true, format: "html", content: html, bytes: Buffer.byteLength(html, "utf-8"), dirty, revision };
|
|
44686
45741
|
} else {
|
|
44687
45742
|
const effectiveImageMode = parsed.data.imageMode ?? (targetPath ? "file" : "embed");
|
|
44688
|
-
const docBase =
|
|
45743
|
+
const docBase = path19.basename(
|
|
44689
45744
|
targetPath || session.filePath || "document.docx",
|
|
44690
|
-
|
|
45745
|
+
path19.extname(targetPath || session.filePath || "document.docx")
|
|
44691
45746
|
) || "document";
|
|
44692
45747
|
const safeDocBase = docBase.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^\.+/, "").slice(0, 120) || "document";
|
|
44693
45748
|
const defaultAssetPrefix = `${safeDocBase}_assets`;
|
|
@@ -44695,39 +45750,39 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44695
45750
|
if (effectiveImageMode === "file") {
|
|
44696
45751
|
const initialAssets = collectMarkdownImageAssets(ast, effectiveAssetPrefix);
|
|
44697
45752
|
if (!targetPath) throw new Error("Markdown file image mode requires an output path.");
|
|
44698
|
-
const destDir =
|
|
45753
|
+
const destDir = path19.dirname(targetPath);
|
|
44699
45754
|
let generation;
|
|
44700
45755
|
let staging;
|
|
44701
45756
|
let published = false;
|
|
44702
45757
|
try {
|
|
44703
45758
|
assertWriteTargetNoSymlink(targetPath);
|
|
44704
|
-
await
|
|
45759
|
+
await fsp11.mkdir(destDir, { recursive: true });
|
|
44705
45760
|
assertWriteTargetNoSymlink(targetPath);
|
|
44706
45761
|
let outputPrefix = effectiveAssetPrefix;
|
|
44707
45762
|
if (initialAssets.length > 0) {
|
|
44708
|
-
const prefixDir = validateAssetTarget(
|
|
45763
|
+
const prefixDir = validateAssetTarget(path19.resolve(destDir, effectiveAssetPrefix));
|
|
44709
45764
|
assertWriteTargetNoSymlink(prefixDir);
|
|
44710
|
-
await
|
|
45765
|
+
await fsp11.mkdir(prefixDir, { recursive: true });
|
|
44711
45766
|
assertWriteTargetNoSymlink(prefixDir);
|
|
44712
|
-
generation = await
|
|
45767
|
+
generation = await fsp11.mkdtemp(path19.join(prefixDir, "export-"));
|
|
44713
45768
|
assertWriteTargetNoSymlink(generation);
|
|
44714
|
-
outputPrefix =
|
|
45769
|
+
outputPrefix = path19.relative(destDir, generation).split(path19.sep).join("/");
|
|
44715
45770
|
}
|
|
44716
45771
|
const md = convertAstToMarkdown(ast, session.parser, { imageMode: "file", assetPrefix: outputPrefix });
|
|
44717
45772
|
const imageAssets = collectMarkdownImageAssets(ast, outputPrefix);
|
|
44718
45773
|
const writtenAssets = [];
|
|
44719
45774
|
for (const asset of imageAssets) {
|
|
44720
|
-
const assetPath = validateAssetTarget(
|
|
45775
|
+
const assetPath = validateAssetTarget(path19.resolve(destDir, asset.name));
|
|
44721
45776
|
assertWriteTargetNoSymlink(assetPath);
|
|
44722
45777
|
await writeFileNoFollow(assetPath, asset.data);
|
|
44723
45778
|
writtenAssets.push(assetPath);
|
|
44724
45779
|
}
|
|
44725
|
-
staging = await
|
|
44726
|
-
const stagedFile =
|
|
45780
|
+
staging = await fsp11.mkdtemp(path19.join(destDir, ".astra-export-"));
|
|
45781
|
+
const stagedFile = path19.join(staging, "document.md");
|
|
44727
45782
|
assertWriteTargetNoSymlink(stagedFile);
|
|
44728
45783
|
await writeFileNoFollow(stagedFile, md, "utf-8");
|
|
44729
45784
|
assertWriteTargetNoSymlink(targetPath);
|
|
44730
|
-
await
|
|
45785
|
+
await fsp11.rename(stagedFile, targetPath);
|
|
44731
45786
|
published = true;
|
|
44732
45787
|
return {
|
|
44733
45788
|
ok: true,
|
|
@@ -44742,17 +45797,17 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44742
45797
|
assetCount: writtenAssets.length
|
|
44743
45798
|
};
|
|
44744
45799
|
} finally {
|
|
44745
|
-
if (staging) await
|
|
44746
|
-
if (!published && generation) await
|
|
45800
|
+
if (staging) await fsp11.rm(staging, { recursive: true, force: true }).catch(() => void 0);
|
|
45801
|
+
if (!published && generation) await fsp11.rm(generation, { recursive: true, force: true }).catch(() => void 0);
|
|
44747
45802
|
}
|
|
44748
45803
|
} else {
|
|
44749
45804
|
const md = convertAstToMarkdown(ast, session.parser, { imageMode: "embed" });
|
|
44750
45805
|
if (targetPath) {
|
|
44751
45806
|
assertWriteTargetNoSymlink(targetPath);
|
|
44752
|
-
await
|
|
45807
|
+
await fsp11.mkdir(path19.dirname(targetPath), { recursive: true });
|
|
44753
45808
|
assertWriteTargetNoSymlink(targetPath);
|
|
44754
45809
|
await writeFileNoFollow(targetPath, md, "utf-8");
|
|
44755
|
-
const stat3 = await
|
|
45810
|
+
const stat3 = await fsp11.stat(targetPath);
|
|
44756
45811
|
return {
|
|
44757
45812
|
ok: true,
|
|
44758
45813
|
format: "markdown",
|
|
@@ -44790,8 +45845,8 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44790
45845
|
}
|
|
44791
45846
|
|
|
44792
45847
|
// src/tools/visual.ts
|
|
44793
|
-
var
|
|
44794
|
-
var
|
|
45848
|
+
var fs18 = __toESM(require("fs"));
|
|
45849
|
+
var path20 = __toESM(require("path"));
|
|
44795
45850
|
var import_zod7 = require("zod");
|
|
44796
45851
|
function visualError(code, message, remedy) {
|
|
44797
45852
|
return { ok: false, code, message, remedy };
|
|
@@ -44939,14 +45994,14 @@ function serializeSessionCopy2(parser2, ast) {
|
|
|
44939
45994
|
return fork.rezip();
|
|
44940
45995
|
}
|
|
44941
45996
|
function placeImages(images, dir, docPath) {
|
|
44942
|
-
|
|
44943
|
-
const base =
|
|
45997
|
+
fs18.mkdirSync(dir, { recursive: true });
|
|
45998
|
+
const base = path20.basename(docPath || "document", path20.extname(docPath || "")).replace(/[^a-zA-Z0-9._-]+/g, "-") || "document";
|
|
44944
45999
|
return images.map((img) => {
|
|
44945
|
-
const ext =
|
|
44946
|
-
const dest =
|
|
44947
|
-
writeFileNoFollowSync(dest,
|
|
46000
|
+
const ext = path20.extname(img.path) || (img.format === "jpeg" ? ".jpg" : ".png");
|
|
46001
|
+
const dest = path20.join(dir, `${base}-page-${img.page}${ext}`);
|
|
46002
|
+
writeFileNoFollowSync(dest, fs18.readFileSync(img.path));
|
|
44948
46003
|
try {
|
|
44949
|
-
|
|
46004
|
+
fs18.rmSync(img.path, { force: true });
|
|
44950
46005
|
} catch {
|
|
44951
46006
|
}
|
|
44952
46007
|
return { ...img, path: dest };
|
|
@@ -45073,8 +46128,8 @@ async function handleVisualInspect(store, input) {
|
|
|
45073
46128
|
|
|
45074
46129
|
// src/tools/new-document.ts
|
|
45075
46130
|
var import_zod8 = require("zod");
|
|
45076
|
-
var
|
|
45077
|
-
var
|
|
46131
|
+
var fsp12 = __toESM(require("fs/promises"));
|
|
46132
|
+
var path21 = __toESM(require("path"));
|
|
45078
46133
|
var NEW_DOCUMENT_TEMPLATES = ["report", "memo", "blank"];
|
|
45079
46134
|
var NewDocumentSchema = import_zod8.z.object({
|
|
45080
46135
|
outPath: import_zod8.z.string().min(1).max(1024),
|
|
@@ -45150,10 +46205,10 @@ async function handleNewDocument(store, rawArgs) {
|
|
|
45150
46205
|
}
|
|
45151
46206
|
if (!force) {
|
|
45152
46207
|
try {
|
|
45153
|
-
await
|
|
46208
|
+
await fsp12.access(target);
|
|
45154
46209
|
return newDocError(
|
|
45155
46210
|
"E_CONFLICT",
|
|
45156
|
-
`new_document: destination "${
|
|
46211
|
+
`new_document: destination "${path21.basename(target)}" already exists (refusing to overwrite without force)`,
|
|
45157
46212
|
'Retry with {"force":true} to overwrite, or choose a different outPath.'
|
|
45158
46213
|
);
|
|
45159
46214
|
} catch {
|
|
@@ -45204,7 +46259,7 @@ async function handleNewDocument(store, rawArgs) {
|
|
|
45204
46259
|
}
|
|
45205
46260
|
try {
|
|
45206
46261
|
assertWriteTargetNoSymlink(target);
|
|
45207
|
-
await
|
|
46262
|
+
await fsp12.mkdir(path21.dirname(target), { recursive: true });
|
|
45208
46263
|
assertWriteTargetNoSymlink(target);
|
|
45209
46264
|
await writeFileNoFollow(target, buf);
|
|
45210
46265
|
} catch (err) {
|
|
@@ -45216,12 +46271,12 @@ async function handleNewDocument(store, rawArgs) {
|
|
|
45216
46271
|
);
|
|
45217
46272
|
}
|
|
45218
46273
|
try {
|
|
45219
|
-
const handle = await store.openDocument(target, parsed.data.sessionId);
|
|
46274
|
+
const handle = await store.openDocument(target, parsed.data.sessionId, void 0, false);
|
|
45220
46275
|
handle.parser.adoptBuiltinSeedFrom(parser2);
|
|
45221
46276
|
if (parsed.data.author !== void 0) handle.ast.settings.defaultAuthor = parsed.data.author;
|
|
45222
46277
|
let bytes = buf.length;
|
|
45223
46278
|
try {
|
|
45224
|
-
bytes = (await
|
|
46279
|
+
bytes = (await fsp12.stat(target)).size;
|
|
45225
46280
|
} catch {
|
|
45226
46281
|
}
|
|
45227
46282
|
if (guideRef !== void 0 && resolvedGuide !== void 0) {
|
|
@@ -45251,8 +46306,8 @@ async function handleNewDocument(store, rawArgs) {
|
|
|
45251
46306
|
}
|
|
45252
46307
|
|
|
45253
46308
|
// src/lib/mcp/handlers/parity.ts
|
|
45254
|
-
var
|
|
45255
|
-
var
|
|
46309
|
+
var fs19 = __toESM(require("fs"));
|
|
46310
|
+
var path22 = __toESM(require("path"));
|
|
45256
46311
|
var IMPORT_FILE_MAX_BYTES = COMPOSE_MARKDOWN_MAX;
|
|
45257
46312
|
async function refreshSessionAst(handle) {
|
|
45258
46313
|
await refreshAstInPlace(handle.ast, handle.parser);
|
|
@@ -45507,17 +46562,17 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
|
|
|
45507
46562
|
if (segments.includes("..")) {
|
|
45508
46563
|
throw toError("E_SANDBOX", 'import path contains forbidden ".." traversal', "Use a path inside the workspace documents directory.");
|
|
45509
46564
|
}
|
|
45510
|
-
const resolved =
|
|
46565
|
+
const resolved = path22.resolve(trimmed);
|
|
45511
46566
|
const canonical = canonicalizeForAsset(resolved);
|
|
45512
46567
|
const inside = roots.some((root) => {
|
|
45513
46568
|
let canonicalRoot = root;
|
|
45514
46569
|
try {
|
|
45515
|
-
canonicalRoot =
|
|
46570
|
+
canonicalRoot = fs19.realpathSync(root);
|
|
45516
46571
|
} catch {
|
|
45517
46572
|
}
|
|
45518
|
-
const normC =
|
|
45519
|
-
const normR =
|
|
45520
|
-
return normC === normR || normC.startsWith(normR +
|
|
46573
|
+
const normC = path22.normalize(canonical);
|
|
46574
|
+
const normR = path22.normalize(canonicalRoot);
|
|
46575
|
+
return normC === normR || normC.startsWith(normR + path22.sep);
|
|
45521
46576
|
});
|
|
45522
46577
|
if (!inside) {
|
|
45523
46578
|
throw toError(
|
|
@@ -45526,12 +46581,12 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
|
|
|
45526
46581
|
"Place the Markdown file inside one of the allowed directories, or name its folder with --allowed-dir or ASTRA_EXTRA_DOC_ROOTS."
|
|
45527
46582
|
);
|
|
45528
46583
|
}
|
|
45529
|
-
const base =
|
|
45530
|
-
const nofollow =
|
|
45531
|
-
const flags = typeof nofollow === "number" && nofollow !== 0 ?
|
|
46584
|
+
const base = path22.basename(canonical);
|
|
46585
|
+
const nofollow = fs19.constants.O_NOFOLLOW;
|
|
46586
|
+
const flags = typeof nofollow === "number" && nofollow !== 0 ? fs19.constants.O_RDONLY | nofollow : "r";
|
|
45532
46587
|
let fd;
|
|
45533
46588
|
try {
|
|
45534
|
-
fd =
|
|
46589
|
+
fd = fs19.openSync(canonical, flags);
|
|
45535
46590
|
} catch (err) {
|
|
45536
46591
|
const code = err?.code;
|
|
45537
46592
|
if (code === "ENOENT") {
|
|
@@ -45543,7 +46598,7 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
|
|
|
45543
46598
|
throw toError("E_IO", `import file cannot be read (${base})`, "Check the file exists and is readable, then retry.");
|
|
45544
46599
|
}
|
|
45545
46600
|
try {
|
|
45546
|
-
const st =
|
|
46601
|
+
const st = fs19.fstatSync(fd);
|
|
45547
46602
|
if (!st.isFile()) {
|
|
45548
46603
|
throw toError("E_IO", `import reference is not a file (${base})`, "Point the import at a regular Markdown file.");
|
|
45549
46604
|
}
|
|
@@ -45557,7 +46612,7 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
|
|
|
45557
46612
|
"Split the Markdown source into smaller files and import each in its own call."
|
|
45558
46613
|
);
|
|
45559
46614
|
}
|
|
45560
|
-
const buf =
|
|
46615
|
+
const buf = fs19.readFileSync(fd);
|
|
45561
46616
|
if (buf.length > IMPORT_FILE_MAX_BYTES) {
|
|
45562
46617
|
throw toError(
|
|
45563
46618
|
"E_LIMIT",
|
|
@@ -45567,18 +46622,18 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
|
|
|
45567
46622
|
}
|
|
45568
46623
|
let post = canonical;
|
|
45569
46624
|
try {
|
|
45570
|
-
post =
|
|
46625
|
+
post = fs19.realpathSync(canonical);
|
|
45571
46626
|
} catch {
|
|
45572
46627
|
}
|
|
45573
46628
|
const stillInside = roots.some((root) => {
|
|
45574
46629
|
let canonicalRoot = root;
|
|
45575
46630
|
try {
|
|
45576
|
-
canonicalRoot =
|
|
46631
|
+
canonicalRoot = fs19.realpathSync(root);
|
|
45577
46632
|
} catch {
|
|
45578
46633
|
}
|
|
45579
|
-
const normC =
|
|
45580
|
-
const normR =
|
|
45581
|
-
return normC === normR || normC.startsWith(normR +
|
|
46634
|
+
const normC = path22.normalize(post);
|
|
46635
|
+
const normR = path22.normalize(canonicalRoot);
|
|
46636
|
+
return normC === normR || normC.startsWith(normR + path22.sep);
|
|
45582
46637
|
});
|
|
45583
46638
|
if (!stillInside) {
|
|
45584
46639
|
throw toError("E_SANDBOX", "import path points outside the allowed directory", `Keep Markdown files ${describeAllowedFolders()}.`);
|
|
@@ -45586,7 +46641,7 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
|
|
|
45586
46641
|
return buf.toString("utf8");
|
|
45587
46642
|
} finally {
|
|
45588
46643
|
try {
|
|
45589
|
-
if (fd !== void 0)
|
|
46644
|
+
if (fd !== void 0) fs19.closeSync(fd);
|
|
45590
46645
|
} catch {
|
|
45591
46646
|
}
|
|
45592
46647
|
}
|
|
@@ -45602,12 +46657,12 @@ async function handleImportDocument(store, input) {
|
|
|
45602
46657
|
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).'
|
|
45603
46658
|
};
|
|
45604
46659
|
}
|
|
45605
|
-
const srcExt =
|
|
46660
|
+
const srcExt = path22.extname(parsed.data.path).toLowerCase();
|
|
45606
46661
|
if (srcExt === ".html" || srcExt === ".htm") {
|
|
45607
46662
|
return {
|
|
45608
46663
|
ok: false,
|
|
45609
46664
|
code: "E_VALIDATION",
|
|
45610
|
-
message: `HTML import is not supported directly ("${
|
|
46665
|
+
message: `HTML import is not supported directly ("${path22.basename(parsed.data.path)}")`,
|
|
45611
46666
|
remedy: "ASTra imports Markdown (.md). Convert the HTML file to Markdown or supply a .md document"
|
|
45612
46667
|
};
|
|
45613
46668
|
}
|
|
@@ -45615,7 +46670,7 @@ async function handleImportDocument(store, input) {
|
|
|
45615
46670
|
return {
|
|
45616
46671
|
ok: false,
|
|
45617
46672
|
code: "E_VALIDATION",
|
|
45618
|
-
message: `import_document cannot ingest .docx files ("${
|
|
46673
|
+
message: `import_document cannot ingest .docx files ("${path22.basename(parsed.data.path)}"): open .docx files directly with open_document instead`,
|
|
45619
46674
|
remedy: "Call open_document with the .docx path to work with it, or convert it to Markdown first and import the .md file."
|
|
45620
46675
|
};
|
|
45621
46676
|
}
|
|
@@ -45651,15 +46706,15 @@ async function handleImportDocument(store, input) {
|
|
|
45651
46706
|
}
|
|
45652
46707
|
|
|
45653
46708
|
// src/lib/mcp/schema-dump.ts
|
|
45654
|
-
var
|
|
45655
|
-
var
|
|
46709
|
+
var fs20 = __toESM(require("fs"));
|
|
46710
|
+
var path23 = __toESM(require("path"));
|
|
45656
46711
|
var import_zod9 = require("zod");
|
|
45657
46712
|
|
|
45658
46713
|
// package.json
|
|
45659
46714
|
var package_default = {
|
|
45660
46715
|
name: "@attentionform/astra-mcp-server",
|
|
45661
46716
|
mcpName: "com.attentionform/astra-mcp-server",
|
|
45662
|
-
version: "0.
|
|
46717
|
+
version: "0.5.0",
|
|
45663
46718
|
description: "Official Model Context Protocol (MCP) server for ASTra document IDE",
|
|
45664
46719
|
main: "dist/index.js",
|
|
45665
46720
|
types: "dist/index.d.ts",
|
|
@@ -45700,8 +46755,8 @@ var package_default = {
|
|
|
45700
46755
|
"adm-zip-gate": "node ../../scripts/check-adm-zip-imports.mjs"
|
|
45701
46756
|
},
|
|
45702
46757
|
dependencies: {
|
|
45703
|
-
"@attentionform/astra-engine": "^0.
|
|
45704
|
-
"@attentionform/ooxml-validator": "^0.
|
|
46758
|
+
"@attentionform/astra-engine": "^0.5.0",
|
|
46759
|
+
"@attentionform/ooxml-validator": "^0.5.0",
|
|
45705
46760
|
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
45706
46761
|
"@xmldom/xmldom": "0.8.15",
|
|
45707
46762
|
"adm-zip": "0.6.1",
|
|
@@ -45746,6 +46801,8 @@ function getToolDefinitions() {
|
|
|
45746
46801
|
{ name: "compose_document", description: COMPOSE_DOCUMENT_DESCRIPTION, schema: ComposeDocumentSchema },
|
|
45747
46802
|
{ name: "export_document", description: EXPORT_DOCUMENT_DESCRIPTION, schema: ExportDocumentSchema },
|
|
45748
46803
|
{ name: "visual_inspect", description: VISUAL_INSPECT_DESCRIPTION, schema: VisualInspectSchema },
|
|
46804
|
+
{ name: "export_original_document", description: GIT_HISTORY_DESCRIPTIONS.export_original_document, schema: ExportOriginalDocumentSchema },
|
|
46805
|
+
{ name: "restore_original_document", description: GIT_HISTORY_DESCRIPTIONS.restore_original_document, schema: RestoreOriginalDocumentSchema },
|
|
45749
46806
|
{ name: "get_document_history", description: GIT_HISTORY_DESCRIPTIONS.get_document_history, schema: GetDocumentHistorySchema },
|
|
45750
46807
|
{ name: "preview_revert", description: GIT_HISTORY_DESCRIPTIONS.preview_revert, schema: PreviewRevertSchema },
|
|
45751
46808
|
{ name: "revert_document", description: GIT_HISTORY_DESCRIPTIONS.revert_document, schema: RevertDocumentSchema },
|
|
@@ -45766,8 +46823,8 @@ function compactToolSchema(schema) {
|
|
|
45766
46823
|
return raw;
|
|
45767
46824
|
}
|
|
45768
46825
|
function generateSchemasToDir(targetDir, version = getServerVersion()) {
|
|
45769
|
-
if (!
|
|
45770
|
-
|
|
46826
|
+
if (!fs20.existsSync(targetDir)) {
|
|
46827
|
+
fs20.mkdirSync(targetDir, { recursive: true });
|
|
45771
46828
|
}
|
|
45772
46829
|
const stamp2 = buildStamp(version);
|
|
45773
46830
|
const written = [];
|
|
@@ -45779,8 +46836,8 @@ function generateSchemasToDir(targetDir, version = getServerVersion()) {
|
|
|
45779
46836
|
parameters: raw,
|
|
45780
46837
|
[SCHEMA_STAMP_KEY]: stamp2
|
|
45781
46838
|
};
|
|
45782
|
-
const filePath =
|
|
45783
|
-
|
|
46839
|
+
const filePath = path23.join(targetDir, `${tool.name}.json`);
|
|
46840
|
+
fs20.writeFileSync(filePath, JSON.stringify(payload, null, 2) + "\n", "utf8");
|
|
45784
46841
|
written.push(filePath);
|
|
45785
46842
|
}
|
|
45786
46843
|
return { written, count: written.length };
|
|
@@ -45790,11 +46847,11 @@ function checkSchemasInDir(dir, expectedVersion2 = getServerVersion()) {
|
|
|
45790
46847
|
const mismatches = [];
|
|
45791
46848
|
let checked = 0;
|
|
45792
46849
|
for (const tool of getToolDefinitions()) {
|
|
45793
|
-
const file =
|
|
46850
|
+
const file = path23.join(dir, `${tool.name}.json`);
|
|
45794
46851
|
checked += 1;
|
|
45795
46852
|
let stamped = "<missing>";
|
|
45796
46853
|
try {
|
|
45797
|
-
const raw = JSON.parse(
|
|
46854
|
+
const raw = JSON.parse(fs20.readFileSync(file, "utf8"));
|
|
45798
46855
|
const value = raw[SCHEMA_STAMP_KEY];
|
|
45799
46856
|
stamped = typeof value === "string" ? value : "<unstamped>";
|
|
45800
46857
|
} catch {
|
|
@@ -45807,7 +46864,7 @@ function checkSchemasInDir(dir, expectedVersion2 = getServerVersion()) {
|
|
|
45807
46864
|
return { ok: mismatches.length === 0, mismatches, checked };
|
|
45808
46865
|
}
|
|
45809
46866
|
function formatMismatchWarning(mismatches) {
|
|
45810
|
-
const lines = mismatches.map((m) => ` ${
|
|
46867
|
+
const lines = mismatches.map((m) => ` ${path23.basename(m.file)}: stamped ${m.stamped}, installed ${m.expected}`);
|
|
45811
46868
|
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");
|
|
45812
46869
|
}
|
|
45813
46870
|
|
|
@@ -45874,20 +46931,20 @@ function buildUsageText() {
|
|
|
45874
46931
|
var CLI_USAGE_ERROR = 2;
|
|
45875
46932
|
function initAllowedDirs(argv = process.argv) {
|
|
45876
46933
|
const currentRoots = new Set(
|
|
45877
|
-
(process.env.ASTRA_EXTRA_DOC_ROOTS || "").split(
|
|
46934
|
+
(process.env.ASTRA_EXTRA_DOC_ROOTS || "").split(path24.delimiter).map((p) => p.trim()).filter(Boolean)
|
|
45878
46935
|
);
|
|
45879
46936
|
for (let i = 0; i < argv.length; i++) {
|
|
45880
46937
|
if (argv[i] === "--") break;
|
|
45881
46938
|
if (argv[i] === "--allowed-dir" && argv[i + 1] && !argv[i + 1].startsWith("-")) {
|
|
45882
|
-
currentRoots.add(
|
|
46939
|
+
currentRoots.add(path24.resolve(argv[i + 1]));
|
|
45883
46940
|
i++;
|
|
45884
46941
|
} else if (argv[i].startsWith("--allowed-dir=")) {
|
|
45885
46942
|
const value = argv[i].slice("--allowed-dir=".length).trim();
|
|
45886
|
-
if (value && !value.startsWith("-")) currentRoots.add(
|
|
46943
|
+
if (value && !value.startsWith("-")) currentRoots.add(path24.resolve(value));
|
|
45887
46944
|
}
|
|
45888
46945
|
}
|
|
45889
46946
|
if (currentRoots.size > 0) {
|
|
45890
|
-
process.env.ASTRA_EXTRA_DOC_ROOTS = Array.from(currentRoots).join(
|
|
46947
|
+
process.env.ASTRA_EXTRA_DOC_ROOTS = Array.from(currentRoots).join(path24.delimiter);
|
|
45891
46948
|
}
|
|
45892
46949
|
}
|
|
45893
46950
|
function takeDirFlag(argv, flag) {
|
|
@@ -45953,6 +47010,16 @@ function createAstraMcpServer(store = createMcpSessionStore()) {
|
|
|
45953
47010
|
},
|
|
45954
47011
|
async (args) => toTextResult(await handleVisualInspect(store, args))
|
|
45955
47012
|
);
|
|
47013
|
+
server.registerTool(
|
|
47014
|
+
"export_original_document",
|
|
47015
|
+
{ description: GIT_HISTORY_DESCRIPTIONS.export_original_document, inputSchema: ExportOriginalDocumentSchema },
|
|
47016
|
+
async (args) => toTextResult(await GIT_HISTORY_HANDLERS.export_original_document(store, args))
|
|
47017
|
+
);
|
|
47018
|
+
server.registerTool(
|
|
47019
|
+
"restore_original_document",
|
|
47020
|
+
{ description: GIT_HISTORY_DESCRIPTIONS.restore_original_document, inputSchema: RestoreOriginalDocumentSchema },
|
|
47021
|
+
async (args) => toTextResult(await GIT_HISTORY_HANDLERS.restore_original_document(store, args))
|
|
47022
|
+
);
|
|
45956
47023
|
server.registerTool(
|
|
45957
47024
|
"get_document_history",
|
|
45958
47025
|
{ description: GIT_HISTORY_DESCRIPTIONS.get_document_history, inputSchema: GetDocumentHistorySchema },
|
|
@@ -46080,7 +47147,7 @@ async function main(argv = process.argv) {
|
|
|
46080
47147
|
if (openTimeout !== void 0) setOpenValidationTimeoutMs(Number(openTimeout));
|
|
46081
47148
|
const dumpDir = takeDirFlag(argv, "--dump-schemas");
|
|
46082
47149
|
if (dumpDir !== void 0) {
|
|
46083
|
-
const resolved =
|
|
47150
|
+
const resolved = path24.resolve(dumpDir);
|
|
46084
47151
|
const result = generateSchemasToDir(resolved, SERVER_VERSION);
|
|
46085
47152
|
process.stdout.write(`[${SERVER_NAME} v${SERVER_VERSION}] wrote ${result.count} schemas to ${resolved}
|
|
46086
47153
|
`);
|
|
@@ -46088,7 +47155,7 @@ async function main(argv = process.argv) {
|
|
|
46088
47155
|
}
|
|
46089
47156
|
const checkDir = takeDirFlag(argv, "--check-schemas");
|
|
46090
47157
|
if (checkDir !== void 0) {
|
|
46091
|
-
const resolved =
|
|
47158
|
+
const resolved = path24.resolve(checkDir);
|
|
46092
47159
|
const verdict = checkSchemasInDir(resolved, SERVER_VERSION);
|
|
46093
47160
|
if (verdict.ok) {
|
|
46094
47161
|
process.stdout.write(`[${SERVER_NAME} v${SERVER_VERSION}] schemas fresh (${verdict.checked} tools)
|