@magic-markdown/cli 0.3.0 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/index.js +1430 -156
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -321,6 +321,17 @@ function fnv1a(value) {
|
|
|
321
321
|
return hash;
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
// ../core/src/wikilinks.ts
|
|
325
|
+
function parseWikilinkBody(value) {
|
|
326
|
+
const separatorIndex = value.indexOf("|");
|
|
327
|
+
const rawTarget = separatorIndex >= 0 ? value.slice(0, separatorIndex) : value;
|
|
328
|
+
const rawLabel = separatorIndex >= 0 ? value.slice(separatorIndex + 1) : void 0;
|
|
329
|
+
const target = rawTarget.trim();
|
|
330
|
+
if (!target) return void 0;
|
|
331
|
+
const label = rawLabel?.trim();
|
|
332
|
+
return label ? { target, label } : { target };
|
|
333
|
+
}
|
|
334
|
+
|
|
324
335
|
// ../core/src/markdown.ts
|
|
325
336
|
var POLLUTING_PATTERNS = [
|
|
326
337
|
{ name: "CriticMarkup addition", pattern: /\{\+\+[\s\S]*?\+\+\}/ },
|
|
@@ -381,9 +392,16 @@ function replaceLineRange(markdown, range, replacement) {
|
|
|
381
392
|
const lines = getLines(markdown);
|
|
382
393
|
const before = lines.slice(0, range.startLine - 1);
|
|
383
394
|
const after = lines.slice(range.endLine);
|
|
384
|
-
const replacementLines = replacement
|
|
395
|
+
const replacementLines = replacementLinesForRange(replacement);
|
|
385
396
|
return [...before, ...replacementLines, ...after].join("\n");
|
|
386
397
|
}
|
|
398
|
+
function replacementLinesForRange(replacement) {
|
|
399
|
+
if (replacement.length === 0) return [];
|
|
400
|
+
const normalized = replacement.replace(/\r\n?/g, "\n");
|
|
401
|
+
const lines = normalized.split("\n");
|
|
402
|
+
if (normalized.endsWith("\n")) lines.pop();
|
|
403
|
+
return lines;
|
|
404
|
+
}
|
|
387
405
|
function extractMarkdownImages(markdown, documentPath) {
|
|
388
406
|
const masked = maskCode(markdown);
|
|
389
407
|
const referenceDefinitions = collectReferenceDefinitions(markdown, masked);
|
|
@@ -469,6 +487,86 @@ function extractMarkdownImages(markdown, documentPath) {
|
|
|
469
487
|
}
|
|
470
488
|
return images;
|
|
471
489
|
}
|
|
490
|
+
function extractMarkdownLinks(markdown) {
|
|
491
|
+
const codeMasked = maskCode(markdown);
|
|
492
|
+
const referenceDefinitions = collectReferenceDefinitions(markdown, codeMasked);
|
|
493
|
+
const masked = maskReferenceDefinitions(markdown, codeMasked);
|
|
494
|
+
const lineStarts = getLineStarts(markdown);
|
|
495
|
+
const links = [];
|
|
496
|
+
let index = 0;
|
|
497
|
+
while (index < masked.length) {
|
|
498
|
+
const start = masked.indexOf("[", index);
|
|
499
|
+
if (start === -1) break;
|
|
500
|
+
if (masked[start + 1] === "[" || markdown[start - 1] === "!" || isEscaped(markdown, start)) {
|
|
501
|
+
index = start + 1;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const labelClose = findClosingBracket(masked, start);
|
|
505
|
+
if (labelClose === -1) {
|
|
506
|
+
index = start + 1;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const label = markdown.slice(start + 1, labelClose);
|
|
510
|
+
const afterLabel = labelClose + 1;
|
|
511
|
+
const position = positionAt(lineStarts, start);
|
|
512
|
+
if (masked[afterLabel] === "(") {
|
|
513
|
+
const destinationClose = findClosingParen(markdown, afterLabel + 1);
|
|
514
|
+
if (destinationClose === -1) {
|
|
515
|
+
index = afterLabel + 1;
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const parsed = parseDestinationAndTitle(markdown.slice(afterLabel + 1, destinationClose));
|
|
519
|
+
if (parsed) {
|
|
520
|
+
links.push(toLinkReference({
|
|
521
|
+
label,
|
|
522
|
+
target: parsed.source,
|
|
523
|
+
title: parsed.title,
|
|
524
|
+
line: position.line,
|
|
525
|
+
column: position.column,
|
|
526
|
+
syntax: "inline"
|
|
527
|
+
}));
|
|
528
|
+
}
|
|
529
|
+
index = destinationClose + 1;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (masked[afterLabel] === "[") {
|
|
533
|
+
const referenceClose = findClosingBracket(masked, afterLabel);
|
|
534
|
+
if (referenceClose === -1) {
|
|
535
|
+
index = afterLabel + 1;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
const explicitLabel = markdown.slice(afterLabel + 1, referenceClose);
|
|
539
|
+
const definitionLabel = explicitLabel.length > 0 ? explicitLabel : label;
|
|
540
|
+
const definition = referenceDefinitions.get(normalizeReferenceLabel(definitionLabel));
|
|
541
|
+
if (definition) {
|
|
542
|
+
links.push(toLinkReference({
|
|
543
|
+
label,
|
|
544
|
+
target: definition.source,
|
|
545
|
+
title: definition.title,
|
|
546
|
+
line: position.line,
|
|
547
|
+
column: position.column,
|
|
548
|
+
syntax: explicitLabel.length > 0 ? "reference" : "collapsed_reference"
|
|
549
|
+
}));
|
|
550
|
+
}
|
|
551
|
+
index = referenceClose + 1;
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const shortcutDefinition = referenceDefinitions.get(normalizeReferenceLabel(label));
|
|
555
|
+
if (shortcutDefinition) {
|
|
556
|
+
links.push(toLinkReference({
|
|
557
|
+
label,
|
|
558
|
+
target: shortcutDefinition.source,
|
|
559
|
+
title: shortcutDefinition.title,
|
|
560
|
+
line: position.line,
|
|
561
|
+
column: position.column,
|
|
562
|
+
syntax: "shortcut_reference"
|
|
563
|
+
}));
|
|
564
|
+
}
|
|
565
|
+
index = labelClose + 1;
|
|
566
|
+
}
|
|
567
|
+
extractWikilinks(markdown, masked, lineStarts).forEach((link2) => links.push(link2));
|
|
568
|
+
return links.sort((left, right) => left.line - right.line || left.column - right.column);
|
|
569
|
+
}
|
|
472
570
|
function toImageReference(image2) {
|
|
473
571
|
const workspacePath = resolveWorkspaceImagePath(image2.source, image2.documentPath);
|
|
474
572
|
return {
|
|
@@ -483,6 +581,54 @@ function toImageReference(image2) {
|
|
|
483
581
|
...workspacePath ? { workspacePath } : {}
|
|
484
582
|
};
|
|
485
583
|
}
|
|
584
|
+
function toLinkReference(link2) {
|
|
585
|
+
return {
|
|
586
|
+
label: unescapeMarkdown(link2.label),
|
|
587
|
+
target: link2.target,
|
|
588
|
+
...link2.title ? { title: link2.title } : {},
|
|
589
|
+
line: link2.line,
|
|
590
|
+
column: link2.column,
|
|
591
|
+
syntax: link2.syntax
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function extractWikilinks(markdown, masked, lineStarts) {
|
|
595
|
+
const links = [];
|
|
596
|
+
let index = 0;
|
|
597
|
+
while (index < masked.length) {
|
|
598
|
+
const start = masked.indexOf("[[", index);
|
|
599
|
+
if (start === -1) break;
|
|
600
|
+
if (isEscaped(markdown, start)) {
|
|
601
|
+
index = start + 2;
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
const close2 = masked.indexOf("]]", start + 2);
|
|
605
|
+
if (close2 === -1 || markdown.slice(start + 2, close2).includes("\n")) {
|
|
606
|
+
index = start + 2;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const parsed = parseWikilinkBody(markdown.slice(start + 2, close2));
|
|
610
|
+
if (parsed) {
|
|
611
|
+
const position = positionAt(lineStarts, start);
|
|
612
|
+
links.push({
|
|
613
|
+
label: parsed.label ?? parsed.target,
|
|
614
|
+
target: parsed.target,
|
|
615
|
+
line: position.line,
|
|
616
|
+
column: position.column,
|
|
617
|
+
syntax: "wikilink"
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
index = close2 + 2;
|
|
621
|
+
}
|
|
622
|
+
return links;
|
|
623
|
+
}
|
|
624
|
+
function maskReferenceDefinitions(markdown, masked) {
|
|
625
|
+
const markdownLines = markdown.split(/(?<=\n)/);
|
|
626
|
+
const maskedLines = masked.split(/(?<=\n)/);
|
|
627
|
+
return maskedLines.map((line, index) => {
|
|
628
|
+
const original = markdownLines[index] ?? "";
|
|
629
|
+
return parseReferenceDefinition(original.replace(/\r?\n$/, "")) ? maskLine(line) : line;
|
|
630
|
+
}).join("");
|
|
631
|
+
}
|
|
486
632
|
function collectReferenceDefinitions(markdown, masked) {
|
|
487
633
|
const definitions = /* @__PURE__ */ new Map();
|
|
488
634
|
const lines = getLines(markdown);
|
|
@@ -818,8 +964,129 @@ function scoreContext(lines, startLine, endLine, selector) {
|
|
|
818
964
|
return Math.min(1, score);
|
|
819
965
|
}
|
|
820
966
|
|
|
967
|
+
// ../core/src/text-merge.ts
|
|
968
|
+
var MAX_DIFF_CELLS = 25e6;
|
|
969
|
+
function mergeText(base, ours, theirs) {
|
|
970
|
+
if (ours === theirs) return { ok: true, text: ours };
|
|
971
|
+
if (base === ours) return { ok: true, text: theirs };
|
|
972
|
+
if (base === theirs) return { ok: true, text: ours };
|
|
973
|
+
const baseLines = base.split("\n");
|
|
974
|
+
const ourLines = ours.split("\n");
|
|
975
|
+
const theirLines = theirs.split("\n");
|
|
976
|
+
let regionsA;
|
|
977
|
+
let regionsB;
|
|
978
|
+
try {
|
|
979
|
+
regionsA = diffTextRegions(baseLines, ourLines);
|
|
980
|
+
regionsB = diffTextRegions(baseLines, theirLines);
|
|
981
|
+
} catch {
|
|
982
|
+
return { ok: false, reason: "too_large" };
|
|
983
|
+
}
|
|
984
|
+
const clusters = clusterRegions(regionsA, regionsB);
|
|
985
|
+
const merged = [];
|
|
986
|
+
let baseCursor = 0;
|
|
987
|
+
for (const cluster of clusters) {
|
|
988
|
+
for (let line = baseCursor; line < cluster.lo; line += 1) merged.push(baseLines[line]);
|
|
989
|
+
const baseText = baseLines.slice(cluster.lo, cluster.hi);
|
|
990
|
+
const ourText = sideSlice(ourLines, regionsA, cluster.lo, cluster.hi);
|
|
991
|
+
const theirText = sideSlice(theirLines, regionsB, cluster.lo, cluster.hi);
|
|
992
|
+
const oursChanged = !linesEqual(ourText, baseText);
|
|
993
|
+
const theirsChanged = !linesEqual(theirText, baseText);
|
|
994
|
+
if (oursChanged && theirsChanged && !linesEqual(ourText, theirText)) {
|
|
995
|
+
return { ok: false, reason: "overlapping_changes" };
|
|
996
|
+
}
|
|
997
|
+
merged.push(...oursChanged ? ourText : theirText);
|
|
998
|
+
baseCursor = cluster.hi;
|
|
999
|
+
}
|
|
1000
|
+
for (let line = baseCursor; line < baseLines.length; line += 1) merged.push(baseLines[line]);
|
|
1001
|
+
return { ok: true, text: merged.join("\n") };
|
|
1002
|
+
}
|
|
1003
|
+
function diffTextRegions(base, side) {
|
|
1004
|
+
let prefix = 0;
|
|
1005
|
+
while (prefix < base.length && prefix < side.length && base[prefix] === side[prefix]) prefix += 1;
|
|
1006
|
+
let suffix = 0;
|
|
1007
|
+
while (suffix < base.length - prefix && suffix < side.length - prefix && base[base.length - 1 - suffix] === side[side.length - 1 - suffix]) {
|
|
1008
|
+
suffix += 1;
|
|
1009
|
+
}
|
|
1010
|
+
const baseCore = base.slice(prefix, base.length - suffix);
|
|
1011
|
+
const sideCore = side.slice(prefix, side.length - suffix);
|
|
1012
|
+
if (!baseCore.length && !sideCore.length) return [];
|
|
1013
|
+
if ((baseCore.length + 1) * (sideCore.length + 1) > MAX_DIFF_CELLS) throw new Error("diff too large");
|
|
1014
|
+
const rows = baseCore.length + 1;
|
|
1015
|
+
const cols = sideCore.length + 1;
|
|
1016
|
+
const lcs = new Uint32Array(rows * cols);
|
|
1017
|
+
for (let row2 = baseCore.length - 1; row2 >= 0; row2 -= 1) {
|
|
1018
|
+
for (let col2 = sideCore.length - 1; col2 >= 0; col2 -= 1) {
|
|
1019
|
+
lcs[row2 * cols + col2] = baseCore[row2] === sideCore[col2] ? lcs[(row2 + 1) * cols + col2 + 1] + 1 : Math.max(lcs[(row2 + 1) * cols + col2], lcs[row2 * cols + col2 + 1]);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
const regions = [];
|
|
1023
|
+
let row = 0;
|
|
1024
|
+
let col = 0;
|
|
1025
|
+
let pendingBase = -1;
|
|
1026
|
+
let pendingSide = -1;
|
|
1027
|
+
const flush = (baseEnd, sideEnd) => {
|
|
1028
|
+
if (pendingBase < 0) return;
|
|
1029
|
+
regions.push({
|
|
1030
|
+
baseStart: prefix + pendingBase,
|
|
1031
|
+
baseLength: baseEnd - pendingBase,
|
|
1032
|
+
sideStart: prefix + pendingSide,
|
|
1033
|
+
sideLength: sideEnd - pendingSide
|
|
1034
|
+
});
|
|
1035
|
+
pendingBase = -1;
|
|
1036
|
+
pendingSide = -1;
|
|
1037
|
+
};
|
|
1038
|
+
while (row < baseCore.length || col < sideCore.length) {
|
|
1039
|
+
if (row < baseCore.length && col < sideCore.length && baseCore[row] === sideCore[col]) {
|
|
1040
|
+
flush(row, col);
|
|
1041
|
+
row += 1;
|
|
1042
|
+
col += 1;
|
|
1043
|
+
} else {
|
|
1044
|
+
if (pendingBase < 0) {
|
|
1045
|
+
pendingBase = row;
|
|
1046
|
+
pendingSide = col;
|
|
1047
|
+
}
|
|
1048
|
+
if (col >= sideCore.length || row < baseCore.length && lcs[(row + 1) * cols + col] >= lcs[row * cols + col + 1]) {
|
|
1049
|
+
row += 1;
|
|
1050
|
+
} else {
|
|
1051
|
+
col += 1;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
flush(row, col);
|
|
1056
|
+
return regions;
|
|
1057
|
+
}
|
|
1058
|
+
function clusterRegions(regionsA, regionsB) {
|
|
1059
|
+
const spans = [...regionsA, ...regionsB].map((region) => ({ lo: region.baseStart, hi: region.baseStart + region.baseLength })).sort((left, right) => left.lo - right.lo || left.hi - right.hi);
|
|
1060
|
+
const clusters = [];
|
|
1061
|
+
for (const span of spans) {
|
|
1062
|
+
const last = clusters.at(-1);
|
|
1063
|
+
if (last && span.lo <= last.hi) {
|
|
1064
|
+
last.hi = Math.max(last.hi, span.hi);
|
|
1065
|
+
} else {
|
|
1066
|
+
clusters.push({ lo: span.lo, hi: span.hi });
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return clusters;
|
|
1070
|
+
}
|
|
1071
|
+
function sideSlice(side, regions, lo, hi) {
|
|
1072
|
+
return side.slice(lo + sideDeltaBefore(regions, lo, false), hi + sideDeltaBefore(regions, hi, true));
|
|
1073
|
+
}
|
|
1074
|
+
function sideDeltaBefore(regions, basePos, includeInsertionAt) {
|
|
1075
|
+
let delta = 0;
|
|
1076
|
+
for (const region of regions) {
|
|
1077
|
+
const end = region.baseStart + region.baseLength;
|
|
1078
|
+
if (end > basePos) break;
|
|
1079
|
+
if (end === basePos && region.baseLength === 0 && !includeInsertionAt) break;
|
|
1080
|
+
delta += region.sideLength - region.baseLength;
|
|
1081
|
+
}
|
|
1082
|
+
return delta;
|
|
1083
|
+
}
|
|
1084
|
+
function linesEqual(left, right) {
|
|
1085
|
+
return left.length === right.length && left.every((line, index) => line === right[index]);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
821
1088
|
// ../core/src/patches.ts
|
|
822
|
-
function applySuggestion(markdown, suggestion) {
|
|
1089
|
+
function applySuggestion(markdown, suggestion, input) {
|
|
823
1090
|
if (suggestion.status !== "open") {
|
|
824
1091
|
throw new MdocsError(
|
|
825
1092
|
"validation_error",
|
|
@@ -827,18 +1094,308 @@ function applySuggestion(markdown, suggestion) {
|
|
|
827
1094
|
"Only open suggestions can be applied. List open suggestions with the suggestions command."
|
|
828
1095
|
);
|
|
829
1096
|
}
|
|
830
|
-
const
|
|
831
|
-
|
|
1097
|
+
const context = applySuggestionContext(input);
|
|
1098
|
+
const baseResult = applySuggestionFromBase(markdown, suggestion, context.baseMarkdown);
|
|
1099
|
+
if (baseResult.kind === "applied") return baseResult.markdown;
|
|
1100
|
+
if (baseResult.kind === "conflict") {
|
|
1101
|
+
throw new MdocsError(
|
|
1102
|
+
"conflict",
|
|
1103
|
+
`Suggestion ${suggestion.id} no longer applies cleanly: the document changed inside the suggested edit.`,
|
|
1104
|
+
"Review the current text around the suggestion, then accept it manually or ask the agent for a fresh suggestion."
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
const range = currentSuggestionRange(markdown, suggestion, context.anchor);
|
|
1108
|
+
if (!range) {
|
|
832
1109
|
throw new MdocsError(
|
|
833
1110
|
"conflict",
|
|
834
1111
|
`Suggestion ${suggestion.id} no longer applies cleanly: the document changed since it was created.`,
|
|
835
1112
|
"Re-read the document, then create a fresh suggestion against the current content."
|
|
836
1113
|
);
|
|
837
1114
|
}
|
|
838
|
-
const next = replaceLineRange(markdown,
|
|
1115
|
+
const next = replaceLineRange(markdown, range, suggestion.patch.after);
|
|
1116
|
+
assertCleanMarkdown(next);
|
|
1117
|
+
return next;
|
|
1118
|
+
}
|
|
1119
|
+
function applySuggestionContext(input) {
|
|
1120
|
+
if (!input) return {};
|
|
1121
|
+
if ("selector" in input) return { anchor: input };
|
|
1122
|
+
return input;
|
|
1123
|
+
}
|
|
1124
|
+
function applySuggestionFromBase(markdown, suggestion, baseMarkdown) {
|
|
1125
|
+
if (!baseMarkdown || !suggestion.base?.contentHash) return { kind: "unavailable" };
|
|
1126
|
+
if (contentHashForText(baseMarkdown) !== suggestion.base.contentHash) return { kind: "unavailable" };
|
|
1127
|
+
if (!rangeIsWithin(baseMarkdown, suggestion.patch.range) || extractLineRange(baseMarkdown, suggestion.patch.range) !== suggestion.patch.before) {
|
|
1128
|
+
return { kind: "conflict" };
|
|
1129
|
+
}
|
|
1130
|
+
const rebased = rebaseSuggestionPatch(baseMarkdown, markdown, suggestion);
|
|
1131
|
+
if (rebased !== void 0) return { kind: "applied", markdown: rebased };
|
|
1132
|
+
const suggestedMarkdown = replaceLineRange(baseMarkdown, suggestion.patch.range, suggestion.patch.after);
|
|
1133
|
+
assertCleanMarkdown(suggestedMarkdown);
|
|
1134
|
+
const merged = mergeText(baseMarkdown, markdown, suggestedMarkdown);
|
|
1135
|
+
if (!merged.ok) return { kind: "conflict" };
|
|
1136
|
+
assertCleanMarkdown(merged.text);
|
|
1137
|
+
return { kind: "applied", markdown: merged.text };
|
|
1138
|
+
}
|
|
1139
|
+
function rebaseSuggestionPatch(baseMarkdown, markdown, suggestion) {
|
|
1140
|
+
const baseLines = getLines(baseMarkdown);
|
|
1141
|
+
const currentLines = getLines(markdown);
|
|
1142
|
+
let regions;
|
|
1143
|
+
try {
|
|
1144
|
+
regions = diffTextRegions(baseLines, currentLines);
|
|
1145
|
+
} catch {
|
|
1146
|
+
return void 0;
|
|
1147
|
+
}
|
|
1148
|
+
const baseStart = suggestion.patch.range.startLine - 1;
|
|
1149
|
+
const baseEnd = suggestion.patch.range.endLine;
|
|
1150
|
+
if (regions.some((region) => regionTouchesSuggestionRange(region, baseStart, baseEnd))) return void 0;
|
|
1151
|
+
const currentStart = baseStart + sideDeltaBefore(regions, baseStart, true);
|
|
1152
|
+
const currentEnd = baseEnd + sideDeltaBefore(regions, baseEnd, false);
|
|
1153
|
+
const currentRange = { startLine: currentStart + 1, endLine: currentEnd };
|
|
1154
|
+
if (!rangeIsWithin(markdown, currentRange)) return void 0;
|
|
1155
|
+
if (extractLineRange(markdown, currentRange) !== suggestion.patch.before) return void 0;
|
|
1156
|
+
const next = replaceLineRange(markdown, currentRange, suggestion.patch.after);
|
|
839
1157
|
assertCleanMarkdown(next);
|
|
840
1158
|
return next;
|
|
841
1159
|
}
|
|
1160
|
+
function regionTouchesSuggestionRange(region, baseStart, baseEnd) {
|
|
1161
|
+
if (region.baseLength === 0) return region.baseStart > baseStart && region.baseStart < baseEnd;
|
|
1162
|
+
const regionEnd = region.baseStart + region.baseLength;
|
|
1163
|
+
return region.baseStart < baseEnd && regionEnd > baseStart;
|
|
1164
|
+
}
|
|
1165
|
+
function currentSuggestionRange(markdown, suggestion, anchor) {
|
|
1166
|
+
const hintedRange = suggestion.patch.range;
|
|
1167
|
+
if (rangeIsWithin(markdown, hintedRange) && extractLineRange(markdown, hintedRange) === suggestion.patch.before) return hintedRange;
|
|
1168
|
+
const candidates = findBeforeCandidates(markdown, suggestion.patch.before);
|
|
1169
|
+
if (candidates.length === 0) return void 0;
|
|
1170
|
+
if (candidates.length === 1) return candidates[0];
|
|
1171
|
+
if (!anchor) return void 0;
|
|
1172
|
+
const scored = candidates.map((range) => ({ range, score: scoreContext2(markdown, range, anchor) })).sort((left, right) => right.score - left.score);
|
|
1173
|
+
const best = scored[0];
|
|
1174
|
+
if (!best) return void 0;
|
|
1175
|
+
const tied = scored.filter((candidate) => candidate.score === best.score);
|
|
1176
|
+
if (tied.length > 1) return void 0;
|
|
1177
|
+
if (best.score < 0.7) return void 0;
|
|
1178
|
+
return best.range;
|
|
1179
|
+
}
|
|
1180
|
+
function findBeforeCandidates(markdown, before) {
|
|
1181
|
+
const lines = getLines(markdown);
|
|
1182
|
+
const beforeLines = linePattern(before);
|
|
1183
|
+
if (beforeLines.length === 0 || beforeLines.length > lines.length) return [];
|
|
1184
|
+
const ranges = [];
|
|
1185
|
+
const beforeText = beforeLines.join("\n");
|
|
1186
|
+
for (let index = 0; index <= lines.length - beforeLines.length; index += 1) {
|
|
1187
|
+
if (lines.slice(index, index + beforeLines.length).join("\n") === beforeText) {
|
|
1188
|
+
ranges.push({ startLine: index + 1, endLine: index + beforeLines.length });
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return ranges;
|
|
1192
|
+
}
|
|
1193
|
+
function rangeIsWithin(markdown, range) {
|
|
1194
|
+
const lineCount = getLines(markdown).length;
|
|
1195
|
+
return range.startLine >= 1 && range.endLine >= range.startLine && range.endLine <= lineCount;
|
|
1196
|
+
}
|
|
1197
|
+
function linePattern(value) {
|
|
1198
|
+
return value.replace(/\r\n?/g, "\n").split("\n");
|
|
1199
|
+
}
|
|
1200
|
+
function scoreContext2(markdown, range, anchor) {
|
|
1201
|
+
const lines = getLines(markdown);
|
|
1202
|
+
let score = anchor.selector.quote === extractLineRange(markdown, range) ? 0.7 : 0.65;
|
|
1203
|
+
const expectedPrefix = contextPrefix(anchor.selector.prefix);
|
|
1204
|
+
if (expectedPrefix) {
|
|
1205
|
+
const actualPrefix = contextPrefix(lines.slice(Math.max(0, range.startLine - 4), range.startLine - 1).join("\n"));
|
|
1206
|
+
if (actualPrefix?.endsWith(expectedPrefix)) score += 0.15;
|
|
1207
|
+
}
|
|
1208
|
+
const expectedSuffix = contextSuffix(anchor.selector.suffix);
|
|
1209
|
+
if (expectedSuffix) {
|
|
1210
|
+
const actualSuffix = contextSuffix(lines.slice(range.endLine, range.endLine + 3).join("\n"));
|
|
1211
|
+
if (actualSuffix?.startsWith(expectedSuffix)) score += 0.15;
|
|
1212
|
+
}
|
|
1213
|
+
return Math.min(1, score);
|
|
1214
|
+
}
|
|
1215
|
+
function contextPrefix(value) {
|
|
1216
|
+
return value?.split(/\r?\n/).filter(Boolean).slice(-3).join("\n") || void 0;
|
|
1217
|
+
}
|
|
1218
|
+
function contextSuffix(value) {
|
|
1219
|
+
return value?.split(/\r?\n/).filter(Boolean).slice(0, 3).join("\n") || void 0;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// ../core/src/graph.ts
|
|
1223
|
+
function resolveMarkdownLinks(links, currentDocument, documents) {
|
|
1224
|
+
return links.map((link2) => resolveMarkdownLink(link2, currentDocument, documents));
|
|
1225
|
+
}
|
|
1226
|
+
function buildWorkspaceGraph(workspaceId, schemaVersion, documents) {
|
|
1227
|
+
const edges = documents.flatMap((document) => document.links.map((link2, index) => toGraphEdge(document, link2, index)));
|
|
1228
|
+
const incomingByDocId = /* @__PURE__ */ new Map();
|
|
1229
|
+
for (const edge of edges) {
|
|
1230
|
+
if (!edge.targetDocId) continue;
|
|
1231
|
+
incomingByDocId.set(edge.targetDocId, (incomingByDocId.get(edge.targetDocId) ?? 0) + 1);
|
|
1232
|
+
}
|
|
1233
|
+
const nodes = documents.map((document) => ({
|
|
1234
|
+
docId: document.docId,
|
|
1235
|
+
path: document.path,
|
|
1236
|
+
title: document.title,
|
|
1237
|
+
openComments: document.openComments,
|
|
1238
|
+
openSuggestions: document.openSuggestions,
|
|
1239
|
+
anchorsNeedingReview: document.anchorsNeedingReview,
|
|
1240
|
+
outgoingLinks: document.links.length,
|
|
1241
|
+
incomingLinks: incomingByDocId.get(document.docId) ?? 0,
|
|
1242
|
+
unresolvedLinks: document.unresolvedLinks,
|
|
1243
|
+
externalLinks: document.externalLinks
|
|
1244
|
+
}));
|
|
1245
|
+
return {
|
|
1246
|
+
workspaceId,
|
|
1247
|
+
schemaVersion,
|
|
1248
|
+
nodes,
|
|
1249
|
+
edges
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
function resolveMarkdownLink(link2, currentDocument, documents) {
|
|
1253
|
+
const target = link2.target.trim();
|
|
1254
|
+
const parsed = parseInternalTarget(target);
|
|
1255
|
+
if (!target) return { ...link2, status: "unresolved" };
|
|
1256
|
+
if (isExternalTarget(target)) return { ...link2, status: "external" };
|
|
1257
|
+
if (!parsed) return { ...link2, status: "unresolved" };
|
|
1258
|
+
if (!parsed.path) {
|
|
1259
|
+
return parsed.fragment ? {
|
|
1260
|
+
...link2,
|
|
1261
|
+
status: "anchor",
|
|
1262
|
+
fragment: parsed.fragment,
|
|
1263
|
+
targetDocId: currentDocument.docId,
|
|
1264
|
+
targetPath: currentDocument.path,
|
|
1265
|
+
targetTitle: currentDocument.title
|
|
1266
|
+
} : { ...link2, status: "unresolved" };
|
|
1267
|
+
}
|
|
1268
|
+
if (link2.syntax === "wikilink" && !isPathLikeWikilinkTarget(parsed.path)) {
|
|
1269
|
+
const candidates = findBasenameWikilinkCandidates(parsed.path, parsed.fragment, documents);
|
|
1270
|
+
if (candidates.length === 1) return resolvedLink(link2, candidates[0]);
|
|
1271
|
+
if (candidates.length > 1) return { ...link2, status: "ambiguous", fragment: parsed.fragment, candidates };
|
|
1272
|
+
return { ...link2, status: "unresolved", fragment: parsed.fragment };
|
|
1273
|
+
}
|
|
1274
|
+
const linked = link2.syntax === "wikilink" ? resolvePathLikeWikilink(target, currentDocument.path, documents) : resolveWorkspaceDocumentLink(target, currentDocument.path, documents);
|
|
1275
|
+
return linked ? resolvedLink(link2, linked) : { ...link2, status: "unresolved", fragment: parsed.fragment };
|
|
1276
|
+
}
|
|
1277
|
+
function resolvedLink(link2, target) {
|
|
1278
|
+
return {
|
|
1279
|
+
...link2,
|
|
1280
|
+
status: "resolved",
|
|
1281
|
+
...target.fragment ? { fragment: target.fragment } : {},
|
|
1282
|
+
targetDocId: target.docId,
|
|
1283
|
+
targetPath: target.path,
|
|
1284
|
+
targetTitle: target.title
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
function toGraphEdge(document, link2, index) {
|
|
1288
|
+
return {
|
|
1289
|
+
id: `${document.docId}:${index}`,
|
|
1290
|
+
sourceDocId: document.docId,
|
|
1291
|
+
sourcePath: document.path,
|
|
1292
|
+
sourceTitle: document.title,
|
|
1293
|
+
label: link2.label,
|
|
1294
|
+
target: link2.target,
|
|
1295
|
+
...link2.title ? { title: link2.title } : {},
|
|
1296
|
+
line: link2.line,
|
|
1297
|
+
column: link2.column,
|
|
1298
|
+
syntax: link2.syntax,
|
|
1299
|
+
status: link2.status,
|
|
1300
|
+
...link2.fragment ? { fragment: link2.fragment } : {},
|
|
1301
|
+
...link2.targetDocId ? { targetDocId: link2.targetDocId } : {},
|
|
1302
|
+
...link2.targetPath ? { targetPath: link2.targetPath } : {},
|
|
1303
|
+
...link2.targetTitle ? { targetTitle: link2.targetTitle } : {},
|
|
1304
|
+
...link2.candidates ? { candidates: link2.candidates } : {}
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
function resolveWorkspaceDocumentLink(target, currentPath, documents) {
|
|
1308
|
+
const parsed = parseInternalTarget(target);
|
|
1309
|
+
if (!parsed || !parsed.path) return void 0;
|
|
1310
|
+
const workspacePath = resolveWorkspacePath(parsed.path, currentPath);
|
|
1311
|
+
if (!workspacePath) return void 0;
|
|
1312
|
+
const document = findLinkedDocument(candidateDocumentPaths(workspacePath), documents);
|
|
1313
|
+
return document ? { ...document, ...parsed.fragment ? { fragment: parsed.fragment } : {} } : void 0;
|
|
1314
|
+
}
|
|
1315
|
+
function resolvePathLikeWikilink(target, currentPath, documents) {
|
|
1316
|
+
const linked = resolveWorkspaceDocumentLink(target, currentPath, documents);
|
|
1317
|
+
if (linked) return linked;
|
|
1318
|
+
if (/^(?:\/|\.\/|\.\.\/)/.test(target)) return void 0;
|
|
1319
|
+
return resolveWorkspaceDocumentLink(`/${target}`, currentPath, documents);
|
|
1320
|
+
}
|
|
1321
|
+
function findBasenameWikilinkCandidates(name, fragment, documents) {
|
|
1322
|
+
const normalizedName = normalizeWikilinkName(name);
|
|
1323
|
+
return documents.filter((doc) => normalizeWikilinkName(markdownStem(doc.path)) === normalizedName).map((doc) => ({ ...doc, ...fragment ? { fragment } : {} })).sort((left, right) => left.path.localeCompare(right.path, void 0, { numeric: true, sensitivity: "base" }));
|
|
1324
|
+
}
|
|
1325
|
+
function findLinkedDocument(paths, documents) {
|
|
1326
|
+
for (const path of paths) {
|
|
1327
|
+
const document = documents.find((candidate) => candidate.path === path);
|
|
1328
|
+
if (document) return document;
|
|
1329
|
+
}
|
|
1330
|
+
return void 0;
|
|
1331
|
+
}
|
|
1332
|
+
function parseInternalTarget(target) {
|
|
1333
|
+
const trimmed = target.trim();
|
|
1334
|
+
if (!trimmed) return void 0;
|
|
1335
|
+
const hashIndex = trimmed.indexOf("#");
|
|
1336
|
+
const withoutFragment = hashIndex >= 0 ? trimmed.slice(0, hashIndex) : trimmed;
|
|
1337
|
+
const fragment = hashIndex >= 0 ? safeDecode(trimmed.slice(hashIndex + 1)) : void 0;
|
|
1338
|
+
const queryIndex = withoutFragment.indexOf("?");
|
|
1339
|
+
const path = safeDecode(queryIndex >= 0 ? withoutFragment.slice(0, queryIndex) : withoutFragment);
|
|
1340
|
+
return { path, ...fragment ? { fragment } : {} };
|
|
1341
|
+
}
|
|
1342
|
+
function resolveWorkspacePath(linkPath, currentPath) {
|
|
1343
|
+
const path = linkPath.replaceAll("\\", "/").trim();
|
|
1344
|
+
if (!path) return void 0;
|
|
1345
|
+
const source = path.startsWith("/") ? path.slice(1) : `${dirname2(currentPath)}/${path}`;
|
|
1346
|
+
return normalizeWorkspacePath2(source);
|
|
1347
|
+
}
|
|
1348
|
+
function normalizeWorkspacePath2(path) {
|
|
1349
|
+
const segments = [];
|
|
1350
|
+
for (const segment of path.split("/")) {
|
|
1351
|
+
if (!segment || segment === ".") continue;
|
|
1352
|
+
if (segment === "..") {
|
|
1353
|
+
if (segments.length === 0) return void 0;
|
|
1354
|
+
segments.pop();
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
segments.push(segment);
|
|
1358
|
+
}
|
|
1359
|
+
return segments.join("/");
|
|
1360
|
+
}
|
|
1361
|
+
function candidateDocumentPaths(path) {
|
|
1362
|
+
const candidates = [path];
|
|
1363
|
+
if (path.endsWith("/")) {
|
|
1364
|
+
candidates.push(`${path}README.md`, `${path}index.md`);
|
|
1365
|
+
} else if (!/\.[^/.]+$/.test(path)) {
|
|
1366
|
+
candidates.push(`${path}.md`, `${path}.mdx`, `${path}/README.md`, `${path}/index.md`);
|
|
1367
|
+
}
|
|
1368
|
+
return [...new Set(candidates)];
|
|
1369
|
+
}
|
|
1370
|
+
function isPathLikeWikilinkTarget(path) {
|
|
1371
|
+
return /^(?:\/|\.\/|\.\.\/)/.test(path) || path.includes("/") || path.includes("\\") || /\.(md|mdx)$/i.test(path);
|
|
1372
|
+
}
|
|
1373
|
+
function isExternalTarget(target) {
|
|
1374
|
+
const trimmed = target.trim();
|
|
1375
|
+
if (!trimmed || trimmed.startsWith("#")) return false;
|
|
1376
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(trimmed) || trimmed.startsWith("//") || looksLikeBareDomain(trimmed);
|
|
1377
|
+
}
|
|
1378
|
+
function looksLikeBareDomain(value) {
|
|
1379
|
+
return /^[a-z0-9-]+(?:\.[a-z0-9-]+)+(?:[/?#].*)?$/i.test(value) && !/\.(md|mdx)(?:[?#]|$)/i.test(value);
|
|
1380
|
+
}
|
|
1381
|
+
function markdownStem(path) {
|
|
1382
|
+
const basename3 = path.split("/").pop() ?? path;
|
|
1383
|
+
return basename3.replace(/\.mdx?$/i, "");
|
|
1384
|
+
}
|
|
1385
|
+
function normalizeWikilinkName(value) {
|
|
1386
|
+
return value.trim().toLowerCase();
|
|
1387
|
+
}
|
|
1388
|
+
function dirname2(path) {
|
|
1389
|
+
const index = path.lastIndexOf("/");
|
|
1390
|
+
return index >= 0 ? path.slice(0, index) : "";
|
|
1391
|
+
}
|
|
1392
|
+
function safeDecode(value) {
|
|
1393
|
+
try {
|
|
1394
|
+
return decodeURIComponent(value);
|
|
1395
|
+
} catch {
|
|
1396
|
+
return value;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
842
1399
|
|
|
843
1400
|
// ../core/src/sidecar.ts
|
|
844
1401
|
function defaultActor(name = "mdocs") {
|
|
@@ -916,9 +1473,12 @@ async function indexWorkspace(io) {
|
|
|
916
1473
|
}
|
|
917
1474
|
async function getWorkspaceMap(io) {
|
|
918
1475
|
const manifest = await indexWorkspace(io);
|
|
1476
|
+
const graph = await workspaceGraphForManifest(io, manifest);
|
|
1477
|
+
const graphNodesByDocId = new Map(graph.nodes.map((node) => [node.docId, node]));
|
|
919
1478
|
const docs = await Promise.all(
|
|
920
1479
|
manifest.docs.map(async (entry) => {
|
|
921
1480
|
const sidecar = await readSidecar(io, entry.docId);
|
|
1481
|
+
const graphNode = graphNodesByDocId.get(entry.docId);
|
|
922
1482
|
return {
|
|
923
1483
|
docId: entry.docId,
|
|
924
1484
|
path: entry.path,
|
|
@@ -926,7 +1486,11 @@ async function getWorkspaceMap(io) {
|
|
|
926
1486
|
sidecarPath: sidecarPath(entry.docId),
|
|
927
1487
|
openComments: sidecar.comments.filter((comment2) => comment2.status === "open").length,
|
|
928
1488
|
openSuggestions: sidecar.suggestions.filter((suggestion) => suggestion.status === "open").length,
|
|
929
|
-
anchorsNeedingReview: sidecar.anchors.filter((anchor) => anchor.status !== "mapped").length
|
|
1489
|
+
anchorsNeedingReview: sidecar.anchors.filter((anchor) => anchor.status !== "mapped").length,
|
|
1490
|
+
outgoingLinks: graphNode?.outgoingLinks ?? 0,
|
|
1491
|
+
incomingLinks: graphNode?.incomingLinks ?? 0,
|
|
1492
|
+
unresolvedLinks: graphNode?.unresolvedLinks ?? 0,
|
|
1493
|
+
externalLinks: graphNode?.externalLinks ?? 0
|
|
930
1494
|
};
|
|
931
1495
|
})
|
|
932
1496
|
);
|
|
@@ -936,6 +1500,9 @@ async function getWorkspaceMap(io) {
|
|
|
936
1500
|
docs
|
|
937
1501
|
};
|
|
938
1502
|
}
|
|
1503
|
+
async function getWorkspaceGraph(io) {
|
|
1504
|
+
return workspaceGraphForManifest(io, await indexWorkspace(io));
|
|
1505
|
+
}
|
|
939
1506
|
async function getDocumentState(io, pathOrDocId) {
|
|
940
1507
|
const manifest = await indexWorkspace(io);
|
|
941
1508
|
const entry = findDoc(manifest, pathOrDocId);
|
|
@@ -964,10 +1531,31 @@ async function getDocumentState(io, pathOrDocId) {
|
|
|
964
1531
|
confidence: anchor.confidence
|
|
965
1532
|
})),
|
|
966
1533
|
images: extractMarkdownImages(markdown, entry.path),
|
|
1534
|
+
links: resolveMarkdownLinks(extractMarkdownLinks(markdown), entry, manifest.docs),
|
|
967
1535
|
openComments: nextSidecar.comments.filter((comment2) => comment2.status === "open").length,
|
|
968
1536
|
openSuggestions: nextSidecar.suggestions.filter((suggestion) => suggestion.status === "open").length
|
|
969
1537
|
};
|
|
970
1538
|
}
|
|
1539
|
+
async function workspaceGraphForManifest(io, manifest) {
|
|
1540
|
+
const documents = await Promise.all(
|
|
1541
|
+
manifest.docs.map(async (entry) => {
|
|
1542
|
+
const [markdown, sidecar] = await Promise.all([io.readText(entry.path), readSidecar(io, entry.docId)]);
|
|
1543
|
+
const links = resolveMarkdownLinks(extractMarkdownLinks(markdown), entry, manifest.docs);
|
|
1544
|
+
return {
|
|
1545
|
+
docId: entry.docId,
|
|
1546
|
+
path: entry.path,
|
|
1547
|
+
title: entry.title,
|
|
1548
|
+
openComments: sidecar.comments.filter((comment2) => comment2.status === "open").length,
|
|
1549
|
+
openSuggestions: sidecar.suggestions.filter((suggestion) => suggestion.status === "open").length,
|
|
1550
|
+
anchorsNeedingReview: sidecar.anchors.filter((anchor) => anchor.status !== "mapped").length,
|
|
1551
|
+
externalLinks: links.filter((link2) => link2.status === "external").length,
|
|
1552
|
+
unresolvedLinks: links.filter((link2) => link2.status === "unresolved" || link2.status === "ambiguous").length,
|
|
1553
|
+
links
|
|
1554
|
+
};
|
|
1555
|
+
})
|
|
1556
|
+
);
|
|
1557
|
+
return buildWorkspaceGraph(manifest.workspaceId, manifest.schemaVersion, documents);
|
|
1558
|
+
}
|
|
971
1559
|
async function addComment(io, pathOrDocId, range, body, author = defaultActor()) {
|
|
972
1560
|
const state = await getDocumentState(io, pathOrDocId);
|
|
973
1561
|
assertLineRangeWithin(state.markdown, range);
|
|
@@ -1001,6 +1589,8 @@ async function addComment(io, pathOrDocId, range, body, author = defaultActor())
|
|
|
1001
1589
|
}
|
|
1002
1590
|
async function addSuggestion(io, pathOrDocId, range, replacement, message, author = { id: "agent_local", kind: "agent", name: "Local Agent" }, changeSetId) {
|
|
1003
1591
|
const state = await getDocumentState(io, pathOrDocId);
|
|
1592
|
+
const manifest = await indexWorkspace(io);
|
|
1593
|
+
const entry = manifest.docs.find((doc) => doc.docId === state.docId);
|
|
1004
1594
|
assertLineRangeWithin(state.markdown, range);
|
|
1005
1595
|
assertCleanMarkdown(replacement);
|
|
1006
1596
|
const now = nowIso();
|
|
@@ -1026,6 +1616,10 @@ async function addSuggestion(io, pathOrDocId, range, replacement, message, autho
|
|
|
1026
1616
|
before,
|
|
1027
1617
|
after: replacement
|
|
1028
1618
|
},
|
|
1619
|
+
base: {
|
|
1620
|
+
contentHash: contentHashForText(state.markdown),
|
|
1621
|
+
...entry?.currentSha ? { head: entry.currentSha } : {}
|
|
1622
|
+
},
|
|
1029
1623
|
changeSetId,
|
|
1030
1624
|
createdAt: now,
|
|
1031
1625
|
updatedAt: now
|
|
@@ -1068,7 +1662,8 @@ async function acceptSuggestion(io, suggestionId, actor = defaultActor()) {
|
|
|
1068
1662
|
const sidecar = await readSidecar(io, entry.docId);
|
|
1069
1663
|
const suggestion = sidecar.suggestions.find((candidate) => candidate.id === suggestionId);
|
|
1070
1664
|
if (!suggestion) continue;
|
|
1071
|
-
const
|
|
1665
|
+
const anchor = sidecar.anchors.find((candidate) => candidate.id === suggestion.anchorId);
|
|
1666
|
+
const nextMarkdown = applySuggestion(markdown, suggestion, anchor);
|
|
1072
1667
|
await io.writeText(entry.path, nextMarkdown);
|
|
1073
1668
|
return updateSuggestionStatus(io, suggestionId, "accepted", actor);
|
|
1074
1669
|
}
|
|
@@ -1192,6 +1787,105 @@ function sidecarMarkdownMatchScore(sidecar, markdown) {
|
|
|
1192
1787
|
return score;
|
|
1193
1788
|
}
|
|
1194
1789
|
|
|
1790
|
+
// ../core/src/context.ts
|
|
1791
|
+
function sliceMarkdown(markdown, startLine, endLine) {
|
|
1792
|
+
const lines = markdown.split("\n");
|
|
1793
|
+
const totalLines = lines.length;
|
|
1794
|
+
const sl = startLine ?? 1;
|
|
1795
|
+
const el = endLine ?? totalLines;
|
|
1796
|
+
if (sl < 1 || el > totalLines || sl > el) {
|
|
1797
|
+
throw new MdocsError(
|
|
1798
|
+
"invalid_range",
|
|
1799
|
+
`Line range ${sl}:${el} is out of bounds for a ${totalLines}-line document.`,
|
|
1800
|
+
`Use startLine/endLine or --start-line/--end-line with 1-based line numbers within 1:${totalLines}.`
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1803
|
+
return { markdown: lines.slice(sl - 1, el).join("\n"), totalLines, startLine: sl, endLine: el };
|
|
1804
|
+
}
|
|
1805
|
+
function summarizeDocumentContext(document) {
|
|
1806
|
+
const comments = openComments(document);
|
|
1807
|
+
const suggestions = openSuggestions(document);
|
|
1808
|
+
const currentSha = currentDocumentSha(document);
|
|
1809
|
+
return {
|
|
1810
|
+
docId: document.docId,
|
|
1811
|
+
path: document.path,
|
|
1812
|
+
title: document.title,
|
|
1813
|
+
...currentSha ? { currentSha } : {},
|
|
1814
|
+
totalLines: document.markdown.split("\n").length,
|
|
1815
|
+
byteLength: new TextEncoder().encode(document.markdown).byteLength,
|
|
1816
|
+
headings: extractMarkdownHeadings(document.markdown),
|
|
1817
|
+
reviewCounts: {
|
|
1818
|
+
openComments: comments.length,
|
|
1819
|
+
openSuggestions: suggestions.length,
|
|
1820
|
+
anchorsNeedingReview: document.anchors.filter((anchor) => anchor.status !== "mapped").length
|
|
1821
|
+
},
|
|
1822
|
+
imageCounts: imageCounts(document.images),
|
|
1823
|
+
linkCounts: linkCounts(document.links)
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
function reviewStateForDocument(document) {
|
|
1827
|
+
const currentSha = currentDocumentSha(document);
|
|
1828
|
+
return {
|
|
1829
|
+
docId: document.docId,
|
|
1830
|
+
path: document.path,
|
|
1831
|
+
title: document.title,
|
|
1832
|
+
...currentSha ? { currentSha } : {},
|
|
1833
|
+
comments: openComments(document),
|
|
1834
|
+
suggestions: openSuggestions(document),
|
|
1835
|
+
anchors: document.anchors
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
function currentDocumentSha(document) {
|
|
1839
|
+
return "currentSha" in document ? document.currentSha : void 0;
|
|
1840
|
+
}
|
|
1841
|
+
function extractMarkdownHeadings(markdown) {
|
|
1842
|
+
const headings = [];
|
|
1843
|
+
let fence2;
|
|
1844
|
+
const lines = markdown.split("\n");
|
|
1845
|
+
lines.forEach((line, index) => {
|
|
1846
|
+
const fenceMatch = /^( {0,3})(`{3,}|~{3,})/.exec(line);
|
|
1847
|
+
if (fenceMatch) {
|
|
1848
|
+
const marker = fenceMatch[2]?.[0];
|
|
1849
|
+
if (!fence2) fence2 = marker;
|
|
1850
|
+
else if (marker === fence2) fence2 = void 0;
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
if (fence2) return;
|
|
1854
|
+
const match2 = /^(#{1,6})[ \t]+(.+?)\s*#*\s*$/.exec(line);
|
|
1855
|
+
if (!match2) return;
|
|
1856
|
+
headings.push({
|
|
1857
|
+
level: match2[1].length,
|
|
1858
|
+
text: match2[2].trim(),
|
|
1859
|
+
line: index + 1
|
|
1860
|
+
});
|
|
1861
|
+
});
|
|
1862
|
+
return headings;
|
|
1863
|
+
}
|
|
1864
|
+
function openComments(document) {
|
|
1865
|
+
return document.sidecar.comments.filter((comment2) => comment2.status === "open");
|
|
1866
|
+
}
|
|
1867
|
+
function openSuggestions(document) {
|
|
1868
|
+
return document.sidecar.suggestions.filter((suggestion) => suggestion.status === "open");
|
|
1869
|
+
}
|
|
1870
|
+
function imageCounts(images) {
|
|
1871
|
+
const local = images.filter((image2) => image2.isLocal).length;
|
|
1872
|
+
return {
|
|
1873
|
+
total: images.length,
|
|
1874
|
+
local,
|
|
1875
|
+
remote: images.length - local
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
function linkCounts(links) {
|
|
1879
|
+
return {
|
|
1880
|
+
total: links.length,
|
|
1881
|
+
resolved: links.filter((link2) => link2.status === "resolved").length,
|
|
1882
|
+
ambiguous: links.filter((link2) => link2.status === "ambiguous").length,
|
|
1883
|
+
unresolved: links.filter((link2) => link2.status === "unresolved").length,
|
|
1884
|
+
external: links.filter((link2) => link2.status === "external").length,
|
|
1885
|
+
anchors: links.filter((link2) => link2.status === "anchor").length
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1195
1889
|
// ../core/src/root-policy.ts
|
|
1196
1890
|
var DEFAULT_ROOT_PATH_RULES = {
|
|
1197
1891
|
include: ["**/*.md", "**/*.mdx", ".mdocs/**/*.json"],
|
|
@@ -1234,7 +1928,7 @@ var DEFAULT_ROOT_PATH_RULES = {
|
|
|
1234
1928
|
maxFileSizeBytes: 1024 * 1024
|
|
1235
1929
|
};
|
|
1236
1930
|
var MARKDOWN_FILE_PATTERN = /\.mdx?$/i;
|
|
1237
|
-
function
|
|
1931
|
+
function normalizeWorkspacePath3(path) {
|
|
1238
1932
|
const normalized = path.replaceAll("\\", "/").replace(/\/+/g, "/").trim();
|
|
1239
1933
|
if (!normalized) throw new Error("Workspace path cannot be empty.");
|
|
1240
1934
|
if (normalized.startsWith("/") || /^[a-zA-Z]:\//.test(normalized)) {
|
|
@@ -1247,7 +1941,7 @@ function normalizeWorkspacePath2(path) {
|
|
|
1247
1941
|
return parts.filter((part) => part && part !== ".").join("/");
|
|
1248
1942
|
}
|
|
1249
1943
|
function assertWorkspacePathAllowed(path, rules = DEFAULT_ROOT_PATH_RULES, sizeBytes, ignoreMatcher) {
|
|
1250
|
-
const normalized =
|
|
1944
|
+
const normalized = normalizeWorkspacePath3(path);
|
|
1251
1945
|
if (isIgnoredWorkspacePath(normalized, rules)) {
|
|
1252
1946
|
throw new Error(`Workspace path is ignored by root policy: ${normalized}`);
|
|
1253
1947
|
}
|
|
@@ -1278,7 +1972,7 @@ function isWorkspaceMarkdownPathAllowed(path, rules = DEFAULT_ROOT_PATH_RULES, s
|
|
|
1278
1972
|
function createWorkspaceIgnoreMatcher(ignoreFiles) {
|
|
1279
1973
|
const rules = parseWorkspaceIgnoreRules(ignoreFiles);
|
|
1280
1974
|
return (path) => {
|
|
1281
|
-
const normalized =
|
|
1975
|
+
const normalized = normalizeWorkspacePath3(path);
|
|
1282
1976
|
let ignored = false;
|
|
1283
1977
|
for (const rule of rules) {
|
|
1284
1978
|
if (!matchesWorkspaceIgnoreRule(normalized, rule)) continue;
|
|
@@ -1288,7 +1982,7 @@ function createWorkspaceIgnoreMatcher(ignoreFiles) {
|
|
|
1288
1982
|
};
|
|
1289
1983
|
}
|
|
1290
1984
|
function isIgnoredWorkspacePath(path, rules = DEFAULT_ROOT_PATH_RULES) {
|
|
1291
|
-
const normalized =
|
|
1985
|
+
const normalized = normalizeWorkspacePath3(path);
|
|
1292
1986
|
const parts = normalized.split("/");
|
|
1293
1987
|
return parts.some((part) => rules.ignoredDirectories.includes(part));
|
|
1294
1988
|
}
|
|
@@ -1308,8 +2002,8 @@ function matchesAny(path, patterns) {
|
|
|
1308
2002
|
return patterns.some((pattern) => globToRegExp(pattern).test(path));
|
|
1309
2003
|
}
|
|
1310
2004
|
function parseWorkspaceIgnoreRules(ignoreFiles) {
|
|
1311
|
-
return [...ignoreFiles].sort((left, right) =>
|
|
1312
|
-
const normalizedPath =
|
|
2005
|
+
return [...ignoreFiles].sort((left, right) => normalizeWorkspacePath3(left.path).localeCompare(normalizeWorkspacePath3(right.path))).flatMap((file) => {
|
|
2006
|
+
const normalizedPath = normalizeWorkspacePath3(file.path);
|
|
1313
2007
|
const basePath = normalizedPath.endsWith("/.gitignore") ? normalizedPath.slice(0, -"/.gitignore".length) : "";
|
|
1314
2008
|
return file.content.split(/\r?\n/).flatMap((line) => {
|
|
1315
2009
|
const rule = parseWorkspaceIgnoreRule(basePath, line);
|
|
@@ -1416,14 +2110,14 @@ function createSourceMapping(input, updatedAt = (/* @__PURE__ */ new Date()).toI
|
|
|
1416
2110
|
};
|
|
1417
2111
|
}
|
|
1418
2112
|
function canonicalPathToReplicaPath(path, mapping) {
|
|
1419
|
-
if (path === ".mdocs" || path.startsWith(".mdocs/")) return
|
|
1420
|
-
const canonicalPath =
|
|
2113
|
+
if (path === ".mdocs" || path.startsWith(".mdocs/")) return normalizeWorkspacePath3(path);
|
|
2114
|
+
const canonicalPath = normalizeWorkspacePath3(path);
|
|
1421
2115
|
const relativePath = stripPrefix(canonicalPath, normalizeOptionalPrefix(mapping.canonicalPrefix), "canonical");
|
|
1422
2116
|
return joinWorkspacePath(normalizeOptionalPrefix(mapping.replicaPrefix), relativePath);
|
|
1423
2117
|
}
|
|
1424
2118
|
function replicaPathToCanonicalPath(path, mapping) {
|
|
1425
|
-
if (path === ".mdocs" || path.startsWith(".mdocs/")) return
|
|
1426
|
-
const replicaPath =
|
|
2119
|
+
if (path === ".mdocs" || path.startsWith(".mdocs/")) return normalizeWorkspacePath3(path);
|
|
2120
|
+
const replicaPath = normalizeWorkspacePath3(path);
|
|
1427
2121
|
const relativePath = stripPrefix(replicaPath, normalizeOptionalPrefix(mapping.replicaPrefix), "replica");
|
|
1428
2122
|
return joinWorkspacePath(normalizeOptionalPrefix(mapping.canonicalPrefix), relativePath);
|
|
1429
2123
|
}
|
|
@@ -1438,7 +2132,7 @@ function sourceMappingPathRules(mapping, base) {
|
|
|
1438
2132
|
function normalizeOptionalPrefix(prefix) {
|
|
1439
2133
|
const value = prefix?.trim();
|
|
1440
2134
|
if (!value || value === ".") return "";
|
|
1441
|
-
return
|
|
2135
|
+
return normalizeWorkspacePath3(value).replace(/\/+$/, "");
|
|
1442
2136
|
}
|
|
1443
2137
|
function stripPrefix(path, prefix, label) {
|
|
1444
2138
|
if (!prefix) return path;
|
|
@@ -1447,21 +2141,27 @@ function stripPrefix(path, prefix, label) {
|
|
|
1447
2141
|
throw new Error(`Path ${path} is outside ${label} prefix ${prefix}`);
|
|
1448
2142
|
}
|
|
1449
2143
|
function joinWorkspacePath(prefix, path) {
|
|
1450
|
-
if (!prefix) return
|
|
2144
|
+
if (!prefix) return normalizeWorkspacePath3(path);
|
|
1451
2145
|
if (!path) return prefix;
|
|
1452
|
-
return
|
|
2146
|
+
return normalizeWorkspacePath3(`${prefix}/${path}`);
|
|
1453
2147
|
}
|
|
1454
2148
|
|
|
1455
2149
|
// ../core/src/share-routes.ts
|
|
1456
2150
|
function parseSharePath(pathname) {
|
|
1457
|
-
const [, workspaceId, rootId, docId] = pathname.match(/^\/share\/([^/]+)
|
|
1458
|
-
if (!workspaceId
|
|
2151
|
+
const [, workspaceId, rootId, docId] = pathname.match(/^\/share\/([^/]+)(?:\/([^/]+)(?:\/([^/]+))?)?\/?$/) ?? [];
|
|
2152
|
+
if (!workspaceId) return void 0;
|
|
1459
2153
|
return {
|
|
1460
2154
|
workspaceId: decodeURIComponent(workspaceId),
|
|
1461
|
-
rootId: decodeURIComponent(rootId),
|
|
2155
|
+
rootId: rootId ? decodeURIComponent(rootId) : void 0,
|
|
1462
2156
|
docId: docId ? decodeURIComponent(docId) : void 0
|
|
1463
2157
|
};
|
|
1464
2158
|
}
|
|
2159
|
+
function buildSharePath(workspaceId, rootId, docId) {
|
|
2160
|
+
const workspacePath = `/share/${encodeURIComponent(workspaceId)}`;
|
|
2161
|
+
if (!rootId) return workspacePath;
|
|
2162
|
+
const base = `${workspacePath}/${encodeURIComponent(rootId)}`;
|
|
2163
|
+
return docId ? `${base}/${encodeURIComponent(docId)}` : base;
|
|
2164
|
+
}
|
|
1465
2165
|
|
|
1466
2166
|
// ../../node_modules/orderedmap/dist/index.js
|
|
1467
2167
|
function OrderedMap(content) {
|
|
@@ -3901,7 +4601,10 @@ function gatherMarks(schema2, marks) {
|
|
|
3901
4601
|
// ../core/src/pm-schema.ts
|
|
3902
4602
|
var canonicalSchema = new Schema({
|
|
3903
4603
|
nodes: {
|
|
3904
|
-
doc
|
|
4604
|
+
// The doc allows suggestion marks on its block children so pending
|
|
4605
|
+
// block-level suggestions (node marks) survive fromJSON and can be
|
|
4606
|
+
// structurally reverted before serialization.
|
|
4607
|
+
doc: { content: "block+", marks: "insertion deletion modification" },
|
|
3905
4608
|
paragraph: { group: "block", content: "inline*" },
|
|
3906
4609
|
heading: {
|
|
3907
4610
|
group: "block",
|
|
@@ -3969,14 +4672,45 @@ var canonicalSchema = new Schema({
|
|
|
3969
4672
|
},
|
|
3970
4673
|
inclusive: false
|
|
3971
4674
|
},
|
|
4675
|
+
wikilink: {
|
|
4676
|
+
attrs: {
|
|
4677
|
+
target: { default: null }
|
|
4678
|
+
},
|
|
4679
|
+
inclusive: false
|
|
4680
|
+
},
|
|
3972
4681
|
bold: {},
|
|
3973
4682
|
italic: {},
|
|
3974
4683
|
strike: {},
|
|
3975
|
-
code: {}
|
|
4684
|
+
code: {},
|
|
4685
|
+
// Pending-suggestion marks (mirroring the web editor's extensions). They
|
|
4686
|
+
// are never serialized — the canonical serializer reverts them first —
|
|
4687
|
+
// but the schema must know them so suggestion-marked docs load intact.
|
|
4688
|
+
insertion: {
|
|
4689
|
+
attrs: { id: { default: null } },
|
|
4690
|
+
inclusive: false,
|
|
4691
|
+
excludes: "deletion modification insertion"
|
|
4692
|
+
},
|
|
4693
|
+
deletion: {
|
|
4694
|
+
attrs: { id: { default: null } },
|
|
4695
|
+
inclusive: false,
|
|
4696
|
+
excludes: "insertion modification deletion"
|
|
4697
|
+
},
|
|
4698
|
+
modification: {
|
|
4699
|
+
attrs: {
|
|
4700
|
+
id: { default: null },
|
|
4701
|
+
type: { default: null },
|
|
4702
|
+
attrName: { default: null },
|
|
4703
|
+
previousValue: { default: null },
|
|
4704
|
+
newValue: { default: null }
|
|
4705
|
+
},
|
|
4706
|
+
inclusive: false,
|
|
4707
|
+
excludes: "deletion insertion"
|
|
4708
|
+
}
|
|
3976
4709
|
}
|
|
3977
4710
|
});
|
|
4711
|
+
var SUGGESTION_MARK_TYPES = /* @__PURE__ */ new Set(["insertion", "deletion", "modification"]);
|
|
3978
4712
|
var CANONICAL_NODE_TYPES = new Set(Object.keys(canonicalSchema.nodes));
|
|
3979
|
-
var CANONICAL_MARK_TYPES = new Set(Object.keys(canonicalSchema.marks));
|
|
4713
|
+
var CANONICAL_MARK_TYPES = new Set(Object.keys(canonicalSchema.marks).filter((name) => !SUGGESTION_MARK_TYPES.has(name)));
|
|
3980
4714
|
|
|
3981
4715
|
// ../../node_modules/markdown-it/lib/common/utils.mjs
|
|
3982
4716
|
var utils_exports = {};
|
|
@@ -10171,9 +10905,28 @@ var markdownSerializer = new MarkdownSerializer(
|
|
|
10171
10905
|
return `](${href.replace(/[()"]/g, "\\$&")})`;
|
|
10172
10906
|
},
|
|
10173
10907
|
mixable: false
|
|
10908
|
+
},
|
|
10909
|
+
wikilink: {
|
|
10910
|
+
open(_state, mark, parent, index) {
|
|
10911
|
+
const target = typeof mark.attrs.target === "string" ? mark.attrs.target : "";
|
|
10912
|
+
const label = markedRangeText(parent, index, mark);
|
|
10913
|
+
if (!target || label === target) return "[[";
|
|
10914
|
+
return `[[${target}|`;
|
|
10915
|
+
},
|
|
10916
|
+
close: "]]",
|
|
10917
|
+
mixable: false
|
|
10174
10918
|
}
|
|
10175
10919
|
}
|
|
10176
10920
|
);
|
|
10921
|
+
function markedRangeText(parent, index, mark) {
|
|
10922
|
+
let text2 = "";
|
|
10923
|
+
for (let childIndex = index; childIndex < parent.childCount; childIndex += 1) {
|
|
10924
|
+
const child = parent.child(childIndex);
|
|
10925
|
+
if (!mark.isInSet(child.marks)) break;
|
|
10926
|
+
text2 += child.textContent;
|
|
10927
|
+
}
|
|
10928
|
+
return text2;
|
|
10929
|
+
}
|
|
10177
10930
|
function escapeImageAlt(value) {
|
|
10178
10931
|
return value.replace(/\\/g, "\\\\").replace(/\[/g, "\\[").replace(/]/g, "\\]").replace(/\n+/g, " ");
|
|
10179
10932
|
}
|
|
@@ -10237,7 +10990,7 @@ var RemoteDocumentIO = class {
|
|
|
10237
10990
|
};
|
|
10238
10991
|
|
|
10239
10992
|
// src/agent.ts
|
|
10240
|
-
var CLI_VERSION = "0.3.
|
|
10993
|
+
var CLI_VERSION = "0.3.5";
|
|
10241
10994
|
var CLI_PACKAGE_NAME = "@magic-markdown/cli";
|
|
10242
10995
|
var AGENT_COMMANDS = [
|
|
10243
10996
|
{
|
|
@@ -10267,11 +11020,12 @@ var AGENT_COMMANDS = [
|
|
|
10267
11020
|
{
|
|
10268
11021
|
name: "join",
|
|
10269
11022
|
summary: "Join a Magic Markdown share URL through the CLI, announce presence, and remember the session for later rejoin.",
|
|
10270
|
-
usage: "mdocs join <share-url> --scope file|project --doc <docId> --name <agent-name> --json",
|
|
11023
|
+
usage: "mdocs join <share-url> --scope file|project|workspace --doc <docId> --name <agent-name> --json",
|
|
10271
11024
|
output: "json",
|
|
10272
11025
|
mutates: true,
|
|
10273
11026
|
examples: [
|
|
10274
11027
|
'mdocs join https://example.com/share/workspace/root/doc --scope file --doc doc_abc123 --name "Claude Code" --json',
|
|
11028
|
+
'mdocs join https://example.com/share/workspace --scope workspace --name "Claude Code" --json',
|
|
10275
11029
|
"mdocs join --json"
|
|
10276
11030
|
],
|
|
10277
11031
|
notes: [
|
|
@@ -10288,26 +11042,40 @@ var AGENT_COMMANDS = [
|
|
|
10288
11042
|
},
|
|
10289
11043
|
{
|
|
10290
11044
|
name: "remote",
|
|
10291
|
-
summary: "Read, comment on, suggest edits to, monitor, and restore the active Magic Markdown remote join.",
|
|
10292
|
-
usage: "mdocs remote map|context|comment|suggest|reject|events|history|restore --json",
|
|
11045
|
+
summary: "Read, organize, comment on, suggest edits to, monitor, and restore the active Magic Markdown remote join.",
|
|
11046
|
+
usage: "mdocs remote map|graph|context|review|create-file|move-file|comment|suggest|reject|events|history|restore|library|create-folder|update-folder|move-root|invite-folder --json",
|
|
10293
11047
|
output: "json",
|
|
10294
11048
|
mutates: true,
|
|
10295
11049
|
examples: [
|
|
10296
|
-
"mdocs remote context --json",
|
|
10297
|
-
"mdocs remote context --start-line 1 --end-line 100 --json",
|
|
10298
|
-
"mdocs remote context --start-line 101 --end-line 200 --json",
|
|
11050
|
+
"mdocs remote context --summary --json",
|
|
11051
|
+
"mdocs remote context --start-line 1 --end-line 100 --no-review --json",
|
|
11052
|
+
"mdocs remote context --start-line 101 --end-line 200 --no-review --json",
|
|
11053
|
+
"mdocs remote review --json",
|
|
10299
11054
|
"mdocs remote map --json",
|
|
11055
|
+
"mdocs remote graph --json",
|
|
11056
|
+
"mdocs remote create-file docs/new-note.md --with-file /tmp/initial.md --json",
|
|
11057
|
+
"mdocs remote move-file docs/new-note.md --to archive/new-note.md --json",
|
|
10300
11058
|
"mdocs remote suggest --range 4:9 --with-file /tmp/replacement.md --message-file /tmp/message.txt --json",
|
|
10301
11059
|
"mdocs remote comment docs/example.md --range 3:5 --body-file /tmp/comment.txt --json",
|
|
10302
11060
|
"mdocs remote reject suggestion_abc123 --json",
|
|
10303
11061
|
"mdocs remote history --json",
|
|
10304
|
-
"mdocs remote restore head_abc123 --json"
|
|
11062
|
+
"mdocs remote restore head_abc123 --json",
|
|
11063
|
+
"mdocs remote library --json",
|
|
11064
|
+
"mdocs remote create-folder Research --json",
|
|
11065
|
+
"mdocs remote move-root --folder fold_abc123 --json",
|
|
11066
|
+
"mdocs remote invite-folder fold_abc123 --email person@example.com --role edit --json"
|
|
10305
11067
|
],
|
|
10306
11068
|
notes: [
|
|
11069
|
+
"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.",
|
|
10307
11070
|
"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>.",
|
|
11071
|
+
"Use remote context --no-review when reading document content only; use remote review to list open comments, open suggestions, and anchors separately.",
|
|
11072
|
+
"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>.",
|
|
11073
|
+
"remote graph requires a project-scoped join and returns the shared project's Obsidian-style Markdown/wikilink graph.",
|
|
11074
|
+
"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.",
|
|
11075
|
+
"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.",
|
|
10308
11076
|
"remote comment/suggest never clobber concurrent edits: the server merges review additions, and the CLI rebases and retries automatically if a conflict still occurs.",
|
|
10309
11077
|
"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.",
|
|
10310
|
-
"remote events returns truncated: true when older events were dropped from the bounded log; refetch
|
|
11078
|
+
"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.",
|
|
10311
11079
|
"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.",
|
|
10312
11080
|
"remote history lists content-addressed commits (every commit carries a full snapshot); remote restore <headId> restores the joined document to that snapshot as a new commit. Restore requires edit access \u2014 only run it when the user asked."
|
|
10313
11081
|
]
|
|
@@ -10322,28 +11090,51 @@ var AGENT_COMMANDS = [
|
|
|
10322
11090
|
},
|
|
10323
11091
|
{
|
|
10324
11092
|
name: "map",
|
|
10325
|
-
summary: "Return the indexed Markdown documents and
|
|
11093
|
+
summary: "Return the indexed Markdown documents, review counts, and link counts.",
|
|
10326
11094
|
usage: "mdocs map --json",
|
|
10327
11095
|
output: "json",
|
|
10328
11096
|
mutates: true,
|
|
10329
11097
|
examples: ["mdocs map --json"]
|
|
10330
11098
|
},
|
|
11099
|
+
{
|
|
11100
|
+
name: "graph",
|
|
11101
|
+
summary: "Return the workspace knowledge graph as document nodes and Markdown/wikilink edges.",
|
|
11102
|
+
usage: "mdocs graph --json",
|
|
11103
|
+
output: "json",
|
|
11104
|
+
mutates: true,
|
|
11105
|
+
examples: ["mdocs graph --json"],
|
|
11106
|
+
notes: [
|
|
11107
|
+
"Edges include resolved document links, ambiguous wikilinks, unresolved links, external links, and same-document anchor links.",
|
|
11108
|
+
"Use this before broad agent edits to understand the Obsidian-style document graph and avoid changing related notes in isolation."
|
|
11109
|
+
]
|
|
11110
|
+
},
|
|
10331
11111
|
{
|
|
10332
11112
|
name: "context",
|
|
10333
|
-
summary: "Return agent-ready document Markdown,
|
|
10334
|
-
usage: "mdocs context <path|docId> [--start-line N] [--end-line N] --json",
|
|
11113
|
+
summary: "Return agent-ready document Markdown, optional review state, image references, and resolved outgoing links.",
|
|
11114
|
+
usage: "mdocs context <path|docId> [--summary] [--no-review] [--start-line N] [--end-line N] --json",
|
|
10335
11115
|
output: "json",
|
|
10336
11116
|
mutates: true,
|
|
10337
11117
|
examples: [
|
|
11118
|
+
"mdocs context docs/example.md --summary --json",
|
|
10338
11119
|
"mdocs context docs/example.md --json",
|
|
10339
|
-
"mdocs context docs/example.md --start-line 1 --end-line 100 --json",
|
|
10340
|
-
"mdocs context docs/example.md --start-line 101 --end-line 200 --json"
|
|
11120
|
+
"mdocs context docs/example.md --start-line 1 --end-line 100 --no-review --json",
|
|
11121
|
+
"mdocs context docs/example.md --start-line 101 --end-line 200 --no-review --json"
|
|
10341
11122
|
],
|
|
10342
11123
|
notes: [
|
|
11124
|
+
"--summary returns metadata, heading line numbers, review counts, image counts, and link counts without returning Markdown.",
|
|
10343
11125
|
"The response always includes totalLines, startLine, and endLine. If totalLines > endLine, request the next page with --start-line <endLine+1>.",
|
|
10344
|
-
"--start-line and --end-line are 1-based and inclusive. Omitting both returns the full document."
|
|
11126
|
+
"--start-line and --end-line are 1-based and inclusive. Omitting both returns the full document.",
|
|
11127
|
+
"--no-review omits comments, suggestions, and anchors from content reads; use review/comments/suggestions commands when you need review state."
|
|
10345
11128
|
]
|
|
10346
11129
|
},
|
|
11130
|
+
{
|
|
11131
|
+
name: "review",
|
|
11132
|
+
summary: "Return open comments, open suggestions, and anchors for one document without returning Markdown.",
|
|
11133
|
+
usage: "mdocs review <path|docId> --json",
|
|
11134
|
+
output: "json",
|
|
11135
|
+
mutates: true,
|
|
11136
|
+
examples: ["mdocs review docs/example.md --json"]
|
|
11137
|
+
},
|
|
10347
11138
|
{
|
|
10348
11139
|
name: "state",
|
|
10349
11140
|
summary: "Return full merged Markdown and sidecar state for one document.",
|
|
@@ -10376,7 +11167,8 @@ var AGENT_COMMANDS = [
|
|
|
10376
11167
|
],
|
|
10377
11168
|
notes: [
|
|
10378
11169
|
"--range is 1-based and inclusive, validated against the current document line count.",
|
|
10379
|
-
"The replacement text replaces the whole range; re-read context first so the range matches current content."
|
|
11170
|
+
"The replacement text replaces the whole range; re-read context first so the range matches current content.",
|
|
11171
|
+
"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."
|
|
10380
11172
|
]
|
|
10381
11173
|
},
|
|
10382
11174
|
{
|
|
@@ -10493,7 +11285,7 @@ function getAgentGuidePayload() {
|
|
|
10493
11285
|
function getAgentSkillMarkdown() {
|
|
10494
11286
|
return `---
|
|
10495
11287
|
name: magic-markdown
|
|
10496
|
-
description: Use the Magic Markdown CLI or MCP server to read, review, comment on, suggest edits to, and sync clean Markdown workspaces without hand-editing .mdocs sidecars. Use when an agent needs to interact with Magic Markdown documents, comments, suggestions, image context, checkpoints, or local source sync.
|
|
11288
|
+
description: Use the Magic Markdown CLI or MCP server to read, organize, review, comment on, suggest edits to, and sync clean Markdown workspaces without hand-editing .mdocs sidecars. Use when an agent needs to interact with Magic Markdown documents, library folders, comments, suggestions, image context, checkpoints, or local source sync.
|
|
10497
11289
|
metadata:
|
|
10498
11290
|
short-description: Operate Magic Markdown via CLI/MCP
|
|
10499
11291
|
---
|
|
@@ -10520,14 +11312,18 @@ Run \`mdocs help <command>\` (or \`mdocs <command> --help\`) for usage, examples
|
|
|
10520
11312
|
|
|
10521
11313
|
1. If given a Magic Markdown share URL, run \`mdocs join <share-url> --json\` first, and always include \`--name "<your agent name>"\` (for example \`--name "Claude Code"\`) so collaborators can see which agent is connected. Use \`mdocs remote ...\` commands after joining.
|
|
10522
11314
|
2. If working in a local workspace, run \`mdocs doctor --json\` to validate the workspace and learn recommended next commands.
|
|
10523
|
-
3. Run \`mdocs map --json\` or \`mdocs remote map --json\` to list documents, paths, docIds, open comments, open suggestions,
|
|
10524
|
-
4.
|
|
10525
|
-
5.
|
|
11315
|
+
3. 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>\`.
|
|
11316
|
+
4. 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.
|
|
11317
|
+
5. 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.
|
|
11318
|
+
6. 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 insert comments, CriticMarkup, directives, or Magic markers into Markdown files.
|
|
10526
11319
|
|
|
10527
11320
|
## Editing Rules
|
|
10528
11321
|
|
|
10529
11322
|
- Comments and suggestions are sidecar operations. They should not modify clean Markdown until a suggestion is accepted.
|
|
10530
11323
|
- \`--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.
|
|
11324
|
+
- 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.
|
|
11325
|
+
- 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.
|
|
11326
|
+
- When the narrowest Markdown line contains unchanged surrounding text, keep that unchanged text byte-for-byte identical in the replacement instead of rewording the whole paragraph.
|
|
10531
11327
|
- Prefer \`--json\` for agent calls and parse stdout as JSON.
|
|
10532
11328
|
- For multiline suggestion text, write the exact replacement to a temporary file and pass \`--with-file <file>\`.
|
|
10533
11329
|
- For long comments or messages, use \`--body-file <file>\` or \`--message-file <file>\`.
|
|
@@ -10540,13 +11336,14 @@ Run \`mdocs help <command>\` (or \`mdocs <command> --help\`) for usage, examples
|
|
|
10540
11336
|
- Errors go to stderr. With \`--json\` they are structured: \`{ "ok": false, "error": { "code", "message", "hint" }, "cliVersion" }\`. Follow the \`hint\`.
|
|
10541
11337
|
- Exit codes: 0 ok, 1 internal, 2 usage/invalid range, 3 conflict, 4 not found, 5 network, 6 unauthorized (share link revoked or expired).
|
|
10542
11338
|
- \`remote comment\` and \`remote suggest\` are concurrency-safe: the server merges review additions without clobbering concurrent edits, and the CLI rebases and retries automatically. A surviving \`conflict\` error means the document is changing rapidly \u2014 refetch context and retry.
|
|
10543
|
-
- \`remote events --json\` may return \`"truncated": true\` when older events were dropped from the bounded event log. Do not assume nothing happened; refetch
|
|
11339
|
+
- \`remote events --json\` may return \`"truncated": true\` when older events were dropped from the bounded event log. Do not assume nothing happened; refetch with \`mdocs remote context --summary --json\`, then read needed pages and review state.
|
|
10544
11340
|
- Retries are safe: remote writes carry a changeId and the server deduplicates replays.
|
|
10545
11341
|
- Stale reads are safe too: the server merges your change three-way against the version you read, so non-overlapping concurrent edits never conflict. Only true overlaps return \`conflict\`.
|
|
10546
11342
|
|
|
10547
11343
|
## Access Roles and Version History
|
|
10548
11344
|
|
|
10549
11345
|
- Agents are **suggest-only by default**: comments and suggestions always work, but direct content rewrites are rejected with a \`forbidden_role\` error until a human grants edit access. Propose changes with \`mdocs remote suggest\` instead of rewriting content.
|
|
11346
|
+
- Project-scoped agents with edit access can create and move Markdown files and can move the joined root into library folders. Workspace-scoped agents can read/comment/suggest across Home roots; root organization commands still require a project-scoped join. Folder invites and folder hierarchy updates require admin access on the joined root.
|
|
10550
11347
|
- Every accepted change is a content-addressed commit with a full snapshot. \`mdocs remote history --json\` lists commits with changed paths; \`mdocs remote restore <headId> --json\` restores the joined document to that snapshot (as a new commit, so it is reversible). Restore requires edit access \u2014 only run it when the user asked.
|
|
10551
11348
|
|
|
10552
11349
|
## Core Commands
|
|
@@ -10561,7 +11358,7 @@ Start the local stdio MCP server with:
|
|
|
10561
11358
|
mdocs serve-mcp --cwd /path/to/workspace
|
|
10562
11359
|
\`\`\`
|
|
10563
11360
|
|
|
10564
|
-
The MCP server exposes document resources, image resources,
|
|
11361
|
+
The MCP server exposes document resources, image resources, workspace map and graph resources, an agent guide resource, typed tools for comments and suggestions, and the \`magic_markdown_agent_workflow\` prompt. Use \`mdocs_context\` / \`magic_context\` with \`summary: true\` before dumping Markdown; use \`includeReview: false\` for content-only reads and \`mdocs_review\` / \`magic_review\` for review state.
|
|
10565
11362
|
`;
|
|
10566
11363
|
}
|
|
10567
11364
|
function formatCommandReference() {
|
|
@@ -10618,7 +11415,7 @@ function toCliError(error) {
|
|
|
10618
11415
|
|
|
10619
11416
|
// src/checkpoints.ts
|
|
10620
11417
|
import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
10621
|
-
import { basename, dirname as
|
|
11418
|
+
import { basename, dirname as dirname3, join } from "node:path";
|
|
10622
11419
|
async function createCheckpoint(root, label) {
|
|
10623
11420
|
const id = createId("checkpoint");
|
|
10624
11421
|
const createdAt = nowIso();
|
|
@@ -10683,7 +11480,7 @@ async function restoreTree(snapshotRoot, root) {
|
|
|
10683
11480
|
await mkdir(dest, { recursive: true });
|
|
10684
11481
|
await restoreTree(source, dest);
|
|
10685
11482
|
} else if (entry.isFile()) {
|
|
10686
|
-
await mkdir(
|
|
11483
|
+
await mkdir(dirname3(dest), { recursive: true });
|
|
10687
11484
|
await cp(source, dest);
|
|
10688
11485
|
}
|
|
10689
11486
|
}
|
|
@@ -10739,7 +11536,7 @@ function nextCommands(map) {
|
|
|
10739
11536
|
|
|
10740
11537
|
// src/fs-io.ts
|
|
10741
11538
|
import { mkdir as mkdir2, readFile as readFile2, readdir as readdir2, realpath, rename, rm as rm2, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
|
|
10742
|
-
import { dirname as
|
|
11539
|
+
import { dirname as dirname4, join as join2, relative, resolve } from "node:path";
|
|
10743
11540
|
var NodeWorkspaceIO = class {
|
|
10744
11541
|
constructor(root, pathRules = DEFAULT_ROOT_PATH_RULES) {
|
|
10745
11542
|
this.root = root;
|
|
@@ -10752,12 +11549,12 @@ var NodeWorkspaceIO = class {
|
|
|
10752
11549
|
}
|
|
10753
11550
|
async writeText(path, content) {
|
|
10754
11551
|
const target = await this.resolveWritablePath(path, Buffer.byteLength(content, "utf8"));
|
|
10755
|
-
await mkdir2(
|
|
11552
|
+
await mkdir2(dirname4(target), { recursive: true });
|
|
10756
11553
|
await writeFile2(target, content, "utf8");
|
|
10757
11554
|
}
|
|
10758
11555
|
async writeTextAtomic(path, content) {
|
|
10759
11556
|
const target = await this.resolveWritablePath(path, Buffer.byteLength(content, "utf8"));
|
|
10760
|
-
await mkdir2(
|
|
11557
|
+
await mkdir2(dirname4(target), { recursive: true });
|
|
10761
11558
|
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
10762
11559
|
await writeFile2(temporary, content, "utf8");
|
|
10763
11560
|
await rename(temporary, target);
|
|
@@ -10794,12 +11591,12 @@ var NodeWorkspaceIO = class {
|
|
|
10794
11591
|
const normalized = assertWorkspacePathAllowed(path, this.pathRules, sizeBytes);
|
|
10795
11592
|
const root = await realpath(this.root);
|
|
10796
11593
|
const target = resolve(root, normalized);
|
|
10797
|
-
await mkdir2(
|
|
10798
|
-
assertPathInsideRoot(root, await realpath(
|
|
11594
|
+
await mkdir2(dirname4(target), { recursive: true });
|
|
11595
|
+
assertPathInsideRoot(root, await realpath(dirname4(target)));
|
|
10799
11596
|
return target;
|
|
10800
11597
|
}
|
|
10801
11598
|
async resolvePath(path) {
|
|
10802
|
-
const normalized =
|
|
11599
|
+
const normalized = normalizeWorkspacePath3(path);
|
|
10803
11600
|
const root = await realpath(this.root);
|
|
10804
11601
|
const target = resolve(root, normalized);
|
|
10805
11602
|
assertPathInsideRoot(root, target);
|
|
@@ -10836,7 +11633,7 @@ var PathMappedWorkspaceIO = class {
|
|
|
10836
11633
|
return files.map((path) => replicaPathToCanonicalPath(path, this.mapping)).sort();
|
|
10837
11634
|
}
|
|
10838
11635
|
toReplicaPath(path) {
|
|
10839
|
-
if (path === ".mdocs" || path.startsWith(".mdocs/")) return
|
|
11636
|
+
if (path === ".mdocs" || path.startsWith(".mdocs/")) return normalizeWorkspacePath3(path);
|
|
10840
11637
|
return canonicalPathToReplicaPath(path, this.mapping);
|
|
10841
11638
|
}
|
|
10842
11639
|
};
|
|
@@ -10857,14 +11654,14 @@ async function walk(root, output, base, rules, ignoreFiles) {
|
|
|
10857
11654
|
}
|
|
10858
11655
|
if (entry.isFile()) {
|
|
10859
11656
|
const stats = await stat2(absolute);
|
|
10860
|
-
if (isWorkspaceMarkdownPathAllowed(relativePath, rules, stats.size, ignoreMatcher)) output.push(
|
|
11657
|
+
if (isWorkspaceMarkdownPathAllowed(relativePath, rules, stats.size, ignoreMatcher)) output.push(normalizeWorkspacePath3(relativePath));
|
|
10861
11658
|
}
|
|
10862
11659
|
}
|
|
10863
11660
|
}
|
|
10864
11661
|
async function withDirectoryIgnoreFile(root, base, ignoreFiles) {
|
|
10865
11662
|
try {
|
|
10866
11663
|
const path = join2(relative(base, root).replaceAll("\\", "/"), ".gitignore").replace(/^\.\//, "");
|
|
10867
|
-
const normalizedPath = path === ".gitignore" ? path :
|
|
11664
|
+
const normalizedPath = path === ".gitignore" ? path : normalizeWorkspacePath3(path);
|
|
10868
11665
|
const content = await readFile2(join2(root, ".gitignore"), "utf8");
|
|
10869
11666
|
return [...ignoreFiles, { path: normalizedPath, content }];
|
|
10870
11667
|
} catch {
|
|
@@ -10985,8 +11782,9 @@ function withMcpImageResources(io, state) {
|
|
|
10985
11782
|
}))
|
|
10986
11783
|
};
|
|
10987
11784
|
}
|
|
10988
|
-
async function contextForDocument(io, path, startLine, endLine) {
|
|
11785
|
+
async function contextForDocument(io, path, startLine, endLine, options = {}) {
|
|
10989
11786
|
const state = await getDocumentState(io, path);
|
|
11787
|
+
if (options.summary) return summarizeDocumentContext(state);
|
|
10990
11788
|
const { markdown, totalLines, startLine: sl, endLine: el } = sliceMarkdown(state.markdown, startLine, endLine);
|
|
10991
11789
|
return {
|
|
10992
11790
|
path: state.path,
|
|
@@ -10997,21 +11795,20 @@ async function contextForDocument(io, path, startLine, endLine) {
|
|
|
10997
11795
|
endLine: el,
|
|
10998
11796
|
markdown,
|
|
10999
11797
|
images: withMcpImageResources(io, state).images,
|
|
11798
|
+
links: state.links,
|
|
11799
|
+
...options.includeReview === false ? {} : reviewFields(state)
|
|
11800
|
+
};
|
|
11801
|
+
}
|
|
11802
|
+
async function reviewForDocument(io, path) {
|
|
11803
|
+
return reviewStateForDocument(await getDocumentState(io, path));
|
|
11804
|
+
}
|
|
11805
|
+
function reviewFields(state) {
|
|
11806
|
+
return {
|
|
11000
11807
|
comments: state.sidecar.comments.filter((comment2) => comment2.status === "open"),
|
|
11001
11808
|
suggestions: state.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
|
|
11002
11809
|
anchors: state.anchors
|
|
11003
11810
|
};
|
|
11004
11811
|
}
|
|
11005
|
-
function sliceMarkdown(markdown, startLine, endLine) {
|
|
11006
|
-
const lines = markdown.split("\n");
|
|
11007
|
-
const totalLines = lines.length;
|
|
11008
|
-
const sl = startLine ?? 1;
|
|
11009
|
-
const el = endLine ?? totalLines;
|
|
11010
|
-
if (sl < 1 || el > totalLines || sl > el) {
|
|
11011
|
-
throw new Error(`Line range ${sl}:${el} is out of bounds for a ${totalLines}-line document.`);
|
|
11012
|
-
}
|
|
11013
|
-
return { markdown: lines.slice(sl - 1, el).join("\n"), totalLines, startLine: sl, endLine: el };
|
|
11014
|
-
}
|
|
11015
11812
|
function imageResourceUri(docId, index) {
|
|
11016
11813
|
return `mdocs://documents/${docId}/images/${index}`;
|
|
11017
11814
|
}
|
|
@@ -11074,7 +11871,12 @@ var actorProperties = {
|
|
|
11074
11871
|
var tools = [
|
|
11075
11872
|
{
|
|
11076
11873
|
name: "mdocs_map",
|
|
11077
|
-
description: "Return the workspace document map.",
|
|
11874
|
+
description: "Return the workspace document map with review and link counts.",
|
|
11875
|
+
inputSchema: noArgsSchema
|
|
11876
|
+
},
|
|
11877
|
+
{
|
|
11878
|
+
name: "mdocs_graph",
|
|
11879
|
+
description: "Return the workspace knowledge graph as document nodes and Markdown/wikilink edges. Edges include resolved, ambiguous, unresolved, external, and same-document anchor links.",
|
|
11078
11880
|
inputSchema: noArgsSchema
|
|
11079
11881
|
},
|
|
11080
11882
|
{
|
|
@@ -11084,7 +11886,7 @@ var tools = [
|
|
|
11084
11886
|
},
|
|
11085
11887
|
{
|
|
11086
11888
|
name: "mdocs_context",
|
|
11087
|
-
description: "Return agent-ready document Markdown
|
|
11889
|
+
description: "Return agent-ready document 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. Use includeReview=false when reading content only, and mdocs_review for comments, suggestions, and anchors.",
|
|
11088
11890
|
inputSchema: {
|
|
11089
11891
|
type: "object",
|
|
11090
11892
|
properties: {
|
|
@@ -11096,12 +11898,25 @@ var tools = [
|
|
|
11096
11898
|
endLine: {
|
|
11097
11899
|
type: "number",
|
|
11098
11900
|
description: "1-based last line to include (default: last line of document)."
|
|
11901
|
+
},
|
|
11902
|
+
summary: {
|
|
11903
|
+
type: "boolean",
|
|
11904
|
+
description: "Return metadata, heading outline, current head, and counts instead of Markdown content."
|
|
11905
|
+
},
|
|
11906
|
+
includeReview: {
|
|
11907
|
+
type: "boolean",
|
|
11908
|
+
description: "Whether to include open comments, suggestions, and anchors in the content response (default: true). Use false to keep document reading separate from review state."
|
|
11099
11909
|
}
|
|
11100
11910
|
},
|
|
11101
11911
|
required: ["path"],
|
|
11102
11912
|
additionalProperties: false
|
|
11103
11913
|
}
|
|
11104
11914
|
},
|
|
11915
|
+
{
|
|
11916
|
+
name: "mdocs_review",
|
|
11917
|
+
description: "Return open comments, open suggestions, and mapped anchors for one document without returning document Markdown.",
|
|
11918
|
+
inputSchema: documentPathSchema
|
|
11919
|
+
},
|
|
11105
11920
|
{
|
|
11106
11921
|
name: "mdocs_state",
|
|
11107
11922
|
description: "Return full merged Markdown and sidecar state for one document.",
|
|
@@ -11125,14 +11940,14 @@ var tools = [
|
|
|
11125
11940
|
},
|
|
11126
11941
|
{
|
|
11127
11942
|
name: "mdocs_suggest",
|
|
11128
|
-
description: "Create a sidecar replacement suggestion without modifying clean Markdown.",
|
|
11943
|
+
description: "Create a sidecar replacement suggestion without modifying clean Markdown. Prefer the smallest coherent range; use longer ranges only when the broader rewrite is genuinely needed.",
|
|
11129
11944
|
inputSchema: {
|
|
11130
11945
|
type: "object",
|
|
11131
11946
|
properties: {
|
|
11132
11947
|
path: documentPathSchema.properties.path,
|
|
11133
11948
|
startLine: { type: "number", description: "1-based start line." },
|
|
11134
11949
|
endLine: { type: "number", description: "1-based end line." },
|
|
11135
|
-
replacement: { type: "string", description: "Exact replacement Markdown for the line range." },
|
|
11950
|
+
replacement: { type: "string", description: "Exact replacement Markdown for the line range; keep unchanged surrounding text identical for narrow edits." },
|
|
11136
11951
|
message: { type: "string", description: "Short explanation for the suggestion." },
|
|
11137
11952
|
...actorProperties
|
|
11138
11953
|
},
|
|
@@ -11223,6 +12038,11 @@ async function dispatch(io, method, params) {
|
|
|
11223
12038
|
uri: "mdocs://workspace/map",
|
|
11224
12039
|
name: "Workspace Map",
|
|
11225
12040
|
mimeType: "application/vnd.mdocs.workspace-map+json"
|
|
12041
|
+
},
|
|
12042
|
+
{
|
|
12043
|
+
uri: "mdocs://workspace/graph",
|
|
12044
|
+
name: "Workspace Knowledge Graph",
|
|
12045
|
+
mimeType: "application/vnd.mdocs.workspace-graph+json"
|
|
11226
12046
|
}
|
|
11227
12047
|
])
|
|
11228
12048
|
};
|
|
@@ -11315,6 +12135,17 @@ async function readResource(io, path) {
|
|
|
11315
12135
|
]
|
|
11316
12136
|
};
|
|
11317
12137
|
}
|
|
12138
|
+
if (path === "mdocs://workspace/graph") {
|
|
12139
|
+
return {
|
|
12140
|
+
contents: [
|
|
12141
|
+
{
|
|
12142
|
+
uri: path,
|
|
12143
|
+
mimeType: "application/vnd.mdocs.workspace-graph+json",
|
|
12144
|
+
text: JSON.stringify(await getWorkspaceGraph(io), null, 2)
|
|
12145
|
+
}
|
|
12146
|
+
]
|
|
12147
|
+
};
|
|
12148
|
+
}
|
|
11318
12149
|
const imageMatch = /^mdocs:\/\/documents\/([^/]+)\/images\/(\d+)$/.exec(path);
|
|
11319
12150
|
if (imageMatch) {
|
|
11320
12151
|
return readImageResource(io, path, imageMatch[1] ?? "", Number(imageMatch[2]));
|
|
@@ -11333,12 +12164,17 @@ async function readResource(io, path) {
|
|
|
11333
12164
|
}
|
|
11334
12165
|
async function callTool(io, name, args) {
|
|
11335
12166
|
if (name === "mdocs_map") return getWorkspaceMap(io);
|
|
12167
|
+
if (name === "mdocs_graph") return getWorkspaceGraph(io);
|
|
11336
12168
|
if (name === "mdocs_doctor") return runDoctor(io, io.root);
|
|
11337
12169
|
if (name === "mdocs_context") {
|
|
11338
12170
|
const startLine = typeof args.startLine === "number" ? args.startLine : void 0;
|
|
11339
12171
|
const endLine = typeof args.endLine === "number" ? args.endLine : void 0;
|
|
11340
|
-
return contextForDocument(io, requiredString(args, "path"), startLine, endLine
|
|
12172
|
+
return contextForDocument(io, requiredString(args, "path"), startLine, endLine, {
|
|
12173
|
+
summary: args.summary === true,
|
|
12174
|
+
includeReview: args.includeReview !== false
|
|
12175
|
+
});
|
|
11341
12176
|
}
|
|
12177
|
+
if (name === "mdocs_review") return reviewForDocument(io, requiredString(args, "path"));
|
|
11342
12178
|
if (name === "mdocs_state") return withMcpImageResources(io, await getDocumentState(io, requiredString(args, "path")));
|
|
11343
12179
|
if (name === "mdocs_comment") {
|
|
11344
12180
|
return addComment(
|
|
@@ -11445,12 +12281,22 @@ async function postPresenceAndReadState(share, shareUrl, docId, agentId, agentNa
|
|
|
11445
12281
|
});
|
|
11446
12282
|
}
|
|
11447
12283
|
async function fetchState(record) {
|
|
11448
|
-
if (!record.docId) {
|
|
12284
|
+
if (!record.rootId || !record.docId) {
|
|
11449
12285
|
throw new CliError("usage_error", "This join has no current document.", {
|
|
11450
12286
|
hint: "Pass a document path or docId, or list documents with mdocs remote map --json."
|
|
11451
12287
|
});
|
|
11452
12288
|
}
|
|
11453
|
-
return fetchJson(agentUrl(record, record.docId, "state"), {
|
|
12289
|
+
return fetchJson(agentUrl({ ...record, rootId: record.rootId }, record.docId, "state"), {
|
|
12290
|
+
headers: shareHeaders(record.shareUrl)
|
|
12291
|
+
});
|
|
12292
|
+
}
|
|
12293
|
+
async function fetchWorkspaceRoots(record) {
|
|
12294
|
+
return fetchJson(`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots`, {
|
|
12295
|
+
headers: shareHeaders(record.shareUrl)
|
|
12296
|
+
});
|
|
12297
|
+
}
|
|
12298
|
+
async function fetchWorkspaceLibrary(record) {
|
|
12299
|
+
return fetchJson(`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/library`, {
|
|
11454
12300
|
headers: shareHeaders(record.shareUrl)
|
|
11455
12301
|
});
|
|
11456
12302
|
}
|
|
@@ -11459,9 +12305,47 @@ async function fetchTree(record) {
|
|
|
11459
12305
|
headers: shareHeaders(record.shareUrl)
|
|
11460
12306
|
});
|
|
11461
12307
|
}
|
|
12308
|
+
async function fetchLibrary(record) {
|
|
12309
|
+
return fetchJson(libraryUrl(record), {
|
|
12310
|
+
headers: shareHeaders(record.shareUrl)
|
|
12311
|
+
});
|
|
12312
|
+
}
|
|
12313
|
+
async function postLibraryFolder(record, input, actor) {
|
|
12314
|
+
return fetchJson(`${libraryUrl(record)}/folders`, {
|
|
12315
|
+
method: "POST",
|
|
12316
|
+
headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
|
|
12317
|
+
body: JSON.stringify({ ...input, actor })
|
|
12318
|
+
});
|
|
12319
|
+
}
|
|
12320
|
+
async function patchLibraryFolder(record, folderId, input, actor) {
|
|
12321
|
+
return fetchJson(`${libraryUrl(record)}/folders/${encodeURIComponent(folderId)}`, {
|
|
12322
|
+
method: "PATCH",
|
|
12323
|
+
headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
|
|
12324
|
+
body: JSON.stringify({ ...input, actor })
|
|
12325
|
+
});
|
|
12326
|
+
}
|
|
12327
|
+
async function postMoveRoot(record, folderId, actor) {
|
|
12328
|
+
return fetchJson(`${libraryUrl(record)}/move-root`, {
|
|
12329
|
+
method: "POST",
|
|
12330
|
+
headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
|
|
12331
|
+
body: JSON.stringify({ folderId, actor })
|
|
12332
|
+
});
|
|
12333
|
+
}
|
|
12334
|
+
async function postFolderInvite(record, folderId, input, actor) {
|
|
12335
|
+
return fetchJson(
|
|
12336
|
+
`${libraryUrl(record)}/folders/${encodeURIComponent(folderId)}/invites`,
|
|
12337
|
+
{
|
|
12338
|
+
method: "POST",
|
|
12339
|
+
headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
|
|
12340
|
+
body: JSON.stringify({ ...input, actor })
|
|
12341
|
+
}
|
|
12342
|
+
);
|
|
12343
|
+
}
|
|
11462
12344
|
async function fetchDocument(record, pathOrDocId) {
|
|
12345
|
+
if (record.scope === "workspace") return fetchWorkspaceDocument(record, pathOrDocId);
|
|
12346
|
+
const rootRecord = requireRootRecord(record);
|
|
11463
12347
|
if (record.scope === "file" && record.docId) {
|
|
11464
|
-
const document = await fetchDocumentById(
|
|
12348
|
+
const document = await fetchDocumentById(rootRecord, record.docId);
|
|
11465
12349
|
if (pathOrDocId === document.docId || pathOrDocId === document.path) return document;
|
|
11466
12350
|
throw new CliError("usage_error", `This file-scoped join only covers ${document.path} (${document.docId}).`, {
|
|
11467
12351
|
hint: `Join a share link for ${pathOrDocId} to work on it.`
|
|
@@ -11469,19 +12353,19 @@ async function fetchDocument(record, pathOrDocId) {
|
|
|
11469
12353
|
}
|
|
11470
12354
|
if (pathOrDocId.startsWith("doc_") || pathOrDocId === record.docId) {
|
|
11471
12355
|
try {
|
|
11472
|
-
return await fetchDocumentById(
|
|
12356
|
+
return await fetchDocumentById(rootRecord, pathOrDocId);
|
|
11473
12357
|
} catch (error) {
|
|
11474
12358
|
if (!(error instanceof CliError) || error.code !== "not_found") throw error;
|
|
11475
12359
|
}
|
|
11476
12360
|
}
|
|
11477
|
-
const tree = await fetchTree(
|
|
12361
|
+
const tree = await fetchTree(rootRecord);
|
|
11478
12362
|
const docId = tree.docs.find((doc) => doc.docId === pathOrDocId || doc.path === pathOrDocId)?.docId;
|
|
11479
12363
|
if (!docId) {
|
|
11480
12364
|
throw new CliError("not_found", `No document matches "${pathOrDocId}" in the joined workspace.`, {
|
|
11481
12365
|
hint: "List documents and their paths/docIds with mdocs remote map --json."
|
|
11482
12366
|
});
|
|
11483
12367
|
}
|
|
11484
|
-
return fetchDocumentById(
|
|
12368
|
+
return fetchDocumentById(rootRecord, docId);
|
|
11485
12369
|
}
|
|
11486
12370
|
function fetchDocumentById(record, docId) {
|
|
11487
12371
|
return fetchJson(
|
|
@@ -11489,6 +12373,51 @@ function fetchDocumentById(record, docId) {
|
|
|
11489
12373
|
{ headers: shareHeaders(record.shareUrl) }
|
|
11490
12374
|
);
|
|
11491
12375
|
}
|
|
12376
|
+
async function fetchWorkspaceDocument(record, pathOrDocId) {
|
|
12377
|
+
if (!pathOrDocId) {
|
|
12378
|
+
throw new CliError("usage_error", "Workspace-scoped context requires a document path or docId.", {
|
|
12379
|
+
hint: "Run mdocs remote map --json, then pass a path, docId, or <rootId>:<path-or-docId>."
|
|
12380
|
+
});
|
|
12381
|
+
}
|
|
12382
|
+
const roots = await fetchWorkspaceRoots(record);
|
|
12383
|
+
const rootIds = new Set(roots.map((root) => root.rootId));
|
|
12384
|
+
const prefixed = splitWorkspaceDocumentTarget(pathOrDocId, rootIds);
|
|
12385
|
+
if (prefixed) {
|
|
12386
|
+
const rootRecord = { ...record, rootId: prefixed.rootId };
|
|
12387
|
+
const tree = await fetchTree(rootRecord);
|
|
12388
|
+
const match3 = tree.docs.find((doc) => doc.docId === prefixed.target || doc.path === prefixed.target);
|
|
12389
|
+
if (!match3) {
|
|
12390
|
+
throw new CliError("not_found", `No document matches "${prefixed.target}" in ${prefixed.rootId}.`, {
|
|
12391
|
+
hint: "Run mdocs remote map --json to verify the root id and document path."
|
|
12392
|
+
});
|
|
12393
|
+
}
|
|
12394
|
+
return fetchDocumentById(rootRecord, match3.docId);
|
|
12395
|
+
}
|
|
12396
|
+
const trees = await Promise.all(roots.map((root) => fetchTree({ ...record, rootId: root.rootId })));
|
|
12397
|
+
const matches = trees.flatMap(
|
|
12398
|
+
(tree) => tree.docs.filter((doc) => doc.docId === pathOrDocId || doc.path === pathOrDocId).map((doc) => ({ rootId: tree.root.rootId, doc }))
|
|
12399
|
+
);
|
|
12400
|
+
if (matches.length === 0) {
|
|
12401
|
+
throw new CliError("not_found", `No document matches "${pathOrDocId}" in the joined Home workspace.`, {
|
|
12402
|
+
hint: "Run mdocs remote map --json to list roots, paths, and docIds."
|
|
12403
|
+
});
|
|
12404
|
+
}
|
|
12405
|
+
if (matches.length > 1) {
|
|
12406
|
+
throw new CliError("usage_error", `Document target "${pathOrDocId}" is ambiguous across ${matches.length} roots.`, {
|
|
12407
|
+
hint: `Prefix the target with a root id, for example ${matches[0].rootId}:${pathOrDocId}.`
|
|
12408
|
+
});
|
|
12409
|
+
}
|
|
12410
|
+
const match2 = matches[0];
|
|
12411
|
+
return fetchDocumentById({ ...record, rootId: match2.rootId }, match2.doc.docId);
|
|
12412
|
+
}
|
|
12413
|
+
function splitWorkspaceDocumentTarget(value, rootIds) {
|
|
12414
|
+
const separator = value.indexOf(":");
|
|
12415
|
+
if (separator <= 0) return void 0;
|
|
12416
|
+
const rootId = value.slice(0, separator);
|
|
12417
|
+
if (!rootIds.has(rootId)) return void 0;
|
|
12418
|
+
const target = value.slice(separator + 1);
|
|
12419
|
+
return target ? { rootId, target } : void 0;
|
|
12420
|
+
}
|
|
11492
12421
|
async function postReview(record, docId, additions, actor) {
|
|
11493
12422
|
const response = await fetchJson(
|
|
11494
12423
|
`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/review`,
|
|
@@ -11571,7 +12500,7 @@ function conflictError(eventPayload) {
|
|
|
11571
12500
|
const conflict = eventPayload ?? {};
|
|
11572
12501
|
const head = typeof conflict.canonicalHead === "string" ? ` Current head: ${conflict.canonicalHead}.` : "";
|
|
11573
12502
|
return new CliError("conflict", `Remote change was not accepted: the document changed since it was read.${head}`, {
|
|
11574
|
-
hint: "Refetch with mdocs remote context --json, re-apply your change against the fresh content, and retry.",
|
|
12503
|
+
hint: "Refetch with mdocs remote context --summary --json, re-apply your change against the needed fresh content range, and retry.",
|
|
11575
12504
|
details: eventPayload
|
|
11576
12505
|
});
|
|
11577
12506
|
}
|
|
@@ -11579,6 +12508,17 @@ function agentUrl(record, docId, action, query) {
|
|
|
11579
12508
|
const base = `${record.origin}/api/agent/${encodeURIComponent(record.workspaceId)}/${encodeURIComponent(record.rootId)}/${encodeURIComponent(docId)}/${action}`;
|
|
11580
12509
|
return query ? `${base}?${query}` : base;
|
|
11581
12510
|
}
|
|
12511
|
+
function libraryUrl(record) {
|
|
12512
|
+
return `${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/library`;
|
|
12513
|
+
}
|
|
12514
|
+
function requireRootRecord(record) {
|
|
12515
|
+
if (!record.rootId) {
|
|
12516
|
+
throw new CliError("usage_error", "This command needs a root-scoped join.", {
|
|
12517
|
+
hint: "For Home joins, pass a document target from `mdocs remote map --json`, or use a project share for root organization commands."
|
|
12518
|
+
});
|
|
12519
|
+
}
|
|
12520
|
+
return { origin: record.origin, workspaceId: record.workspaceId, rootId: record.rootId, shareUrl: record.shareUrl };
|
|
12521
|
+
}
|
|
11582
12522
|
function shareHeaders(shareUrl) {
|
|
11583
12523
|
return { Referer: shareUrl, Authorization: `Bearer ${shareUrl}` };
|
|
11584
12524
|
}
|
|
@@ -11616,7 +12556,7 @@ function remoteHttpError(status, value, text2) {
|
|
|
11616
12556
|
}
|
|
11617
12557
|
if (status === 409) {
|
|
11618
12558
|
return new CliError("conflict", message, {
|
|
11619
|
-
hint: "Refetch with mdocs remote context --json and retry.",
|
|
12559
|
+
hint: "Refetch with mdocs remote context --summary --json and retry.",
|
|
11620
12560
|
details: value
|
|
11621
12561
|
});
|
|
11622
12562
|
}
|
|
@@ -11757,13 +12697,15 @@ async function runJoinCommand(root, parsed) {
|
|
|
11757
12697
|
const parsedShare = parseShareUrl(target, parsed.flags);
|
|
11758
12698
|
const agentName = resolveAgentName(parsed.flags);
|
|
11759
12699
|
const agentId = normalizeAgentId(String(parsed.flags["agent-id"] ?? parsed.flags.actor ?? agentName));
|
|
11760
|
-
const
|
|
11761
|
-
const
|
|
12700
|
+
const rootScopedShare = rootScopedRecord(parsedShare);
|
|
12701
|
+
const docId = parsedShare.docId ?? (rootScopedShare ? await firstDocIdForProject(rootScopedShare) : void 0);
|
|
12702
|
+
const shareUrl = shareUrlForScope(target, { ...parsedShare, docId });
|
|
12703
|
+
const state = rootScopedShare && docId ? await postPresenceAndReadState(rootScopedShare, shareUrl, docId, agentId, agentName) : void 0;
|
|
11762
12704
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11763
12705
|
const record = {
|
|
11764
12706
|
joinId: joinIdFor(target),
|
|
11765
12707
|
scope: parsedShare.scope,
|
|
11766
|
-
shareUrl
|
|
12708
|
+
shareUrl,
|
|
11767
12709
|
origin: parsedShare.origin,
|
|
11768
12710
|
workspaceId: parsedShare.workspaceId,
|
|
11769
12711
|
rootId: parsedShare.rootId,
|
|
@@ -11773,28 +12715,39 @@ async function runJoinCommand(root, parsed) {
|
|
|
11773
12715
|
joinedAt: now,
|
|
11774
12716
|
updatedAt: now,
|
|
11775
12717
|
lastSeenEventId: 0,
|
|
11776
|
-
currentHead: state
|
|
12718
|
+
currentHead: state?.document.currentSha
|
|
11777
12719
|
};
|
|
11778
12720
|
await writeJoinRecord(root, record);
|
|
11779
|
-
return joinSummary(record, state
|
|
12721
|
+
return joinSummary(record, state?.document);
|
|
11780
12722
|
}
|
|
11781
12723
|
async function runJoinsCommand(root) {
|
|
11782
12724
|
return listJoinRecords(root);
|
|
11783
12725
|
}
|
|
11784
12726
|
async function runRemoteCommand(root, subcommand, parsed) {
|
|
11785
|
-
const record = await
|
|
12727
|
+
const record = await normalizedSelectedJoin(root, parsed.flags);
|
|
11786
12728
|
switch (subcommand) {
|
|
11787
12729
|
case "map":
|
|
11788
|
-
case void 0:
|
|
12730
|
+
case void 0: {
|
|
11789
12731
|
if (record.scope === "file") {
|
|
11790
12732
|
throw new CliError("usage_error", "This join is file-scoped, so the project tree is not accessible.", {
|
|
11791
|
-
hint: "Use `mdocs remote context --json` to
|
|
12733
|
+
hint: "Use `mdocs remote context --summary --json` to inspect the joined document."
|
|
11792
12734
|
});
|
|
11793
12735
|
}
|
|
11794
|
-
|
|
11795
|
-
|
|
12736
|
+
if (record.scope === "workspace") return remoteWorkspaceMap(record);
|
|
12737
|
+
const rootRecord = assertRootScoped(record, "map");
|
|
12738
|
+
await refreshPresence(rootRecord, record.docId);
|
|
12739
|
+
return fetchTree(rootRecord);
|
|
12740
|
+
}
|
|
12741
|
+
case "graph":
|
|
12742
|
+
return remoteGraph(record);
|
|
11796
12743
|
case "context":
|
|
11797
12744
|
return remoteContext(record, parsed.command[2], parsed.flags);
|
|
12745
|
+
case "review":
|
|
12746
|
+
return remoteReview(record, parsed.command[2]);
|
|
12747
|
+
case "create-file":
|
|
12748
|
+
return remoteCreateFile(root, record, parsed);
|
|
12749
|
+
case "move-file":
|
|
12750
|
+
return remoteMoveFile(root, record, parsed);
|
|
11798
12751
|
case "comment":
|
|
11799
12752
|
return remoteComment(root, record, parsed);
|
|
11800
12753
|
case "suggest":
|
|
@@ -11807,18 +12760,83 @@ async function runRemoteCommand(root, subcommand, parsed) {
|
|
|
11807
12760
|
return remoteHistory(record, parsed.flags);
|
|
11808
12761
|
case "restore":
|
|
11809
12762
|
return remoteRestore(record, parsed);
|
|
12763
|
+
case "library": {
|
|
12764
|
+
if (record.scope === "workspace") return fetchWorkspaceLibrary(record);
|
|
12765
|
+
assertProjectScope(record, "library");
|
|
12766
|
+
const rootRecord = assertRootScoped(record, "library");
|
|
12767
|
+
await refreshPresence(rootRecord, record.docId);
|
|
12768
|
+
return fetchLibrary(rootRecord);
|
|
12769
|
+
}
|
|
12770
|
+
case "create-folder":
|
|
12771
|
+
return remoteCreateFolder(record, parsed);
|
|
12772
|
+
case "update-folder":
|
|
12773
|
+
return remoteUpdateFolder(record, parsed);
|
|
12774
|
+
case "move-root":
|
|
12775
|
+
return remoteMoveRoot(record, parsed);
|
|
12776
|
+
case "invite-folder":
|
|
12777
|
+
return remoteInviteFolder(record, parsed);
|
|
11810
12778
|
case "rejoin":
|
|
11811
12779
|
return rejoin(root, record.joinId, parsed.flags);
|
|
11812
12780
|
default:
|
|
11813
12781
|
throw new CliError("usage_error", `Unknown remote subcommand: ${subcommand}`, {
|
|
11814
|
-
hint: "Use mdocs remote map|context|comment|suggest|reject|events|history|restore|rejoin."
|
|
12782
|
+
hint: "Use mdocs remote map|graph|context|review|create-file|move-file|comment|suggest|reject|events|history|restore|library|create-folder|update-folder|move-root|invite-folder|rejoin."
|
|
11815
12783
|
});
|
|
11816
12784
|
}
|
|
11817
12785
|
}
|
|
12786
|
+
async function remoteGraph(record) {
|
|
12787
|
+
assertProjectScope(record, "graph");
|
|
12788
|
+
const rootRecord = assertRootScoped(record, "graph");
|
|
12789
|
+
await refreshPresence(rootRecord, record.docId);
|
|
12790
|
+
const tree = await fetchTree(rootRecord);
|
|
12791
|
+
const documents = await Promise.all(tree.docs.map((doc) => fetchDocument(rootScopedRecordFor(record, rootRecord.rootId), doc.docId)));
|
|
12792
|
+
const lookup = documents.map((doc) => ({ docId: doc.docId, path: doc.path, title: doc.title }));
|
|
12793
|
+
const graphDocuments = documents.map((doc) => {
|
|
12794
|
+
const links = doc.links ?? resolveMarkdownLinks(extractMarkdownLinks(doc.markdown), doc, lookup);
|
|
12795
|
+
return {
|
|
12796
|
+
docId: doc.docId,
|
|
12797
|
+
path: doc.path,
|
|
12798
|
+
title: doc.title,
|
|
12799
|
+
openComments: doc.openComments,
|
|
12800
|
+
openSuggestions: doc.openSuggestions,
|
|
12801
|
+
anchorsNeedingReview: doc.anchors.filter((anchor) => anchor.status !== "mapped").length,
|
|
12802
|
+
externalLinks: links.filter((link2) => link2.status === "external").length,
|
|
12803
|
+
unresolvedLinks: links.filter((link2) => link2.status === "unresolved" || link2.status === "ambiguous").length,
|
|
12804
|
+
links
|
|
12805
|
+
};
|
|
12806
|
+
});
|
|
12807
|
+
return buildWorkspaceGraph(record.workspaceId, SIDECAR_SCHEMA_VERSION, graphDocuments);
|
|
12808
|
+
}
|
|
12809
|
+
async function remoteWorkspaceMap(record) {
|
|
12810
|
+
const roots = await fetchWorkspaceRoots(record);
|
|
12811
|
+
const trees = await Promise.all(roots.map((root) => fetchTree(rootScopedRecordFor(record, root.rootId))));
|
|
12812
|
+
const library = await fetchWorkspaceLibrary(record).catch(() => void 0);
|
|
12813
|
+
return {
|
|
12814
|
+
workspaceId: record.workspaceId,
|
|
12815
|
+
scope: "workspace",
|
|
12816
|
+
roots: trees.map((tree) => ({
|
|
12817
|
+
root: tree.root,
|
|
12818
|
+
docs: tree.docs.map((doc) => ({
|
|
12819
|
+
rootId: tree.root.rootId,
|
|
12820
|
+
docId: doc.docId,
|
|
12821
|
+
path: doc.path,
|
|
12822
|
+
title: doc.title,
|
|
12823
|
+
openComments: doc.openComments,
|
|
12824
|
+
openSuggestions: doc.openSuggestions,
|
|
12825
|
+
anchorsNeedingReview: doc.anchorsNeedingReview,
|
|
12826
|
+
outgoingLinks: doc.outgoingLinks,
|
|
12827
|
+
incomingLinks: doc.incomingLinks,
|
|
12828
|
+
unresolvedLinks: doc.unresolvedLinks,
|
|
12829
|
+
externalLinks: doc.externalLinks
|
|
12830
|
+
}))
|
|
12831
|
+
})),
|
|
12832
|
+
...library ? { folders: library.folders } : {}
|
|
12833
|
+
};
|
|
12834
|
+
}
|
|
11818
12835
|
async function remoteHistory(record, flags) {
|
|
11819
|
-
|
|
12836
|
+
const rootRecord = assertRootScoped(record, "history");
|
|
12837
|
+
await refreshPresence(rootRecord, record.docId);
|
|
11820
12838
|
const limit = typeof flags.limit === "string" ? Number(flags.limit) || 50 : 50;
|
|
11821
|
-
return fetchCommits(
|
|
12839
|
+
return fetchCommits(rootRecord, limit);
|
|
11822
12840
|
}
|
|
11823
12841
|
async function remoteRestore(record, parsed) {
|
|
11824
12842
|
const headId = parsed.command[2];
|
|
@@ -11827,9 +12845,10 @@ async function remoteRestore(record, parsed) {
|
|
|
11827
12845
|
hint: "List restorable commits with mdocs remote history --json, then run mdocs remote restore <headId>."
|
|
11828
12846
|
});
|
|
11829
12847
|
}
|
|
11830
|
-
await refreshPresence(record, record.docId);
|
|
11831
12848
|
const restoreAll = parsed.flags.all === true || parsed.flags.all === "true";
|
|
11832
12849
|
let docId;
|
|
12850
|
+
const rootRecord = assertRootScoped(record, "restore");
|
|
12851
|
+
await refreshPresence(rootRecord, record.docId);
|
|
11833
12852
|
if (!restoreAll) {
|
|
11834
12853
|
const target = typeof parsed.flags.doc === "string" ? parsed.flags.doc : record.docId;
|
|
11835
12854
|
if (!target) {
|
|
@@ -11839,20 +12858,32 @@ async function remoteRestore(record, parsed) {
|
|
|
11839
12858
|
}
|
|
11840
12859
|
docId = (await fetchDocument(record, target)).docId;
|
|
11841
12860
|
}
|
|
11842
|
-
return postRestore(
|
|
12861
|
+
return postRestore(rootRecord, headId, docId);
|
|
11843
12862
|
}
|
|
11844
12863
|
async function rejoin(root, joinId, flags) {
|
|
11845
12864
|
const record = await readSelectedJoin(root, { ...flags, ...joinId ? { join: joinId } : {} });
|
|
11846
|
-
const
|
|
11847
|
-
const
|
|
12865
|
+
const rootRecord = rootScopedRecord(record);
|
|
12866
|
+
const docId = record.docId ?? (rootRecord ? await firstDocIdForProject(rootRecord) : void 0);
|
|
12867
|
+
const shareUrl = shareUrlForScope(record.shareUrl, { ...record, docId });
|
|
12868
|
+
const state = rootRecord && docId ? await postPresenceAndReadState(rootRecord, shareUrl, docId, record.agentId, record.agentName) : void 0;
|
|
11848
12869
|
const next = {
|
|
11849
12870
|
...record,
|
|
12871
|
+
shareUrl,
|
|
11850
12872
|
docId,
|
|
11851
12873
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11852
|
-
currentHead: state
|
|
12874
|
+
currentHead: state?.document.currentSha
|
|
11853
12875
|
};
|
|
11854
12876
|
await writeJoinRecord(root, next);
|
|
11855
|
-
return joinSummary(next, state
|
|
12877
|
+
return joinSummary(next, state?.document);
|
|
12878
|
+
}
|
|
12879
|
+
async function normalizedSelectedJoin(root, flags) {
|
|
12880
|
+
const record = await readSelectedJoin(root, flags);
|
|
12881
|
+
if (!record.docId && record.scope === "file") return record;
|
|
12882
|
+
const shareUrl = shareUrlForScope(record.shareUrl, record);
|
|
12883
|
+
if (shareUrl === record.shareUrl) return record;
|
|
12884
|
+
const next = { ...record, shareUrl, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
12885
|
+
await writeJoinRecord(root, next);
|
|
12886
|
+
return next;
|
|
11856
12887
|
}
|
|
11857
12888
|
async function refreshPresence(record, docId) {
|
|
11858
12889
|
if (!docId) return;
|
|
@@ -11860,19 +12891,13 @@ async function refreshPresence(record, docId) {
|
|
|
11860
12891
|
}
|
|
11861
12892
|
async function remoteContext(record, pathOrDocId, flags = {}) {
|
|
11862
12893
|
const document = pathOrDocId ? await fetchDocument(record, pathOrDocId) : (await fetchState(record)).document;
|
|
11863
|
-
await refreshPresence(record, document.docId);
|
|
12894
|
+
await refreshPresence(rootScopedRecordFor(record, document.rootId), document.docId);
|
|
12895
|
+
if (flags.summary || flags["metadata-only"]) {
|
|
12896
|
+
return withRemoteSummaryNextCommands(summarizeDocumentContext(document), record, document, pathOrDocId);
|
|
12897
|
+
}
|
|
11864
12898
|
const startLine = typeof flags["start-line"] === "string" ? Number(flags["start-line"]) : void 0;
|
|
11865
12899
|
const endLine = typeof flags["end-line"] === "string" ? Number(flags["end-line"]) : void 0;
|
|
11866
|
-
const
|
|
11867
|
-
const totalLines = lines.length;
|
|
11868
|
-
const sl = startLine ?? 1;
|
|
11869
|
-
const el = endLine ?? totalLines;
|
|
11870
|
-
if (sl < 1 || el > totalLines || sl > el) {
|
|
11871
|
-
throw new CliError("invalid_range", `Line range ${sl}:${el} is out of bounds for a ${totalLines}-line document.`, {
|
|
11872
|
-
hint: `Use --start-line and --end-line with 1-based line numbers within 1:${totalLines}.`
|
|
11873
|
-
});
|
|
11874
|
-
}
|
|
11875
|
-
const markdown = startLine !== void 0 || endLine !== void 0 ? lines.slice(sl - 1, el).join("\n") : document.markdown;
|
|
12900
|
+
const { markdown, totalLines, startLine: sl, endLine: el } = sliceMarkdown(document.markdown, startLine, endLine);
|
|
11876
12901
|
return {
|
|
11877
12902
|
joinId: record.joinId,
|
|
11878
12903
|
scope: record.scope,
|
|
@@ -11884,11 +12909,107 @@ async function remoteContext(record, pathOrDocId, flags = {}) {
|
|
|
11884
12909
|
startLine: sl,
|
|
11885
12910
|
endLine: el,
|
|
11886
12911
|
markdown,
|
|
11887
|
-
|
|
11888
|
-
|
|
11889
|
-
|
|
11890
|
-
|
|
12912
|
+
...flags["no-review"] ? {} : {
|
|
12913
|
+
comments: document.sidecar.comments.filter((comment2) => comment2.status === "open"),
|
|
12914
|
+
suggestions: document.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
|
|
12915
|
+
anchors: document.anchors
|
|
12916
|
+
},
|
|
12917
|
+
images: document.images,
|
|
12918
|
+
links: document.links
|
|
12919
|
+
};
|
|
12920
|
+
}
|
|
12921
|
+
async function remoteReview(record, pathOrDocId) {
|
|
12922
|
+
const document = pathOrDocId ? await fetchDocument(record, pathOrDocId) : (await fetchState(record)).document;
|
|
12923
|
+
await refreshPresence(rootScopedRecordFor(record, document.rootId), document.docId);
|
|
12924
|
+
return {
|
|
12925
|
+
joinId: record.joinId,
|
|
12926
|
+
scope: record.scope,
|
|
12927
|
+
...reviewStateForDocument(document)
|
|
12928
|
+
};
|
|
12929
|
+
}
|
|
12930
|
+
function withRemoteSummaryNextCommands(summary, record, document, requestedTarget) {
|
|
12931
|
+
const target = remoteCommandTarget(record, document, requestedTarget);
|
|
12932
|
+
const firstEnd = Math.min(summary.totalLines, 120);
|
|
12933
|
+
return {
|
|
12934
|
+
...summary,
|
|
12935
|
+
nextCommands: [
|
|
12936
|
+
`mdocs remote context${target} --start-line 1 --end-line ${firstEnd} --no-review --json`,
|
|
12937
|
+
summary.totalLines > firstEnd ? `mdocs remote context${target} --start-line ${firstEnd + 1} --end-line ${Math.min(summary.totalLines, firstEnd + 120)} --no-review --json` : void 0,
|
|
12938
|
+
summary.reviewCounts.openComments || summary.reviewCounts.openSuggestions || summary.reviewCounts.anchorsNeedingReview ? `mdocs remote review${target} --json` : void 0
|
|
12939
|
+
].filter((command) => Boolean(command))
|
|
12940
|
+
};
|
|
12941
|
+
}
|
|
12942
|
+
function remoteCommandTarget(record, document, requestedTarget) {
|
|
12943
|
+
if (record.scope === "file" && (!requestedTarget || requestedTarget === document.docId || requestedTarget === document.path)) return "";
|
|
12944
|
+
if (record.scope === "workspace") return ` ${document.rootId}:${document.docId}`;
|
|
12945
|
+
return ` ${document.docId}`;
|
|
12946
|
+
}
|
|
12947
|
+
async function remoteCreateFile(root, record, parsed) {
|
|
12948
|
+
assertProjectScope(record, "create-file");
|
|
12949
|
+
const rootRecord = assertRootScoped(record, "create-file");
|
|
12950
|
+
const path = parsed.command[2];
|
|
12951
|
+
if (!path) {
|
|
12952
|
+
throw new CliError("usage_error", "Missing file path.", {
|
|
12953
|
+
hint: "Run mdocs remote create-file <path> --with-file /tmp/initial.md --json."
|
|
12954
|
+
});
|
|
12955
|
+
}
|
|
12956
|
+
const tree = await fetchTree(rootRecord);
|
|
12957
|
+
const markdownInput = await readOptionalTextFlag(parsed.flags, root, ["with", "markdown", "body"]);
|
|
12958
|
+
const markdown = markdownInput ?? defaultMarkdown(path);
|
|
12959
|
+
const normalizedPath = normalizeRemoteWorkspacePath(path, tree.root.pathRules, Buffer.byteLength(markdown, "utf8"));
|
|
12960
|
+
if (tree.docs.some((doc) => doc.path === normalizedPath)) {
|
|
12961
|
+
throw new CliError("validation_error", `Document already exists: ${normalizedPath}`, {
|
|
12962
|
+
hint: "Choose a new path or use mdocs remote context to inspect the existing document."
|
|
12963
|
+
});
|
|
12964
|
+
}
|
|
12965
|
+
const docId = createDocId();
|
|
12966
|
+
const title = titleFromMarkdown(normalizedPath, markdown);
|
|
12967
|
+
const sidecar = emptySidecar(docId, normalizedPath, title);
|
|
12968
|
+
const baseDocument = {
|
|
12969
|
+
workspaceId: rootRecord.workspaceId,
|
|
12970
|
+
rootId: rootRecord.rootId,
|
|
12971
|
+
docId,
|
|
12972
|
+
path: normalizedPath,
|
|
12973
|
+
title,
|
|
12974
|
+
markdown,
|
|
12975
|
+
sidecar,
|
|
12976
|
+
anchors: [],
|
|
12977
|
+
images: [],
|
|
12978
|
+
links: [],
|
|
12979
|
+
openComments: 0,
|
|
12980
|
+
openSuggestions: 0,
|
|
12981
|
+
currentSha: tree.root.canonical.head
|
|
11891
12982
|
};
|
|
12983
|
+
const document = await pushDocument(rootRecord, baseDocument, markdown, sidecar);
|
|
12984
|
+
await recordHead(root, record, document);
|
|
12985
|
+
return { document };
|
|
12986
|
+
}
|
|
12987
|
+
async function remoteMoveFile(root, record, parsed) {
|
|
12988
|
+
assertProjectScope(record, "move-file");
|
|
12989
|
+
const rootRecord = assertRootScoped(record, "move-file");
|
|
12990
|
+
const pathOrDocId = parsed.command[2];
|
|
12991
|
+
if (!pathOrDocId) {
|
|
12992
|
+
throw new CliError("usage_error", "Missing source document path or docId.", {
|
|
12993
|
+
hint: "Run mdocs remote move-file <path|docId> --to <new-path> --json."
|
|
12994
|
+
});
|
|
12995
|
+
}
|
|
12996
|
+
const nextPathFlag = requiredFlag(parsed.flags, "to");
|
|
12997
|
+
const document = await fetchDocument(record, pathOrDocId);
|
|
12998
|
+
const tree = await fetchTree(rootRecord);
|
|
12999
|
+
const nextPath = normalizeRemoteWorkspacePath(nextPathFlag, tree.root.pathRules, Buffer.byteLength(document.markdown, "utf8"));
|
|
13000
|
+
if (tree.docs.some((doc) => doc.docId !== document.docId && doc.path === nextPath)) {
|
|
13001
|
+
throw new CliError("validation_error", `Document already exists: ${nextPath}`, {
|
|
13002
|
+
hint: "Choose a path that is not already in use."
|
|
13003
|
+
});
|
|
13004
|
+
}
|
|
13005
|
+
const moved = await pushDocument(
|
|
13006
|
+
rootScopedRecordFor(record, document.rootId),
|
|
13007
|
+
{ ...document, path: nextPath, title: titleFromMarkdown(nextPath, document.markdown) },
|
|
13008
|
+
document.markdown,
|
|
13009
|
+
document.sidecar
|
|
13010
|
+
);
|
|
13011
|
+
await recordHead(root, record, moved);
|
|
13012
|
+
return { document: moved };
|
|
11892
13013
|
}
|
|
11893
13014
|
async function remoteComment(root, record, parsed) {
|
|
11894
13015
|
const body = await readRequiredTextFlag(parsed.flags, root, ["body", "message"]);
|
|
@@ -11913,11 +13034,71 @@ async function remoteSuggest(root, record, parsed) {
|
|
|
11913
13034
|
);
|
|
11914
13035
|
return { suggestion: result.created, document: result.document };
|
|
11915
13036
|
}
|
|
13037
|
+
async function remoteCreateFolder(record, parsed) {
|
|
13038
|
+
assertProjectScope(record, "create-folder");
|
|
13039
|
+
const rootRecord = assertRootScoped(record, "create-folder");
|
|
13040
|
+
const name = folderNameFromArgs(parsed);
|
|
13041
|
+
const parentId = optionalFolderTarget(parsed.flags.parent);
|
|
13042
|
+
await refreshPresence(rootRecord, record.docId);
|
|
13043
|
+
return postLibraryFolder(rootRecord, { name, ...parentId ? { parentId } : {} }, actorForRecord(record));
|
|
13044
|
+
}
|
|
13045
|
+
async function remoteUpdateFolder(record, parsed) {
|
|
13046
|
+
assertProjectScope(record, "update-folder");
|
|
13047
|
+
const rootRecord = assertRootScoped(record, "update-folder");
|
|
13048
|
+
const folderId = parsed.command[2];
|
|
13049
|
+
if (!folderId) {
|
|
13050
|
+
throw new CliError("usage_error", "Missing folder id.", {
|
|
13051
|
+
hint: "Run mdocs remote update-folder <folderId> --name <name> --json."
|
|
13052
|
+
});
|
|
13053
|
+
}
|
|
13054
|
+
const input = {};
|
|
13055
|
+
if (typeof parsed.flags.name === "string") input.name = parsed.flags.name;
|
|
13056
|
+
if (parsed.flags.home === true || parsed.flags.home === "true") input.parentId = null;
|
|
13057
|
+
else if (parsed.flags.parent !== void 0) input.parentId = optionalFolderTarget(parsed.flags.parent);
|
|
13058
|
+
if (input.name === void 0 && input.parentId === void 0) {
|
|
13059
|
+
throw new CliError("usage_error", "Nothing to update.", {
|
|
13060
|
+
hint: "Pass --name <name>, --parent <folderId>, or --home."
|
|
13061
|
+
});
|
|
13062
|
+
}
|
|
13063
|
+
await refreshPresence(rootRecord, record.docId);
|
|
13064
|
+
return patchLibraryFolder(rootRecord, folderId, input, actorForRecord(record));
|
|
13065
|
+
}
|
|
13066
|
+
async function remoteMoveRoot(record, parsed) {
|
|
13067
|
+
assertProjectScope(record, "move-root");
|
|
13068
|
+
const rootRecord = assertRootScoped(record, "move-root");
|
|
13069
|
+
const folderId = parsed.flags.home === true || parsed.flags.home === "true" ? null : optionalFolderTarget(parsed.flags.folder);
|
|
13070
|
+
if (folderId === void 0) {
|
|
13071
|
+
throw new CliError("usage_error", "Missing target folder.", {
|
|
13072
|
+
hint: "Pass --folder <folderId> to move into a folder, or --home to move to the top level."
|
|
13073
|
+
});
|
|
13074
|
+
}
|
|
13075
|
+
await refreshPresence(rootRecord, record.docId);
|
|
13076
|
+
return postMoveRoot(rootRecord, folderId, actorForRecord(record));
|
|
13077
|
+
}
|
|
13078
|
+
async function remoteInviteFolder(record, parsed) {
|
|
13079
|
+
assertProjectScope(record, "invite-folder");
|
|
13080
|
+
const rootRecord = assertRootScoped(record, "invite-folder");
|
|
13081
|
+
const folderId = parsed.command[2];
|
|
13082
|
+
if (!folderId) {
|
|
13083
|
+
throw new CliError("usage_error", "Missing folder id.", {
|
|
13084
|
+
hint: "Run mdocs remote invite-folder <folderId> --email person@example.com --role edit --json."
|
|
13085
|
+
});
|
|
13086
|
+
}
|
|
13087
|
+
const email = requiredFlag(parsed.flags, "email");
|
|
13088
|
+
const role = typeof parsed.flags.role === "string" ? parsed.flags.role : "edit";
|
|
13089
|
+
if (!["read", "suggest", "edit", "admin"].includes(role)) {
|
|
13090
|
+
throw new CliError("usage_error", "Invalid --role.", {
|
|
13091
|
+
hint: "Use --role read, suggest, edit, or admin."
|
|
13092
|
+
});
|
|
13093
|
+
}
|
|
13094
|
+
await refreshPresence(rootRecord, record.docId);
|
|
13095
|
+
return postFolderInvite(rootRecord, folderId, { email, role }, actorForRecord(record));
|
|
13096
|
+
}
|
|
11916
13097
|
async function remoteReject(root, record, parsed) {
|
|
11917
13098
|
const suggestionId = parsed.command[2];
|
|
11918
13099
|
if (!suggestionId) {
|
|
11919
13100
|
throw new CliError("usage_error", "Missing suggestion id.", {
|
|
11920
|
-
hint: "List open suggestions with mdocs remote
|
|
13101
|
+
hint: "List open suggestions with mdocs remote review --json, then run mdocs remote reject <suggestionId>."
|
|
11921
13102
|
});
|
|
11922
13103
|
}
|
|
11923
13104
|
const pathOrDocId = parsed.command[3] ?? record.docId;
|
|
@@ -11927,12 +13108,13 @@ async function remoteReject(root, record, parsed) {
|
|
|
11927
13108
|
});
|
|
11928
13109
|
}
|
|
11929
13110
|
let document = await fetchDocument(record, pathOrDocId);
|
|
11930
|
-
|
|
13111
|
+
let documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
13112
|
+
await refreshPresence(documentRecord, document.docId);
|
|
11931
13113
|
const actor = actorForRecord(record);
|
|
11932
13114
|
const suggestion = document.sidecar.suggestions.find((candidate) => candidate.id === suggestionId);
|
|
11933
13115
|
if (!suggestion) {
|
|
11934
13116
|
throw new CliError("not_found", `Unknown suggestion: ${suggestionId}`, {
|
|
11935
|
-
hint: "List suggestion ids with mdocs remote
|
|
13117
|
+
hint: "List suggestion ids with mdocs remote review --json."
|
|
11936
13118
|
});
|
|
11937
13119
|
}
|
|
11938
13120
|
if (suggestion.status !== "open") {
|
|
@@ -11942,7 +13124,7 @@ async function remoteReject(root, record, parsed) {
|
|
|
11942
13124
|
}
|
|
11943
13125
|
try {
|
|
11944
13126
|
const additions = { anchors: [], comments: [], suggestions: [], withdrawSuggestionIds: [suggestionId] };
|
|
11945
|
-
const pushed = await postReview(
|
|
13127
|
+
const pushed = await postReview(documentRecord, document.docId, additions, actor);
|
|
11946
13128
|
await recordHead(root, record, pushed);
|
|
11947
13129
|
const updated = pushed.sidecar.suggestions.find((candidate) => candidate.id === suggestionId);
|
|
11948
13130
|
return { suggestion: updated ?? suggestion, document: pushed };
|
|
@@ -11956,11 +13138,12 @@ async function remoteReject(root, record, parsed) {
|
|
|
11956
13138
|
}
|
|
11957
13139
|
for (let index = 0; index < PUSH_REBASE_ATTEMPTS; index += 1) {
|
|
11958
13140
|
if (index > 0) document = await fetchDocument(record, document.docId);
|
|
13141
|
+
documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
11959
13142
|
const io = new RemoteDocumentIO(document);
|
|
11960
13143
|
const rejected = await rejectSuggestion(io, suggestionId, actor);
|
|
11961
13144
|
const state = await getDocumentState(io, document.docId);
|
|
11962
13145
|
try {
|
|
11963
|
-
const pushed = await pushDocument(
|
|
13146
|
+
const pushed = await pushDocument(documentRecord, document, state.markdown, state.sidecar);
|
|
11964
13147
|
await recordHead(root, record, pushed);
|
|
11965
13148
|
return { suggestion: rejected, document: pushed };
|
|
11966
13149
|
} catch (error) {
|
|
@@ -11969,7 +13152,7 @@ async function remoteReject(root, record, parsed) {
|
|
|
11969
13152
|
}
|
|
11970
13153
|
}
|
|
11971
13154
|
throw new CliError("conflict", "Remote change was not accepted after retries.", {
|
|
11972
|
-
hint: "The document is changing rapidly. Refetch with mdocs remote context --json
|
|
13155
|
+
hint: "The document is changing rapidly. Refetch with mdocs remote context --summary --json, then retry against the needed content range."
|
|
11973
13156
|
});
|
|
11974
13157
|
}
|
|
11975
13158
|
function isWithdrawalUnsupported(error) {
|
|
@@ -11985,7 +13168,8 @@ async function submitReview(root, record, pathOrDocId, mutate) {
|
|
|
11985
13168
|
});
|
|
11986
13169
|
}
|
|
11987
13170
|
let document = await fetchDocument(record, pathOrDocId);
|
|
11988
|
-
|
|
13171
|
+
let documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
13172
|
+
await refreshPresence(documentRecord, document.docId);
|
|
11989
13173
|
const build = async (base) => {
|
|
11990
13174
|
const io = new RemoteDocumentIO(base);
|
|
11991
13175
|
const created = await mutate(io, base.docId);
|
|
@@ -12002,7 +13186,7 @@ async function submitReview(root, record, pathOrDocId, mutate) {
|
|
|
12002
13186
|
};
|
|
12003
13187
|
let attempt = await build(document);
|
|
12004
13188
|
try {
|
|
12005
|
-
const pushed = await postReview(
|
|
13189
|
+
const pushed = await postReview(documentRecord, document.docId, attempt.additions, actorForRecord(record));
|
|
12006
13190
|
await recordHead(root, record, pushed);
|
|
12007
13191
|
return { created: attempt.created, document: pushed };
|
|
12008
13192
|
} catch (error) {
|
|
@@ -12011,10 +13195,11 @@ async function submitReview(root, record, pathOrDocId, mutate) {
|
|
|
12011
13195
|
for (let index = 0; index < PUSH_REBASE_ATTEMPTS; index += 1) {
|
|
12012
13196
|
if (index > 0) {
|
|
12013
13197
|
document = await fetchDocument(record, document.docId);
|
|
13198
|
+
documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
12014
13199
|
attempt = await build(document);
|
|
12015
13200
|
}
|
|
12016
13201
|
try {
|
|
12017
|
-
const pushed = await pushDocument(
|
|
13202
|
+
const pushed = await pushDocument(documentRecord, document, attempt.state.markdown, attempt.state.sidecar);
|
|
12018
13203
|
await recordHead(root, record, pushed);
|
|
12019
13204
|
return { created: attempt.created, document: pushed };
|
|
12020
13205
|
} catch (error) {
|
|
@@ -12023,7 +13208,7 @@ async function submitReview(root, record, pathOrDocId, mutate) {
|
|
|
12023
13208
|
}
|
|
12024
13209
|
}
|
|
12025
13210
|
throw new CliError("conflict", "Remote change was not accepted after retries.", {
|
|
12026
|
-
hint: "The document is changing rapidly. Refetch with mdocs remote context --json
|
|
13211
|
+
hint: "The document is changing rapidly. Refetch with mdocs remote context --summary --json, then retry against the needed content range."
|
|
12027
13212
|
});
|
|
12028
13213
|
}
|
|
12029
13214
|
function isReviewEndpointUnsupported(error) {
|
|
@@ -12038,8 +13223,9 @@ async function recordHead(root, record, document) {
|
|
|
12038
13223
|
}
|
|
12039
13224
|
async function remoteEvents(root, record, flags) {
|
|
12040
13225
|
const after = typeof flags.after === "string" ? Number(flags.after) : record.lastSeenEventId;
|
|
12041
|
-
|
|
12042
|
-
|
|
13226
|
+
const rootRecord = assertRootScoped(record, "events");
|
|
13227
|
+
await refreshPresence(rootRecord, record.docId);
|
|
13228
|
+
const response = await fetchPendingEvents(rootRecord, after);
|
|
12043
13229
|
await writeJoinRecord(root, {
|
|
12044
13230
|
...record,
|
|
12045
13231
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -12048,7 +13234,7 @@ async function remoteEvents(root, record, flags) {
|
|
|
12048
13234
|
if (response.truncated) {
|
|
12049
13235
|
return {
|
|
12050
13236
|
...response,
|
|
12051
|
-
hint: "Older events were dropped from the bounded event log. Refetch
|
|
13237
|
+
hint: "Older events were dropped from the bounded event log. Refetch with mdocs remote context --summary --json, then read needed content pages and review state."
|
|
12052
13238
|
};
|
|
12053
13239
|
}
|
|
12054
13240
|
return response;
|
|
@@ -12057,11 +13243,11 @@ function parseShareUrl(value, flags) {
|
|
|
12057
13243
|
const url = new URL(value);
|
|
12058
13244
|
const route = parseSharePath(url.pathname);
|
|
12059
13245
|
if (!route) {
|
|
12060
|
-
throw new CliError("usage_error", "Share URL must be a Magic Markdown /share/<workspace
|
|
13246
|
+
throw new CliError("usage_error", "Share URL must be a Magic Markdown /share/<workspace>[/<root>[/doc]] URL.", {
|
|
12061
13247
|
hint: "Copy the share link from the Magic Markdown share dialog."
|
|
12062
13248
|
});
|
|
12063
13249
|
}
|
|
12064
|
-
const scope = flags.scope === "project" || !route.docId && flags.scope !== "file" ? "project" : "file";
|
|
13250
|
+
const scope = flags.scope === "workspace" || !route.rootId ? "workspace" : flags.scope === "project" || !route.docId && flags.scope !== "file" ? "project" : "file";
|
|
12065
13251
|
const explicitDoc = typeof flags.doc === "string" ? flags.doc : typeof flags["doc-id"] === "string" ? flags["doc-id"] : void 0;
|
|
12066
13252
|
return {
|
|
12067
13253
|
joinId: joinIdFor(value),
|
|
@@ -12069,8 +13255,8 @@ function parseShareUrl(value, flags) {
|
|
|
12069
13255
|
shareUrl: value,
|
|
12070
13256
|
origin: url.origin,
|
|
12071
13257
|
workspaceId: route.workspaceId,
|
|
12072
|
-
rootId: route.rootId,
|
|
12073
|
-
docId: explicitDoc ?? route.docId,
|
|
13258
|
+
rootId: scope === "workspace" ? void 0 : route.rootId,
|
|
13259
|
+
docId: scope === "workspace" ? void 0 : explicitDoc ?? route.docId,
|
|
12074
13260
|
agentId: "",
|
|
12075
13261
|
agentName: "",
|
|
12076
13262
|
joinedAt: "",
|
|
@@ -12078,6 +13264,15 @@ function parseShareUrl(value, flags) {
|
|
|
12078
13264
|
lastSeenEventId: 0
|
|
12079
13265
|
};
|
|
12080
13266
|
}
|
|
13267
|
+
function shareUrlForScope(value, record) {
|
|
13268
|
+
const url = new URL(value);
|
|
13269
|
+
url.pathname = buildSharePath(
|
|
13270
|
+
record.workspaceId,
|
|
13271
|
+
record.scope === "workspace" ? void 0 : record.rootId,
|
|
13272
|
+
record.scope === "file" ? record.docId : void 0
|
|
13273
|
+
);
|
|
13274
|
+
return url.toString();
|
|
13275
|
+
}
|
|
12081
13276
|
function joinSummary(record, document) {
|
|
12082
13277
|
return {
|
|
12083
13278
|
connected: true,
|
|
@@ -12086,19 +13281,84 @@ function joinSummary(record, document) {
|
|
|
12086
13281
|
workspaceId: record.workspaceId,
|
|
12087
13282
|
rootId: record.rootId,
|
|
12088
13283
|
docId: record.docId,
|
|
12089
|
-
currentHead: document
|
|
12090
|
-
document
|
|
12091
|
-
|
|
12092
|
-
|
|
12093
|
-
|
|
12094
|
-
|
|
13284
|
+
currentHead: document?.currentSha,
|
|
13285
|
+
...document ? {
|
|
13286
|
+
document: {
|
|
13287
|
+
docId: document.docId,
|
|
13288
|
+
path: document.path,
|
|
13289
|
+
title: document.title
|
|
13290
|
+
}
|
|
13291
|
+
} : {},
|
|
12095
13292
|
nextCommands: [
|
|
12096
|
-
"mdocs remote context --json",
|
|
13293
|
+
record.scope === "workspace" ? "mdocs remote map --json" : "mdocs remote context --summary --json",
|
|
12097
13294
|
record.scope === "project" ? "mdocs remote map --json" : void 0,
|
|
12098
|
-
"mdocs remote
|
|
13295
|
+
record.scope === "workspace" ? "mdocs remote library --json" : void 0,
|
|
13296
|
+
record.scope === "workspace" ? void 0 : "mdocs remote events --json"
|
|
12099
13297
|
].filter(Boolean)
|
|
12100
13298
|
};
|
|
12101
13299
|
}
|
|
13300
|
+
function assertProjectScope(record, command) {
|
|
13301
|
+
if (record.scope === "project") return;
|
|
13302
|
+
throw new CliError("usage_error", `remote ${command} requires a project-scoped join.`, {
|
|
13303
|
+
hint: "Rejoin the share with `mdocs join <share-url> --scope project --json`, then retry."
|
|
13304
|
+
});
|
|
13305
|
+
}
|
|
13306
|
+
function assertRootScoped(record, command) {
|
|
13307
|
+
if (record.rootId) return rootScopedRecordFor(record, record.rootId);
|
|
13308
|
+
throw new CliError("usage_error", `remote ${command} needs a specific project root.`, {
|
|
13309
|
+
hint: "For Home workspace joins, run `mdocs remote map --json` and use a document target, or join a project share for root organization commands."
|
|
13310
|
+
});
|
|
13311
|
+
}
|
|
13312
|
+
function rootScopedRecord(record) {
|
|
13313
|
+
return record.rootId ? rootScopedRecordFor(record, record.rootId) : void 0;
|
|
13314
|
+
}
|
|
13315
|
+
function rootScopedRecordFor(record, rootId) {
|
|
13316
|
+
return {
|
|
13317
|
+
...record,
|
|
13318
|
+
rootId,
|
|
13319
|
+
scope: record.scope === "file" ? "file" : "project"
|
|
13320
|
+
};
|
|
13321
|
+
}
|
|
13322
|
+
function emptySidecar(docId, path, title) {
|
|
13323
|
+
return {
|
|
13324
|
+
schemaVersion: SIDECAR_SCHEMA_VERSION,
|
|
13325
|
+
docId,
|
|
13326
|
+
path,
|
|
13327
|
+
title,
|
|
13328
|
+
anchors: [],
|
|
13329
|
+
comments: [],
|
|
13330
|
+
suggestions: [],
|
|
13331
|
+
changeSets: [],
|
|
13332
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13333
|
+
};
|
|
13334
|
+
}
|
|
13335
|
+
function defaultMarkdown(path) {
|
|
13336
|
+
return `# ${titleFromMarkdown(path, "")}
|
|
13337
|
+
`;
|
|
13338
|
+
}
|
|
13339
|
+
function normalizeRemoteWorkspacePath(path, rules, sizeBytes) {
|
|
13340
|
+
try {
|
|
13341
|
+
return assertWorkspacePathAllowed(path, rules, sizeBytes);
|
|
13342
|
+
} catch (error) {
|
|
13343
|
+
throw new CliError("validation_error", error instanceof Error ? error.message : String(error));
|
|
13344
|
+
}
|
|
13345
|
+
}
|
|
13346
|
+
function folderNameFromArgs(parsed) {
|
|
13347
|
+
const name = parsed.command[2] ?? (typeof parsed.flags.name === "string" ? parsed.flags.name : void 0);
|
|
13348
|
+
if (!name?.trim()) {
|
|
13349
|
+
throw new CliError("usage_error", "Missing folder name.", {
|
|
13350
|
+
hint: "Run mdocs remote create-folder <name> --json."
|
|
13351
|
+
});
|
|
13352
|
+
}
|
|
13353
|
+
return name;
|
|
13354
|
+
}
|
|
13355
|
+
function optionalFolderTarget(value) {
|
|
13356
|
+
if (value === void 0) return void 0;
|
|
13357
|
+
if (typeof value !== "string") return void 0;
|
|
13358
|
+
const trimmed = value.trim();
|
|
13359
|
+
if (!trimmed || trimmed === "home" || trimmed === "null" || trimmed === "top") return null;
|
|
13360
|
+
return trimmed;
|
|
13361
|
+
}
|
|
12102
13362
|
|
|
12103
13363
|
// src/bridge.ts
|
|
12104
13364
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -12529,24 +13789,23 @@ async function main() {
|
|
|
12529
13789
|
print(await getWorkspaceMap(io), parsed.flags);
|
|
12530
13790
|
return;
|
|
12531
13791
|
}
|
|
13792
|
+
case "graph": {
|
|
13793
|
+
print(await getWorkspaceGraph(io), parsed.flags);
|
|
13794
|
+
return;
|
|
13795
|
+
}
|
|
12532
13796
|
case "state": {
|
|
12533
13797
|
print(await getDocumentState(io, requiredArg(parsed.command[1], "path")), parsed.flags);
|
|
12534
13798
|
return;
|
|
12535
13799
|
}
|
|
12536
13800
|
case "context": {
|
|
12537
13801
|
const state = await getDocumentState(io, requiredArg(parsed.command[1], "path"));
|
|
13802
|
+
if (parsed.flags.summary || parsed.flags["metadata-only"]) {
|
|
13803
|
+
print(summarizeDocumentContext(state), parsed.flags);
|
|
13804
|
+
return;
|
|
13805
|
+
}
|
|
12538
13806
|
const startLine = parsed.flags["start-line"] !== void 0 ? Number(parsed.flags["start-line"]) : void 0;
|
|
12539
13807
|
const endLine = parsed.flags["end-line"] !== void 0 ? Number(parsed.flags["end-line"]) : void 0;
|
|
12540
|
-
const
|
|
12541
|
-
const totalLines = lines.length;
|
|
12542
|
-
const sl = startLine ?? 1;
|
|
12543
|
-
const el = endLine ?? totalLines;
|
|
12544
|
-
if (sl < 1 || el > totalLines || sl > el) {
|
|
12545
|
-
throw new CliError("invalid_range", `Line range ${sl}:${el} is out of bounds for a ${totalLines}-line document.`, {
|
|
12546
|
-
hint: `Use --start-line and --end-line with 1-based line numbers within 1:${totalLines}.`
|
|
12547
|
-
});
|
|
12548
|
-
}
|
|
12549
|
-
const markdown = startLine !== void 0 || endLine !== void 0 ? lines.slice(sl - 1, el).join("\n") : state.markdown;
|
|
13808
|
+
const { markdown, totalLines, startLine: sl, endLine: el } = sliceMarkdown(state.markdown, startLine, endLine);
|
|
12550
13809
|
print({
|
|
12551
13810
|
docId: state.docId,
|
|
12552
13811
|
path: state.path,
|
|
@@ -12559,12 +13818,20 @@ async function main() {
|
|
|
12559
13818
|
...image2,
|
|
12560
13819
|
...image2.workspacePath ? { absolutePath: resolve5(cwd, image2.workspacePath) } : {}
|
|
12561
13820
|
})),
|
|
12562
|
-
|
|
12563
|
-
|
|
12564
|
-
|
|
13821
|
+
links: state.links,
|
|
13822
|
+
...parsed.flags["no-review"] ? {} : {
|
|
13823
|
+
comments: state.sidecar.comments.filter((comment2) => comment2.status === "open"),
|
|
13824
|
+
suggestions: state.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
|
|
13825
|
+
anchors: state.anchors
|
|
13826
|
+
}
|
|
12565
13827
|
}, parsed.flags);
|
|
12566
13828
|
return;
|
|
12567
13829
|
}
|
|
13830
|
+
case "review": {
|
|
13831
|
+
const state = await getDocumentState(io, requiredArg(parsed.command[1], "path"));
|
|
13832
|
+
print(reviewStateForDocument(state), parsed.flags);
|
|
13833
|
+
return;
|
|
13834
|
+
}
|
|
12568
13835
|
case "comments": {
|
|
12569
13836
|
const state = await getDocumentState(io, requiredArg(parsed.command[1], "path"));
|
|
12570
13837
|
print(state.sidecar.comments, parsed.flags);
|
|
@@ -12822,8 +14089,11 @@ Commands:
|
|
|
12822
14089
|
agent commands --json Print machine-readable command metadata
|
|
12823
14090
|
init [path] Initialize .mdocs and index Markdown files
|
|
12824
14091
|
map --json Print workspace map
|
|
14092
|
+
graph --json Print workspace knowledge graph nodes and edges
|
|
12825
14093
|
state <path> --json Print merged Markdown and sidecar state
|
|
12826
14094
|
context <path> --json Print agent-ready context
|
|
14095
|
+
context <path> --summary --json Print document metadata and headings
|
|
14096
|
+
review <path> --json Print open comments, suggestions, anchors
|
|
12827
14097
|
comments <path> --json List comments
|
|
12828
14098
|
comment <path> --range 3:5 --body Add a sidecar comment
|
|
12829
14099
|
suggestions <path> --json List suggestions
|
|
@@ -12841,9 +14111,13 @@ Commands:
|
|
|
12841
14111
|
checkpoint create|list|restore Manage local reversible checkpoints
|
|
12842
14112
|
join <share-url> --json Join a Magic Markdown share through the CLI
|
|
12843
14113
|
joins --json List saved Magic Markdown remote joins
|
|
12844
|
-
remote map|context|
|
|
14114
|
+
remote map|graph|context|review|create-file|move-file
|
|
14115
|
+
Work with documents in the active remote join
|
|
14116
|
+
remote comment|suggest Add remote review comments and suggestions
|
|
12845
14117
|
remote reject <suggestionId> Withdraw a suggestion you submitted remotely
|
|
12846
14118
|
remote events|history|restore Poll events, list commits, restore snapshots
|
|
14119
|
+
remote library|create-folder|update-folder|move-root|invite-folder
|
|
14120
|
+
Organize the joined project library
|
|
12847
14121
|
bridge --workspace <id> --root . --url <base-url> --token <bridge-token>
|
|
12848
14122
|
Sync an approved local root with the workspace
|
|
12849
14123
|
(--token from the web "Bind agent filesystem"
|