@attentionform/astra-mcp-server 0.4.2 → 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 +1237 -296
- package/dist/lib/mcp/handlers/history.d.ts +5 -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);
|
|
@@ -7416,20 +7416,20 @@ function sanitizeEmbeddedWorkbookBytes(buf, outerName) {
|
|
|
7416
7416
|
}
|
|
7417
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;
|
|
7418
7418
|
function bufferZipAdapter(files, touched) {
|
|
7419
|
-
const write = (
|
|
7420
|
-
files.set(
|
|
7421
|
-
touched?.add(
|
|
7419
|
+
const write = (path25, content) => {
|
|
7420
|
+
files.set(path25, content);
|
|
7421
|
+
touched?.add(path25);
|
|
7422
7422
|
};
|
|
7423
7423
|
return {
|
|
7424
|
-
hasFile: (
|
|
7425
|
-
readText: (
|
|
7426
|
-
const data = files.get(
|
|
7427
|
-
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.`);
|
|
7428
7428
|
return data.toString("utf8");
|
|
7429
7429
|
},
|
|
7430
|
-
readTextOrNull: (
|
|
7431
|
-
writeText: (
|
|
7432
|
-
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))
|
|
7433
7433
|
};
|
|
7434
7434
|
}
|
|
7435
7435
|
function deleteRemovedComments(files, ids, touched) {
|
|
@@ -10414,7 +10414,7 @@ var DocxParser = class _DocxParser {
|
|
|
10414
10414
|
* Captures a full package snapshot: a clone of every zip entry buffer.
|
|
10415
10415
|
* Used by OperationEngine for transactional rollback of failed batches.
|
|
10416
10416
|
*/
|
|
10417
|
-
capturePackageSnapshot() {
|
|
10417
|
+
capturePackageSnapshot(options) {
|
|
10418
10418
|
this.purgeRogueZipEntries();
|
|
10419
10419
|
const snapshot3 = /* @__PURE__ */ new Map();
|
|
10420
10420
|
for (const entry of this.zip.getEntries()) {
|
|
@@ -10425,7 +10425,7 @@ var DocxParser = class _DocxParser {
|
|
|
10425
10425
|
snapshot3.set(entry.entryName, Buffer.from(entry.getData()));
|
|
10426
10426
|
}
|
|
10427
10427
|
try {
|
|
10428
|
-
snapshot3.set("word/document.xml", Buffer.from(this.readDocumentXml(), "utf8"));
|
|
10428
|
+
snapshot3.set("word/document.xml", Buffer.from(options?.documentXml ?? this.readDocumentXml(), "utf8"));
|
|
10429
10429
|
} catch {
|
|
10430
10430
|
}
|
|
10431
10431
|
return snapshot3;
|
|
@@ -11068,20 +11068,20 @@ var DocxParser = class _DocxParser {
|
|
|
11068
11068
|
}
|
|
11069
11069
|
getZipAdapter() {
|
|
11070
11070
|
return {
|
|
11071
|
-
hasFile: (
|
|
11072
|
-
readText: (
|
|
11073
|
-
const cleanPath = this.sanitizePath(
|
|
11071
|
+
hasFile: (path25) => !!this.zip.getEntry(this.sanitizePath(path25)),
|
|
11072
|
+
readText: (path25) => {
|
|
11073
|
+
const cleanPath = this.sanitizePath(path25);
|
|
11074
11074
|
const entry = this.zip.getEntry(cleanPath);
|
|
11075
11075
|
if (!entry) throw new Error(`File not found: ${cleanPath}`);
|
|
11076
11076
|
return entry.getData().toString("utf8");
|
|
11077
11077
|
},
|
|
11078
|
-
readTextOrNull: (
|
|
11079
|
-
const cleanPath = this.sanitizePath(
|
|
11078
|
+
readTextOrNull: (path25) => {
|
|
11079
|
+
const cleanPath = this.sanitizePath(path25);
|
|
11080
11080
|
const entry = this.zip.getEntry(cleanPath);
|
|
11081
11081
|
return entry ? entry.getData().toString("utf8") : null;
|
|
11082
11082
|
},
|
|
11083
|
-
writeText: (
|
|
11084
|
-
const cleanPath = this.sanitizePath(
|
|
11083
|
+
writeText: (path25, content) => {
|
|
11084
|
+
const cleanPath = this.sanitizePath(path25);
|
|
11085
11085
|
assertNoXxe(content, cleanPath);
|
|
11086
11086
|
assertNoXxeAdjacent(content, cleanPath);
|
|
11087
11087
|
if (this.zip.getEntry(cleanPath)) {
|
|
@@ -11090,8 +11090,8 @@ var DocxParser = class _DocxParser {
|
|
|
11090
11090
|
this.zip.addFile(cleanPath, Buffer.from(content, "utf8"));
|
|
11091
11091
|
}
|
|
11092
11092
|
},
|
|
11093
|
-
writeBuffer: (
|
|
11094
|
-
const cleanPath = this.sanitizePath(
|
|
11093
|
+
writeBuffer: (path25, content) => {
|
|
11094
|
+
const cleanPath = this.sanitizePath(path25);
|
|
11095
11095
|
assertNoXxe(content.toString("utf8"), cleanPath);
|
|
11096
11096
|
assertNoXxeAdjacent(content.toString("utf8"), cleanPath);
|
|
11097
11097
|
if (this.zip.getEntry(cleanPath)) {
|
|
@@ -12915,12 +12915,12 @@ ${validation.errors.join("\n")}`, "repair the part XML or re-save the document i
|
|
|
12915
12915
|
if (!comments || /<w:comment[\s>]/.test(comments.getData().toString("utf8"))) return;
|
|
12916
12916
|
const parts = ["comments", "commentsExtended", "commentsIds", "commentsExtensible"].map((n) => `${n}.xml`);
|
|
12917
12917
|
for (const part of parts) if (this.zip.getEntry(`word/${part}`)) this.zip.deleteFile(`word/${part}`);
|
|
12918
|
-
const edit = (
|
|
12919
|
-
const entry = this.zip.getEntry(
|
|
12918
|
+
const edit = (path25, tag, attr, drop) => {
|
|
12919
|
+
const entry = this.zip.getEntry(path25);
|
|
12920
12920
|
if (!entry) return;
|
|
12921
12921
|
const dom = this.parser.parseFromString(entry.getData().toString("utf8"), "text/xml");
|
|
12922
12922
|
for (const el of Array.from(dom.getElementsByTagName(tag))) if (drop(el.getAttribute(attr) ?? "")) el.parentNode?.removeChild(el);
|
|
12923
|
-
this.zip.updateFile(
|
|
12923
|
+
this.zip.updateFile(path25, Buffer.from(this.serializer.serializeToString(dom), "utf8"));
|
|
12924
12924
|
};
|
|
12925
12925
|
edit("word/_rels/document.xml.rels", "Relationship", "Target", (value) => parts.includes(value.replace(/^\/?(word\/)?/, "")));
|
|
12926
12926
|
edit("[Content_Types].xml", "Override", "PartName", (value) => parts.map((p) => `/word/${p}`).includes(value));
|
|
@@ -13658,12 +13658,11 @@ ${validation.errors.join("\n")}`, "repair the part XML or re-save the document i
|
|
|
13658
13658
|
}
|
|
13659
13659
|
};
|
|
13660
13660
|
|
|
13661
|
-
// ../engine/src/
|
|
13662
|
-
var
|
|
13663
|
-
var import_promises2 = __toESM(require("fs/promises"));
|
|
13664
|
-
var
|
|
13665
|
-
var
|
|
13666
|
-
var import_isomorphic_git = __toESM(require("isomorphic-git"));
|
|
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");
|
|
13667
13666
|
|
|
13668
13667
|
// ../engine/src/sandbox/paths.ts
|
|
13669
13668
|
var import_fs3 = __toESM(require("fs"));
|
|
@@ -13805,7 +13804,408 @@ function writeFileNoFollowSync(target, data, encoding = "utf8") {
|
|
|
13805
13804
|
assertWriteTargetNoSymlink(import_path2.default.resolve(target));
|
|
13806
13805
|
}
|
|
13807
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
|
+
|
|
13808
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"));
|
|
13809
14209
|
var HISTORY_DIR_ENV = "ASTRA_HISTORY_DIR";
|
|
13810
14210
|
function historyRoot() {
|
|
13811
14211
|
const raw = process.env[HISTORY_DIR_ENV];
|
|
@@ -13916,7 +14316,7 @@ async function writeAndAdd(repoDir, relPath, content) {
|
|
|
13916
14316
|
const fullPath = import_path3.default.join(repoDir, safeRel);
|
|
13917
14317
|
const dirName = import_path3.default.dirname(fullPath);
|
|
13918
14318
|
assertWriteTargetNoSymlink(fullPath);
|
|
13919
|
-
await
|
|
14319
|
+
await import_promises3.default.mkdir(dirName, { recursive: true, mode: 448 });
|
|
13920
14320
|
await secureDirChain0700(repoDir, dirName);
|
|
13921
14321
|
assertWriteTargetNoSymlink(fullPath);
|
|
13922
14322
|
await writeFileNoFollow(fullPath, content);
|
|
@@ -13945,6 +14345,40 @@ async function indexedPartPaths(repoDir) {
|
|
|
13945
14345
|
const tracked = await import_isomorphic_git.default.listFiles({ fs: import_fs4.default, dir: repoDir });
|
|
13946
14346
|
return new Set(tracked.map((t) => toRepoRelative(t)).filter(isRepoPartPath));
|
|
13947
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
|
+
}
|
|
13948
14382
|
async function snapshotIsProvenComplete(repoDir, extraFiles, written) {
|
|
13949
14383
|
if (!extraFiles) return false;
|
|
13950
14384
|
try {
|
|
@@ -13963,7 +14397,7 @@ async function applySnapshotMarker(repoDir, complete) {
|
|
|
13963
14397
|
return;
|
|
13964
14398
|
}
|
|
13965
14399
|
try {
|
|
13966
|
-
await
|
|
14400
|
+
await import_promises3.default.unlink(import_path3.default.join(repoDir, SNAPSHOT_FORMAT_MARKER));
|
|
13967
14401
|
} catch {
|
|
13968
14402
|
}
|
|
13969
14403
|
try {
|
|
@@ -13978,7 +14412,7 @@ async function applySnapshotMarker(repoDir, complete) {
|
|
|
13978
14412
|
async function chmod0700BestEffort(dir) {
|
|
13979
14413
|
if (process.platform === "win32") return;
|
|
13980
14414
|
try {
|
|
13981
|
-
await
|
|
14415
|
+
await import_promises3.default.chmod(dir, 448);
|
|
13982
14416
|
} catch {
|
|
13983
14417
|
}
|
|
13984
14418
|
}
|
|
@@ -14011,7 +14445,7 @@ async function migrateExistingTree0700(repoDir) {
|
|
|
14011
14445
|
await chmod0700BestEffort(dir);
|
|
14012
14446
|
let entries = [];
|
|
14013
14447
|
try {
|
|
14014
|
-
entries = await
|
|
14448
|
+
entries = await import_promises3.default.readdir(dir, { withFileTypes: true });
|
|
14015
14449
|
} catch {
|
|
14016
14450
|
continue;
|
|
14017
14451
|
}
|
|
@@ -14061,7 +14495,7 @@ async function getSidecarBytes(repoDir) {
|
|
|
14061
14495
|
const dir = stack.pop();
|
|
14062
14496
|
let entries = [];
|
|
14063
14497
|
try {
|
|
14064
|
-
entries = await
|
|
14498
|
+
entries = await import_promises3.default.readdir(dir, { withFileTypes: true });
|
|
14065
14499
|
} catch {
|
|
14066
14500
|
continue;
|
|
14067
14501
|
}
|
|
@@ -14074,7 +14508,7 @@ async function getSidecarBytes(repoDir) {
|
|
|
14074
14508
|
continue;
|
|
14075
14509
|
}
|
|
14076
14510
|
if (typeof entry?.isFile === "function" && !entry.isFile()) continue;
|
|
14077
|
-
const st = await
|
|
14511
|
+
const st = await import_promises3.default.stat(full);
|
|
14078
14512
|
if (st.isDirectory()) stack.push(full);
|
|
14079
14513
|
else if (st.isFile()) total += st.size;
|
|
14080
14514
|
} catch {
|
|
@@ -14092,6 +14526,116 @@ async function getHistoryCommitCount(repoDir) {
|
|
|
14092
14526
|
return 0;
|
|
14093
14527
|
}
|
|
14094
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
|
+
}
|
|
14095
14639
|
var SIDECAR_TOMBSTONE_FILENAME = ".astra-tombstones.json";
|
|
14096
14640
|
var sidecarRegenWarnings = /* @__PURE__ */ new Map();
|
|
14097
14641
|
function resolveDocIdentity(target) {
|
|
@@ -14211,7 +14755,7 @@ async function checkSidecarTombstone(target, repoDir) {
|
|
|
14211
14755
|
const key = import_path3.default.resolve(repoDir);
|
|
14212
14756
|
let hasGit = false;
|
|
14213
14757
|
try {
|
|
14214
|
-
hasGit = (await
|
|
14758
|
+
hasGit = (await import_promises3.default.stat(import_path3.default.join(repoDir, ".git"))).isDirectory();
|
|
14215
14759
|
} catch {
|
|
14216
14760
|
hasGit = false;
|
|
14217
14761
|
}
|
|
@@ -14305,14 +14849,14 @@ async function assertUsableSidecarGit(repoDir) {
|
|
|
14305
14849
|
const gitPath = import_path3.default.join(repoDir, ".git");
|
|
14306
14850
|
let entry;
|
|
14307
14851
|
try {
|
|
14308
|
-
entry = await
|
|
14852
|
+
entry = await import_promises3.default.lstat(gitPath);
|
|
14309
14853
|
} catch {
|
|
14310
14854
|
return;
|
|
14311
14855
|
}
|
|
14312
14856
|
let usable = false;
|
|
14313
14857
|
if (entry.isSymbolicLink()) {
|
|
14314
14858
|
try {
|
|
14315
|
-
usable = (await
|
|
14859
|
+
usable = (await import_promises3.default.stat(gitPath)).isDirectory();
|
|
14316
14860
|
} catch {
|
|
14317
14861
|
usable = false;
|
|
14318
14862
|
}
|
|
@@ -14330,7 +14874,7 @@ async function assertUsableSidecarGit(repoDir) {
|
|
|
14330
14874
|
async function ensureAstraGitRepo(target) {
|
|
14331
14875
|
const repoDir = isRepoDirTarget(target) ? target : getRepoDir(target);
|
|
14332
14876
|
await assertUsableSidecarGit(repoDir);
|
|
14333
|
-
await
|
|
14877
|
+
await import_promises3.default.mkdir(repoDir, { recursive: true, mode: 448 });
|
|
14334
14878
|
if (process.platform !== "win32") {
|
|
14335
14879
|
await chmod0700BestEffort(repoDir);
|
|
14336
14880
|
const parent = import_path3.default.dirname(repoDir);
|
|
@@ -14346,17 +14890,44 @@ async function ensureAstraGitRepo(target) {
|
|
|
14346
14890
|
}
|
|
14347
14891
|
return repoDir;
|
|
14348
14892
|
}
|
|
14349
|
-
async function initAstraRepo(docPath, initialXml, extraFiles) {
|
|
14893
|
+
async function initAstraRepo(docPath, initialXml, extraFiles, originalBytes) {
|
|
14350
14894
|
const repoDir = await ensureAstraGitRepo(docPath);
|
|
14351
|
-
await
|
|
14352
|
-
|
|
14353
|
-
|
|
14354
|
-
|
|
14355
|
-
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
|
|
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
|
+
}
|
|
14920
|
+
}
|
|
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
|
+
});
|
|
14360
14931
|
});
|
|
14361
14932
|
try {
|
|
14362
14933
|
await checkSidecarTombstone(docPath, repoDir);
|
|
@@ -14368,6 +14939,7 @@ async function commitChange(docPath, xmlContent, message, extraFiles, deletedFil
|
|
|
14368
14939
|
const maxCommits = resolveHistoryMaxCommits(limits?.maxCommits);
|
|
14369
14940
|
const maxSidecarBytes = resolveHistoryMaxSidecarBytes(limits?.maxSidecarBytes);
|
|
14370
14941
|
const repoDir = await ensureAstraGitRepo(docPath);
|
|
14942
|
+
await assertGitHistoryReady(repoDir);
|
|
14371
14943
|
const existingCommits = await getHistoryCommitCount(repoDir);
|
|
14372
14944
|
if (existingCommits + 1 > maxCommits) {
|
|
14373
14945
|
throw Object.assign(
|
|
@@ -14446,7 +15018,7 @@ async function commitChange(docPath, xmlContent, message, extraFiles, deletedFil
|
|
|
14446
15018
|
if (isTraversalName(rel)) continue;
|
|
14447
15019
|
const fullPath = import_path3.default.join(repoDir, rel);
|
|
14448
15020
|
try {
|
|
14449
|
-
await
|
|
15021
|
+
await import_promises3.default.unlink(fullPath);
|
|
14450
15022
|
} catch {
|
|
14451
15023
|
}
|
|
14452
15024
|
try {
|
|
@@ -14469,8 +15041,27 @@ async function commitChange(docPath, xmlContent, message, extraFiles, deletedFil
|
|
|
14469
15041
|
}
|
|
14470
15042
|
return sha;
|
|
14471
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
|
+
}
|
|
14472
15062
|
async function getHistory(docPath) {
|
|
14473
15063
|
const repoDir = getRepoDir(docPath);
|
|
15064
|
+
await assertNoGitInitializationInProgress(repoDir);
|
|
14474
15065
|
await assertUsableSidecarGit(repoDir);
|
|
14475
15066
|
let commits;
|
|
14476
15067
|
try {
|
|
@@ -14513,7 +15104,7 @@ async function listRepoPaths(repoDir) {
|
|
|
14513
15104
|
async function walk(dir, relBase) {
|
|
14514
15105
|
let entries = [];
|
|
14515
15106
|
try {
|
|
14516
|
-
entries = await
|
|
15107
|
+
entries = await import_promises3.default.readdir(dir, { withFileTypes: true });
|
|
14517
15108
|
} catch {
|
|
14518
15109
|
return;
|
|
14519
15110
|
}
|
|
@@ -35195,7 +35786,7 @@ var ToonGenerator = class _ToonGenerator {
|
|
|
35195
35786
|
// ../engine/src/tmp.ts
|
|
35196
35787
|
var import_crypto5 = __toESM(require("crypto"));
|
|
35197
35788
|
var import_fs7 = __toESM(require("fs"));
|
|
35198
|
-
var
|
|
35789
|
+
var import_os2 = __toESM(require("os"));
|
|
35199
35790
|
var import_path6 = __toESM(require("path"));
|
|
35200
35791
|
var ASTRA_TMPDIR_ENV = "ASTRA_TMPDIR";
|
|
35201
35792
|
var TMP_SYMLINK_REMEDY = "Point $ASTRA_TMPDIR at a real directory (not a symlink), or unset it and retry.";
|
|
@@ -35251,7 +35842,7 @@ function resolveAstraTmpBase() {
|
|
|
35251
35842
|
try {
|
|
35252
35843
|
import_fs7.default.mkdirSync(dir, { recursive: true });
|
|
35253
35844
|
} catch {
|
|
35254
|
-
return
|
|
35845
|
+
return import_os2.default.tmpdir();
|
|
35255
35846
|
}
|
|
35256
35847
|
refuseSymlinkBase(dir);
|
|
35257
35848
|
refuseForeignOwner(dir);
|
|
@@ -35261,7 +35852,7 @@ function resolveAstraTmpBase() {
|
|
|
35261
35852
|
}
|
|
35262
35853
|
return dir;
|
|
35263
35854
|
}
|
|
35264
|
-
const fallback =
|
|
35855
|
+
const fallback = import_os2.default.tmpdir();
|
|
35265
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()));
|
|
35266
35857
|
if (injected.includes(import_path6.default.resolve(fallback))) {
|
|
35267
35858
|
const dir = import_path6.default.resolve(fallback);
|
|
@@ -35301,12 +35892,12 @@ function logForensics(p, reason) {
|
|
|
35301
35892
|
try {
|
|
35302
35893
|
const rawName = import_path6.default.basename(p);
|
|
35303
35894
|
const name2 = rawName !== "" ? rawName : "artifact";
|
|
35304
|
-
let
|
|
35895
|
+
let hash2 = "unknown";
|
|
35305
35896
|
try {
|
|
35306
|
-
|
|
35897
|
+
hash2 = import_crypto5.default.createHash("sha256").update(p).digest("hex").slice(0, 16);
|
|
35307
35898
|
} catch {
|
|
35308
35899
|
}
|
|
35309
|
-
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.
|
|
35310
35901
|
`);
|
|
35311
35902
|
} catch {
|
|
35312
35903
|
}
|
|
@@ -35370,12 +35961,12 @@ function logForensicPrune(target, reason) {
|
|
|
35370
35961
|
try {
|
|
35371
35962
|
const rawName = import_path6.default.basename(target);
|
|
35372
35963
|
const name2 = rawName !== "" ? rawName : "artifact";
|
|
35373
|
-
let
|
|
35964
|
+
let hash2 = "unknown";
|
|
35374
35965
|
try {
|
|
35375
|
-
|
|
35966
|
+
hash2 = import_crypto5.default.createHash("sha256").update(target).digest("hex").slice(0, 16);
|
|
35376
35967
|
} catch {
|
|
35377
35968
|
}
|
|
35378
|
-
process.stderr.write(`[ASTra] tmp forensics prune: name="${name2}" sha256="${
|
|
35969
|
+
process.stderr.write(`[ASTra] tmp forensics prune: name="${name2}" sha256="${hash2}" (${reason}).
|
|
35379
35970
|
`);
|
|
35380
35971
|
} catch {
|
|
35381
35972
|
}
|
|
@@ -37611,9 +38202,9 @@ function escapeHtml(str) {
|
|
|
37611
38202
|
|
|
37612
38203
|
// ../engine/src/export/pdf.ts
|
|
37613
38204
|
var import_child_process = require("child_process");
|
|
37614
|
-
var
|
|
37615
|
-
var
|
|
37616
|
-
var
|
|
38205
|
+
var fs9 = __toESM(require("fs"));
|
|
38206
|
+
var fsp4 = __toESM(require("fs/promises"));
|
|
38207
|
+
var path8 = __toESM(require("path"));
|
|
37617
38208
|
var import_url = require("url");
|
|
37618
38209
|
|
|
37619
38210
|
// ../engine/src/export/preflight.ts
|
|
@@ -38072,7 +38663,7 @@ var MAX_CONVERT_ATTEMPTS = 3;
|
|
|
38072
38663
|
async function removeStagedDir(dir) {
|
|
38073
38664
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
38074
38665
|
try {
|
|
38075
|
-
await
|
|
38666
|
+
await fsp4.rm(dir, { recursive: true, force: true });
|
|
38076
38667
|
return;
|
|
38077
38668
|
} catch {
|
|
38078
38669
|
if (attempt < 2) {
|
|
@@ -38084,10 +38675,10 @@ async function removeStagedDir(dir) {
|
|
|
38084
38675
|
}
|
|
38085
38676
|
async function convertWithFilter(docxBuffer, outPdfPath, filterSpec, options, binary, stagedForensics) {
|
|
38086
38677
|
const tmpDir = secureMkdtemp("astra-pdf-");
|
|
38087
|
-
const tmpDocxPath =
|
|
38678
|
+
const tmpDocxPath = path8.join(tmpDir, `doc_${Date.now()}.docx`);
|
|
38088
38679
|
let succeeded = false;
|
|
38089
38680
|
try {
|
|
38090
|
-
await
|
|
38681
|
+
await fsp4.writeFile(tmpDocxPath, docxBuffer);
|
|
38091
38682
|
secureFile(tmpDocxPath);
|
|
38092
38683
|
let converted = false;
|
|
38093
38684
|
let lastError = null;
|
|
@@ -38096,7 +38687,7 @@ async function convertWithFilter(docxBuffer, outPdfPath, filterSpec, options, bi
|
|
|
38096
38687
|
await new Promise((resolve10, reject) => {
|
|
38097
38688
|
const child = (0, import_child_process.spawn)(
|
|
38098
38689
|
binary,
|
|
38099
|
-
[`-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],
|
|
38100
38691
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
38101
38692
|
);
|
|
38102
38693
|
let stderr = "";
|
|
@@ -38132,23 +38723,23 @@ async function convertWithFilter(docxBuffer, outPdfPath, filterSpec, options, bi
|
|
|
38132
38723
|
if (!converted && lastError) {
|
|
38133
38724
|
throw lastError;
|
|
38134
38725
|
}
|
|
38135
|
-
const expectedPdfName =
|
|
38136
|
-
const generatedPdfPath =
|
|
38137
|
-
if (!
|
|
38726
|
+
const expectedPdfName = path8.basename(tmpDocxPath).replace(/\.docx$/i, ".pdf");
|
|
38727
|
+
const generatedPdfPath = path8.join(tmpDir, expectedPdfName);
|
|
38728
|
+
if (!fs9.existsSync(generatedPdfPath)) {
|
|
38138
38729
|
throw new Error(`PDF conversion failed: output file ${generatedPdfPath} was not created`);
|
|
38139
38730
|
}
|
|
38140
|
-
const targetDir =
|
|
38141
|
-
assertWriteTargetNoSymlink(
|
|
38142
|
-
await
|
|
38143
|
-
assertWriteTargetNoSymlink(
|
|
38144
|
-
await writeFileNoFollow(
|
|
38145
|
-
|
|
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);
|
|
38146
38737
|
succeeded = true;
|
|
38147
|
-
return
|
|
38738
|
+
return path8.resolve(outPdfPath);
|
|
38148
38739
|
} finally {
|
|
38149
38740
|
if (succeeded) {
|
|
38150
38741
|
try {
|
|
38151
|
-
await
|
|
38742
|
+
await fsp4.rm(tmpDir, { recursive: true, force: true });
|
|
38152
38743
|
} catch {
|
|
38153
38744
|
keepForForensics(tmpDir, "could not remove tmp dir after success");
|
|
38154
38745
|
}
|
|
@@ -38161,8 +38752,8 @@ async function convertWithFilter(docxBuffer, outPdfPath, filterSpec, options, bi
|
|
|
38161
38752
|
}
|
|
38162
38753
|
|
|
38163
38754
|
// ../engine/src/visual/backend.ts
|
|
38164
|
-
var
|
|
38165
|
-
var
|
|
38755
|
+
var fs10 = __toESM(require("fs"));
|
|
38756
|
+
var path9 = __toESM(require("path"));
|
|
38166
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).";
|
|
38167
38758
|
function createDependencyError(missing) {
|
|
38168
38759
|
const which = missing.length > 0 ? missing.join(", ") : "soffice, pdftoppm";
|
|
@@ -38177,9 +38768,9 @@ function findOnPath(name2, pathEnv) {
|
|
|
38177
38768
|
const trimmed = name2.trim();
|
|
38178
38769
|
if (trimmed.includes("/") || trimmed.includes("\\")) {
|
|
38179
38770
|
try {
|
|
38180
|
-
const abs =
|
|
38181
|
-
|
|
38182
|
-
const stat3 =
|
|
38771
|
+
const abs = path9.resolve(trimmed);
|
|
38772
|
+
fs10.accessSync(abs, fs10.constants.X_OK);
|
|
38773
|
+
const stat3 = fs10.statSync(abs);
|
|
38183
38774
|
if (stat3.isFile()) return abs;
|
|
38184
38775
|
return null;
|
|
38185
38776
|
} catch {
|
|
@@ -38188,12 +38779,12 @@ function findOnPath(name2, pathEnv) {
|
|
|
38188
38779
|
}
|
|
38189
38780
|
const pathValue = pathEnv !== void 0 ? pathEnv : process.env.PATH ?? "";
|
|
38190
38781
|
if (!pathValue) return null;
|
|
38191
|
-
const dirs = pathValue.split(
|
|
38782
|
+
const dirs = pathValue.split(path9.delimiter).filter((d) => d.length > 0);
|
|
38192
38783
|
for (const dir of dirs) {
|
|
38193
38784
|
try {
|
|
38194
|
-
const candidate =
|
|
38195
|
-
|
|
38196
|
-
const stat3 =
|
|
38785
|
+
const candidate = path9.join(dir, trimmed);
|
|
38786
|
+
fs10.accessSync(candidate, fs10.constants.X_OK);
|
|
38787
|
+
const stat3 = fs10.statSync(candidate);
|
|
38197
38788
|
if (stat3.isFile()) return candidate;
|
|
38198
38789
|
} catch {
|
|
38199
38790
|
continue;
|
|
@@ -38238,8 +38829,8 @@ function assertPdfBackend(override) {
|
|
|
38238
38829
|
}
|
|
38239
38830
|
|
|
38240
38831
|
// ../engine/src/visual/renderer.ts
|
|
38241
|
-
var
|
|
38242
|
-
var
|
|
38832
|
+
var fs11 = __toESM(require("fs"));
|
|
38833
|
+
var path10 = __toESM(require("path"));
|
|
38243
38834
|
var import_url2 = require("url");
|
|
38244
38835
|
var import_child_process2 = require("child_process");
|
|
38245
38836
|
var import_util = require("util");
|
|
@@ -38250,13 +38841,13 @@ var VISUAL_DEFAULT_MAX_PAGES = 4;
|
|
|
38250
38841
|
var VISUAL_MIN_DPI = 72;
|
|
38251
38842
|
var VISUAL_MAX_DPI = 300;
|
|
38252
38843
|
function getVisualArtifactsBaseDir() {
|
|
38253
|
-
const dir =
|
|
38844
|
+
const dir = path10.join(resolveAstraTmpBase(), "ASTra", "visual");
|
|
38254
38845
|
try {
|
|
38255
|
-
|
|
38846
|
+
fs11.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
38256
38847
|
} catch {
|
|
38257
38848
|
}
|
|
38258
38849
|
try {
|
|
38259
|
-
secureDir(
|
|
38850
|
+
secureDir(path10.join(resolveAstraTmpBase(), "ASTra"));
|
|
38260
38851
|
} catch {
|
|
38261
38852
|
}
|
|
38262
38853
|
secureDir(dir);
|
|
@@ -38266,9 +38857,9 @@ function getVisualArtifactsDir(subDir) {
|
|
|
38266
38857
|
const base = getVisualArtifactsBaseDir();
|
|
38267
38858
|
if (!subDir || subDir.trim() === "") return base;
|
|
38268
38859
|
const safe = subDir.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 64) || "run";
|
|
38269
|
-
const dir =
|
|
38860
|
+
const dir = path10.join(base, safe);
|
|
38270
38861
|
try {
|
|
38271
|
-
|
|
38862
|
+
fs11.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
38272
38863
|
} catch {
|
|
38273
38864
|
}
|
|
38274
38865
|
secureDir(dir);
|
|
@@ -38302,7 +38893,7 @@ function parsePageNumber(fileName) {
|
|
|
38302
38893
|
function listRenderedPages(workDir, prefixBase) {
|
|
38303
38894
|
let entries = [];
|
|
38304
38895
|
try {
|
|
38305
|
-
entries =
|
|
38896
|
+
entries = fs11.readdirSync(workDir);
|
|
38306
38897
|
} catch {
|
|
38307
38898
|
return [];
|
|
38308
38899
|
}
|
|
@@ -38311,7 +38902,7 @@ function listRenderedPages(workDir, prefixBase) {
|
|
|
38311
38902
|
if (!name2.startsWith(prefixBase)) continue;
|
|
38312
38903
|
const page = parsePageNumber(name2);
|
|
38313
38904
|
if (page === null) continue;
|
|
38314
|
-
out.push({ page, file:
|
|
38905
|
+
out.push({ page, file: path10.join(workDir, name2) });
|
|
38315
38906
|
}
|
|
38316
38907
|
out.sort((a, b) => a.page - b.page);
|
|
38317
38908
|
return out;
|
|
@@ -38325,7 +38916,7 @@ function sandboxError(message, remedy) {
|
|
|
38325
38916
|
function canonicalVisualBaseDir() {
|
|
38326
38917
|
const base = getVisualArtifactsBaseDir();
|
|
38327
38918
|
try {
|
|
38328
|
-
return
|
|
38919
|
+
return fs11.realpathSync(base);
|
|
38329
38920
|
} catch {
|
|
38330
38921
|
return base;
|
|
38331
38922
|
}
|
|
@@ -38349,7 +38940,7 @@ function resolveJailedVisualOutDir(rawOutDir) {
|
|
|
38349
38940
|
if (trimmed.includes("\n") || trimmed.includes("\r")) {
|
|
38350
38941
|
throw sandboxError("E_SANDBOX: visual outDir contains a forbidden line break.", "Remove line breaks from the path and retry.");
|
|
38351
38942
|
}
|
|
38352
|
-
const resolved =
|
|
38943
|
+
const resolved = path10.resolve(trimmed);
|
|
38353
38944
|
const canonical = canonicalizeForAsset(resolved);
|
|
38354
38945
|
const canonicalBase = canonicalVisualBaseDir();
|
|
38355
38946
|
assertVisualInsideBase(canonical, trimmed, canonicalBase);
|
|
@@ -38381,7 +38972,7 @@ async function renderDocxToImages(docxBuffer, opts = {}) {
|
|
|
38381
38972
|
outDir = getVisualArtifactsDir(runId);
|
|
38382
38973
|
}
|
|
38383
38974
|
try {
|
|
38384
|
-
|
|
38975
|
+
fs11.mkdirSync(outDir, { recursive: true });
|
|
38385
38976
|
} catch (err) {
|
|
38386
38977
|
const msg = err instanceof Error ? err.message : String(err);
|
|
38387
38978
|
throw validationError(`E_VALIDATION: cannot create artifacts dir "${outDir}": ${msg}`, "Pass a writable outDir and retry.");
|
|
@@ -38392,16 +38983,16 @@ async function renderDocxToImages(docxBuffer, opts = {}) {
|
|
|
38392
38983
|
outDir = live;
|
|
38393
38984
|
}
|
|
38394
38985
|
const workDir = secureMkdtemp("astra-visual-");
|
|
38395
|
-
const inputDocx =
|
|
38986
|
+
const inputDocx = path10.join(workDir, "input.docx");
|
|
38396
38987
|
let succeeded = false;
|
|
38397
38988
|
try {
|
|
38398
|
-
|
|
38989
|
+
fs11.writeFileSync(inputDocx, docxBuffer);
|
|
38399
38990
|
secureFile(inputDocx);
|
|
38400
38991
|
let lastErr;
|
|
38401
38992
|
let converted = false;
|
|
38402
38993
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
38403
38994
|
try {
|
|
38404
|
-
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], {
|
|
38405
38996
|
timeout: timeoutMs
|
|
38406
38997
|
});
|
|
38407
38998
|
converted = true;
|
|
@@ -38419,17 +39010,17 @@ async function renderDocxToImages(docxBuffer, opts = {}) {
|
|
|
38419
39010
|
}
|
|
38420
39011
|
let pdfPath = null;
|
|
38421
39012
|
try {
|
|
38422
|
-
const names =
|
|
39013
|
+
const names = fs11.readdirSync(workDir).filter((n) => n.toLowerCase().endsWith(".pdf"));
|
|
38423
39014
|
names.sort((a, b) => a === "input.pdf" ? -1 : b === "input.pdf" ? 1 : a.localeCompare(b));
|
|
38424
|
-
if (names.length > 0) pdfPath =
|
|
39015
|
+
if (names.length > 0) pdfPath = path10.join(workDir, names[0]);
|
|
38425
39016
|
} catch {
|
|
38426
39017
|
pdfPath = null;
|
|
38427
39018
|
}
|
|
38428
|
-
if (!pdfPath || !
|
|
39019
|
+
if (!pdfPath || !fs11.existsSync(pdfPath)) {
|
|
38429
39020
|
throw validationError("E_VALIDATION: libreoffice produced no PDF output.", "Verify the .docx opens in Word/LibreOffice and retry.");
|
|
38430
39021
|
}
|
|
38431
39022
|
const prefixBase = "page";
|
|
38432
|
-
const prefix =
|
|
39023
|
+
const prefix = path10.join(workDir, prefixBase);
|
|
38433
39024
|
const ppmArgs = format === "png" ? ["-png", "-r", String(dpi), pdfPath, prefix] : ["-jpeg", "-jpegopt", `quality=${jpegQuality}`, "-r", String(dpi), pdfPath, prefix];
|
|
38434
39025
|
try {
|
|
38435
39026
|
await execFileAsync(pdftoppmPath, ppmArgs, { timeout: Math.min(timeoutMs, 6e4) });
|
|
@@ -38448,15 +39039,15 @@ async function renderDocxToImages(docxBuffer, opts = {}) {
|
|
|
38448
39039
|
const ext = format === "png" ? "png" : "jpg";
|
|
38449
39040
|
for (const entry of selected) {
|
|
38450
39041
|
const destName = `page-${entry.page}.${ext}`;
|
|
38451
|
-
const dest =
|
|
38452
|
-
if (!isPathInsideDir(
|
|
39042
|
+
const dest = path10.join(outDir, destName);
|
|
39043
|
+
if (!isPathInsideDir(path10.resolve(dest), path10.resolve(outDir))) {
|
|
38453
39044
|
throw sandboxError(
|
|
38454
39045
|
`E_SANDBOX: visual artifact destination escapes outDir: "${dest}".`,
|
|
38455
39046
|
"Pass an outDir inside the visual artifacts dir (use getVisualArtifactsDir(subDir) for a run-scoped dir) and retry."
|
|
38456
39047
|
);
|
|
38457
39048
|
}
|
|
38458
39049
|
try {
|
|
38459
|
-
|
|
39050
|
+
fs11.copyFileSync(entry.file, dest);
|
|
38460
39051
|
secureFile(dest);
|
|
38461
39052
|
} catch (err) {
|
|
38462
39053
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -38465,7 +39056,7 @@ async function renderDocxToImages(docxBuffer, opts = {}) {
|
|
|
38465
39056
|
const canonicalDest = canonicalizeForAsset(dest);
|
|
38466
39057
|
if (!isPathInsideDir(canonicalDest, canonicalizeForAsset(outDir))) {
|
|
38467
39058
|
try {
|
|
38468
|
-
|
|
39059
|
+
fs11.unlinkSync(dest);
|
|
38469
39060
|
} catch {
|
|
38470
39061
|
}
|
|
38471
39062
|
throw sandboxError(
|
|
@@ -38475,7 +39066,7 @@ async function renderDocxToImages(docxBuffer, opts = {}) {
|
|
|
38475
39066
|
}
|
|
38476
39067
|
let bytes = 0;
|
|
38477
39068
|
try {
|
|
38478
|
-
bytes =
|
|
39069
|
+
bytes = fs11.statSync(dest).size;
|
|
38479
39070
|
} catch {
|
|
38480
39071
|
bytes = 0;
|
|
38481
39072
|
}
|
|
@@ -40791,9 +41382,39 @@ function summarizeStyleGuide(guide) {
|
|
|
40791
41382
|
return out;
|
|
40792
41383
|
}
|
|
40793
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
|
+
|
|
40794
41415
|
// src/lib/mcp/baseline-gate.ts
|
|
40795
41416
|
var crypto13 = __toESM(require("crypto"));
|
|
40796
|
-
var
|
|
41417
|
+
var fsp6 = __toESM(require("fs/promises"));
|
|
40797
41418
|
|
|
40798
41419
|
// src/lib/mcp/errors.ts
|
|
40799
41420
|
function mcpError(code, message, remedy) {
|
|
@@ -40873,8 +41494,8 @@ function invalidInput(message) {
|
|
|
40873
41494
|
}
|
|
40874
41495
|
|
|
40875
41496
|
// src/lib/mcp/diff.ts
|
|
40876
|
-
var
|
|
40877
|
-
var
|
|
41497
|
+
var fs14 = __toESM(require("fs"));
|
|
41498
|
+
var path13 = __toESM(require("path"));
|
|
40878
41499
|
var DISCARDED_NODE_IDS_MAX = 20;
|
|
40879
41500
|
var AUDIT_NODE_IDS_MAX = 100;
|
|
40880
41501
|
var AUDIT_FILES_MAX = 100;
|
|
@@ -41241,7 +41862,7 @@ function pathDirname(p) {
|
|
|
41241
41862
|
}
|
|
41242
41863
|
function sidecarRepoExists(docPath) {
|
|
41243
41864
|
try {
|
|
41244
|
-
return
|
|
41865
|
+
return fs14.existsSync(path13.join(getRepoDir(docPath), ".git"));
|
|
41245
41866
|
} catch {
|
|
41246
41867
|
return false;
|
|
41247
41868
|
}
|
|
@@ -41280,7 +41901,7 @@ async function resolveBaseline(handle) {
|
|
|
41280
41901
|
if (handle.validationBaseline !== void 0) return handle.validationBaseline;
|
|
41281
41902
|
if (!handle.openSha256) return null;
|
|
41282
41903
|
try {
|
|
41283
|
-
const bytes = await
|
|
41904
|
+
const bytes = await fsp6.readFile(handle.filePath);
|
|
41284
41905
|
if (sha256Hex(bytes) !== handle.openSha256) return null;
|
|
41285
41906
|
const baseline = buildBaseline(await validateForBaseline(bytes));
|
|
41286
41907
|
handle.validationBaseline = baseline;
|
|
@@ -41364,7 +41985,7 @@ function withToleratedNote(message, preExisting) {
|
|
|
41364
41985
|
async function validateAtOpen(handle, timeoutMs) {
|
|
41365
41986
|
let bytes;
|
|
41366
41987
|
try {
|
|
41367
|
-
bytes = await
|
|
41988
|
+
bytes = await fsp6.readFile(handle.filePath);
|
|
41368
41989
|
} catch (err) {
|
|
41369
41990
|
return { status: "skipped", note: `the file could not be read for validation: ${err instanceof Error ? err.message : String(err)}` };
|
|
41370
41991
|
}
|
|
@@ -41408,10 +42029,10 @@ async function validateAtOpen(handle, timeoutMs) {
|
|
|
41408
42029
|
}
|
|
41409
42030
|
|
|
41410
42031
|
// src/lib/session-store.ts
|
|
41411
|
-
var
|
|
41412
|
-
var
|
|
41413
|
-
var
|
|
41414
|
-
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"));
|
|
41415
42036
|
var import_crypto6 = require("crypto");
|
|
41416
42037
|
var MCP_SESSION_MAX_DOCS = 10;
|
|
41417
42038
|
var MCP_SESSION_MAX_BYTES = 200 * 1024 * 1024;
|
|
@@ -41609,19 +42230,19 @@ function getMcpAllowedRoots() {
|
|
|
41609
42230
|
if (named.length > 0) return [...new Set(named)];
|
|
41610
42231
|
const roots = [];
|
|
41611
42232
|
try {
|
|
41612
|
-
roots.push(
|
|
42233
|
+
roots.push(path14.resolve(process.cwd(), "documents"));
|
|
41613
42234
|
} catch {
|
|
41614
42235
|
}
|
|
41615
42236
|
try {
|
|
41616
|
-
roots.push(
|
|
42237
|
+
roots.push(path14.resolve(os3.tmpdir()));
|
|
41617
42238
|
} catch {
|
|
41618
42239
|
}
|
|
41619
42240
|
try {
|
|
41620
|
-
roots.push(
|
|
42241
|
+
roots.push(path14.resolve(process.cwd(), "documents", "Drafts"));
|
|
41621
42242
|
} catch {
|
|
41622
42243
|
}
|
|
41623
42244
|
try {
|
|
41624
|
-
roots.push(
|
|
42245
|
+
roots.push(path14.resolve(os3.tmpdir(), "ASTra"));
|
|
41625
42246
|
} catch {
|
|
41626
42247
|
}
|
|
41627
42248
|
return [...new Set(roots)];
|
|
@@ -41638,12 +42259,12 @@ function getExistingExtraRoots() {
|
|
|
41638
42259
|
const raw = (process.env.ASTRA_EXTRA_DOC_ROOTS || "").trim();
|
|
41639
42260
|
if (!raw) return [];
|
|
41640
42261
|
const out = [];
|
|
41641
|
-
for (const part of raw.split(
|
|
42262
|
+
for (const part of raw.split(path14.delimiter)) {
|
|
41642
42263
|
const p = part.trim();
|
|
41643
42264
|
if (!p) continue;
|
|
41644
42265
|
try {
|
|
41645
|
-
const resolved =
|
|
41646
|
-
const stat3 =
|
|
42266
|
+
const resolved = path14.resolve(p);
|
|
42267
|
+
const stat3 = fs15.statSync(resolved);
|
|
41647
42268
|
if (!stat3.isDirectory()) continue;
|
|
41648
42269
|
out.push(resolved);
|
|
41649
42270
|
} catch {
|
|
@@ -41655,7 +42276,7 @@ function findMcpContainingRoot(canonical) {
|
|
|
41655
42276
|
for (const root of getMcpAllowedRoots()) {
|
|
41656
42277
|
let canonicalRoot = root;
|
|
41657
42278
|
try {
|
|
41658
|
-
canonicalRoot =
|
|
42279
|
+
canonicalRoot = fs15.realpathSync(root);
|
|
41659
42280
|
} catch {
|
|
41660
42281
|
}
|
|
41661
42282
|
if (isPathInsideDir(canonical, canonicalRoot) || isPathInsideDir(canonical, root)) {
|
|
@@ -41665,8 +42286,8 @@ function findMcpContainingRoot(canonical) {
|
|
|
41665
42286
|
return void 0;
|
|
41666
42287
|
}
|
|
41667
42288
|
function assertNoDotSegments(canonical, root) {
|
|
41668
|
-
const rel =
|
|
41669
|
-
const segs = rel.split(
|
|
42289
|
+
const rel = path14.relative(root, canonical);
|
|
42290
|
+
const segs = rel.split(path14.sep).filter((s) => s && s !== "." && s !== "..");
|
|
41670
42291
|
for (const seg of segs) {
|
|
41671
42292
|
if (seg.startsWith(".")) {
|
|
41672
42293
|
throw Object.assign(new Error(`Document path traverses a hidden directory "${seg}"`), {
|
|
@@ -41675,7 +42296,7 @@ function assertNoDotSegments(canonical, root) {
|
|
|
41675
42296
|
});
|
|
41676
42297
|
}
|
|
41677
42298
|
}
|
|
41678
|
-
const base =
|
|
42299
|
+
const base = path14.basename(canonical);
|
|
41679
42300
|
if (base.startsWith(".")) {
|
|
41680
42301
|
throw Object.assign(new Error("Document path must not use a hidden (dot-prefixed) file name"), {
|
|
41681
42302
|
code: "E_SANDBOX",
|
|
@@ -41716,8 +42337,8 @@ function validateDocPath(rawPath) {
|
|
|
41716
42337
|
remedy: `Use a path ${describeAllowedFolders()}.`
|
|
41717
42338
|
});
|
|
41718
42339
|
}
|
|
41719
|
-
const resolved =
|
|
41720
|
-
const resolvedSegments = resolved.split(
|
|
42340
|
+
const resolved = path14.resolve(trimmed);
|
|
42341
|
+
const resolvedSegments = resolved.split(path14.sep);
|
|
41721
42342
|
if (resolvedSegments.includes("..")) {
|
|
41722
42343
|
throw Object.assign(new Error("Document path resolves outside its directory"), {
|
|
41723
42344
|
code: "E_SANDBOX",
|
|
@@ -41736,7 +42357,7 @@ function validateDocPath(rawPath) {
|
|
|
41736
42357
|
);
|
|
41737
42358
|
}
|
|
41738
42359
|
assertNoDotSegments(canonical, containingRoot);
|
|
41739
|
-
const ext =
|
|
42360
|
+
const ext = path14.extname(trimmed).toLowerCase();
|
|
41740
42361
|
if (ext !== ".docx") {
|
|
41741
42362
|
throw Object.assign(new Error(`Document path must use a .docx extension (got "${ext || "(none)"}")`), {
|
|
41742
42363
|
code: "E_VALIDATION",
|
|
@@ -41758,9 +42379,9 @@ function validateOpenBufferLabel(rawLabel) {
|
|
|
41758
42379
|
remedy: "Pass a plain memory label such as memory.docx."
|
|
41759
42380
|
});
|
|
41760
42381
|
}
|
|
41761
|
-
if (
|
|
42382
|
+
if (path14.isAbsolute(trimmed)) return validateDocPath(trimmed);
|
|
41762
42383
|
if (trimmed.includes("\n") || trimmed.includes("\r")) return fallback;
|
|
41763
|
-
const base =
|
|
42384
|
+
const base = path14.basename(trimmed.replace(/\\/g, "/"));
|
|
41764
42385
|
if (!base || base === "." || base === "..") return fallback;
|
|
41765
42386
|
const clean = base.slice(0, 128);
|
|
41766
42387
|
return clean.toLowerCase().endsWith(".docx") ? clean : fallback;
|
|
@@ -41774,29 +42395,141 @@ function collectTrackedExtrasForGit(parser2) {
|
|
|
41774
42395
|
}
|
|
41775
42396
|
return extras;
|
|
41776
42397
|
} catch (err) {
|
|
41777
|
-
|
|
41778
|
-
console.error(`[collectTrackedExtrasForGit] snapshot failed: ${msg}`);
|
|
41779
|
-
return {};
|
|
42398
|
+
throw snapshotCollectionError(err);
|
|
41780
42399
|
}
|
|
41781
42400
|
}
|
|
41782
|
-
|
|
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) {
|
|
41783
42412
|
const repoDir = getRepoDir(docPath);
|
|
41784
42413
|
let hasGit = false;
|
|
41785
42414
|
try {
|
|
41786
|
-
hasGit =
|
|
42415
|
+
hasGit = fs15.existsSync(path14.join(repoDir, ".git"));
|
|
41787
42416
|
} catch {
|
|
41788
42417
|
hasGit = false;
|
|
41789
42418
|
}
|
|
41790
|
-
if (hasGit)
|
|
41791
|
-
|
|
41792
|
-
|
|
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
|
+
}
|
|
41793
42434
|
}
|
|
41794
|
-
|
|
42435
|
+
function collectBaselineForGit(parser2) {
|
|
42436
|
+
const xml = parser2.readDocumentXml();
|
|
41795
42437
|
try {
|
|
41796
|
-
|
|
41797
|
-
const
|
|
41798
|
-
|
|
41799
|
-
|
|
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
|
+
}
|
|
41800
42533
|
}
|
|
41801
42534
|
}
|
|
41802
42535
|
var McpSessionStore = class {
|
|
@@ -42046,12 +42779,12 @@ var McpSessionStore = class {
|
|
|
42046
42779
|
* Macro/active content refuses by default (E_VALIDATION naming the parts);
|
|
42047
42780
|
* pass {allowMacros:true} to preserve or {stripMacros:true} to strip.
|
|
42048
42781
|
*/
|
|
42049
|
-
async openDocument(filePath, sessionId, macroOpts) {
|
|
42782
|
+
async openDocument(filePath, sessionId, macroOpts, captureInput = true) {
|
|
42050
42783
|
const cleanPath = validateDocPath(filePath);
|
|
42051
42784
|
const expiredEvicted = this.evictExpired();
|
|
42052
42785
|
let buf;
|
|
42053
42786
|
try {
|
|
42054
|
-
buf = await
|
|
42787
|
+
buf = await fsp7.readFile(cleanPath);
|
|
42055
42788
|
} catch (err) {
|
|
42056
42789
|
const msg = err instanceof Error ? err.message : String(err);
|
|
42057
42790
|
throw Object.assign(new Error(`open_document: cannot read "${cleanPath}": ${msg}`), {
|
|
@@ -42086,10 +42819,12 @@ var McpSessionStore = class {
|
|
|
42086
42819
|
lastAccess: Date.now(),
|
|
42087
42820
|
mutex: new SessionMutex(),
|
|
42088
42821
|
redoStack: this.createRedoStack(),
|
|
42089
|
-
pendingAudit: { ops: 0, nodeIds: [] }
|
|
42822
|
+
pendingAudit: { ops: 0, nodeIds: [] },
|
|
42823
|
+
lastWrittenSha256: (0, import_crypto6.createHash)("sha256").update(buf).digest("hex"),
|
|
42824
|
+
macroPolicy: macroOpts
|
|
42090
42825
|
};
|
|
42091
42826
|
this.sessions.set(id, handle);
|
|
42092
|
-
await initGitForHandleBestEffort(handle);
|
|
42827
|
+
await initGitForHandleBestEffort(handle, captureInput ? buf : void 0);
|
|
42093
42828
|
this.lastOpenEvicted = [...expiredEvicted, ...this.evictToFit(id)];
|
|
42094
42829
|
const live = this.sessions.get(id);
|
|
42095
42830
|
if (!live) {
|
|
@@ -42156,10 +42891,13 @@ var McpSessionStore = class {
|
|
|
42156
42891
|
lastAccess: Date.now(),
|
|
42157
42892
|
mutex: new SessionMutex(),
|
|
42158
42893
|
redoStack: this.createRedoStack(),
|
|
42159
|
-
pendingAudit: { ops: 0, nodeIds: [] }
|
|
42894
|
+
pendingAudit: { ops: 0, nodeIds: [] },
|
|
42895
|
+
lastWrittenSha256: (0, import_crypto6.createHash)("sha256").update(buf).digest("hex"),
|
|
42896
|
+
macroPolicy: macroOpts
|
|
42160
42897
|
};
|
|
42161
42898
|
this.sessions.set(id, handle);
|
|
42162
|
-
await initGitForHandleBestEffort(handle);
|
|
42899
|
+
await initGitForHandleBestEffort(handle, buf);
|
|
42900
|
+
if (!path14.isAbsolute(handle.filePath)) handle.originalInfo = { available: false, reason: "not-applicable" };
|
|
42163
42901
|
this.lastOpenEvicted = [...expiredEvicted, ...this.evictToFit(id)];
|
|
42164
42902
|
const live = this.sessions.get(id);
|
|
42165
42903
|
if (!live) {
|
|
@@ -42334,6 +43072,8 @@ var LEAN6_TOOL_NAMES = [
|
|
|
42334
43072
|
];
|
|
42335
43073
|
var GIT_HISTORY_TOOL_NAMES = [
|
|
42336
43074
|
"get_document_history",
|
|
43075
|
+
"export_original_document",
|
|
43076
|
+
"restore_original_document",
|
|
42337
43077
|
"preview_revert",
|
|
42338
43078
|
"revert_document",
|
|
42339
43079
|
"redo_document"
|
|
@@ -42381,8 +43121,10 @@ var LEAN6_DESCRIPTIONS = {
|
|
|
42381
43121
|
)
|
|
42382
43122
|
};
|
|
42383
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.",
|
|
42384
43126
|
get_document_history: withSharedDocs(
|
|
42385
|
-
'List the saved git history for a session (newest first, read-only: never mutates, never flips dirty). Paginate with limit (1-100, default 20) and offset (default 0). Returns {ok:true,sessionId,documentPath,currentCommit,totalCommits,history:[{oid,message,author,timestamp,timestampMs,opCount,affectedNodeIds,resultingHeadOid}]} (timestampMs = ms-precision epoch millis; commit-time clock for tracked commits, git seconds for older ones; opCount is always present (0 for pre-tracking commits and heals-only saves: it folds operations, not normalizer rewrites); affectedNodeIds is always present ([] when untracked); resultingHeadOid always equals oid (append-only; refs never move). Example {"sessionId":"sess_abc","limit":10} -> history slice. Use the oids with revert_document (or preview_revert first) to restore an earlier saved state. '
|
|
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. '
|
|
42386
43128
|
),
|
|
42387
43129
|
preview_revert: withSharedDocs(
|
|
42388
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.`
|
|
@@ -42571,6 +43313,15 @@ var ImportDocumentSchema = import_zod4.z.object({
|
|
|
42571
43313
|
// Read $...$ as inline math (off by default: prices and shell text can look like math).
|
|
42572
43314
|
dollarMath: import_zod4.z.boolean().optional()
|
|
42573
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();
|
|
42574
43325
|
|
|
42575
43326
|
// src/lib/mcp/handlers/open-inspect.ts
|
|
42576
43327
|
function extractStylePalette2(parser2) {
|
|
@@ -42664,7 +43415,7 @@ async function handleOpenDocument(store, input) {
|
|
|
42664
43415
|
const evicted = store.drainLastOpenEvicted();
|
|
42665
43416
|
const pendingNormalizations = readPendingNormalizations(handle);
|
|
42666
43417
|
const validation = await validateAtOpen(handle, parsed.data.validationTimeoutMs ?? openValidationTimeoutMs());
|
|
42667
|
-
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 } : {} };
|
|
42668
43419
|
} catch (err) {
|
|
42669
43420
|
return mcpErrorFrom(err, "E_IO", "Check the .docx path is readable and retry open_document.");
|
|
42670
43421
|
}
|
|
@@ -42889,11 +43640,11 @@ async function handleInspectDocument(store, input) {
|
|
|
42889
43640
|
}
|
|
42890
43641
|
|
|
42891
43642
|
// src/lib/mcp/handlers/search-batch.ts
|
|
42892
|
-
var
|
|
43643
|
+
var path16 = __toESM(require("path"));
|
|
42893
43644
|
|
|
42894
43645
|
// src/tools/compose-document.ts
|
|
42895
|
-
var
|
|
42896
|
-
var
|
|
43646
|
+
var fs16 = __toESM(require("fs"));
|
|
43647
|
+
var path15 = __toESM(require("path"));
|
|
42897
43648
|
var import_zod5 = require("zod");
|
|
42898
43649
|
var COMPOSE_MARKDOWN_MAX = 5e5;
|
|
42899
43650
|
var COMPOSE_VALIDATION_ERRORS_MAX = 3;
|
|
@@ -42908,8 +43659,8 @@ function resolveMcpStyleGuide(ref, jail) {
|
|
|
42908
43659
|
let anchor;
|
|
42909
43660
|
if (jail?.allowedDir !== void 0) {
|
|
42910
43661
|
anchor = jail.allowedDir;
|
|
42911
|
-
} else if (jail?.sessionFilePath !== void 0 &&
|
|
42912
|
-
anchor = canonicalizeForAsset(
|
|
43662
|
+
} else if (jail?.sessionFilePath !== void 0 && path15.isAbsolute(jail.sessionFilePath)) {
|
|
43663
|
+
anchor = canonicalizeForAsset(path15.resolve(path15.dirname(jail.sessionFilePath)));
|
|
42913
43664
|
}
|
|
42914
43665
|
if (anchor === void 0 && !hasGuideScheme(trimmed) && isFileGuideRef(trimmed)) {
|
|
42915
43666
|
throw guideSandboxError(
|
|
@@ -43008,22 +43759,22 @@ function assertPathAllowed(allowedDir, roots) {
|
|
|
43008
43759
|
remedy: `Use a folder ${describeAllowedFolders()}.`
|
|
43009
43760
|
});
|
|
43010
43761
|
}
|
|
43011
|
-
const resolved =
|
|
43762
|
+
const resolved = path15.resolve(trimmed);
|
|
43012
43763
|
const canonical = canonicalizeForAsset(resolved);
|
|
43013
43764
|
const inside = roots.some((root) => {
|
|
43014
43765
|
let canonicalRoot = root;
|
|
43015
43766
|
try {
|
|
43016
|
-
canonicalRoot =
|
|
43767
|
+
canonicalRoot = fs16.realpathSync(root);
|
|
43017
43768
|
} catch {
|
|
43018
43769
|
}
|
|
43019
|
-
const normC =
|
|
43020
|
-
const normR =
|
|
43770
|
+
const normC = path15.normalize(canonical);
|
|
43771
|
+
const normR = path15.normalize(canonicalRoot);
|
|
43021
43772
|
if (process.platform === "win32") {
|
|
43022
43773
|
const lc = normC.toLowerCase();
|
|
43023
43774
|
const lr = normR.toLowerCase();
|
|
43024
|
-
return lc === lr || lc.startsWith(lr +
|
|
43775
|
+
return lc === lr || lc.startsWith(lr + path15.sep.toLowerCase());
|
|
43025
43776
|
}
|
|
43026
|
-
return normC === normR || normC.startsWith(normR +
|
|
43777
|
+
return normC === normR || normC.startsWith(normR + path15.sep);
|
|
43027
43778
|
});
|
|
43028
43779
|
if (!inside) {
|
|
43029
43780
|
throw Object.assign(
|
|
@@ -43401,8 +44152,8 @@ async function handleExecuteBatch(store, input) {
|
|
|
43401
44152
|
const explicit = parsed.data.allowedDir;
|
|
43402
44153
|
if (explicit !== void 0) {
|
|
43403
44154
|
batchAllowedDir = assertPathAllowed(explicit, getMcpAllowedRoots());
|
|
43404
|
-
} else if (
|
|
43405
|
-
batchAllowedDir = canonicalizeForAsset(
|
|
44155
|
+
} else if (path16.isAbsolute(handle.filePath)) {
|
|
44156
|
+
batchAllowedDir = canonicalizeForAsset(path16.resolve(path16.dirname(handle.filePath)));
|
|
43406
44157
|
} else {
|
|
43407
44158
|
throw Object.assign(
|
|
43408
44159
|
new Error(
|
|
@@ -43551,8 +44302,9 @@ async function handleExecuteBatch(store, input) {
|
|
|
43551
44302
|
}
|
|
43552
44303
|
|
|
43553
44304
|
// src/lib/mcp/handlers/save-close.ts
|
|
43554
|
-
var
|
|
43555
|
-
var
|
|
44305
|
+
var import_crypto7 = require("crypto");
|
|
44306
|
+
var fsp8 = __toESM(require("fs/promises"));
|
|
44307
|
+
var path17 = __toESM(require("path"));
|
|
43556
44308
|
async function executeSaveDocument(store, input) {
|
|
43557
44309
|
const parsed = SaveDocumentSchema.safeParse(input);
|
|
43558
44310
|
if (!parsed.success) {
|
|
@@ -43598,18 +44350,18 @@ async function executeSaveDocument(store, input) {
|
|
|
43598
44350
|
),
|
|
43599
44351
|
{
|
|
43600
44352
|
code: "E_VALIDATION",
|
|
43601
|
-
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`
|
|
43602
44354
|
}
|
|
43603
44355
|
);
|
|
43604
44356
|
}
|
|
43605
44357
|
if (isStripped && macroParts.length === 0 && /\.(docm|dotm)$/i.test(target)) {
|
|
43606
44358
|
throw Object.assign(
|
|
43607
44359
|
new Error(
|
|
43608
|
-
`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`
|
|
43609
44361
|
),
|
|
43610
44362
|
{
|
|
43611
44363
|
code: "E_VALIDATION",
|
|
43612
|
-
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`
|
|
43613
44365
|
}
|
|
43614
44366
|
);
|
|
43615
44367
|
}
|
|
@@ -43650,19 +44402,22 @@ async function executeSaveDocument(store, input) {
|
|
|
43650
44402
|
const saveGateNormalized = readParserNormalizations(handle.parser);
|
|
43651
44403
|
const buf = handle.parser.rezip();
|
|
43652
44404
|
const saveNormalized = mergeNormalized(saveGateNormalized, readParserNormalizations(handle.parser));
|
|
44405
|
+
const savedXml = handle.parser.readDocumentXml();
|
|
44406
|
+
const extras = collectTrackedExtrasForGit(handle.parser);
|
|
43653
44407
|
const prevRevision = handle.revision;
|
|
43654
44408
|
const prevSavedRevision = handle.savedRevision;
|
|
43655
44409
|
const prevByteSize = handle.byteSize;
|
|
43656
44410
|
const prevFilePath = handle.filePath;
|
|
43657
44411
|
let prevBytes = null;
|
|
43658
44412
|
try {
|
|
43659
|
-
prevBytes = await
|
|
44413
|
+
prevBytes = await fsp8.readFile(target);
|
|
43660
44414
|
} catch {
|
|
43661
44415
|
prevBytes = null;
|
|
43662
44416
|
}
|
|
44417
|
+
await assertHistoryReadyForWrite(target);
|
|
43663
44418
|
try {
|
|
43664
44419
|
assertWriteTargetNoSymlink(target);
|
|
43665
|
-
await
|
|
44420
|
+
await fsp8.mkdir(pathDirname(target), { recursive: true });
|
|
43666
44421
|
assertWriteTargetNoSymlink(target);
|
|
43667
44422
|
await writeFileNoFollow(target, buf);
|
|
43668
44423
|
} catch (err) {
|
|
@@ -43673,7 +44428,7 @@ async function executeSaveDocument(store, input) {
|
|
|
43673
44428
|
if (prevBytes !== null) {
|
|
43674
44429
|
await writeFileNoFollow(target, prevBytes);
|
|
43675
44430
|
} else {
|
|
43676
|
-
await
|
|
44431
|
+
await fsp8.unlink(target);
|
|
43677
44432
|
}
|
|
43678
44433
|
} catch (restoreErr) {
|
|
43679
44434
|
diskOk = false;
|
|
@@ -43685,8 +44440,6 @@ async function executeSaveDocument(store, input) {
|
|
|
43685
44440
|
remedy
|
|
43686
44441
|
});
|
|
43687
44442
|
}
|
|
43688
|
-
const savedXml = handle.parser.readDocumentXml();
|
|
43689
|
-
const extras = collectTrackedExtrasForGit(handle.parser);
|
|
43690
44443
|
const opsSinceSave = handle.revision - handle.savedRevision;
|
|
43691
44444
|
const fallbackMessage = opsSinceSave <= 0 ? `save_document: rev ${handle.revision} (initial baseline)` : `save_document: rev ${handle.revision} (${opsSinceSave} ops since last save)`;
|
|
43692
44445
|
const baseMessage = sanitizeCommitMessage(parsed.data.message, fallbackMessage);
|
|
@@ -43694,7 +44447,7 @@ async function executeSaveDocument(store, input) {
|
|
|
43694
44447
|
const authorName = sanitizeCommitAuthor(parsed.data.author);
|
|
43695
44448
|
let commitOid;
|
|
43696
44449
|
try {
|
|
43697
|
-
await ensureGitRepo(target, savedXml, extras);
|
|
44450
|
+
await ensureGitRepo(target, savedXml, extras, prevBytes ?? void 0);
|
|
43698
44451
|
commitOid = await commitChange(target, savedXml, commitMessage, extras, void 0, authorName);
|
|
43699
44452
|
} catch (err) {
|
|
43700
44453
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -43703,7 +44456,7 @@ async function executeSaveDocument(store, input) {
|
|
|
43703
44456
|
if (prevBytes !== null) {
|
|
43704
44457
|
await writeFileNoFollow(target, prevBytes);
|
|
43705
44458
|
} else {
|
|
43706
|
-
await
|
|
44459
|
+
await fsp8.unlink(target);
|
|
43707
44460
|
}
|
|
43708
44461
|
} catch (restoreErr) {
|
|
43709
44462
|
diskOk = false;
|
|
@@ -43724,6 +44477,15 @@ async function executeSaveDocument(store, input) {
|
|
|
43724
44477
|
}
|
|
43725
44478
|
try {
|
|
43726
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");
|
|
43727
44489
|
handle.byteSize = buf.length;
|
|
43728
44490
|
const auditNodeIds = [...handle.pendingAudit.nodeIds];
|
|
43729
44491
|
const auditOps = handle.pendingAudit.ops;
|
|
@@ -43761,6 +44523,7 @@ async function executeSaveDocument(store, input) {
|
|
|
43761
44523
|
ok: true,
|
|
43762
44524
|
sessionId: handle.sessionId,
|
|
43763
44525
|
path: target,
|
|
44526
|
+
...handle.originalWarning ? { originalWarning: handle.originalWarning } : {},
|
|
43764
44527
|
revision: handle.revision,
|
|
43765
44528
|
commitOid,
|
|
43766
44529
|
commitMessage,
|
|
@@ -43830,8 +44593,151 @@ async function handleCloseDocument(store, input) {
|
|
|
43830
44593
|
}
|
|
43831
44594
|
}
|
|
43832
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
|
+
|
|
43833
44738
|
// src/lib/mcp/handlers/history.ts
|
|
43834
|
-
var
|
|
44739
|
+
var import_crypto9 = require("crypto");
|
|
44740
|
+
var fsp10 = __toESM(require("fs/promises"));
|
|
43835
44741
|
async function handleGetDocumentHistory(store, input) {
|
|
43836
44742
|
const parsed = GetDocumentHistorySchema.safeParse(input);
|
|
43837
44743
|
if (!parsed.success) {
|
|
@@ -43858,7 +44764,21 @@ async function handleGetDocumentHistory(store, input) {
|
|
|
43858
44764
|
currentCommit: null,
|
|
43859
44765
|
totalCommits: 0,
|
|
43860
44766
|
history: [],
|
|
43861
|
-
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
|
|
43862
44782
|
};
|
|
43863
44783
|
}
|
|
43864
44784
|
const full = await getHistory(filePath);
|
|
@@ -43892,7 +44812,10 @@ async function handleGetDocumentHistory(store, input) {
|
|
|
43892
44812
|
currentCommit: full.length > 0 ? full[0].oid : null,
|
|
43893
44813
|
totalCommits: full.length,
|
|
43894
44814
|
history: slice,
|
|
43895
|
-
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
|
|
43896
44819
|
};
|
|
43897
44820
|
});
|
|
43898
44821
|
} catch (err) {
|
|
@@ -43994,7 +44917,7 @@ async function handleRevertDocument(store, input) {
|
|
|
43994
44917
|
const prevByteSize = handle.byteSize;
|
|
43995
44918
|
let prevFileBytes = null;
|
|
43996
44919
|
try {
|
|
43997
|
-
prevFileBytes = await
|
|
44920
|
+
prevFileBytes = await fsp10.readFile(handle.filePath);
|
|
43998
44921
|
} catch {
|
|
43999
44922
|
prevFileBytes = null;
|
|
44000
44923
|
}
|
|
@@ -44017,6 +44940,7 @@ async function handleRevertDocument(store, input) {
|
|
|
44017
44940
|
truncated: false
|
|
44018
44941
|
};
|
|
44019
44942
|
}
|
|
44943
|
+
await assertHistoryReadyForWrite(handle.filePath);
|
|
44020
44944
|
let target;
|
|
44021
44945
|
try {
|
|
44022
44946
|
target = await checkoutCommit(handle.filePath, targetOid);
|
|
@@ -44046,7 +44970,7 @@ async function handleRevertDocument(store, input) {
|
|
|
44046
44970
|
}
|
|
44047
44971
|
assertWriteTargetNoSymlink(handle.filePath);
|
|
44048
44972
|
writeAttempted = true;
|
|
44049
|
-
await
|
|
44973
|
+
await fsp10.mkdir(pathDirname(handle.filePath), { recursive: true });
|
|
44050
44974
|
assertWriteTargetNoSymlink(handle.filePath);
|
|
44051
44975
|
await writeFileNoFollow(handle.filePath, buf);
|
|
44052
44976
|
handle.byteSize = buf.length;
|
|
@@ -44075,6 +44999,7 @@ async function handleRevertDocument(store, input) {
|
|
|
44075
44999
|
affectedNodeIds: diffIdTexts(liveBefore.texts, liveAfter.order, liveAfter.texts)
|
|
44076
45000
|
});
|
|
44077
45001
|
store.markSaved(handle);
|
|
45002
|
+
handle.lastWrittenSha256 = (0, import_crypto9.createHash)("sha256").update(buf).digest("hex");
|
|
44078
45003
|
adoptBaseline(handle, candidate);
|
|
44079
45004
|
return {
|
|
44080
45005
|
ok: true,
|
|
@@ -44110,7 +45035,7 @@ async function handleRevertDocument(store, input) {
|
|
|
44110
45035
|
if (prevFileBytes !== null) {
|
|
44111
45036
|
await writeFileNoFollow(handle.filePath, prevFileBytes);
|
|
44112
45037
|
} else {
|
|
44113
|
-
await
|
|
45038
|
+
await fsp10.unlink(handle.filePath);
|
|
44114
45039
|
}
|
|
44115
45040
|
} catch (restoreErr) {
|
|
44116
45041
|
diskOk = false;
|
|
@@ -44167,6 +45092,7 @@ async function handleRedoDocument(store, input) {
|
|
|
44167
45092
|
const redoReasonProvided = "reason" in rawRedo && parsed.data.reason !== void 0;
|
|
44168
45093
|
const redoReasonText = redoReasonProvided ? sanitizeCommitMessage(parsed.data.reason, "") : "";
|
|
44169
45094
|
const authorName = sanitizeCommitAuthor(parsed.data.author);
|
|
45095
|
+
await assertHistoryReadyForWrite(handle.filePath);
|
|
44170
45096
|
const redoBefore = collectAstIdTexts(handle.ast);
|
|
44171
45097
|
const prevRevision = handle.revision;
|
|
44172
45098
|
const prevSavedRevision = handle.savedRevision;
|
|
@@ -44174,7 +45100,7 @@ async function handleRedoDocument(store, input) {
|
|
|
44174
45100
|
const preRedoSnap = handle.parser.capturePackageSnapshot();
|
|
44175
45101
|
let prevFileBytes = null;
|
|
44176
45102
|
try {
|
|
44177
|
-
prevFileBytes = await
|
|
45103
|
+
prevFileBytes = await fsp10.readFile(handle.filePath);
|
|
44178
45104
|
} catch {
|
|
44179
45105
|
prevFileBytes = null;
|
|
44180
45106
|
}
|
|
@@ -44194,7 +45120,7 @@ async function handleRedoDocument(store, input) {
|
|
|
44194
45120
|
didPop = true;
|
|
44195
45121
|
assertWriteTargetNoSymlink(handle.filePath);
|
|
44196
45122
|
writeAttempted = true;
|
|
44197
|
-
await
|
|
45123
|
+
await fsp10.mkdir(pathDirname(handle.filePath), { recursive: true });
|
|
44198
45124
|
assertWriteTargetNoSymlink(handle.filePath);
|
|
44199
45125
|
await writeFileNoFollow(handle.filePath, buf);
|
|
44200
45126
|
handle.byteSize = buf.length;
|
|
@@ -44227,6 +45153,7 @@ async function handleRedoDocument(store, input) {
|
|
|
44227
45153
|
affectedNodeIds: diffIdTexts(redoBefore.texts, redoAfter.order, redoAfter.texts)
|
|
44228
45154
|
});
|
|
44229
45155
|
store.markSaved(handle);
|
|
45156
|
+
handle.lastWrittenSha256 = (0, import_crypto9.createHash)("sha256").update(buf).digest("hex");
|
|
44230
45157
|
adoptBaseline(handle, candidate);
|
|
44231
45158
|
return {
|
|
44232
45159
|
ok: true,
|
|
@@ -44261,7 +45188,7 @@ async function handleRedoDocument(store, input) {
|
|
|
44261
45188
|
if (prevFileBytes !== null) {
|
|
44262
45189
|
await writeFileNoFollow(handle.filePath, prevFileBytes);
|
|
44263
45190
|
} else {
|
|
44264
|
-
await
|
|
45191
|
+
await fsp10.unlink(handle.filePath);
|
|
44265
45192
|
}
|
|
44266
45193
|
} catch (restoreErr) {
|
|
44267
45194
|
diskOk = false;
|
|
@@ -44455,6 +45382,8 @@ async function handlePreviewRevert(store, input) {
|
|
|
44455
45382
|
}
|
|
44456
45383
|
}
|
|
44457
45384
|
var GIT_HISTORY_HANDLERS = {
|
|
45385
|
+
export_original_document: handleExportOriginalDocument,
|
|
45386
|
+
restore_original_document: handleRestoreOriginalDocument,
|
|
44458
45387
|
get_document_history: handleGetDocumentHistory,
|
|
44459
45388
|
preview_revert: handlePreviewRevert,
|
|
44460
45389
|
revert_document: handleRevertDocument,
|
|
@@ -44502,9 +45431,9 @@ function adoptForkNumbering(fork, ast) {
|
|
|
44502
45431
|
}
|
|
44503
45432
|
|
|
44504
45433
|
// src/tools/export.ts
|
|
44505
|
-
var
|
|
44506
|
-
var
|
|
44507
|
-
var
|
|
45434
|
+
var fs17 = __toESM(require("fs"));
|
|
45435
|
+
var fsp11 = __toESM(require("fs/promises"));
|
|
45436
|
+
var path19 = __toESM(require("path"));
|
|
44508
45437
|
var ExportDocumentSchema = import_zod6.z.object({
|
|
44509
45438
|
sessionId: import_zod6.z.string().describe("Active document session ID returned by open_document"),
|
|
44510
45439
|
format: import_zod6.z.enum(["html", "markdown", "md", "pdf"]).describe('Target export format: "html", "markdown", "md", or "pdf"'),
|
|
@@ -44560,18 +45489,18 @@ function validateExportTarget(rawPath, expectedFormat) {
|
|
|
44560
45489
|
remedy: "Use a path inside the workspace documents directory."
|
|
44561
45490
|
});
|
|
44562
45491
|
}
|
|
44563
|
-
const resolved =
|
|
45492
|
+
const resolved = path19.resolve(trimmed);
|
|
44564
45493
|
const canonical = canonicalizeForAsset(resolved);
|
|
44565
45494
|
const allowedRoots = getMcpAllowedRoots();
|
|
44566
45495
|
const isInside2 = allowedRoots.some((root) => {
|
|
44567
45496
|
let canonicalRoot = root;
|
|
44568
45497
|
try {
|
|
44569
|
-
canonicalRoot =
|
|
45498
|
+
canonicalRoot = fs17.realpathSync(root);
|
|
44570
45499
|
} catch {
|
|
44571
45500
|
}
|
|
44572
|
-
const normC =
|
|
44573
|
-
const normR =
|
|
44574
|
-
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);
|
|
44575
45504
|
});
|
|
44576
45505
|
if (!isInside2) {
|
|
44577
45506
|
throw Object.assign(
|
|
@@ -44582,7 +45511,7 @@ function validateExportTarget(rawPath, expectedFormat) {
|
|
|
44582
45511
|
}
|
|
44583
45512
|
);
|
|
44584
45513
|
}
|
|
44585
|
-
const ext =
|
|
45514
|
+
const ext = path19.extname(trimmed).toLowerCase();
|
|
44586
45515
|
const validExts = {
|
|
44587
45516
|
html: [".html", ".htm"],
|
|
44588
45517
|
markdown: [".md", ".markdown"],
|
|
@@ -44626,18 +45555,18 @@ function validateAssetTarget(rawPath) {
|
|
|
44626
45555
|
remedy: "Use a path inside the workspace documents directory."
|
|
44627
45556
|
});
|
|
44628
45557
|
}
|
|
44629
|
-
const resolved =
|
|
45558
|
+
const resolved = path19.resolve(trimmed);
|
|
44630
45559
|
const canonical = canonicalizeForAsset(resolved);
|
|
44631
45560
|
const allowedRoots = getMcpAllowedRoots();
|
|
44632
45561
|
const isInside2 = allowedRoots.some((root) => {
|
|
44633
45562
|
let canonicalRoot = root;
|
|
44634
45563
|
try {
|
|
44635
|
-
canonicalRoot =
|
|
45564
|
+
canonicalRoot = fs17.realpathSync(root);
|
|
44636
45565
|
} catch {
|
|
44637
45566
|
}
|
|
44638
|
-
const normC =
|
|
44639
|
-
const normR =
|
|
44640
|
-
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);
|
|
44641
45570
|
});
|
|
44642
45571
|
if (!isInside2) {
|
|
44643
45572
|
throw Object.assign(
|
|
@@ -44745,7 +45674,7 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44745
45674
|
remedy: "Install LibreOffice on the host system: sudo apt install libreoffice (Linux) or brew install --cask libreoffice (macOS)."
|
|
44746
45675
|
};
|
|
44747
45676
|
}
|
|
44748
|
-
const defaultPdf = targetPath ||
|
|
45677
|
+
const defaultPdf = targetPath || path19.join(path19.dirname(session.filePath), `${path19.basename(session.filePath, path19.extname(session.filePath))}.pdf`);
|
|
44749
45678
|
try {
|
|
44750
45679
|
validatedPdfPath = validateExportTarget(defaultPdf, "pdf");
|
|
44751
45680
|
} catch (err) {
|
|
@@ -44784,7 +45713,7 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44784
45713
|
}
|
|
44785
45714
|
throw original;
|
|
44786
45715
|
}
|
|
44787
|
-
const stat3 = await
|
|
45716
|
+
const stat3 = await fsp11.stat(validatedPdfPath);
|
|
44788
45717
|
return {
|
|
44789
45718
|
ok: true,
|
|
44790
45719
|
format: "pdf",
|
|
@@ -44798,22 +45727,22 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44798
45727
|
}
|
|
44799
45728
|
const ast = session.ast;
|
|
44800
45729
|
if (format === "html") {
|
|
44801
|
-
const title = htmlDocumentTitle(ast,
|
|
45730
|
+
const title = htmlDocumentTitle(ast, path19.basename(session.filePath, path19.extname(session.filePath)));
|
|
44802
45731
|
const html = convertAstToHtml(ast, title, void 0, session.parser);
|
|
44803
45732
|
if (targetPath) {
|
|
44804
45733
|
assertWriteTargetNoSymlink(targetPath);
|
|
44805
|
-
await
|
|
45734
|
+
await fsp11.mkdir(path19.dirname(targetPath), { recursive: true });
|
|
44806
45735
|
assertWriteTargetNoSymlink(targetPath);
|
|
44807
45736
|
await writeFileNoFollow(targetPath, html, "utf-8");
|
|
44808
|
-
const stat3 = await
|
|
45737
|
+
const stat3 = await fsp11.stat(targetPath);
|
|
44809
45738
|
return { ok: true, format: "html", path: targetPath, bytes: stat3.size, dirty, revision };
|
|
44810
45739
|
}
|
|
44811
45740
|
return { ok: true, format: "html", content: html, bytes: Buffer.byteLength(html, "utf-8"), dirty, revision };
|
|
44812
45741
|
} else {
|
|
44813
45742
|
const effectiveImageMode = parsed.data.imageMode ?? (targetPath ? "file" : "embed");
|
|
44814
|
-
const docBase =
|
|
45743
|
+
const docBase = path19.basename(
|
|
44815
45744
|
targetPath || session.filePath || "document.docx",
|
|
44816
|
-
|
|
45745
|
+
path19.extname(targetPath || session.filePath || "document.docx")
|
|
44817
45746
|
) || "document";
|
|
44818
45747
|
const safeDocBase = docBase.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^\.+/, "").slice(0, 120) || "document";
|
|
44819
45748
|
const defaultAssetPrefix = `${safeDocBase}_assets`;
|
|
@@ -44821,39 +45750,39 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44821
45750
|
if (effectiveImageMode === "file") {
|
|
44822
45751
|
const initialAssets = collectMarkdownImageAssets(ast, effectiveAssetPrefix);
|
|
44823
45752
|
if (!targetPath) throw new Error("Markdown file image mode requires an output path.");
|
|
44824
|
-
const destDir =
|
|
45753
|
+
const destDir = path19.dirname(targetPath);
|
|
44825
45754
|
let generation;
|
|
44826
45755
|
let staging;
|
|
44827
45756
|
let published = false;
|
|
44828
45757
|
try {
|
|
44829
45758
|
assertWriteTargetNoSymlink(targetPath);
|
|
44830
|
-
await
|
|
45759
|
+
await fsp11.mkdir(destDir, { recursive: true });
|
|
44831
45760
|
assertWriteTargetNoSymlink(targetPath);
|
|
44832
45761
|
let outputPrefix = effectiveAssetPrefix;
|
|
44833
45762
|
if (initialAssets.length > 0) {
|
|
44834
|
-
const prefixDir = validateAssetTarget(
|
|
45763
|
+
const prefixDir = validateAssetTarget(path19.resolve(destDir, effectiveAssetPrefix));
|
|
44835
45764
|
assertWriteTargetNoSymlink(prefixDir);
|
|
44836
|
-
await
|
|
45765
|
+
await fsp11.mkdir(prefixDir, { recursive: true });
|
|
44837
45766
|
assertWriteTargetNoSymlink(prefixDir);
|
|
44838
|
-
generation = await
|
|
45767
|
+
generation = await fsp11.mkdtemp(path19.join(prefixDir, "export-"));
|
|
44839
45768
|
assertWriteTargetNoSymlink(generation);
|
|
44840
|
-
outputPrefix =
|
|
45769
|
+
outputPrefix = path19.relative(destDir, generation).split(path19.sep).join("/");
|
|
44841
45770
|
}
|
|
44842
45771
|
const md = convertAstToMarkdown(ast, session.parser, { imageMode: "file", assetPrefix: outputPrefix });
|
|
44843
45772
|
const imageAssets = collectMarkdownImageAssets(ast, outputPrefix);
|
|
44844
45773
|
const writtenAssets = [];
|
|
44845
45774
|
for (const asset of imageAssets) {
|
|
44846
|
-
const assetPath = validateAssetTarget(
|
|
45775
|
+
const assetPath = validateAssetTarget(path19.resolve(destDir, asset.name));
|
|
44847
45776
|
assertWriteTargetNoSymlink(assetPath);
|
|
44848
45777
|
await writeFileNoFollow(assetPath, asset.data);
|
|
44849
45778
|
writtenAssets.push(assetPath);
|
|
44850
45779
|
}
|
|
44851
|
-
staging = await
|
|
44852
|
-
const stagedFile =
|
|
45780
|
+
staging = await fsp11.mkdtemp(path19.join(destDir, ".astra-export-"));
|
|
45781
|
+
const stagedFile = path19.join(staging, "document.md");
|
|
44853
45782
|
assertWriteTargetNoSymlink(stagedFile);
|
|
44854
45783
|
await writeFileNoFollow(stagedFile, md, "utf-8");
|
|
44855
45784
|
assertWriteTargetNoSymlink(targetPath);
|
|
44856
|
-
await
|
|
45785
|
+
await fsp11.rename(stagedFile, targetPath);
|
|
44857
45786
|
published = true;
|
|
44858
45787
|
return {
|
|
44859
45788
|
ok: true,
|
|
@@ -44868,17 +45797,17 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44868
45797
|
assetCount: writtenAssets.length
|
|
44869
45798
|
};
|
|
44870
45799
|
} finally {
|
|
44871
|
-
if (staging) await
|
|
44872
|
-
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);
|
|
44873
45802
|
}
|
|
44874
45803
|
} else {
|
|
44875
45804
|
const md = convertAstToMarkdown(ast, session.parser, { imageMode: "embed" });
|
|
44876
45805
|
if (targetPath) {
|
|
44877
45806
|
assertWriteTargetNoSymlink(targetPath);
|
|
44878
|
-
await
|
|
45807
|
+
await fsp11.mkdir(path19.dirname(targetPath), { recursive: true });
|
|
44879
45808
|
assertWriteTargetNoSymlink(targetPath);
|
|
44880
45809
|
await writeFileNoFollow(targetPath, md, "utf-8");
|
|
44881
|
-
const stat3 = await
|
|
45810
|
+
const stat3 = await fsp11.stat(targetPath);
|
|
44882
45811
|
return {
|
|
44883
45812
|
ok: true,
|
|
44884
45813
|
format: "markdown",
|
|
@@ -44916,8 +45845,8 @@ async function handleExportDocument(store, rawArgs) {
|
|
|
44916
45845
|
}
|
|
44917
45846
|
|
|
44918
45847
|
// src/tools/visual.ts
|
|
44919
|
-
var
|
|
44920
|
-
var
|
|
45848
|
+
var fs18 = __toESM(require("fs"));
|
|
45849
|
+
var path20 = __toESM(require("path"));
|
|
44921
45850
|
var import_zod7 = require("zod");
|
|
44922
45851
|
function visualError(code, message, remedy) {
|
|
44923
45852
|
return { ok: false, code, message, remedy };
|
|
@@ -45065,14 +45994,14 @@ function serializeSessionCopy2(parser2, ast) {
|
|
|
45065
45994
|
return fork.rezip();
|
|
45066
45995
|
}
|
|
45067
45996
|
function placeImages(images, dir, docPath) {
|
|
45068
|
-
|
|
45069
|
-
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";
|
|
45070
45999
|
return images.map((img) => {
|
|
45071
|
-
const ext =
|
|
45072
|
-
const dest =
|
|
45073
|
-
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));
|
|
45074
46003
|
try {
|
|
45075
|
-
|
|
46004
|
+
fs18.rmSync(img.path, { force: true });
|
|
45076
46005
|
} catch {
|
|
45077
46006
|
}
|
|
45078
46007
|
return { ...img, path: dest };
|
|
@@ -45199,8 +46128,8 @@ async function handleVisualInspect(store, input) {
|
|
|
45199
46128
|
|
|
45200
46129
|
// src/tools/new-document.ts
|
|
45201
46130
|
var import_zod8 = require("zod");
|
|
45202
|
-
var
|
|
45203
|
-
var
|
|
46131
|
+
var fsp12 = __toESM(require("fs/promises"));
|
|
46132
|
+
var path21 = __toESM(require("path"));
|
|
45204
46133
|
var NEW_DOCUMENT_TEMPLATES = ["report", "memo", "blank"];
|
|
45205
46134
|
var NewDocumentSchema = import_zod8.z.object({
|
|
45206
46135
|
outPath: import_zod8.z.string().min(1).max(1024),
|
|
@@ -45276,10 +46205,10 @@ async function handleNewDocument(store, rawArgs) {
|
|
|
45276
46205
|
}
|
|
45277
46206
|
if (!force) {
|
|
45278
46207
|
try {
|
|
45279
|
-
await
|
|
46208
|
+
await fsp12.access(target);
|
|
45280
46209
|
return newDocError(
|
|
45281
46210
|
"E_CONFLICT",
|
|
45282
|
-
`new_document: destination "${
|
|
46211
|
+
`new_document: destination "${path21.basename(target)}" already exists (refusing to overwrite without force)`,
|
|
45283
46212
|
'Retry with {"force":true} to overwrite, or choose a different outPath.'
|
|
45284
46213
|
);
|
|
45285
46214
|
} catch {
|
|
@@ -45330,7 +46259,7 @@ async function handleNewDocument(store, rawArgs) {
|
|
|
45330
46259
|
}
|
|
45331
46260
|
try {
|
|
45332
46261
|
assertWriteTargetNoSymlink(target);
|
|
45333
|
-
await
|
|
46262
|
+
await fsp12.mkdir(path21.dirname(target), { recursive: true });
|
|
45334
46263
|
assertWriteTargetNoSymlink(target);
|
|
45335
46264
|
await writeFileNoFollow(target, buf);
|
|
45336
46265
|
} catch (err) {
|
|
@@ -45342,12 +46271,12 @@ async function handleNewDocument(store, rawArgs) {
|
|
|
45342
46271
|
);
|
|
45343
46272
|
}
|
|
45344
46273
|
try {
|
|
45345
|
-
const handle = await store.openDocument(target, parsed.data.sessionId);
|
|
46274
|
+
const handle = await store.openDocument(target, parsed.data.sessionId, void 0, false);
|
|
45346
46275
|
handle.parser.adoptBuiltinSeedFrom(parser2);
|
|
45347
46276
|
if (parsed.data.author !== void 0) handle.ast.settings.defaultAuthor = parsed.data.author;
|
|
45348
46277
|
let bytes = buf.length;
|
|
45349
46278
|
try {
|
|
45350
|
-
bytes = (await
|
|
46279
|
+
bytes = (await fsp12.stat(target)).size;
|
|
45351
46280
|
} catch {
|
|
45352
46281
|
}
|
|
45353
46282
|
if (guideRef !== void 0 && resolvedGuide !== void 0) {
|
|
@@ -45377,8 +46306,8 @@ async function handleNewDocument(store, rawArgs) {
|
|
|
45377
46306
|
}
|
|
45378
46307
|
|
|
45379
46308
|
// src/lib/mcp/handlers/parity.ts
|
|
45380
|
-
var
|
|
45381
|
-
var
|
|
46309
|
+
var fs19 = __toESM(require("fs"));
|
|
46310
|
+
var path22 = __toESM(require("path"));
|
|
45382
46311
|
var IMPORT_FILE_MAX_BYTES = COMPOSE_MARKDOWN_MAX;
|
|
45383
46312
|
async function refreshSessionAst(handle) {
|
|
45384
46313
|
await refreshAstInPlace(handle.ast, handle.parser);
|
|
@@ -45633,17 +46562,17 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
|
|
|
45633
46562
|
if (segments.includes("..")) {
|
|
45634
46563
|
throw toError("E_SANDBOX", 'import path contains forbidden ".." traversal', "Use a path inside the workspace documents directory.");
|
|
45635
46564
|
}
|
|
45636
|
-
const resolved =
|
|
46565
|
+
const resolved = path22.resolve(trimmed);
|
|
45637
46566
|
const canonical = canonicalizeForAsset(resolved);
|
|
45638
46567
|
const inside = roots.some((root) => {
|
|
45639
46568
|
let canonicalRoot = root;
|
|
45640
46569
|
try {
|
|
45641
|
-
canonicalRoot =
|
|
46570
|
+
canonicalRoot = fs19.realpathSync(root);
|
|
45642
46571
|
} catch {
|
|
45643
46572
|
}
|
|
45644
|
-
const normC =
|
|
45645
|
-
const normR =
|
|
45646
|
-
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);
|
|
45647
46576
|
});
|
|
45648
46577
|
if (!inside) {
|
|
45649
46578
|
throw toError(
|
|
@@ -45652,12 +46581,12 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
|
|
|
45652
46581
|
"Place the Markdown file inside one of the allowed directories, or name its folder with --allowed-dir or ASTRA_EXTRA_DOC_ROOTS."
|
|
45653
46582
|
);
|
|
45654
46583
|
}
|
|
45655
|
-
const base =
|
|
45656
|
-
const nofollow =
|
|
45657
|
-
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";
|
|
45658
46587
|
let fd;
|
|
45659
46588
|
try {
|
|
45660
|
-
fd =
|
|
46589
|
+
fd = fs19.openSync(canonical, flags);
|
|
45661
46590
|
} catch (err) {
|
|
45662
46591
|
const code = err?.code;
|
|
45663
46592
|
if (code === "ENOENT") {
|
|
@@ -45669,7 +46598,7 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
|
|
|
45669
46598
|
throw toError("E_IO", `import file cannot be read (${base})`, "Check the file exists and is readable, then retry.");
|
|
45670
46599
|
}
|
|
45671
46600
|
try {
|
|
45672
|
-
const st =
|
|
46601
|
+
const st = fs19.fstatSync(fd);
|
|
45673
46602
|
if (!st.isFile()) {
|
|
45674
46603
|
throw toError("E_IO", `import reference is not a file (${base})`, "Point the import at a regular Markdown file.");
|
|
45675
46604
|
}
|
|
@@ -45683,7 +46612,7 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
|
|
|
45683
46612
|
"Split the Markdown source into smaller files and import each in its own call."
|
|
45684
46613
|
);
|
|
45685
46614
|
}
|
|
45686
|
-
const buf =
|
|
46615
|
+
const buf = fs19.readFileSync(fd);
|
|
45687
46616
|
if (buf.length > IMPORT_FILE_MAX_BYTES) {
|
|
45688
46617
|
throw toError(
|
|
45689
46618
|
"E_LIMIT",
|
|
@@ -45693,18 +46622,18 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
|
|
|
45693
46622
|
}
|
|
45694
46623
|
let post = canonical;
|
|
45695
46624
|
try {
|
|
45696
|
-
post =
|
|
46625
|
+
post = fs19.realpathSync(canonical);
|
|
45697
46626
|
} catch {
|
|
45698
46627
|
}
|
|
45699
46628
|
const stillInside = roots.some((root) => {
|
|
45700
46629
|
let canonicalRoot = root;
|
|
45701
46630
|
try {
|
|
45702
|
-
canonicalRoot =
|
|
46631
|
+
canonicalRoot = fs19.realpathSync(root);
|
|
45703
46632
|
} catch {
|
|
45704
46633
|
}
|
|
45705
|
-
const normC =
|
|
45706
|
-
const normR =
|
|
45707
|
-
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);
|
|
45708
46637
|
});
|
|
45709
46638
|
if (!stillInside) {
|
|
45710
46639
|
throw toError("E_SANDBOX", "import path points outside the allowed directory", `Keep Markdown files ${describeAllowedFolders()}.`);
|
|
@@ -45712,7 +46641,7 @@ function readImportMarkdownFile(rawPath, roots = getMcpAllowedRoots()) {
|
|
|
45712
46641
|
return buf.toString("utf8");
|
|
45713
46642
|
} finally {
|
|
45714
46643
|
try {
|
|
45715
|
-
if (fd !== void 0)
|
|
46644
|
+
if (fd !== void 0) fs19.closeSync(fd);
|
|
45716
46645
|
} catch {
|
|
45717
46646
|
}
|
|
45718
46647
|
}
|
|
@@ -45728,12 +46657,12 @@ async function handleImportDocument(store, input) {
|
|
|
45728
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).'
|
|
45729
46658
|
};
|
|
45730
46659
|
}
|
|
45731
|
-
const srcExt =
|
|
46660
|
+
const srcExt = path22.extname(parsed.data.path).toLowerCase();
|
|
45732
46661
|
if (srcExt === ".html" || srcExt === ".htm") {
|
|
45733
46662
|
return {
|
|
45734
46663
|
ok: false,
|
|
45735
46664
|
code: "E_VALIDATION",
|
|
45736
|
-
message: `HTML import is not supported directly ("${
|
|
46665
|
+
message: `HTML import is not supported directly ("${path22.basename(parsed.data.path)}")`,
|
|
45737
46666
|
remedy: "ASTra imports Markdown (.md). Convert the HTML file to Markdown or supply a .md document"
|
|
45738
46667
|
};
|
|
45739
46668
|
}
|
|
@@ -45741,7 +46670,7 @@ async function handleImportDocument(store, input) {
|
|
|
45741
46670
|
return {
|
|
45742
46671
|
ok: false,
|
|
45743
46672
|
code: "E_VALIDATION",
|
|
45744
|
-
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`,
|
|
45745
46674
|
remedy: "Call open_document with the .docx path to work with it, or convert it to Markdown first and import the .md file."
|
|
45746
46675
|
};
|
|
45747
46676
|
}
|
|
@@ -45777,15 +46706,15 @@ async function handleImportDocument(store, input) {
|
|
|
45777
46706
|
}
|
|
45778
46707
|
|
|
45779
46708
|
// src/lib/mcp/schema-dump.ts
|
|
45780
|
-
var
|
|
45781
|
-
var
|
|
46709
|
+
var fs20 = __toESM(require("fs"));
|
|
46710
|
+
var path23 = __toESM(require("path"));
|
|
45782
46711
|
var import_zod9 = require("zod");
|
|
45783
46712
|
|
|
45784
46713
|
// package.json
|
|
45785
46714
|
var package_default = {
|
|
45786
46715
|
name: "@attentionform/astra-mcp-server",
|
|
45787
46716
|
mcpName: "com.attentionform/astra-mcp-server",
|
|
45788
|
-
version: "0.
|
|
46717
|
+
version: "0.5.0",
|
|
45789
46718
|
description: "Official Model Context Protocol (MCP) server for ASTra document IDE",
|
|
45790
46719
|
main: "dist/index.js",
|
|
45791
46720
|
types: "dist/index.d.ts",
|
|
@@ -45826,8 +46755,8 @@ var package_default = {
|
|
|
45826
46755
|
"adm-zip-gate": "node ../../scripts/check-adm-zip-imports.mjs"
|
|
45827
46756
|
},
|
|
45828
46757
|
dependencies: {
|
|
45829
|
-
"@attentionform/astra-engine": "^0.
|
|
45830
|
-
"@attentionform/ooxml-validator": "^0.
|
|
46758
|
+
"@attentionform/astra-engine": "^0.5.0",
|
|
46759
|
+
"@attentionform/ooxml-validator": "^0.5.0",
|
|
45831
46760
|
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
45832
46761
|
"@xmldom/xmldom": "0.8.15",
|
|
45833
46762
|
"adm-zip": "0.6.1",
|
|
@@ -45872,6 +46801,8 @@ function getToolDefinitions() {
|
|
|
45872
46801
|
{ name: "compose_document", description: COMPOSE_DOCUMENT_DESCRIPTION, schema: ComposeDocumentSchema },
|
|
45873
46802
|
{ name: "export_document", description: EXPORT_DOCUMENT_DESCRIPTION, schema: ExportDocumentSchema },
|
|
45874
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 },
|
|
45875
46806
|
{ name: "get_document_history", description: GIT_HISTORY_DESCRIPTIONS.get_document_history, schema: GetDocumentHistorySchema },
|
|
45876
46807
|
{ name: "preview_revert", description: GIT_HISTORY_DESCRIPTIONS.preview_revert, schema: PreviewRevertSchema },
|
|
45877
46808
|
{ name: "revert_document", description: GIT_HISTORY_DESCRIPTIONS.revert_document, schema: RevertDocumentSchema },
|
|
@@ -45892,8 +46823,8 @@ function compactToolSchema(schema) {
|
|
|
45892
46823
|
return raw;
|
|
45893
46824
|
}
|
|
45894
46825
|
function generateSchemasToDir(targetDir, version = getServerVersion()) {
|
|
45895
|
-
if (!
|
|
45896
|
-
|
|
46826
|
+
if (!fs20.existsSync(targetDir)) {
|
|
46827
|
+
fs20.mkdirSync(targetDir, { recursive: true });
|
|
45897
46828
|
}
|
|
45898
46829
|
const stamp2 = buildStamp(version);
|
|
45899
46830
|
const written = [];
|
|
@@ -45905,8 +46836,8 @@ function generateSchemasToDir(targetDir, version = getServerVersion()) {
|
|
|
45905
46836
|
parameters: raw,
|
|
45906
46837
|
[SCHEMA_STAMP_KEY]: stamp2
|
|
45907
46838
|
};
|
|
45908
|
-
const filePath =
|
|
45909
|
-
|
|
46839
|
+
const filePath = path23.join(targetDir, `${tool.name}.json`);
|
|
46840
|
+
fs20.writeFileSync(filePath, JSON.stringify(payload, null, 2) + "\n", "utf8");
|
|
45910
46841
|
written.push(filePath);
|
|
45911
46842
|
}
|
|
45912
46843
|
return { written, count: written.length };
|
|
@@ -45916,11 +46847,11 @@ function checkSchemasInDir(dir, expectedVersion2 = getServerVersion()) {
|
|
|
45916
46847
|
const mismatches = [];
|
|
45917
46848
|
let checked = 0;
|
|
45918
46849
|
for (const tool of getToolDefinitions()) {
|
|
45919
|
-
const file =
|
|
46850
|
+
const file = path23.join(dir, `${tool.name}.json`);
|
|
45920
46851
|
checked += 1;
|
|
45921
46852
|
let stamped = "<missing>";
|
|
45922
46853
|
try {
|
|
45923
|
-
const raw = JSON.parse(
|
|
46854
|
+
const raw = JSON.parse(fs20.readFileSync(file, "utf8"));
|
|
45924
46855
|
const value = raw[SCHEMA_STAMP_KEY];
|
|
45925
46856
|
stamped = typeof value === "string" ? value : "<unstamped>";
|
|
45926
46857
|
} catch {
|
|
@@ -45933,7 +46864,7 @@ function checkSchemasInDir(dir, expectedVersion2 = getServerVersion()) {
|
|
|
45933
46864
|
return { ok: mismatches.length === 0, mismatches, checked };
|
|
45934
46865
|
}
|
|
45935
46866
|
function formatMismatchWarning(mismatches) {
|
|
45936
|
-
const lines = mismatches.map((m) => ` ${
|
|
46867
|
+
const lines = mismatches.map((m) => ` ${path23.basename(m.file)}: stamped ${m.stamped}, installed ${m.expected}`);
|
|
45937
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");
|
|
45938
46869
|
}
|
|
45939
46870
|
|
|
@@ -46000,20 +46931,20 @@ function buildUsageText() {
|
|
|
46000
46931
|
var CLI_USAGE_ERROR = 2;
|
|
46001
46932
|
function initAllowedDirs(argv = process.argv) {
|
|
46002
46933
|
const currentRoots = new Set(
|
|
46003
|
-
(process.env.ASTRA_EXTRA_DOC_ROOTS || "").split(
|
|
46934
|
+
(process.env.ASTRA_EXTRA_DOC_ROOTS || "").split(path24.delimiter).map((p) => p.trim()).filter(Boolean)
|
|
46004
46935
|
);
|
|
46005
46936
|
for (let i = 0; i < argv.length; i++) {
|
|
46006
46937
|
if (argv[i] === "--") break;
|
|
46007
46938
|
if (argv[i] === "--allowed-dir" && argv[i + 1] && !argv[i + 1].startsWith("-")) {
|
|
46008
|
-
currentRoots.add(
|
|
46939
|
+
currentRoots.add(path24.resolve(argv[i + 1]));
|
|
46009
46940
|
i++;
|
|
46010
46941
|
} else if (argv[i].startsWith("--allowed-dir=")) {
|
|
46011
46942
|
const value = argv[i].slice("--allowed-dir=".length).trim();
|
|
46012
|
-
if (value && !value.startsWith("-")) currentRoots.add(
|
|
46943
|
+
if (value && !value.startsWith("-")) currentRoots.add(path24.resolve(value));
|
|
46013
46944
|
}
|
|
46014
46945
|
}
|
|
46015
46946
|
if (currentRoots.size > 0) {
|
|
46016
|
-
process.env.ASTRA_EXTRA_DOC_ROOTS = Array.from(currentRoots).join(
|
|
46947
|
+
process.env.ASTRA_EXTRA_DOC_ROOTS = Array.from(currentRoots).join(path24.delimiter);
|
|
46017
46948
|
}
|
|
46018
46949
|
}
|
|
46019
46950
|
function takeDirFlag(argv, flag) {
|
|
@@ -46079,6 +47010,16 @@ function createAstraMcpServer(store = createMcpSessionStore()) {
|
|
|
46079
47010
|
},
|
|
46080
47011
|
async (args) => toTextResult(await handleVisualInspect(store, args))
|
|
46081
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
|
+
);
|
|
46082
47023
|
server.registerTool(
|
|
46083
47024
|
"get_document_history",
|
|
46084
47025
|
{ description: GIT_HISTORY_DESCRIPTIONS.get_document_history, inputSchema: GetDocumentHistorySchema },
|
|
@@ -46206,7 +47147,7 @@ async function main(argv = process.argv) {
|
|
|
46206
47147
|
if (openTimeout !== void 0) setOpenValidationTimeoutMs(Number(openTimeout));
|
|
46207
47148
|
const dumpDir = takeDirFlag(argv, "--dump-schemas");
|
|
46208
47149
|
if (dumpDir !== void 0) {
|
|
46209
|
-
const resolved =
|
|
47150
|
+
const resolved = path24.resolve(dumpDir);
|
|
46210
47151
|
const result = generateSchemasToDir(resolved, SERVER_VERSION);
|
|
46211
47152
|
process.stdout.write(`[${SERVER_NAME} v${SERVER_VERSION}] wrote ${result.count} schemas to ${resolved}
|
|
46212
47153
|
`);
|
|
@@ -46214,7 +47155,7 @@ async function main(argv = process.argv) {
|
|
|
46214
47155
|
}
|
|
46215
47156
|
const checkDir = takeDirFlag(argv, "--check-schemas");
|
|
46216
47157
|
if (checkDir !== void 0) {
|
|
46217
|
-
const resolved =
|
|
47158
|
+
const resolved = path24.resolve(checkDir);
|
|
46218
47159
|
const verdict = checkSchemasInDir(resolved, SERVER_VERSION);
|
|
46219
47160
|
if (verdict.ok) {
|
|
46220
47161
|
process.stdout.write(`[${SERVER_NAME} v${SERVER_VERSION}] schemas fresh (${verdict.checked} tools)
|