@magic-markdown/cli 0.3.19 → 0.3.24
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 +1265 -1697
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -270,13 +270,11 @@ var require_punycode = __commonJS({
|
|
|
270
270
|
});
|
|
271
271
|
|
|
272
272
|
// src/index.ts
|
|
273
|
-
import { readFile as readFile7 } from "node:fs/promises";
|
|
274
273
|
import { resolve as resolve6 } from "node:path";
|
|
275
274
|
|
|
276
275
|
// ../core/src/types.ts
|
|
277
|
-
var
|
|
276
|
+
var WORKSPACE_SCHEMA_VERSION = 1;
|
|
278
277
|
var MANIFEST_PATH = ".mdocs/manifest.json";
|
|
279
|
-
var SIDECAR_DIR = ".mdocs/docs";
|
|
280
278
|
var CHECKPOINT_DIR = ".mdocs/checkpoints";
|
|
281
279
|
|
|
282
280
|
// ../core/src/errors.ts
|
|
@@ -295,6 +293,7 @@ function isMdocsError(error) {
|
|
|
295
293
|
}
|
|
296
294
|
|
|
297
295
|
// ../core/src/ids.ts
|
|
296
|
+
import { createHash } from "node:crypto";
|
|
298
297
|
function createId(prefix) {
|
|
299
298
|
const id = globalThis.crypto?.randomUUID?.() ?? fallbackRandomId();
|
|
300
299
|
return `${prefix}_${id.replaceAll("-", "").slice(0, 24)}`;
|
|
@@ -302,6 +301,11 @@ function createId(prefix) {
|
|
|
302
301
|
function createDocId() {
|
|
303
302
|
return createId("doc");
|
|
304
303
|
}
|
|
304
|
+
var DOC_ID_SEPARATOR = "\0";
|
|
305
|
+
function deterministicDocId(rootId, path) {
|
|
306
|
+
const digest = createHash("sha256").update(`${rootId}${DOC_ID_SEPARATOR}${path}`).digest("hex");
|
|
307
|
+
return `doc_${digest.slice(0, 16)}`;
|
|
308
|
+
}
|
|
305
309
|
function contentHashForText(value) {
|
|
306
310
|
return `text_${fnv1a(value).toString(16).padStart(16, "0")}`;
|
|
307
311
|
}
|
|
@@ -334,10 +338,10 @@ function parseWikilinkBody(value) {
|
|
|
334
338
|
|
|
335
339
|
// ../core/src/markdown.ts
|
|
336
340
|
var POLLUTING_PATTERNS = [
|
|
337
|
-
{ name: "
|
|
338
|
-
{ name: "
|
|
339
|
-
{ name: "
|
|
340
|
-
{ name: "
|
|
341
|
+
{ name: "inline review addition marker", pattern: /\{\+\+[\s\S]*?\+\+\}/ },
|
|
342
|
+
{ name: "inline review deletion marker", pattern: /\{--[\s\S]*?--\}/ },
|
|
343
|
+
{ name: "inline review substitution marker", pattern: /\{~~[\s\S]*?~~\}/ },
|
|
344
|
+
{ name: "inline review comment marker", pattern: /\{>>[\s\S]*?<<\}/ },
|
|
341
345
|
{ name: "mdocs HTML marker", pattern: /<!--\s*mdocs:/i },
|
|
342
346
|
{ name: "mdocs suggestion marker", pattern: /<!--\s*\/?\s*mdocs-suggestion\b/i },
|
|
343
347
|
{ name: "mdocs directive", pattern: /:{1,3}(?:comment|suggestion|anchor)\b/i }
|
|
@@ -355,7 +359,7 @@ function assertCleanMarkdown(markdown) {
|
|
|
355
359
|
throw new MdocsError(
|
|
356
360
|
"validation_error",
|
|
357
361
|
`Markdown contains collaboration markup: ${names}`,
|
|
358
|
-
"Keep Markdown clean; record comments and suggestions through the
|
|
362
|
+
"Keep Markdown clean; record comments and suggestions through the document authority instead of inline markers."
|
|
359
363
|
);
|
|
360
364
|
}
|
|
361
365
|
}
|
|
@@ -368,10 +372,6 @@ function titleFromMarkdown(path, markdown) {
|
|
|
368
372
|
function getLines(markdown) {
|
|
369
373
|
return markdown.length === 0 ? [] : markdown.split(/\r?\n/);
|
|
370
374
|
}
|
|
371
|
-
function extractLineRange(markdown, range) {
|
|
372
|
-
const lines = getLines(markdown);
|
|
373
|
-
return lines.slice(range.startLine - 1, range.endLine).join("\n");
|
|
374
|
-
}
|
|
375
375
|
function assertLineRangeWithin(markdown, range) {
|
|
376
376
|
if (!Number.isInteger(range.startLine) || !Number.isInteger(range.endLine) || range.startLine < 1 || range.endLine < range.startLine) {
|
|
377
377
|
throw new MdocsError(
|
|
@@ -406,7 +406,7 @@ function replacementLinesForRange(replacement) {
|
|
|
406
406
|
function extractMarkdownImages(markdown, documentPath) {
|
|
407
407
|
const masked = maskCode(markdown);
|
|
408
408
|
const referenceDefinitions = collectReferenceDefinitions(markdown, masked);
|
|
409
|
-
const
|
|
409
|
+
const lineStarts = getLineStarts(markdown);
|
|
410
410
|
const images = [];
|
|
411
411
|
let index = 0;
|
|
412
412
|
while (index < masked.length) {
|
|
@@ -424,7 +424,7 @@ function extractMarkdownImages(markdown, documentPath) {
|
|
|
424
424
|
}
|
|
425
425
|
const alt = markdown.slice(labelOpen + 1, labelClose);
|
|
426
426
|
const afterLabel = labelClose + 1;
|
|
427
|
-
const position = positionAt(
|
|
427
|
+
const position = positionAt(lineStarts, start);
|
|
428
428
|
if (masked[afterLabel] === "(") {
|
|
429
429
|
const destinationClose = findClosingParen(markdown, afterLabel + 1);
|
|
430
430
|
if (destinationClose === -1) {
|
|
@@ -492,7 +492,7 @@ function extractMarkdownLinks(markdown) {
|
|
|
492
492
|
const codeMasked = maskCode(markdown);
|
|
493
493
|
const referenceDefinitions = collectReferenceDefinitions(markdown, codeMasked);
|
|
494
494
|
const masked = maskReferenceDefinitions(markdown, codeMasked);
|
|
495
|
-
const
|
|
495
|
+
const lineStarts = getLineStarts(markdown);
|
|
496
496
|
const links = [];
|
|
497
497
|
let index = 0;
|
|
498
498
|
while (index < masked.length) {
|
|
@@ -509,7 +509,7 @@ function extractMarkdownLinks(markdown) {
|
|
|
509
509
|
}
|
|
510
510
|
const label = markdown.slice(start + 1, labelClose);
|
|
511
511
|
const afterLabel = labelClose + 1;
|
|
512
|
-
const position = positionAt(
|
|
512
|
+
const position = positionAt(lineStarts, start);
|
|
513
513
|
if (masked[afterLabel] === "(") {
|
|
514
514
|
const destinationClose = findClosingParen(markdown, afterLabel + 1);
|
|
515
515
|
if (destinationClose === -1) {
|
|
@@ -565,7 +565,7 @@ function extractMarkdownLinks(markdown) {
|
|
|
565
565
|
}
|
|
566
566
|
index = labelClose + 1;
|
|
567
567
|
}
|
|
568
|
-
extractWikilinks(markdown, masked,
|
|
568
|
+
extractWikilinks(markdown, masked, lineStarts).forEach((link2) => links.push(link2));
|
|
569
569
|
return links.sort((left, right) => left.line - right.line || left.column - right.column);
|
|
570
570
|
}
|
|
571
571
|
function toImageReference(image2) {
|
|
@@ -592,7 +592,7 @@ function toLinkReference(link2) {
|
|
|
592
592
|
syntax: link2.syntax
|
|
593
593
|
};
|
|
594
594
|
}
|
|
595
|
-
function extractWikilinks(markdown, masked,
|
|
595
|
+
function extractWikilinks(markdown, masked, lineStarts) {
|
|
596
596
|
const links = [];
|
|
597
597
|
let index = 0;
|
|
598
598
|
while (index < masked.length) {
|
|
@@ -609,7 +609,7 @@ function extractWikilinks(markdown, masked, lineStarts2) {
|
|
|
609
609
|
}
|
|
610
610
|
const parsed = parseWikilinkBody(markdown.slice(start + 2, close2));
|
|
611
611
|
if (parsed) {
|
|
612
|
-
const position = positionAt(
|
|
612
|
+
const position = positionAt(lineStarts, start);
|
|
613
613
|
links.push({
|
|
614
614
|
label: parsed.label ?? parsed.target,
|
|
615
615
|
target: parsed.target,
|
|
@@ -889,12 +889,12 @@ function getLineStarts(value) {
|
|
|
889
889
|
}
|
|
890
890
|
return starts;
|
|
891
891
|
}
|
|
892
|
-
function positionAt(
|
|
892
|
+
function positionAt(lineStarts, index) {
|
|
893
893
|
let low = 0;
|
|
894
|
-
let high =
|
|
894
|
+
let high = lineStarts.length - 1;
|
|
895
895
|
while (low <= high) {
|
|
896
896
|
const middle = Math.floor((low + high) / 2);
|
|
897
|
-
const start =
|
|
897
|
+
const start = lineStarts[middle] ?? 0;
|
|
898
898
|
if (start <= index) {
|
|
899
899
|
low = middle + 1;
|
|
900
900
|
} else {
|
|
@@ -904,532 +904,9 @@ function positionAt(lineStarts2, index) {
|
|
|
904
904
|
const lineIndex = Math.max(0, high);
|
|
905
905
|
return {
|
|
906
906
|
line: lineIndex + 1,
|
|
907
|
-
column: index - (
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
// ../core/src/anchors.ts
|
|
912
|
-
function selectorFromRange(markdown, range) {
|
|
913
|
-
const lines = getLines(markdown);
|
|
914
|
-
const selected = lines.slice(range.startLine - 1, range.endLine).join("\n");
|
|
915
|
-
const prefix = lines.slice(Math.max(0, range.startLine - 4), range.startLine - 1).join("\n");
|
|
916
|
-
const suffix = lines.slice(range.endLine, range.endLine + 3).join("\n");
|
|
917
|
-
return { quote: selected, prefix, suffix };
|
|
918
|
-
}
|
|
919
|
-
function remapAnchor(markdown, anchor) {
|
|
920
|
-
const lines = getLines(markdown);
|
|
921
|
-
const quoteLines = anchor.selector.quote.split(/\r?\n/);
|
|
922
|
-
const candidates = findQuoteCandidates(lines, quoteLines);
|
|
923
|
-
const best = bestCandidate(lines, candidates, anchor.selector);
|
|
924
|
-
if (!best) {
|
|
925
|
-
return {
|
|
926
|
-
...anchor,
|
|
927
|
-
status: "needs_review",
|
|
928
|
-
confidence: 0,
|
|
929
|
-
updatedAt: nowIso()
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
const confidence = scoreContext(lines, best.startLine, best.endLine, anchor.selector);
|
|
933
|
-
return {
|
|
934
|
-
...anchor,
|
|
935
|
-
status: confidence >= 0.65 ? "mapped" : "needs_review",
|
|
936
|
-
confidence,
|
|
937
|
-
range: best,
|
|
938
|
-
updatedAt: nowIso()
|
|
939
|
-
};
|
|
940
|
-
}
|
|
941
|
-
function remapAnchors(markdown, anchors) {
|
|
942
|
-
return anchors.map((anchor) => remapAnchor(markdown, anchor));
|
|
943
|
-
}
|
|
944
|
-
function findQuoteCandidates(lines, quoteLines) {
|
|
945
|
-
if (quoteLines.length === 0) return [];
|
|
946
|
-
const ranges = [];
|
|
947
|
-
const quoteText = quoteLines.join("\n");
|
|
948
|
-
for (let index = 0; index <= lines.length - quoteLines.length; index += 1) {
|
|
949
|
-
const candidate = lines.slice(index, index + quoteLines.length);
|
|
950
|
-
if (candidate.join("\n") === quoteText) ranges.push({ startLine: index + 1, endLine: index + quoteLines.length });
|
|
951
|
-
}
|
|
952
|
-
return ranges;
|
|
953
|
-
}
|
|
954
|
-
function bestCandidate(lines, candidates, selector) {
|
|
955
|
-
if (candidates.length <= 1) return candidates[0];
|
|
956
|
-
const scored = candidates.map((range) => ({ range, score: scoreContext(lines, range.startLine, range.endLine, selector) })).sort((left, right) => right.score - left.score);
|
|
957
|
-
const best = scored[0];
|
|
958
|
-
if (!best) return void 0;
|
|
959
|
-
const tied = scored.filter((candidate) => candidate.score === best.score);
|
|
960
|
-
return tied.length === 1 ? best.range : void 0;
|
|
961
|
-
}
|
|
962
|
-
function scoreContext(lines, startLine, endLine, selector) {
|
|
963
|
-
let score = 0.7;
|
|
964
|
-
const expectedPrefix = contextPrefix(selector.prefix);
|
|
965
|
-
if (expectedPrefix) {
|
|
966
|
-
const actualPrefix = contextPrefix(lines.slice(Math.max(0, startLine - 4), startLine - 1).join("\n"));
|
|
967
|
-
if (expectedPrefix && actualPrefix.endsWith(expectedPrefix)) score += 0.15;
|
|
968
|
-
}
|
|
969
|
-
const expectedSuffix = contextSuffix(selector.suffix);
|
|
970
|
-
if (expectedSuffix) {
|
|
971
|
-
const actualSuffix = contextSuffix(lines.slice(endLine, endLine + 3).join("\n"));
|
|
972
|
-
if (expectedSuffix && actualSuffix.startsWith(expectedSuffix)) score += 0.15;
|
|
973
|
-
}
|
|
974
|
-
return Math.min(1, score);
|
|
975
|
-
}
|
|
976
|
-
function contextPrefix(value) {
|
|
977
|
-
return value?.split(/\r?\n/).filter(Boolean).slice(-3).join("\n") ?? "";
|
|
978
|
-
}
|
|
979
|
-
function contextSuffix(value) {
|
|
980
|
-
return value?.split(/\r?\n/).filter(Boolean).slice(0, 3).join("\n") ?? "";
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// ../core/src/review-markdown.ts
|
|
984
|
-
var START_MARKER = "<!--mdocs-suggestion";
|
|
985
|
-
var END_MARKER = "<!--/mdocs-suggestion-->";
|
|
986
|
-
function parseReviewMarkdown(reviewMarkdown) {
|
|
987
|
-
const segments = [];
|
|
988
|
-
const suggestions = [];
|
|
989
|
-
let cursor = 0;
|
|
990
|
-
let canonicalOffset = 0;
|
|
991
|
-
while (cursor < reviewMarkdown.length) {
|
|
992
|
-
const markerStart = reviewMarkdown.indexOf(START_MARKER, cursor);
|
|
993
|
-
if (markerStart === -1) {
|
|
994
|
-
appendTextSegment(reviewMarkdown, segments, cursor, reviewMarkdown.length, canonicalOffset);
|
|
995
|
-
break;
|
|
996
|
-
}
|
|
997
|
-
appendTextSegment(reviewMarkdown, segments, cursor, markerStart, canonicalOffset);
|
|
998
|
-
canonicalOffset += markerStart - cursor;
|
|
999
|
-
const startClose = reviewMarkdown.indexOf("-->", markerStart);
|
|
1000
|
-
if (startClose === -1) throw invalidReviewMarkdown("Unclosed mdocs suggestion marker.");
|
|
1001
|
-
const attrs2 = parseMarkerAttributes(reviewMarkdown.slice(markerStart + START_MARKER.length, startClose));
|
|
1002
|
-
if (!attrs2.id) throw invalidReviewMarkdown("Suggestion marker is missing an id.");
|
|
1003
|
-
if (attrs2.kind !== "insert" && attrs2.kind !== "delete" && attrs2.kind !== "replace") {
|
|
1004
|
-
throw invalidReviewMarkdown(`Suggestion ${attrs2.id} has an invalid kind.`);
|
|
1005
|
-
}
|
|
1006
|
-
const bodyStart = startClose + 3;
|
|
1007
|
-
const markerEnd = reviewMarkdown.indexOf(END_MARKER, bodyStart);
|
|
1008
|
-
if (markerEnd === -1) throw invalidReviewMarkdown(`Suggestion ${attrs2.id} is missing its closing marker.`);
|
|
1009
|
-
const body = reviewMarkdown.slice(bodyStart, markerEnd);
|
|
1010
|
-
if (body.includes(START_MARKER)) throw invalidReviewMarkdown(`Suggestion ${attrs2.id} contains a nested suggestion.`);
|
|
1011
|
-
const parsedBody = parseSuggestionBody(attrs2.kind, body, attrs2.id);
|
|
1012
|
-
const token = {
|
|
1013
|
-
id: attrs2.id,
|
|
1014
|
-
kind: attrs2.kind,
|
|
1015
|
-
form: parsedBody.before.includes("\n") || parsedBody.after.includes("\n") ? "block" : "inline",
|
|
1016
|
-
author: actorFromAttributes(attrs2),
|
|
1017
|
-
message: attrs2.message,
|
|
1018
|
-
createdAt: attrs2.createdAt,
|
|
1019
|
-
updatedAt: attrs2.updatedAt,
|
|
1020
|
-
before: parsedBody.before,
|
|
1021
|
-
after: parsedBody.after,
|
|
1022
|
-
markerStart,
|
|
1023
|
-
markerEnd: markerEnd + END_MARKER.length,
|
|
1024
|
-
canonicalStart: canonicalOffset,
|
|
1025
|
-
canonicalEnd: canonicalOffset + parsedBody.before.length,
|
|
1026
|
-
raw: reviewMarkdown.slice(markerStart, markerEnd + END_MARKER.length)
|
|
1027
|
-
};
|
|
1028
|
-
segments.push({ type: "suggestion", token });
|
|
1029
|
-
suggestions.push(token);
|
|
1030
|
-
canonicalOffset += token.before.length;
|
|
1031
|
-
cursor = token.markerEnd;
|
|
1032
|
-
}
|
|
1033
|
-
return {
|
|
1034
|
-
reviewMarkdown,
|
|
1035
|
-
canonicalMarkdown: canonicalFromSegments(segments, statusLookup(void 0)),
|
|
1036
|
-
segments,
|
|
1037
|
-
suggestions
|
|
1038
|
-
};
|
|
1039
|
-
}
|
|
1040
|
-
function projectCanonicalMarkdown(reviewMarkdown, sidecarStatuses) {
|
|
1041
|
-
return canonicalFromSegments(parseReviewMarkdown(reviewMarkdown).segments, statusLookup(sidecarStatuses));
|
|
1042
|
-
}
|
|
1043
|
-
function deriveSidecarFromReviewMarkdown(sidecar, reviewMarkdown, options) {
|
|
1044
|
-
const parsed = parseReviewMarkdown(reviewMarkdown);
|
|
1045
|
-
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1046
|
-
const existingSuggestions = new Map(sidecar.suggestions.map((suggestion) => [suggestion.id, suggestion]));
|
|
1047
|
-
const existingAnchors = new Map(sidecar.anchors.map((anchor) => [anchor.id, anchor]));
|
|
1048
|
-
const anchors = sidecar.anchors.filter((anchor) => anchor.kind !== "suggestion");
|
|
1049
|
-
const suggestions = [];
|
|
1050
|
-
const base = { contentHash: contentHashForText(parsed.canonicalMarkdown), ...options.baseHead ? { head: options.baseHead } : {} };
|
|
1051
|
-
for (const token of parsed.suggestions) {
|
|
1052
|
-
const existing = existingSuggestions.get(token.id);
|
|
1053
|
-
const anchorId = existing?.anchorId ?? stableAnchorIdForSuggestion(token.id);
|
|
1054
|
-
const range = lineRangeForCanonicalSpan(parsed.canonicalMarkdown, token.canonicalStart, token.canonicalEnd);
|
|
1055
|
-
const selector = selectorForCanonicalSpan(parsed.canonicalMarkdown, token.canonicalStart, token.canonicalEnd);
|
|
1056
|
-
const anchor = {
|
|
1057
|
-
...existingAnchors.get(anchorId) ?? {},
|
|
1058
|
-
id: anchorId,
|
|
1059
|
-
kind: "suggestion",
|
|
1060
|
-
status: "mapped",
|
|
1061
|
-
selector,
|
|
1062
|
-
range,
|
|
1063
|
-
confidence: 1,
|
|
1064
|
-
updatedAt: now
|
|
1065
|
-
};
|
|
1066
|
-
const author = token.author ?? existing?.author ?? options.author;
|
|
1067
|
-
const message = token.message ?? existing?.message ?? "Suggested edit";
|
|
1068
|
-
const suggestion = {
|
|
1069
|
-
...existing ?? {},
|
|
1070
|
-
id: token.id,
|
|
1071
|
-
anchorId,
|
|
1072
|
-
status: "open",
|
|
1073
|
-
kind: token.kind,
|
|
1074
|
-
author,
|
|
1075
|
-
message,
|
|
1076
|
-
patch: {
|
|
1077
|
-
type: "replace_lines",
|
|
1078
|
-
range,
|
|
1079
|
-
before: token.before,
|
|
1080
|
-
after: token.after
|
|
1081
|
-
},
|
|
1082
|
-
base: existing?.base ?? base,
|
|
1083
|
-
diff: {
|
|
1084
|
-
before: token.before,
|
|
1085
|
-
after: token.after,
|
|
1086
|
-
summary: diffSummary(token.before, token.after)
|
|
1087
|
-
},
|
|
1088
|
-
projectionStatus: "clean",
|
|
1089
|
-
reviewRange: { start: token.markerStart, end: token.markerEnd },
|
|
1090
|
-
createdAt: token.createdAt ?? existing?.createdAt ?? now,
|
|
1091
|
-
updatedAt: token.updatedAt ?? now
|
|
1092
|
-
};
|
|
1093
|
-
anchors.push(anchor);
|
|
1094
|
-
suggestions.push(suggestion);
|
|
1095
|
-
}
|
|
1096
|
-
return {
|
|
1097
|
-
...sidecar,
|
|
1098
|
-
reviewMarkdown,
|
|
1099
|
-
anchors,
|
|
1100
|
-
suggestions,
|
|
1101
|
-
updatedAt: now
|
|
1102
|
-
};
|
|
1103
|
-
}
|
|
1104
|
-
function createSuggestionMarkup(reviewMarkdown, patch, metadata) {
|
|
1105
|
-
const parsed = parseReviewMarkdown(reviewMarkdown);
|
|
1106
|
-
assertNoDuplicateSuggestionId(parsed, metadata.suggestionId);
|
|
1107
|
-
const [contentStart, contentEnd] = lineRangeOffsets(parsed.canonicalMarkdown, patch.range);
|
|
1108
|
-
const selected = parsed.canonicalMarkdown.slice(contentStart, contentEnd);
|
|
1109
|
-
if (selected !== patch.before) {
|
|
1110
|
-
throw new MdocsError(
|
|
1111
|
-
"conflict",
|
|
1112
|
-
"Suggestion target does not match the review document projection.",
|
|
1113
|
-
"Re-read the document and create the suggestion against the current canonical text."
|
|
1114
|
-
);
|
|
1115
|
-
}
|
|
1116
|
-
const [canonicalStart, canonicalEnd] = lineRangeReplacementOffsets(parsed.canonicalMarkdown, patch.range);
|
|
1117
|
-
if (selectionTouchesSuggestion(parsed.suggestions, canonicalStart, canonicalEnd)) {
|
|
1118
|
-
throw new MdocsError(
|
|
1119
|
-
"conflict",
|
|
1120
|
-
"Suggestion overlaps an unresolved suggestion.",
|
|
1121
|
-
"Accept or reject the existing suggestion before creating another edit over the same text."
|
|
1122
|
-
);
|
|
1123
|
-
}
|
|
1124
|
-
const rawStart = rawOffsetForCanonicalOffset(parsed.segments, canonicalStart);
|
|
1125
|
-
const rawEnd = rawOffsetForCanonicalOffset(parsed.segments, canonicalEnd);
|
|
1126
|
-
const before = parsed.canonicalMarkdown.slice(canonicalStart, canonicalEnd);
|
|
1127
|
-
const after = replacementTextForSpan(parsed.canonicalMarkdown, patch, canonicalStart, canonicalEnd);
|
|
1128
|
-
const marker = formatSuggestionMarker(metadata, kindForText(before, after), before, after);
|
|
1129
|
-
const nextReviewMarkdown = `${reviewMarkdown.slice(0, rawStart)}${marker}${reviewMarkdown.slice(rawEnd)}`;
|
|
1130
|
-
const token = parseReviewMarkdown(nextReviewMarkdown).suggestions.find((candidate) => candidate.id === metadata.suggestionId);
|
|
1131
|
-
if (!token) throw invalidReviewMarkdown(`Failed to create suggestion ${metadata.suggestionId}.`);
|
|
1132
|
-
return { reviewMarkdown: nextReviewMarkdown, token };
|
|
1133
|
-
}
|
|
1134
|
-
function projectReviewMarkdown(canonicalMarkdown, suggestions) {
|
|
1135
|
-
let reviewMarkdown = canonicalMarkdown;
|
|
1136
|
-
const open = suggestions.filter((suggestion) => suggestion.status === "open" && suggestion.patch?.type === "replace_lines").slice().sort((left, right) => left.patch.range.startLine - right.patch.range.startLine);
|
|
1137
|
-
for (const suggestion of open) {
|
|
1138
|
-
try {
|
|
1139
|
-
reviewMarkdown = createSuggestionMarkup(reviewMarkdown, suggestion.patch, {
|
|
1140
|
-
suggestionId: suggestion.id,
|
|
1141
|
-
author: suggestion.author,
|
|
1142
|
-
message: suggestion.message,
|
|
1143
|
-
createdAt: suggestion.createdAt,
|
|
1144
|
-
updatedAt: suggestion.updatedAt
|
|
1145
|
-
}).reviewMarkdown;
|
|
1146
|
-
} catch {
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
return reviewMarkdown;
|
|
1150
|
-
}
|
|
1151
|
-
function resolveSuggestionMarkup(reviewMarkdown, suggestionId, resolution) {
|
|
1152
|
-
const parsed = parseReviewMarkdown(reviewMarkdown);
|
|
1153
|
-
const token = parsed.suggestions.find((candidate) => candidate.id === suggestionId);
|
|
1154
|
-
if (!token) {
|
|
1155
|
-
throw new MdocsError("not_found", `Unknown suggestion: ${suggestionId}`, "Refresh review state and try again.");
|
|
1156
|
-
}
|
|
1157
|
-
const resolved = resolutionText(token, resolution);
|
|
1158
|
-
return `${reviewMarkdown.slice(0, token.markerStart)}${resolved}${reviewMarkdown.slice(token.markerEnd)}`;
|
|
1159
|
-
}
|
|
1160
|
-
function escapeReviewSuggestionText(value) {
|
|
1161
|
-
return value.replaceAll("\\", "\\\\").replaceAll("{++", "\\{++").replaceAll("++}", "\\++}").replaceAll("{--", "\\{--").replaceAll("--}", "\\--}").replaceAll("{~~", "\\{~~").replaceAll("~>", "\\~>").replaceAll("~~}", "\\~~}");
|
|
1162
|
-
}
|
|
1163
|
-
function lineRangeForCanonicalSpan(markdown, start, end) {
|
|
1164
|
-
assertCanonicalSpan(markdown, start, end);
|
|
1165
|
-
return lineRangeForOffsets(markdown, start, end);
|
|
1166
|
-
}
|
|
1167
|
-
function selectorForCanonicalSpan(markdown, start, end) {
|
|
1168
|
-
assertCanonicalSpan(markdown, start, end);
|
|
1169
|
-
const context = 160;
|
|
1170
|
-
const prefix = markdown.slice(Math.max(0, start - context), start);
|
|
1171
|
-
const suffix = markdown.slice(end, Math.min(markdown.length, end + context));
|
|
1172
|
-
return {
|
|
1173
|
-
quote: markdown.slice(start, end),
|
|
1174
|
-
...prefix ? { prefix } : {},
|
|
1175
|
-
...suffix ? { suffix } : {}
|
|
1176
|
-
};
|
|
1177
|
-
}
|
|
1178
|
-
function appendTextSegment(source, segments, start, end, canonicalStart) {
|
|
1179
|
-
if (end <= start) return;
|
|
1180
|
-
segments.push({
|
|
1181
|
-
type: "text",
|
|
1182
|
-
text: source.slice(start, end),
|
|
1183
|
-
start,
|
|
1184
|
-
end,
|
|
1185
|
-
canonicalStart,
|
|
1186
|
-
canonicalEnd: canonicalStart + (end - start)
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
|
-
function canonicalFromSegments(segments, statuses) {
|
|
1190
|
-
return segments.map((segment) => {
|
|
1191
|
-
if (segment.type === "text") return segment.text;
|
|
1192
|
-
const status = statuses(segment.token.id);
|
|
1193
|
-
if (status === "accepted") return acceptedText(segment.token);
|
|
1194
|
-
return originalText(segment.token);
|
|
1195
|
-
}).join("");
|
|
1196
|
-
}
|
|
1197
|
-
function parseMarkerAttributes(raw) {
|
|
1198
|
-
const attrs2 = {};
|
|
1199
|
-
const pattern = /\s+([a-zA-Z][\w:-]*)="([^"]*)"/g;
|
|
1200
|
-
let match2;
|
|
1201
|
-
while ((match2 = pattern.exec(raw)) !== null) {
|
|
1202
|
-
const key = match2[1];
|
|
1203
|
-
if (key === "id" || key === "kind" || key === "authorId" || key === "authorKind" || key === "authorName" || key === "authorEmail" || key === "message" || key === "createdAt" || key === "updatedAt") {
|
|
1204
|
-
attrs2[key] = decodeAttribute(match2[2] ?? "");
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
return attrs2;
|
|
1208
|
-
}
|
|
1209
|
-
function actorFromAttributes(attrs2) {
|
|
1210
|
-
if (!attrs2.authorId || !attrs2.authorName) return void 0;
|
|
1211
|
-
const kind = attrs2.authorKind === "agent" || attrs2.authorKind === "system" || attrs2.authorKind === "human" ? attrs2.authorKind : "human";
|
|
1212
|
-
return {
|
|
1213
|
-
id: attrs2.authorId,
|
|
1214
|
-
kind,
|
|
1215
|
-
name: attrs2.authorName,
|
|
1216
|
-
...attrs2.authorEmail ? { email: attrs2.authorEmail } : {}
|
|
1217
|
-
};
|
|
1218
|
-
}
|
|
1219
|
-
function parseSuggestionBody(kind, body, id) {
|
|
1220
|
-
if (kind === "insert") {
|
|
1221
|
-
return { before: "", after: parseWrappedBody(body, "{++", "++}", id) };
|
|
1222
|
-
}
|
|
1223
|
-
if (kind === "delete") {
|
|
1224
|
-
return { before: parseWrappedBody(body, "{--", "--}", id), after: "" };
|
|
1225
|
-
}
|
|
1226
|
-
if (!body.startsWith("{~~") || !body.endsWith("~~}")) {
|
|
1227
|
-
throw invalidReviewMarkdown(`Replacement suggestion ${id} has invalid CriticMarkup payload.`);
|
|
1228
|
-
}
|
|
1229
|
-
const inner = body.slice(3, -3);
|
|
1230
|
-
const separator = findUnescaped2(inner, "~>");
|
|
1231
|
-
if (separator === -1) throw invalidReviewMarkdown(`Replacement suggestion ${id} is missing a separator.`);
|
|
1232
|
-
return {
|
|
1233
|
-
before: unescapeReviewSuggestionText(inner.slice(0, separator)),
|
|
1234
|
-
after: unescapeReviewSuggestionText(inner.slice(separator + 2))
|
|
907
|
+
column: index - (lineStarts[lineIndex] ?? 0) + 1
|
|
1235
908
|
};
|
|
1236
909
|
}
|
|
1237
|
-
function parseWrappedBody(body, open, close2, id) {
|
|
1238
|
-
if (!body.startsWith(open) || !body.endsWith(close2) || isEscapedAt(body, body.length - close2.length)) {
|
|
1239
|
-
throw invalidReviewMarkdown(`Suggestion ${id} has invalid CriticMarkup payload.`);
|
|
1240
|
-
}
|
|
1241
|
-
return unescapeReviewSuggestionText(body.slice(open.length, -close2.length));
|
|
1242
|
-
}
|
|
1243
|
-
function formatSuggestionMarker(metadata, kind, before, after) {
|
|
1244
|
-
const attrs2 = [
|
|
1245
|
-
["id", metadata.suggestionId],
|
|
1246
|
-
["kind", kind]
|
|
1247
|
-
];
|
|
1248
|
-
if (metadata.author) {
|
|
1249
|
-
attrs2.push(["authorId", metadata.author.id]);
|
|
1250
|
-
attrs2.push(["authorKind", metadata.author.kind]);
|
|
1251
|
-
attrs2.push(["authorName", metadata.author.name]);
|
|
1252
|
-
if (metadata.author.email) attrs2.push(["authorEmail", metadata.author.email]);
|
|
1253
|
-
}
|
|
1254
|
-
if (metadata.message) attrs2.push(["message", metadata.message]);
|
|
1255
|
-
if (metadata.createdAt) attrs2.push(["createdAt", metadata.createdAt]);
|
|
1256
|
-
if (metadata.updatedAt) attrs2.push(["updatedAt", metadata.updatedAt]);
|
|
1257
|
-
const attrText = attrs2.map(([key, value]) => `${key}="${encodeAttribute(value)}"`).join(" ");
|
|
1258
|
-
const body = kind === "insert" ? `{++${escapeReviewSuggestionText(after)}++}` : kind === "delete" ? `{--${escapeReviewSuggestionText(before)}--}` : `{~~${escapeReviewSuggestionText(before)}~>${escapeReviewSuggestionText(after)}~~}`;
|
|
1259
|
-
return `${START_MARKER} ${attrText} -->${body}${END_MARKER}`;
|
|
1260
|
-
}
|
|
1261
|
-
function stableAnchorIdForSuggestion(suggestionId) {
|
|
1262
|
-
return `anchor_${suggestionId.replace(/[^a-zA-Z0-9_:-]/g, "_")}`;
|
|
1263
|
-
}
|
|
1264
|
-
function diffSummary(before, after) {
|
|
1265
|
-
if (!before && after) return "Inserted text";
|
|
1266
|
-
if (before && !after) return "Deleted text";
|
|
1267
|
-
return "Replaced text";
|
|
1268
|
-
}
|
|
1269
|
-
function unescapeReviewSuggestionText(value) {
|
|
1270
|
-
let result = "";
|
|
1271
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
1272
|
-
if (value[index] !== "\\") {
|
|
1273
|
-
result += value[index];
|
|
1274
|
-
continue;
|
|
1275
|
-
}
|
|
1276
|
-
const next = value[index + 1];
|
|
1277
|
-
if (next === void 0) {
|
|
1278
|
-
result += "\\";
|
|
1279
|
-
continue;
|
|
1280
|
-
}
|
|
1281
|
-
result += next;
|
|
1282
|
-
index += 1;
|
|
1283
|
-
}
|
|
1284
|
-
return result;
|
|
1285
|
-
}
|
|
1286
|
-
function findUnescaped2(value, needle, from = 0) {
|
|
1287
|
-
let index = value.indexOf(needle, from);
|
|
1288
|
-
while (index !== -1) {
|
|
1289
|
-
if (!isEscapedAt(value, index)) return index;
|
|
1290
|
-
index = value.indexOf(needle, index + needle.length);
|
|
1291
|
-
}
|
|
1292
|
-
return -1;
|
|
1293
|
-
}
|
|
1294
|
-
function isEscapedAt(value, index) {
|
|
1295
|
-
let slashes = 0;
|
|
1296
|
-
for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1) slashes += 1;
|
|
1297
|
-
return slashes % 2 === 1;
|
|
1298
|
-
}
|
|
1299
|
-
function statusLookup(statuses) {
|
|
1300
|
-
if (!statuses) return () => "open";
|
|
1301
|
-
if (isStatusMap(statuses)) return (suggestionId) => statuses.get(suggestionId) ?? "open";
|
|
1302
|
-
if (Array.isArray(statuses)) {
|
|
1303
|
-
const map = /* @__PURE__ */ new Map();
|
|
1304
|
-
for (const item of statuses) {
|
|
1305
|
-
const id = item.id ?? item.suggestionId;
|
|
1306
|
-
if (id && item.status) map.set(id, item.status);
|
|
1307
|
-
}
|
|
1308
|
-
return (suggestionId) => map.get(suggestionId) ?? "open";
|
|
1309
|
-
}
|
|
1310
|
-
const record = statuses;
|
|
1311
|
-
return (suggestionId) => record[suggestionId] ?? "open";
|
|
1312
|
-
}
|
|
1313
|
-
function isStatusMap(value) {
|
|
1314
|
-
return typeof value.get === "function";
|
|
1315
|
-
}
|
|
1316
|
-
function acceptedText(token) {
|
|
1317
|
-
return token.kind === "delete" ? "" : token.after;
|
|
1318
|
-
}
|
|
1319
|
-
function originalText(token) {
|
|
1320
|
-
return token.kind === "insert" ? "" : token.before;
|
|
1321
|
-
}
|
|
1322
|
-
function resolutionText(token, resolution) {
|
|
1323
|
-
return resolution === "accept" ? acceptedText(token) : originalText(token);
|
|
1324
|
-
}
|
|
1325
|
-
function kindForText(before, after) {
|
|
1326
|
-
if (before.length === 0) return "insert";
|
|
1327
|
-
if (after.length === 0) return "delete";
|
|
1328
|
-
return "replace";
|
|
1329
|
-
}
|
|
1330
|
-
function assertNoDuplicateSuggestionId(parsed, suggestionId) {
|
|
1331
|
-
if (parsed.suggestions.some((suggestion) => suggestion.id === suggestionId)) {
|
|
1332
|
-
throw new MdocsError("validation_error", `Duplicate suggestion id: ${suggestionId}`, "Suggestion ids must be stable and unique per document.");
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
function assertCanonicalSpan(markdown, start, end) {
|
|
1336
|
-
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > markdown.length) {
|
|
1337
|
-
throw new MdocsError("invalid_range", `Invalid canonical span ${start}:${end}.`, "Re-read the document and try again.");
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
function selectionTouchesSuggestion(tokens, start, end) {
|
|
1341
|
-
return tokens.some((token) => {
|
|
1342
|
-
if (token.canonicalStart === token.canonicalEnd) return start <= token.canonicalStart && end >= token.canonicalEnd;
|
|
1343
|
-
return start < token.canonicalEnd && end > token.canonicalStart;
|
|
1344
|
-
});
|
|
1345
|
-
}
|
|
1346
|
-
function rawOffsetForCanonicalOffset(segments, offset) {
|
|
1347
|
-
for (const segment of segments) {
|
|
1348
|
-
if (segment.type === "text") {
|
|
1349
|
-
if (offset >= segment.canonicalStart && offset <= segment.canonicalEnd) {
|
|
1350
|
-
return segment.start + (offset - segment.canonicalStart);
|
|
1351
|
-
}
|
|
1352
|
-
continue;
|
|
1353
|
-
}
|
|
1354
|
-
const token = segment.token;
|
|
1355
|
-
if (offset > token.canonicalStart && offset < token.canonicalEnd) {
|
|
1356
|
-
throw new MdocsError(
|
|
1357
|
-
"conflict",
|
|
1358
|
-
"Selection falls inside an unresolved suggestion.",
|
|
1359
|
-
"Accept or reject the existing suggestion before editing inside it."
|
|
1360
|
-
);
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
if (offset === segmentsEndCanonicalOffset(segments)) return segmentsEndRawOffset(segments);
|
|
1364
|
-
throw new MdocsError("invalid_range", "Could not map canonical offset into review Markdown.", "Re-read the document and try again.");
|
|
1365
|
-
}
|
|
1366
|
-
function segmentsEndCanonicalOffset(segments) {
|
|
1367
|
-
const last = segments[segments.length - 1];
|
|
1368
|
-
if (!last) return 0;
|
|
1369
|
-
return last.type === "text" ? last.canonicalEnd : last.token.canonicalEnd;
|
|
1370
|
-
}
|
|
1371
|
-
function segmentsEndRawOffset(segments) {
|
|
1372
|
-
const last = segments[segments.length - 1];
|
|
1373
|
-
if (!last) return 0;
|
|
1374
|
-
return last.type === "text" ? last.end : last.token.markerEnd;
|
|
1375
|
-
}
|
|
1376
|
-
function lineRangeOffsets(markdown, range) {
|
|
1377
|
-
const lines = getLines(markdown);
|
|
1378
|
-
if (range.startLine < 1 || range.endLine < range.startLine || range.endLine > lines.length) {
|
|
1379
|
-
throw new MdocsError("invalid_range", `Invalid line range ${range.startLine}:${range.endLine}.`, "Re-read the document and try again.");
|
|
1380
|
-
}
|
|
1381
|
-
let start = 0;
|
|
1382
|
-
for (let index = 0; index < range.startLine - 1; index += 1) start += lines[index].length + 1;
|
|
1383
|
-
let end = start;
|
|
1384
|
-
for (let index = range.startLine - 1; index < range.endLine; index += 1) {
|
|
1385
|
-
if (index > range.startLine - 1) end += 1;
|
|
1386
|
-
end += lines[index].length;
|
|
1387
|
-
}
|
|
1388
|
-
return [start, end];
|
|
1389
|
-
}
|
|
1390
|
-
function lineRangeReplacementOffsets(markdown, range) {
|
|
1391
|
-
const [start, contentEnd] = lineRangeOffsets(markdown, range);
|
|
1392
|
-
if (contentEnd < markdown.length && markdown[contentEnd] === "\n") return [start, contentEnd + 1];
|
|
1393
|
-
return [start, contentEnd];
|
|
1394
|
-
}
|
|
1395
|
-
function replacementTextForSpan(markdown, patch, start, end) {
|
|
1396
|
-
const applied = replaceLineRange(markdown, patch.range, patch.after);
|
|
1397
|
-
const suffixLength = markdown.length - end;
|
|
1398
|
-
const replacementEnd = applied.length - suffixLength;
|
|
1399
|
-
if (replacementEnd < start) return "";
|
|
1400
|
-
return applied.slice(start, replacementEnd);
|
|
1401
|
-
}
|
|
1402
|
-
function lineRangeForOffsets(markdown, start, end) {
|
|
1403
|
-
const starts = lineStarts(markdown);
|
|
1404
|
-
return {
|
|
1405
|
-
startLine: lineNumberAt(starts, start),
|
|
1406
|
-
endLine: lineNumberAt(starts, Math.max(start, end - 1))
|
|
1407
|
-
};
|
|
1408
|
-
}
|
|
1409
|
-
function lineStarts(markdown) {
|
|
1410
|
-
const starts = [0];
|
|
1411
|
-
for (let index = 0; index < markdown.length; index += 1) {
|
|
1412
|
-
if (markdown[index] === "\n") starts.push(index + 1);
|
|
1413
|
-
}
|
|
1414
|
-
return starts;
|
|
1415
|
-
}
|
|
1416
|
-
function lineNumberAt(starts, offset) {
|
|
1417
|
-
let line = 1;
|
|
1418
|
-
for (let index = 0; index < starts.length; index += 1) {
|
|
1419
|
-
if (starts[index] <= offset) line = index + 1;
|
|
1420
|
-
else break;
|
|
1421
|
-
}
|
|
1422
|
-
return line;
|
|
1423
|
-
}
|
|
1424
|
-
function encodeAttribute(value) {
|
|
1425
|
-
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
1426
|
-
}
|
|
1427
|
-
function decodeAttribute(value) {
|
|
1428
|
-
return value.replaceAll(""", '"').replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&");
|
|
1429
|
-
}
|
|
1430
|
-
function invalidReviewMarkdown(message) {
|
|
1431
|
-
return new MdocsError("validation_error", `Invalid review Markdown: ${message}`, "Repair or remove the malformed suggestion markup.");
|
|
1432
|
-
}
|
|
1433
910
|
|
|
1434
911
|
// ../core/src/graph.ts
|
|
1435
912
|
function resolveMarkdownLinks(links, currentDocument, documents) {
|
|
@@ -1609,23 +1086,16 @@ function safeDecode(value) {
|
|
|
1609
1086
|
}
|
|
1610
1087
|
}
|
|
1611
1088
|
|
|
1612
|
-
// ../core/src/
|
|
1613
|
-
|
|
1614
|
-
return {
|
|
1615
|
-
id: "actor_local",
|
|
1616
|
-
kind: "human",
|
|
1617
|
-
name
|
|
1618
|
-
};
|
|
1619
|
-
}
|
|
1089
|
+
// ../core/src/workspace-store.ts
|
|
1090
|
+
var cacheLookupSignatures = /* @__PURE__ */ new WeakMap();
|
|
1620
1091
|
async function initWorkspace(io, workspaceId = createId("workspace")) {
|
|
1621
1092
|
await io.mkdir(".mdocs");
|
|
1622
|
-
await io.mkdir(SIDECAR_DIR);
|
|
1623
1093
|
await io.mkdir(CHECKPOINT_DIR);
|
|
1624
1094
|
const existing = await readManifestIfPresent(io);
|
|
1625
1095
|
if (existing) return existing;
|
|
1626
1096
|
const now = nowIso();
|
|
1627
1097
|
const manifest = {
|
|
1628
|
-
schemaVersion:
|
|
1098
|
+
schemaVersion: WORKSPACE_SCHEMA_VERSION,
|
|
1629
1099
|
workspaceId,
|
|
1630
1100
|
createdAt: now,
|
|
1631
1101
|
updatedAt: now,
|
|
@@ -1634,315 +1104,169 @@ async function initWorkspace(io, workspaceId = createId("workspace")) {
|
|
|
1634
1104
|
await writeManifest(io, manifest);
|
|
1635
1105
|
return manifest;
|
|
1636
1106
|
}
|
|
1637
|
-
async function indexWorkspace(io) {
|
|
1638
|
-
|
|
1639
|
-
const markdownFiles = await io.listMarkdownFiles();
|
|
1640
|
-
const existingByPath = new Map(manifest.docs.map((doc) => [doc.path, doc]));
|
|
1641
|
-
const markdownFileSet = new Set(markdownFiles);
|
|
1642
|
-
const unmatchedExisting = manifest.docs.filter((doc) => !markdownFileSet.has(doc.path));
|
|
1643
|
-
const claimedRenamedDocIds = /* @__PURE__ */ new Set();
|
|
1644
|
-
const now = nowIso();
|
|
1645
|
-
const docs = [];
|
|
1646
|
-
const markdownByPath = /* @__PURE__ */ new Map();
|
|
1647
|
-
const newPathCountByHash = /* @__PURE__ */ new Map();
|
|
1648
|
-
for (const path of markdownFiles) {
|
|
1649
|
-
const markdown = await io.readText(path);
|
|
1650
|
-
assertCleanMarkdown(markdown);
|
|
1651
|
-
const contentHash = contentHashForText(markdown);
|
|
1652
|
-
markdownByPath.set(path, { contentHash, markdown });
|
|
1653
|
-
newPathCountByHash.set(contentHash, (newPathCountByHash.get(contentHash) ?? 0) + 1);
|
|
1654
|
-
}
|
|
1655
|
-
for (const path of markdownFiles) {
|
|
1656
|
-
const { contentHash, markdown } = markdownByPath.get(path);
|
|
1657
|
-
const existing = existingByPath.get(path) ?? await findRenamedDocumentEntry(
|
|
1658
|
-
io,
|
|
1659
|
-
unmatchedExisting.filter((doc) => !claimedRenamedDocIds.has(doc.docId)),
|
|
1660
|
-
markdown,
|
|
1661
|
-
contentHash,
|
|
1662
|
-
newPathCountByHash.get(contentHash) ?? 0
|
|
1663
|
-
);
|
|
1664
|
-
if (existing && existing.path !== path) claimedRenamedDocIds.add(existing.docId);
|
|
1665
|
-
const docId = existing?.docId ?? createDocId();
|
|
1666
|
-
const title = titleFromMarkdown(path, markdown);
|
|
1667
|
-
const entry = {
|
|
1668
|
-
docId,
|
|
1669
|
-
path,
|
|
1670
|
-
title,
|
|
1671
|
-
contentHash,
|
|
1672
|
-
currentSha: existing?.currentSha,
|
|
1673
|
-
updatedAt: now
|
|
1674
|
-
};
|
|
1675
|
-
docs.push(entry);
|
|
1676
|
-
await ensureDocumentSidecar(io, entry, markdown);
|
|
1677
|
-
}
|
|
1678
|
-
const next = {
|
|
1679
|
-
...manifest,
|
|
1680
|
-
docs,
|
|
1681
|
-
updatedAt: now
|
|
1682
|
-
};
|
|
1683
|
-
await writeManifest(io, next);
|
|
1684
|
-
return next;
|
|
1107
|
+
async function indexWorkspace(io, options) {
|
|
1108
|
+
return (await computeWorkspaceIndex(io, options)).manifest;
|
|
1685
1109
|
}
|
|
1686
|
-
async function getWorkspaceMap(io) {
|
|
1687
|
-
const manifest = await
|
|
1688
|
-
const graph =
|
|
1110
|
+
async function getWorkspaceMap(io, options) {
|
|
1111
|
+
const { manifest, graphDocuments } = await computeWorkspaceIndex(io, options);
|
|
1112
|
+
const graph = buildWorkspaceGraph(manifest.workspaceId, manifest.schemaVersion, graphDocuments);
|
|
1689
1113
|
const graphNodesByDocId = new Map(graph.nodes.map((node) => [node.docId, node]));
|
|
1690
|
-
|
|
1691
|
-
manifest.
|
|
1692
|
-
|
|
1114
|
+
return {
|
|
1115
|
+
workspaceId: manifest.workspaceId,
|
|
1116
|
+
schemaVersion: manifest.schemaVersion,
|
|
1117
|
+
docs: manifest.docs.map((entry) => {
|
|
1693
1118
|
const graphNode = graphNodesByDocId.get(entry.docId);
|
|
1694
1119
|
return {
|
|
1695
1120
|
docId: entry.docId,
|
|
1696
1121
|
path: entry.path,
|
|
1697
1122
|
title: entry.title,
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
anchorsNeedingReview: sidecar.anchors.filter((anchor) => anchor.status !== "mapped").length,
|
|
1123
|
+
openComments: 0,
|
|
1124
|
+
openSuggestions: 0,
|
|
1125
|
+
anchorsNeedingReview: 0,
|
|
1702
1126
|
outgoingLinks: graphNode?.outgoingLinks ?? 0,
|
|
1703
1127
|
incomingLinks: graphNode?.incomingLinks ?? 0,
|
|
1704
1128
|
unresolvedLinks: graphNode?.unresolvedLinks ?? 0,
|
|
1705
1129
|
externalLinks: graphNode?.externalLinks ?? 0
|
|
1706
1130
|
};
|
|
1707
1131
|
})
|
|
1708
|
-
);
|
|
1709
|
-
return {
|
|
1710
|
-
workspaceId: manifest.workspaceId,
|
|
1711
|
-
schemaVersion: manifest.schemaVersion,
|
|
1712
|
-
docs
|
|
1713
1132
|
};
|
|
1714
1133
|
}
|
|
1715
|
-
async function getWorkspaceGraph(io) {
|
|
1716
|
-
|
|
1134
|
+
async function getWorkspaceGraph(io, options) {
|
|
1135
|
+
const { manifest, graphDocuments } = await computeWorkspaceIndex(io, options);
|
|
1136
|
+
return buildWorkspaceGraph(manifest.workspaceId, manifest.schemaVersion, graphDocuments);
|
|
1717
1137
|
}
|
|
1718
|
-
async function getDocumentState(io, pathOrDocId) {
|
|
1719
|
-
const manifest = await
|
|
1138
|
+
async function getDocumentState(io, pathOrDocId, options) {
|
|
1139
|
+
const { manifest, graphDocuments, markdownByPath } = await computeWorkspaceIndex(io, options);
|
|
1720
1140
|
const entry = findDoc(manifest, pathOrDocId);
|
|
1721
|
-
const markdown = await io.readText(entry.path);
|
|
1141
|
+
const markdown = markdownByPath.get(entry.path) ?? await io.readText(entry.path);
|
|
1722
1142
|
assertCleanMarkdown(markdown);
|
|
1723
|
-
const
|
|
1724
|
-
const
|
|
1725
|
-
const nextSidecar = {
|
|
1726
|
-
...sidecar,
|
|
1727
|
-
anchors: remappedAnchors,
|
|
1728
|
-
updatedAt: nowIso()
|
|
1729
|
-
};
|
|
1730
|
-
await writeSidecar(io, nextSidecar);
|
|
1143
|
+
const source = graphDocuments.find((document) => document.docId === entry.docId);
|
|
1144
|
+
const links = source?.links ?? resolveMarkdownLinks(extractMarkdownLinks(markdown), entry, manifest.docs);
|
|
1731
1145
|
return {
|
|
1732
1146
|
docId: entry.docId,
|
|
1733
1147
|
path: entry.path,
|
|
1734
1148
|
title: entry.title,
|
|
1735
1149
|
markdown,
|
|
1736
|
-
reviewMarkdown: nextSidecar.reviewMarkdown ?? markdown,
|
|
1737
|
-
sidecar: nextSidecar,
|
|
1738
|
-
anchors: remappedAnchors.map((anchor) => ({
|
|
1739
|
-
id: anchor.id,
|
|
1740
|
-
kind: anchor.kind,
|
|
1741
|
-
status: anchor.status,
|
|
1742
|
-
range: anchor.range,
|
|
1743
|
-
quote: anchor.selector.quote,
|
|
1744
|
-
confidence: anchor.confidence
|
|
1745
|
-
})),
|
|
1746
1150
|
images: extractMarkdownImages(markdown, entry.path),
|
|
1747
|
-
links
|
|
1748
|
-
openComments:
|
|
1749
|
-
openSuggestions:
|
|
1750
|
-
|
|
1751
|
-
}
|
|
1752
|
-
async function workspaceGraphForManifest(io, manifest) {
|
|
1753
|
-
const documents = await Promise.all(
|
|
1754
|
-
manifest.docs.map(async (entry) => {
|
|
1755
|
-
const [markdown, sidecar] = await Promise.all([io.readText(entry.path), readSidecar(io, entry.docId)]);
|
|
1756
|
-
const links = resolveMarkdownLinks(extractMarkdownLinks(markdown), entry, manifest.docs);
|
|
1757
|
-
return {
|
|
1758
|
-
docId: entry.docId,
|
|
1759
|
-
path: entry.path,
|
|
1760
|
-
title: entry.title,
|
|
1761
|
-
openComments: sidecar.comments.filter((comment2) => comment2.status === "open").length,
|
|
1762
|
-
openSuggestions: sidecar.suggestions.filter((suggestion) => suggestion.status === "open").length,
|
|
1763
|
-
anchorsNeedingReview: sidecar.anchors.filter((anchor) => anchor.status !== "mapped").length,
|
|
1764
|
-
externalLinks: links.filter((link2) => link2.status === "external").length,
|
|
1765
|
-
unresolvedLinks: links.filter((link2) => link2.status === "unresolved" || link2.status === "ambiguous").length,
|
|
1766
|
-
links
|
|
1767
|
-
};
|
|
1768
|
-
})
|
|
1769
|
-
);
|
|
1770
|
-
return buildWorkspaceGraph(manifest.workspaceId, manifest.schemaVersion, documents);
|
|
1771
|
-
}
|
|
1772
|
-
async function addComment(io, pathOrDocId, range, body, author = defaultActor()) {
|
|
1773
|
-
const state = await getDocumentState(io, pathOrDocId);
|
|
1774
|
-
assertLineRangeWithin(state.markdown, range);
|
|
1775
|
-
const now = nowIso();
|
|
1776
|
-
const anchor = {
|
|
1777
|
-
id: createId("anchor"),
|
|
1778
|
-
kind: "comment",
|
|
1779
|
-
status: "mapped",
|
|
1780
|
-
selector: selectorFromRange(state.markdown, range),
|
|
1781
|
-
range,
|
|
1782
|
-
confidence: 1,
|
|
1783
|
-
updatedAt: now
|
|
1784
|
-
};
|
|
1785
|
-
const comment2 = {
|
|
1786
|
-
id: createId("comment"),
|
|
1787
|
-
anchorId: anchor.id,
|
|
1788
|
-
status: "open",
|
|
1789
|
-
author,
|
|
1790
|
-
body,
|
|
1791
|
-
replies: [],
|
|
1792
|
-
createdAt: now,
|
|
1793
|
-
updatedAt: now
|
|
1151
|
+
links,
|
|
1152
|
+
openComments: 0,
|
|
1153
|
+
openSuggestions: 0,
|
|
1154
|
+
anchorsNeedingReview: 0
|
|
1794
1155
|
};
|
|
1795
|
-
await writeSidecar(io, {
|
|
1796
|
-
...state.sidecar,
|
|
1797
|
-
anchors: [...state.sidecar.anchors, anchor],
|
|
1798
|
-
comments: [...state.sidecar.comments, comment2],
|
|
1799
|
-
updatedAt: now
|
|
1800
|
-
});
|
|
1801
|
-
return comment2;
|
|
1802
1156
|
}
|
|
1803
|
-
async function
|
|
1804
|
-
const
|
|
1805
|
-
const
|
|
1806
|
-
const
|
|
1807
|
-
|
|
1808
|
-
assertCleanMarkdown(replacement);
|
|
1157
|
+
async function computeWorkspaceIndex(io, options) {
|
|
1158
|
+
const baseline = await initWorkspace(io);
|
|
1159
|
+
const cache = options?.cache;
|
|
1160
|
+
const markdownFiles = await io.listMarkdownFiles();
|
|
1161
|
+
const existingByPath = new Map(baseline.docs.map((doc) => [doc.path, doc]));
|
|
1809
1162
|
const now = nowIso();
|
|
1810
|
-
const
|
|
1811
|
-
const
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
author,
|
|
1844
|
-
message,
|
|
1845
|
-
createdAt: now,
|
|
1846
|
-
updatedAt: now
|
|
1847
|
-
});
|
|
1848
|
-
const nextSidecar = deriveSidecarFromReviewMarkdown(
|
|
1849
|
-
{ ...state.sidecar, reviewMarkdown: review.reviewMarkdown, anchors: [...state.sidecar.anchors, anchor] },
|
|
1850
|
-
review.reviewMarkdown,
|
|
1851
|
-
{ author, baseHead: entry?.currentSha, now }
|
|
1852
|
-
);
|
|
1853
|
-
await writeSidecar(io, nextSidecar);
|
|
1854
|
-
const enriched = nextSidecar.suggestions.find((candidate) => candidate.id === suggestion.id);
|
|
1855
|
-
if (!enriched) throw new MdocsError("validation_error", `Could not derive suggestion ${suggestion.id}.`, "Refresh review state and try again.");
|
|
1856
|
-
return enriched;
|
|
1857
|
-
}
|
|
1858
|
-
async function acceptSuggestion(io, suggestionId, actor = defaultActor()) {
|
|
1859
|
-
const manifest = await indexWorkspace(io);
|
|
1860
|
-
for (const entry of manifest.docs) {
|
|
1861
|
-
const markdown = await io.readText(entry.path);
|
|
1862
|
-
const sidecar = await readSidecar(io, entry.docId);
|
|
1863
|
-
const suggestion = sidecar.suggestions.find((candidate) => candidate.id === suggestionId);
|
|
1864
|
-
if (!suggestion) continue;
|
|
1865
|
-
const now = nowIso();
|
|
1866
|
-
const reviewMarkdown = sidecar.reviewMarkdown ?? markdown;
|
|
1867
|
-
const resolvedReviewMarkdown = resolveSuggestionMarkup(reviewMarkdown, suggestionId, "accept");
|
|
1868
|
-
const nextMarkdown = projectCanonicalMarkdown(resolvedReviewMarkdown);
|
|
1869
|
-
assertCleanMarkdown(nextMarkdown);
|
|
1870
|
-
const nextSidecar = deriveSidecarFromReviewMarkdown(
|
|
1871
|
-
{ ...sidecar, reviewMarkdown: resolvedReviewMarkdown },
|
|
1872
|
-
resolvedReviewMarkdown,
|
|
1873
|
-
{ author: suggestion.author, baseHead: entry.currentSha, now }
|
|
1874
|
-
);
|
|
1875
|
-
await io.writeText(entry.path, nextMarkdown);
|
|
1876
|
-
await writeSidecar(io, nextSidecar);
|
|
1877
|
-
return resolvedSuggestion(suggestion, "accepted", actor, now);
|
|
1163
|
+
const drafts = [];
|
|
1164
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
1165
|
+
for (const path of markdownFiles) {
|
|
1166
|
+
seenPaths.add(path);
|
|
1167
|
+
const existing = existingByPath.get(path);
|
|
1168
|
+
const stat4 = io.statFile ? await io.statFile(path).catch(() => void 0) : void 0;
|
|
1169
|
+
const cached = cache?.get(path);
|
|
1170
|
+
const statUnchanged = Boolean(stat4 && cached && cached.mtimeMs === stat4.mtimeMs && cached.size === stat4.size);
|
|
1171
|
+
if (statUnchanged && cached) {
|
|
1172
|
+
drafts.push({
|
|
1173
|
+
entry: manifestEntry(cached.docId, path, cached.title, cached.contentHash, existing, now, stat4),
|
|
1174
|
+
markdown: cached.markdown,
|
|
1175
|
+
reusedFromCache: true
|
|
1176
|
+
});
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
let markdown;
|
|
1180
|
+
try {
|
|
1181
|
+
markdown = await io.readText(path);
|
|
1182
|
+
assertCleanMarkdown(markdown);
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
emitIndexDiagnostic(path, error);
|
|
1185
|
+
if (existing) drafts.push({ entry: { ...existing }, markdown: cached?.markdown ?? "", reusedFromCache: true });
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
const contentHash = contentHashForText(markdown);
|
|
1189
|
+
const title = titleFromMarkdown(path, markdown);
|
|
1190
|
+
const docId = existing?.docId ?? mintDocId(options?.rootId, path);
|
|
1191
|
+
drafts.push({
|
|
1192
|
+
entry: manifestEntry(docId, path, title, contentHash, existing, now, stat4),
|
|
1193
|
+
markdown,
|
|
1194
|
+
reusedFromCache: false
|
|
1195
|
+
});
|
|
1878
1196
|
}
|
|
1879
|
-
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
const
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
const
|
|
1889
|
-
const
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1197
|
+
const docs = drafts.map((draft) => draft.entry);
|
|
1198
|
+
const lookup = docs.map((entry) => ({ docId: entry.docId, path: entry.path, title: entry.title }));
|
|
1199
|
+
const lookupSignature = JSON.stringify(lookup);
|
|
1200
|
+
const reuseLinks = cache ? cacheLookupSignatures.get(cache) === lookupSignature : false;
|
|
1201
|
+
const graphDocuments = [];
|
|
1202
|
+
const markdownByPath = /* @__PURE__ */ new Map();
|
|
1203
|
+
for (const draft of drafts) {
|
|
1204
|
+
const { entry, markdown, reusedFromCache } = draft;
|
|
1205
|
+
markdownByPath.set(entry.path, markdown);
|
|
1206
|
+
const cached = cache?.get(entry.path);
|
|
1207
|
+
const links = reusedFromCache && reuseLinks && cached?.links && cached.docId === entry.docId ? cached.links : resolveMarkdownLinks(extractMarkdownLinks(markdown), entry, lookup);
|
|
1208
|
+
graphDocuments.push({
|
|
1209
|
+
docId: entry.docId,
|
|
1210
|
+
path: entry.path,
|
|
1211
|
+
title: entry.title,
|
|
1212
|
+
openComments: 0,
|
|
1213
|
+
openSuggestions: 0,
|
|
1214
|
+
anchorsNeedingReview: 0,
|
|
1215
|
+
externalLinks: links.filter((link2) => link2.status === "external").length,
|
|
1216
|
+
unresolvedLinks: links.filter((link2) => link2.status === "unresolved" || link2.status === "ambiguous").length,
|
|
1217
|
+
links
|
|
1893
1218
|
});
|
|
1894
|
-
|
|
1895
|
-
|
|
1219
|
+
if (cache && entry.mtimeMs !== void 0 && entry.size !== void 0) {
|
|
1220
|
+
cache.set(entry.path, {
|
|
1221
|
+
mtimeMs: entry.mtimeMs,
|
|
1222
|
+
size: entry.size,
|
|
1223
|
+
contentHash: entry.contentHash ?? contentHashForText(markdown),
|
|
1224
|
+
markdown,
|
|
1225
|
+
title: entry.title,
|
|
1226
|
+
docId: entry.docId,
|
|
1227
|
+
links
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1896
1230
|
}
|
|
1897
|
-
|
|
1231
|
+
if (cache) {
|
|
1232
|
+
cacheLookupSignatures.set(cache, lookupSignature);
|
|
1233
|
+
for (const key of [...cache.keys()]) {
|
|
1234
|
+
if (!seenPaths.has(key)) cache.delete(key);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
const nextManifest = { ...baseline, docs, updatedAt: baseline.updatedAt };
|
|
1238
|
+
if (stableManifestKey(baseline) === stableManifestKey(nextManifest)) {
|
|
1239
|
+
return { manifest: baseline, graphDocuments, markdownByPath };
|
|
1240
|
+
}
|
|
1241
|
+
const written = { ...nextManifest, updatedAt: now };
|
|
1242
|
+
await writeManifest(io, written);
|
|
1243
|
+
return { manifest: written, graphDocuments, markdownByPath };
|
|
1898
1244
|
}
|
|
1899
|
-
function
|
|
1245
|
+
function manifestEntry(docId, path, title, contentHash, existing, now, stat4) {
|
|
1900
1246
|
return {
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1247
|
+
docId,
|
|
1248
|
+
path,
|
|
1249
|
+
title,
|
|
1250
|
+
contentHash,
|
|
1251
|
+
currentSha: existing?.currentSha,
|
|
1252
|
+
// Preserve the prior timestamp when the content is unchanged so the manifest
|
|
1253
|
+
// stays byte-identical; only a real content change advances it.
|
|
1254
|
+
updatedAt: existing && existing.contentHash === contentHash ? existing.updatedAt : now,
|
|
1255
|
+
...stat4 ? { mtimeMs: stat4.mtimeMs, size: stat4.size } : {}
|
|
1906
1256
|
};
|
|
1907
1257
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
for (const entry of manifest.docs) {
|
|
1911
|
-
const sidecar = await readSidecar(io, entry.docId);
|
|
1912
|
-
const comment2 = sidecar.comments.find((candidate) => candidate.id === commentId);
|
|
1913
|
-
if (!comment2) continue;
|
|
1914
|
-
const now = nowIso();
|
|
1915
|
-
const updated = {
|
|
1916
|
-
...comment2,
|
|
1917
|
-
status: "resolved",
|
|
1918
|
-
updatedAt: now,
|
|
1919
|
-
resolvedAt: now,
|
|
1920
|
-
resolvedBy: actor
|
|
1921
|
-
};
|
|
1922
|
-
await writeSidecar(io, {
|
|
1923
|
-
...sidecar,
|
|
1924
|
-
comments: sidecar.comments.map((candidate) => candidate.id === commentId ? updated : candidate),
|
|
1925
|
-
updatedAt: now
|
|
1926
|
-
});
|
|
1927
|
-
return updated;
|
|
1928
|
-
}
|
|
1929
|
-
throw new MdocsError("not_found", `Unknown comment: ${commentId}`, "List comment ids with the comments command.");
|
|
1258
|
+
function mintDocId(rootId, path) {
|
|
1259
|
+
return rootId ? deterministicDocId(rootId, path) : createDocId();
|
|
1930
1260
|
}
|
|
1931
|
-
function
|
|
1932
|
-
return {
|
|
1933
|
-
schemaVersion: SIDECAR_SCHEMA_VERSION,
|
|
1934
|
-
docId: entry.docId,
|
|
1935
|
-
path: entry.path,
|
|
1936
|
-
title: entry.title,
|
|
1937
|
-
anchors: [],
|
|
1938
|
-
comments: [],
|
|
1939
|
-
suggestions: [],
|
|
1940
|
-
changeSets: [],
|
|
1941
|
-
updatedAt: entry.updatedAt
|
|
1942
|
-
};
|
|
1261
|
+
function stableManifestKey(manifest) {
|
|
1262
|
+
return JSON.stringify({ ...manifest, updatedAt: "" });
|
|
1943
1263
|
}
|
|
1944
|
-
function
|
|
1945
|
-
|
|
1264
|
+
function emitIndexDiagnostic(path, error) {
|
|
1265
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1266
|
+
if (typeof process !== "undefined" && process.stderr && typeof process.stderr.write === "function") {
|
|
1267
|
+
process.stderr.write(`mdocs index skipped ${path}: ${detail}
|
|
1268
|
+
`);
|
|
1269
|
+
}
|
|
1946
1270
|
}
|
|
1947
1271
|
async function readManifest(io) {
|
|
1948
1272
|
const raw = await io.readText(MANIFEST_PATH);
|
|
@@ -1952,79 +1276,21 @@ async function writeManifest(io, manifest) {
|
|
|
1952
1276
|
await writeJsonText(io, MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}
|
|
1953
1277
|
`);
|
|
1954
1278
|
}
|
|
1955
|
-
async function readSidecar(io, docId) {
|
|
1956
|
-
const raw = await io.readText(sidecarPath(docId));
|
|
1957
|
-
return JSON.parse(raw);
|
|
1958
|
-
}
|
|
1959
|
-
async function writeSidecar(io, sidecar) {
|
|
1960
|
-
await writeJsonText(io, sidecarPath(sidecar.docId), `${JSON.stringify(sidecar, null, 2)}
|
|
1961
|
-
`);
|
|
1962
|
-
}
|
|
1963
|
-
async function writeJsonText(io, path, content) {
|
|
1964
|
-
if (io.writeTextAtomic) {
|
|
1965
|
-
await io.writeTextAtomic(path, content);
|
|
1966
|
-
return;
|
|
1967
|
-
}
|
|
1968
|
-
await io.writeText(path, content);
|
|
1969
|
-
}
|
|
1970
1279
|
function findDoc(manifest, pathOrDocId) {
|
|
1971
1280
|
const entry = manifest.docs.find((doc) => doc.path === pathOrDocId || doc.docId === pathOrDocId);
|
|
1972
|
-
if (!entry) throw new
|
|
1281
|
+
if (!entry) throw new MdocsError("not_found", `Unknown document: ${pathOrDocId}`);
|
|
1973
1282
|
return entry;
|
|
1974
1283
|
}
|
|
1975
1284
|
async function readManifestIfPresent(io) {
|
|
1976
1285
|
if (!await io.exists(MANIFEST_PATH)) return void 0;
|
|
1977
1286
|
return readManifest(io);
|
|
1978
1287
|
}
|
|
1979
|
-
async function
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
const sidecar = await readSidecar(io, entry.docId).catch(() => createEmptySidecar(entry));
|
|
1983
|
-
await writeSidecar(io, {
|
|
1984
|
-
...sidecar,
|
|
1985
|
-
path: entry.path,
|
|
1986
|
-
title: entry.title,
|
|
1987
|
-
anchors: remapAnchors(markdown, sidecar.anchors),
|
|
1988
|
-
updatedAt: nowIso()
|
|
1989
|
-
});
|
|
1288
|
+
async function writeJsonText(io, path, content) {
|
|
1289
|
+
if (io.writeTextAtomic) {
|
|
1290
|
+
await io.writeTextAtomic(path, content);
|
|
1990
1291
|
return;
|
|
1991
1292
|
}
|
|
1992
|
-
await
|
|
1993
|
-
}
|
|
1994
|
-
async function findRenamedDocumentEntry(io, candidates, markdown, contentHash, newPathCountForHash) {
|
|
1995
|
-
if (newPathCountForHash === 1) {
|
|
1996
|
-
const hashMatches = candidates.filter((entry) => entry.contentHash === contentHash);
|
|
1997
|
-
if (hashMatches.length === 1) return hashMatches[0];
|
|
1998
|
-
}
|
|
1999
|
-
const scored = [];
|
|
2000
|
-
for (const entry of candidates) {
|
|
2001
|
-
const path = sidecarPath(entry.docId);
|
|
2002
|
-
if (!await io.exists(path)) continue;
|
|
2003
|
-
const sidecar = await readSidecar(io, entry.docId).catch(() => void 0);
|
|
2004
|
-
if (!sidecar) continue;
|
|
2005
|
-
const score = sidecarMarkdownMatchScore(sidecar, markdown);
|
|
2006
|
-
if (score > 0) scored.push({ entry, score });
|
|
2007
|
-
}
|
|
2008
|
-
scored.sort((left, right) => right.score - left.score);
|
|
2009
|
-
const best = scored[0];
|
|
2010
|
-
const runnerUp = scored[1];
|
|
2011
|
-
return best && best.score > (runnerUp?.score ?? 0) ? best.entry : void 0;
|
|
2012
|
-
}
|
|
2013
|
-
function sidecarMarkdownMatchScore(sidecar, markdown) {
|
|
2014
|
-
const needles = /* @__PURE__ */ new Set();
|
|
2015
|
-
for (const anchor of sidecar.anchors) {
|
|
2016
|
-
const quote = anchor.selector.quote.trim();
|
|
2017
|
-
if (quote) needles.add(quote);
|
|
2018
|
-
}
|
|
2019
|
-
for (const suggestion of sidecar.suggestions) {
|
|
2020
|
-
if (suggestion.patch.before.trim()) needles.add(suggestion.patch.before.trim());
|
|
2021
|
-
if (suggestion.patch.after.trim()) needles.add(suggestion.patch.after.trim());
|
|
2022
|
-
}
|
|
2023
|
-
let score = 0;
|
|
2024
|
-
for (const needle of needles) {
|
|
2025
|
-
if (markdown.includes(needle)) score += 1;
|
|
2026
|
-
}
|
|
2027
|
-
return score;
|
|
1293
|
+
await io.writeText(path, content);
|
|
2028
1294
|
}
|
|
2029
1295
|
|
|
2030
1296
|
// ../core/src/context.ts
|
|
@@ -2043,8 +1309,6 @@ function sliceMarkdown(markdown, startLine, endLine) {
|
|
|
2043
1309
|
return { markdown: lines.slice(sl - 1, el).join("\n"), totalLines, startLine: sl, endLine: el };
|
|
2044
1310
|
}
|
|
2045
1311
|
function summarizeDocumentContext(document) {
|
|
2046
|
-
const comments = openComments(document);
|
|
2047
|
-
const suggestions = openSuggestions(document);
|
|
2048
1312
|
const currentSha = currentDocumentSha(document);
|
|
2049
1313
|
return {
|
|
2050
1314
|
docId: document.docId,
|
|
@@ -2055,9 +1319,9 @@ function summarizeDocumentContext(document) {
|
|
|
2055
1319
|
byteLength: new TextEncoder().encode(document.markdown).byteLength,
|
|
2056
1320
|
headings: extractMarkdownHeadings(document.markdown),
|
|
2057
1321
|
reviewCounts: {
|
|
2058
|
-
openComments:
|
|
2059
|
-
openSuggestions:
|
|
2060
|
-
anchorsNeedingReview: document.
|
|
1322
|
+
openComments: document.openComments,
|
|
1323
|
+
openSuggestions: document.openSuggestions,
|
|
1324
|
+
anchorsNeedingReview: document.anchorsNeedingReview
|
|
2061
1325
|
},
|
|
2062
1326
|
imageCounts: imageCounts(document.images),
|
|
2063
1327
|
linkCounts: linkCounts(document.links)
|
|
@@ -2070,9 +1334,12 @@ function reviewStateForDocument(document) {
|
|
|
2070
1334
|
path: document.path,
|
|
2071
1335
|
title: document.title,
|
|
2072
1336
|
...currentSha ? { currentSha } : {},
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
1337
|
+
reviewCounts: {
|
|
1338
|
+
openComments: document.openComments,
|
|
1339
|
+
openSuggestions: document.openSuggestions,
|
|
1340
|
+
anchorsNeedingReview: document.anchorsNeedingReview
|
|
1341
|
+
},
|
|
1342
|
+
message: "Review state is authority-backed. Use remote authority commands or MCP endpoints for comments, suggestions, accept, and reject."
|
|
2076
1343
|
};
|
|
2077
1344
|
}
|
|
2078
1345
|
function currentDocumentSha(document) {
|
|
@@ -2101,12 +1368,6 @@ function extractMarkdownHeadings(markdown) {
|
|
|
2101
1368
|
});
|
|
2102
1369
|
return headings;
|
|
2103
1370
|
}
|
|
2104
|
-
function openComments(document) {
|
|
2105
|
-
return document.sidecar.comments.filter((comment2) => comment2.status === "open");
|
|
2106
|
-
}
|
|
2107
|
-
function openSuggestions(document) {
|
|
2108
|
-
return document.sidecar.suggestions.filter((suggestion) => suggestion.status === "open");
|
|
2109
|
-
}
|
|
2110
1371
|
function imageCounts(images) {
|
|
2111
1372
|
const local = images.filter((image2) => image2.isLocal).length;
|
|
2112
1373
|
return {
|
|
@@ -4844,7 +4105,8 @@ var canonicalSchema = new Schema({
|
|
|
4844
4105
|
nodes: {
|
|
4845
4106
|
// The doc allows suggestion marks on its block children so pending
|
|
4846
4107
|
// block-level suggestions (node marks) survive fromJSON and can be
|
|
4847
|
-
// structurally
|
|
4108
|
+
// structurally resolved before serialization. Only inline insert/delete is
|
|
4109
|
+
// used today; block-level (modification) is reserved for a later pass.
|
|
4848
4110
|
doc: { content: "block+", marks: "insertion deletion modification" },
|
|
4849
4111
|
paragraph: { group: "block", content: "inline*", attrs: blockAttrs },
|
|
4850
4112
|
heading: {
|
|
@@ -4926,11 +4188,14 @@ var canonicalSchema = new Schema({
|
|
|
4926
4188
|
strike: {},
|
|
4927
4189
|
code: {},
|
|
4928
4190
|
// Pending-suggestion marks (mirroring the web editor's extensions). They
|
|
4929
|
-
//
|
|
4930
|
-
// but the schema must know them so suggestion-marked docs load
|
|
4191
|
+
// never appear in canonical bytes — the canonical serializer resolves them
|
|
4192
|
+
// first — but the schema must know them so suggestion-marked docs load and
|
|
4193
|
+
// the CriticMarkup codec can emit/parse them. `id` tags a rendered span
|
|
4194
|
+
// with its owning branch at overlay time; it is empty while editing one
|
|
4195
|
+
// branch.
|
|
4931
4196
|
insertion: {
|
|
4932
4197
|
attrs: { id: { default: null } },
|
|
4933
|
-
inclusive:
|
|
4198
|
+
inclusive: true,
|
|
4934
4199
|
excludes: "deletion modification insertion"
|
|
4935
4200
|
},
|
|
4936
4201
|
deletion: {
|
|
@@ -11014,7 +10279,70 @@ var MarkdownSerializerState = class {
|
|
|
11014
10279
|
index++;
|
|
11015
10280
|
}
|
|
11016
10281
|
}
|
|
11017
|
-
};
|
|
10282
|
+
};
|
|
10283
|
+
|
|
10284
|
+
// ../core/src/suggestion-id.ts
|
|
10285
|
+
var SUGGESTION_ID_PREFIX = "suggestion:";
|
|
10286
|
+
|
|
10287
|
+
// ../core/src/critic-markup.ts
|
|
10288
|
+
var INSERTION_OPEN_SENTINEL = String.fromCharCode(57344);
|
|
10289
|
+
var INSERTION_CLOSE_SENTINEL = String.fromCharCode(57345);
|
|
10290
|
+
var DELETION_OPEN_SENTINEL = String.fromCharCode(57346);
|
|
10291
|
+
var DELETION_CLOSE_SENTINEL = String.fromCharCode(57347);
|
|
10292
|
+
var ID_OPEN_SENTINEL = String.fromCharCode(57348);
|
|
10293
|
+
var ID_CLOSE_SENTINEL = String.fromCharCode(57349);
|
|
10294
|
+
function suggestionSpanAt(src, start) {
|
|
10295
|
+
const open = src.slice(start, start + 3);
|
|
10296
|
+
if (open === "{++") return { markName: "insertion", closeSequence: "++}" };
|
|
10297
|
+
if (open === "{--") return { markName: "deletion", closeSequence: "--}" };
|
|
10298
|
+
return void 0;
|
|
10299
|
+
}
|
|
10300
|
+
function suggestionIdSuffixAt(src, pos) {
|
|
10301
|
+
if (!src.startsWith("{#", pos)) return void 0;
|
|
10302
|
+
const end = src.indexOf("}", pos + 2);
|
|
10303
|
+
if (end === -1) return void 0;
|
|
10304
|
+
const id = src.slice(pos + 2, end);
|
|
10305
|
+
return id.startsWith(SUGGESTION_ID_PREFIX) ? id : void 0;
|
|
10306
|
+
}
|
|
10307
|
+
function findUnescapedClose(src, from, closeSequence) {
|
|
10308
|
+
let pos = from;
|
|
10309
|
+
while (pos < src.length) {
|
|
10310
|
+
if (src.charCodeAt(pos) === 92) {
|
|
10311
|
+
pos += 2;
|
|
10312
|
+
continue;
|
|
10313
|
+
}
|
|
10314
|
+
if (src.startsWith(closeSequence, pos)) return pos;
|
|
10315
|
+
pos += 1;
|
|
10316
|
+
}
|
|
10317
|
+
return -1;
|
|
10318
|
+
}
|
|
10319
|
+
function criticMarkupPlugin(md) {
|
|
10320
|
+
md.inline.ruler.before("emphasis", "criticmarkup", (state, silent) => {
|
|
10321
|
+
const start = state.pos;
|
|
10322
|
+
if (state.src.charCodeAt(start) !== 123) return false;
|
|
10323
|
+
const span = suggestionSpanAt(state.src, start);
|
|
10324
|
+
if (!span) return false;
|
|
10325
|
+
const contentStart = start + 3;
|
|
10326
|
+
const closeStart = findUnescapedClose(state.src, contentStart, span.closeSequence);
|
|
10327
|
+
if (closeStart === -1) return false;
|
|
10328
|
+
if (silent) return true;
|
|
10329
|
+
const openToken = state.push(`${span.markName}_open`, "", 1);
|
|
10330
|
+
const savedMax = state.posMax;
|
|
10331
|
+
state.pos = contentStart;
|
|
10332
|
+
state.posMax = closeStart;
|
|
10333
|
+
state.md.inline.tokenize(state);
|
|
10334
|
+
state.posMax = savedMax;
|
|
10335
|
+
state.push(`${span.markName}_close`, "", -1);
|
|
10336
|
+
let nextPos = closeStart + span.closeSequence.length;
|
|
10337
|
+
const id = suggestionIdSuffixAt(state.src, nextPos);
|
|
10338
|
+
if (id !== void 0) {
|
|
10339
|
+
openToken.attrSet("id", id);
|
|
10340
|
+
nextPos += `{#${id}}`.length;
|
|
10341
|
+
}
|
|
10342
|
+
state.pos = nextPos;
|
|
10343
|
+
return true;
|
|
10344
|
+
});
|
|
10345
|
+
}
|
|
11018
10346
|
|
|
11019
10347
|
// ../core/src/pm-markdown.ts
|
|
11020
10348
|
function serializeInlineFragmentToMarkdown(node) {
|
|
@@ -11058,109 +10386,119 @@ function backtickFence(node) {
|
|
|
11058
10386
|
const longest = matches.reduce((length, run) => Math.max(length, run.length), 2);
|
|
11059
10387
|
return "`".repeat(longest + 1);
|
|
11060
10388
|
}
|
|
11061
|
-
var
|
|
11062
|
-
{
|
|
11063
|
-
|
|
11064
|
-
|
|
11065
|
-
|
|
11066
|
-
|
|
11067
|
-
|
|
11068
|
-
|
|
11069
|
-
|
|
11070
|
-
|
|
11071
|
-
|
|
11072
|
-
|
|
11073
|
-
|
|
11074
|
-
|
|
11075
|
-
|
|
11076
|
-
|
|
11077
|
-
state.write(`${fence2}${typeof node.attrs.language === "string" ? node.attrs.language : ""}
|
|
10389
|
+
var NODE_SERIALIZERS = {
|
|
10390
|
+
paragraph(state, node) {
|
|
10391
|
+
state.renderInline(node);
|
|
10392
|
+
state.closeBlock(node);
|
|
10393
|
+
},
|
|
10394
|
+
heading(state, node) {
|
|
10395
|
+
state.write(`${"#".repeat(Number(node.attrs.level) || 1)} `);
|
|
10396
|
+
state.renderInline(node, false);
|
|
10397
|
+
state.closeBlock(node);
|
|
10398
|
+
},
|
|
10399
|
+
blockquote(state, node) {
|
|
10400
|
+
state.wrapBlock("> ", null, node, () => state.renderContent(node));
|
|
10401
|
+
},
|
|
10402
|
+
codeBlock(state, node) {
|
|
10403
|
+
const fence2 = backtickFence(node);
|
|
10404
|
+
state.write(`${fence2}${typeof node.attrs.language === "string" ? node.attrs.language : ""}
|
|
11078
10405
|
`);
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
|
|
11093
|
-
|
|
11094
|
-
|
|
11095
|
-
|
|
11096
|
-
|
|
11097
|
-
|
|
11098
|
-
|
|
11099
|
-
|
|
11100
|
-
|
|
11101
|
-
|
|
11102
|
-
|
|
11103
|
-
|
|
11104
|
-
|
|
11105
|
-
|
|
11106
|
-
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
|
|
11117
|
-
|
|
11118
|
-
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
|
|
11122
|
-
|
|
11123
|
-
|
|
11124
|
-
}
|
|
10406
|
+
state.text(node.textContent, false);
|
|
10407
|
+
state.ensureNewLine();
|
|
10408
|
+
state.write(fence2);
|
|
10409
|
+
state.closeBlock(node);
|
|
10410
|
+
},
|
|
10411
|
+
horizontalRule(state, node) {
|
|
10412
|
+
state.write("---");
|
|
10413
|
+
state.closeBlock(node);
|
|
10414
|
+
},
|
|
10415
|
+
bulletList(state, node) {
|
|
10416
|
+
state.renderList(node, " ", () => "- ");
|
|
10417
|
+
},
|
|
10418
|
+
orderedList(state, node) {
|
|
10419
|
+
const start = Number(node.attrs.start) || 1;
|
|
10420
|
+
const maxWidth = String(start + node.childCount - 1).length;
|
|
10421
|
+
const space = state.repeat(" ", maxWidth + 2);
|
|
10422
|
+
state.renderList(node, space, (index) => {
|
|
10423
|
+
const label = String(start + index);
|
|
10424
|
+
return `${state.repeat(" ", maxWidth - label.length)}${label}. `;
|
|
10425
|
+
});
|
|
10426
|
+
},
|
|
10427
|
+
listItem(state, node) {
|
|
10428
|
+
state.renderContent(node);
|
|
10429
|
+
},
|
|
10430
|
+
taskList(state, node) {
|
|
10431
|
+
state.renderList(node, " ", () => "- ");
|
|
10432
|
+
},
|
|
10433
|
+
taskItem(state, node) {
|
|
10434
|
+
state.write(`[${node.attrs.checked ? "x" : " "}] `);
|
|
10435
|
+
state.renderContent(node);
|
|
10436
|
+
},
|
|
10437
|
+
table: serializeTable,
|
|
10438
|
+
image(state, node) {
|
|
10439
|
+
const src = typeof node.attrs.src === "string" ? node.attrs.src : "";
|
|
10440
|
+
const alt = typeof node.attrs.alt === "string" ? node.attrs.alt : "";
|
|
10441
|
+
const title = typeof node.attrs.title === "string" ? node.attrs.title : "";
|
|
10442
|
+
const destination = src.replace(/[()"]/g, "\\$&");
|
|
10443
|
+
const titleSuffix = title ? ` "${title.replace(/["\\]/g, "\\$&")}"` : "";
|
|
10444
|
+
state.write(``);
|
|
10445
|
+
},
|
|
10446
|
+
hardBreak(state, node, parent, index) {
|
|
10447
|
+
for (let after = index + 1; after < parent.childCount; after += 1) {
|
|
10448
|
+
if (parent.child(after).type !== node.type) {
|
|
10449
|
+
state.write("\\\n");
|
|
10450
|
+
return;
|
|
11125
10451
|
}
|
|
11126
|
-
},
|
|
11127
|
-
text(state, node) {
|
|
11128
|
-
state.text(node.text ?? "");
|
|
11129
10452
|
}
|
|
11130
10453
|
},
|
|
11131
|
-
{
|
|
11132
|
-
|
|
11133
|
-
|
|
11134
|
-
|
|
11135
|
-
|
|
11136
|
-
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
|
|
11140
|
-
|
|
11141
|
-
|
|
11142
|
-
escape: false
|
|
10454
|
+
text(state, node) {
|
|
10455
|
+
state.text(node.text ?? "");
|
|
10456
|
+
}
|
|
10457
|
+
};
|
|
10458
|
+
var BASE_MARK_SERIALIZERS = {
|
|
10459
|
+
bold: { open: "**", close: "**", mixable: true, expelEnclosingWhitespace: true },
|
|
10460
|
+
italic: { open: "_", close: "_", mixable: true, expelEnclosingWhitespace: true },
|
|
10461
|
+
strike: { open: "~~", close: "~~", mixable: true, expelEnclosingWhitespace: true },
|
|
10462
|
+
code: {
|
|
10463
|
+
open(_state, _mark, parent, index) {
|
|
10464
|
+
return backticksFor2(parent.child(index), -1);
|
|
11143
10465
|
},
|
|
11144
|
-
|
|
11145
|
-
|
|
11146
|
-
close(_state, mark) {
|
|
11147
|
-
const href = typeof mark.attrs.href === "string" ? mark.attrs.href : "";
|
|
11148
|
-
return `](${href.replace(/[()"]/g, "\\$&")})`;
|
|
11149
|
-
},
|
|
11150
|
-
mixable: false
|
|
10466
|
+
close(_state, _mark, parent, index) {
|
|
10467
|
+
return backticksFor2(parent.child(index - 1), 1);
|
|
11151
10468
|
},
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
}
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
10469
|
+
escape: false
|
|
10470
|
+
},
|
|
10471
|
+
link: {
|
|
10472
|
+
open: "[",
|
|
10473
|
+
close(_state, mark) {
|
|
10474
|
+
const href = typeof mark.attrs.href === "string" ? mark.attrs.href : "";
|
|
10475
|
+
return `](${href.replace(/[()"]/g, "\\$&")})`;
|
|
10476
|
+
},
|
|
10477
|
+
mixable: false
|
|
10478
|
+
},
|
|
10479
|
+
wikilink: {
|
|
10480
|
+
open(_state, mark, parent, index) {
|
|
10481
|
+
const target = typeof mark.attrs.target === "string" ? mark.attrs.target : "";
|
|
10482
|
+
const label = markedRangeText(parent, index, mark);
|
|
10483
|
+
if (!target || label === target) return "[[";
|
|
10484
|
+
return `[[${target}|`;
|
|
10485
|
+
},
|
|
10486
|
+
close: "]]",
|
|
10487
|
+
mixable: false
|
|
11162
10488
|
}
|
|
11163
|
-
|
|
10489
|
+
};
|
|
10490
|
+
var markdownSerializer = new MarkdownSerializer(NODE_SERIALIZERS, BASE_MARK_SERIALIZERS);
|
|
10491
|
+
function suggestionMarkClose(closeSentinel) {
|
|
10492
|
+
return (_state, mark) => {
|
|
10493
|
+
const id = typeof mark.attrs.id === "string" ? mark.attrs.id : "";
|
|
10494
|
+
return id ? `${closeSentinel}${ID_OPEN_SENTINEL}${id}${ID_CLOSE_SENTINEL}` : closeSentinel;
|
|
10495
|
+
};
|
|
10496
|
+
}
|
|
10497
|
+
var criticMarkupSerializer = new MarkdownSerializer(NODE_SERIALIZERS, {
|
|
10498
|
+
...BASE_MARK_SERIALIZERS,
|
|
10499
|
+
insertion: { open: INSERTION_OPEN_SENTINEL, close: suggestionMarkClose(INSERTION_CLOSE_SENTINEL), mixable: true },
|
|
10500
|
+
deletion: { open: DELETION_OPEN_SENTINEL, close: suggestionMarkClose(DELETION_CLOSE_SENTINEL), mixable: true }
|
|
10501
|
+
});
|
|
11164
10502
|
function markedRangeText(parent, index, mark) {
|
|
11165
10503
|
let text2 = "";
|
|
11166
10504
|
for (let childIndex = index; childIndex < parent.childCount; childIndex += 1) {
|
|
@@ -11187,7 +10525,7 @@ function backticksFor2(node, side) {
|
|
|
11187
10525
|
}
|
|
11188
10526
|
|
|
11189
10527
|
// ../core/src/pm-parse.ts
|
|
11190
|
-
var
|
|
10528
|
+
var BASE_TOKEN_SPEC = {
|
|
11191
10529
|
blockquote: { block: "blockquote" },
|
|
11192
10530
|
paragraph: { block: "paragraph" },
|
|
11193
10531
|
list_item: { block: "listItem" },
|
|
@@ -11214,56 +10552,246 @@ var parser = new MarkdownParser(canonicalSchema, new lib_default(), {
|
|
|
11214
10552
|
title: tok.attrGet("title")
|
|
11215
10553
|
})
|
|
11216
10554
|
}
|
|
10555
|
+
};
|
|
10556
|
+
var parser = new MarkdownParser(canonicalSchema, new lib_default(), BASE_TOKEN_SPEC);
|
|
10557
|
+
var criticMarkupParser = new MarkdownParser(canonicalSchema, new lib_default().use(criticMarkupPlugin), {
|
|
10558
|
+
...BASE_TOKEN_SPEC,
|
|
10559
|
+
insertion: { mark: "insertion", getAttrs: (tok) => ({ id: tok.attrGet("id") }) },
|
|
10560
|
+
deletion: { mark: "deletion", getAttrs: (tok) => ({ id: tok.attrGet("id") }) }
|
|
11217
10561
|
});
|
|
11218
10562
|
|
|
11219
|
-
// ../core/src/
|
|
11220
|
-
var
|
|
11221
|
-
|
|
11222
|
-
|
|
11223
|
-
|
|
11224
|
-
|
|
11225
|
-
|
|
11226
|
-
|
|
11227
|
-
|
|
11228
|
-
|
|
11229
|
-
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11239
|
-
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
|
|
10563
|
+
// ../core/src/patch.ts
|
|
10564
|
+
var PATCH_SCHEMA = "magic-markdown.patch.v1";
|
|
10565
|
+
function textPatchHash(text2) {
|
|
10566
|
+
return contentHashForText(text2);
|
|
10567
|
+
}
|
|
10568
|
+
function buildPatchEnvelope(options) {
|
|
10569
|
+
const ops = diffToPatchOps(options.base, options.result);
|
|
10570
|
+
return patchEnvelopeFromOps({
|
|
10571
|
+
baseVersion: options.baseVersion,
|
|
10572
|
+
base: options.base,
|
|
10573
|
+
ops,
|
|
10574
|
+
clientMutationId: options.clientMutationId,
|
|
10575
|
+
clientMeta: options.clientMeta
|
|
10576
|
+
});
|
|
10577
|
+
}
|
|
10578
|
+
function buildLineRangePatchEnvelope(options) {
|
|
10579
|
+
assertLineRangeWithin(options.base, options.range);
|
|
10580
|
+
const result = replaceLineRange(options.base, options.range, options.replacement);
|
|
10581
|
+
return buildPatchEnvelope({
|
|
10582
|
+
baseVersion: options.baseVersion,
|
|
10583
|
+
base: options.base,
|
|
10584
|
+
result,
|
|
10585
|
+
clientMutationId: options.clientMutationId,
|
|
10586
|
+
clientMeta: { ...options.clientMeta ?? {}, source: options.clientMeta?.source ?? "line_range" }
|
|
10587
|
+
});
|
|
10588
|
+
}
|
|
10589
|
+
function patchEnvelopeFromOps(options) {
|
|
10590
|
+
const ops = normalizePatchOps(options.ops);
|
|
10591
|
+
const result = applyPatchOps(options.base, ops);
|
|
10592
|
+
return {
|
|
10593
|
+
schema: PATCH_SCHEMA,
|
|
10594
|
+
baseVersion: options.baseVersion,
|
|
10595
|
+
baseHash: textPatchHash(options.base),
|
|
10596
|
+
baseLength: options.base.length,
|
|
10597
|
+
resultLength: result.length,
|
|
10598
|
+
resultHash: textPatchHash(result),
|
|
10599
|
+
ops,
|
|
10600
|
+
...options.clientMutationId ? { clientMutationId: options.clientMutationId } : {},
|
|
10601
|
+
...options.clientMeta ? { clientMeta: options.clientMeta } : {}
|
|
10602
|
+
};
|
|
10603
|
+
}
|
|
10604
|
+
function applyPatchOps(base, ops) {
|
|
10605
|
+
validatePatchOpsShape(ops);
|
|
10606
|
+
let baseCursor = 0;
|
|
10607
|
+
let result = "";
|
|
10608
|
+
for (const op of ops) {
|
|
10609
|
+
if (op.type === "retain") {
|
|
10610
|
+
const next = baseCursor + op.count;
|
|
10611
|
+
if (next > base.length) throw new MdocsError("validation_error", "Patch retain runs past the base document.");
|
|
10612
|
+
result += base.slice(baseCursor, next);
|
|
10613
|
+
baseCursor = next;
|
|
10614
|
+
continue;
|
|
10615
|
+
}
|
|
10616
|
+
if (op.type === "delete") {
|
|
10617
|
+
const actual = base.slice(baseCursor, baseCursor + op.text.length);
|
|
10618
|
+
if (actual !== op.text) throw new MdocsError("conflict", "Patch delete text does not match the base document.");
|
|
10619
|
+
baseCursor += op.text.length;
|
|
10620
|
+
continue;
|
|
10621
|
+
}
|
|
10622
|
+
result += op.text;
|
|
10623
|
+
}
|
|
10624
|
+
return result + base.slice(baseCursor);
|
|
10625
|
+
}
|
|
10626
|
+
function normalizePatchOps(ops) {
|
|
10627
|
+
validatePatchOpsShape(ops);
|
|
10628
|
+
const normalized = [];
|
|
10629
|
+
for (const op of ops) {
|
|
10630
|
+
if (op.type === "retain" && op.count === 0) continue;
|
|
10631
|
+
if ((op.type === "insert" || op.type === "delete") && op.text.length === 0) continue;
|
|
10632
|
+
const last = normalized.at(-1);
|
|
10633
|
+
if (last?.type === op.type) {
|
|
10634
|
+
if (op.type === "retain" && last.type === "retain") last.count += op.count;
|
|
10635
|
+
if (op.type === "insert" && last.type === "insert") last.text += op.text;
|
|
10636
|
+
if (op.type === "delete" && last.type === "delete") last.text += op.text;
|
|
10637
|
+
continue;
|
|
10638
|
+
}
|
|
10639
|
+
normalized.push({ ...op });
|
|
11243
10640
|
}
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
|
|
11247
|
-
|
|
11248
|
-
|
|
11249
|
-
|
|
10641
|
+
return normalized;
|
|
10642
|
+
}
|
|
10643
|
+
function diffToPatchOps(base, result) {
|
|
10644
|
+
if (base === result) return [];
|
|
10645
|
+
let prefix = 0;
|
|
10646
|
+
while (prefix < base.length && prefix < result.length && base[prefix] === result[prefix]) prefix += 1;
|
|
10647
|
+
let suffix = 0;
|
|
10648
|
+
while (suffix < base.length - prefix && suffix < result.length - prefix && base[base.length - 1 - suffix] === result[result.length - 1 - suffix]) {
|
|
10649
|
+
suffix += 1;
|
|
10650
|
+
}
|
|
10651
|
+
const tokenRefined = diffMiddleByTokens(base, result, prefix, suffix);
|
|
10652
|
+
if (tokenRefined) return tokenRefined;
|
|
10653
|
+
const refined = diffMiddleByLines(base, result, prefix, suffix);
|
|
10654
|
+
if (refined) return refined;
|
|
10655
|
+
return singleWindowPatchOps(base, result, prefix, suffix);
|
|
10656
|
+
}
|
|
10657
|
+
function singleWindowPatchOps(base, result, prefix, suffix) {
|
|
10658
|
+
const ops = [];
|
|
10659
|
+
if (prefix > 0) ops.push({ type: "retain", count: prefix });
|
|
10660
|
+
const deleted = base.slice(prefix, base.length - suffix);
|
|
10661
|
+
const inserted = result.slice(prefix, result.length - suffix);
|
|
10662
|
+
if (deleted.length > 0) ops.push({ type: "delete", text: deleted });
|
|
10663
|
+
if (inserted.length > 0) ops.push({ type: "insert", text: inserted });
|
|
10664
|
+
if (suffix > 0) ops.push({ type: "retain", count: suffix });
|
|
10665
|
+
return normalizePatchOps(ops);
|
|
10666
|
+
}
|
|
10667
|
+
function diffMiddleByLines(base, result, prefix, suffix) {
|
|
10668
|
+
const baseMiddle = base.slice(prefix, base.length - suffix);
|
|
10669
|
+
const resultMiddle = result.slice(prefix, result.length - suffix);
|
|
10670
|
+
if (!baseMiddle.includes("\n") && !resultMiddle.includes("\n")) return void 0;
|
|
10671
|
+
const baseLines = splitLinesPreservingNewline(baseMiddle);
|
|
10672
|
+
const resultLines = splitLinesPreservingNewline(resultMiddle);
|
|
10673
|
+
if (baseLines.length + resultLines.length > 800) return void 0;
|
|
10674
|
+
if (!hasSharedLine(baseLines, resultLines)) return void 0;
|
|
10675
|
+
const middleOps = lineDiffOps(baseLines, resultLines);
|
|
10676
|
+
if (middleOps.length <= 2) return void 0;
|
|
10677
|
+
const ops = [];
|
|
10678
|
+
if (prefix > 0) ops.push({ type: "retain", count: prefix });
|
|
10679
|
+
ops.push(...middleOps);
|
|
10680
|
+
if (suffix > 0) ops.push({ type: "retain", count: suffix });
|
|
10681
|
+
return normalizePatchOps(ops);
|
|
10682
|
+
}
|
|
10683
|
+
function diffMiddleByTokens(base, result, prefix, suffix) {
|
|
10684
|
+
const baseMiddle = base.slice(prefix, base.length - suffix);
|
|
10685
|
+
const resultMiddle = result.slice(prefix, result.length - suffix);
|
|
10686
|
+
if (!baseMiddle || !resultMiddle) return void 0;
|
|
10687
|
+
const baseTokens = tokenizePatchMiddle(baseMiddle);
|
|
10688
|
+
const resultTokens = tokenizePatchMiddle(resultMiddle);
|
|
10689
|
+
if (baseTokens.length + resultTokens.length > 1200) return void 0;
|
|
10690
|
+
if (!hasSharedToken(baseTokens, resultTokens)) return void 0;
|
|
10691
|
+
const middleOps = tokenDiffOps(baseTokens, resultTokens);
|
|
10692
|
+
if (!middleOps.some((op) => op.type === "retain")) return void 0;
|
|
10693
|
+
const ops = [];
|
|
10694
|
+
if (prefix > 0) ops.push({ type: "retain", count: prefix });
|
|
10695
|
+
ops.push(...middleOps);
|
|
10696
|
+
if (suffix > 0) ops.push({ type: "retain", count: suffix });
|
|
10697
|
+
const normalized = normalizePatchOps(ops);
|
|
10698
|
+
return normalized.length > 3 ? normalized : void 0;
|
|
10699
|
+
}
|
|
10700
|
+
function tokenizePatchMiddle(text2) {
|
|
10701
|
+
return text2.match(/[\p{L}\p{N}_]+|\s+|[^\s\p{L}\p{N}_]+/gu) ?? [];
|
|
10702
|
+
}
|
|
10703
|
+
function hasSharedToken(first, second) {
|
|
10704
|
+
const secondSet = new Set(second);
|
|
10705
|
+
return first.some((token) => secondSet.has(token));
|
|
10706
|
+
}
|
|
10707
|
+
function tokenDiffOps(baseTokens, resultTokens) {
|
|
10708
|
+
const lcs = tokenLcsTable(baseTokens, resultTokens);
|
|
10709
|
+
const ops = [];
|
|
10710
|
+
let baseIndex = 0;
|
|
10711
|
+
let resultIndex = 0;
|
|
10712
|
+
while (baseIndex < baseTokens.length || resultIndex < resultTokens.length) {
|
|
10713
|
+
if (baseIndex < baseTokens.length && resultIndex < resultTokens.length && baseTokens[baseIndex] === resultTokens[resultIndex]) {
|
|
10714
|
+
ops.push({ type: "retain", count: baseTokens[baseIndex].length });
|
|
10715
|
+
baseIndex += 1;
|
|
10716
|
+
resultIndex += 1;
|
|
10717
|
+
continue;
|
|
10718
|
+
}
|
|
10719
|
+
if (resultIndex >= resultTokens.length || baseIndex < baseTokens.length && lcs[baseIndex + 1][resultIndex] >= lcs[baseIndex][resultIndex + 1]) {
|
|
10720
|
+
ops.push({ type: "delete", text: baseTokens[baseIndex] });
|
|
10721
|
+
baseIndex += 1;
|
|
10722
|
+
continue;
|
|
10723
|
+
}
|
|
10724
|
+
ops.push({ type: "insert", text: resultTokens[resultIndex] });
|
|
10725
|
+
resultIndex += 1;
|
|
11250
10726
|
}
|
|
11251
|
-
|
|
11252
|
-
|
|
10727
|
+
return normalizePatchOps(ops);
|
|
10728
|
+
}
|
|
10729
|
+
function tokenLcsTable(baseTokens, resultTokens) {
|
|
10730
|
+
const table2 = Array.from({ length: baseTokens.length + 1 }, () => Array.from({ length: resultTokens.length + 1 }, () => 0));
|
|
10731
|
+
for (let baseIndex = baseTokens.length - 1; baseIndex >= 0; baseIndex -= 1) {
|
|
10732
|
+
for (let resultIndex = resultTokens.length - 1; resultIndex >= 0; resultIndex -= 1) {
|
|
10733
|
+
table2[baseIndex][resultIndex] = baseTokens[baseIndex] === resultTokens[resultIndex] ? table2[baseIndex + 1][resultIndex + 1] + 1 : Math.max(table2[baseIndex + 1][resultIndex], table2[baseIndex][resultIndex + 1]);
|
|
10734
|
+
}
|
|
11253
10735
|
}
|
|
11254
|
-
|
|
11255
|
-
|
|
10736
|
+
return table2;
|
|
10737
|
+
}
|
|
10738
|
+
function splitLinesPreservingNewline(text2) {
|
|
10739
|
+
return text2.match(/[^\n]*\n|[^\n]+/g) ?? [];
|
|
10740
|
+
}
|
|
10741
|
+
function hasSharedLine(first, second) {
|
|
10742
|
+
const secondSet = new Set(second);
|
|
10743
|
+
return first.some((line) => secondSet.has(line));
|
|
10744
|
+
}
|
|
10745
|
+
function lineDiffOps(baseLines, resultLines) {
|
|
10746
|
+
const lcs = lineLcsTable(baseLines, resultLines);
|
|
10747
|
+
const ops = [];
|
|
10748
|
+
let baseIndex = 0;
|
|
10749
|
+
let resultIndex = 0;
|
|
10750
|
+
while (baseIndex < baseLines.length || resultIndex < resultLines.length) {
|
|
10751
|
+
if (baseIndex < baseLines.length && resultIndex < resultLines.length && baseLines[baseIndex] === resultLines[resultIndex]) {
|
|
10752
|
+
ops.push({ type: "retain", count: baseLines[baseIndex].length });
|
|
10753
|
+
baseIndex += 1;
|
|
10754
|
+
resultIndex += 1;
|
|
10755
|
+
continue;
|
|
10756
|
+
}
|
|
10757
|
+
if (resultIndex >= resultLines.length || baseIndex < baseLines.length && lcs[baseIndex + 1][resultIndex] >= lcs[baseIndex][resultIndex + 1]) {
|
|
10758
|
+
ops.push({ type: "delete", text: baseLines[baseIndex] });
|
|
10759
|
+
baseIndex += 1;
|
|
10760
|
+
continue;
|
|
10761
|
+
}
|
|
10762
|
+
ops.push({ type: "insert", text: resultLines[resultIndex] });
|
|
10763
|
+
resultIndex += 1;
|
|
11256
10764
|
}
|
|
11257
|
-
|
|
11258
|
-
|
|
10765
|
+
return normalizePatchOps(ops);
|
|
10766
|
+
}
|
|
10767
|
+
function lineLcsTable(baseLines, resultLines) {
|
|
10768
|
+
const table2 = Array.from({ length: baseLines.length + 1 }, () => Array.from({ length: resultLines.length + 1 }, () => 0));
|
|
10769
|
+
for (let baseIndex = baseLines.length - 1; baseIndex >= 0; baseIndex -= 1) {
|
|
10770
|
+
for (let resultIndex = resultLines.length - 1; resultIndex >= 0; resultIndex -= 1) {
|
|
10771
|
+
table2[baseIndex][resultIndex] = baseLines[baseIndex] === resultLines[resultIndex] ? table2[baseIndex + 1][resultIndex + 1] + 1 : Math.max(table2[baseIndex + 1][resultIndex], table2[baseIndex][resultIndex + 1]);
|
|
10772
|
+
}
|
|
11259
10773
|
}
|
|
11260
|
-
|
|
11261
|
-
|
|
10774
|
+
return table2;
|
|
10775
|
+
}
|
|
10776
|
+
function validatePatchOpsShape(ops) {
|
|
10777
|
+
if (!Array.isArray(ops)) throw new MdocsError("validation_error", "Patch ops must be an array.");
|
|
10778
|
+
for (const op of ops) {
|
|
10779
|
+
if (op.type === "retain") {
|
|
10780
|
+
if (!Number.isInteger(op.count) || op.count < 0) {
|
|
10781
|
+
throw new MdocsError("validation_error", "Patch retain count must be a non-negative integer.");
|
|
10782
|
+
}
|
|
10783
|
+
continue;
|
|
10784
|
+
}
|
|
10785
|
+
if (op.type === "insert" || op.type === "delete") {
|
|
10786
|
+
if (typeof op.text !== "string") throw new MdocsError("validation_error", `Patch ${op.type} text must be a string.`);
|
|
10787
|
+
continue;
|
|
10788
|
+
}
|
|
10789
|
+
throw new MdocsError("validation_error", `Unknown patch op type: ${op.type}.`);
|
|
11262
10790
|
}
|
|
11263
|
-
}
|
|
10791
|
+
}
|
|
11264
10792
|
|
|
11265
10793
|
// src/agent.ts
|
|
11266
|
-
var CLI_VERSION = "0.3.
|
|
10794
|
+
var CLI_VERSION = "0.3.24";
|
|
11267
10795
|
var CLI_PACKAGE_NAME = "@magic-markdown/cli";
|
|
11268
10796
|
var AGENT_COMMANDS = [
|
|
11269
10797
|
{
|
|
@@ -11351,12 +10879,12 @@ var AGENT_COMMANDS = [
|
|
|
11351
10879
|
"Use remote status --json immediately after joining when the handoff includes expected scope/root/doc values. If it reports wrong_binding, stop and ask for the correct share or connector.",
|
|
11352
10880
|
"Use remote context --summary --json first on file-scoped joins and before reading large documents. It returns metadata, heading line numbers, current head, review counts, and suggested next commands without dumping Markdown.",
|
|
11353
10881
|
"remote context accepts --start-line and --end-line (1-based, inclusive) to page through large documents. The response always includes totalLines, startLine, and endLine \u2014 if totalLines > endLine, request the next page with --start-line <endLine+1>.",
|
|
11354
|
-
"Use remote context --no-review when reading document content only; use remote review to list open comments
|
|
10882
|
+
"Use remote context --no-review when reading document content only; use remote review to list authority-projected open comments and suggestions separately.",
|
|
11355
10883
|
"remote map works on project and workspace-scoped joins. Workspace-scoped map lists every shared Home root; target duplicate paths as <rootId>:<path-or-docId>.",
|
|
11356
10884
|
"remote graph requires a project-scoped join and returns the shared project's Obsidian-style Markdown/wikilink graph.",
|
|
11357
10885
|
"remote create-file and remote move-file require a project-scoped join and edit access. They write through the root sync API, so version history and conflict handling stay intact.",
|
|
11358
10886
|
"remote library, create-folder, update-folder, move-root, and invite-folder use the organization suite on top of the joined project. Folder invites require admin access on the joined root and can only target folders containing that root.",
|
|
11359
|
-
"remote comment/suggest never clobber concurrent edits:
|
|
10887
|
+
"remote comment/suggest never clobber concurrent edits: comments, suggestions, accept, and reject go through DocumentAuthority.",
|
|
11360
10888
|
"remote reject <suggestionId> withdraws a suggestion you submitted (for example, a duplicate). You can always withdraw your own; rejecting another author's suggestion requires edit access, and resolving a human's review work needs the user's explicit ask.",
|
|
11361
10889
|
"remote events returns truncated: true when older events were dropped from the bounded log; refetch with remote context --summary, then read needed content pages and remote review instead of trusting the gap.",
|
|
11362
10890
|
"Agents have suggest-only access by default: comments and suggestions work, direct content rewrites are rejected with a forbidden_role error unless a human grants edit access.",
|
|
@@ -11365,7 +10893,7 @@ var AGENT_COMMANDS = [
|
|
|
11365
10893
|
},
|
|
11366
10894
|
{
|
|
11367
10895
|
name: "doctor",
|
|
11368
|
-
summary: "Validate the workspace map
|
|
10896
|
+
summary: "Validate the workspace map and agent interface readiness.",
|
|
11369
10897
|
usage: "mdocs doctor --json",
|
|
11370
10898
|
output: "json",
|
|
11371
10899
|
mutates: true,
|
|
@@ -11393,7 +10921,7 @@ var AGENT_COMMANDS = [
|
|
|
11393
10921
|
},
|
|
11394
10922
|
{
|
|
11395
10923
|
name: "context",
|
|
11396
|
-
summary: "Return agent-ready
|
|
10924
|
+
summary: "Return agent-ready clean Markdown, image references, and resolved outgoing links.",
|
|
11397
10925
|
usage: "mdocs context <path|docId> [--summary] [--no-review] [--start-line N] [--end-line N] --json",
|
|
11398
10926
|
output: "json",
|
|
11399
10927
|
mutates: true,
|
|
@@ -11407,12 +10935,12 @@ var AGENT_COMMANDS = [
|
|
|
11407
10935
|
"--summary returns metadata, heading line numbers, review counts, image counts, and link counts without returning Markdown.",
|
|
11408
10936
|
"The response always includes totalLines, startLine, and endLine. If totalLines > endLine, request the next page with --start-line <endLine+1>.",
|
|
11409
10937
|
"--start-line and --end-line are 1-based and inclusive. Omitting both returns the full document.",
|
|
11410
|
-
"--no-review omits
|
|
10938
|
+
"--no-review omits authority-review redirect metadata from content reads. Use remote review for comments and suggestions."
|
|
11411
10939
|
]
|
|
11412
10940
|
},
|
|
11413
10941
|
{
|
|
11414
10942
|
name: "review",
|
|
11415
|
-
summary: "Return
|
|
10943
|
+
summary: "Return local authority-review redirect metadata for one document without returning Markdown.",
|
|
11416
10944
|
usage: "mdocs review <path|docId> --json",
|
|
11417
10945
|
output: "json",
|
|
11418
10946
|
mutates: true,
|
|
@@ -11420,82 +10948,12 @@ var AGENT_COMMANDS = [
|
|
|
11420
10948
|
},
|
|
11421
10949
|
{
|
|
11422
10950
|
name: "state",
|
|
11423
|
-
summary: "Return
|
|
10951
|
+
summary: "Return clean Markdown document state for one document.",
|
|
11424
10952
|
usage: "mdocs state <path|docId> --json",
|
|
11425
10953
|
output: "json",
|
|
11426
10954
|
mutates: true,
|
|
11427
10955
|
examples: ["mdocs state docs/example.md --json"]
|
|
11428
10956
|
},
|
|
11429
|
-
{
|
|
11430
|
-
name: "comment",
|
|
11431
|
-
summary: "Create a sidecar comment without modifying clean Markdown.",
|
|
11432
|
-
usage: "mdocs comment <path|docId> --range <start:end> --body <text> --json",
|
|
11433
|
-
output: "json",
|
|
11434
|
-
mutates: true,
|
|
11435
|
-
examples: [
|
|
11436
|
-
'mdocs comment docs/example.md --range 3:5 --body "Check this claim." --json',
|
|
11437
|
-
'mdocs comment docs/example.md --range 3:5 --body-file /tmp/comment.txt --actor agent_local --actor-kind agent --actor-name "Local Agent" --json'
|
|
11438
|
-
],
|
|
11439
|
-
notes: ["--range is 1-based and inclusive, validated against the current document line count."]
|
|
11440
|
-
},
|
|
11441
|
-
{
|
|
11442
|
-
name: "suggest",
|
|
11443
|
-
summary: "Create a reviewMarkdown replacement suggestion without modifying canonical Markdown.",
|
|
11444
|
-
usage: "mdocs suggest <path|docId> --range <start:end> --with <markdown> --message <text> --json",
|
|
11445
|
-
output: "json",
|
|
11446
|
-
mutates: true,
|
|
11447
|
-
examples: [
|
|
11448
|
-
'mdocs suggest docs/example.md --range 4:4 --with "Better line." --message "Tighten wording." --json',
|
|
11449
|
-
'mdocs suggest docs/example.md --range 4:9 --with-file /tmp/replacement.md --message-file /tmp/message.txt --actor agent_local --actor-kind agent --actor-name "Local Agent" --json'
|
|
11450
|
-
],
|
|
11451
|
-
notes: [
|
|
11452
|
-
"--range is 1-based and inclusive, validated against the current document line count.",
|
|
11453
|
-
"The replacement text replaces the whole range; re-read context first so the range matches current content.",
|
|
11454
|
-
"Prefer the smallest coherent range for the intended revision. If only one sentence inside a line changes, keep the rest of that line identical; use longer ranges only when the broader rewrite is genuinely needed."
|
|
11455
|
-
]
|
|
11456
|
-
},
|
|
11457
|
-
{
|
|
11458
|
-
name: "comments",
|
|
11459
|
-
summary: "List all comment threads for a document.",
|
|
11460
|
-
usage: "mdocs comments <path|docId> --json",
|
|
11461
|
-
output: "json",
|
|
11462
|
-
mutates: true,
|
|
11463
|
-
examples: ["mdocs comments docs/example.md --json"]
|
|
11464
|
-
},
|
|
11465
|
-
{
|
|
11466
|
-
name: "suggestions",
|
|
11467
|
-
summary: "List all suggestions for a document.",
|
|
11468
|
-
usage: "mdocs suggestions <path|docId> --json",
|
|
11469
|
-
output: "json",
|
|
11470
|
-
mutates: true,
|
|
11471
|
-
examples: ["mdocs suggestions docs/example.md --json"]
|
|
11472
|
-
},
|
|
11473
|
-
{
|
|
11474
|
-
name: "accept",
|
|
11475
|
-
summary: "Apply and accept an existing suggestion.",
|
|
11476
|
-
usage: "mdocs accept <suggestionId> --json",
|
|
11477
|
-
output: "json",
|
|
11478
|
-
mutates: true,
|
|
11479
|
-
examples: ["mdocs accept suggestion_abc123 --json"],
|
|
11480
|
-
notes: ["Only use when the user explicitly asks to apply a suggestion."]
|
|
11481
|
-
},
|
|
11482
|
-
{
|
|
11483
|
-
name: "reject",
|
|
11484
|
-
summary: "Reject an existing suggestion.",
|
|
11485
|
-
usage: "mdocs reject <suggestionId> --json",
|
|
11486
|
-
output: "json",
|
|
11487
|
-
mutates: true,
|
|
11488
|
-
examples: ["mdocs reject suggestion_abc123 --json"],
|
|
11489
|
-
notes: ["Only use when the user explicitly asks to reject a suggestion."]
|
|
11490
|
-
},
|
|
11491
|
-
{
|
|
11492
|
-
name: "resolve-comment",
|
|
11493
|
-
summary: "Resolve an existing comment thread.",
|
|
11494
|
-
usage: "mdocs resolve-comment <commentId> --json",
|
|
11495
|
-
output: "json",
|
|
11496
|
-
mutates: true,
|
|
11497
|
-
examples: ["mdocs resolve-comment comment_abc123 --json"]
|
|
11498
|
-
},
|
|
11499
10957
|
{
|
|
11500
10958
|
name: "status",
|
|
11501
10959
|
summary: "Return local Git status plus the Magic Markdown workspace map.",
|
|
@@ -11534,34 +10992,36 @@ var AGENT_COMMANDS = [
|
|
|
11534
10992
|
},
|
|
11535
10993
|
{
|
|
11536
10994
|
name: "bridge setup",
|
|
11537
|
-
summary: "Initialize, validate,
|
|
11538
|
-
usage: "mdocs bridge setup --workspace <id> --root <path> --root-id <id> [--folder-id <id>] --url <base-url> --actor-name <agent-name> --
|
|
10995
|
+
summary: "Initialize, validate, claim, and start an agent filesystem bridge.",
|
|
10996
|
+
usage: "mdocs bridge setup --workspace <id> --root <path> --root-id <id> [--folder-id <id>] --url <base-url> --actor-name <agent-name> --claim <claim-token>",
|
|
11539
10997
|
output: "long-running",
|
|
11540
10998
|
mutates: true,
|
|
11541
10999
|
examples: [
|
|
11542
|
-
"mdocs bridge setup --workspace workspace_abc --root . --root-id agentfs-hermes --url https://magic.example.com --actor-name Hermes --
|
|
11000
|
+
"mdocs bridge setup --workspace workspace_abc --root . --root-id agentfs-hermes --url https://magic.example.com --actor-name Hermes --claim mdocsclaim_abc123"
|
|
11543
11001
|
],
|
|
11544
11002
|
notes: [
|
|
11545
11003
|
"Runs init and doctor before starting the bridge.",
|
|
11546
|
-
"--
|
|
11004
|
+
"--claim exchanges a one-time binding claim for the durable bridge token over HTTPS, then stores the durable token in the local CLI profile.",
|
|
11005
|
+
"--request-token remains available as a legacy human-approval fallback, but Magic binding packets should prefer --claim.",
|
|
11547
11006
|
"--token (or MDOCS_BRIDGE_TOKEN) still works when a scoped bridge token is already available."
|
|
11548
11007
|
]
|
|
11549
11008
|
},
|
|
11550
11009
|
{
|
|
11551
11010
|
name: "bridge resume",
|
|
11552
11011
|
summary: "Backfill missed Magic changes after an agent restart, then keep the bridge running.",
|
|
11553
|
-
usage: "mdocs bridge resume --root <path> [--url <base-url>] [--workspace <id>] [--root-id <id>] [--
|
|
11012
|
+
usage: "mdocs bridge resume --root <path> [--url <base-url>] [--workspace <id>] [--root-id <id>] [--forever] [--once]",
|
|
11554
11013
|
output: "long-running",
|
|
11555
11014
|
mutates: true,
|
|
11556
11015
|
examples: [
|
|
11557
|
-
"mdocs bridge resume --root . --forever
|
|
11558
|
-
"mdocs bridge resume --root . --once
|
|
11016
|
+
"mdocs bridge resume --root . --forever",
|
|
11017
|
+
"mdocs bridge resume --root . --once"
|
|
11559
11018
|
],
|
|
11560
11019
|
notes: [
|
|
11561
11020
|
"Reads non-secret connection defaults from .mdocs/bridge.json written by bridge setup.",
|
|
11021
|
+
"Reads the durable bridge token from the local CLI profile written by claim-backed bridge setup.",
|
|
11562
11022
|
"Use this after a sandbox/container/agent session restarts. It backfills canonical Magic changes before publishing local edits.",
|
|
11563
11023
|
"--forever is the intended startup/sandbox command: keep it running for live collaboration and liveness heartbeats.",
|
|
11564
|
-
"
|
|
11024
|
+
"If the local CLI profile token is missing, rerun bridge setup from a fresh Magic binding packet. Do not ask users to paste bridge tokens by default.",
|
|
11565
11025
|
"--once performs only the backfill/reconcile step and exits."
|
|
11566
11026
|
]
|
|
11567
11027
|
},
|
|
@@ -11580,18 +11040,19 @@ var AGENT_COMMANDS = [
|
|
|
11580
11040
|
"Run this after bridge setup has written .mdocs/bridge.json.",
|
|
11581
11041
|
"Sandbox providers should put the returned startupCommand in their on-start hook.",
|
|
11582
11042
|
"--install installs and starts the generated launchd/systemd user service when the host supports it.",
|
|
11583
|
-
"The generated runner stores no bridge token;
|
|
11043
|
+
"The generated runner stores no bridge token; it reuses the durable token saved by bridge setup in the local CLI profile."
|
|
11584
11044
|
]
|
|
11585
11045
|
},
|
|
11586
11046
|
{
|
|
11587
11047
|
name: "bridge",
|
|
11588
11048
|
summary: "Sync a local Markdown root with a Magic workspace over WebSocket (long-running).",
|
|
11589
|
-
usage: "mdocs bridge --workspace <id> --root <path> --url <base-url> --
|
|
11049
|
+
usage: "mdocs bridge --workspace <id> --root <path> --url <base-url> --claim <claim-token>",
|
|
11590
11050
|
output: "long-running",
|
|
11591
11051
|
mutates: true,
|
|
11592
|
-
examples: ["mdocs bridge --workspace workspace_abc --root . --root-id root_abc --url https://magic.example.com --
|
|
11052
|
+
examples: ["mdocs bridge --workspace workspace_abc --root . --root-id root_abc --url https://magic.example.com --claim mdocsclaim_abc123"],
|
|
11593
11053
|
notes: [
|
|
11594
|
-
"--
|
|
11054
|
+
"--claim exchanges a one-time binding claim for the durable bridge token over HTTPS, then stores the durable token in the local CLI profile.",
|
|
11055
|
+
"--request-token remains available as a legacy human-approval fallback.",
|
|
11595
11056
|
"--token (or MDOCS_BRIDGE_TOKEN) still works when a scoped bridge token is already available.",
|
|
11596
11057
|
"--url (or MDOCS_BASE_URL) is required; the bridge never assumes a localhost server."
|
|
11597
11058
|
]
|
|
@@ -11623,14 +11084,14 @@ function getAgentGuidePayload() {
|
|
|
11623
11084
|
function getAgentSkillMarkdown() {
|
|
11624
11085
|
return `---
|
|
11625
11086
|
name: magic-markdown
|
|
11626
|
-
description: Use the Magic Markdown CLI or MCP server to read
|
|
11087
|
+
description: Use the Magic Markdown CLI or MCP server to read clean Markdown workspaces, and use remote authority commands for comments, suggestions, accept, and reject. Use when an agent needs to interact with Magic Markdown documents, library folders, image context, checkpoints, remote review state, or local source sync.
|
|
11627
11088
|
metadata:
|
|
11628
11089
|
short-description: Operate Magic Markdown via CLI/MCP
|
|
11629
11090
|
---
|
|
11630
11091
|
|
|
11631
11092
|
# Magic Markdown Agent Workflow
|
|
11632
11093
|
|
|
11633
|
-
Magic Markdown keeps Markdown files clean
|
|
11094
|
+
Magic Markdown keeps Markdown files clean. Review state lives in DocumentAuthority: comments, suggestions, accept, and reject must use \`mdocs remote ...\` or the remote MCP connector, not local \`.mdocs\` files.
|
|
11634
11095
|
|
|
11635
11096
|
## Install / Bootstrap
|
|
11636
11097
|
|
|
@@ -11650,11 +11111,11 @@ Run \`mdocs help <command>\` (or \`mdocs <command> --help\`) for usage, examples
|
|
|
11650
11111
|
4. Run \`mdocs map --json\` or \`mdocs remote map --json\` to list documents, paths, docIds, open comments, open suggestions, anchor review counts, and link counts. File-scoped joins (most share links) cover a single document, so \`mdocs remote map\` is unavailable for them \u2014 use \`mdocs remote context --summary --json\` instead. Project-scoped joins cover one root; workspace-scoped joins cover Home and can target duplicate paths as \`<rootId>:<path-or-docId>\`.
|
|
11651
11112
|
5. Run \`mdocs graph --json\` or project-scoped \`mdocs remote graph --json\` before broad edits to inspect the Obsidian-style document graph built from Markdown links and wikilinks.
|
|
11652
11113
|
6. For a document, run \`mdocs context <path|docId> --summary --json\` locally or \`mdocs remote context <path|docId> --summary --json\` for a joined share before reading full content. Then page Markdown with \`--start-line\` / \`--end-line\` and \`--no-review\` when you only need document text.
|
|
11653
|
-
7. Pull review state separately with \`mdocs review <path|docId> --json\` locally or \`mdocs remote review <path|docId> --json\` for a joined share. Use \`mdocs comment\` / \`mdocs remote comment\` for review notes and \`mdocs suggest\` / \`mdocs remote suggest\` for proposed replacements. Do not hand-edit canonical Markdown files or
|
|
11114
|
+
7. Pull review state separately with \`mdocs review <path|docId> --json\` locally or \`mdocs remote review <path|docId> --json\` for a joined share. Use \`mdocs comment\` / \`mdocs remote comment\` for review notes and \`mdocs suggest\` / \`mdocs remote suggest\` for proposed replacements. Do not hand-edit canonical Markdown files or local review metadata directly.
|
|
11654
11115
|
|
|
11655
11116
|
## Filesystem Bridge / Resume
|
|
11656
11117
|
|
|
11657
|
-
When a human asks you to bind an editable Markdown filesystem to Magic Markdown, use the latest official package and the
|
|
11118
|
+
When a human asks you to bind an editable Markdown filesystem to Magic Markdown, use the latest official package and the one-time claim from the binding packet. The claim is not the durable bridge token; the CLI exchanges it over HTTPS and stores the durable token in the local CLI profile.
|
|
11658
11119
|
|
|
11659
11120
|
Bind the path-addressable Markdown root. Some agent platforms expose a native or projected filesystem backed by SQLite or another virtual store; treat that projection as normal files and run setup from the projected directory. Never point \`--root\` at the SQLite database file or raw state tables. If the platform only exposes agent state through APIs or tables, first mirror the intended Markdown files into a path-addressable workspace directory, then bind that directory.
|
|
11660
11121
|
|
|
@@ -11665,18 +11126,18 @@ npx --yes --package=@magic-markdown/cli@latest mdocs bridge setup \\
|
|
|
11665
11126
|
--root-id <root-id> \\
|
|
11666
11127
|
--url <magic-url> \\
|
|
11667
11128
|
--actor-name "<agent name>" \\
|
|
11668
|
-
--
|
|
11129
|
+
--claim <claim-token>
|
|
11669
11130
|
\`\`\`
|
|
11670
11131
|
|
|
11671
|
-
|
|
11132
|
+
Keep the bridge process running. Success looks like \`mdocs bridge connected ...\`.
|
|
11672
11133
|
|
|
11673
|
-
If your sandbox, container, terminal, or agent session restarts after setup, do not ask for a raw bridge token. Start the latest CLI again and resume from the same root:
|
|
11134
|
+
If your sandbox, container, terminal, or agent session restarts after setup, do not ask for a raw bridge token or a new approval. Start the latest CLI again and resume from the same root:
|
|
11674
11135
|
|
|
11675
11136
|
\`\`\`bash
|
|
11676
|
-
npx --yes --package=@magic-markdown/cli@latest mdocs bridge resume --root . --forever
|
|
11137
|
+
npx --yes --package=@magic-markdown/cli@latest mdocs bridge resume --root . --forever
|
|
11677
11138
|
\`\`\`
|
|
11678
11139
|
|
|
11679
|
-
\`bridge resume --forever\` reads non-secret defaults from \`.mdocs/bridge.json\`, backfills missed Magic changes before publishing local edits, and then keeps polling/watching with liveness heartbeats. Use \`--once\` only when the user asked for a one-shot backfill rather than a live bridge.
|
|
11140
|
+
\`bridge resume --forever\` reads non-secret defaults from \`.mdocs/bridge.json\`, reads the durable bridge token from the local CLI profile, backfills missed Magic changes before publishing local edits, and then keeps polling/watching with liveness heartbeats. Use \`--once\` only when the user asked for a one-shot backfill rather than a live bridge. If the local profile token is missing, rerun \`bridge setup\` from a fresh Magic binding packet.
|
|
11680
11141
|
|
|
11681
11142
|
For sandboxes, VPSs, or local machines that support startup hooks, generate a durable runner after setup:
|
|
11682
11143
|
|
|
@@ -11684,13 +11145,13 @@ For sandboxes, VPSs, or local machines that support startup hooks, generate a du
|
|
|
11684
11145
|
npx --yes --package=@magic-markdown/cli@latest mdocs bridge startup --root . --json
|
|
11685
11146
|
\`\`\`
|
|
11686
11147
|
|
|
11687
|
-
Put the returned \`startupCommand\` into the provider's on-start hook. On local macOS or systemd Linux hosts, \`mdocs bridge startup --root . --install\` writes and starts a user service. The generated runner stores no bridge token;
|
|
11148
|
+
Put the returned \`startupCommand\` into the provider's on-start hook. On local macOS or systemd Linux hosts, \`mdocs bridge startup --root . --install\` writes and starts a user service. The generated runner stores no bridge token; it reuses the durable token saved by \`bridge setup\` in the local CLI profile.
|
|
11688
11149
|
|
|
11689
11150
|
If Magic and the local root both changed the same document while you were offline, the bridge keeps the Magic canonical path, saves your local divergent version as a \`.conflict-...\` Markdown copy, and lets the conflict copy sync as a normal file. If Magic reports a server-side conflict, wait for the human to resolve it in Magic Markdown before retrying content pushes.
|
|
11690
11151
|
|
|
11691
11152
|
## Editing Rules
|
|
11692
11153
|
|
|
11693
|
-
- Comments
|
|
11154
|
+
- Comments and suggestions are review operations. They should not modify canonical Markdown until a suggestion is accepted through authority.
|
|
11694
11155
|
- \`--range <start:end>\` is 1-based and inclusive, and validated against the current document: read the document first and quote line numbers from what you just read.
|
|
11695
11156
|
- Prefer the smallest coherent suggestion range that contains the actual change. If one sentence, list item, table row, or short paragraph changes, suggest that unit rather than replacing surrounding unchanged paragraphs or sections.
|
|
11696
11157
|
- Broader paragraph, section, or multi-section rewrites are appropriate when the edit genuinely changes structure, ordering, transitions, or multiple interdependent ideas. Do not avoid a long edit when it is the clearest correct revision.
|
|
@@ -11875,14 +11336,14 @@ async function runDoctor(io, workspaceRoot) {
|
|
|
11875
11336
|
details: `${map.docs.length} Markdown document${map.docs.length === 1 ? "" : "s"} indexed.`
|
|
11876
11337
|
},
|
|
11877
11338
|
{
|
|
11878
|
-
name: "
|
|
11339
|
+
name: "authority_projection_mapping",
|
|
11879
11340
|
ok: warnings.length === 0,
|
|
11880
|
-
details: warnings.length === 0 ? "
|
|
11341
|
+
details: warnings.length === 0 ? "No authority projection mapping warnings." : `${warnings.length} document(s) have review targets needing projection attention.`
|
|
11881
11342
|
},
|
|
11882
11343
|
{
|
|
11883
11344
|
name: "agent_cli_contract",
|
|
11884
11345
|
ok: AGENT_COMMANDS.length > 0,
|
|
11885
|
-
details: "Agent guide, JSON commands, checkpoints, document context,
|
|
11346
|
+
details: "Agent guide, JSON commands, checkpoints, clean document context, and MCP stdio are available. Review mutations require remote authority commands."
|
|
11886
11347
|
}
|
|
11887
11348
|
];
|
|
11888
11349
|
return {
|
|
@@ -11944,6 +11405,14 @@ var NodeWorkspaceIO = class {
|
|
|
11944
11405
|
return false;
|
|
11945
11406
|
}
|
|
11946
11407
|
}
|
|
11408
|
+
async statFile(path) {
|
|
11409
|
+
try {
|
|
11410
|
+
const stats = await stat2(await this.resolveExistingPath(path));
|
|
11411
|
+
return { mtimeMs: stats.mtimeMs, size: stats.size };
|
|
11412
|
+
} catch {
|
|
11413
|
+
return void 0;
|
|
11414
|
+
}
|
|
11415
|
+
}
|
|
11947
11416
|
async mkdir(path) {
|
|
11948
11417
|
const target = await this.resolvePath(path);
|
|
11949
11418
|
await mkdir2(target, { recursive: true });
|
|
@@ -11998,6 +11467,9 @@ var PathMappedWorkspaceIO = class {
|
|
|
11998
11467
|
async exists(path) {
|
|
11999
11468
|
return this.local.exists(this.toReplicaPath(path));
|
|
12000
11469
|
}
|
|
11470
|
+
async statFile(path) {
|
|
11471
|
+
return this.local.statFile(this.toReplicaPath(path));
|
|
11472
|
+
}
|
|
12001
11473
|
async mkdir(path) {
|
|
12002
11474
|
await this.local.mkdir(this.toReplicaPath(path));
|
|
12003
11475
|
}
|
|
@@ -12095,28 +11567,6 @@ function optionalString(args, name) {
|
|
|
12095
11567
|
const value = args[name];
|
|
12096
11568
|
return typeof value === "string" ? value : void 0;
|
|
12097
11569
|
}
|
|
12098
|
-
function requiredNumber(args, name) {
|
|
12099
|
-
const value = args[name];
|
|
12100
|
-
const number = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
12101
|
-
if (!Number.isFinite(number)) throw new Error(`Missing numeric tool argument: ${name}`);
|
|
12102
|
-
return number;
|
|
12103
|
-
}
|
|
12104
|
-
function actorFromArgs(args, fallback) {
|
|
12105
|
-
const id = optionalString(args, "actorId") ?? fallback.id;
|
|
12106
|
-
const name = optionalString(args, "actorName") ?? fallback.name;
|
|
12107
|
-
const email = optionalString(args, "actorEmail") ?? fallback.email;
|
|
12108
|
-
const kind = actorKindFromArg(args.actorKind) ?? fallback.kind;
|
|
12109
|
-
return {
|
|
12110
|
-
id,
|
|
12111
|
-
kind,
|
|
12112
|
-
name,
|
|
12113
|
-
...email ? { email } : {}
|
|
12114
|
-
};
|
|
12115
|
-
}
|
|
12116
|
-
function actorKindFromArg(value) {
|
|
12117
|
-
if (value === "human" || value === "agent" || value === "system") return value;
|
|
12118
|
-
return void 0;
|
|
12119
|
-
}
|
|
12120
11570
|
|
|
12121
11571
|
// src/mcp-resources.ts
|
|
12122
11572
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
@@ -12177,19 +11627,12 @@ async function contextForDocument(io, path, startLine, endLine, options = {}) {
|
|
|
12177
11627
|
markdown,
|
|
12178
11628
|
images: withMcpImageResources(io, state).images,
|
|
12179
11629
|
links: state.links,
|
|
12180
|
-
...options.includeReview === false ? {} :
|
|
11630
|
+
...options.includeReview === false ? {} : { review: reviewStateForDocument(state) }
|
|
12181
11631
|
};
|
|
12182
11632
|
}
|
|
12183
11633
|
async function reviewForDocument(io, path) {
|
|
12184
11634
|
return reviewStateForDocument(await getDocumentState(io, path));
|
|
12185
11635
|
}
|
|
12186
|
-
function reviewFields(state) {
|
|
12187
|
-
return {
|
|
12188
|
-
comments: state.sidecar.comments.filter((comment2) => comment2.status === "open"),
|
|
12189
|
-
suggestions: state.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
|
|
12190
|
-
anchors: state.anchors
|
|
12191
|
-
};
|
|
12192
|
-
}
|
|
12193
11636
|
function imageResourceUri(docId, index) {
|
|
12194
11637
|
return `mdocs://documents/${docId}/images/${index}`;
|
|
12195
11638
|
}
|
|
@@ -12230,25 +11673,6 @@ var documentPathSchema = {
|
|
|
12230
11673
|
required: ["path"],
|
|
12231
11674
|
additionalProperties: false
|
|
12232
11675
|
};
|
|
12233
|
-
var actorProperties = {
|
|
12234
|
-
actorId: {
|
|
12235
|
-
type: "string",
|
|
12236
|
-
description: "Optional actor id to record in sidecar metadata."
|
|
12237
|
-
},
|
|
12238
|
-
actorName: {
|
|
12239
|
-
type: "string",
|
|
12240
|
-
description: "Optional actor display name to record in sidecar metadata."
|
|
12241
|
-
},
|
|
12242
|
-
actorKind: {
|
|
12243
|
-
type: "string",
|
|
12244
|
-
enum: ["human", "agent", "system"],
|
|
12245
|
-
description: "Optional actor kind to record in sidecar metadata."
|
|
12246
|
-
},
|
|
12247
|
-
actorEmail: {
|
|
12248
|
-
type: "string",
|
|
12249
|
-
description: "Optional actor email to record in sidecar metadata."
|
|
12250
|
-
}
|
|
12251
|
-
};
|
|
12252
11676
|
var tools = [
|
|
12253
11677
|
{
|
|
12254
11678
|
name: "mdocs_map",
|
|
@@ -12267,7 +11691,7 @@ var tools = [
|
|
|
12267
11691
|
},
|
|
12268
11692
|
{
|
|
12269
11693
|
name: "mdocs_context",
|
|
12270
|
-
description: "Return agent-ready
|
|
11694
|
+
description: "Return agent-ready clean Markdown and resolved outgoing links. Use summary=true first to inspect title, line count, headings, current head, and review/link/image counts without dumping the full document. Review mutations require the remote MCP connector or mdocs remote commands.",
|
|
12271
11695
|
inputSchema: {
|
|
12272
11696
|
type: "object",
|
|
12273
11697
|
properties: {
|
|
@@ -12286,7 +11710,7 @@ var tools = [
|
|
|
12286
11710
|
},
|
|
12287
11711
|
includeReview: {
|
|
12288
11712
|
type: "boolean",
|
|
12289
|
-
description: "Whether to include
|
|
11713
|
+
description: "Whether to include authority-review redirect metadata in the content response (default: true). Use false to keep document reads minimal."
|
|
12290
11714
|
}
|
|
12291
11715
|
},
|
|
12292
11716
|
required: ["path"],
|
|
@@ -12295,74 +11719,14 @@ var tools = [
|
|
|
12295
11719
|
},
|
|
12296
11720
|
{
|
|
12297
11721
|
name: "mdocs_review",
|
|
12298
|
-
description: "Return
|
|
11722
|
+
description: "Return local review redirect metadata for one document. Use remote MCP or mdocs remote review for authority-backed comments and suggestions.",
|
|
12299
11723
|
inputSchema: documentPathSchema
|
|
12300
11724
|
},
|
|
12301
11725
|
{
|
|
12302
11726
|
name: "mdocs_state",
|
|
12303
|
-
description: "Return
|
|
11727
|
+
description: "Return clean Markdown document state and metadata for one document.",
|
|
12304
11728
|
inputSchema: documentPathSchema
|
|
12305
11729
|
},
|
|
12306
|
-
{
|
|
12307
|
-
name: "mdocs_comment",
|
|
12308
|
-
description: "Create a sidecar comment without modifying clean Markdown.",
|
|
12309
|
-
inputSchema: {
|
|
12310
|
-
type: "object",
|
|
12311
|
-
properties: {
|
|
12312
|
-
path: documentPathSchema.properties.path,
|
|
12313
|
-
startLine: { type: "number", description: "1-based start line." },
|
|
12314
|
-
endLine: { type: "number", description: "1-based end line." },
|
|
12315
|
-
body: { type: "string", description: "Comment body." },
|
|
12316
|
-
...actorProperties
|
|
12317
|
-
},
|
|
12318
|
-
required: ["path", "startLine", "endLine", "body"],
|
|
12319
|
-
additionalProperties: false
|
|
12320
|
-
}
|
|
12321
|
-
},
|
|
12322
|
-
{
|
|
12323
|
-
name: "mdocs_suggest",
|
|
12324
|
-
description: "Create a reviewMarkdown replacement suggestion without modifying canonical Markdown. Prefer the smallest coherent range; use longer ranges only when the broader rewrite is genuinely needed.",
|
|
12325
|
-
inputSchema: {
|
|
12326
|
-
type: "object",
|
|
12327
|
-
properties: {
|
|
12328
|
-
path: documentPathSchema.properties.path,
|
|
12329
|
-
startLine: { type: "number", description: "1-based start line." },
|
|
12330
|
-
endLine: { type: "number", description: "1-based end line." },
|
|
12331
|
-
replacement: { type: "string", description: "Exact replacement Markdown for the line range; keep unchanged surrounding text identical for narrow edits." },
|
|
12332
|
-
message: { type: "string", description: "Short explanation for the suggestion." },
|
|
12333
|
-
...actorProperties
|
|
12334
|
-
},
|
|
12335
|
-
required: ["path", "startLine", "endLine", "replacement"],
|
|
12336
|
-
additionalProperties: false
|
|
12337
|
-
}
|
|
12338
|
-
},
|
|
12339
|
-
{
|
|
12340
|
-
name: "mdocs_resolve_comment",
|
|
12341
|
-
description: "Resolve a sidecar comment thread.",
|
|
12342
|
-
inputSchema: {
|
|
12343
|
-
type: "object",
|
|
12344
|
-
properties: {
|
|
12345
|
-
commentId: { type: "string", description: "Comment thread id." },
|
|
12346
|
-
...actorProperties
|
|
12347
|
-
},
|
|
12348
|
-
required: ["commentId"],
|
|
12349
|
-
additionalProperties: false
|
|
12350
|
-
}
|
|
12351
|
-
},
|
|
12352
|
-
{
|
|
12353
|
-
name: "mdocs_resolve_suggestion",
|
|
12354
|
-
description: "Accept or reject a suggestion.",
|
|
12355
|
-
inputSchema: {
|
|
12356
|
-
type: "object",
|
|
12357
|
-
properties: {
|
|
12358
|
-
suggestionId: { type: "string", description: "Suggestion id." },
|
|
12359
|
-
status: { type: "string", enum: ["accepted", "rejected"], description: "Resolution status." },
|
|
12360
|
-
...actorProperties
|
|
12361
|
-
},
|
|
12362
|
-
required: ["suggestionId", "status"],
|
|
12363
|
-
additionalProperties: false
|
|
12364
|
-
}
|
|
12365
|
-
},
|
|
12366
11730
|
{
|
|
12367
11731
|
name: "mdocs_diff",
|
|
12368
11732
|
description: "Return local Git diff for a workspace path.",
|
|
@@ -12557,38 +11921,8 @@ async function callTool(io, name, args) {
|
|
|
12557
11921
|
}
|
|
12558
11922
|
if (name === "mdocs_review") return reviewForDocument(io, requiredString(args, "path"));
|
|
12559
11923
|
if (name === "mdocs_state") return withMcpImageResources(io, await getDocumentState(io, requiredString(args, "path")));
|
|
12560
|
-
if (name === "mdocs_comment") {
|
|
12561
|
-
|
|
12562
|
-
io,
|
|
12563
|
-
requiredString(args, "path"),
|
|
12564
|
-
{ startLine: requiredNumber(args, "startLine"), endLine: requiredNumber(args, "endLine") },
|
|
12565
|
-
requiredString(args, "body"),
|
|
12566
|
-
actorFromArgs(args, defaultActor())
|
|
12567
|
-
);
|
|
12568
|
-
}
|
|
12569
|
-
if (name === "mdocs_suggest") {
|
|
12570
|
-
return addSuggestion(
|
|
12571
|
-
io,
|
|
12572
|
-
requiredString(args, "path"),
|
|
12573
|
-
{ startLine: requiredNumber(args, "startLine"), endLine: requiredNumber(args, "endLine") },
|
|
12574
|
-
requiredString(args, "replacement"),
|
|
12575
|
-
optionalString(args, "message") ?? "Suggested edit",
|
|
12576
|
-
actorFromArgs(args, {
|
|
12577
|
-
id: "agent_local",
|
|
12578
|
-
kind: "agent",
|
|
12579
|
-
name: "Local Agent"
|
|
12580
|
-
})
|
|
12581
|
-
);
|
|
12582
|
-
}
|
|
12583
|
-
if (name === "mdocs_resolve_suggestion") {
|
|
12584
|
-
const suggestionId = requiredString(args, "suggestionId");
|
|
12585
|
-
const status = requiredString(args, "status");
|
|
12586
|
-
if (status === "accepted") return acceptSuggestion(io, suggestionId, actorFromArgs(args, defaultActor()));
|
|
12587
|
-
if (status === "rejected") return rejectSuggestion(io, suggestionId, actorFromArgs(args, defaultActor()));
|
|
12588
|
-
throw new Error("status must be accepted or rejected");
|
|
12589
|
-
}
|
|
12590
|
-
if (name === "mdocs_resolve_comment") {
|
|
12591
|
-
return resolveComment(io, requiredString(args, "commentId"), actorFromArgs(args, defaultActor()));
|
|
11924
|
+
if (name === "mdocs_comment" || name === "mdocs_suggest" || name === "mdocs_resolve_suggestion" || name === "mdocs_resolve_comment") {
|
|
11925
|
+
throw new Error("Local MCP review mutations were removed. Use the remote MCP connector or `mdocs remote ...` so review state goes through DocumentAuthority.");
|
|
12592
11926
|
}
|
|
12593
11927
|
if (name === "mdocs_diff") {
|
|
12594
11928
|
return { patch: await gitDiff(io.root, optionalString(args, "path")) };
|
|
@@ -12799,33 +12133,47 @@ function splitWorkspaceDocumentTarget(value, rootIds) {
|
|
|
12799
12133
|
const target = value.slice(separator + 1);
|
|
12800
12134
|
return target ? { rootId, target } : void 0;
|
|
12801
12135
|
}
|
|
12802
|
-
async function
|
|
12803
|
-
|
|
12804
|
-
`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/
|
|
12136
|
+
async function postSuggestionBranch(record, docId, input, actor) {
|
|
12137
|
+
return fetchJson(
|
|
12138
|
+
`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/suggestions`,
|
|
12139
|
+
{
|
|
12140
|
+
method: "POST",
|
|
12141
|
+
headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
|
|
12142
|
+
body: JSON.stringify({ actor, ...input })
|
|
12143
|
+
}
|
|
12144
|
+
);
|
|
12145
|
+
}
|
|
12146
|
+
async function fetchAuthorityState(record, docId) {
|
|
12147
|
+
return fetchJson(
|
|
12148
|
+
`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/authority-state`,
|
|
12149
|
+
{ headers: shareHeaders(record.shareUrl) }
|
|
12150
|
+
);
|
|
12151
|
+
}
|
|
12152
|
+
async function postResolveSuggestionBranch(record, docId, suggestionId, resolution, actor, changeId = randomUUID2()) {
|
|
12153
|
+
return fetchJson(
|
|
12154
|
+
`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/suggestions/${encodeURIComponent(suggestionId)}/${resolution}`,
|
|
12805
12155
|
{
|
|
12806
12156
|
method: "POST",
|
|
12807
12157
|
headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
|
|
12808
|
-
body: JSON.stringify({ actor, changeId
|
|
12158
|
+
body: JSON.stringify({ actor, changeId })
|
|
12809
12159
|
}
|
|
12810
12160
|
);
|
|
12811
|
-
return response.document;
|
|
12812
12161
|
}
|
|
12813
|
-
async function
|
|
12162
|
+
async function postAuthorityComment(record, docId, input, actor) {
|
|
12814
12163
|
return fetchJson(
|
|
12815
|
-
`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/
|
|
12164
|
+
`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/comments`,
|
|
12816
12165
|
{
|
|
12817
12166
|
method: "POST",
|
|
12818
12167
|
headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
|
|
12819
|
-
body: JSON.stringify({ actor,
|
|
12168
|
+
body: JSON.stringify({ actor, ...input })
|
|
12820
12169
|
}
|
|
12821
12170
|
);
|
|
12822
12171
|
}
|
|
12823
|
-
async function pushDocument(record, baseDocument, markdown
|
|
12172
|
+
async function pushDocument(record, baseDocument, markdown) {
|
|
12824
12173
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12825
12174
|
const payload = {
|
|
12826
12175
|
...baseDocument,
|
|
12827
12176
|
markdown,
|
|
12828
|
-
sidecar,
|
|
12829
12177
|
baseHead: baseDocument.currentSha,
|
|
12830
12178
|
currentSha: baseDocument.currentSha
|
|
12831
12179
|
};
|
|
@@ -13024,7 +12372,7 @@ function parseRange(value) {
|
|
|
13024
12372
|
}
|
|
13025
12373
|
|
|
13026
12374
|
// src/remote-join-store.ts
|
|
13027
|
-
import { createHash } from "node:crypto";
|
|
12375
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
13028
12376
|
import { mkdir as mkdir3, readFile as readFile5, readdir as readdir3, writeFile as writeFile3 } from "node:fs/promises";
|
|
13029
12377
|
import { join as join3 } from "node:path";
|
|
13030
12378
|
async function readSelectedJoin(root, flags) {
|
|
@@ -13058,7 +12406,7 @@ async function writeJoinRecord(root, record) {
|
|
|
13058
12406
|
`, "utf8");
|
|
13059
12407
|
}
|
|
13060
12408
|
function joinIdFor(value) {
|
|
13061
|
-
return `join_${
|
|
12409
|
+
return `join_${createHash2("sha256").update(value).digest("hex").slice(0, 16)}`;
|
|
13062
12410
|
}
|
|
13063
12411
|
function looksLikeUrl(value) {
|
|
13064
12412
|
try {
|
|
@@ -13078,6 +12426,7 @@ function joinStoreDir(root) {
|
|
|
13078
12426
|
}
|
|
13079
12427
|
|
|
13080
12428
|
// src/remote.ts
|
|
12429
|
+
var WORKSPACE_GRAPH_SCHEMA_VERSION = 1;
|
|
13081
12430
|
async function runJoinCommand(root, parsed) {
|
|
13082
12431
|
const target = parsed.command[1];
|
|
13083
12432
|
const existingId = typeof parsed.flags.id === "string" ? parsed.flags.id : void 0;
|
|
@@ -13128,7 +12477,7 @@ async function runRemoteCommand(root, subcommand, parsed) {
|
|
|
13128
12477
|
if (record.scope === "workspace") return remoteWorkspaceMap(record);
|
|
13129
12478
|
const rootRecord = assertRootScoped(record, "map");
|
|
13130
12479
|
await refreshPresence(rootRecord, record.docId);
|
|
13131
|
-
return fetchTree(rootRecord);
|
|
12480
|
+
return treeWithAuthorityCounts(rootRecord, await fetchTree(rootRecord));
|
|
13132
12481
|
}
|
|
13133
12482
|
case "graph":
|
|
13134
12483
|
return remoteGraph(record);
|
|
@@ -13222,28 +12571,35 @@ async function remoteGraph(record) {
|
|
|
13222
12571
|
assertProjectScope(record, "graph");
|
|
13223
12572
|
const rootRecord = assertRootScoped(record, "graph");
|
|
13224
12573
|
await refreshPresence(rootRecord, record.docId);
|
|
13225
|
-
const tree = await fetchTree(rootRecord);
|
|
12574
|
+
const tree = await treeWithAuthorityCounts(rootRecord, await fetchTree(rootRecord));
|
|
13226
12575
|
const documents = await Promise.all(tree.docs.map((doc) => fetchDocument(rootScopedRecordFor(record, rootRecord.rootId), doc.docId)));
|
|
12576
|
+
const authorityCounts = new Map(tree.docs.map((doc) => [doc.docId, doc]));
|
|
13227
12577
|
const lookup = documents.map((doc) => ({ docId: doc.docId, path: doc.path, title: doc.title }));
|
|
13228
12578
|
const graphDocuments = documents.map((doc) => {
|
|
13229
12579
|
const links = doc.links ?? resolveMarkdownLinks(extractMarkdownLinks(doc.markdown), doc, lookup);
|
|
12580
|
+
const counts = authorityCounts.get(doc.docId);
|
|
13230
12581
|
return {
|
|
13231
12582
|
docId: doc.docId,
|
|
13232
12583
|
path: doc.path,
|
|
13233
12584
|
title: doc.title,
|
|
13234
|
-
openComments:
|
|
13235
|
-
openSuggestions:
|
|
13236
|
-
anchorsNeedingReview:
|
|
12585
|
+
openComments: counts?.openComments ?? 0,
|
|
12586
|
+
openSuggestions: counts?.openSuggestions ?? 0,
|
|
12587
|
+
anchorsNeedingReview: counts?.anchorsNeedingReview ?? 0,
|
|
13237
12588
|
externalLinks: links.filter((link2) => link2.status === "external").length,
|
|
13238
12589
|
unresolvedLinks: links.filter((link2) => link2.status === "unresolved" || link2.status === "ambiguous").length,
|
|
13239
12590
|
links
|
|
13240
12591
|
};
|
|
13241
12592
|
});
|
|
13242
|
-
return buildWorkspaceGraph(record.workspaceId,
|
|
12593
|
+
return buildWorkspaceGraph(record.workspaceId, WORKSPACE_GRAPH_SCHEMA_VERSION, graphDocuments);
|
|
13243
12594
|
}
|
|
13244
12595
|
async function remoteWorkspaceMap(record) {
|
|
13245
12596
|
const roots = await fetchWorkspaceRoots(record);
|
|
13246
|
-
const trees = await Promise.all(
|
|
12597
|
+
const trees = await Promise.all(
|
|
12598
|
+
roots.map(async (root) => {
|
|
12599
|
+
const rootRecord = rootScopedRecordFor(record, root.rootId);
|
|
12600
|
+
return treeWithAuthorityCounts(rootRecord, await fetchTree(rootRecord));
|
|
12601
|
+
})
|
|
12602
|
+
);
|
|
13247
12603
|
const library = await fetchWorkspaceLibrary(record).catch(() => void 0);
|
|
13248
12604
|
return {
|
|
13249
12605
|
workspaceId: record.workspaceId,
|
|
@@ -13267,6 +12623,27 @@ async function remoteWorkspaceMap(record) {
|
|
|
13267
12623
|
...library ? { folders: library.folders } : {}
|
|
13268
12624
|
};
|
|
13269
12625
|
}
|
|
12626
|
+
async function treeWithAuthorityCounts(record, tree) {
|
|
12627
|
+
const counts = await Promise.all(
|
|
12628
|
+
tree.docs.map(async (doc) => ({
|
|
12629
|
+
docId: doc.docId,
|
|
12630
|
+
counts: authorityReviewCounts(await fetchAuthorityState(record, doc.docId).catch(() => void 0))
|
|
12631
|
+
}))
|
|
12632
|
+
);
|
|
12633
|
+
const countsByDocId = new Map(counts.map((entry) => [entry.docId, entry.counts]));
|
|
12634
|
+
return {
|
|
12635
|
+
...tree,
|
|
12636
|
+
docs: tree.docs.map((doc) => {
|
|
12637
|
+
const review = countsByDocId.get(doc.docId);
|
|
12638
|
+
return review ? {
|
|
12639
|
+
...doc,
|
|
12640
|
+
openComments: review.openComments,
|
|
12641
|
+
openSuggestions: review.openSuggestions,
|
|
12642
|
+
anchorsNeedingReview: review.anchorsNeedingReview
|
|
12643
|
+
} : doc;
|
|
12644
|
+
})
|
|
12645
|
+
};
|
|
12646
|
+
}
|
|
13270
12647
|
async function remoteHistory(record, flags) {
|
|
13271
12648
|
const rootRecord = assertRootScoped(record, "history");
|
|
13272
12649
|
await refreshPresence(rootRecord, record.docId);
|
|
@@ -13326,9 +12703,11 @@ async function refreshPresence(record, docId) {
|
|
|
13326
12703
|
}
|
|
13327
12704
|
async function remoteContext(record, pathOrDocId, flags = {}) {
|
|
13328
12705
|
const document = pathOrDocId ? await fetchDocument(record, pathOrDocId) : (await fetchState(record)).document;
|
|
13329
|
-
|
|
12706
|
+
const rootRecord = rootScopedRecordFor(record, document.rootId);
|
|
12707
|
+
await refreshPresence(rootRecord, document.docId);
|
|
12708
|
+
const authority = flags["no-review"] ? void 0 : await fetchAuthorityState(rootRecord, document.docId).catch(() => void 0);
|
|
13330
12709
|
if (flags.summary || flags["metadata-only"]) {
|
|
13331
|
-
return withRemoteSummaryNextCommands(
|
|
12710
|
+
return withRemoteSummaryNextCommands(summarizeDocumentContextFromAuthority(document, authority), record, document, pathOrDocId);
|
|
13332
12711
|
}
|
|
13333
12712
|
const startLine = typeof flags["start-line"] === "string" ? Number(flags["start-line"]) : void 0;
|
|
13334
12713
|
const endLine = typeof flags["end-line"] === "string" ? Number(flags["end-line"]) : void 0;
|
|
@@ -13344,22 +12723,24 @@ async function remoteContext(record, pathOrDocId, flags = {}) {
|
|
|
13344
12723
|
startLine: sl,
|
|
13345
12724
|
endLine: el,
|
|
13346
12725
|
markdown,
|
|
13347
|
-
...flags["no-review"] ? {} :
|
|
13348
|
-
comments: document.sidecar.comments.filter((comment2) => comment2.status === "open"),
|
|
13349
|
-
suggestions: document.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
|
|
13350
|
-
anchors: document.anchors
|
|
13351
|
-
},
|
|
12726
|
+
...flags["no-review"] ? {} : authorityReviewPayload(authority),
|
|
13352
12727
|
images: document.images,
|
|
13353
12728
|
links: document.links
|
|
13354
12729
|
};
|
|
13355
12730
|
}
|
|
13356
12731
|
async function remoteReview(record, pathOrDocId) {
|
|
13357
12732
|
const document = pathOrDocId ? await fetchDocument(record, pathOrDocId) : (await fetchState(record)).document;
|
|
13358
|
-
|
|
12733
|
+
const rootRecord = rootScopedRecordFor(record, document.rootId);
|
|
12734
|
+
await refreshPresence(rootRecord, document.docId);
|
|
12735
|
+
const authority = await fetchAuthorityState(rootRecord, document.docId).catch(() => void 0);
|
|
13359
12736
|
return {
|
|
13360
12737
|
joinId: record.joinId,
|
|
13361
12738
|
scope: record.scope,
|
|
13362
|
-
|
|
12739
|
+
docId: document.docId,
|
|
12740
|
+
path: document.path,
|
|
12741
|
+
title: document.title,
|
|
12742
|
+
currentSha: document.currentSha,
|
|
12743
|
+
...authorityReviewPayload(authority)
|
|
13363
12744
|
};
|
|
13364
12745
|
}
|
|
13365
12746
|
function withRemoteSummaryNextCommands(summary, record, document, requestedTarget) {
|
|
@@ -13379,6 +12760,48 @@ function remoteCommandTarget(record, document, requestedTarget) {
|
|
|
13379
12760
|
if (record.scope === "workspace") return ` ${document.rootId}:${document.docId}`;
|
|
13380
12761
|
return ` ${document.docId}`;
|
|
13381
12762
|
}
|
|
12763
|
+
function summarizeDocumentContextFromAuthority(document, authority) {
|
|
12764
|
+
const summary = summarizeDocumentContext(document);
|
|
12765
|
+
return {
|
|
12766
|
+
...summary,
|
|
12767
|
+
reviewCounts: authorityReviewCounts(authority)
|
|
12768
|
+
};
|
|
12769
|
+
}
|
|
12770
|
+
function authorityReviewPayload(authority) {
|
|
12771
|
+
return {
|
|
12772
|
+
comments: authorityOpenComments(authority),
|
|
12773
|
+
suggestions: authorityOpenSuggestions(authority),
|
|
12774
|
+
commentTargets: authority?.commentProjections ?? []
|
|
12775
|
+
};
|
|
12776
|
+
}
|
|
12777
|
+
function authorityReviewCounts(authority) {
|
|
12778
|
+
const suggestions = authorityOpenSuggestions(authority);
|
|
12779
|
+
const comments = authorityOpenComments(authority);
|
|
12780
|
+
return {
|
|
12781
|
+
openComments: comments.length,
|
|
12782
|
+
openSuggestions: suggestions.length,
|
|
12783
|
+
anchorsNeedingReview: (authority?.commentProjections ?? []).filter((projection) => authorityRecordStatus(projection) !== "mapped").length + suggestions.filter((suggestion) => {
|
|
12784
|
+
const projection = suggestion && typeof suggestion === "object" ? suggestion.projection : void 0;
|
|
12785
|
+
const status = authorityRecordStatus(projection);
|
|
12786
|
+
return status === "conflicted" || status === "stale";
|
|
12787
|
+
}).length
|
|
12788
|
+
};
|
|
12789
|
+
}
|
|
12790
|
+
function authorityOpenComments(authority) {
|
|
12791
|
+
return (authority?.comments ?? []).filter((comment2) => {
|
|
12792
|
+
const status = authorityRecordStatus(comment2);
|
|
12793
|
+
return status === void 0 || status === "open" || status === "displaced";
|
|
12794
|
+
});
|
|
12795
|
+
}
|
|
12796
|
+
function authorityOpenSuggestions(authority) {
|
|
12797
|
+
return (authority?.suggestions ?? []).filter((suggestion) => authorityRecordStatus(suggestion) === "open");
|
|
12798
|
+
}
|
|
12799
|
+
function authorityRecordId(value) {
|
|
12800
|
+
return value && typeof value === "object" && typeof value.id === "string" ? value.id : void 0;
|
|
12801
|
+
}
|
|
12802
|
+
function authorityRecordStatus(value) {
|
|
12803
|
+
return value && typeof value === "object" && typeof value.status === "string" ? value.status : void 0;
|
|
12804
|
+
}
|
|
13382
12805
|
async function remoteCreateFile(root, record, parsed) {
|
|
13383
12806
|
assertProjectScope(record, "create-file");
|
|
13384
12807
|
const rootRecord = assertRootScoped(record, "create-file");
|
|
@@ -13399,7 +12822,7 @@ async function remoteCreateFile(root, record, parsed) {
|
|
|
13399
12822
|
}
|
|
13400
12823
|
const docId = createDocId();
|
|
13401
12824
|
const title = titleFromMarkdown(normalizedPath, markdown);
|
|
13402
|
-
const
|
|
12825
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13403
12826
|
const baseDocument = {
|
|
13404
12827
|
workspaceId: rootRecord.workspaceId,
|
|
13405
12828
|
rootId: rootRecord.rootId,
|
|
@@ -13407,16 +12830,15 @@ async function remoteCreateFile(root, record, parsed) {
|
|
|
13407
12830
|
path: normalizedPath,
|
|
13408
12831
|
title,
|
|
13409
12832
|
markdown,
|
|
13410
|
-
reviewMarkdown: markdown,
|
|
13411
|
-
sidecar,
|
|
13412
|
-
anchors: [],
|
|
13413
12833
|
images: [],
|
|
13414
12834
|
links: [],
|
|
13415
12835
|
openComments: 0,
|
|
13416
12836
|
openSuggestions: 0,
|
|
12837
|
+
anchorsNeedingReview: 0,
|
|
12838
|
+
updatedAt: now,
|
|
13417
12839
|
currentSha: tree.root.canonical.head
|
|
13418
12840
|
};
|
|
13419
|
-
const document = await pushDocument(rootRecord, baseDocument, markdown
|
|
12841
|
+
const document = await pushDocument(rootRecord, baseDocument, markdown);
|
|
13420
12842
|
await recordHead(root, record, document);
|
|
13421
12843
|
return { document };
|
|
13422
12844
|
}
|
|
@@ -13441,8 +12863,7 @@ async function remoteMoveFile(root, record, parsed) {
|
|
|
13441
12863
|
const moved = await pushDocument(
|
|
13442
12864
|
rootScopedRecordFor(record, document.rootId),
|
|
13443
12865
|
{ ...document, path: nextPath, title: titleFromMarkdown(nextPath, document.markdown) },
|
|
13444
|
-
document.markdown
|
|
13445
|
-
document.sidecar
|
|
12866
|
+
document.markdown
|
|
13446
12867
|
);
|
|
13447
12868
|
await recordHead(root, record, moved);
|
|
13448
12869
|
return { document: moved };
|
|
@@ -13450,13 +12871,17 @@ async function remoteMoveFile(root, record, parsed) {
|
|
|
13450
12871
|
async function remoteComment(root, record, parsed) {
|
|
13451
12872
|
const body = await readRequiredTextFlag(parsed.flags, root, ["body", "message"]);
|
|
13452
12873
|
const range = parseRange(requiredFlag(parsed.flags, "range"));
|
|
13453
|
-
const
|
|
13454
|
-
|
|
13455
|
-
|
|
13456
|
-
|
|
13457
|
-
|
|
13458
|
-
|
|
13459
|
-
|
|
12874
|
+
const pathOrDocId = parsed.command[2] ?? record.docId;
|
|
12875
|
+
if (!pathOrDocId) {
|
|
12876
|
+
throw new CliError("usage_error", "Missing document path or docId.", {
|
|
12877
|
+
hint: "Pass a document path or docId, or join with --doc <docId>."
|
|
12878
|
+
});
|
|
12879
|
+
}
|
|
12880
|
+
const document = await fetchDocument(record, pathOrDocId);
|
|
12881
|
+
const documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
12882
|
+
await refreshPresence(documentRecord, document.docId);
|
|
12883
|
+
const result = await postAuthorityComment(documentRecord, document.docId, { ...range, body }, actorForRecord(record));
|
|
12884
|
+
return { comment: result.thread, projection: result.projection, document: { docId: document.docId, path: document.path, currentSha: document.currentSha } };
|
|
13460
12885
|
}
|
|
13461
12886
|
async function remoteSuggest(root, record, parsed) {
|
|
13462
12887
|
const replacement = await readRequiredTextFlag(parsed.flags, root, ["with", "replacement"]);
|
|
@@ -13471,15 +12896,35 @@ async function remoteSuggest(root, record, parsed) {
|
|
|
13471
12896
|
const document = await fetchDocument(record, pathOrDocId);
|
|
13472
12897
|
const documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
13473
12898
|
await refreshPresence(documentRecord, document.docId);
|
|
13474
|
-
const
|
|
12899
|
+
const authority = await fetchAuthorityState(documentRecord, document.docId);
|
|
12900
|
+
if (!authority.head) {
|
|
12901
|
+
throw new CliError("conflict", "Document authority is not initialized yet.", {
|
|
12902
|
+
hint: "Open the document once or retry after the worker initializes authority-state."
|
|
12903
|
+
});
|
|
12904
|
+
}
|
|
12905
|
+
const clientMutationId = typeof parsed.flags["client-mutation-id"] === "string" ? parsed.flags["client-mutation-id"] : typeof parsed.flags["mutation-id"] === "string" ? parsed.flags["mutation-id"] : void 0;
|
|
12906
|
+
const patch = buildLineRangePatchEnvelope({
|
|
12907
|
+
baseVersion: authority.head.headVersion,
|
|
12908
|
+
base: authority.markdown ?? document.markdown,
|
|
12909
|
+
range,
|
|
12910
|
+
replacement,
|
|
12911
|
+
clientMutationId,
|
|
12912
|
+
clientMeta: { source: "cli_remote_suggest" }
|
|
12913
|
+
});
|
|
12914
|
+
const result = await postSuggestionBranch(
|
|
13475
12915
|
documentRecord,
|
|
13476
12916
|
document.docId,
|
|
13477
|
-
{
|
|
12917
|
+
{ patch, title: message },
|
|
13478
12918
|
actorForRecord(record)
|
|
13479
12919
|
);
|
|
13480
|
-
|
|
13481
|
-
|
|
13482
|
-
|
|
12920
|
+
return {
|
|
12921
|
+
suggestion: result.suggestion,
|
|
12922
|
+
suggestionUpdate: result.update,
|
|
12923
|
+
suggestionId: result.suggestion?.id,
|
|
12924
|
+
projectionStatus: result.projection?.status,
|
|
12925
|
+
replayed: result.replayed,
|
|
12926
|
+
document: { docId: document.docId, path: document.path, currentSha: document.currentSha }
|
|
12927
|
+
};
|
|
13483
12928
|
}
|
|
13484
12929
|
async function remoteCreateFolder(record, parsed) {
|
|
13485
12930
|
assertProjectScope(record, "create-folder");
|
|
@@ -13558,53 +13003,14 @@ async function remoteReject(root, record, parsed) {
|
|
|
13558
13003
|
let documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
13559
13004
|
await refreshPresence(documentRecord, document.docId);
|
|
13560
13005
|
const actor = actorForRecord(record);
|
|
13561
|
-
const
|
|
13562
|
-
|
|
13563
|
-
|
|
13564
|
-
|
|
13565
|
-
});
|
|
13566
|
-
}
|
|
13567
|
-
if (suggestion.status !== "open") {
|
|
13568
|
-
throw new CliError("usage_error", `Suggestion ${suggestionId} is already ${suggestion.status}.`, {
|
|
13006
|
+
const authority = await fetchAuthorityState(documentRecord, document.docId).catch(() => void 0);
|
|
13007
|
+
const suggestionStatus = authorityRecordStatus(authority?.suggestions?.find((candidate) => authorityRecordId(candidate) === suggestionId));
|
|
13008
|
+
if (suggestionStatus && suggestionStatus !== "open") {
|
|
13009
|
+
throw new CliError("usage_error", `Suggestion ${suggestionId} is already ${suggestionStatus}.`, {
|
|
13569
13010
|
hint: "Only open suggestions can be rejected."
|
|
13570
13011
|
});
|
|
13571
13012
|
}
|
|
13572
|
-
|
|
13573
|
-
documentRecord,
|
|
13574
|
-
document.docId,
|
|
13575
|
-
{ kind: "reject_suggestion", baseHead: document.currentSha, payload: { suggestionId } },
|
|
13576
|
-
actor
|
|
13577
|
-
);
|
|
13578
|
-
if (result.document) await recordHead(root, record, result.document);
|
|
13579
|
-
return { suggestion: result.suggestion ?? { ...suggestion, status: "rejected" }, reviewRecord: result.reviewRecord, document: result.document };
|
|
13580
|
-
}
|
|
13581
|
-
async function submitReview(root, record, pathOrDocId, mutate) {
|
|
13582
|
-
if (!pathOrDocId) {
|
|
13583
|
-
throw new CliError("usage_error", "Missing document path or docId.", {
|
|
13584
|
-
hint: "Pass a document path or docId, or join with --doc <docId>."
|
|
13585
|
-
});
|
|
13586
|
-
}
|
|
13587
|
-
let document = await fetchDocument(record, pathOrDocId);
|
|
13588
|
-
let documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
13589
|
-
await refreshPresence(documentRecord, document.docId);
|
|
13590
|
-
const build = async (base) => {
|
|
13591
|
-
const io = new RemoteDocumentIO(base);
|
|
13592
|
-
const created = await mutate(io, base.docId);
|
|
13593
|
-
const state = await getDocumentState(io, base.docId);
|
|
13594
|
-
return {
|
|
13595
|
-
created,
|
|
13596
|
-
state,
|
|
13597
|
-
additions: {
|
|
13598
|
-
anchors: state.sidecar.anchors.filter((anchor) => anchor.id === created.anchorId),
|
|
13599
|
-
comments: state.sidecar.comments.filter((comment2) => comment2.id === created.id),
|
|
13600
|
-
reviewMarkdown: state.sidecar.reviewMarkdown
|
|
13601
|
-
}
|
|
13602
|
-
};
|
|
13603
|
-
};
|
|
13604
|
-
const attempt = await build(document);
|
|
13605
|
-
const pushed = await postReview(documentRecord, document.docId, attempt.additions, actorForRecord(record));
|
|
13606
|
-
await recordHead(root, record, pushed);
|
|
13607
|
-
return { created: attempt.created, document: pushed };
|
|
13013
|
+
return postResolveSuggestionBranch(documentRecord, document.docId, suggestionId, "reject", actor);
|
|
13608
13014
|
}
|
|
13609
13015
|
async function recordHead(root, record, document) {
|
|
13610
13016
|
await writeJoinRecord(root, { ...record, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), currentHead: document.currentSha });
|
|
@@ -13734,19 +13140,6 @@ function rootScopedRecordFor(record, rootId) {
|
|
|
13734
13140
|
scope: record.scope === "file" ? "file" : "project"
|
|
13735
13141
|
};
|
|
13736
13142
|
}
|
|
13737
|
-
function emptySidecar(docId, path, title) {
|
|
13738
|
-
return {
|
|
13739
|
-
schemaVersion: SIDECAR_SCHEMA_VERSION,
|
|
13740
|
-
docId,
|
|
13741
|
-
path,
|
|
13742
|
-
title,
|
|
13743
|
-
anchors: [],
|
|
13744
|
-
comments: [],
|
|
13745
|
-
suggestions: [],
|
|
13746
|
-
changeSets: [],
|
|
13747
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13748
|
-
};
|
|
13749
|
-
}
|
|
13750
13143
|
function defaultMarkdown(path) {
|
|
13751
13144
|
return `# ${titleFromMarkdown(path, "")}
|
|
13752
13145
|
`;
|
|
@@ -13776,11 +13169,55 @@ function optionalFolderTarget(value) {
|
|
|
13776
13169
|
}
|
|
13777
13170
|
|
|
13778
13171
|
// src/bridge.ts
|
|
13779
|
-
import { createHash as
|
|
13780
|
-
import {
|
|
13172
|
+
import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
|
|
13173
|
+
import { unlinkSync } from "node:fs";
|
|
13174
|
+
import { chmod, mkdir as mkdir4, readFile as readFile6, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
|
|
13175
|
+
import { homedir } from "node:os";
|
|
13176
|
+
import { basename as basename2, join as join4, resolve as resolve4 } from "node:path";
|
|
13781
13177
|
var BRIDGE_CONFIG_PATH = ".mdocs/bridge.json";
|
|
13782
13178
|
var BRIDGE_STATUS_PATH = ".mdocs/bridge-status.json";
|
|
13179
|
+
var BRIDGE_LOCK_PATH = ".mdocs/bridge.lock";
|
|
13783
13180
|
var BRIDGE_HEARTBEAT_INTERVAL_MS = 1e4;
|
|
13181
|
+
var BRIDGE_TOKEN_STORE_PATH = join4(homedir(), ".mdocs", "bridge-tokens.json");
|
|
13182
|
+
var BRIDGE_MIN_INTERVAL_MS = 500;
|
|
13183
|
+
var BRIDGE_DEFAULT_INTERVAL_MS = 1e3;
|
|
13184
|
+
var BRIDGE_RECONNECT_BASE_MS = 1e3;
|
|
13185
|
+
var BRIDGE_RECONNECT_CAP_MS = 3e4;
|
|
13186
|
+
function createSerialQueue() {
|
|
13187
|
+
let tail = Promise.resolve();
|
|
13188
|
+
return (task) => {
|
|
13189
|
+
const run = tail.then(task, task);
|
|
13190
|
+
tail = run.then(
|
|
13191
|
+
() => void 0,
|
|
13192
|
+
() => void 0
|
|
13193
|
+
);
|
|
13194
|
+
return run;
|
|
13195
|
+
};
|
|
13196
|
+
}
|
|
13197
|
+
function validateBridgeIntervalMs(raw) {
|
|
13198
|
+
if (typeof raw === "boolean" || raw === void 0 || raw === null || raw === "") return BRIDGE_DEFAULT_INTERVAL_MS;
|
|
13199
|
+
const value = Number(raw);
|
|
13200
|
+
if (!Number.isFinite(value) || value <= 0) return BRIDGE_DEFAULT_INTERVAL_MS;
|
|
13201
|
+
return Math.max(BRIDGE_MIN_INTERVAL_MS, Math.floor(value));
|
|
13202
|
+
}
|
|
13203
|
+
function reconnectBackoffCeiling(attempt, baseMs = BRIDGE_RECONNECT_BASE_MS, capMs = BRIDGE_RECONNECT_CAP_MS) {
|
|
13204
|
+
const exponent = Math.max(0, Math.floor(attempt));
|
|
13205
|
+
return Math.min(capMs, baseMs * 2 ** exponent);
|
|
13206
|
+
}
|
|
13207
|
+
function reconnectDelayMs(attempt, baseMs = BRIDGE_RECONNECT_BASE_MS, capMs = BRIDGE_RECONNECT_CAP_MS, random = Math.random) {
|
|
13208
|
+
return Math.floor(random() * (reconnectBackoffCeiling(attempt, baseMs, capMs) + 1));
|
|
13209
|
+
}
|
|
13210
|
+
function isConfirmingEcho(params) {
|
|
13211
|
+
if (params.messageSourceId && params.messageSourceId === params.selfSourceId) return true;
|
|
13212
|
+
return params.sentSignature !== void 0 && params.sentSignature === params.payloadSignature;
|
|
13213
|
+
}
|
|
13214
|
+
function shouldConflictCopyOnDelete(localSignature, baseSignature) {
|
|
13215
|
+
return baseSignature === void 0 || localSignature !== baseSignature;
|
|
13216
|
+
}
|
|
13217
|
+
function conflictCopyStamp(date = /* @__PURE__ */ new Date(), entropy = randomUUID3().slice(0, 6)) {
|
|
13218
|
+
const compact = date.toISOString().replaceAll(/[-:.]/g, "");
|
|
13219
|
+
return `${compact.slice(0, 15)}-${entropy}`;
|
|
13220
|
+
}
|
|
13784
13221
|
function bridgeSetupIdentity(input) {
|
|
13785
13222
|
const actorName = input.actorName?.trim() || "Agent";
|
|
13786
13223
|
return {
|
|
@@ -13838,7 +13275,7 @@ async function runBridgeResume(options) {
|
|
|
13838
13275
|
baseUrl,
|
|
13839
13276
|
intervalMs: options.intervalMs ?? config2?.intervalMs ?? 1e3,
|
|
13840
13277
|
token: options.token,
|
|
13841
|
-
requestToken: options.requestToken
|
|
13278
|
+
requestToken: Boolean(options.requestToken),
|
|
13842
13279
|
pairingTimeoutMs: options.pairingTimeoutMs,
|
|
13843
13280
|
once: options.once,
|
|
13844
13281
|
resume: true
|
|
@@ -13851,7 +13288,7 @@ async function runBridge(options) {
|
|
|
13851
13288
|
}
|
|
13852
13289
|
const localManifestSource = await readLocalSource(root);
|
|
13853
13290
|
const rootId = options.rootId ?? options.sourceId ?? localManifestSource?.sourceId ?? rootIdForPath(root);
|
|
13854
|
-
const replicaId = `replica_${
|
|
13291
|
+
const replicaId = `replica_${createHash3("sha256").update(`${root}:${options.actorId}`).digest("hex").slice(0, 12)}`;
|
|
13855
13292
|
const replicaKind = actorKindForBridge(options.actorId) === "agent" ? "agent_runtime" : "local";
|
|
13856
13293
|
const mapping = createSourceMapping({
|
|
13857
13294
|
sourceId: rootId,
|
|
@@ -13868,23 +13305,44 @@ async function runBridge(options) {
|
|
|
13868
13305
|
const pendingDocs = /* @__PURE__ */ new Set();
|
|
13869
13306
|
const pendingDeletes = /* @__PURE__ */ new Set();
|
|
13870
13307
|
const seenDocs = /* @__PURE__ */ new Set();
|
|
13308
|
+
const sentSignatures = /* @__PURE__ */ new Map();
|
|
13871
13309
|
const deniedSignatures = /* @__PURE__ */ new Map();
|
|
13310
|
+
const indexCache = /* @__PURE__ */ new Map();
|
|
13311
|
+
const indexOptions = { cache: indexCache, rootId };
|
|
13312
|
+
const runManifest = createSerialQueue();
|
|
13872
13313
|
let lastTreeSignature = "";
|
|
13873
13314
|
const claimMode = Boolean(options.claimToken || localManifestSource?.canonicalHead);
|
|
13874
13315
|
let lastAppliedHead = localManifestSource?.canonicalHead;
|
|
13875
13316
|
let socket;
|
|
13317
|
+
let pollTimer;
|
|
13318
|
+
let reconnectTimer;
|
|
13319
|
+
let ticking = false;
|
|
13320
|
+
let reconnectAttempt = 0;
|
|
13321
|
+
let suppressOpenResume = false;
|
|
13322
|
+
let claimPrimed = false;
|
|
13876
13323
|
let lastSuccessfulSyncAt;
|
|
13877
13324
|
let lastHeartbeatAt;
|
|
13878
13325
|
let lastHeartbeatSentMs = 0;
|
|
13879
13326
|
let lastError;
|
|
13327
|
+
const lock = await acquireBridgeLock(root);
|
|
13880
13328
|
await writeCurrentBridgeStatus("starting");
|
|
13881
|
-
let token = options.token;
|
|
13882
|
-
if (!token && options.requestToken) {
|
|
13329
|
+
let token = options.token ?? await readStoredBridgeToken(options, rootId).catch(() => void 0);
|
|
13330
|
+
if (!token && options.resume && !options.requestToken) {
|
|
13331
|
+
const message = "Missing stored bridge token. Re-run mdocs bridge setup from a fresh Magic binding packet.";
|
|
13332
|
+
lastError = message;
|
|
13333
|
+
await writeCurrentBridgeStatus("error");
|
|
13334
|
+
await lock.release();
|
|
13335
|
+
throw new Error(message);
|
|
13336
|
+
}
|
|
13337
|
+
if (!token && (options.claimToken || options.requestToken)) {
|
|
13883
13338
|
try {
|
|
13884
|
-
|
|
13339
|
+
const issued = await requestBridgeToken(options, rootId);
|
|
13340
|
+
token = issued.token;
|
|
13341
|
+
await writeStoredBridgeToken(options, rootId, issued).catch(() => void 0);
|
|
13885
13342
|
} catch (error) {
|
|
13886
13343
|
lastError = errorMessage(error);
|
|
13887
13344
|
await writeCurrentBridgeStatus("error");
|
|
13345
|
+
await lock.release();
|
|
13888
13346
|
throw error;
|
|
13889
13347
|
}
|
|
13890
13348
|
}
|
|
@@ -13894,6 +13352,7 @@ async function runBridge(options) {
|
|
|
13894
13352
|
} catch (error) {
|
|
13895
13353
|
lastError = errorMessage(error);
|
|
13896
13354
|
await writeCurrentBridgeStatus("error");
|
|
13355
|
+
await lock.release();
|
|
13897
13356
|
throw error;
|
|
13898
13357
|
}
|
|
13899
13358
|
lastAppliedHead = lastAppliedHead ?? registeredRoot?.canonical.head;
|
|
@@ -13913,32 +13372,54 @@ async function runBridge(options) {
|
|
|
13913
13372
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13914
13373
|
});
|
|
13915
13374
|
const backfilled = await resumeFromCanonical();
|
|
13375
|
+
suppressOpenResume = true;
|
|
13916
13376
|
if (options.once) {
|
|
13917
13377
|
lastSuccessfulSyncAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
13918
13378
|
await writeCurrentBridgeStatus("once_complete");
|
|
13379
|
+
await lock.release();
|
|
13919
13380
|
return;
|
|
13920
13381
|
}
|
|
13382
|
+
if (claimMode && !backfilled) {
|
|
13383
|
+
await primeLocalSignatures();
|
|
13384
|
+
claimPrimed = true;
|
|
13385
|
+
}
|
|
13921
13386
|
connect();
|
|
13922
|
-
|
|
13923
|
-
else await publishSnapshot("initial");
|
|
13924
|
-
const timer = setInterval(() => {
|
|
13925
|
-
void publishSnapshot("poll").catch((error) => {
|
|
13926
|
-
lastError = errorMessage(error);
|
|
13927
|
-
void writeCurrentBridgeStatus("error").catch(() => void 0);
|
|
13928
|
-
sendHeartbeat("error", lastError);
|
|
13929
|
-
process.stderr.write(`bridge poll failed: ${lastError}
|
|
13930
|
-
`);
|
|
13931
|
-
});
|
|
13932
|
-
}, options.intervalMs);
|
|
13387
|
+
scheduleNextPoll();
|
|
13933
13388
|
process.once("SIGINT", () => {
|
|
13934
|
-
|
|
13389
|
+
shutdownTimers();
|
|
13935
13390
|
sendHeartbeat("offline", void 0, false);
|
|
13936
13391
|
void writeCurrentBridgeStatus("stopped").catch(() => void 0).finally(() => {
|
|
13937
13392
|
socket?.close();
|
|
13938
|
-
process.exit(0);
|
|
13393
|
+
void lock.release().finally(() => process.exit(0));
|
|
13939
13394
|
});
|
|
13940
13395
|
});
|
|
13941
13396
|
await new Promise(() => void 0);
|
|
13397
|
+
function shutdownTimers() {
|
|
13398
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
13399
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
13400
|
+
}
|
|
13401
|
+
function scheduleNextPoll() {
|
|
13402
|
+
pollTimer = setTimeout(runPollTick, options.intervalMs);
|
|
13403
|
+
}
|
|
13404
|
+
async function runPollTick() {
|
|
13405
|
+
if (ticking) {
|
|
13406
|
+
scheduleNextPoll();
|
|
13407
|
+
return;
|
|
13408
|
+
}
|
|
13409
|
+
ticking = true;
|
|
13410
|
+
try {
|
|
13411
|
+
await publishSnapshot("poll");
|
|
13412
|
+
} catch (error) {
|
|
13413
|
+
lastError = errorMessage(error);
|
|
13414
|
+
void writeCurrentBridgeStatus("error").catch(() => void 0);
|
|
13415
|
+
sendHeartbeat("error", lastError);
|
|
13416
|
+
process.stderr.write(`bridge poll failed: ${lastError}
|
|
13417
|
+
`);
|
|
13418
|
+
} finally {
|
|
13419
|
+
ticking = false;
|
|
13420
|
+
scheduleNextPoll();
|
|
13421
|
+
}
|
|
13422
|
+
}
|
|
13942
13423
|
function connect() {
|
|
13943
13424
|
const url = new URL(`/api/workspaces/${encodeURIComponent(options.workspaceId)}/roots/${encodeURIComponent(rootId)}/sync`, options.baseUrl);
|
|
13944
13425
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
@@ -13947,11 +13428,10 @@ async function runBridge(options) {
|
|
|
13947
13428
|
if (token) url.searchParams.set("token", token);
|
|
13948
13429
|
socket = new WebSocket(url);
|
|
13949
13430
|
socket.addEventListener("open", () => {
|
|
13950
|
-
|
|
13951
|
-
|
|
13431
|
+
void onSocketOpen().catch((error) => {
|
|
13432
|
+
process.stderr.write(`bridge open handler failed: ${errorMessage(error)}
|
|
13952
13433
|
`);
|
|
13953
|
-
|
|
13954
|
-
void writeCurrentBridgeStatus("connected").catch(() => void 0);
|
|
13434
|
+
});
|
|
13955
13435
|
});
|
|
13956
13436
|
socket.addEventListener("message", (event) => {
|
|
13957
13437
|
if (typeof event.data !== "string") return;
|
|
@@ -13965,7 +13445,7 @@ async function runBridge(options) {
|
|
|
13965
13445
|
socket.addEventListener("close", () => {
|
|
13966
13446
|
socket = void 0;
|
|
13967
13447
|
void writeCurrentBridgeStatus("offline").catch(() => void 0);
|
|
13968
|
-
|
|
13448
|
+
scheduleReconnect();
|
|
13969
13449
|
});
|
|
13970
13450
|
socket.addEventListener("error", () => {
|
|
13971
13451
|
lastError = "WebSocket error";
|
|
@@ -13973,8 +13453,34 @@ async function runBridge(options) {
|
|
|
13973
13453
|
socket?.close();
|
|
13974
13454
|
});
|
|
13975
13455
|
}
|
|
13456
|
+
function scheduleReconnect() {
|
|
13457
|
+
const delayMs = reconnectDelayMs(reconnectAttempt);
|
|
13458
|
+
reconnectAttempt += 1;
|
|
13459
|
+
reconnectTimer = setTimeout(connect, delayMs);
|
|
13460
|
+
}
|
|
13461
|
+
async function onSocketOpen() {
|
|
13462
|
+
lastError = void 0;
|
|
13463
|
+
reconnectAttempt = 0;
|
|
13464
|
+
process.stdout.write(`mdocs bridge connected ${root} -> ${options.workspaceId}/${rootId}
|
|
13465
|
+
`);
|
|
13466
|
+
sendHeartbeat("synced");
|
|
13467
|
+
void writeCurrentBridgeStatus("connected").catch(() => void 0);
|
|
13468
|
+
pendingDocs.clear();
|
|
13469
|
+
pendingDeletes.clear();
|
|
13470
|
+
sentSignatures.clear();
|
|
13471
|
+
lastTreeSignature = "";
|
|
13472
|
+
if (suppressOpenResume) suppressOpenResume = false;
|
|
13473
|
+
else await resumeFromCanonical();
|
|
13474
|
+
await publishSnapshot("open");
|
|
13475
|
+
}
|
|
13476
|
+
function indexedMap() {
|
|
13477
|
+
return runManifest(() => getWorkspaceMap(io, indexOptions));
|
|
13478
|
+
}
|
|
13479
|
+
function indexedDocState(pathOrDocId) {
|
|
13480
|
+
return runManifest(() => getDocumentState(io, pathOrDocId, indexOptions));
|
|
13481
|
+
}
|
|
13976
13482
|
async function publishSnapshot(reason) {
|
|
13977
|
-
const map = await
|
|
13483
|
+
const map = await indexedMap();
|
|
13978
13484
|
const currentDocIds = new Set(map.docs.map((doc) => doc.docId));
|
|
13979
13485
|
const treeSignature = hashJson(map.docs.map((doc) => ({ docId: doc.docId, path: doc.path })));
|
|
13980
13486
|
if (treeSignature !== lastTreeSignature) {
|
|
@@ -13983,19 +13489,21 @@ async function runBridge(options) {
|
|
|
13983
13489
|
}
|
|
13984
13490
|
}
|
|
13985
13491
|
for (const doc of map.docs) {
|
|
13986
|
-
const state = await
|
|
13492
|
+
const state = await indexedDocState(doc.docId);
|
|
13987
13493
|
const payload = {
|
|
13988
13494
|
...state,
|
|
13989
13495
|
workspaceId: options.workspaceId,
|
|
13990
13496
|
rootId,
|
|
13497
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13991
13498
|
currentSha: lastAppliedHead
|
|
13992
13499
|
};
|
|
13993
|
-
const signature = documentSignature(state.markdown
|
|
13500
|
+
const signature = documentSignature(state.markdown);
|
|
13994
13501
|
if (signatures.get(doc.docId) === signature || pendingDocs.has(doc.docId)) continue;
|
|
13995
13502
|
if (deniedSignatures.get(doc.docId) === signature) continue;
|
|
13996
13503
|
if (send("file-changed", { ...payload, baseHead: lastAppliedHead, replicaId })) {
|
|
13997
13504
|
seenDocs.add(doc.docId);
|
|
13998
13505
|
pendingDocs.add(doc.docId);
|
|
13506
|
+
sentSignatures.set(doc.docId, signature);
|
|
13999
13507
|
}
|
|
14000
13508
|
}
|
|
14001
13509
|
for (const docId of seenDocs) {
|
|
@@ -14041,10 +13549,10 @@ async function runBridge(options) {
|
|
|
14041
13549
|
return true;
|
|
14042
13550
|
}
|
|
14043
13551
|
async function reconcileRemoteDocument(remote, base) {
|
|
14044
|
-
const remoteSignature = documentSignature(remote.markdown
|
|
14045
|
-
const baseSignature = base ? documentSignature(base.markdown
|
|
13552
|
+
const remoteSignature = documentSignature(remote.markdown);
|
|
13553
|
+
const baseSignature = base ? documentSignature(base.markdown) : void 0;
|
|
14046
13554
|
const localState = await localStateForCanonical(remote);
|
|
14047
|
-
const localSignature = localState ? documentSignature(localState.markdown
|
|
13555
|
+
const localSignature = localState ? documentSignature(localState.markdown) : void 0;
|
|
14048
13556
|
if (!localState) {
|
|
14049
13557
|
await applyCanonicalDocument(remote);
|
|
14050
13558
|
return "applied";
|
|
@@ -14072,24 +13580,37 @@ async function runBridge(options) {
|
|
|
14072
13580
|
return "conflict";
|
|
14073
13581
|
}
|
|
14074
13582
|
async function reconcileRemoteDelete(docId, base) {
|
|
14075
|
-
|
|
14076
|
-
|
|
14077
|
-
|
|
14078
|
-
|
|
14079
|
-
if (
|
|
13583
|
+
return applyCanonicalDelete(docId, base.path, documentSignature(base.markdown));
|
|
13584
|
+
}
|
|
13585
|
+
async function applyCanonicalDelete(docId, path, baseSignature) {
|
|
13586
|
+
let localState = await indexedDocState(docId).catch(() => void 0);
|
|
13587
|
+
if (!localState && path) {
|
|
13588
|
+
const map = await indexedMap().catch(() => void 0);
|
|
13589
|
+
const byPath = map?.docs.find((doc) => doc.path === path);
|
|
13590
|
+
if (byPath) localState = await indexedDocState(byPath.docId).catch(() => void 0);
|
|
13591
|
+
}
|
|
13592
|
+
if (!localState) {
|
|
13593
|
+
signatures.delete(docId);
|
|
13594
|
+
seenDocs.delete(docId);
|
|
13595
|
+
pendingDeletes.delete(docId);
|
|
13596
|
+
return "noop";
|
|
13597
|
+
}
|
|
13598
|
+
const localSignature = documentSignature(localState.markdown);
|
|
13599
|
+
const diverged = shouldConflictCopyOnDelete(localSignature, baseSignature);
|
|
13600
|
+
if (diverged) {
|
|
14080
13601
|
const copyPath = conflictCopyPath(localState.path);
|
|
14081
13602
|
await io.writeTextAtomic(copyPath, localState.markdown);
|
|
14082
|
-
await
|
|
13603
|
+
await seedConflictCopyDocument(copyPath, localState.markdown);
|
|
14083
13604
|
process.stderr.write(`local conflict ${localState.path}; local version saved as ${copyPath}, canonical delete applied
|
|
14084
13605
|
`);
|
|
14085
13606
|
}
|
|
14086
13607
|
await io.deleteFile(localState.path).catch(() => void 0);
|
|
14087
|
-
await io.deleteFile(sidecarPath(docId)).catch(() => void 0);
|
|
14088
13608
|
await removeManifestDocument(docId, localState.path);
|
|
14089
13609
|
signatures.delete(docId);
|
|
14090
13610
|
seenDocs.delete(docId);
|
|
13611
|
+
sentSignatures.delete(docId);
|
|
14091
13612
|
pendingDeletes.delete(docId);
|
|
14092
|
-
return
|
|
13613
|
+
return diverged ? "conflict" : "applied";
|
|
14093
13614
|
}
|
|
14094
13615
|
async function handleIncoming(message) {
|
|
14095
13616
|
if (message.type === "agent-unbound") {
|
|
@@ -14098,23 +13619,36 @@ async function runBridge(options) {
|
|
|
14098
13619
|
if (!targetsUs) return;
|
|
14099
13620
|
process.stdout.write(`mdocs bridge unbound by workspace; stopping ${root}
|
|
14100
13621
|
`);
|
|
13622
|
+
shutdownTimers();
|
|
14101
13623
|
await writeCurrentBridgeStatus("stopped").catch(() => void 0);
|
|
14102
13624
|
socket?.close();
|
|
13625
|
+
await lock.release();
|
|
14103
13626
|
process.exit(0);
|
|
14104
13627
|
}
|
|
14105
13628
|
if (message.type === "file-changed") {
|
|
14106
|
-
|
|
13629
|
+
const payload = readDocumentPayload(message.payload);
|
|
13630
|
+
const payloadSignature = documentSignature(payload.markdown);
|
|
13631
|
+
if (isConfirmingEcho({
|
|
13632
|
+
messageSourceId: message.sourceId,
|
|
13633
|
+
selfSourceId: sourceId,
|
|
13634
|
+
payloadSignature,
|
|
13635
|
+
sentSignature: sentSignatures.get(payload.docId)
|
|
13636
|
+
})) {
|
|
13637
|
+
signatures.set(payload.docId, payloadSignature);
|
|
13638
|
+
sentSignatures.delete(payload.docId);
|
|
13639
|
+
pendingDocs.delete(payload.docId);
|
|
13640
|
+
deniedSignatures.delete(payload.docId);
|
|
13641
|
+
seenDocs.add(payload.docId);
|
|
13642
|
+
lastAppliedHead = payload.canonicalHead ?? payload.currentSha ?? lastAppliedHead;
|
|
13643
|
+
await writeSourceState(lastAppliedHead);
|
|
13644
|
+
return;
|
|
13645
|
+
}
|
|
13646
|
+
await applyCanonicalDocument(payload);
|
|
14107
13647
|
}
|
|
14108
13648
|
if (message.type === "file-deleted") {
|
|
14109
13649
|
const payload = message.payload;
|
|
14110
13650
|
if (!payload.docId) return;
|
|
14111
|
-
|
|
14112
|
-
const doc = map.docs.find((candidate) => candidate.docId === payload.docId);
|
|
14113
|
-
if (doc) await io.deleteFile(doc.path);
|
|
14114
|
-
await io.deleteFile(sidecarPath(payload.docId)).catch(() => void 0);
|
|
14115
|
-
signatures.delete(payload.docId);
|
|
14116
|
-
seenDocs.delete(payload.docId);
|
|
14117
|
-
pendingDeletes.delete(payload.docId);
|
|
13651
|
+
await applyCanonicalDelete(payload.docId, payload.path, signatures.get(payload.docId));
|
|
14118
13652
|
lastAppliedHead = payload.canonicalHead ?? lastAppliedHead;
|
|
14119
13653
|
await writeSourceState(lastAppliedHead);
|
|
14120
13654
|
process.stdout.write(`applied canonical delete ${payload.docId} at ${lastAppliedHead ?? "unknown"}
|
|
@@ -14142,14 +13676,14 @@ async function runBridge(options) {
|
|
|
14142
13676
|
}
|
|
14143
13677
|
}
|
|
14144
13678
|
async function applyCanonicalDocument(payload) {
|
|
14145
|
-
const remoteSignature = documentSignature(payload.markdown
|
|
13679
|
+
const remoteSignature = documentSignature(payload.markdown);
|
|
14146
13680
|
const localState = await localStateForCanonical(payload);
|
|
14147
|
-
const localSignature = localState ? documentSignature(localState.markdown
|
|
13681
|
+
const localSignature = localState ? documentSignature(localState.markdown) : void 0;
|
|
14148
13682
|
if (localSignature && signatures.has(payload.docId) && localSignature !== signatures.get(payload.docId) && localSignature !== remoteSignature) {
|
|
14149
13683
|
const copyPath = conflictCopyPath(payload.path);
|
|
14150
13684
|
if (localState) {
|
|
14151
13685
|
await io.writeTextAtomic(copyPath, localState.markdown);
|
|
14152
|
-
await
|
|
13686
|
+
await seedConflictCopyDocument(copyPath, localState.markdown);
|
|
14153
13687
|
}
|
|
14154
13688
|
send("conflict", {
|
|
14155
13689
|
docId: payload.docId,
|
|
@@ -14166,21 +13700,13 @@ async function runBridge(options) {
|
|
|
14166
13700
|
await io.deleteFile(localState.path).catch(() => void 0);
|
|
14167
13701
|
}
|
|
14168
13702
|
if (localState && localState.docId !== payload.docId) {
|
|
14169
|
-
await io.deleteFile(sidecarPath(localState.docId)).catch(() => void 0);
|
|
14170
13703
|
await removeManifestDocument(localState.docId, localState.path);
|
|
14171
13704
|
}
|
|
14172
13705
|
await io.writeTextAtomic(payload.path, payload.markdown);
|
|
14173
|
-
await io.writeTextAtomic(sidecarPath(payload.docId), `${JSON.stringify(payload.sidecar, null, 2)}
|
|
14174
|
-
`);
|
|
14175
|
-
const reviewMarkdown = projectReviewMarkdown(payload.markdown, payload.sidecar.suggestions);
|
|
14176
|
-
if (reviewMarkdown === payload.markdown) {
|
|
14177
|
-
await io.deleteFile(reviewFilePath(payload.path)).catch(() => void 0);
|
|
14178
|
-
} else {
|
|
14179
|
-
await io.writeTextAtomic(reviewFilePath(payload.path), reviewMarkdown);
|
|
14180
|
-
}
|
|
14181
13706
|
await upsertManifestDocument(payload);
|
|
14182
13707
|
signatures.set(payload.docId, remoteSignature);
|
|
14183
13708
|
deniedSignatures.delete(payload.docId);
|
|
13709
|
+
sentSignatures.delete(payload.docId);
|
|
14184
13710
|
seenDocs.add(payload.docId);
|
|
14185
13711
|
pendingDocs.delete(payload.docId);
|
|
14186
13712
|
lastAppliedHead = payload.canonicalHead ?? payload.currentSha ?? lastAppliedHead;
|
|
@@ -14188,38 +13714,33 @@ async function runBridge(options) {
|
|
|
14188
13714
|
process.stdout.write(`applied canonical change ${payload.path} at ${lastAppliedHead ?? "unknown"}
|
|
14189
13715
|
`);
|
|
14190
13716
|
}
|
|
14191
|
-
async function
|
|
14192
|
-
|
|
14193
|
-
|
|
14194
|
-
|
|
14195
|
-
|
|
14196
|
-
|
|
14197
|
-
|
|
14198
|
-
|
|
14199
|
-
|
|
14200
|
-
|
|
14201
|
-
|
|
14202
|
-
|
|
14203
|
-
|
|
14204
|
-
|
|
14205
|
-
|
|
14206
|
-
|
|
14207
|
-
|
|
14208
|
-
|
|
14209
|
-
|
|
14210
|
-
});
|
|
14211
|
-
await writeManifest(io, {
|
|
14212
|
-
...manifest,
|
|
14213
|
-
docs: [...manifest.docs.filter((doc) => doc.path !== copyPath && doc.docId !== docId), entry],
|
|
14214
|
-
updatedAt: now
|
|
13717
|
+
async function seedConflictCopyDocument(copyPath, markdown) {
|
|
13718
|
+
await runManifest(async () => {
|
|
13719
|
+
const manifest = await readManifest(io).catch(() => void 0);
|
|
13720
|
+
if (!manifest) return;
|
|
13721
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13722
|
+
const docId = deterministicDocId(rootId, copyPath);
|
|
13723
|
+
const title = titleFromMarkdown(copyPath, markdown);
|
|
13724
|
+
const entry = {
|
|
13725
|
+
docId,
|
|
13726
|
+
path: copyPath,
|
|
13727
|
+
title,
|
|
13728
|
+
contentHash: contentHashForText(markdown),
|
|
13729
|
+
updatedAt: now
|
|
13730
|
+
};
|
|
13731
|
+
await writeManifest(io, {
|
|
13732
|
+
...manifest,
|
|
13733
|
+
docs: [...manifest.docs.filter((doc) => doc.path !== copyPath && doc.docId !== docId), entry],
|
|
13734
|
+
updatedAt: now
|
|
13735
|
+
});
|
|
14215
13736
|
});
|
|
14216
13737
|
}
|
|
14217
13738
|
async function localStateForCanonical(payload) {
|
|
14218
|
-
const byDocId = await
|
|
13739
|
+
const byDocId = await indexedDocState(payload.docId).catch(() => void 0);
|
|
14219
13740
|
if (byDocId) return byDocId;
|
|
14220
|
-
const map = await
|
|
13741
|
+
const map = await indexedMap().catch(() => void 0);
|
|
14221
13742
|
const byPath = map?.docs.find((doc) => doc.path === payload.path);
|
|
14222
|
-
return byPath ?
|
|
13743
|
+
return byPath ? indexedDocState(byPath.docId).catch(() => void 0) : void 0;
|
|
14223
13744
|
}
|
|
14224
13745
|
async function fetchCanonicalDocument2(docId) {
|
|
14225
13746
|
try {
|
|
@@ -14316,103 +13837,97 @@ async function runBridge(options) {
|
|
|
14316
13837
|
await writeBridgeStatus(root, currentBridgeStatus(state));
|
|
14317
13838
|
}
|
|
14318
13839
|
async function primeLocalSignatures() {
|
|
14319
|
-
const map = await
|
|
13840
|
+
const map = await indexedMap();
|
|
14320
13841
|
for (const doc of map.docs) {
|
|
14321
|
-
const state = await
|
|
14322
|
-
signatures.set(doc.docId, documentSignature(state.markdown
|
|
13842
|
+
const state = await indexedDocState(doc.docId);
|
|
13843
|
+
signatures.set(doc.docId, documentSignature(state.markdown));
|
|
14323
13844
|
seenDocs.add(doc.docId);
|
|
14324
13845
|
}
|
|
14325
13846
|
process.stdout.write(`mdocs bridge claimed ${root} as ${rootId} at ${lastAppliedHead ?? "unknown"}
|
|
14326
13847
|
`);
|
|
14327
13848
|
}
|
|
14328
13849
|
async function localSignatureFor(docId) {
|
|
14329
|
-
const state = await
|
|
14330
|
-
return documentSignature(state.markdown
|
|
13850
|
+
const state = await indexedDocState(docId);
|
|
13851
|
+
return documentSignature(state.markdown);
|
|
14331
13852
|
}
|
|
14332
13853
|
async function writeSourceState(head) {
|
|
14333
|
-
|
|
14334
|
-
|
|
14335
|
-
|
|
14336
|
-
|
|
14337
|
-
|
|
14338
|
-
|
|
14339
|
-
|
|
14340
|
-
|
|
14341
|
-
|
|
14342
|
-
|
|
14343
|
-
|
|
13854
|
+
await runManifest(async () => {
|
|
13855
|
+
const manifest = await indexWorkspace(io, indexOptions);
|
|
13856
|
+
await writeManifest(io, {
|
|
13857
|
+
...manifest,
|
|
13858
|
+
source: {
|
|
13859
|
+
sourceId: rootId,
|
|
13860
|
+
sourceName: options.sourceName ?? (basename2(root) || rootId),
|
|
13861
|
+
canonicalHead: head,
|
|
13862
|
+
claimedAt: manifest.source?.claimedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
13863
|
+
mappings: [{ ...mapping, lastAppliedHead: head, status: head ? "synced" : "connecting", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }]
|
|
13864
|
+
},
|
|
13865
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13866
|
+
});
|
|
14344
13867
|
});
|
|
14345
13868
|
}
|
|
14346
13869
|
async function upsertManifestDocument(payload) {
|
|
14347
|
-
|
|
14348
|
-
|
|
14349
|
-
|
|
14350
|
-
|
|
14351
|
-
|
|
14352
|
-
|
|
14353
|
-
|
|
14354
|
-
|
|
14355
|
-
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
|
|
14360
|
-
|
|
14361
|
-
|
|
14362
|
-
|
|
13870
|
+
await runManifest(async () => {
|
|
13871
|
+
const manifest = await readManifest(io).catch(() => void 0);
|
|
13872
|
+
if (!manifest) return;
|
|
13873
|
+
await writeManifest(io, {
|
|
13874
|
+
...manifest,
|
|
13875
|
+
docs: [
|
|
13876
|
+
...manifest.docs.filter((doc) => doc.docId !== payload.docId && doc.path !== payload.path),
|
|
13877
|
+
{
|
|
13878
|
+
docId: payload.docId,
|
|
13879
|
+
path: payload.path,
|
|
13880
|
+
title: payload.title || titleFromMarkdown(payload.path, payload.markdown),
|
|
13881
|
+
contentHash: contentHashForText(payload.markdown),
|
|
13882
|
+
currentSha: payload.canonicalHead ?? payload.currentSha,
|
|
13883
|
+
updatedAt: payload.updatedAt
|
|
13884
|
+
}
|
|
13885
|
+
].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0),
|
|
13886
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13887
|
+
});
|
|
14363
13888
|
});
|
|
14364
13889
|
}
|
|
14365
13890
|
async function removeManifestDocument(docId, path) {
|
|
14366
|
-
|
|
14367
|
-
|
|
14368
|
-
|
|
14369
|
-
|
|
14370
|
-
|
|
14371
|
-
|
|
13891
|
+
await runManifest(async () => {
|
|
13892
|
+
const manifest = await readManifest(io).catch(() => void 0);
|
|
13893
|
+
if (!manifest) return;
|
|
13894
|
+
await writeManifest(io, {
|
|
13895
|
+
...manifest,
|
|
13896
|
+
docs: manifest.docs.filter((doc) => doc.docId !== docId && doc.path !== path),
|
|
13897
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13898
|
+
});
|
|
14372
13899
|
});
|
|
14373
13900
|
}
|
|
14374
13901
|
}
|
|
14375
13902
|
async function requestBridgeToken(options, rootId) {
|
|
14376
|
-
const
|
|
14377
|
-
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
|
|
14381
|
-
|
|
14382
|
-
|
|
14383
|
-
|
|
14384
|
-
folderId: options.folderId,
|
|
14385
|
-
sourceName: options.sourceName,
|
|
14386
|
-
role: "edit"
|
|
14387
|
-
})
|
|
14388
|
-
}).catch((error) => {
|
|
14389
|
-
throw new Error(`Failed to create bridge approval request: ${error instanceof Error ? error.message : String(error)}`);
|
|
14390
|
-
});
|
|
14391
|
-
if (!response.ok) {
|
|
14392
|
-
throw new Error(`Failed to create bridge approval request: ${response.status} ${await response.text()}`);
|
|
14393
|
-
}
|
|
14394
|
-
const created = await response.json();
|
|
14395
|
-
if (!created.requestId || !created.pollToken || !created.approveUrl) {
|
|
14396
|
-
throw new Error("Bridge approval request response was missing requestId, pollToken, or approveUrl.");
|
|
14397
|
-
}
|
|
14398
|
-
process.stdout.write(`mdocs bridge approval needed: ${created.approveUrl}
|
|
13903
|
+
const created = options.claimToken ? await exchangeBridgeClaim(options) : await createBridgeApprovalRequest(options, rootId);
|
|
13904
|
+
if (!created.requestId || !created.pollToken) {
|
|
13905
|
+
throw new Error("Bridge token request response was missing requestId or pollToken.");
|
|
13906
|
+
}
|
|
13907
|
+
if (created.autoApproved) {
|
|
13908
|
+
process.stdout.write("mdocs bridge claim accepted. Finishing bind...\n");
|
|
13909
|
+
} else if (created.approveUrl) {
|
|
13910
|
+
process.stdout.write(`mdocs bridge approval needed: ${created.approveUrl}
|
|
14399
13911
|
`);
|
|
14400
|
-
|
|
13912
|
+
if (created.expiresAt) process.stdout.write(`Waiting for approval until ${created.expiresAt}.
|
|
14401
13913
|
`);
|
|
13914
|
+
}
|
|
14402
13915
|
const deadline = Date.now() + (options.pairingTimeoutMs ?? 1e3 * 60 * 15);
|
|
13916
|
+
let firstPoll = true;
|
|
14403
13917
|
while (Date.now() < deadline) {
|
|
14404
|
-
|
|
13918
|
+
if (firstPoll) firstPoll = false;
|
|
13919
|
+
else await delay(2e3);
|
|
14405
13920
|
const pollResponse = await fetch(new URL(`/api/bridge-requests/${encodeURIComponent(created.requestId)}/token`, options.baseUrl), {
|
|
14406
13921
|
headers: { Authorization: `Bearer ${created.pollToken}` }
|
|
14407
13922
|
}).catch((error) => {
|
|
14408
|
-
throw new Error(`Bridge
|
|
13923
|
+
throw new Error(`Bridge token polling failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
14409
13924
|
});
|
|
14410
13925
|
const payload = await pollResponse.json().catch(() => ({}));
|
|
14411
13926
|
if (pollResponse.status === 202) continue;
|
|
14412
13927
|
if (pollResponse.ok && payload.token) {
|
|
14413
|
-
process.stdout.write(`mdocs bridge
|
|
13928
|
+
process.stdout.write(`mdocs bridge token received${payload.expiresAt ? `; token expires ${payload.expiresAt}` : ""}.
|
|
14414
13929
|
`);
|
|
14415
|
-
return payload.token;
|
|
13930
|
+
return { token: payload.token, expiresAt: payload.expiresAt };
|
|
14416
13931
|
}
|
|
14417
13932
|
if (pollResponse.status === 409 && payload.status === "rejected") {
|
|
14418
13933
|
throw new Error("Bridge approval request was rejected.");
|
|
@@ -14420,9 +13935,43 @@ async function requestBridgeToken(options, rootId) {
|
|
|
14420
13935
|
if (pollResponse.status === 410 || payload.status === "expired") {
|
|
14421
13936
|
throw new Error("Bridge approval request expired before it was approved.");
|
|
14422
13937
|
}
|
|
14423
|
-
throw new Error(`Bridge
|
|
13938
|
+
throw new Error(`Bridge token polling failed with status ${pollResponse.status}.`);
|
|
13939
|
+
}
|
|
13940
|
+
throw new Error("Bridge token request timed out before a token was issued.");
|
|
13941
|
+
}
|
|
13942
|
+
async function exchangeBridgeClaim(options) {
|
|
13943
|
+
const response = await fetch(new URL("/api/bridge-claims/exchange", options.baseUrl), {
|
|
13944
|
+
method: "POST",
|
|
13945
|
+
headers: { "Content-Type": "application/json" },
|
|
13946
|
+
body: JSON.stringify({ claimToken: options.claimToken })
|
|
13947
|
+
}).catch((error) => {
|
|
13948
|
+
throw new Error(`Failed to exchange bridge claim: ${error instanceof Error ? error.message : String(error)}`);
|
|
13949
|
+
});
|
|
13950
|
+
if (!response.ok) {
|
|
13951
|
+
throw new Error(`Failed to exchange bridge claim: ${response.status} ${await response.text()}`);
|
|
13952
|
+
}
|
|
13953
|
+
return await response.json();
|
|
13954
|
+
}
|
|
13955
|
+
async function createBridgeApprovalRequest(options, rootId) {
|
|
13956
|
+
const response = await fetch(new URL("/api/bridge-requests", options.baseUrl), {
|
|
13957
|
+
method: "POST",
|
|
13958
|
+
headers: { "Content-Type": "application/json" },
|
|
13959
|
+
body: JSON.stringify({
|
|
13960
|
+
workspaceId: options.workspaceId,
|
|
13961
|
+
rootId,
|
|
13962
|
+
actorId: options.actorId,
|
|
13963
|
+
actorName: options.actorName,
|
|
13964
|
+
folderId: options.folderId,
|
|
13965
|
+
sourceName: options.sourceName,
|
|
13966
|
+
role: "edit"
|
|
13967
|
+
})
|
|
13968
|
+
}).catch((error) => {
|
|
13969
|
+
throw new Error(`Failed to create bridge approval request: ${error instanceof Error ? error.message : String(error)}`);
|
|
13970
|
+
});
|
|
13971
|
+
if (!response.ok) {
|
|
13972
|
+
throw new Error(`Failed to create bridge approval request: ${response.status} ${await response.text()}`);
|
|
14424
13973
|
}
|
|
14425
|
-
|
|
13974
|
+
return await response.json();
|
|
14426
13975
|
}
|
|
14427
13976
|
async function readBridgeConfig(root) {
|
|
14428
13977
|
const io = new NodeWorkspaceIO(root);
|
|
@@ -14448,6 +13997,49 @@ async function writeBridgeStatus(root, status) {
|
|
|
14448
13997
|
await io.writeTextAtomic(BRIDGE_STATUS_PATH, `${JSON.stringify(status, null, 2)}
|
|
14449
13998
|
`);
|
|
14450
13999
|
}
|
|
14000
|
+
async function readStoredBridgeToken(options, rootId) {
|
|
14001
|
+
const store = await readBridgeTokenStore();
|
|
14002
|
+
const entry = store.tokens[bridgeTokenStoreKey(options, rootId)];
|
|
14003
|
+
if (!entry?.token) return void 0;
|
|
14004
|
+
if (entry.expiresAt && Date.parse(entry.expiresAt) <= Date.now() + 6e4) return void 0;
|
|
14005
|
+
return entry.token;
|
|
14006
|
+
}
|
|
14007
|
+
async function writeStoredBridgeToken(options, rootId, issued) {
|
|
14008
|
+
const store = await readBridgeTokenStore();
|
|
14009
|
+
store.tokens[bridgeTokenStoreKey(options, rootId)] = {
|
|
14010
|
+
token: issued.token,
|
|
14011
|
+
expiresAt: issued.expiresAt,
|
|
14012
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14013
|
+
};
|
|
14014
|
+
await mkdir4(join4(homedir(), ".mdocs"), { recursive: true, mode: 448 });
|
|
14015
|
+
await writeFile4(BRIDGE_TOKEN_STORE_PATH, `${JSON.stringify(store, null, 2)}
|
|
14016
|
+
`, "utf8");
|
|
14017
|
+
await chmod(BRIDGE_TOKEN_STORE_PATH, 384);
|
|
14018
|
+
}
|
|
14019
|
+
async function readBridgeTokenStore() {
|
|
14020
|
+
try {
|
|
14021
|
+
const parsed = JSON.parse(await readFile6(BRIDGE_TOKEN_STORE_PATH, "utf8"));
|
|
14022
|
+
if (parsed.schemaVersion === 1 && parsed.tokens && typeof parsed.tokens === "object") {
|
|
14023
|
+
return { schemaVersion: 1, tokens: parsed.tokens };
|
|
14024
|
+
}
|
|
14025
|
+
} catch {
|
|
14026
|
+
}
|
|
14027
|
+
return { schemaVersion: 1, tokens: {} };
|
|
14028
|
+
}
|
|
14029
|
+
function bridgeTokenStoreKey(options, rootId) {
|
|
14030
|
+
return createHash3("sha256").update(JSON.stringify([normalizeBridgeBaseUrl(options.baseUrl), options.workspaceId, rootId, options.actorId])).digest("hex").slice(0, 32);
|
|
14031
|
+
}
|
|
14032
|
+
function normalizeBridgeBaseUrl(baseUrl) {
|
|
14033
|
+
try {
|
|
14034
|
+
const url = new URL(baseUrl);
|
|
14035
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
14036
|
+
url.search = "";
|
|
14037
|
+
url.hash = "";
|
|
14038
|
+
return url.toString();
|
|
14039
|
+
} catch {
|
|
14040
|
+
return baseUrl;
|
|
14041
|
+
}
|
|
14042
|
+
}
|
|
14451
14043
|
async function fetchCanonicalSnapshot(options, rootId) {
|
|
14452
14044
|
const response = await fetch(new URL(`/api/workspaces/${encodeURIComponent(options.workspaceId)}/roots/${encodeURIComponent(rootId)}/sync`, options.baseUrl), {
|
|
14453
14045
|
headers: authHeaders(options.token)
|
|
@@ -14494,7 +14086,7 @@ async function fetchCanonicalDocument(options, rootId, docId) {
|
|
|
14494
14086
|
function readCommitDocumentPayload(payload, workspaceId, rootId, headId) {
|
|
14495
14087
|
if (!payload || typeof payload !== "object") return void 0;
|
|
14496
14088
|
const value = payload;
|
|
14497
|
-
if (typeof value.markdown !== "string"
|
|
14089
|
+
if (typeof value.markdown !== "string") return void 0;
|
|
14498
14090
|
return readDocumentPayload({
|
|
14499
14091
|
workspaceId,
|
|
14500
14092
|
rootId,
|
|
@@ -14502,7 +14094,7 @@ function readCommitDocumentPayload(payload, workspaceId, rootId, headId) {
|
|
|
14502
14094
|
path: value.path,
|
|
14503
14095
|
title: value.title,
|
|
14504
14096
|
markdown: value.markdown,
|
|
14505
|
-
|
|
14097
|
+
updatedAt: value.updatedAt,
|
|
14506
14098
|
currentSha: headId,
|
|
14507
14099
|
canonicalHead: headId
|
|
14508
14100
|
});
|
|
@@ -14623,20 +14215,67 @@ function mergeReplicas(replicas, replica) {
|
|
|
14623
14215
|
return [...replicas.filter((candidate) => candidate.replicaId !== replica.replicaId), replica];
|
|
14624
14216
|
}
|
|
14625
14217
|
function rootIdForPath(path) {
|
|
14626
|
-
return `root_${
|
|
14218
|
+
return `root_${createHash3("sha256").update(path).digest("hex").slice(0, 12)}`;
|
|
14627
14219
|
}
|
|
14628
|
-
function conflictCopyPath(path) {
|
|
14629
|
-
const stamp = (/* @__PURE__ */ new Date()).toISOString().replaceAll(/[:.]/g, "").slice(0, 15);
|
|
14220
|
+
function conflictCopyPath(path, stamp = conflictCopyStamp()) {
|
|
14630
14221
|
return path.replace(/(\.[^./]+)?$/, (extension) => `.conflict-${stamp}${extension || ".md"}`);
|
|
14631
14222
|
}
|
|
14632
|
-
function
|
|
14633
|
-
|
|
14223
|
+
async function acquireBridgeLock(root) {
|
|
14224
|
+
const lockPath = join4(root, BRIDGE_LOCK_PATH);
|
|
14225
|
+
await mkdir4(join4(root, ".mdocs"), { recursive: true });
|
|
14226
|
+
const payload = `${JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
14227
|
+
`;
|
|
14228
|
+
try {
|
|
14229
|
+
await writeFile4(lockPath, payload, { flag: "wx" });
|
|
14230
|
+
} catch (error) {
|
|
14231
|
+
if (error.code !== "EEXIST") throw error;
|
|
14232
|
+
const holderPid = await readBridgeLockPid(lockPath);
|
|
14233
|
+
if (holderPid !== void 0 && isProcessAlive(holderPid)) {
|
|
14234
|
+
throw new Error(
|
|
14235
|
+
`Another mdocs bridge (pid ${holderPid}) is already running for ${root}. Stop it first or remove ${lockPath} if it is stale.`
|
|
14236
|
+
);
|
|
14237
|
+
}
|
|
14238
|
+
await writeFile4(lockPath, payload);
|
|
14239
|
+
}
|
|
14240
|
+
let released = false;
|
|
14241
|
+
const removeSync = () => {
|
|
14242
|
+
if (released) return;
|
|
14243
|
+
released = true;
|
|
14244
|
+
try {
|
|
14245
|
+
unlinkSync(lockPath);
|
|
14246
|
+
} catch {
|
|
14247
|
+
}
|
|
14248
|
+
};
|
|
14249
|
+
process.once("exit", removeSync);
|
|
14250
|
+
return {
|
|
14251
|
+
release: async () => {
|
|
14252
|
+
if (released) return;
|
|
14253
|
+
released = true;
|
|
14254
|
+
await rm3(lockPath, { force: true }).catch(() => void 0);
|
|
14255
|
+
}
|
|
14256
|
+
};
|
|
14257
|
+
}
|
|
14258
|
+
async function readBridgeLockPid(lockPath) {
|
|
14259
|
+
try {
|
|
14260
|
+
const parsed = JSON.parse(await readFile6(lockPath, "utf8"));
|
|
14261
|
+
return typeof parsed.pid === "number" && Number.isInteger(parsed.pid) ? parsed.pid : void 0;
|
|
14262
|
+
} catch {
|
|
14263
|
+
return void 0;
|
|
14264
|
+
}
|
|
14265
|
+
}
|
|
14266
|
+
function isProcessAlive(pid) {
|
|
14267
|
+
try {
|
|
14268
|
+
process.kill(pid, 0);
|
|
14269
|
+
return true;
|
|
14270
|
+
} catch (error) {
|
|
14271
|
+
return error.code === "EPERM";
|
|
14272
|
+
}
|
|
14634
14273
|
}
|
|
14635
14274
|
function hashJson(value) {
|
|
14636
|
-
return
|
|
14275
|
+
return createHash3("sha256").update(JSON.stringify(value)).digest("hex");
|
|
14637
14276
|
}
|
|
14638
|
-
function documentSignature(markdown
|
|
14639
|
-
return hashJson({ markdown
|
|
14277
|
+
function documentSignature(markdown) {
|
|
14278
|
+
return hashJson({ markdown });
|
|
14640
14279
|
}
|
|
14641
14280
|
async function readLocalSource(root) {
|
|
14642
14281
|
const io = new NodeWorkspaceIO(root);
|
|
@@ -14652,12 +14291,12 @@ function readDocumentPayload(payload) {
|
|
|
14652
14291
|
if (typeof value.docId !== "string") throw new Error("file-changed payload missing docId");
|
|
14653
14292
|
if (typeof value.path !== "string") throw new Error("file-changed payload missing path");
|
|
14654
14293
|
if (typeof value.markdown !== "string") throw new Error("file-changed payload missing markdown");
|
|
14655
|
-
if (!value.sidecar || typeof value.sidecar !== "object") throw new Error("file-changed payload missing sidecar");
|
|
14656
14294
|
return {
|
|
14657
14295
|
docId: value.docId,
|
|
14658
14296
|
path: value.path,
|
|
14297
|
+
title: typeof value.title === "string" ? value.title : titleFromMarkdown(value.path, value.markdown),
|
|
14659
14298
|
markdown: value.markdown,
|
|
14660
|
-
|
|
14299
|
+
updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
14661
14300
|
currentSha: typeof value.currentSha === "string" ? value.currentSha : void 0,
|
|
14662
14301
|
canonicalHead: typeof value.canonicalHead === "string" ? value.canonicalHead : void 0
|
|
14663
14302
|
};
|
|
@@ -14665,10 +14304,10 @@ function readDocumentPayload(payload) {
|
|
|
14665
14304
|
|
|
14666
14305
|
// src/bridge-startup.ts
|
|
14667
14306
|
import { execFile as execFile2 } from "node:child_process";
|
|
14668
|
-
import { createHash as
|
|
14669
|
-
import { chmod, copyFile, mkdir as
|
|
14670
|
-
import { homedir, platform } from "node:os";
|
|
14671
|
-
import { dirname as dirname5, join as
|
|
14307
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
14308
|
+
import { chmod as chmod2, copyFile, mkdir as mkdir5, readFile as readFile7, stat as stat3, writeFile as writeFile5 } from "node:fs/promises";
|
|
14309
|
+
import { homedir as homedir2, platform } from "node:os";
|
|
14310
|
+
import { dirname as dirname5, join as join5, resolve as resolve5 } from "node:path";
|
|
14672
14311
|
import { promisify as promisify2 } from "node:util";
|
|
14673
14312
|
var execFileAsync2 = promisify2(execFile2);
|
|
14674
14313
|
async function runBridgeStartup(options) {
|
|
@@ -14676,13 +14315,13 @@ async function runBridgeStartup(options) {
|
|
|
14676
14315
|
await assertBridgeConfigured(root);
|
|
14677
14316
|
const requestedMode = readBridgeStartupMode(options.mode);
|
|
14678
14317
|
const mode = requestedMode === "auto" ? await autoBridgeStartupMode() : requestedMode;
|
|
14679
|
-
const startupDir =
|
|
14680
|
-
await
|
|
14681
|
-
const runnerPath =
|
|
14318
|
+
const startupDir = join5(root, ".mdocs", "startup");
|
|
14319
|
+
await mkdir5(startupDir, { recursive: true });
|
|
14320
|
+
const runnerPath = join5(startupDir, "bridge-runner.sh");
|
|
14682
14321
|
const runnerArgs = bridgeResumeArgs(root, options);
|
|
14683
14322
|
const runnerScript = bridgeRunnerScript(root, runnerArgs);
|
|
14684
|
-
await
|
|
14685
|
-
await
|
|
14323
|
+
await writeFile5(runnerPath, runnerScript, "utf8");
|
|
14324
|
+
await chmod2(runnerPath, 493);
|
|
14686
14325
|
const nativePlan = mode === "launchd" ? await writeLaunchdPlan(root, startupDir, runnerPath) : mode === "systemd" ? await writeSystemdPlan(root, startupDir, runnerPath) : void 0;
|
|
14687
14326
|
if (options.install && !nativePlan) {
|
|
14688
14327
|
throw new CliError("usage_error", "Shell startup mode does not have a native service to install.", {
|
|
@@ -14702,11 +14341,11 @@ async function runBridgeStartup(options) {
|
|
|
14702
14341
|
installCommands: nativePlan?.installCommands ?? [],
|
|
14703
14342
|
notes: [
|
|
14704
14343
|
"Sandbox providers such as Daytona, Sprites, and Modal should run startupCommand from their on-start hook.",
|
|
14705
|
-
"The runner stores no bridge token
|
|
14706
|
-
"If
|
|
14344
|
+
"The runner stores no bridge token; it reuses the durable token saved by bridge setup in the local CLI profile.",
|
|
14345
|
+
"If you move the root to a different machine or profile, run bridge setup from a fresh Magic binding packet again."
|
|
14707
14346
|
]
|
|
14708
14347
|
};
|
|
14709
|
-
await
|
|
14348
|
+
await writeFile5(join5(startupDir, "bridge-startup.json"), `${JSON.stringify(result, null, 2)}
|
|
14710
14349
|
`, "utf8");
|
|
14711
14350
|
return result;
|
|
14712
14351
|
}
|
|
@@ -14733,7 +14372,7 @@ exec npx --yes --package=@magic-markdown/cli@latest mdocs ${shellCommand(args)}
|
|
|
14733
14372
|
}
|
|
14734
14373
|
function bridgeResumeArgs(root, options) {
|
|
14735
14374
|
const args = ["bridge", "resume", "--root", root, "--forever"];
|
|
14736
|
-
if (options.requestToken
|
|
14375
|
+
if (options.requestToken === true) args.push("--request-token");
|
|
14737
14376
|
if (options.baseUrl) args.push("--url", options.baseUrl);
|
|
14738
14377
|
if (options.intervalMs) args.push("--interval", String(options.intervalMs));
|
|
14739
14378
|
return args;
|
|
@@ -14752,9 +14391,9 @@ async function autoBridgeStartupMode() {
|
|
|
14752
14391
|
return "shell";
|
|
14753
14392
|
}
|
|
14754
14393
|
async function assertBridgeConfigured(root) {
|
|
14755
|
-
const configPath =
|
|
14394
|
+
const configPath = join5(root, ".mdocs", "bridge.json");
|
|
14756
14395
|
try {
|
|
14757
|
-
const config2 = JSON.parse(await
|
|
14396
|
+
const config2 = JSON.parse(await readFile7(configPath, "utf8"));
|
|
14758
14397
|
if (config2.schemaVersion !== 1) throw new Error("invalid schema");
|
|
14759
14398
|
} catch {
|
|
14760
14399
|
throw new CliError("usage_error", "This root does not have a bridge configuration yet.", {
|
|
@@ -14764,11 +14403,11 @@ async function assertBridgeConfigured(root) {
|
|
|
14764
14403
|
}
|
|
14765
14404
|
async function writeLaunchdPlan(root, startupDir, runnerPath) {
|
|
14766
14405
|
const label = serviceLabel(root);
|
|
14767
|
-
const servicePath =
|
|
14768
|
-
const installPath =
|
|
14769
|
-
const outLog =
|
|
14770
|
-
const errLog =
|
|
14771
|
-
await
|
|
14406
|
+
const servicePath = join5(startupDir, `${label}.plist`);
|
|
14407
|
+
const installPath = join5(homedir2(), "Library", "LaunchAgents", `${label}.plist`);
|
|
14408
|
+
const outLog = join5(root, ".mdocs", "bridge.log");
|
|
14409
|
+
const errLog = join5(root, ".mdocs", "bridge.err.log");
|
|
14410
|
+
await writeFile5(servicePath, launchdPlist(label, root, runnerPath, outLog, errLog), "utf8");
|
|
14772
14411
|
const target = `gui/$(id -u)`;
|
|
14773
14412
|
return {
|
|
14774
14413
|
servicePath,
|
|
@@ -14782,7 +14421,7 @@ async function writeLaunchdPlan(root, startupDir, runnerPath) {
|
|
|
14782
14421
|
`launchctl kickstart -k ${target}/${label}`
|
|
14783
14422
|
],
|
|
14784
14423
|
install: async () => {
|
|
14785
|
-
await
|
|
14424
|
+
await mkdir5(dirname5(installPath), { recursive: true });
|
|
14786
14425
|
await copyFile(servicePath, installPath);
|
|
14787
14426
|
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
14788
14427
|
if (uid === void 0) return;
|
|
@@ -14796,9 +14435,9 @@ async function writeLaunchdPlan(root, startupDir, runnerPath) {
|
|
|
14796
14435
|
}
|
|
14797
14436
|
async function writeSystemdPlan(root, startupDir, runnerPath) {
|
|
14798
14437
|
const unitName = `${serviceLabel(root)}.service`;
|
|
14799
|
-
const servicePath =
|
|
14800
|
-
const installPath =
|
|
14801
|
-
await
|
|
14438
|
+
const servicePath = join5(startupDir, unitName);
|
|
14439
|
+
const installPath = join5(homedir2(), ".config", "systemd", "user", unitName);
|
|
14440
|
+
await writeFile5(servicePath, systemdUnit(unitName, root, runnerPath), "utf8");
|
|
14802
14441
|
return {
|
|
14803
14442
|
servicePath,
|
|
14804
14443
|
installPath,
|
|
@@ -14809,7 +14448,7 @@ async function writeSystemdPlan(root, startupDir, runnerPath) {
|
|
|
14809
14448
|
shellCommand(["systemctl", "--user", "enable", "--now", unitName])
|
|
14810
14449
|
],
|
|
14811
14450
|
install: async () => {
|
|
14812
|
-
await
|
|
14451
|
+
await mkdir5(dirname5(installPath), { recursive: true });
|
|
14813
14452
|
await copyFile(servicePath, installPath);
|
|
14814
14453
|
await runCommand("systemctl", ["--user", "daemon-reload"]);
|
|
14815
14454
|
await runCommand("systemctl", ["--user", "enable", "--now", unitName]);
|
|
@@ -14861,7 +14500,7 @@ WantedBy=default.target
|
|
|
14861
14500
|
`;
|
|
14862
14501
|
}
|
|
14863
14502
|
function serviceLabel(root) {
|
|
14864
|
-
const digest =
|
|
14503
|
+
const digest = createHash4("sha256").update(root).digest("hex").slice(0, 12);
|
|
14865
14504
|
return `com.magic-markdown.bridge.${digest}`;
|
|
14866
14505
|
}
|
|
14867
14506
|
async function runOptionalCommand(command, args) {
|
|
@@ -14957,11 +14596,7 @@ async function main() {
|
|
|
14957
14596
|
...image2.workspacePath ? { absolutePath: resolve6(cwd, image2.workspacePath) } : {}
|
|
14958
14597
|
})),
|
|
14959
14598
|
links: state.links,
|
|
14960
|
-
...parsed.flags["no-review"] ? {} : {
|
|
14961
|
-
comments: state.sidecar.comments.filter((comment2) => comment2.status === "open"),
|
|
14962
|
-
suggestions: state.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
|
|
14963
|
-
anchors: state.anchors
|
|
14964
|
-
}
|
|
14599
|
+
...parsed.flags["no-review"] ? {} : { review: reviewStateForDocument(state) }
|
|
14965
14600
|
}, parsed.flags);
|
|
14966
14601
|
return;
|
|
14967
14602
|
}
|
|
@@ -14971,49 +14606,25 @@ async function main() {
|
|
|
14971
14606
|
return;
|
|
14972
14607
|
}
|
|
14973
14608
|
case "comments": {
|
|
14974
|
-
|
|
14975
|
-
print(state.sidecar.comments, parsed.flags);
|
|
14976
|
-
return;
|
|
14609
|
+
throw localReviewCommandError("comments");
|
|
14977
14610
|
}
|
|
14978
14611
|
case "comment": {
|
|
14979
|
-
|
|
14980
|
-
const range = parseRange2(requiredFlag2(parsed.flags, "range"));
|
|
14981
|
-
const body = await readRequiredTextFlag2(parsed.flags, cwd, ["body", "message"]);
|
|
14982
|
-
if (!body) throw new CliError("usage_error", "Comment body is empty.", { hint: "Pass --body <text> or a non-empty --body-file." });
|
|
14983
|
-
print(await addComment(io, path, range, body, actorFromFlags(parsed.flags, defaultActor())), parsed.flags);
|
|
14984
|
-
return;
|
|
14612
|
+
throw localReviewCommandError("comment");
|
|
14985
14613
|
}
|
|
14986
14614
|
case "suggestions": {
|
|
14987
|
-
|
|
14988
|
-
print(state.sidecar.suggestions, parsed.flags);
|
|
14989
|
-
return;
|
|
14615
|
+
throw localReviewCommandError("suggestions");
|
|
14990
14616
|
}
|
|
14991
14617
|
case "suggest": {
|
|
14992
|
-
|
|
14993
|
-
const range = parseRange2(requiredFlag2(parsed.flags, "range"));
|
|
14994
|
-
const replacement = await readRequiredTextFlag2(parsed.flags, cwd, ["with", "replacement"]);
|
|
14995
|
-
const message = await readOptionalTextFlag2(parsed.flags, cwd, ["message"]) ?? "Suggested edit";
|
|
14996
|
-
print(await addSuggestion(io, path, range, replacement, message, actorFromFlags(parsed.flags, {
|
|
14997
|
-
id: "agent_local",
|
|
14998
|
-
kind: "agent",
|
|
14999
|
-
name: "Local Agent"
|
|
15000
|
-
})), parsed.flags);
|
|
15001
|
-
return;
|
|
14618
|
+
throw localReviewCommandError("suggest");
|
|
15002
14619
|
}
|
|
15003
14620
|
case "accept": {
|
|
15004
|
-
|
|
15005
|
-
print(await acceptSuggestion(io, suggestionId, actorFromFlags(parsed.flags, defaultActor())), parsed.flags);
|
|
15006
|
-
return;
|
|
14621
|
+
throw localReviewCommandError("accept");
|
|
15007
14622
|
}
|
|
15008
14623
|
case "reject": {
|
|
15009
|
-
|
|
15010
|
-
print(await rejectSuggestion(io, suggestionId, actorFromFlags(parsed.flags, defaultActor())), parsed.flags);
|
|
15011
|
-
return;
|
|
14624
|
+
throw localReviewCommandError("reject");
|
|
15012
14625
|
}
|
|
15013
14626
|
case "resolve-comment": {
|
|
15014
|
-
|
|
15015
|
-
print(await resolveComment(io, commentId, actorFromFlags(parsed.flags, defaultActor())), parsed.flags);
|
|
15016
|
-
return;
|
|
14627
|
+
throw localReviewCommandError("resolve-comment");
|
|
15017
14628
|
}
|
|
15018
14629
|
case "sync":
|
|
15019
14630
|
case "status": {
|
|
@@ -15070,7 +14681,7 @@ async function main() {
|
|
|
15070
14681
|
claimToken: typeof parsed.flags.claim === "string" ? parsed.flags.claim : void 0,
|
|
15071
14682
|
actorId: typeof parsed.flags.actor === "string" ? parsed.flags.actor : void 0,
|
|
15072
14683
|
actorName: typeof parsed.flags["actor-name"] === "string" ? parsed.flags["actor-name"] : void 0,
|
|
15073
|
-
intervalMs:
|
|
14684
|
+
intervalMs: validateBridgeIntervalMs(parsed.flags.interval),
|
|
15074
14685
|
token: typeof parsed.flags.token === "string" ? parsed.flags.token : process.env.MDOCS_BRIDGE_TOKEN || void 0,
|
|
15075
14686
|
requestToken: Boolean(parsed.flags["request-token"] || parsed.flags.pair),
|
|
15076
14687
|
pairingTimeoutMs: typeof parsed.flags["pairing-timeout-ms"] === "string" ? Number(parsed.flags["pairing-timeout-ms"]) : void 0,
|
|
@@ -15082,7 +14693,7 @@ async function main() {
|
|
|
15082
14693
|
root,
|
|
15083
14694
|
mode: typeof parsed.flags.mode === "string" ? parsed.flags.mode : void 0,
|
|
15084
14695
|
install: Boolean(parsed.flags.install),
|
|
15085
|
-
requestToken:
|
|
14696
|
+
requestToken: Boolean(parsed.flags["request-token"] || parsed.flags.pair),
|
|
15086
14697
|
baseUrl: optionalBridgeBaseUrl(parsed.flags),
|
|
15087
14698
|
intervalMs: typeof parsed.flags.interval === "string" ? Number(parsed.flags.interval) : void 0
|
|
15088
14699
|
});
|
|
@@ -15097,9 +14708,15 @@ async function main() {
|
|
|
15097
14708
|
});
|
|
15098
14709
|
return;
|
|
15099
14710
|
}
|
|
14711
|
+
const workspaceId = typeof parsed.flags.workspace === "string" && parsed.flags.workspace.trim() ? parsed.flags.workspace.trim() : void 0;
|
|
14712
|
+
if (!workspaceId) {
|
|
14713
|
+
throw new CliError("usage_error", "Missing --workspace <id> for mdocs bridge.", {
|
|
14714
|
+
hint: "Pass the workspace explicitly, for example --workspace ws_123. The bridge no longer assumes a demo workspace."
|
|
14715
|
+
});
|
|
14716
|
+
}
|
|
15100
14717
|
const options = {
|
|
15101
14718
|
...baseOptions,
|
|
15102
|
-
workspaceId
|
|
14719
|
+
workspaceId,
|
|
15103
14720
|
baseUrl: bridgeBaseUrl(parsed.flags)
|
|
15104
14721
|
};
|
|
15105
14722
|
if (subcommand === "setup") await runBridgeSetup(options);
|
|
@@ -15185,23 +14802,14 @@ function parseArgs(args) {
|
|
|
15185
14802
|
}
|
|
15186
14803
|
return { command, flags };
|
|
15187
14804
|
}
|
|
15188
|
-
function parseRange2(value) {
|
|
15189
|
-
const [start, end = start] = value.split(":").map(Number);
|
|
15190
|
-
if (!Number.isInteger(start) || !Number.isInteger(end) || !start || !end || start < 1 || end < start) {
|
|
15191
|
-
throw new CliError("invalid_range", `Invalid --range value "${value}".`, {
|
|
15192
|
-
hint: "Use --range <startLine>:<endLine> with 1-based inclusive line numbers, for example --range 4:9."
|
|
15193
|
-
});
|
|
15194
|
-
}
|
|
15195
|
-
return { startLine: start, endLine: end };
|
|
15196
|
-
}
|
|
15197
14805
|
function requiredArg(value, name) {
|
|
15198
14806
|
if (!value) throw new CliError("usage_error", `Missing <${name}> argument.`);
|
|
15199
14807
|
return value;
|
|
15200
14808
|
}
|
|
15201
|
-
function
|
|
15202
|
-
|
|
15203
|
-
|
|
15204
|
-
|
|
14809
|
+
function localReviewCommandError(command) {
|
|
14810
|
+
return new CliError("usage_error", `Local review command "${command}" was removed.`, {
|
|
14811
|
+
hint: "Use `mdocs remote ...` or the remote MCP connector so comments, suggestions, accept, and reject go through DocumentAuthority."
|
|
14812
|
+
});
|
|
15205
14813
|
}
|
|
15206
14814
|
function bridgeBaseUrl(flags) {
|
|
15207
14815
|
const configured = optionalBridgeBaseUrl(flags);
|
|
@@ -15215,40 +14823,6 @@ function optionalBridgeBaseUrl(flags) {
|
|
|
15215
14823
|
const configured = process.env.MDOCS_BASE_URL?.trim();
|
|
15216
14824
|
return configured || void 0;
|
|
15217
14825
|
}
|
|
15218
|
-
async function readRequiredTextFlag2(flags, cwd, names) {
|
|
15219
|
-
const value = await readOptionalTextFlag2(flags, cwd, names);
|
|
15220
|
-
if (value === void 0) {
|
|
15221
|
-
throw new CliError("usage_error", `Pass --${names[0]} <text> or --${names[0]}-file <path>.`, {
|
|
15222
|
-
hint: `For multiline text, write it to a file and pass --${names[0]}-file.`
|
|
15223
|
-
});
|
|
15224
|
-
}
|
|
15225
|
-
return value;
|
|
15226
|
-
}
|
|
15227
|
-
async function readOptionalTextFlag2(flags, cwd, names) {
|
|
15228
|
-
for (const name of names) {
|
|
15229
|
-
const fileValue = flags[`${name}-file`];
|
|
15230
|
-
if (typeof fileValue === "string") return readFile7(resolve6(cwd, fileValue), "utf8");
|
|
15231
|
-
const value = flags[name];
|
|
15232
|
-
if (typeof value === "string") return value;
|
|
15233
|
-
}
|
|
15234
|
-
return void 0;
|
|
15235
|
-
}
|
|
15236
|
-
function actorFromFlags(flags, fallback) {
|
|
15237
|
-
const id = typeof flags.actor === "string" ? flags.actor : fallback.id;
|
|
15238
|
-
const name = typeof flags["actor-name"] === "string" ? flags["actor-name"] : fallback.name;
|
|
15239
|
-
const email = typeof flags["actor-email"] === "string" ? flags["actor-email"] : fallback.email;
|
|
15240
|
-
const kind = actorKindFromFlag(flags["actor-kind"]) ?? fallback.kind;
|
|
15241
|
-
return {
|
|
15242
|
-
id,
|
|
15243
|
-
kind,
|
|
15244
|
-
name,
|
|
15245
|
-
...email ? { email } : {}
|
|
15246
|
-
};
|
|
15247
|
-
}
|
|
15248
|
-
function actorKindFromFlag(value) {
|
|
15249
|
-
if (value === "human" || value === "agent" || value === "system") return value;
|
|
15250
|
-
return void 0;
|
|
15251
|
-
}
|
|
15252
14826
|
function print(value, flags) {
|
|
15253
14827
|
if (flags.json || typeof value !== "string") {
|
|
15254
14828
|
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
@@ -15268,20 +14842,14 @@ Commands:
|
|
|
15268
14842
|
init [path] Initialize .mdocs and index Markdown files
|
|
15269
14843
|
map --json Print workspace map
|
|
15270
14844
|
graph --json Print workspace knowledge graph nodes and edges
|
|
15271
|
-
state <path> --json Print
|
|
14845
|
+
state <path> --json Print clean Markdown document state
|
|
15272
14846
|
context <path> --json Print agent-ready context
|
|
15273
14847
|
context <path> --summary --json Print document metadata and headings
|
|
15274
|
-
review <path> --json Print
|
|
15275
|
-
comments
|
|
15276
|
-
comment
|
|
15277
|
-
|
|
15278
|
-
|
|
15279
|
-
suggest <path> --range 3:5 --with-file replacement.md
|
|
15280
|
-
Add a multiline reviewMarkdown suggestion
|
|
15281
|
-
accept <suggestionId> Apply and accept a suggestion
|
|
15282
|
-
reject <suggestionId> Reject a suggestion
|
|
15283
|
-
resolve-comment <commentId> Resolve a comment thread
|
|
15284
|
-
status --json Print Git status and sidecar map
|
|
14848
|
+
review <path> --json Print local authority-review redirect metadata
|
|
14849
|
+
comments|comment|suggestions Removed locally; use remote authority commands
|
|
14850
|
+
suggest|accept|reject|resolve-comment
|
|
14851
|
+
Removed locally; use remote authority commands
|
|
14852
|
+
status --json Print Git status and workspace map
|
|
15285
14853
|
sync --json Alias for status until hosted sync is configured
|
|
15286
14854
|
diff [path] Print local Git diff
|
|
15287
14855
|
history [path] --json Print local Git history
|
|
@@ -15297,24 +14865,24 @@ Commands:
|
|
|
15297
14865
|
remote events|history|restore Poll events, list commits, restore snapshots
|
|
15298
14866
|
remote library|create-folder|update-folder|move-root|invite-folder
|
|
15299
14867
|
Organize the joined project library
|
|
15300
|
-
bridge setup --workspace <id> --root . --url <base-url> [--folder-id <id>] --
|
|
15301
|
-
Initialize, validate,
|
|
14868
|
+
bridge setup --workspace <id> --root . --url <base-url> [--folder-id <id>] --claim <claim-token>
|
|
14869
|
+
Initialize, validate, claim,
|
|
15302
14870
|
and start an agent filesystem bridge
|
|
15303
|
-
bridge resume --root . --forever
|
|
14871
|
+
bridge resume --root . --forever [--once]
|
|
15304
14872
|
Backfill missed Magic changes, then keep
|
|
15305
14873
|
the bridge running unless --once is set
|
|
15306
14874
|
bridge startup|daemon --root . [--mode auto|shell|launchd|systemd] [--install]
|
|
15307
14875
|
Write a bridge runner and startup hook
|
|
15308
14876
|
for sandboxes, launchd, or systemd
|
|
15309
|
-
bridge --workspace <id> --root . --url <base-url> --
|
|
15310
|
-
|
|
15311
|
-
|
|
14877
|
+
bridge --workspace <id> --root . --url <base-url> --claim <claim-token>
|
|
14878
|
+
Claim, then sync an approved local root
|
|
14879
|
+
with the workspace
|
|
15312
14880
|
bridge --workspace <id> --root . --url <base-url> --token <bridge-token>
|
|
15313
14881
|
Sync an approved local root with the workspace
|
|
15314
14882
|
(or set MDOCS_BRIDGE_TOKEN)
|
|
15315
14883
|
bridge --claim <token> --root . --canonical-prefix prompts --replica-prefix packages/agent/prompts
|
|
15316
14884
|
Claim an existing repo path as a Magic-canonical source mapping
|
|
15317
|
-
doctor --json Validate
|
|
14885
|
+
doctor --json Validate workspace map and agent readiness
|
|
15318
14886
|
serve-mcp Start JSON-RPC MCP-compatible stdio server
|
|
15319
14887
|
version [--json] Print CLI version
|
|
15320
14888
|
help <command> Show usage, examples, and notes for one command
|