@json-to-office/mcp-server 1.5.0 → 1.8.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
@@ -2,7 +2,7 @@
2
2
  import { McpServer } from "@modelcontextprotocol/server";
3
3
 
4
4
  // src/lib/version.ts
5
- var SERVER_VERSION = true ? "1.5.0" : "dev-mode";
5
+ var SERVER_VERSION = true ? "1.8.0" : "dev-mode";
6
6
  var SERVER_NAME = "json-to-office";
7
7
  var PACKAGE_NAME = "@json-to-office/mcp-server";
8
8
 
@@ -415,9 +415,9 @@ var ROOT_SENTINELS = /* @__PURE__ */ new Set(["", "/", "#", "root"]);
415
415
  function escapePointerSegment(segment) {
416
416
  return segment.replace(/~/g, "~0").replace(/\//g, "~1");
417
417
  }
418
- function toJsonPointer(path6) {
419
- if (path6 === void 0) return void 0;
420
- const trimmed = path6.trim();
418
+ function toJsonPointer(path7) {
419
+ if (path7 === void 0) return void 0;
420
+ const trimmed = path7.trim();
421
421
  if (ROOT_SENTINELS.has(trimmed)) return "";
422
422
  const segments = trimmed.startsWith("/") ? trimmed.slice(1).split("/") : trimmed.replace(/\[(\d+)\]/g, ".$1").split(".").filter((segment) => segment !== "");
423
423
  if (segments.length === 0) return "";
@@ -618,11 +618,11 @@ async function probeService(rawUrl, envVar) {
618
618
  }
619
619
  const port = Number(target.port || (target.protocol === "https:" ? 443 : 80));
620
620
  const net = await import("net");
621
- return new Promise((resolve2) => {
621
+ return new Promise((resolve3) => {
622
622
  const socket = net.connect({ host: target.hostname, port });
623
623
  const settle = (detail) => {
624
624
  socket.destroy();
625
- resolve2({
625
+ resolve3({
626
626
  available: detail === void 0,
627
627
  url: target.origin,
628
628
  envVar,
@@ -662,7 +662,7 @@ function register(server, deps) {
662
662
  "jto_info",
663
663
  {
664
664
  title: "Server info",
665
- description: "Versions, supported formats with each renderer and whether its backend loads here, workspace availability, output-root and size limits, and whether the optional host dependencies (LibreOffice and poppler for jto_preview, a Highcharts export server for the DOCX `highcharts` component) are present on this host. Call this first.",
665
+ description: "Versions, supported formats with each renderer and whether its backend loads here, workspace availability and whether workspaces survive a lost connection, output-root and size limits, and whether the optional host dependencies (LibreOffice and poppler for jto_preview, a Highcharts export server for the DOCX `highcharts` component) are present on this host. Call this first.",
666
666
  annotations: { readOnlyHint: true, openWorldHint: false },
667
667
  inputSchema: S({
668
668
  type: "object",
@@ -747,9 +747,17 @@ function register(server, deps) {
747
747
  type: "object",
748
748
  properties: {
749
749
  available: { type: "boolean" },
750
- open: { type: "integer" }
750
+ open: { type: "integer" },
751
+ persistent: {
752
+ type: "boolean",
753
+ description: "True when revisions are mirrored to disk and survive a lost connection; false when handles are memory-only, which is the default."
754
+ },
755
+ root: {
756
+ type: "string",
757
+ description: "Directory the revisions are mirrored to, when persistent."
758
+ }
751
759
  },
752
- required: ["available", "open"],
760
+ required: ["available", "open", "persistent"],
753
761
  additionalProperties: false
754
762
  },
755
763
  output: {
@@ -910,7 +918,11 @@ function register(server, deps) {
910
918
  formats,
911
919
  workspaces: {
912
920
  available: store.available,
913
- open: listed.ok ? listed.records.length : 0
921
+ open: listed.ok ? listed.records.length : 0,
922
+ persistent: deps.workspacePersistence !== void 0,
923
+ ...deps.workspacePersistence && {
924
+ root: deps.workspacePersistence.root
925
+ }
914
926
  },
915
927
  output: {
916
928
  root: deps.outputRoot.path,
@@ -2025,14 +2037,14 @@ function sourceSummary(resolved) {
2025
2037
  revision: resolved.revision
2026
2038
  } : { origin: "inline" };
2027
2039
  }
2028
- function parseDocumentJson(text, path6) {
2040
+ function parseDocumentJson(text, path7) {
2029
2041
  try {
2030
2042
  return { ok: true, document: JSON.parse(text) };
2031
2043
  } catch (error) {
2032
2044
  return failure(
2033
2045
  ERROR_CODES.INVALID_JSON,
2034
2046
  error instanceof Error ? error.message : String(error),
2035
- { ...path6 !== void 0 && { path: path6 } }
2047
+ { ...path7 !== void 0 && { path: path7 } }
2036
2048
  );
2037
2049
  }
2038
2050
  }
@@ -3148,9 +3160,9 @@ async function digestAssets(document, baseDir) {
3148
3160
  [...references].map(async (reference) => {
3149
3161
  const resolved = path4.resolve(root, reference);
3150
3162
  try {
3151
- const stat = await fs4.stat(resolved);
3152
- if (!stat.isFile()) return;
3153
- entries.push(`${resolved}|${stat.size}|${stat.mtimeMs}`);
3163
+ const stat2 = await fs4.stat(resolved);
3164
+ if (!stat2.isFile()) return;
3165
+ entries.push(`${resolved}|${stat2.size}|${stat2.mtimeMs}`);
3154
3166
  } catch {
3155
3167
  }
3156
3168
  })
@@ -3180,8 +3192,8 @@ function collectAssetReferences(value, into) {
3180
3192
  async function digestThemeFile(themePath) {
3181
3193
  if (!themePath) return "none";
3182
3194
  try {
3183
- const stat = await fs4.stat(themePath);
3184
- return `${path4.resolve(themePath)}|${stat.size}|${stat.mtimeMs}`;
3195
+ const stat2 = await fs4.stat(themePath);
3196
+ return `${path4.resolve(themePath)}|${stat2.size}|${stat2.mtimeMs}`;
3185
3197
  } catch {
3186
3198
  return `${path4.resolve(themePath)}|missing`;
3187
3199
  }
@@ -3253,18 +3265,18 @@ function missingDependencyFailure(dependencies) {
3253
3265
  );
3254
3266
  }
3255
3267
  async function binaryIdentity(binary, args, signal) {
3256
- const [version, stat] = await Promise.all([
3268
+ const [version, stat2] = await Promise.all([
3257
3269
  readVersion(binary, args, signal),
3258
3270
  fs5.stat(binary).catch(() => void 0)
3259
3271
  ]);
3260
3272
  const identity = [
3261
3273
  version ?? "unknown",
3262
- stat ? `${stat.size}:${stat.mtimeMs}` : "nostat"
3274
+ stat2 ? `${stat2.size}:${stat2.mtimeMs}` : "nostat"
3263
3275
  ].join("|");
3264
3276
  return { identity, ...version !== void 0 && { version } };
3265
3277
  }
3266
3278
  function readVersion(binary, args, signal) {
3267
- return new Promise((resolve2) => {
3279
+ return new Promise((resolve3) => {
3268
3280
  execFile(
3269
3281
  binary,
3270
3282
  args,
@@ -3278,8 +3290,8 @@ function readVersion(binary, args, signal) {
3278
3290
  const text = `${stdout ?? ""}
3279
3291
  ${stderr ?? ""}`.trim();
3280
3292
  const line = text.split("\n")[0]?.trim();
3281
- if (!line && error) return resolve2(void 0);
3282
- resolve2(line || void 0);
3293
+ if (!line && error) return resolve3(void 0);
3294
+ resolve3(line || void 0);
3283
3295
  }
3284
3296
  );
3285
3297
  });
@@ -3324,12 +3336,12 @@ function defaultPreviewCacheDir() {
3324
3336
  }
3325
3337
  var CACHE_ENTRY_NAME = /^[a-f0-9]{64}\.(?:png|meta\.json)(?:\.tmp-\d+-\d+)?$/;
3326
3338
  async function inspectPrivateCacheDir(cacheDir) {
3327
- const stat = await fs6.lstat(cacheDir).catch(() => void 0);
3328
- if (!stat || stat.isSymbolicLink() || !stat.isDirectory()) return false;
3329
- if (typeof process.getuid === "function" && stat.uid !== process.getuid()) {
3339
+ const stat2 = await fs6.lstat(cacheDir).catch(() => void 0);
3340
+ if (!stat2 || stat2.isSymbolicLink() || !stat2.isDirectory()) return false;
3341
+ if (typeof process.getuid === "function" && stat2.uid !== process.getuid()) {
3330
3342
  return false;
3331
3343
  }
3332
- if (process.platform !== "win32" && (stat.mode & 63) !== 0) {
3344
+ if (process.platform !== "win32" && (stat2.mode & 63) !== 0) {
3333
3345
  try {
3334
3346
  await fs6.chmod(cacheDir, 448);
3335
3347
  } catch {
@@ -3359,9 +3371,9 @@ async function sweepPreviewCache(cacheDir, options = {}) {
3359
3371
  for (const name of names) {
3360
3372
  if (!CACHE_ENTRY_NAME.test(name)) continue;
3361
3373
  const file = path5.join(cacheDir, name);
3362
- const stat = await fs6.lstat(file).catch(() => void 0);
3363
- if (!stat?.isFile()) continue;
3364
- entries.push({ file, mtimeMs: stat.mtimeMs, size: stat.size });
3374
+ const stat2 = await fs6.lstat(file).catch(() => void 0);
3375
+ if (!stat2?.isFile()) continue;
3376
+ entries.push({ file, mtimeMs: stat2.mtimeMs, size: stat2.size });
3365
3377
  }
3366
3378
  const doomed = entries.filter((entry) => now - entry.mtimeMs > maxAgeMs);
3367
3379
  const kept = entries.filter((entry) => now - entry.mtimeMs <= maxAgeMs).sort((a, b) => a.mtimeMs - b.mtimeMs);
@@ -3448,7 +3460,7 @@ function countPdfPages(pdf) {
3448
3460
  return matches ? matches.length : 0;
3449
3461
  }
3450
3462
  function exec(binary, args, timeoutMs, options = {}) {
3451
- return new Promise((resolve2, reject) => {
3463
+ return new Promise((resolve3, reject) => {
3452
3464
  execFile2(
3453
3465
  binary,
3454
3466
  args,
@@ -3459,7 +3471,7 @@ function exec(binary, args, timeoutMs, options = {}) {
3459
3471
  env: options.env ? { ...process.env, ...options.env } : process.env,
3460
3472
  ...options.signal && { signal: options.signal }
3461
3473
  },
3462
- (error) => error ? reject(error) : resolve2()
3474
+ (error) => error ? reject(error) : resolve3()
3463
3475
  );
3464
3476
  });
3465
3477
  }
@@ -3553,7 +3565,7 @@ async function resolvePdfinfo(pdftoppmPath) {
3553
3565
  return status.available ? status.path : void 0;
3554
3566
  }
3555
3567
  async function pdfinfoPageCount(pdfinfo, pdfPath, signal) {
3556
- return new Promise((resolve2) => {
3568
+ return new Promise((resolve3) => {
3557
3569
  execFile2(
3558
3570
  pdfinfo,
3559
3571
  [pdfPath],
@@ -3564,9 +3576,9 @@ async function pdfinfoPageCount(pdfinfo, pdfPath, signal) {
3564
3576
  ...signal && { signal }
3565
3577
  },
3566
3578
  (error, stdout) => {
3567
- if (error && !stdout) return resolve2(void 0);
3579
+ if (error && !stdout) return resolve3(void 0);
3568
3580
  const match = /^Pages:\s+(\d+)/m.exec(stdout ?? "");
3569
- resolve2(match ? Number(match[1]) : void 0);
3581
+ resolve3(match ? Number(match[1]) : void 0);
3570
3582
  }
3571
3583
  );
3572
3584
  });
@@ -4679,9 +4691,9 @@ function compilePatch(operations) {
4679
4691
  }
4680
4692
  );
4681
4693
  }
4682
- const path6 = parsePointer(raw.path);
4683
- if (!path6.ok) {
4684
- return problem(PATCH_ERROR_CODES.INVALID_POINTER, path6.message, index, {
4694
+ const path7 = parsePointer(raw.path);
4695
+ if (!path7.ok) {
4696
+ return problem(PATCH_ERROR_CODES.INVALID_POINTER, path7.message, index, {
4685
4697
  context: { op, pointer: raw.path },
4686
4698
  suggestion: 'Pointers are RFC 6901: "/children/0/props/text", with "~0" for "~" and "~1" for "/".'
4687
4699
  });
@@ -4689,7 +4701,7 @@ function compilePatch(operations) {
4689
4701
  const entry = {
4690
4702
  op,
4691
4703
  path: raw.path,
4692
- tokens: path6.tokens
4704
+ tokens: path7.tokens
4693
4705
  };
4694
4706
  if (VALUE_OPS.has(op)) {
4695
4707
  if (raw.value === void 0) {
@@ -5039,12 +5051,338 @@ function jsonEqualTokens(left, right) {
5039
5051
  }
5040
5052
 
5041
5053
  // src/workspace/store.ts
5054
+ import { randomBytes as randomBytes3 } from "crypto";
5055
+
5056
+ // src/workspace/persistence.ts
5057
+ import * as fs7 from "fs/promises";
5058
+ import * as path6 from "path";
5042
5059
  import { randomBytes as randomBytes2 } from "crypto";
5060
+ var WORKSPACE_DIR_ENV = "JTO_MCP_WORKSPACE_DIR";
5061
+ var SCHEMA_VERSION = 1;
5062
+ var META_FILE = "meta.json";
5063
+ var REVISION_PREFIX = "rev-";
5064
+ var REVISION_SUFFIX = ".json";
5065
+ var TEMP_SUFFIX = ".tmp";
5066
+ var STALE_TEMP_MS = 60 * 60 * 1e3;
5067
+ var SAFE_HANDLE = /^[A-Za-z0-9_-]{1,128}$/;
5068
+ function isPersistableHandle(handle) {
5069
+ return SAFE_HANDLE.test(handle);
5070
+ }
5071
+ var DEFAULT_PERSISTENCE_LIMITS = {
5072
+ maxWorkspaces: 32,
5073
+ maxRevisionsPerWorkspace: 9,
5074
+ maxEntryBytes: 16 * 1024 * 1024
5075
+ };
5076
+ function createWorkspacePersistence(options = {}) {
5077
+ const env = options.env ?? process.env;
5078
+ const configured = options.flagDir?.trim() || env[WORKSPACE_DIR_ENV]?.trim();
5079
+ if (!configured) return void 0;
5080
+ return createWorkspacePersistenceAt(path6.resolve(configured), options);
5081
+ }
5082
+ function createWorkspacePersistenceAt(root, limits = {}) {
5083
+ const bounds = {
5084
+ maxWorkspaces: limits.maxWorkspaces ?? DEFAULT_PERSISTENCE_LIMITS.maxWorkspaces,
5085
+ maxRevisionsPerWorkspace: limits.maxRevisionsPerWorkspace ?? DEFAULT_PERSISTENCE_LIMITS.maxRevisionsPerWorkspace,
5086
+ maxEntryBytes: limits.maxEntryBytes ?? DEFAULT_PERSISTENCE_LIMITS.maxEntryBytes
5087
+ };
5088
+ let ensured;
5089
+ const authored = /* @__PURE__ */ new Map();
5090
+ function claim(handle, revision) {
5091
+ const owned = authored.get(handle) ?? /* @__PURE__ */ new Set();
5092
+ owned.add(revision);
5093
+ authored.set(handle, owned);
5094
+ }
5095
+ async function ensureRoot() {
5096
+ if (ensured === void 0) {
5097
+ ensured = fs7.mkdir(root, { recursive: true, mode: 448 }).then(
5098
+ () => void 0,
5099
+ (error) => {
5100
+ ensured = void 0;
5101
+ throw error;
5102
+ }
5103
+ );
5104
+ }
5105
+ return ensured;
5106
+ }
5107
+ function dirFor(handle) {
5108
+ return path6.join(root, handle);
5109
+ }
5110
+ async function writeAtomic2(target, contents) {
5111
+ const temp = `${target}.${randomBytes2(6).toString("hex")}${TEMP_SUFFIX}`;
5112
+ try {
5113
+ await fs7.writeFile(temp, contents, { encoding: "utf8", mode: 384 });
5114
+ await fs7.rename(temp, target);
5115
+ } catch (error) {
5116
+ await fs7.rm(temp, { force: true }).catch(() => void 0);
5117
+ throw error;
5118
+ }
5119
+ }
5120
+ function revisionFile(handle, revision) {
5121
+ return path6.join(
5122
+ dirFor(handle),
5123
+ `${REVISION_PREFIX}${revision}${REVISION_SUFFIX}`
5124
+ );
5125
+ }
5126
+ async function readMeta(handle) {
5127
+ let raw;
5128
+ try {
5129
+ raw = await fs7.readFile(path6.join(dirFor(handle), META_FILE), "utf8");
5130
+ } catch {
5131
+ return void 0;
5132
+ }
5133
+ return parseMeta(raw, handle);
5134
+ }
5135
+ async function readRevision(handle, revision) {
5136
+ let text;
5137
+ try {
5138
+ text = await fs7.readFile(revisionFile(handle, revision), "utf8");
5139
+ } catch {
5140
+ return void 0;
5141
+ }
5142
+ try {
5143
+ const parsed = JSON.parse(text);
5144
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
5145
+ return void 0;
5146
+ } catch {
5147
+ return void 0;
5148
+ }
5149
+ return { revision, text, bytes: Buffer.byteLength(text, "utf8") };
5150
+ }
5151
+ async function handleDirs() {
5152
+ try {
5153
+ const entries = await fs7.readdir(root, { withFileTypes: true });
5154
+ return entries.filter(
5155
+ (entry) => entry.isDirectory() && isPersistableHandle(entry.name)
5156
+ ).map((entry) => entry.name);
5157
+ } catch {
5158
+ return [];
5159
+ }
5160
+ }
5161
+ async function pruneWorkspaces(incoming) {
5162
+ const handles = (await handleDirs()).filter(
5163
+ (handle) => handle !== incoming
5164
+ );
5165
+ const excess = handles.length + 1 - bounds.maxWorkspaces;
5166
+ if (excess <= 0) return;
5167
+ const metas = await Promise.all(
5168
+ handles.map(async (handle) => ({
5169
+ handle,
5170
+ // An unreadable entry sorts first: it is the one worth losing.
5171
+ updatedAt: (await readMeta(handle))?.updatedAt ?? 0
5172
+ }))
5173
+ );
5174
+ metas.sort((a, b) => a.updatedAt - b.updatedAt);
5175
+ for (const victim of metas.slice(0, excess)) {
5176
+ await fs7.rm(dirFor(victim.handle), { recursive: true, force: true }).catch(() => void 0);
5177
+ authored.delete(victim.handle);
5178
+ }
5179
+ }
5180
+ async function pruneRevisions(handle, retained) {
5181
+ let names;
5182
+ try {
5183
+ names = await fs7.readdir(dirFor(handle));
5184
+ } catch {
5185
+ return;
5186
+ }
5187
+ const owned = authored.get(handle);
5188
+ for (const name of names) {
5189
+ const target = path6.join(dirFor(handle), name);
5190
+ if (name.endsWith(TEMP_SUFFIX)) {
5191
+ if (await isStale(target)) {
5192
+ await fs7.rm(target, { force: true }).catch(() => void 0);
5193
+ }
5194
+ continue;
5195
+ }
5196
+ const revision = revisionOf(name);
5197
+ if (revision === void 0 || retained.has(revision)) continue;
5198
+ await fs7.rm(target, { force: true }).catch(() => void 0);
5199
+ owned?.delete(revision);
5200
+ }
5201
+ }
5202
+ return {
5203
+ root,
5204
+ limits: bounds,
5205
+ async save(snapshot) {
5206
+ if (!isPersistableHandle(snapshot.handle)) {
5207
+ throw new Error(
5208
+ `Workspace handle ${JSON.stringify(
5209
+ snapshot.handle
5210
+ )} cannot be used as a directory name.`
5211
+ );
5212
+ }
5213
+ if (snapshot.head.bytes > bounds.maxEntryBytes) {
5214
+ throw new Error(
5215
+ `Revision ${snapshot.head.revision} is ${snapshot.head.bytes} bytes, over the ${bounds.maxEntryBytes}-byte persistence limit.`
5216
+ );
5217
+ }
5218
+ await ensureRoot();
5219
+ const dir = dirFor(snapshot.handle);
5220
+ const fresh = !await exists(dir);
5221
+ if (fresh) await pruneWorkspaces(snapshot.handle);
5222
+ await fs7.mkdir(dir, { recursive: true, mode: 448 });
5223
+ const files = [snapshot.head];
5224
+ const retained = /* @__PURE__ */ new Set([snapshot.head.revision]);
5225
+ const pinned = [];
5226
+ for (const pin of [...snapshot.pins].sort(
5227
+ (a, b) => b.revision - a.revision
5228
+ )) {
5229
+ if (!retained.has(pin.revision)) {
5230
+ if (files.length >= bounds.maxRevisionsPerWorkspace) continue;
5231
+ files.push(pin);
5232
+ retained.add(pin.revision);
5233
+ }
5234
+ pinned.push(pin.revision);
5235
+ }
5236
+ const owned = authored.get(snapshot.handle);
5237
+ for (const document of files) {
5238
+ if (owned?.has(document.revision)) continue;
5239
+ await writeAtomic2(
5240
+ revisionFile(snapshot.handle, document.revision),
5241
+ document.text
5242
+ );
5243
+ claim(snapshot.handle, document.revision);
5244
+ }
5245
+ const meta = {
5246
+ schema: SCHEMA_VERSION,
5247
+ handle: snapshot.handle,
5248
+ format: snapshot.format,
5249
+ revision: snapshot.head.revision,
5250
+ bytes: snapshot.head.bytes,
5251
+ createdAt: snapshot.createdAt,
5252
+ updatedAt: snapshot.updatedAt,
5253
+ ...snapshot.title !== void 0 && { title: snapshot.title },
5254
+ pinnedRevisions: pinned.sort((a, b) => a - b)
5255
+ };
5256
+ await writeAtomic2(path6.join(dir, META_FILE), JSON.stringify(meta));
5257
+ await pruneRevisions(snapshot.handle, retained);
5258
+ },
5259
+ async list() {
5260
+ const handles = await handleDirs();
5261
+ const metas = await Promise.all(
5262
+ handles.map((handle) => readMeta(handle))
5263
+ );
5264
+ return metas.filter((meta) => meta !== void 0).sort((a, b) => b.updatedAt - a.updatedAt);
5265
+ },
5266
+ async load(handle) {
5267
+ if (!isPersistableHandle(handle)) return void 0;
5268
+ const meta = await readMeta(handle);
5269
+ if (!meta) return void 0;
5270
+ const head = await readRevision(handle, meta.revision);
5271
+ if (!head) {
5272
+ await fs7.rm(dirFor(handle), { recursive: true, force: true }).catch(() => void 0);
5273
+ return void 0;
5274
+ }
5275
+ const pins = [];
5276
+ for (const revision of meta.pinnedRevisions) {
5277
+ const pin = await readRevision(handle, revision);
5278
+ if (pin) pins.push(pin);
5279
+ }
5280
+ return {
5281
+ ...meta,
5282
+ // A pin whose file went missing is dropped rather than advertised: the
5283
+ // record has to describe what can actually be read back.
5284
+ pinnedRevisions: pins.map((pin) => pin.revision).sort((a, b) => a - b),
5285
+ head,
5286
+ pins
5287
+ };
5288
+ },
5289
+ async remove(handle) {
5290
+ if (!isPersistableHandle(handle)) return false;
5291
+ const dir = dirFor(handle);
5292
+ if (!await exists(dir)) {
5293
+ authored.delete(handle);
5294
+ return false;
5295
+ }
5296
+ await fs7.rm(dir, { recursive: true, force: true });
5297
+ authored.delete(handle);
5298
+ return true;
5299
+ }
5300
+ };
5301
+ }
5302
+ async function exists(target) {
5303
+ try {
5304
+ await fs7.stat(target);
5305
+ return true;
5306
+ } catch {
5307
+ return false;
5308
+ }
5309
+ }
5310
+ async function isStale(target) {
5311
+ try {
5312
+ const { mtimeMs } = await fs7.stat(target);
5313
+ return Date.now() - mtimeMs > STALE_TEMP_MS;
5314
+ } catch {
5315
+ return false;
5316
+ }
5317
+ }
5318
+ function revisionOf(name) {
5319
+ if (!name.startsWith(REVISION_PREFIX) || !name.endsWith(REVISION_SUFFIX)) {
5320
+ return void 0;
5321
+ }
5322
+ const digits = name.slice(
5323
+ REVISION_PREFIX.length,
5324
+ name.length - REVISION_SUFFIX.length
5325
+ );
5326
+ if (!/^[0-9]+$/.test(digits)) return void 0;
5327
+ return Number(digits);
5328
+ }
5329
+ function parseMeta(raw, handle) {
5330
+ let parsed;
5331
+ try {
5332
+ parsed = JSON.parse(raw);
5333
+ } catch {
5334
+ return void 0;
5335
+ }
5336
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
5337
+ return void 0;
5338
+ }
5339
+ const meta = parsed;
5340
+ if (meta.schema !== SCHEMA_VERSION) return void 0;
5341
+ if (meta.handle !== handle) return void 0;
5342
+ if (!FORMAT_NAMES.includes(meta.format)) return void 0;
5343
+ if (!isPositiveInteger(meta.revision)) return void 0;
5344
+ if (!isNonNegativeInteger(meta.bytes)) return void 0;
5345
+ if (!isNonNegativeInteger(meta.createdAt)) return void 0;
5346
+ if (!isNonNegativeInteger(meta.updatedAt)) return void 0;
5347
+ if (meta.title !== void 0 && typeof meta.title !== "string")
5348
+ return void 0;
5349
+ const pinnedRevisions = Array.isArray(meta.pinnedRevisions) ? meta.pinnedRevisions.filter(isPositiveInteger) : [];
5350
+ return {
5351
+ handle,
5352
+ format: meta.format,
5353
+ revision: meta.revision,
5354
+ bytes: meta.bytes,
5355
+ createdAt: meta.createdAt,
5356
+ updatedAt: meta.updatedAt,
5357
+ ...typeof meta.title === "string" && { title: meta.title },
5358
+ pinnedRevisions: [...pinnedRevisions].sort((a, b) => a - b)
5359
+ };
5360
+ }
5361
+ function isPositiveInteger(value) {
5362
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
5363
+ }
5364
+ function isNonNegativeInteger(value) {
5365
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
5366
+ }
5367
+
5368
+ // src/workspace/store.ts
5043
5369
  var WORKSPACE_ERROR_CODES = {
5044
5370
  EVICTED: "E_WORKSPACE_EVICTED",
5045
5371
  LIMIT: "E_WORKSPACE_LIMIT",
5046
5372
  DOCUMENT_TOO_LARGE: "E_DOCUMENT_TOO_LARGE",
5047
- INVALID_ROOT: "E_INVALID_DOCUMENT_ROOT"
5373
+ INVALID_ROOT: "E_INVALID_DOCUMENT_ROOT",
5374
+ /**
5375
+ * A revision could not be mirrored to disk (#290). A warning, never a
5376
+ * failure: the edit landed and the workspace is usable, but this connection
5377
+ * has stopped being survivable and saying so is the whole point.
5378
+ */
5379
+ NOT_PERSISTED: "W_WORKSPACE_NOT_PERSISTED",
5380
+ /**
5381
+ * A close could not delete the durable copy, so nothing was released (#290).
5382
+ * An error rather than a warning: the agent asked for the document to be
5383
+ * destroyed, and it is still there to be read by the next connection.
5384
+ */
5385
+ NOT_CLOSED: "E_WORKSPACE_NOT_CLOSED"
5048
5386
  };
5049
5387
  var DEFAULT_WORKSPACE_LIMITS = {
5050
5388
  maxWorkspaces: 16,
@@ -5064,6 +5402,7 @@ function createMemoryWorkspaceStore(options = {}) {
5064
5402
  };
5065
5403
  const now = options.now ?? Date.now;
5066
5404
  const newHandle = options.newHandle ?? defaultHandle;
5405
+ const persistence = options.persistence;
5067
5406
  const entries = /* @__PURE__ */ new Map();
5068
5407
  const tombstones = /* @__PURE__ */ new Map();
5069
5408
  function footprint(entry) {
@@ -5076,12 +5415,8 @@ function createMemoryWorkspaceStore(options = {}) {
5076
5415
  for (const entry of entries.values()) total += footprint(entry);
5077
5416
  return total;
5078
5417
  }
5079
- function tombstone(entry, reason) {
5080
- tombstones.set(entry.handle, {
5081
- reason,
5082
- at: now(),
5083
- revision: entry.revision
5084
- });
5418
+ function tombstone(handle, reason, revision) {
5419
+ tombstones.set(handle, { reason, at: now(), revision });
5085
5420
  while (tombstones.size > MAX_TOMBSTONES) {
5086
5421
  const oldest = tombstones.keys().next();
5087
5422
  if (oldest.done) break;
@@ -5091,7 +5426,7 @@ function createMemoryWorkspaceStore(options = {}) {
5091
5426
  function evict(entry, reason) {
5092
5427
  entries.delete(entry.handle);
5093
5428
  tombstones.delete(entry.handle);
5094
- tombstone(entry, reason);
5429
+ tombstone(entry.handle, reason, entry.revision);
5095
5430
  }
5096
5431
  function sweep() {
5097
5432
  const at = now();
@@ -5099,6 +5434,108 @@ function createMemoryWorkspaceStore(options = {}) {
5099
5434
  if (at - entry.touchedAt > limits.idleTtlMs) evict(entry, "ttl");
5100
5435
  }
5101
5436
  }
5437
+ async function persist(entry) {
5438
+ if (!persistence) return void 0;
5439
+ try {
5440
+ await persistence.save({
5441
+ handle: entry.handle,
5442
+ format: entry.format,
5443
+ createdAt: entry.createdAt,
5444
+ updatedAt: entry.updatedAt,
5445
+ ...entry.title !== void 0 && { title: entry.title },
5446
+ head: {
5447
+ revision: entry.revision,
5448
+ text: entry.text,
5449
+ bytes: entry.bytes
5450
+ },
5451
+ pins: [...entry.pins.entries()].map(([revision, pin]) => ({
5452
+ revision,
5453
+ text: pin.text,
5454
+ bytes: pin.bytes
5455
+ }))
5456
+ });
5457
+ entry.persisted = true;
5458
+ return void 0;
5459
+ } catch (error) {
5460
+ entry.persisted = false;
5461
+ return diagnostic(
5462
+ WORKSPACE_ERROR_CODES.NOT_PERSISTED,
5463
+ `Revision ${entry.revision} of ${entry.handle} is held in memory but was not written to ${persistence.root}: ${error instanceof Error ? error.message : String(error)}`,
5464
+ {
5465
+ severity: "warning",
5466
+ suggestion: "Export the JSON with jto_workspace_snapshot and keep it yourself; this handle will not survive a lost connection.",
5467
+ context: {
5468
+ handle: entry.handle,
5469
+ revision: entry.revision,
5470
+ workspaceRoot: persistence.root
5471
+ }
5472
+ }
5473
+ );
5474
+ }
5475
+ }
5476
+ function committed(record, warning) {
5477
+ return { ok: true, record, ...warning && { warnings: [warning] } };
5478
+ }
5479
+ async function rehydrate(handle) {
5480
+ if (!persistence || !isPersistableHandle(handle)) return void 0;
5481
+ let loaded;
5482
+ try {
5483
+ loaded = await persistence.load(handle);
5484
+ } catch {
5485
+ return void 0;
5486
+ }
5487
+ if (!loaded) return void 0;
5488
+ const restored = loaded;
5489
+ const pins = [...restored.pins].sort((a, b) => b.revision - a.revision).slice(0, limits.maxPinnedRevisions);
5490
+ const needed = pins.reduce(
5491
+ (total, pin) => total + pin.bytes,
5492
+ restored.head.bytes
5493
+ );
5494
+ if (restored.head.bytes > limits.maxDocumentBytes) {
5495
+ return tooLarge(restored.head.bytes, limits.maxDocumentBytes);
5496
+ }
5497
+ if (!hasRoom(needed, true)) {
5498
+ return failure(
5499
+ WORKSPACE_ERROR_CODES.LIMIT,
5500
+ `Workspace ${handle} is on disk at revision ${restored.revision} but does not fit in this connection's ${limits.maxTotalBytes}-byte budget.`,
5501
+ {
5502
+ suggestion: "Close a workspace you have finished with, then use this handle again.",
5503
+ context: {
5504
+ handle,
5505
+ bytes: needed,
5506
+ maxTotalBytes: limits.maxTotalBytes,
5507
+ maxWorkspaces: limits.maxWorkspaces
5508
+ }
5509
+ }
5510
+ );
5511
+ }
5512
+ const entry = {
5513
+ handle,
5514
+ format: restored.format,
5515
+ revision: restored.head.revision,
5516
+ text: restored.head.text,
5517
+ bytes: restored.head.bytes,
5518
+ createdAt: restored.createdAt,
5519
+ updatedAt: restored.updatedAt,
5520
+ touchedAt: now(),
5521
+ ...restored.title !== void 0 && { title: restored.title },
5522
+ pins: new Map(
5523
+ pins.map((pin) => [pin.revision, { text: pin.text, bytes: pin.bytes }])
5524
+ ),
5525
+ persisted: true
5526
+ };
5527
+ entries.set(handle, entry);
5528
+ tombstones.delete(handle);
5529
+ return { ok: true, entry };
5530
+ }
5531
+ async function resolve3(handle) {
5532
+ const entry = entries.get(handle);
5533
+ if (entry) return { entry };
5534
+ const restored = await rehydrate(handle);
5535
+ if (restored === void 0) return missing(handle);
5536
+ if (!restored.ok) return restored;
5537
+ return { entry: restored.entry };
5538
+ }
5102
5539
  function missing(handle) {
5103
5540
  const grave = tombstones.get(handle);
5104
5541
  if (grave?.reason === "ttl") {
@@ -5134,12 +5571,29 @@ function createMemoryWorkspaceStore(options = {}) {
5134
5571
  createdAt: new Date(entry.createdAt).toISOString(),
5135
5572
  updatedAt: new Date(entry.updatedAt).toISOString(),
5136
5573
  ...entry.title !== void 0 && { title: entry.title },
5137
- pinnedRevisions: [...entry.pins.keys()].sort((a, b) => a - b)
5574
+ pinnedRevisions: [...entry.pins.keys()].sort((a, b) => a - b),
5575
+ ...persistence && { persisted: entry.persisted }
5576
+ };
5577
+ }
5578
+ function fromMeta(meta) {
5579
+ return {
5580
+ handle: meta.handle,
5581
+ format: meta.format,
5582
+ revision: meta.revision,
5583
+ bytes: meta.bytes,
5584
+ createdAt: new Date(meta.createdAt).toISOString(),
5585
+ updatedAt: new Date(meta.updatedAt).toISOString(),
5586
+ ...meta.title !== void 0 && { title: meta.title },
5587
+ pinnedRevisions: [...meta.pinnedRevisions],
5588
+ persisted: true
5138
5589
  };
5139
5590
  }
5140
5591
  return {
5141
5592
  available: true,
5142
5593
  limits,
5594
+ ...persistence && {
5595
+ persistence: { root: persistence.root, limits: persistence.limits }
5596
+ },
5143
5597
  usage() {
5144
5598
  sweep();
5145
5599
  return { workspaces: entries.size, bytes: totalBytes() };
@@ -5180,15 +5634,18 @@ function createMemoryWorkspaceStore(options = {}) {
5180
5634
  updatedAt: at,
5181
5635
  touchedAt: at,
5182
5636
  ...input.title !== void 0 && { title: input.title },
5183
- pins: /* @__PURE__ */ new Map()
5637
+ pins: /* @__PURE__ */ new Map(),
5638
+ persisted: false
5184
5639
  };
5185
5640
  entries.set(entry.handle, entry);
5186
- return { ok: true, record: toRecord(entry) };
5641
+ const warning = await persist(entry);
5642
+ return committed(toRecord(entry), warning);
5187
5643
  },
5188
5644
  async get(handle, readOptions) {
5189
5645
  sweep();
5190
- const entry = entries.get(handle);
5191
- if (!entry) return missing(handle);
5646
+ const found = await resolve3(handle);
5647
+ if (!("entry" in found)) return found;
5648
+ const entry = found.entry;
5192
5649
  entry.touchedAt = now();
5193
5650
  let text = entry.text;
5194
5651
  let bytes = entry.bytes;
@@ -5213,15 +5670,16 @@ function createMemoryWorkspaceStore(options = {}) {
5213
5670
  context: { handle, pointer }
5214
5671
  });
5215
5672
  }
5216
- const found = resolvePointer(document, parsed.tokens);
5217
- if (found.found) projection[pointer] = found.value;
5673
+ const found2 = resolvePointer(document, parsed.tokens);
5674
+ if (found2.found) projection[pointer] = found2.value;
5218
5675
  }
5219
5676
  return { ok: true, record, document, projection };
5220
5677
  },
5221
5678
  async patch(input) {
5222
5679
  sweep();
5223
- const entry = entries.get(input.handle);
5224
- if (!entry) return missing(input.handle);
5680
+ const found = await resolve3(input.handle);
5681
+ if (!("entry" in found)) return found;
5682
+ const entry = found.entry;
5225
5683
  entry.touchedAt = now();
5226
5684
  if (input.baseRevision !== void 0 && input.baseRevision !== entry.revision) {
5227
5685
  return stale(
@@ -5287,12 +5745,14 @@ function createMemoryWorkspaceStore(options = {}) {
5287
5745
  entry.revision += 1;
5288
5746
  entry.updatedAt = now();
5289
5747
  entry.touchedAt = entry.updatedAt;
5290
- return { ok: true, record: toRecord(entry) };
5748
+ const warning = await persist(entry);
5749
+ return committed(toRecord(entry), warning);
5291
5750
  },
5292
5751
  async snapshot(handle) {
5293
5752
  sweep();
5294
- const entry = entries.get(handle);
5295
- if (!entry) return missing(handle);
5753
+ const found = await resolve3(handle);
5754
+ if (!("entry" in found)) return found;
5755
+ const entry = found.entry;
5296
5756
  entry.touchedAt = now();
5297
5757
  const revision = entry.revision;
5298
5758
  const document = JSON.parse(entry.text);
@@ -5301,24 +5761,70 @@ function createMemoryWorkspaceStore(options = {}) {
5301
5761
  entry.pins.set(revision, { text: entry.text, bytes: entry.bytes });
5302
5762
  }
5303
5763
  }
5304
- return { ok: true, record: toRecord(entry), document };
5305
- },
5306
- async list() {
5307
- sweep();
5764
+ const warning = entry.pins.has(revision) ? await persist(entry) : void 0;
5308
5765
  return {
5309
5766
  ok: true,
5310
- records: [...entries.values()].map((entry) => toRecord(entry))
5767
+ record: toRecord(entry),
5768
+ document,
5769
+ ...warning && { warnings: [warning] }
5311
5770
  };
5312
5771
  },
5772
+ async list() {
5773
+ sweep();
5774
+ const records = [...entries.values()].map((entry) => toRecord(entry));
5775
+ if (!persistence) return { ok: true, records };
5776
+ const resident = new Set(records.map((record) => record.handle));
5777
+ let stored = [];
5778
+ try {
5779
+ stored = await persistence.list();
5780
+ } catch {
5781
+ }
5782
+ for (const meta of stored) {
5783
+ if (resident.has(meta.handle)) continue;
5784
+ if (tombstones.get(meta.handle)?.reason === "closed") continue;
5785
+ records.push(fromMeta(meta));
5786
+ }
5787
+ return { ok: true, records };
5788
+ },
5313
5789
  async close(handle) {
5314
5790
  sweep();
5315
5791
  const entry = entries.get(handle);
5316
- if (!entry) return { ok: true, handle, closed: false };
5317
- evict(entry, "closed");
5318
- return { ok: true, handle, closed: true };
5792
+ let durable = false;
5793
+ if (persistence) {
5794
+ try {
5795
+ durable = await persistence.remove(handle);
5796
+ } catch (error) {
5797
+ return failure(
5798
+ WORKSPACE_ERROR_CODES.NOT_CLOSED,
5799
+ `Workspace ${handle} could not be removed from ${persistence.root}: ${error instanceof Error ? error.message : String(error)}. It is still open and still on disk.`,
5800
+ {
5801
+ suggestion: "Check the workspace directory is writable, then close again.",
5802
+ context: { handle, workspaceRoot: persistence.root }
5803
+ }
5804
+ );
5805
+ }
5806
+ }
5807
+ if (entry) evict(entry, "closed");
5808
+ else if (durable) tombstone(handle, "closed", 0);
5809
+ return { ok: true, handle, closed: entry !== void 0 || durable };
5319
5810
  },
5811
+ /**
5812
+ * Release memory, keep the disk.
5813
+ *
5814
+ * The asymmetry with `close` is the point: this exists for a host
5815
+ * reclaiming memory at a moment of its own choosing, and #290 is precisely
5816
+ * about an event that ends a connection not being allowed to destroy
5817
+ * revisions. The handles come back from disk on next use.
5818
+ */
5320
5819
  async closeAll() {
5321
- for (const entry of [...entries.values()]) evict(entry, "closed");
5820
+ for (const entry of [...entries.values()]) {
5821
+ if (!persistence) {
5822
+ evict(entry, "closed");
5823
+ continue;
5824
+ }
5825
+ entries.delete(entry.handle);
5826
+ tombstones.delete(entry.handle);
5827
+ }
5322
5828
  }
5323
5829
  };
5324
5830
  function stale(entry, wanted, pinned, mutation = false) {
@@ -5338,7 +5844,7 @@ function createMemoryWorkspaceStore(options = {}) {
5338
5844
  }
5339
5845
  }
5340
5846
  function defaultHandle() {
5341
- return `ws_${randomBytes2(9).toString("base64url")}`;
5847
+ return `ws_${randomBytes3(9).toString("base64url")}`;
5342
5848
  }
5343
5849
  function serialize(document) {
5344
5850
  let text;
@@ -5391,7 +5897,7 @@ var workspaceSchema = {
5391
5897
  properties: {
5392
5898
  handle: {
5393
5899
  type: "string",
5394
- description: "Opaque, valid only on this connection."
5900
+ description: "Opaque and server-generated. Valid only on this connection, unless the server has a workspace directory \u2014 then it survives a reconnect and jto_workspace_list hands it back."
5395
5901
  },
5396
5902
  format: { type: "string", enum: [...FORMAT_NAMES] },
5397
5903
  revision: {
@@ -5409,6 +5915,10 @@ var workspaceSchema = {
5409
5915
  type: "array",
5410
5916
  items: { type: "integer" },
5411
5917
  description: "Revisions kept retrievable by jto_workspace_snapshot; read one back with `revision`."
5918
+ },
5919
+ persisted: {
5920
+ type: "boolean",
5921
+ description: "True when this revision is mirrored to disk and so survives a lost connection. Absent when this connection has no workspace directory configured."
5412
5922
  }
5413
5923
  },
5414
5924
  required: [
@@ -5422,6 +5932,26 @@ var workspaceSchema = {
5422
5932
  ],
5423
5933
  additionalProperties: false
5424
5934
  };
5935
+ var persistenceSchema = {
5936
+ type: "object",
5937
+ description: "Where revisions survive the connection. Absent when workspaces are memory-only, which is the default.",
5938
+ properties: {
5939
+ root: { type: "string" },
5940
+ maxWorkspaces: { type: "integer" },
5941
+ maxRevisionsPerWorkspace: {
5942
+ type: "integer",
5943
+ description: "Revision files kept per handle: the head plus its pins."
5944
+ },
5945
+ maxEntryBytes: { type: "integer" }
5946
+ },
5947
+ required: [
5948
+ "root",
5949
+ "maxWorkspaces",
5950
+ "maxRevisionsPerWorkspace",
5951
+ "maxEntryBytes"
5952
+ ],
5953
+ additionalProperties: false
5954
+ };
5425
5955
  var limitsSchema = {
5426
5956
  type: "object",
5427
5957
  description: "What this connection allows. Bounded on purpose; see the codes above.",
@@ -5453,7 +5983,9 @@ function isMemoryStore(store) {
5453
5983
  }
5454
5984
  function ensureStore(deps) {
5455
5985
  if (deps.workspaces().available || hasWorkspaceStore()) return;
5456
- const owned = createMemoryWorkspaceStore();
5986
+ const owned = createMemoryWorkspaceStore(
5987
+ deps.workspacePersistence ? { persistence: deps.workspacePersistence } : {}
5988
+ );
5457
5989
  deps.workspaces = () => owned;
5458
5990
  }
5459
5991
  function register8(server, deps) {
@@ -5496,9 +6028,9 @@ function register8(server, deps) {
5496
6028
  ...args.title !== void 0 && { title: args.title }
5497
6029
  });
5498
6030
  if (!created.ok) return created;
5499
- return success(
5500
- { workspace: created.record },
5501
- seeded ? [
6031
+ return success({ workspace: created.record }, [
6032
+ ...created.warnings ?? [],
6033
+ ...seeded ? [
5502
6034
  diagnostic(
5503
6035
  "W_BLANK_DOCUMENT",
5504
6036
  `Opened an empty ${args.format} skeleton; it has no content until you patch some in.`,
@@ -5508,7 +6040,7 @@ function register8(server, deps) {
5508
6040
  }
5509
6041
  )
5510
6042
  ] : []
5511
- );
6043
+ ]);
5512
6044
  })
5513
6045
  )
5514
6046
  );
@@ -5574,17 +6106,20 @@ function register8(server, deps) {
5574
6106
  const missingPaths = projecting ? args.paths.filter(
5575
6107
  (pointer) => !(pointer in projection)
5576
6108
  ) : [];
5577
- const diagnostics = missingPaths.map(
5578
- (pointer) => diagnostic(
5579
- "W_PATH_NOT_FOUND",
5580
- `${pointer} does not resolve in revision ${read.record.revision}.`,
5581
- {
5582
- severity: "warning",
5583
- path: pointer,
5584
- suggestion: "Read a shorter prefix of the pointer to see what is actually there."
5585
- }
6109
+ const diagnostics = [
6110
+ ...read.warnings ?? [],
6111
+ ...missingPaths.map(
6112
+ (pointer) => diagnostic(
6113
+ "W_PATH_NOT_FOUND",
6114
+ `${pointer} does not resolve in revision ${read.record.revision}.`,
6115
+ {
6116
+ severity: "warning",
6117
+ path: pointer,
6118
+ suggestion: "Read a shorter prefix of the pointer to see what is actually there."
6119
+ }
6120
+ )
5586
6121
  )
5587
- );
6122
+ ];
5588
6123
  return success(
5589
6124
  {
5590
6125
  workspace: read.record,
@@ -5653,7 +6188,7 @@ function register8(server, deps) {
5653
6188
  }
5654
6189
  });
5655
6190
  if (!patched.ok) return patched;
5656
- return success({ workspace: patched.record });
6191
+ return success({ workspace: patched.record }, patched.warnings ?? []);
5657
6192
  })
5658
6193
  )
5659
6194
  );
@@ -5699,7 +6234,7 @@ function register8(server, deps) {
5699
6234
  await guarded(async () => {
5700
6235
  const snapshot = await deps.workspaces().snapshot(args.handle);
5701
6236
  if (!snapshot.ok) return snapshot;
5702
- const diagnostics = [];
6237
+ const diagnostics = [...snapshot.warnings ?? []];
5703
6238
  if (!snapshot.record.pinnedRevisions.includes(snapshot.record.revision)) {
5704
6239
  diagnostics.push(
5705
6240
  diagnostic(
@@ -5742,7 +6277,7 @@ function register8(server, deps) {
5742
6277
  "jto_workspace_list",
5743
6278
  {
5744
6279
  title: "List open workspaces",
5745
- description: "Every document open on this connection, with its revision and size. Call this to recover handles you no longer have \u2014 it is the cheapest way back after losing track of what you opened. An empty list means nothing is open, not that anything failed.",
6280
+ description: "Every document open on this connection, with its revision and size. Call this to recover handles you no longer have \u2014 it is the cheapest way back after losing track of what you opened, including after a reconnect when the server has a workspace directory. An empty list means nothing is open, not that anything failed.",
5746
6281
  annotations: { readOnlyHint: true, openWorldHint: false },
5747
6282
  inputSchema: S({
5748
6283
  type: "object",
@@ -5757,6 +6292,7 @@ function register8(server, deps) {
5757
6292
  description: "False when this connection has workspaces switched off."
5758
6293
  },
5759
6294
  limits: limitsSchema,
6295
+ persistence: persistenceSchema,
5760
6296
  usage: {
5761
6297
  type: "object",
5762
6298
  properties: {
@@ -5774,12 +6310,24 @@ function register8(server, deps) {
5774
6310
  const store = deps.workspaces();
5775
6311
  const listed = await store.list();
5776
6312
  if (!listed.ok) return listed;
5777
- const budget = isMemoryStore(store) ? { limits: store.limits, usage: store.usage() } : {};
5778
- return success({
5779
- workspaces: listed.records,
5780
- available: store.available,
5781
- ...budget
5782
- });
6313
+ const budget = isMemoryStore(store) ? {
6314
+ limits: store.limits,
6315
+ usage: store.usage(),
6316
+ ...store.persistence && {
6317
+ persistence: {
6318
+ root: store.persistence.root,
6319
+ ...store.persistence.limits
6320
+ }
6321
+ }
6322
+ } : {};
6323
+ return success(
6324
+ {
6325
+ workspaces: listed.records,
6326
+ available: store.available,
6327
+ ...budget
6328
+ },
6329
+ listed.warnings ?? []
6330
+ );
5783
6331
  })
5784
6332
  )
5785
6333
  );
@@ -5787,7 +6335,7 @@ function register8(server, deps) {
5787
6335
  "jto_workspace_close",
5788
6336
  {
5789
6337
  title: "Close a workspace",
5790
- description: "Release a handle and the memory behind it, including its pinned snapshots. Idempotent: closing a handle that is already gone reports `closed: false` rather than failing. Snapshot anything you still want first \u2014 closing is not recoverable.",
6338
+ description: "Release a handle and everything behind it \u2014 the document, its pinned snapshots, and the durable copy when this connection keeps one. Idempotent: closing a handle that is already gone reports `closed: false` rather than failing. Snapshot anything you still want first \u2014 closing is not recoverable.",
5791
6339
  annotations: {
5792
6340
  readOnlyHint: false,
5793
6341
  destructiveHint: true,
@@ -6004,6 +6552,12 @@ function createServerFactory(deps) {
6004
6552
 
6005
6553
  // src/lib/deps.ts
6006
6554
  function createToolDeps(options = {}) {
6555
+ const workspacePersistence = options.workspacePersistence ?? createWorkspacePersistence({
6556
+ ...options.workspaceDir !== void 0 && {
6557
+ flagDir: options.workspaceDir
6558
+ },
6559
+ ...options.env !== void 0 && { env: options.env }
6560
+ });
6007
6561
  return {
6008
6562
  serverVersion: options.serverVersion ?? SERVER_VERSION,
6009
6563
  outputRoot: options.outputRoot ?? createOutputRoot({
@@ -6012,10 +6566,13 @@ function createToolDeps(options = {}) {
6012
6566
  }),
6013
6567
  getAdapter: options.getAdapter ?? getAdapter,
6014
6568
  workspaces: options.workspaces ?? getWorkspaceStore,
6569
+ ...workspacePersistence !== void 0 && { workspacePersistence },
6015
6570
  maxInlineArtifactBytes: options.maxInlineArtifactBytes ?? MAX_INLINE_ARTIFACT_BYTES
6016
6571
  };
6017
6572
  }
6018
6573
  export {
6574
+ DEFAULT_PERSISTENCE_LIMITS,
6575
+ DEFAULT_WORKSPACE_LIMITS,
6019
6576
  DOCUMENT_SOURCE_RULE,
6020
6577
  ERROR_CODES,
6021
6578
  FORMAT_NAMES,
@@ -6028,15 +6585,20 @@ export {
6028
6585
  SERVER_INSTRUCTIONS,
6029
6586
  SERVER_NAME,
6030
6587
  SERVER_VERSION,
6588
+ WORKSPACE_DIR_ENV,
6589
+ WORKSPACE_ERROR_CODES,
6031
6590
  artifactOutputProperties,
6032
6591
  artifactSchema,
6033
6592
  checkOutputName,
6034
6593
  checkRenderer,
6035
6594
  countDiagnostics,
6595
+ createMemoryWorkspaceStore,
6036
6596
  createOutputRoot,
6037
6597
  createServer,
6038
6598
  createServerFactory,
6039
6599
  createToolDeps,
6600
+ createWorkspacePersistence,
6601
+ createWorkspacePersistenceAt,
6040
6602
  deliverArtifact,
6041
6603
  diagnostic,
6042
6604
  diagnosticSchema,