@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/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 path21 = __toESM(require("path"));
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 hash = import_crypto2.default.createHash("sha256").update(`${seed}:${counter}`).digest();
2007
- const raw = hash.readUInt32BE(0) & 2147483647;
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 hash = (parserDeterministicSeq * 2654435761 >>> 0) / 4294967296;
2909
- return hash === 0 ? 0.5 : hash;
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(path22, opts) {
4533
+ async validateFile(path25, opts) {
4534
4534
  const native = await import("@attentionform/ooxml-validator");
4535
- return native.validateFile(path22, opts ? { officeVersion: opts.officeVersion } : void 0);
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 path22 = "[Content_Types].xml";
5497
+ const path25 = "[Content_Types].xml";
5498
5498
  let raw = null;
5499
5499
  try {
5500
- raw = adapter.readTextOrNull(path22);
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(path22, new import_xmldom7.XMLSerializer().serializeToString(doc));
5517
+ adapter.writeText(path25, new import_xmldom7.XMLSerializer().serializeToString(doc));
5518
5518
  }
5519
5519
  function ensurePackageRel(adapter) {
5520
- const path22 = "_rels/.rels";
5520
+ const path25 = "_rels/.rels";
5521
5521
  let raw = null;
5522
5522
  try {
5523
- raw = adapter.readTextOrNull(path22);
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(path22, new import_xmldom7.XMLSerializer().serializeToString(doc));
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 = (path22, content) => {
7420
- files.set(path22, content);
7421
- touched?.add(path22);
7419
+ const write = (path25, content) => {
7420
+ files.set(path25, content);
7421
+ touched?.add(path25);
7422
7422
  };
7423
7423
  return {
7424
- hasFile: (path22) => files.has(path22),
7425
- readText: (path22) => {
7426
- const data = files.get(path22);
7427
- if (!data) throw new Error(`E_VALIDATION: Part ${path22} was not found.`);
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: (path22) => files.get(path22)?.toString("utf8") ?? null,
7431
- writeText: (path22, content) => write(path22, Buffer.from(content, "utf8")),
7432
- writeBuffer: (path22, content) => write(path22, Buffer.from(content))
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: (path22) => !!this.zip.getEntry(this.sanitizePath(path22)),
11072
- readText: (path22) => {
11073
- const cleanPath = this.sanitizePath(path22);
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: (path22) => {
11079
- const cleanPath = this.sanitizePath(path22);
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: (path22, content) => {
11084
- const cleanPath = this.sanitizePath(path22);
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: (path22, content) => {
11094
- const cleanPath = this.sanitizePath(path22);
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 = (path22, tag, attr, drop) => {
12919
- const entry = this.zip.getEntry(path22);
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(path22, Buffer.from(this.serializer.serializeToString(dom), "utf8"));
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/git.ts
13662
- var import_fs4 = __toESM(require("fs"));
13663
- var import_promises2 = __toESM(require("fs/promises"));
13664
- var import_path3 = __toESM(require("path"));
13665
- var import_crypto4 = __toESM(require("crypto"));
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 import_promises2.default.mkdir(dirName, { recursive: true, mode: 448 });
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 import_promises2.default.unlink(import_path3.default.join(repoDir, SNAPSHOT_FORMAT_MARKER));
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 import_promises2.default.chmod(dir, 448);
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 import_promises2.default.readdir(dir, { withFileTypes: true });
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 import_promises2.default.readdir(dir, { withFileTypes: true });
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 import_promises2.default.stat(full);
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 import_promises2.default.stat(import_path3.default.join(repoDir, ".git"))).isDirectory();
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 import_promises2.default.lstat(gitPath);
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 import_promises2.default.stat(gitPath)).isDirectory();
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 import_promises2.default.mkdir(repoDir, { recursive: true, mode: 448 });
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 import_isomorphic_git.default.init({ fs: import_fs4.default, dir: repoDir });
14352
- await writeAndAdd(repoDir, "document.xml", initialXml);
14353
- const writtenParts = await writeSnapshotParts(repoDir, extraFiles);
14354
- await applySnapshotMarker(repoDir, await snapshotIsProvenComplete(repoDir, extraFiles, writtenParts));
14355
- await import_isomorphic_git.default.commit({
14356
- fs: import_fs4.default,
14357
- dir: repoDir,
14358
- author: { name: "ASTra", email: "astra@attentionform.com" },
14359
- message: "Initial state"
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 import_promises2.default.unlink(fullPath);
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 import_promises2.default.readdir(dir, { withFileTypes: true });
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 import_os = __toESM(require("os"));
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 import_os.default.tmpdir();
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 = import_os.default.tmpdir();
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 hash = "unknown";
35895
+ let hash2 = "unknown";
35305
35896
  try {
35306
- hash = import_crypto5.default.createHash("sha256").update(p).digest("hex").slice(0, 16);
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="${hash}" (${reason}). Base honors $ASTRA_TMPDIR; remove manually after inspection.
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 hash = "unknown";
35964
+ let hash2 = "unknown";
35374
35965
  try {
35375
- hash = import_crypto5.default.createHash("sha256").update(target).digest("hex").slice(0, 16);
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="${hash}" (${reason}).
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 fs8 = __toESM(require("fs"));
37615
- var fsp3 = __toESM(require("fs/promises"));
37616
- var path7 = __toESM(require("path"));
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 fsp3.rm(dir, { recursive: true, force: true });
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 = path7.join(tmpDir, `doc_${Date.now()}.docx`);
38678
+ const tmpDocxPath = path8.join(tmpDir, `doc_${Date.now()}.docx`);
38088
38679
  let succeeded = false;
38089
38680
  try {
38090
- await fsp3.writeFile(tmpDocxPath, docxBuffer);
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)(path7.join(tmpDir, "lo_profile")).href}`, "--headless", "--convert-to", filterSpec, tmpDocxPath, "--outdir", tmpDir],
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 = path7.basename(tmpDocxPath).replace(/\.docx$/i, ".pdf");
38136
- const generatedPdfPath = path7.join(tmpDir, expectedPdfName);
38137
- if (!fs8.existsSync(generatedPdfPath)) {
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 = path7.dirname(path7.resolve(outPdfPath));
38141
- assertWriteTargetNoSymlink(path7.resolve(outPdfPath));
38142
- await fsp3.mkdir(targetDir, { recursive: true });
38143
- assertWriteTargetNoSymlink(path7.resolve(outPdfPath));
38144
- await writeFileNoFollow(path7.resolve(outPdfPath), await fsp3.readFile(generatedPdfPath));
38145
- fs8.chmodSync(path7.resolve(outPdfPath), 420);
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 path7.resolve(outPdfPath);
38738
+ return path8.resolve(outPdfPath);
38148
38739
  } finally {
38149
38740
  if (succeeded) {
38150
38741
  try {
38151
- await fsp3.rm(tmpDir, { recursive: true, force: true });
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 fs9 = __toESM(require("fs"));
38165
- var path8 = __toESM(require("path"));
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 = path8.resolve(trimmed);
38181
- fs9.accessSync(abs, fs9.constants.X_OK);
38182
- const stat3 = fs9.statSync(abs);
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(path8.delimiter).filter((d) => d.length > 0);
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 = path8.join(dir, trimmed);
38195
- fs9.accessSync(candidate, fs9.constants.X_OK);
38196
- const stat3 = fs9.statSync(candidate);
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 fs10 = __toESM(require("fs"));
38242
- var path9 = __toESM(require("path"));
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 = path9.join(resolveAstraTmpBase(), "ASTra", "visual");
38844
+ const dir = path10.join(resolveAstraTmpBase(), "ASTra", "visual");
38254
38845
  try {
38255
- fs10.mkdirSync(dir, { recursive: true, mode: 448 });
38846
+ fs11.mkdirSync(dir, { recursive: true, mode: 448 });
38256
38847
  } catch {
38257
38848
  }
38258
38849
  try {
38259
- secureDir(path9.join(resolveAstraTmpBase(), "ASTra"));
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 = path9.join(base, safe);
38860
+ const dir = path10.join(base, safe);
38270
38861
  try {
38271
- fs10.mkdirSync(dir, { recursive: true, mode: 448 });
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 = fs10.readdirSync(workDir);
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: path9.join(workDir, name2) });
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 fs10.realpathSync(base);
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 = path9.resolve(trimmed);
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
- fs10.mkdirSync(outDir, { recursive: true });
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 = path9.join(workDir, "input.docx");
38986
+ const inputDocx = path10.join(workDir, "input.docx");
38396
38987
  let succeeded = false;
38397
38988
  try {
38398
- fs10.writeFileSync(inputDocx, docxBuffer);
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)(path9.join(workDir, "lo_profile")).href}`, "--headless", "--convert-to", "pdf", "--outdir", workDir, inputDocx], {
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 = fs10.readdirSync(workDir).filter((n) => n.toLowerCase().endsWith(".pdf"));
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 = path9.join(workDir, names[0]);
39015
+ if (names.length > 0) pdfPath = path10.join(workDir, names[0]);
38425
39016
  } catch {
38426
39017
  pdfPath = null;
38427
39018
  }
38428
- if (!pdfPath || !fs10.existsSync(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 = path9.join(workDir, prefixBase);
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 = path9.join(outDir, destName);
38452
- if (!isPathInsideDir(path9.resolve(dest), path9.resolve(outDir))) {
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
- fs10.copyFileSync(entry.file, dest);
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
- fs10.unlinkSync(dest);
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 = fs10.statSync(dest).size;
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 fsp4 = __toESM(require("fs/promises"));
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 fs12 = __toESM(require("fs"));
40877
- var path11 = __toESM(require("path"));
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 fs12.existsSync(path11.join(getRepoDir(docPath), ".git"));
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 fsp4.readFile(handle.filePath);
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 fsp4.readFile(handle.filePath);
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 fs13 = __toESM(require("fs"));
41412
- var fsp5 = __toESM(require("fs/promises"));
41413
- var os2 = __toESM(require("os"));
41414
- var path12 = __toESM(require("path"));
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(path12.resolve(process.cwd(), "documents"));
42233
+ roots.push(path14.resolve(process.cwd(), "documents"));
41613
42234
  } catch {
41614
42235
  }
41615
42236
  try {
41616
- roots.push(path12.resolve(os2.tmpdir()));
42237
+ roots.push(path14.resolve(os3.tmpdir()));
41617
42238
  } catch {
41618
42239
  }
41619
42240
  try {
41620
- roots.push(path12.resolve(process.cwd(), "documents", "Drafts"));
42241
+ roots.push(path14.resolve(process.cwd(), "documents", "Drafts"));
41621
42242
  } catch {
41622
42243
  }
41623
42244
  try {
41624
- roots.push(path12.resolve(os2.tmpdir(), "ASTra"));
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(path12.delimiter)) {
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 = path12.resolve(p);
41646
- const stat3 = fs13.statSync(resolved);
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 = fs13.realpathSync(root);
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 = path12.relative(root, canonical);
41669
- const segs = rel.split(path12.sep).filter((s) => s && s !== "." && s !== "..");
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 = path12.basename(canonical);
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 = path12.resolve(trimmed);
41720
- const resolvedSegments = resolved.split(path12.sep);
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 = path12.extname(trimmed).toLowerCase();
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 (path12.isAbsolute(trimmed)) return validateDocPath(trimmed);
42382
+ if (path14.isAbsolute(trimmed)) return validateDocPath(trimmed);
41762
42383
  if (trimmed.includes("\n") || trimmed.includes("\r")) return fallback;
41763
- const base = path12.basename(trimmed.replace(/\\/g, "/"));
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
- const msg = err instanceof Error ? err.message : String(err);
41778
- console.error(`[collectTrackedExtrasForGit] snapshot failed: ${msg}`);
41779
- return {};
42398
+ throw snapshotCollectionError(err);
41780
42399
  }
41781
42400
  }
41782
- async function ensureGitRepo(docPath, xml, extras) {
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 = fs13.existsSync(path12.join(repoDir, ".git"));
42415
+ hasGit = fs15.existsSync(path14.join(repoDir, ".git"));
41787
42416
  } catch {
41788
42417
  hasGit = false;
41789
42418
  }
41790
- if (hasGit) return true;
41791
- await initAstraRepo(docPath, xml, extras);
41792
- return false;
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
- async function initGitForHandleBestEffort(handle) {
42435
+ function collectBaselineForGit(parser2) {
42436
+ const xml = parser2.readDocumentXml();
41795
42437
  try {
41796
- if (!path12.isAbsolute(handle.filePath)) return;
41797
- const xml = handle.parser.readDocumentXml();
41798
- await ensureGitRepo(handle.filePath, xml, collectTrackedExtrasForGit(handle.parser));
41799
- } catch {
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 fsp5.readFile(cleanPath);
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 path14 = __toESM(require("path"));
43643
+ var path16 = __toESM(require("path"));
42893
43644
 
42894
43645
  // src/tools/compose-document.ts
42895
- var fs14 = __toESM(require("fs"));
42896
- var path13 = __toESM(require("path"));
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 && path13.isAbsolute(jail.sessionFilePath)) {
42912
- anchor = canonicalizeForAsset(path13.resolve(path13.dirname(jail.sessionFilePath)));
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 = path13.resolve(trimmed);
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 = fs14.realpathSync(root);
43767
+ canonicalRoot = fs16.realpathSync(root);
43017
43768
  } catch {
43018
43769
  }
43019
- const normC = path13.normalize(canonical);
43020
- const normR = path13.normalize(canonicalRoot);
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 + path13.sep.toLowerCase());
43775
+ return lc === lr || lc.startsWith(lr + path15.sep.toLowerCase());
43025
43776
  }
43026
- return normC === normR || normC.startsWith(normR + path13.sep);
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 (path14.isAbsolute(handle.filePath)) {
43405
- batchAllowedDir = canonicalizeForAsset(path14.resolve(path14.dirname(handle.filePath)));
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 fsp6 = __toESM(require("fs/promises"));
43555
- var path15 = __toESM(require("path"));
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 "${path15.basename(target)}" with a .docm extension, or add stripMacros: true to remove the macro parts`
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 "${path15.basename(target)}", a macro-enabled name`
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: "${path15.basename(target).replace(/\.docm$/i, ".docx")}", or use allowMacros: true to keep the macro parts in the .docm`
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 fsp6.readFile(target);
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 fsp6.mkdir(pathDirname(target), { recursive: true });
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 fsp6.unlink(target);
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 fsp6.unlink(target);
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 fsp7 = __toESM(require("fs/promises"));
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 fsp7.readFile(handle.filePath);
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 fsp7.mkdir(pathDirname(handle.filePath), { recursive: true });
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 fsp7.unlink(handle.filePath);
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 fsp7.readFile(handle.filePath);
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 fsp7.mkdir(pathDirname(handle.filePath), { recursive: true });
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 fsp7.unlink(handle.filePath);
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 fs15 = __toESM(require("fs"));
44506
- var fsp8 = __toESM(require("fs/promises"));
44507
- var path16 = __toESM(require("path"));
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 = path16.resolve(trimmed);
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 = fs15.realpathSync(root);
45498
+ canonicalRoot = fs17.realpathSync(root);
44570
45499
  } catch {
44571
45500
  }
44572
- const normC = path16.normalize(canonical);
44573
- const normR = path16.normalize(canonicalRoot);
44574
- return normC === normR || normC.startsWith(normR + path16.sep);
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 = path16.extname(trimmed).toLowerCase();
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 = path16.resolve(trimmed);
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 = fs15.realpathSync(root);
45564
+ canonicalRoot = fs17.realpathSync(root);
44636
45565
  } catch {
44637
45566
  }
44638
- const normC = path16.normalize(canonical);
44639
- const normR = path16.normalize(canonicalRoot);
44640
- return normC === normR || normC.startsWith(normR + path16.sep);
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 || path16.join(path16.dirname(session.filePath), `${path16.basename(session.filePath, path16.extname(session.filePath))}.pdf`);
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 fsp8.stat(validatedPdfPath);
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, path16.basename(session.filePath, path16.extname(session.filePath)));
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 fsp8.mkdir(path16.dirname(targetPath), { recursive: true });
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 fsp8.stat(targetPath);
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 = path16.basename(
45743
+ const docBase = path19.basename(
44815
45744
  targetPath || session.filePath || "document.docx",
44816
- path16.extname(targetPath || session.filePath || "document.docx")
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 = path16.dirname(targetPath);
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 fsp8.mkdir(destDir, { recursive: true });
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(path16.resolve(destDir, effectiveAssetPrefix));
45763
+ const prefixDir = validateAssetTarget(path19.resolve(destDir, effectiveAssetPrefix));
44835
45764
  assertWriteTargetNoSymlink(prefixDir);
44836
- await fsp8.mkdir(prefixDir, { recursive: true });
45765
+ await fsp11.mkdir(prefixDir, { recursive: true });
44837
45766
  assertWriteTargetNoSymlink(prefixDir);
44838
- generation = await fsp8.mkdtemp(path16.join(prefixDir, "export-"));
45767
+ generation = await fsp11.mkdtemp(path19.join(prefixDir, "export-"));
44839
45768
  assertWriteTargetNoSymlink(generation);
44840
- outputPrefix = path16.relative(destDir, generation).split(path16.sep).join("/");
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(path16.resolve(destDir, asset.name));
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 fsp8.mkdtemp(path16.join(destDir, ".astra-export-"));
44852
- const stagedFile = path16.join(staging, "document.md");
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 fsp8.rename(stagedFile, targetPath);
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 fsp8.rm(staging, { recursive: true, force: true }).catch(() => void 0);
44872
- if (!published && generation) await fsp8.rm(generation, { recursive: true, force: true }).catch(() => void 0);
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 fsp8.mkdir(path16.dirname(targetPath), { recursive: true });
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 fsp8.stat(targetPath);
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 fs16 = __toESM(require("fs"));
44920
- var path17 = __toESM(require("path"));
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
- fs16.mkdirSync(dir, { recursive: true });
45069
- const base = path17.basename(docPath || "document", path17.extname(docPath || "")).replace(/[^a-zA-Z0-9._-]+/g, "-") || "document";
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 = path17.extname(img.path) || (img.format === "jpeg" ? ".jpg" : ".png");
45072
- const dest = path17.join(dir, `${base}-page-${img.page}${ext}`);
45073
- writeFileNoFollowSync(dest, fs16.readFileSync(img.path));
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
- fs16.rmSync(img.path, { force: true });
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 fsp9 = __toESM(require("fs/promises"));
45203
- var path18 = __toESM(require("path"));
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 fsp9.access(target);
46208
+ await fsp12.access(target);
45280
46209
  return newDocError(
45281
46210
  "E_CONFLICT",
45282
- `new_document: destination "${path18.basename(target)}" already exists (refusing to overwrite without force)`,
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 fsp9.mkdir(path18.dirname(target), { recursive: true });
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 fsp9.stat(target)).size;
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 fs17 = __toESM(require("fs"));
45381
- var path19 = __toESM(require("path"));
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 = path19.resolve(trimmed);
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 = fs17.realpathSync(root);
46570
+ canonicalRoot = fs19.realpathSync(root);
45642
46571
  } catch {
45643
46572
  }
45644
- const normC = path19.normalize(canonical);
45645
- const normR = path19.normalize(canonicalRoot);
45646
- return normC === normR || normC.startsWith(normR + path19.sep);
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 = path19.basename(canonical);
45656
- const nofollow = fs17.constants.O_NOFOLLOW;
45657
- const flags = typeof nofollow === "number" && nofollow !== 0 ? fs17.constants.O_RDONLY | nofollow : "r";
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 = fs17.openSync(canonical, flags);
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 = fs17.fstatSync(fd);
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 = fs17.readFileSync(fd);
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 = fs17.realpathSync(canonical);
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 = fs17.realpathSync(root);
46631
+ canonicalRoot = fs19.realpathSync(root);
45703
46632
  } catch {
45704
46633
  }
45705
- const normC = path19.normalize(post);
45706
- const normR = path19.normalize(canonicalRoot);
45707
- return normC === normR || normC.startsWith(normR + path19.sep);
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) fs17.closeSync(fd);
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 = path19.extname(parsed.data.path).toLowerCase();
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 ("${path19.basename(parsed.data.path)}")`,
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 ("${path19.basename(parsed.data.path)}"): open .docx files directly with open_document instead`,
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 fs18 = __toESM(require("fs"));
45781
- var path20 = __toESM(require("path"));
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.4.2",
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.4.2",
45830
- "@attentionform/ooxml-validator": "^0.4.2",
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 (!fs18.existsSync(targetDir)) {
45896
- fs18.mkdirSync(targetDir, { recursive: true });
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 = path20.join(targetDir, `${tool.name}.json`);
45909
- fs18.writeFileSync(filePath, JSON.stringify(payload, null, 2) + "\n", "utf8");
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 = path20.join(dir, `${tool.name}.json`);
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(fs18.readFileSync(file, "utf8"));
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) => ` ${path20.basename(m.file)}: stamped ${m.stamped}, installed ${m.expected}`);
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(path21.delimiter).map((p) => p.trim()).filter(Boolean)
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(path21.resolve(argv[i + 1]));
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(path21.resolve(value));
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(path21.delimiter);
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 = path21.resolve(dumpDir);
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 = path21.resolve(checkDir);
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)