@json-to-office/mcp-server 1.4.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/README.md +33 -15
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +846 -109
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +284 -3
- package/dist/index.js +815 -107
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
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
|
+
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
|
|
|
@@ -216,6 +216,8 @@ var ERROR_CODES = {
|
|
|
216
216
|
HOST_NOTE: "W_HOST_NOTE",
|
|
217
217
|
/** A generation warning the core raised without a code of its own. */
|
|
218
218
|
GENERATION: "W_GENERATION",
|
|
219
|
+
/** A design-quality rule threw, so its whole class of findings is missing. */
|
|
220
|
+
QUALITY_RULE_ERROR: "W_QUALITY_RULE_ERROR",
|
|
219
221
|
/** A required host binary (LibreOffice, poppler) is absent. */
|
|
220
222
|
DEPENDENCY_MISSING: "E_DEPENDENCY_MISSING",
|
|
221
223
|
/** The client cancelled the request. */
|
|
@@ -312,6 +314,25 @@ function toolResult(payload) {
|
|
|
312
314
|
};
|
|
313
315
|
}
|
|
314
316
|
var HOST_DEPENDENCY_ERRORS = /* @__PURE__ */ new Set([RENDERER_DEPENDENCY_MISSING]);
|
|
317
|
+
function qualityOptionDiagnostic(error) {
|
|
318
|
+
const code = qualityCallerCode(error);
|
|
319
|
+
if (code === void 0 || code === ERROR_CODES.INVALID_DOCUMENT) {
|
|
320
|
+
return void 0;
|
|
321
|
+
}
|
|
322
|
+
return diagnostic(
|
|
323
|
+
code,
|
|
324
|
+
error instanceof Error ? error.message : String(error)
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
function qualityCallerCode(error) {
|
|
328
|
+
const code = error?.code;
|
|
329
|
+
if (code === "QUALITY_PROFILE_INCOMPATIBLE")
|
|
330
|
+
return OPTION_ERROR_CODES.INVALID_QUALITY_PROFILE;
|
|
331
|
+
if (code === "QUALITY_POLICY_INVALID")
|
|
332
|
+
return OPTION_ERROR_CODES.INVALID_QUALITY_POLICY;
|
|
333
|
+
if (code === "QUALITY_GATE_FAILED") return ERROR_CODES.INVALID_DOCUMENT;
|
|
334
|
+
return void 0;
|
|
335
|
+
}
|
|
315
336
|
function stackAllowed() {
|
|
316
337
|
const flag = process.env.JTO_MCP_DEBUG_STACKS;
|
|
317
338
|
return flag === "1" || flag === "true";
|
|
@@ -362,7 +383,7 @@ async function guarded(body) {
|
|
|
362
383
|
return withHostNotes(result, notes);
|
|
363
384
|
} catch (error) {
|
|
364
385
|
const message2 = error instanceof Error ? error.message : String(error);
|
|
365
|
-
const code = error instanceof Error && HOST_DEPENDENCY_ERRORS.has(error.name) ? ERROR_CODES.DEPENDENCY_MISSING : ERROR_CODES.INTERNAL;
|
|
386
|
+
const code = qualityCallerCode(error) ?? (error instanceof Error && HOST_DEPENDENCY_ERRORS.has(error.name) ? ERROR_CODES.DEPENDENCY_MISSING : ERROR_CODES.INTERNAL);
|
|
366
387
|
return withHostNotes(
|
|
367
388
|
failure(code, message2, {
|
|
368
389
|
context: {
|
|
@@ -381,7 +402,11 @@ var OPTION_ERROR_CODES = {
|
|
|
381
402
|
/** `themePath` is not a data-only JSON theme path. */
|
|
382
403
|
INVALID_THEME_PATH: "E_INVALID_THEME_PATH",
|
|
383
404
|
/** The tool does not support the requested format. */
|
|
384
|
-
UNSUPPORTED_FORMAT: "E_UNSUPPORTED_FORMAT"
|
|
405
|
+
UNSUPPORTED_FORMAT: "E_UNSUPPORTED_FORMAT",
|
|
406
|
+
/** `quality.profile` does not cover the format or renderer of this run. */
|
|
407
|
+
INVALID_QUALITY_PROFILE: "E_INVALID_QUALITY_PROFILE",
|
|
408
|
+
/** `quality.policy` sets a gate, severity or budget that is not a legal value. */
|
|
409
|
+
INVALID_QUALITY_POLICY: "E_INVALID_QUALITY_POLICY"
|
|
385
410
|
};
|
|
386
411
|
var DEFERRED_TO_COMPILER = /* @__PURE__ */ new Set([
|
|
387
412
|
ERROR_CODES.UNSUPPORTED_RENDERER_FEATURE
|
|
@@ -390,9 +415,9 @@ var ROOT_SENTINELS = /* @__PURE__ */ new Set(["", "/", "#", "root"]);
|
|
|
390
415
|
function escapePointerSegment(segment) {
|
|
391
416
|
return segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
392
417
|
}
|
|
393
|
-
function toJsonPointer(
|
|
394
|
-
if (
|
|
395
|
-
const trimmed =
|
|
418
|
+
function toJsonPointer(path7) {
|
|
419
|
+
if (path7 === void 0) return void 0;
|
|
420
|
+
const trimmed = path7.trim();
|
|
396
421
|
if (ROOT_SENTINELS.has(trimmed)) return "";
|
|
397
422
|
const segments = trimmed.startsWith("/") ? trimmed.slice(1).split("/") : trimmed.replace(/\[(\d+)\]/g, ".$1").split(".").filter((segment) => segment !== "");
|
|
398
423
|
if (segments.length === 0) return "";
|
|
@@ -421,6 +446,34 @@ function validationDiagnostics(errors) {
|
|
|
421
446
|
})
|
|
422
447
|
);
|
|
423
448
|
}
|
|
449
|
+
function qualityAnalysisDiagnostics(analysis) {
|
|
450
|
+
return analysis.diagnostics.map((diagnostic2) => ({
|
|
451
|
+
source: diagnostic2.source,
|
|
452
|
+
ruleId: diagnostic2.ruleId,
|
|
453
|
+
category: diagnostic2.category,
|
|
454
|
+
certainty: diagnostic2.certainty,
|
|
455
|
+
severity: diagnostic2.severity,
|
|
456
|
+
code: diagnostic2.code,
|
|
457
|
+
message: diagnostic2.message,
|
|
458
|
+
path: diagnostic2.path,
|
|
459
|
+
blocking: diagnostic2.blocking,
|
|
460
|
+
...diagnostic2.suggestion !== void 0 && {
|
|
461
|
+
suggestion: diagnostic2.suggestion
|
|
462
|
+
},
|
|
463
|
+
...diagnostic2.context !== void 0 && {
|
|
464
|
+
context: { ...diagnostic2.context }
|
|
465
|
+
},
|
|
466
|
+
...diagnostic2.relatedPaths !== void 0 && {
|
|
467
|
+
relatedPaths: diagnostic2.relatedPaths
|
|
468
|
+
},
|
|
469
|
+
...diagnostic2.evidence !== void 0 && {
|
|
470
|
+
evidence: { ...diagnostic2.evidence }
|
|
471
|
+
},
|
|
472
|
+
...diagnostic2.fixes !== void 0 && {
|
|
473
|
+
fixes: diagnostic2.fixes
|
|
474
|
+
}
|
|
475
|
+
}));
|
|
476
|
+
}
|
|
424
477
|
function looksLikeValidationErrors(value) {
|
|
425
478
|
return Array.isArray(value) && value.length > 0 && value.every(
|
|
426
479
|
(entry) => typeof entry === "object" && entry !== null && typeof entry.message === "string"
|
|
@@ -565,11 +618,11 @@ async function probeService(rawUrl, envVar) {
|
|
|
565
618
|
}
|
|
566
619
|
const port = Number(target.port || (target.protocol === "https:" ? 443 : 80));
|
|
567
620
|
const net = await import("net");
|
|
568
|
-
return new Promise((
|
|
621
|
+
return new Promise((resolve3) => {
|
|
569
622
|
const socket = net.connect({ host: target.hostname, port });
|
|
570
623
|
const settle = (detail) => {
|
|
571
624
|
socket.destroy();
|
|
572
|
-
|
|
625
|
+
resolve3({
|
|
573
626
|
available: detail === void 0,
|
|
574
627
|
url: target.origin,
|
|
575
628
|
envVar,
|
|
@@ -609,7 +662,7 @@ function register(server, deps) {
|
|
|
609
662
|
"jto_info",
|
|
610
663
|
{
|
|
611
664
|
title: "Server info",
|
|
612
|
-
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.",
|
|
613
666
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
614
667
|
inputSchema: S({
|
|
615
668
|
type: "object",
|
|
@@ -694,9 +747,17 @@ function register(server, deps) {
|
|
|
694
747
|
type: "object",
|
|
695
748
|
properties: {
|
|
696
749
|
available: { type: "boolean" },
|
|
697
|
-
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
|
+
}
|
|
698
759
|
},
|
|
699
|
-
required: ["available", "open"],
|
|
760
|
+
required: ["available", "open", "persistent"],
|
|
700
761
|
additionalProperties: false
|
|
701
762
|
},
|
|
702
763
|
output: {
|
|
@@ -857,7 +918,11 @@ function register(server, deps) {
|
|
|
857
918
|
formats,
|
|
858
919
|
workspaces: {
|
|
859
920
|
available: store.available,
|
|
860
|
-
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
|
+
}
|
|
861
926
|
},
|
|
862
927
|
output: {
|
|
863
928
|
root: deps.outputRoot.path,
|
|
@@ -1117,10 +1182,10 @@ var STARTERS = [
|
|
|
1117
1182
|
id: "pptx-minimal",
|
|
1118
1183
|
format: "pptx",
|
|
1119
1184
|
title: "Minimal presentation",
|
|
1120
|
-
description: "The smallest
|
|
1185
|
+
description: "The smallest well-formed .pptx: root with a declared 16:9 canvas, one slide, one title text. The canvas stays: without it the renderer silently falls back to 4:3.",
|
|
1121
1186
|
document: {
|
|
1122
1187
|
name: "pptx",
|
|
1123
|
-
props: { title: "Untitled deck" },
|
|
1188
|
+
props: { title: "Untitled deck", slideWidth: 13.333, slideHeight: 7.5 },
|
|
1124
1189
|
children: [
|
|
1125
1190
|
{
|
|
1126
1191
|
name: "slide",
|
|
@@ -1972,14 +2037,14 @@ function sourceSummary(resolved) {
|
|
|
1972
2037
|
revision: resolved.revision
|
|
1973
2038
|
} : { origin: "inline" };
|
|
1974
2039
|
}
|
|
1975
|
-
function parseDocumentJson(text,
|
|
2040
|
+
function parseDocumentJson(text, path7) {
|
|
1976
2041
|
try {
|
|
1977
2042
|
return { ok: true, document: JSON.parse(text) };
|
|
1978
2043
|
} catch (error) {
|
|
1979
2044
|
return failure(
|
|
1980
2045
|
ERROR_CODES.INVALID_JSON,
|
|
1981
2046
|
error instanceof Error ? error.message : String(error),
|
|
1982
|
-
{ ...
|
|
2047
|
+
{ ...path7 !== void 0 && { path: path7 } }
|
|
1983
2048
|
);
|
|
1984
2049
|
}
|
|
1985
2050
|
}
|
|
@@ -1994,17 +2059,26 @@ function capDiagnostics(diagnostics, limit) {
|
|
|
1994
2059
|
if (diagnostics.length <= limit)
|
|
1995
2060
|
return { kept: diagnostics, truncated: false };
|
|
1996
2061
|
const ordered = [...diagnostics].sort(
|
|
1997
|
-
(a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]
|
|
2062
|
+
(a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] || Number(b.blocking === true) - Number(a.blocking === true)
|
|
1998
2063
|
);
|
|
1999
2064
|
return { kept: ordered.slice(0, limit), truncated: true };
|
|
2000
2065
|
}
|
|
2066
|
+
function ruleErrorDiagnostics(analysis) {
|
|
2067
|
+
return analysis.ruleErrors.map(
|
|
2068
|
+
(entry) => diagnostic(
|
|
2069
|
+
ERROR_CODES.QUALITY_RULE_ERROR,
|
|
2070
|
+
`Quality rule "${entry.ruleId}" failed: ${entry.message}`,
|
|
2071
|
+
{ severity: "warning", source: "quality", ruleId: entry.ruleId }
|
|
2072
|
+
)
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2001
2075
|
var DEFAULT_MAX_DIAGNOSTICS = 100;
|
|
2002
2076
|
function register4(server, deps) {
|
|
2003
2077
|
server.registerTool(
|
|
2004
2078
|
"jto_validate",
|
|
2005
2079
|
{
|
|
2006
2080
|
title: "Validate a document",
|
|
2007
|
-
description: "Check a document against its format schema and report every defect as a path-addressed diagnostic. Paths are RFC 6901 JSON Pointers into the document you passed, so they can be used directly as patch targets; codes are the stable `E_`/`W_` vocabulary
|
|
2081
|
+
description: "Check a document against its format schema and report every defect as a path-addressed diagnostic. Paths are RFC 6901 JSON Pointers into the document you passed, so they can be used directly as patch targets; codes are the stable `E_`/`W_` vocabulary. `ok` mirrors generation: schema and semantic errors block; design-quality `W_QUALITY_*` findings advise by default and block only when `quality.policy.gate` requests it. A broken document is a normal result with `ok: false`, never a protocol error.",
|
|
2008
2082
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
2009
2083
|
inputSchema: S({
|
|
2010
2084
|
type: "object",
|
|
@@ -2020,6 +2094,29 @@ function register4(server, deps) {
|
|
|
2020
2094
|
minimum: 1,
|
|
2021
2095
|
maximum: 1e3,
|
|
2022
2096
|
description: `Cap on returned diagnostics (default ${DEFAULT_MAX_DIAGNOSTICS}). Errors are kept ahead of warnings when the cap bites.`
|
|
2097
|
+
},
|
|
2098
|
+
quality: {
|
|
2099
|
+
type: "object",
|
|
2100
|
+
description: "Optional design profile plus per-run enforcement policy.",
|
|
2101
|
+
properties: {
|
|
2102
|
+
profile: {
|
|
2103
|
+
type: "object",
|
|
2104
|
+
properties: { id: { type: "string", minLength: 1 } },
|
|
2105
|
+
required: ["id"],
|
|
2106
|
+
additionalProperties: true
|
|
2107
|
+
},
|
|
2108
|
+
policy: {
|
|
2109
|
+
type: "object",
|
|
2110
|
+
properties: {
|
|
2111
|
+
gate: {
|
|
2112
|
+
type: "string",
|
|
2113
|
+
enum: ["none", "error", "warning", "info"]
|
|
2114
|
+
}
|
|
2115
|
+
},
|
|
2116
|
+
additionalProperties: true
|
|
2117
|
+
}
|
|
2118
|
+
},
|
|
2119
|
+
additionalProperties: false
|
|
2023
2120
|
}
|
|
2024
2121
|
},
|
|
2025
2122
|
required: ["format"],
|
|
@@ -2050,7 +2147,11 @@ function register4(server, deps) {
|
|
|
2050
2147
|
},
|
|
2051
2148
|
truncated: {
|
|
2052
2149
|
type: "boolean",
|
|
2053
|
-
description: "`diagnostics` was capped by `maxDiagnostics
|
|
2150
|
+
description: "`diagnostics` was capped, by `maxDiagnostics` or by the budget the quality policy set."
|
|
2151
|
+
},
|
|
2152
|
+
profileId: {
|
|
2153
|
+
type: "string",
|
|
2154
|
+
description: "The quality profile the design analysis ran under, when one applied."
|
|
2054
2155
|
}
|
|
2055
2156
|
})
|
|
2056
2157
|
)
|
|
@@ -2070,24 +2171,52 @@ function register4(server, deps) {
|
|
|
2070
2171
|
resolved.document,
|
|
2071
2172
|
args.renderer
|
|
2072
2173
|
);
|
|
2073
|
-
|
|
2174
|
+
let analysis;
|
|
2175
|
+
let qualityOption;
|
|
2176
|
+
if (adapter.analyzeQuality) {
|
|
2177
|
+
try {
|
|
2178
|
+
analysis = await adapter.analyzeQuality(resolved.document, {
|
|
2179
|
+
renderer: args.renderer,
|
|
2180
|
+
quality: args.quality
|
|
2181
|
+
});
|
|
2182
|
+
} catch (error) {
|
|
2183
|
+
const option = result.valid ? void 0 : qualityOptionDiagnostic(error);
|
|
2184
|
+
if (!option) throw error;
|
|
2185
|
+
qualityOption = option;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
const structural = [
|
|
2074
2189
|
...validationDiagnostics(result.errors),
|
|
2075
|
-
...unavailable2 ? [unavailable2] : []
|
|
2190
|
+
...unavailable2 ? [unavailable2] : [],
|
|
2191
|
+
...qualityOption ? [qualityOption] : []
|
|
2192
|
+
];
|
|
2193
|
+
const all = [
|
|
2194
|
+
...structural,
|
|
2195
|
+
...analysis ? [
|
|
2196
|
+
...qualityAnalysisDiagnostics(analysis),
|
|
2197
|
+
...ruleErrorDiagnostics(analysis)
|
|
2198
|
+
] : []
|
|
2076
2199
|
];
|
|
2077
2200
|
const counts = countDiagnostics(all);
|
|
2201
|
+
const blocked = countDiagnostics(structural).error > 0 || analysis?.blocked === true;
|
|
2078
2202
|
const { kept, truncated } = capDiagnostics(
|
|
2079
2203
|
all,
|
|
2080
2204
|
args.maxDiagnostics ?? DEFAULT_MAX_DIAGNOSTICS
|
|
2081
2205
|
);
|
|
2082
2206
|
return {
|
|
2083
|
-
ok:
|
|
2207
|
+
ok: !blocked,
|
|
2084
2208
|
diagnostics: kept,
|
|
2085
|
-
valid:
|
|
2209
|
+
valid: !blocked,
|
|
2086
2210
|
format: args.format,
|
|
2087
2211
|
...args.renderer !== void 0 && { renderer: args.renderer },
|
|
2088
2212
|
source: sourceSummary(resolved),
|
|
2089
2213
|
counts,
|
|
2090
|
-
|
|
2214
|
+
// The engine caps its own list under a policy budget, so a report
|
|
2215
|
+
// shortened there would otherwise come back reading complete.
|
|
2216
|
+
truncated: truncated || analysis?.truncated === true,
|
|
2217
|
+
...analysis?.profileId !== void 0 && {
|
|
2218
|
+
profileId: analysis.profileId
|
|
2219
|
+
}
|
|
2091
2220
|
};
|
|
2092
2221
|
})
|
|
2093
2222
|
)
|
|
@@ -3031,9 +3160,9 @@ async function digestAssets(document, baseDir) {
|
|
|
3031
3160
|
[...references].map(async (reference) => {
|
|
3032
3161
|
const resolved = path4.resolve(root, reference);
|
|
3033
3162
|
try {
|
|
3034
|
-
const
|
|
3035
|
-
if (!
|
|
3036
|
-
entries.push(`${resolved}|${
|
|
3163
|
+
const stat2 = await fs4.stat(resolved);
|
|
3164
|
+
if (!stat2.isFile()) return;
|
|
3165
|
+
entries.push(`${resolved}|${stat2.size}|${stat2.mtimeMs}`);
|
|
3037
3166
|
} catch {
|
|
3038
3167
|
}
|
|
3039
3168
|
})
|
|
@@ -3063,8 +3192,8 @@ function collectAssetReferences(value, into) {
|
|
|
3063
3192
|
async function digestThemeFile(themePath) {
|
|
3064
3193
|
if (!themePath) return "none";
|
|
3065
3194
|
try {
|
|
3066
|
-
const
|
|
3067
|
-
return `${path4.resolve(themePath)}|${
|
|
3195
|
+
const stat2 = await fs4.stat(themePath);
|
|
3196
|
+
return `${path4.resolve(themePath)}|${stat2.size}|${stat2.mtimeMs}`;
|
|
3068
3197
|
} catch {
|
|
3069
3198
|
return `${path4.resolve(themePath)}|missing`;
|
|
3070
3199
|
}
|
|
@@ -3136,18 +3265,18 @@ function missingDependencyFailure(dependencies) {
|
|
|
3136
3265
|
);
|
|
3137
3266
|
}
|
|
3138
3267
|
async function binaryIdentity(binary, args, signal) {
|
|
3139
|
-
const [version,
|
|
3268
|
+
const [version, stat2] = await Promise.all([
|
|
3140
3269
|
readVersion(binary, args, signal),
|
|
3141
3270
|
fs5.stat(binary).catch(() => void 0)
|
|
3142
3271
|
]);
|
|
3143
3272
|
const identity = [
|
|
3144
3273
|
version ?? "unknown",
|
|
3145
|
-
|
|
3274
|
+
stat2 ? `${stat2.size}:${stat2.mtimeMs}` : "nostat"
|
|
3146
3275
|
].join("|");
|
|
3147
3276
|
return { identity, ...version !== void 0 && { version } };
|
|
3148
3277
|
}
|
|
3149
3278
|
function readVersion(binary, args, signal) {
|
|
3150
|
-
return new Promise((
|
|
3279
|
+
return new Promise((resolve3) => {
|
|
3151
3280
|
execFile(
|
|
3152
3281
|
binary,
|
|
3153
3282
|
args,
|
|
@@ -3161,8 +3290,8 @@ function readVersion(binary, args, signal) {
|
|
|
3161
3290
|
const text = `${stdout ?? ""}
|
|
3162
3291
|
${stderr ?? ""}`.trim();
|
|
3163
3292
|
const line = text.split("\n")[0]?.trim();
|
|
3164
|
-
if (!line && error) return
|
|
3165
|
-
|
|
3293
|
+
if (!line && error) return resolve3(void 0);
|
|
3294
|
+
resolve3(line || void 0);
|
|
3166
3295
|
}
|
|
3167
3296
|
);
|
|
3168
3297
|
});
|
|
@@ -3207,12 +3336,12 @@ function defaultPreviewCacheDir() {
|
|
|
3207
3336
|
}
|
|
3208
3337
|
var CACHE_ENTRY_NAME = /^[a-f0-9]{64}\.(?:png|meta\.json)(?:\.tmp-\d+-\d+)?$/;
|
|
3209
3338
|
async function inspectPrivateCacheDir(cacheDir) {
|
|
3210
|
-
const
|
|
3211
|
-
if (!
|
|
3212
|
-
if (typeof process.getuid === "function" &&
|
|
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()) {
|
|
3213
3342
|
return false;
|
|
3214
3343
|
}
|
|
3215
|
-
if (process.platform !== "win32" && (
|
|
3344
|
+
if (process.platform !== "win32" && (stat2.mode & 63) !== 0) {
|
|
3216
3345
|
try {
|
|
3217
3346
|
await fs6.chmod(cacheDir, 448);
|
|
3218
3347
|
} catch {
|
|
@@ -3242,9 +3371,9 @@ async function sweepPreviewCache(cacheDir, options = {}) {
|
|
|
3242
3371
|
for (const name of names) {
|
|
3243
3372
|
if (!CACHE_ENTRY_NAME.test(name)) continue;
|
|
3244
3373
|
const file = path5.join(cacheDir, name);
|
|
3245
|
-
const
|
|
3246
|
-
if (!
|
|
3247
|
-
entries.push({ file, mtimeMs:
|
|
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 });
|
|
3248
3377
|
}
|
|
3249
3378
|
const doomed = entries.filter((entry) => now - entry.mtimeMs > maxAgeMs);
|
|
3250
3379
|
const kept = entries.filter((entry) => now - entry.mtimeMs <= maxAgeMs).sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
@@ -3331,7 +3460,7 @@ function countPdfPages(pdf) {
|
|
|
3331
3460
|
return matches ? matches.length : 0;
|
|
3332
3461
|
}
|
|
3333
3462
|
function exec(binary, args, timeoutMs, options = {}) {
|
|
3334
|
-
return new Promise((
|
|
3463
|
+
return new Promise((resolve3, reject) => {
|
|
3335
3464
|
execFile2(
|
|
3336
3465
|
binary,
|
|
3337
3466
|
args,
|
|
@@ -3342,7 +3471,7 @@ function exec(binary, args, timeoutMs, options = {}) {
|
|
|
3342
3471
|
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
3343
3472
|
...options.signal && { signal: options.signal }
|
|
3344
3473
|
},
|
|
3345
|
-
(error) => error ? reject(error) :
|
|
3474
|
+
(error) => error ? reject(error) : resolve3()
|
|
3346
3475
|
);
|
|
3347
3476
|
});
|
|
3348
3477
|
}
|
|
@@ -3436,7 +3565,7 @@ async function resolvePdfinfo(pdftoppmPath) {
|
|
|
3436
3565
|
return status.available ? status.path : void 0;
|
|
3437
3566
|
}
|
|
3438
3567
|
async function pdfinfoPageCount(pdfinfo, pdfPath, signal) {
|
|
3439
|
-
return new Promise((
|
|
3568
|
+
return new Promise((resolve3) => {
|
|
3440
3569
|
execFile2(
|
|
3441
3570
|
pdfinfo,
|
|
3442
3571
|
[pdfPath],
|
|
@@ -3447,9 +3576,9 @@ async function pdfinfoPageCount(pdfinfo, pdfPath, signal) {
|
|
|
3447
3576
|
...signal && { signal }
|
|
3448
3577
|
},
|
|
3449
3578
|
(error, stdout) => {
|
|
3450
|
-
if (error && !stdout) return
|
|
3579
|
+
if (error && !stdout) return resolve3(void 0);
|
|
3451
3580
|
const match = /^Pages:\s+(\d+)/m.exec(stdout ?? "");
|
|
3452
|
-
|
|
3581
|
+
resolve3(match ? Number(match[1]) : void 0);
|
|
3453
3582
|
}
|
|
3454
3583
|
);
|
|
3455
3584
|
});
|
|
@@ -4562,9 +4691,9 @@ function compilePatch(operations) {
|
|
|
4562
4691
|
}
|
|
4563
4692
|
);
|
|
4564
4693
|
}
|
|
4565
|
-
const
|
|
4566
|
-
if (!
|
|
4567
|
-
return problem(PATCH_ERROR_CODES.INVALID_POINTER,
|
|
4694
|
+
const path7 = parsePointer(raw.path);
|
|
4695
|
+
if (!path7.ok) {
|
|
4696
|
+
return problem(PATCH_ERROR_CODES.INVALID_POINTER, path7.message, index, {
|
|
4568
4697
|
context: { op, pointer: raw.path },
|
|
4569
4698
|
suggestion: 'Pointers are RFC 6901: "/children/0/props/text", with "~0" for "~" and "~1" for "/".'
|
|
4570
4699
|
});
|
|
@@ -4572,7 +4701,7 @@ function compilePatch(operations) {
|
|
|
4572
4701
|
const entry = {
|
|
4573
4702
|
op,
|
|
4574
4703
|
path: raw.path,
|
|
4575
|
-
tokens:
|
|
4704
|
+
tokens: path7.tokens
|
|
4576
4705
|
};
|
|
4577
4706
|
if (VALUE_OPS.has(op)) {
|
|
4578
4707
|
if (raw.value === void 0) {
|
|
@@ -4922,12 +5051,338 @@ function jsonEqualTokens(left, right) {
|
|
|
4922
5051
|
}
|
|
4923
5052
|
|
|
4924
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";
|
|
4925
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
|
|
4926
5369
|
var WORKSPACE_ERROR_CODES = {
|
|
4927
5370
|
EVICTED: "E_WORKSPACE_EVICTED",
|
|
4928
5371
|
LIMIT: "E_WORKSPACE_LIMIT",
|
|
4929
5372
|
DOCUMENT_TOO_LARGE: "E_DOCUMENT_TOO_LARGE",
|
|
4930
|
-
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"
|
|
4931
5386
|
};
|
|
4932
5387
|
var DEFAULT_WORKSPACE_LIMITS = {
|
|
4933
5388
|
maxWorkspaces: 16,
|
|
@@ -4947,6 +5402,7 @@ function createMemoryWorkspaceStore(options = {}) {
|
|
|
4947
5402
|
};
|
|
4948
5403
|
const now = options.now ?? Date.now;
|
|
4949
5404
|
const newHandle = options.newHandle ?? defaultHandle;
|
|
5405
|
+
const persistence = options.persistence;
|
|
4950
5406
|
const entries = /* @__PURE__ */ new Map();
|
|
4951
5407
|
const tombstones = /* @__PURE__ */ new Map();
|
|
4952
5408
|
function footprint(entry) {
|
|
@@ -4959,12 +5415,8 @@ function createMemoryWorkspaceStore(options = {}) {
|
|
|
4959
5415
|
for (const entry of entries.values()) total += footprint(entry);
|
|
4960
5416
|
return total;
|
|
4961
5417
|
}
|
|
4962
|
-
function tombstone(
|
|
4963
|
-
tombstones.set(
|
|
4964
|
-
reason,
|
|
4965
|
-
at: now(),
|
|
4966
|
-
revision: entry.revision
|
|
4967
|
-
});
|
|
5418
|
+
function tombstone(handle, reason, revision) {
|
|
5419
|
+
tombstones.set(handle, { reason, at: now(), revision });
|
|
4968
5420
|
while (tombstones.size > MAX_TOMBSTONES) {
|
|
4969
5421
|
const oldest = tombstones.keys().next();
|
|
4970
5422
|
if (oldest.done) break;
|
|
@@ -4974,7 +5426,7 @@ function createMemoryWorkspaceStore(options = {}) {
|
|
|
4974
5426
|
function evict(entry, reason) {
|
|
4975
5427
|
entries.delete(entry.handle);
|
|
4976
5428
|
tombstones.delete(entry.handle);
|
|
4977
|
-
tombstone(entry, reason);
|
|
5429
|
+
tombstone(entry.handle, reason, entry.revision);
|
|
4978
5430
|
}
|
|
4979
5431
|
function sweep() {
|
|
4980
5432
|
const at = now();
|
|
@@ -4982,6 +5434,108 @@ function createMemoryWorkspaceStore(options = {}) {
|
|
|
4982
5434
|
if (at - entry.touchedAt > limits.idleTtlMs) evict(entry, "ttl");
|
|
4983
5435
|
}
|
|
4984
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
|
+
}
|
|
4985
5539
|
function missing(handle) {
|
|
4986
5540
|
const grave = tombstones.get(handle);
|
|
4987
5541
|
if (grave?.reason === "ttl") {
|
|
@@ -5017,12 +5571,29 @@ function createMemoryWorkspaceStore(options = {}) {
|
|
|
5017
5571
|
createdAt: new Date(entry.createdAt).toISOString(),
|
|
5018
5572
|
updatedAt: new Date(entry.updatedAt).toISOString(),
|
|
5019
5573
|
...entry.title !== void 0 && { title: entry.title },
|
|
5020
|
-
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
|
|
5021
5589
|
};
|
|
5022
5590
|
}
|
|
5023
5591
|
return {
|
|
5024
5592
|
available: true,
|
|
5025
5593
|
limits,
|
|
5594
|
+
...persistence && {
|
|
5595
|
+
persistence: { root: persistence.root, limits: persistence.limits }
|
|
5596
|
+
},
|
|
5026
5597
|
usage() {
|
|
5027
5598
|
sweep();
|
|
5028
5599
|
return { workspaces: entries.size, bytes: totalBytes() };
|
|
@@ -5063,15 +5634,18 @@ function createMemoryWorkspaceStore(options = {}) {
|
|
|
5063
5634
|
updatedAt: at,
|
|
5064
5635
|
touchedAt: at,
|
|
5065
5636
|
...input.title !== void 0 && { title: input.title },
|
|
5066
|
-
pins: /* @__PURE__ */ new Map()
|
|
5637
|
+
pins: /* @__PURE__ */ new Map(),
|
|
5638
|
+
persisted: false
|
|
5067
5639
|
};
|
|
5068
5640
|
entries.set(entry.handle, entry);
|
|
5069
|
-
|
|
5641
|
+
const warning = await persist(entry);
|
|
5642
|
+
return committed(toRecord(entry), warning);
|
|
5070
5643
|
},
|
|
5071
5644
|
async get(handle, readOptions) {
|
|
5072
5645
|
sweep();
|
|
5073
|
-
const
|
|
5074
|
-
if (!entry) return
|
|
5646
|
+
const found = await resolve3(handle);
|
|
5647
|
+
if (!("entry" in found)) return found;
|
|
5648
|
+
const entry = found.entry;
|
|
5075
5649
|
entry.touchedAt = now();
|
|
5076
5650
|
let text = entry.text;
|
|
5077
5651
|
let bytes = entry.bytes;
|
|
@@ -5096,15 +5670,16 @@ function createMemoryWorkspaceStore(options = {}) {
|
|
|
5096
5670
|
context: { handle, pointer }
|
|
5097
5671
|
});
|
|
5098
5672
|
}
|
|
5099
|
-
const
|
|
5100
|
-
if (
|
|
5673
|
+
const found2 = resolvePointer(document, parsed.tokens);
|
|
5674
|
+
if (found2.found) projection[pointer] = found2.value;
|
|
5101
5675
|
}
|
|
5102
5676
|
return { ok: true, record, document, projection };
|
|
5103
5677
|
},
|
|
5104
5678
|
async patch(input) {
|
|
5105
5679
|
sweep();
|
|
5106
|
-
const
|
|
5107
|
-
if (!entry) return
|
|
5680
|
+
const found = await resolve3(input.handle);
|
|
5681
|
+
if (!("entry" in found)) return found;
|
|
5682
|
+
const entry = found.entry;
|
|
5108
5683
|
entry.touchedAt = now();
|
|
5109
5684
|
if (input.baseRevision !== void 0 && input.baseRevision !== entry.revision) {
|
|
5110
5685
|
return stale(
|
|
@@ -5170,12 +5745,14 @@ function createMemoryWorkspaceStore(options = {}) {
|
|
|
5170
5745
|
entry.revision += 1;
|
|
5171
5746
|
entry.updatedAt = now();
|
|
5172
5747
|
entry.touchedAt = entry.updatedAt;
|
|
5173
|
-
|
|
5748
|
+
const warning = await persist(entry);
|
|
5749
|
+
return committed(toRecord(entry), warning);
|
|
5174
5750
|
},
|
|
5175
5751
|
async snapshot(handle) {
|
|
5176
5752
|
sweep();
|
|
5177
|
-
const
|
|
5178
|
-
if (!entry) return
|
|
5753
|
+
const found = await resolve3(handle);
|
|
5754
|
+
if (!("entry" in found)) return found;
|
|
5755
|
+
const entry = found.entry;
|
|
5179
5756
|
entry.touchedAt = now();
|
|
5180
5757
|
const revision = entry.revision;
|
|
5181
5758
|
const document = JSON.parse(entry.text);
|
|
@@ -5184,24 +5761,70 @@ function createMemoryWorkspaceStore(options = {}) {
|
|
|
5184
5761
|
entry.pins.set(revision, { text: entry.text, bytes: entry.bytes });
|
|
5185
5762
|
}
|
|
5186
5763
|
}
|
|
5187
|
-
|
|
5188
|
-
},
|
|
5189
|
-
async list() {
|
|
5190
|
-
sweep();
|
|
5764
|
+
const warning = entry.pins.has(revision) ? await persist(entry) : void 0;
|
|
5191
5765
|
return {
|
|
5192
5766
|
ok: true,
|
|
5193
|
-
|
|
5767
|
+
record: toRecord(entry),
|
|
5768
|
+
document,
|
|
5769
|
+
...warning && { warnings: [warning] }
|
|
5194
5770
|
};
|
|
5195
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
|
+
},
|
|
5196
5789
|
async close(handle) {
|
|
5197
5790
|
sweep();
|
|
5198
5791
|
const entry = entries.get(handle);
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
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 };
|
|
5202
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
|
+
*/
|
|
5203
5819
|
async closeAll() {
|
|
5204
|
-
for (const entry of [...entries.values()])
|
|
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
|
+
}
|
|
5205
5828
|
}
|
|
5206
5829
|
};
|
|
5207
5830
|
function stale(entry, wanted, pinned, mutation = false) {
|
|
@@ -5221,7 +5844,7 @@ function createMemoryWorkspaceStore(options = {}) {
|
|
|
5221
5844
|
}
|
|
5222
5845
|
}
|
|
5223
5846
|
function defaultHandle() {
|
|
5224
|
-
return `ws_${
|
|
5847
|
+
return `ws_${randomBytes3(9).toString("base64url")}`;
|
|
5225
5848
|
}
|
|
5226
5849
|
function serialize(document) {
|
|
5227
5850
|
let text;
|
|
@@ -5274,7 +5897,7 @@ var workspaceSchema = {
|
|
|
5274
5897
|
properties: {
|
|
5275
5898
|
handle: {
|
|
5276
5899
|
type: "string",
|
|
5277
|
-
description: "Opaque
|
|
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."
|
|
5278
5901
|
},
|
|
5279
5902
|
format: { type: "string", enum: [...FORMAT_NAMES] },
|
|
5280
5903
|
revision: {
|
|
@@ -5292,6 +5915,10 @@ var workspaceSchema = {
|
|
|
5292
5915
|
type: "array",
|
|
5293
5916
|
items: { type: "integer" },
|
|
5294
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."
|
|
5295
5922
|
}
|
|
5296
5923
|
},
|
|
5297
5924
|
required: [
|
|
@@ -5305,6 +5932,26 @@ var workspaceSchema = {
|
|
|
5305
5932
|
],
|
|
5306
5933
|
additionalProperties: false
|
|
5307
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
|
+
};
|
|
5308
5955
|
var limitsSchema = {
|
|
5309
5956
|
type: "object",
|
|
5310
5957
|
description: "What this connection allows. Bounded on purpose; see the codes above.",
|
|
@@ -5336,7 +5983,9 @@ function isMemoryStore(store) {
|
|
|
5336
5983
|
}
|
|
5337
5984
|
function ensureStore(deps) {
|
|
5338
5985
|
if (deps.workspaces().available || hasWorkspaceStore()) return;
|
|
5339
|
-
const owned = createMemoryWorkspaceStore(
|
|
5986
|
+
const owned = createMemoryWorkspaceStore(
|
|
5987
|
+
deps.workspacePersistence ? { persistence: deps.workspacePersistence } : {}
|
|
5988
|
+
);
|
|
5340
5989
|
deps.workspaces = () => owned;
|
|
5341
5990
|
}
|
|
5342
5991
|
function register8(server, deps) {
|
|
@@ -5379,9 +6028,9 @@ function register8(server, deps) {
|
|
|
5379
6028
|
...args.title !== void 0 && { title: args.title }
|
|
5380
6029
|
});
|
|
5381
6030
|
if (!created.ok) return created;
|
|
5382
|
-
return success(
|
|
5383
|
-
|
|
5384
|
-
seeded ? [
|
|
6031
|
+
return success({ workspace: created.record }, [
|
|
6032
|
+
...created.warnings ?? [],
|
|
6033
|
+
...seeded ? [
|
|
5385
6034
|
diagnostic(
|
|
5386
6035
|
"W_BLANK_DOCUMENT",
|
|
5387
6036
|
`Opened an empty ${args.format} skeleton; it has no content until you patch some in.`,
|
|
@@ -5391,7 +6040,7 @@ function register8(server, deps) {
|
|
|
5391
6040
|
}
|
|
5392
6041
|
)
|
|
5393
6042
|
] : []
|
|
5394
|
-
);
|
|
6043
|
+
]);
|
|
5395
6044
|
})
|
|
5396
6045
|
)
|
|
5397
6046
|
);
|
|
@@ -5457,17 +6106,20 @@ function register8(server, deps) {
|
|
|
5457
6106
|
const missingPaths = projecting ? args.paths.filter(
|
|
5458
6107
|
(pointer) => !(pointer in projection)
|
|
5459
6108
|
) : [];
|
|
5460
|
-
const diagnostics =
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
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
|
+
)
|
|
5469
6121
|
)
|
|
5470
|
-
|
|
6122
|
+
];
|
|
5471
6123
|
return success(
|
|
5472
6124
|
{
|
|
5473
6125
|
workspace: read.record,
|
|
@@ -5536,7 +6188,7 @@ function register8(server, deps) {
|
|
|
5536
6188
|
}
|
|
5537
6189
|
});
|
|
5538
6190
|
if (!patched.ok) return patched;
|
|
5539
|
-
return success({ workspace: patched.record });
|
|
6191
|
+
return success({ workspace: patched.record }, patched.warnings ?? []);
|
|
5540
6192
|
})
|
|
5541
6193
|
)
|
|
5542
6194
|
);
|
|
@@ -5582,7 +6234,7 @@ function register8(server, deps) {
|
|
|
5582
6234
|
await guarded(async () => {
|
|
5583
6235
|
const snapshot = await deps.workspaces().snapshot(args.handle);
|
|
5584
6236
|
if (!snapshot.ok) return snapshot;
|
|
5585
|
-
const diagnostics = [];
|
|
6237
|
+
const diagnostics = [...snapshot.warnings ?? []];
|
|
5586
6238
|
if (!snapshot.record.pinnedRevisions.includes(snapshot.record.revision)) {
|
|
5587
6239
|
diagnostics.push(
|
|
5588
6240
|
diagnostic(
|
|
@@ -5625,7 +6277,7 @@ function register8(server, deps) {
|
|
|
5625
6277
|
"jto_workspace_list",
|
|
5626
6278
|
{
|
|
5627
6279
|
title: "List open workspaces",
|
|
5628
|
-
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.",
|
|
5629
6281
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
5630
6282
|
inputSchema: S({
|
|
5631
6283
|
type: "object",
|
|
@@ -5640,6 +6292,7 @@ function register8(server, deps) {
|
|
|
5640
6292
|
description: "False when this connection has workspaces switched off."
|
|
5641
6293
|
},
|
|
5642
6294
|
limits: limitsSchema,
|
|
6295
|
+
persistence: persistenceSchema,
|
|
5643
6296
|
usage: {
|
|
5644
6297
|
type: "object",
|
|
5645
6298
|
properties: {
|
|
@@ -5657,12 +6310,24 @@ function register8(server, deps) {
|
|
|
5657
6310
|
const store = deps.workspaces();
|
|
5658
6311
|
const listed = await store.list();
|
|
5659
6312
|
if (!listed.ok) return listed;
|
|
5660
|
-
const budget = isMemoryStore(store) ? {
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
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
|
+
);
|
|
5666
6331
|
})
|
|
5667
6332
|
)
|
|
5668
6333
|
);
|
|
@@ -5670,7 +6335,7 @@ function register8(server, deps) {
|
|
|
5670
6335
|
"jto_workspace_close",
|
|
5671
6336
|
{
|
|
5672
6337
|
title: "Close a workspace",
|
|
5673
|
-
description: "Release a handle and
|
|
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.",
|
|
5674
6339
|
annotations: {
|
|
5675
6340
|
readOnlyHint: false,
|
|
5676
6341
|
destructiveHint: true,
|
|
@@ -5717,6 +6382,7 @@ var RESOURCE_URIS = {
|
|
|
5717
6382
|
catalog: "jto://catalog",
|
|
5718
6383
|
renderers: "jto://renderers",
|
|
5719
6384
|
themes: "jto://themes",
|
|
6385
|
+
themeValues: "jto://themes/values",
|
|
5720
6386
|
templates: "jto://templates",
|
|
5721
6387
|
documentSchema: (format) => `jto://schema/${format}/document`,
|
|
5722
6388
|
themeSchema: (format) => `jto://schema/${format}/theme`
|
|
@@ -5768,7 +6434,7 @@ function register9(server, deps) {
|
|
|
5768
6434
|
RESOURCE_URIS.themes,
|
|
5769
6435
|
{
|
|
5770
6436
|
title: "Built-in themes",
|
|
5771
|
-
description: "Theme names shipped with each format, usable as a document\u2019s props.theme or as the tools\u2019 theme option.",
|
|
6437
|
+
description: "Theme names shipped with each format, usable as a document\u2019s props.theme or as the tools\u2019 theme option. jto://themes/values carries what each name actually looks like.",
|
|
5772
6438
|
mimeType: JSON_MIME
|
|
5773
6439
|
},
|
|
5774
6440
|
async (uri) => {
|
|
@@ -5781,6 +6447,33 @@ function register9(server, deps) {
|
|
|
5781
6447
|
});
|
|
5782
6448
|
}
|
|
5783
6449
|
);
|
|
6450
|
+
server.registerResource(
|
|
6451
|
+
"theme-values",
|
|
6452
|
+
RESOURCE_URIS.themeValues,
|
|
6453
|
+
{
|
|
6454
|
+
title: "Built-in theme values",
|
|
6455
|
+
description: "The palette, fonts, style tables and component defaults behind every built-in theme name \u2014 what a document actually opts into with props.theme. A name alone cannot tell you whether a theme fits the brief; this can.",
|
|
6456
|
+
mimeType: JSON_MIME
|
|
6457
|
+
},
|
|
6458
|
+
async (uri) => {
|
|
6459
|
+
const formats = await Promise.all(
|
|
6460
|
+
FORMAT_NAMES.map(async (format) => {
|
|
6461
|
+
const adapter = deps.getAdapter(format);
|
|
6462
|
+
let themes;
|
|
6463
|
+
try {
|
|
6464
|
+
themes = adapter.getBuiltinThemeValues ? await adapter.getBuiltinThemeValues() : adapter.getBuiltinThemes();
|
|
6465
|
+
} catch {
|
|
6466
|
+
themes = adapter.getBuiltinThemes();
|
|
6467
|
+
}
|
|
6468
|
+
return {
|
|
6469
|
+
format,
|
|
6470
|
+
themes
|
|
6471
|
+
};
|
|
6472
|
+
})
|
|
6473
|
+
);
|
|
6474
|
+
return jsonContents(uri, { formats });
|
|
6475
|
+
}
|
|
6476
|
+
);
|
|
5784
6477
|
server.registerResource(
|
|
5785
6478
|
"templates",
|
|
5786
6479
|
RESOURCE_URIS.templates,
|
|
@@ -5829,6 +6522,7 @@ Working rules:
|
|
|
5829
6522
|
- Discover before authoring. Call jto_info first, then jto_discover and jto_describe_component (or read the jto:// resources) for the components and renderer ids a format actually supports.
|
|
5830
6523
|
- Make small edits. With a workspace handle, patch precisely (RFC 6902 over RFC 6901 paths) instead of resending the whole document; without one, change one region at a time.
|
|
5831
6524
|
- Validate often. Run jto_validate after each edit rather than once at the end; diagnostics are path-addressed, so they map straight back onto the JSON you just changed.
|
|
6525
|
+
- Treat design findings as defects. Schema-valid is not well-designed: jto_validate also lints layout and legibility (W_QUALITY_* \u2014 undeclared slide canvas, text overflowing its box, overcrowded slides, table widths exceeding their section). These never block generation, but they almost always show in the rendered result \u2014 repair them like errors.
|
|
5832
6526
|
- Preview when the answer is visual. jto_preview renders pages to PNG; use it whenever layout, overflow or fit is in question, not only before finishing.
|
|
5833
6527
|
- Snapshot before risky changes. jto_workspace_snapshot pins the current revision so a restructuring you cannot cleanly undo is still recoverable.
|
|
5834
6528
|
|
|
@@ -5858,6 +6552,12 @@ function createServerFactory(deps) {
|
|
|
5858
6552
|
|
|
5859
6553
|
// src/lib/deps.ts
|
|
5860
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
|
+
});
|
|
5861
6561
|
return {
|
|
5862
6562
|
serverVersion: options.serverVersion ?? SERVER_VERSION,
|
|
5863
6563
|
outputRoot: options.outputRoot ?? createOutputRoot({
|
|
@@ -5866,10 +6566,13 @@ function createToolDeps(options = {}) {
|
|
|
5866
6566
|
}),
|
|
5867
6567
|
getAdapter: options.getAdapter ?? getAdapter,
|
|
5868
6568
|
workspaces: options.workspaces ?? getWorkspaceStore,
|
|
6569
|
+
...workspacePersistence !== void 0 && { workspacePersistence },
|
|
5869
6570
|
maxInlineArtifactBytes: options.maxInlineArtifactBytes ?? MAX_INLINE_ARTIFACT_BYTES
|
|
5870
6571
|
};
|
|
5871
6572
|
}
|
|
5872
6573
|
export {
|
|
6574
|
+
DEFAULT_PERSISTENCE_LIMITS,
|
|
6575
|
+
DEFAULT_WORKSPACE_LIMITS,
|
|
5873
6576
|
DOCUMENT_SOURCE_RULE,
|
|
5874
6577
|
ERROR_CODES,
|
|
5875
6578
|
FORMAT_NAMES,
|
|
@@ -5882,15 +6585,20 @@ export {
|
|
|
5882
6585
|
SERVER_INSTRUCTIONS,
|
|
5883
6586
|
SERVER_NAME,
|
|
5884
6587
|
SERVER_VERSION,
|
|
6588
|
+
WORKSPACE_DIR_ENV,
|
|
6589
|
+
WORKSPACE_ERROR_CODES,
|
|
5885
6590
|
artifactOutputProperties,
|
|
5886
6591
|
artifactSchema,
|
|
5887
6592
|
checkOutputName,
|
|
5888
6593
|
checkRenderer,
|
|
5889
6594
|
countDiagnostics,
|
|
6595
|
+
createMemoryWorkspaceStore,
|
|
5890
6596
|
createOutputRoot,
|
|
5891
6597
|
createServer,
|
|
5892
6598
|
createServerFactory,
|
|
5893
6599
|
createToolDeps,
|
|
6600
|
+
createWorkspacePersistence,
|
|
6601
|
+
createWorkspacePersistenceAt,
|
|
5894
6602
|
deliverArtifact,
|
|
5895
6603
|
diagnostic,
|
|
5896
6604
|
diagnosticSchema,
|