@magic-markdown/cli 0.3.2 → 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 +691 -143
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -392,9 +392,16 @@ function replaceLineRange(markdown, range, replacement) {
|
|
|
392
392
|
const lines = getLines(markdown);
|
|
393
393
|
const before = lines.slice(0, range.startLine - 1);
|
|
394
394
|
const after = lines.slice(range.endLine);
|
|
395
|
-
const replacementLines = replacement
|
|
395
|
+
const replacementLines = replacementLinesForRange(replacement);
|
|
396
396
|
return [...before, ...replacementLines, ...after].join("\n");
|
|
397
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
|
+
}
|
|
398
405
|
function extractMarkdownImages(markdown, documentPath) {
|
|
399
406
|
const masked = maskCode(markdown);
|
|
400
407
|
const referenceDefinitions = collectReferenceDefinitions(markdown, masked);
|
|
@@ -957,8 +964,129 @@ function scoreContext(lines, startLine, endLine, selector) {
|
|
|
957
964
|
return Math.min(1, score);
|
|
958
965
|
}
|
|
959
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
|
+
|
|
960
1088
|
// ../core/src/patches.ts
|
|
961
|
-
function applySuggestion(markdown, suggestion) {
|
|
1089
|
+
function applySuggestion(markdown, suggestion, input) {
|
|
962
1090
|
if (suggestion.status !== "open") {
|
|
963
1091
|
throw new MdocsError(
|
|
964
1092
|
"validation_error",
|
|
@@ -966,18 +1094,130 @@ function applySuggestion(markdown, suggestion) {
|
|
|
966
1094
|
"Only open suggestions can be applied. List open suggestions with the suggestions command."
|
|
967
1095
|
);
|
|
968
1096
|
}
|
|
969
|
-
const
|
|
970
|
-
|
|
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) {
|
|
971
1109
|
throw new MdocsError(
|
|
972
1110
|
"conflict",
|
|
973
1111
|
`Suggestion ${suggestion.id} no longer applies cleanly: the document changed since it was created.`,
|
|
974
1112
|
"Re-read the document, then create a fresh suggestion against the current content."
|
|
975
1113
|
);
|
|
976
1114
|
}
|
|
977
|
-
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);
|
|
978
1157
|
assertCleanMarkdown(next);
|
|
979
1158
|
return next;
|
|
980
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
|
+
}
|
|
981
1221
|
|
|
982
1222
|
// ../core/src/graph.ts
|
|
983
1223
|
function resolveMarkdownLinks(links, currentDocument, documents) {
|
|
@@ -1349,6 +1589,8 @@ async function addComment(io, pathOrDocId, range, body, author = defaultActor())
|
|
|
1349
1589
|
}
|
|
1350
1590
|
async function addSuggestion(io, pathOrDocId, range, replacement, message, author = { id: "agent_local", kind: "agent", name: "Local Agent" }, changeSetId) {
|
|
1351
1591
|
const state = await getDocumentState(io, pathOrDocId);
|
|
1592
|
+
const manifest = await indexWorkspace(io);
|
|
1593
|
+
const entry = manifest.docs.find((doc) => doc.docId === state.docId);
|
|
1352
1594
|
assertLineRangeWithin(state.markdown, range);
|
|
1353
1595
|
assertCleanMarkdown(replacement);
|
|
1354
1596
|
const now = nowIso();
|
|
@@ -1374,6 +1616,10 @@ async function addSuggestion(io, pathOrDocId, range, replacement, message, autho
|
|
|
1374
1616
|
before,
|
|
1375
1617
|
after: replacement
|
|
1376
1618
|
},
|
|
1619
|
+
base: {
|
|
1620
|
+
contentHash: contentHashForText(state.markdown),
|
|
1621
|
+
...entry?.currentSha ? { head: entry.currentSha } : {}
|
|
1622
|
+
},
|
|
1377
1623
|
changeSetId,
|
|
1378
1624
|
createdAt: now,
|
|
1379
1625
|
updatedAt: now
|
|
@@ -1416,7 +1662,8 @@ async function acceptSuggestion(io, suggestionId, actor = defaultActor()) {
|
|
|
1416
1662
|
const sidecar = await readSidecar(io, entry.docId);
|
|
1417
1663
|
const suggestion = sidecar.suggestions.find((candidate) => candidate.id === suggestionId);
|
|
1418
1664
|
if (!suggestion) continue;
|
|
1419
|
-
const
|
|
1665
|
+
const anchor = sidecar.anchors.find((candidate) => candidate.id === suggestion.anchorId);
|
|
1666
|
+
const nextMarkdown = applySuggestion(markdown, suggestion, anchor);
|
|
1420
1667
|
await io.writeText(entry.path, nextMarkdown);
|
|
1421
1668
|
return updateSuggestionStatus(io, suggestionId, "accepted", actor);
|
|
1422
1669
|
}
|
|
@@ -1540,6 +1787,105 @@ function sidecarMarkdownMatchScore(sidecar, markdown) {
|
|
|
1540
1787
|
return score;
|
|
1541
1788
|
}
|
|
1542
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
|
+
|
|
1543
1889
|
// ../core/src/root-policy.ts
|
|
1544
1890
|
var DEFAULT_ROOT_PATH_RULES = {
|
|
1545
1891
|
include: ["**/*.md", "**/*.mdx", ".mdocs/**/*.json"],
|
|
@@ -1802,16 +2148,18 @@ function joinWorkspacePath(prefix, path) {
|
|
|
1802
2148
|
|
|
1803
2149
|
// ../core/src/share-routes.ts
|
|
1804
2150
|
function parseSharePath(pathname) {
|
|
1805
|
-
const [, workspaceId, rootId, docId] = pathname.match(/^\/share\/([^/]+)
|
|
1806
|
-
if (!workspaceId
|
|
2151
|
+
const [, workspaceId, rootId, docId] = pathname.match(/^\/share\/([^/]+)(?:\/([^/]+)(?:\/([^/]+))?)?\/?$/) ?? [];
|
|
2152
|
+
if (!workspaceId) return void 0;
|
|
1807
2153
|
return {
|
|
1808
2154
|
workspaceId: decodeURIComponent(workspaceId),
|
|
1809
|
-
rootId: decodeURIComponent(rootId),
|
|
2155
|
+
rootId: rootId ? decodeURIComponent(rootId) : void 0,
|
|
1810
2156
|
docId: docId ? decodeURIComponent(docId) : void 0
|
|
1811
2157
|
};
|
|
1812
2158
|
}
|
|
1813
2159
|
function buildSharePath(workspaceId, rootId, docId) {
|
|
1814
|
-
const
|
|
2160
|
+
const workspacePath = `/share/${encodeURIComponent(workspaceId)}`;
|
|
2161
|
+
if (!rootId) return workspacePath;
|
|
2162
|
+
const base = `${workspacePath}/${encodeURIComponent(rootId)}`;
|
|
1815
2163
|
return docId ? `${base}/${encodeURIComponent(docId)}` : base;
|
|
1816
2164
|
}
|
|
1817
2165
|
|
|
@@ -10642,7 +10990,7 @@ var RemoteDocumentIO = class {
|
|
|
10642
10990
|
};
|
|
10643
10991
|
|
|
10644
10992
|
// src/agent.ts
|
|
10645
|
-
var CLI_VERSION = "0.3.
|
|
10993
|
+
var CLI_VERSION = "0.3.5";
|
|
10646
10994
|
var CLI_PACKAGE_NAME = "@magic-markdown/cli";
|
|
10647
10995
|
var AGENT_COMMANDS = [
|
|
10648
10996
|
{
|
|
@@ -10672,11 +11020,12 @@ var AGENT_COMMANDS = [
|
|
|
10672
11020
|
{
|
|
10673
11021
|
name: "join",
|
|
10674
11022
|
summary: "Join a Magic Markdown share URL through the CLI, announce presence, and remember the session for later rejoin.",
|
|
10675
|
-
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",
|
|
10676
11024
|
output: "json",
|
|
10677
11025
|
mutates: true,
|
|
10678
11026
|
examples: [
|
|
10679
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',
|
|
10680
11029
|
"mdocs join --json"
|
|
10681
11030
|
],
|
|
10682
11031
|
notes: [
|
|
@@ -10694,13 +11043,14 @@ var AGENT_COMMANDS = [
|
|
|
10694
11043
|
{
|
|
10695
11044
|
name: "remote",
|
|
10696
11045
|
summary: "Read, organize, comment on, suggest edits to, monitor, and restore the active Magic Markdown remote join.",
|
|
10697
|
-
usage: "mdocs remote map|graph|context|create-file|move-file|comment|suggest|reject|events|history|restore|library|create-folder|update-folder|move-root|invite-folder --json",
|
|
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",
|
|
10698
11047
|
output: "json",
|
|
10699
11048
|
mutates: true,
|
|
10700
11049
|
examples: [
|
|
10701
|
-
"mdocs remote context --json",
|
|
10702
|
-
"mdocs remote context --start-line 1 --end-line 100 --json",
|
|
10703
|
-
"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",
|
|
10704
11054
|
"mdocs remote map --json",
|
|
10705
11055
|
"mdocs remote graph --json",
|
|
10706
11056
|
"mdocs remote create-file docs/new-note.md --with-file /tmp/initial.md --json",
|
|
@@ -10716,13 +11066,16 @@ var AGENT_COMMANDS = [
|
|
|
10716
11066
|
"mdocs remote invite-folder fold_abc123 --email person@example.com --role edit --json"
|
|
10717
11067
|
],
|
|
10718
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.",
|
|
10719
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>.",
|
|
10720
11073
|
"remote graph requires a project-scoped join and returns the shared project's Obsidian-style Markdown/wikilink graph.",
|
|
10721
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.",
|
|
10722
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.",
|
|
10723
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.",
|
|
10724
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.",
|
|
10725
|
-
"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.",
|
|
10726
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.",
|
|
10727
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."
|
|
10728
11081
|
]
|
|
@@ -10757,20 +11110,31 @@ var AGENT_COMMANDS = [
|
|
|
10757
11110
|
},
|
|
10758
11111
|
{
|
|
10759
11112
|
name: "context",
|
|
10760
|
-
summary: "Return agent-ready document Markdown,
|
|
10761
|
-
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",
|
|
10762
11115
|
output: "json",
|
|
10763
11116
|
mutates: true,
|
|
10764
11117
|
examples: [
|
|
11118
|
+
"mdocs context docs/example.md --summary --json",
|
|
10765
11119
|
"mdocs context docs/example.md --json",
|
|
10766
|
-
"mdocs context docs/example.md --start-line 1 --end-line 100 --json",
|
|
10767
|
-
"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"
|
|
10768
11122
|
],
|
|
10769
11123
|
notes: [
|
|
11124
|
+
"--summary returns metadata, heading line numbers, review counts, image counts, and link counts without returning Markdown.",
|
|
10770
11125
|
"The response always includes totalLines, startLine, and endLine. If totalLines > endLine, request the next page with --start-line <endLine+1>.",
|
|
10771
|
-
"--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."
|
|
10772
11128
|
]
|
|
10773
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
|
+
},
|
|
10774
11138
|
{
|
|
10775
11139
|
name: "state",
|
|
10776
11140
|
summary: "Return full merged Markdown and sidecar state for one document.",
|
|
@@ -10803,7 +11167,8 @@ var AGENT_COMMANDS = [
|
|
|
10803
11167
|
],
|
|
10804
11168
|
notes: [
|
|
10805
11169
|
"--range is 1-based and inclusive, validated against the current document line count.",
|
|
10806
|
-
"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."
|
|
10807
11172
|
]
|
|
10808
11173
|
},
|
|
10809
11174
|
{
|
|
@@ -10947,15 +11312,18 @@ Run \`mdocs help <command>\` (or \`mdocs <command> --help\`) for usage, examples
|
|
|
10947
11312
|
|
|
10948
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.
|
|
10949
11314
|
2. If working in a local workspace, run \`mdocs doctor --json\` to validate the workspace and learn recommended next commands.
|
|
10950
|
-
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 --json\` instead.
|
|
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>\`.
|
|
10951
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.
|
|
10952
|
-
5. For a document, run \`mdocs context <path|docId> --json\` locally or \`mdocs remote context <path|docId> --json\` for a joined share.
|
|
10953
|
-
6. 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.
|
|
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.
|
|
10954
11319
|
|
|
10955
11320
|
## Editing Rules
|
|
10956
11321
|
|
|
10957
11322
|
- Comments and suggestions are sidecar operations. They should not modify clean Markdown until a suggestion is accepted.
|
|
10958
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.
|
|
10959
11327
|
- Prefer \`--json\` for agent calls and parse stdout as JSON.
|
|
10960
11328
|
- For multiline suggestion text, write the exact replacement to a temporary file and pass \`--with-file <file>\`.
|
|
10961
11329
|
- For long comments or messages, use \`--body-file <file>\` or \`--message-file <file>\`.
|
|
@@ -10968,14 +11336,14 @@ Run \`mdocs help <command>\` (or \`mdocs <command> --help\`) for usage, examples
|
|
|
10968
11336
|
- Errors go to stderr. With \`--json\` they are structured: \`{ "ok": false, "error": { "code", "message", "hint" }, "cliVersion" }\`. Follow the \`hint\`.
|
|
10969
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).
|
|
10970
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.
|
|
10971
|
-
- \`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.
|
|
10972
11340
|
- Retries are safe: remote writes carry a changeId and the server deduplicates replays.
|
|
10973
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\`.
|
|
10974
11342
|
|
|
10975
11343
|
## Access Roles and Version History
|
|
10976
11344
|
|
|
10977
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.
|
|
10978
|
-
- Project-scoped agents with edit access can create and move Markdown files and can move the joined root into library folders. Folder invites and folder hierarchy updates require admin access on the joined root.
|
|
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.
|
|
10979
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.
|
|
10980
11348
|
|
|
10981
11349
|
## Core Commands
|
|
@@ -10990,7 +11358,7 @@ Start the local stdio MCP server with:
|
|
|
10990
11358
|
mdocs serve-mcp --cwd /path/to/workspace
|
|
10991
11359
|
\`\`\`
|
|
10992
11360
|
|
|
10993
|
-
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.
|
|
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.
|
|
10994
11362
|
`;
|
|
10995
11363
|
}
|
|
10996
11364
|
function formatCommandReference() {
|
|
@@ -11414,8 +11782,9 @@ function withMcpImageResources(io, state) {
|
|
|
11414
11782
|
}))
|
|
11415
11783
|
};
|
|
11416
11784
|
}
|
|
11417
|
-
async function contextForDocument(io, path, startLine, endLine) {
|
|
11785
|
+
async function contextForDocument(io, path, startLine, endLine, options = {}) {
|
|
11418
11786
|
const state = await getDocumentState(io, path);
|
|
11787
|
+
if (options.summary) return summarizeDocumentContext(state);
|
|
11419
11788
|
const { markdown, totalLines, startLine: sl, endLine: el } = sliceMarkdown(state.markdown, startLine, endLine);
|
|
11420
11789
|
return {
|
|
11421
11790
|
path: state.path,
|
|
@@ -11427,21 +11796,19 @@ async function contextForDocument(io, path, startLine, endLine) {
|
|
|
11427
11796
|
markdown,
|
|
11428
11797
|
images: withMcpImageResources(io, state).images,
|
|
11429
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 {
|
|
11430
11807
|
comments: state.sidecar.comments.filter((comment2) => comment2.status === "open"),
|
|
11431
11808
|
suggestions: state.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
|
|
11432
11809
|
anchors: state.anchors
|
|
11433
11810
|
};
|
|
11434
11811
|
}
|
|
11435
|
-
function sliceMarkdown(markdown, startLine, endLine) {
|
|
11436
|
-
const lines = markdown.split("\n");
|
|
11437
|
-
const totalLines = lines.length;
|
|
11438
|
-
const sl = startLine ?? 1;
|
|
11439
|
-
const el = endLine ?? totalLines;
|
|
11440
|
-
if (sl < 1 || el > totalLines || sl > el) {
|
|
11441
|
-
throw new Error(`Line range ${sl}:${el} is out of bounds for a ${totalLines}-line document.`);
|
|
11442
|
-
}
|
|
11443
|
-
return { markdown: lines.slice(sl - 1, el).join("\n"), totalLines, startLine: sl, endLine: el };
|
|
11444
|
-
}
|
|
11445
11812
|
function imageResourceUri(docId, index) {
|
|
11446
11813
|
return `mdocs://documents/${docId}/images/${index}`;
|
|
11447
11814
|
}
|
|
@@ -11519,7 +11886,7 @@ var tools = [
|
|
|
11519
11886
|
},
|
|
11520
11887
|
{
|
|
11521
11888
|
name: "mdocs_context",
|
|
11522
|
-
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.",
|
|
11523
11890
|
inputSchema: {
|
|
11524
11891
|
type: "object",
|
|
11525
11892
|
properties: {
|
|
@@ -11531,12 +11898,25 @@ var tools = [
|
|
|
11531
11898
|
endLine: {
|
|
11532
11899
|
type: "number",
|
|
11533
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."
|
|
11534
11909
|
}
|
|
11535
11910
|
},
|
|
11536
11911
|
required: ["path"],
|
|
11537
11912
|
additionalProperties: false
|
|
11538
11913
|
}
|
|
11539
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
|
+
},
|
|
11540
11920
|
{
|
|
11541
11921
|
name: "mdocs_state",
|
|
11542
11922
|
description: "Return full merged Markdown and sidecar state for one document.",
|
|
@@ -11560,14 +11940,14 @@ var tools = [
|
|
|
11560
11940
|
},
|
|
11561
11941
|
{
|
|
11562
11942
|
name: "mdocs_suggest",
|
|
11563
|
-
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.",
|
|
11564
11944
|
inputSchema: {
|
|
11565
11945
|
type: "object",
|
|
11566
11946
|
properties: {
|
|
11567
11947
|
path: documentPathSchema.properties.path,
|
|
11568
11948
|
startLine: { type: "number", description: "1-based start line." },
|
|
11569
11949
|
endLine: { type: "number", description: "1-based end line." },
|
|
11570
|
-
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." },
|
|
11571
11951
|
message: { type: "string", description: "Short explanation for the suggestion." },
|
|
11572
11952
|
...actorProperties
|
|
11573
11953
|
},
|
|
@@ -11789,8 +12169,12 @@ async function callTool(io, name, args) {
|
|
|
11789
12169
|
if (name === "mdocs_context") {
|
|
11790
12170
|
const startLine = typeof args.startLine === "number" ? args.startLine : void 0;
|
|
11791
12171
|
const endLine = typeof args.endLine === "number" ? args.endLine : void 0;
|
|
11792
|
-
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
|
+
});
|
|
11793
12176
|
}
|
|
12177
|
+
if (name === "mdocs_review") return reviewForDocument(io, requiredString(args, "path"));
|
|
11794
12178
|
if (name === "mdocs_state") return withMcpImageResources(io, await getDocumentState(io, requiredString(args, "path")));
|
|
11795
12179
|
if (name === "mdocs_comment") {
|
|
11796
12180
|
return addComment(
|
|
@@ -11897,12 +12281,22 @@ async function postPresenceAndReadState(share, shareUrl, docId, agentId, agentNa
|
|
|
11897
12281
|
});
|
|
11898
12282
|
}
|
|
11899
12283
|
async function fetchState(record) {
|
|
11900
|
-
if (!record.docId) {
|
|
12284
|
+
if (!record.rootId || !record.docId) {
|
|
11901
12285
|
throw new CliError("usage_error", "This join has no current document.", {
|
|
11902
12286
|
hint: "Pass a document path or docId, or list documents with mdocs remote map --json."
|
|
11903
12287
|
});
|
|
11904
12288
|
}
|
|
11905
|
-
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`, {
|
|
11906
12300
|
headers: shareHeaders(record.shareUrl)
|
|
11907
12301
|
});
|
|
11908
12302
|
}
|
|
@@ -11948,8 +12342,10 @@ async function postFolderInvite(record, folderId, input, actor) {
|
|
|
11948
12342
|
);
|
|
11949
12343
|
}
|
|
11950
12344
|
async function fetchDocument(record, pathOrDocId) {
|
|
12345
|
+
if (record.scope === "workspace") return fetchWorkspaceDocument(record, pathOrDocId);
|
|
12346
|
+
const rootRecord = requireRootRecord(record);
|
|
11951
12347
|
if (record.scope === "file" && record.docId) {
|
|
11952
|
-
const document = await fetchDocumentById(
|
|
12348
|
+
const document = await fetchDocumentById(rootRecord, record.docId);
|
|
11953
12349
|
if (pathOrDocId === document.docId || pathOrDocId === document.path) return document;
|
|
11954
12350
|
throw new CliError("usage_error", `This file-scoped join only covers ${document.path} (${document.docId}).`, {
|
|
11955
12351
|
hint: `Join a share link for ${pathOrDocId} to work on it.`
|
|
@@ -11957,19 +12353,19 @@ async function fetchDocument(record, pathOrDocId) {
|
|
|
11957
12353
|
}
|
|
11958
12354
|
if (pathOrDocId.startsWith("doc_") || pathOrDocId === record.docId) {
|
|
11959
12355
|
try {
|
|
11960
|
-
return await fetchDocumentById(
|
|
12356
|
+
return await fetchDocumentById(rootRecord, pathOrDocId);
|
|
11961
12357
|
} catch (error) {
|
|
11962
12358
|
if (!(error instanceof CliError) || error.code !== "not_found") throw error;
|
|
11963
12359
|
}
|
|
11964
12360
|
}
|
|
11965
|
-
const tree = await fetchTree(
|
|
12361
|
+
const tree = await fetchTree(rootRecord);
|
|
11966
12362
|
const docId = tree.docs.find((doc) => doc.docId === pathOrDocId || doc.path === pathOrDocId)?.docId;
|
|
11967
12363
|
if (!docId) {
|
|
11968
12364
|
throw new CliError("not_found", `No document matches "${pathOrDocId}" in the joined workspace.`, {
|
|
11969
12365
|
hint: "List documents and their paths/docIds with mdocs remote map --json."
|
|
11970
12366
|
});
|
|
11971
12367
|
}
|
|
11972
|
-
return fetchDocumentById(
|
|
12368
|
+
return fetchDocumentById(rootRecord, docId);
|
|
11973
12369
|
}
|
|
11974
12370
|
function fetchDocumentById(record, docId) {
|
|
11975
12371
|
return fetchJson(
|
|
@@ -11977,6 +12373,51 @@ function fetchDocumentById(record, docId) {
|
|
|
11977
12373
|
{ headers: shareHeaders(record.shareUrl) }
|
|
11978
12374
|
);
|
|
11979
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
|
+
}
|
|
11980
12421
|
async function postReview(record, docId, additions, actor) {
|
|
11981
12422
|
const response = await fetchJson(
|
|
11982
12423
|
`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/review`,
|
|
@@ -12059,7 +12500,7 @@ function conflictError(eventPayload) {
|
|
|
12059
12500
|
const conflict = eventPayload ?? {};
|
|
12060
12501
|
const head = typeof conflict.canonicalHead === "string" ? ` Current head: ${conflict.canonicalHead}.` : "";
|
|
12061
12502
|
return new CliError("conflict", `Remote change was not accepted: the document changed since it was read.${head}`, {
|
|
12062
|
-
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.",
|
|
12063
12504
|
details: eventPayload
|
|
12064
12505
|
});
|
|
12065
12506
|
}
|
|
@@ -12070,6 +12511,14 @@ function agentUrl(record, docId, action, query) {
|
|
|
12070
12511
|
function libraryUrl(record) {
|
|
12071
12512
|
return `${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/library`;
|
|
12072
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
|
+
}
|
|
12073
12522
|
function shareHeaders(shareUrl) {
|
|
12074
12523
|
return { Referer: shareUrl, Authorization: `Bearer ${shareUrl}` };
|
|
12075
12524
|
}
|
|
@@ -12107,7 +12556,7 @@ function remoteHttpError(status, value, text2) {
|
|
|
12107
12556
|
}
|
|
12108
12557
|
if (status === 409) {
|
|
12109
12558
|
return new CliError("conflict", message, {
|
|
12110
|
-
hint: "Refetch with mdocs remote context --json and retry.",
|
|
12559
|
+
hint: "Refetch with mdocs remote context --summary --json and retry.",
|
|
12111
12560
|
details: value
|
|
12112
12561
|
});
|
|
12113
12562
|
}
|
|
@@ -12248,9 +12697,10 @@ async function runJoinCommand(root, parsed) {
|
|
|
12248
12697
|
const parsedShare = parseShareUrl(target, parsed.flags);
|
|
12249
12698
|
const agentName = resolveAgentName(parsed.flags);
|
|
12250
12699
|
const agentId = normalizeAgentId(String(parsed.flags["agent-id"] ?? parsed.flags.actor ?? agentName));
|
|
12251
|
-
const
|
|
12700
|
+
const rootScopedShare = rootScopedRecord(parsedShare);
|
|
12701
|
+
const docId = parsedShare.docId ?? (rootScopedShare ? await firstDocIdForProject(rootScopedShare) : void 0);
|
|
12252
12702
|
const shareUrl = shareUrlForScope(target, { ...parsedShare, docId });
|
|
12253
|
-
const state = await postPresenceAndReadState(
|
|
12703
|
+
const state = rootScopedShare && docId ? await postPresenceAndReadState(rootScopedShare, shareUrl, docId, agentId, agentName) : void 0;
|
|
12254
12704
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
12255
12705
|
const record = {
|
|
12256
12706
|
joinId: joinIdFor(target),
|
|
@@ -12265,10 +12715,10 @@ async function runJoinCommand(root, parsed) {
|
|
|
12265
12715
|
joinedAt: now,
|
|
12266
12716
|
updatedAt: now,
|
|
12267
12717
|
lastSeenEventId: 0,
|
|
12268
|
-
currentHead: state
|
|
12718
|
+
currentHead: state?.document.currentSha
|
|
12269
12719
|
};
|
|
12270
12720
|
await writeJoinRecord(root, record);
|
|
12271
|
-
return joinSummary(record, state
|
|
12721
|
+
return joinSummary(record, state?.document);
|
|
12272
12722
|
}
|
|
12273
12723
|
async function runJoinsCommand(root) {
|
|
12274
12724
|
return listJoinRecords(root);
|
|
@@ -12277,18 +12727,23 @@ async function runRemoteCommand(root, subcommand, parsed) {
|
|
|
12277
12727
|
const record = await normalizedSelectedJoin(root, parsed.flags);
|
|
12278
12728
|
switch (subcommand) {
|
|
12279
12729
|
case "map":
|
|
12280
|
-
case void 0:
|
|
12730
|
+
case void 0: {
|
|
12281
12731
|
if (record.scope === "file") {
|
|
12282
12732
|
throw new CliError("usage_error", "This join is file-scoped, so the project tree is not accessible.", {
|
|
12283
|
-
hint: "Use `mdocs remote context --json` to
|
|
12733
|
+
hint: "Use `mdocs remote context --summary --json` to inspect the joined document."
|
|
12284
12734
|
});
|
|
12285
12735
|
}
|
|
12286
|
-
|
|
12287
|
-
|
|
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
|
+
}
|
|
12288
12741
|
case "graph":
|
|
12289
12742
|
return remoteGraph(record);
|
|
12290
12743
|
case "context":
|
|
12291
12744
|
return remoteContext(record, parsed.command[2], parsed.flags);
|
|
12745
|
+
case "review":
|
|
12746
|
+
return remoteReview(record, parsed.command[2]);
|
|
12292
12747
|
case "create-file":
|
|
12293
12748
|
return remoteCreateFile(root, record, parsed);
|
|
12294
12749
|
case "move-file":
|
|
@@ -12305,10 +12760,13 @@ async function runRemoteCommand(root, subcommand, parsed) {
|
|
|
12305
12760
|
return remoteHistory(record, parsed.flags);
|
|
12306
12761
|
case "restore":
|
|
12307
12762
|
return remoteRestore(record, parsed);
|
|
12308
|
-
case "library":
|
|
12763
|
+
case "library": {
|
|
12764
|
+
if (record.scope === "workspace") return fetchWorkspaceLibrary(record);
|
|
12309
12765
|
assertProjectScope(record, "library");
|
|
12310
|
-
|
|
12311
|
-
|
|
12766
|
+
const rootRecord = assertRootScoped(record, "library");
|
|
12767
|
+
await refreshPresence(rootRecord, record.docId);
|
|
12768
|
+
return fetchLibrary(rootRecord);
|
|
12769
|
+
}
|
|
12312
12770
|
case "create-folder":
|
|
12313
12771
|
return remoteCreateFolder(record, parsed);
|
|
12314
12772
|
case "update-folder":
|
|
@@ -12321,15 +12779,16 @@ async function runRemoteCommand(root, subcommand, parsed) {
|
|
|
12321
12779
|
return rejoin(root, record.joinId, parsed.flags);
|
|
12322
12780
|
default:
|
|
12323
12781
|
throw new CliError("usage_error", `Unknown remote subcommand: ${subcommand}`, {
|
|
12324
|
-
hint: "Use mdocs remote map|graph|context|create-file|move-file|comment|suggest|reject|events|history|restore|library|create-folder|update-folder|move-root|invite-folder|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."
|
|
12325
12783
|
});
|
|
12326
12784
|
}
|
|
12327
12785
|
}
|
|
12328
12786
|
async function remoteGraph(record) {
|
|
12329
12787
|
assertProjectScope(record, "graph");
|
|
12330
|
-
|
|
12331
|
-
|
|
12332
|
-
const
|
|
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)));
|
|
12333
12792
|
const lookup = documents.map((doc) => ({ docId: doc.docId, path: doc.path, title: doc.title }));
|
|
12334
12793
|
const graphDocuments = documents.map((doc) => {
|
|
12335
12794
|
const links = doc.links ?? resolveMarkdownLinks(extractMarkdownLinks(doc.markdown), doc, lookup);
|
|
@@ -12347,10 +12806,37 @@ async function remoteGraph(record) {
|
|
|
12347
12806
|
});
|
|
12348
12807
|
return buildWorkspaceGraph(record.workspaceId, SIDECAR_SCHEMA_VERSION, graphDocuments);
|
|
12349
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
|
+
}
|
|
12350
12835
|
async function remoteHistory(record, flags) {
|
|
12351
|
-
|
|
12836
|
+
const rootRecord = assertRootScoped(record, "history");
|
|
12837
|
+
await refreshPresence(rootRecord, record.docId);
|
|
12352
12838
|
const limit = typeof flags.limit === "string" ? Number(flags.limit) || 50 : 50;
|
|
12353
|
-
return fetchCommits(
|
|
12839
|
+
return fetchCommits(rootRecord, limit);
|
|
12354
12840
|
}
|
|
12355
12841
|
async function remoteRestore(record, parsed) {
|
|
12356
12842
|
const headId = parsed.command[2];
|
|
@@ -12359,9 +12845,10 @@ async function remoteRestore(record, parsed) {
|
|
|
12359
12845
|
hint: "List restorable commits with mdocs remote history --json, then run mdocs remote restore <headId>."
|
|
12360
12846
|
});
|
|
12361
12847
|
}
|
|
12362
|
-
await refreshPresence(record, record.docId);
|
|
12363
12848
|
const restoreAll = parsed.flags.all === true || parsed.flags.all === "true";
|
|
12364
12849
|
let docId;
|
|
12850
|
+
const rootRecord = assertRootScoped(record, "restore");
|
|
12851
|
+
await refreshPresence(rootRecord, record.docId);
|
|
12365
12852
|
if (!restoreAll) {
|
|
12366
12853
|
const target = typeof parsed.flags.doc === "string" ? parsed.flags.doc : record.docId;
|
|
12367
12854
|
if (!target) {
|
|
@@ -12371,22 +12858,23 @@ async function remoteRestore(record, parsed) {
|
|
|
12371
12858
|
}
|
|
12372
12859
|
docId = (await fetchDocument(record, target)).docId;
|
|
12373
12860
|
}
|
|
12374
|
-
return postRestore(
|
|
12861
|
+
return postRestore(rootRecord, headId, docId);
|
|
12375
12862
|
}
|
|
12376
12863
|
async function rejoin(root, joinId, flags) {
|
|
12377
12864
|
const record = await readSelectedJoin(root, { ...flags, ...joinId ? { join: joinId } : {} });
|
|
12378
|
-
const
|
|
12865
|
+
const rootRecord = rootScopedRecord(record);
|
|
12866
|
+
const docId = record.docId ?? (rootRecord ? await firstDocIdForProject(rootRecord) : void 0);
|
|
12379
12867
|
const shareUrl = shareUrlForScope(record.shareUrl, { ...record, docId });
|
|
12380
|
-
const state = await postPresenceAndReadState(
|
|
12868
|
+
const state = rootRecord && docId ? await postPresenceAndReadState(rootRecord, shareUrl, docId, record.agentId, record.agentName) : void 0;
|
|
12381
12869
|
const next = {
|
|
12382
12870
|
...record,
|
|
12383
12871
|
shareUrl,
|
|
12384
12872
|
docId,
|
|
12385
12873
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12386
|
-
currentHead: state
|
|
12874
|
+
currentHead: state?.document.currentSha
|
|
12387
12875
|
};
|
|
12388
12876
|
await writeJoinRecord(root, next);
|
|
12389
|
-
return joinSummary(next, state
|
|
12877
|
+
return joinSummary(next, state?.document);
|
|
12390
12878
|
}
|
|
12391
12879
|
async function normalizedSelectedJoin(root, flags) {
|
|
12392
12880
|
const record = await readSelectedJoin(root, flags);
|
|
@@ -12403,19 +12891,13 @@ async function refreshPresence(record, docId) {
|
|
|
12403
12891
|
}
|
|
12404
12892
|
async function remoteContext(record, pathOrDocId, flags = {}) {
|
|
12405
12893
|
const document = pathOrDocId ? await fetchDocument(record, pathOrDocId) : (await fetchState(record)).document;
|
|
12406
|
-
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
|
+
}
|
|
12407
12898
|
const startLine = typeof flags["start-line"] === "string" ? Number(flags["start-line"]) : void 0;
|
|
12408
12899
|
const endLine = typeof flags["end-line"] === "string" ? Number(flags["end-line"]) : void 0;
|
|
12409
|
-
const
|
|
12410
|
-
const totalLines = lines.length;
|
|
12411
|
-
const sl = startLine ?? 1;
|
|
12412
|
-
const el = endLine ?? totalLines;
|
|
12413
|
-
if (sl < 1 || el > totalLines || sl > el) {
|
|
12414
|
-
throw new CliError("invalid_range", `Line range ${sl}:${el} is out of bounds for a ${totalLines}-line document.`, {
|
|
12415
|
-
hint: `Use --start-line and --end-line with 1-based line numbers within 1:${totalLines}.`
|
|
12416
|
-
});
|
|
12417
|
-
}
|
|
12418
|
-
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);
|
|
12419
12901
|
return {
|
|
12420
12902
|
joinId: record.joinId,
|
|
12421
12903
|
scope: record.scope,
|
|
@@ -12427,22 +12909,51 @@ async function remoteContext(record, pathOrDocId, flags = {}) {
|
|
|
12427
12909
|
startLine: sl,
|
|
12428
12910
|
endLine: el,
|
|
12429
12911
|
markdown,
|
|
12430
|
-
|
|
12431
|
-
|
|
12432
|
-
|
|
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
|
+
},
|
|
12433
12917
|
images: document.images,
|
|
12434
12918
|
links: document.links
|
|
12435
12919
|
};
|
|
12436
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
|
+
}
|
|
12437
12947
|
async function remoteCreateFile(root, record, parsed) {
|
|
12438
12948
|
assertProjectScope(record, "create-file");
|
|
12949
|
+
const rootRecord = assertRootScoped(record, "create-file");
|
|
12439
12950
|
const path = parsed.command[2];
|
|
12440
12951
|
if (!path) {
|
|
12441
12952
|
throw new CliError("usage_error", "Missing file path.", {
|
|
12442
12953
|
hint: "Run mdocs remote create-file <path> --with-file /tmp/initial.md --json."
|
|
12443
12954
|
});
|
|
12444
12955
|
}
|
|
12445
|
-
const tree = await fetchTree(
|
|
12956
|
+
const tree = await fetchTree(rootRecord);
|
|
12446
12957
|
const markdownInput = await readOptionalTextFlag(parsed.flags, root, ["with", "markdown", "body"]);
|
|
12447
12958
|
const markdown = markdownInput ?? defaultMarkdown(path);
|
|
12448
12959
|
const normalizedPath = normalizeRemoteWorkspacePath(path, tree.root.pathRules, Buffer.byteLength(markdown, "utf8"));
|
|
@@ -12455,8 +12966,8 @@ async function remoteCreateFile(root, record, parsed) {
|
|
|
12455
12966
|
const title = titleFromMarkdown(normalizedPath, markdown);
|
|
12456
12967
|
const sidecar = emptySidecar(docId, normalizedPath, title);
|
|
12457
12968
|
const baseDocument = {
|
|
12458
|
-
workspaceId:
|
|
12459
|
-
rootId:
|
|
12969
|
+
workspaceId: rootRecord.workspaceId,
|
|
12970
|
+
rootId: rootRecord.rootId,
|
|
12460
12971
|
docId,
|
|
12461
12972
|
path: normalizedPath,
|
|
12462
12973
|
title,
|
|
@@ -12469,12 +12980,13 @@ async function remoteCreateFile(root, record, parsed) {
|
|
|
12469
12980
|
openSuggestions: 0,
|
|
12470
12981
|
currentSha: tree.root.canonical.head
|
|
12471
12982
|
};
|
|
12472
|
-
const document = await pushDocument(
|
|
12983
|
+
const document = await pushDocument(rootRecord, baseDocument, markdown, sidecar);
|
|
12473
12984
|
await recordHead(root, record, document);
|
|
12474
12985
|
return { document };
|
|
12475
12986
|
}
|
|
12476
12987
|
async function remoteMoveFile(root, record, parsed) {
|
|
12477
12988
|
assertProjectScope(record, "move-file");
|
|
12989
|
+
const rootRecord = assertRootScoped(record, "move-file");
|
|
12478
12990
|
const pathOrDocId = parsed.command[2];
|
|
12479
12991
|
if (!pathOrDocId) {
|
|
12480
12992
|
throw new CliError("usage_error", "Missing source document path or docId.", {
|
|
@@ -12483,7 +12995,7 @@ async function remoteMoveFile(root, record, parsed) {
|
|
|
12483
12995
|
}
|
|
12484
12996
|
const nextPathFlag = requiredFlag(parsed.flags, "to");
|
|
12485
12997
|
const document = await fetchDocument(record, pathOrDocId);
|
|
12486
|
-
const tree = await fetchTree(
|
|
12998
|
+
const tree = await fetchTree(rootRecord);
|
|
12487
12999
|
const nextPath = normalizeRemoteWorkspacePath(nextPathFlag, tree.root.pathRules, Buffer.byteLength(document.markdown, "utf8"));
|
|
12488
13000
|
if (tree.docs.some((doc) => doc.docId !== document.docId && doc.path === nextPath)) {
|
|
12489
13001
|
throw new CliError("validation_error", `Document already exists: ${nextPath}`, {
|
|
@@ -12491,7 +13003,7 @@ async function remoteMoveFile(root, record, parsed) {
|
|
|
12491
13003
|
});
|
|
12492
13004
|
}
|
|
12493
13005
|
const moved = await pushDocument(
|
|
12494
|
-
record,
|
|
13006
|
+
rootScopedRecordFor(record, document.rootId),
|
|
12495
13007
|
{ ...document, path: nextPath, title: titleFromMarkdown(nextPath, document.markdown) },
|
|
12496
13008
|
document.markdown,
|
|
12497
13009
|
document.sidecar
|
|
@@ -12524,13 +13036,15 @@ async function remoteSuggest(root, record, parsed) {
|
|
|
12524
13036
|
}
|
|
12525
13037
|
async function remoteCreateFolder(record, parsed) {
|
|
12526
13038
|
assertProjectScope(record, "create-folder");
|
|
13039
|
+
const rootRecord = assertRootScoped(record, "create-folder");
|
|
12527
13040
|
const name = folderNameFromArgs(parsed);
|
|
12528
13041
|
const parentId = optionalFolderTarget(parsed.flags.parent);
|
|
12529
|
-
await refreshPresence(
|
|
12530
|
-
return postLibraryFolder(
|
|
13042
|
+
await refreshPresence(rootRecord, record.docId);
|
|
13043
|
+
return postLibraryFolder(rootRecord, { name, ...parentId ? { parentId } : {} }, actorForRecord(record));
|
|
12531
13044
|
}
|
|
12532
13045
|
async function remoteUpdateFolder(record, parsed) {
|
|
12533
13046
|
assertProjectScope(record, "update-folder");
|
|
13047
|
+
const rootRecord = assertRootScoped(record, "update-folder");
|
|
12534
13048
|
const folderId = parsed.command[2];
|
|
12535
13049
|
if (!folderId) {
|
|
12536
13050
|
throw new CliError("usage_error", "Missing folder id.", {
|
|
@@ -12546,22 +13060,24 @@ async function remoteUpdateFolder(record, parsed) {
|
|
|
12546
13060
|
hint: "Pass --name <name>, --parent <folderId>, or --home."
|
|
12547
13061
|
});
|
|
12548
13062
|
}
|
|
12549
|
-
await refreshPresence(
|
|
12550
|
-
return patchLibraryFolder(
|
|
13063
|
+
await refreshPresence(rootRecord, record.docId);
|
|
13064
|
+
return patchLibraryFolder(rootRecord, folderId, input, actorForRecord(record));
|
|
12551
13065
|
}
|
|
12552
13066
|
async function remoteMoveRoot(record, parsed) {
|
|
12553
13067
|
assertProjectScope(record, "move-root");
|
|
13068
|
+
const rootRecord = assertRootScoped(record, "move-root");
|
|
12554
13069
|
const folderId = parsed.flags.home === true || parsed.flags.home === "true" ? null : optionalFolderTarget(parsed.flags.folder);
|
|
12555
13070
|
if (folderId === void 0) {
|
|
12556
13071
|
throw new CliError("usage_error", "Missing target folder.", {
|
|
12557
13072
|
hint: "Pass --folder <folderId> to move into a folder, or --home to move to the top level."
|
|
12558
13073
|
});
|
|
12559
13074
|
}
|
|
12560
|
-
await refreshPresence(
|
|
12561
|
-
return postMoveRoot(
|
|
13075
|
+
await refreshPresence(rootRecord, record.docId);
|
|
13076
|
+
return postMoveRoot(rootRecord, folderId, actorForRecord(record));
|
|
12562
13077
|
}
|
|
12563
13078
|
async function remoteInviteFolder(record, parsed) {
|
|
12564
13079
|
assertProjectScope(record, "invite-folder");
|
|
13080
|
+
const rootRecord = assertRootScoped(record, "invite-folder");
|
|
12565
13081
|
const folderId = parsed.command[2];
|
|
12566
13082
|
if (!folderId) {
|
|
12567
13083
|
throw new CliError("usage_error", "Missing folder id.", {
|
|
@@ -12575,14 +13091,14 @@ async function remoteInviteFolder(record, parsed) {
|
|
|
12575
13091
|
hint: "Use --role read, suggest, edit, or admin."
|
|
12576
13092
|
});
|
|
12577
13093
|
}
|
|
12578
|
-
await refreshPresence(
|
|
12579
|
-
return postFolderInvite(
|
|
13094
|
+
await refreshPresence(rootRecord, record.docId);
|
|
13095
|
+
return postFolderInvite(rootRecord, folderId, { email, role }, actorForRecord(record));
|
|
12580
13096
|
}
|
|
12581
13097
|
async function remoteReject(root, record, parsed) {
|
|
12582
13098
|
const suggestionId = parsed.command[2];
|
|
12583
13099
|
if (!suggestionId) {
|
|
12584
13100
|
throw new CliError("usage_error", "Missing suggestion id.", {
|
|
12585
|
-
hint: "List open suggestions with mdocs remote
|
|
13101
|
+
hint: "List open suggestions with mdocs remote review --json, then run mdocs remote reject <suggestionId>."
|
|
12586
13102
|
});
|
|
12587
13103
|
}
|
|
12588
13104
|
const pathOrDocId = parsed.command[3] ?? record.docId;
|
|
@@ -12592,12 +13108,13 @@ async function remoteReject(root, record, parsed) {
|
|
|
12592
13108
|
});
|
|
12593
13109
|
}
|
|
12594
13110
|
let document = await fetchDocument(record, pathOrDocId);
|
|
12595
|
-
|
|
13111
|
+
let documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
13112
|
+
await refreshPresence(documentRecord, document.docId);
|
|
12596
13113
|
const actor = actorForRecord(record);
|
|
12597
13114
|
const suggestion = document.sidecar.suggestions.find((candidate) => candidate.id === suggestionId);
|
|
12598
13115
|
if (!suggestion) {
|
|
12599
13116
|
throw new CliError("not_found", `Unknown suggestion: ${suggestionId}`, {
|
|
12600
|
-
hint: "List suggestion ids with mdocs remote
|
|
13117
|
+
hint: "List suggestion ids with mdocs remote review --json."
|
|
12601
13118
|
});
|
|
12602
13119
|
}
|
|
12603
13120
|
if (suggestion.status !== "open") {
|
|
@@ -12607,7 +13124,7 @@ async function remoteReject(root, record, parsed) {
|
|
|
12607
13124
|
}
|
|
12608
13125
|
try {
|
|
12609
13126
|
const additions = { anchors: [], comments: [], suggestions: [], withdrawSuggestionIds: [suggestionId] };
|
|
12610
|
-
const pushed = await postReview(
|
|
13127
|
+
const pushed = await postReview(documentRecord, document.docId, additions, actor);
|
|
12611
13128
|
await recordHead(root, record, pushed);
|
|
12612
13129
|
const updated = pushed.sidecar.suggestions.find((candidate) => candidate.id === suggestionId);
|
|
12613
13130
|
return { suggestion: updated ?? suggestion, document: pushed };
|
|
@@ -12621,11 +13138,12 @@ async function remoteReject(root, record, parsed) {
|
|
|
12621
13138
|
}
|
|
12622
13139
|
for (let index = 0; index < PUSH_REBASE_ATTEMPTS; index += 1) {
|
|
12623
13140
|
if (index > 0) document = await fetchDocument(record, document.docId);
|
|
13141
|
+
documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
12624
13142
|
const io = new RemoteDocumentIO(document);
|
|
12625
13143
|
const rejected = await rejectSuggestion(io, suggestionId, actor);
|
|
12626
13144
|
const state = await getDocumentState(io, document.docId);
|
|
12627
13145
|
try {
|
|
12628
|
-
const pushed = await pushDocument(
|
|
13146
|
+
const pushed = await pushDocument(documentRecord, document, state.markdown, state.sidecar);
|
|
12629
13147
|
await recordHead(root, record, pushed);
|
|
12630
13148
|
return { suggestion: rejected, document: pushed };
|
|
12631
13149
|
} catch (error) {
|
|
@@ -12634,7 +13152,7 @@ async function remoteReject(root, record, parsed) {
|
|
|
12634
13152
|
}
|
|
12635
13153
|
}
|
|
12636
13154
|
throw new CliError("conflict", "Remote change was not accepted after retries.", {
|
|
12637
|
-
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."
|
|
12638
13156
|
});
|
|
12639
13157
|
}
|
|
12640
13158
|
function isWithdrawalUnsupported(error) {
|
|
@@ -12650,7 +13168,8 @@ async function submitReview(root, record, pathOrDocId, mutate) {
|
|
|
12650
13168
|
});
|
|
12651
13169
|
}
|
|
12652
13170
|
let document = await fetchDocument(record, pathOrDocId);
|
|
12653
|
-
|
|
13171
|
+
let documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
13172
|
+
await refreshPresence(documentRecord, document.docId);
|
|
12654
13173
|
const build = async (base) => {
|
|
12655
13174
|
const io = new RemoteDocumentIO(base);
|
|
12656
13175
|
const created = await mutate(io, base.docId);
|
|
@@ -12667,7 +13186,7 @@ async function submitReview(root, record, pathOrDocId, mutate) {
|
|
|
12667
13186
|
};
|
|
12668
13187
|
let attempt = await build(document);
|
|
12669
13188
|
try {
|
|
12670
|
-
const pushed = await postReview(
|
|
13189
|
+
const pushed = await postReview(documentRecord, document.docId, attempt.additions, actorForRecord(record));
|
|
12671
13190
|
await recordHead(root, record, pushed);
|
|
12672
13191
|
return { created: attempt.created, document: pushed };
|
|
12673
13192
|
} catch (error) {
|
|
@@ -12676,10 +13195,11 @@ async function submitReview(root, record, pathOrDocId, mutate) {
|
|
|
12676
13195
|
for (let index = 0; index < PUSH_REBASE_ATTEMPTS; index += 1) {
|
|
12677
13196
|
if (index > 0) {
|
|
12678
13197
|
document = await fetchDocument(record, document.docId);
|
|
13198
|
+
documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
12679
13199
|
attempt = await build(document);
|
|
12680
13200
|
}
|
|
12681
13201
|
try {
|
|
12682
|
-
const pushed = await pushDocument(
|
|
13202
|
+
const pushed = await pushDocument(documentRecord, document, attempt.state.markdown, attempt.state.sidecar);
|
|
12683
13203
|
await recordHead(root, record, pushed);
|
|
12684
13204
|
return { created: attempt.created, document: pushed };
|
|
12685
13205
|
} catch (error) {
|
|
@@ -12688,7 +13208,7 @@ async function submitReview(root, record, pathOrDocId, mutate) {
|
|
|
12688
13208
|
}
|
|
12689
13209
|
}
|
|
12690
13210
|
throw new CliError("conflict", "Remote change was not accepted after retries.", {
|
|
12691
|
-
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."
|
|
12692
13212
|
});
|
|
12693
13213
|
}
|
|
12694
13214
|
function isReviewEndpointUnsupported(error) {
|
|
@@ -12703,8 +13223,9 @@ async function recordHead(root, record, document) {
|
|
|
12703
13223
|
}
|
|
12704
13224
|
async function remoteEvents(root, record, flags) {
|
|
12705
13225
|
const after = typeof flags.after === "string" ? Number(flags.after) : record.lastSeenEventId;
|
|
12706
|
-
|
|
12707
|
-
|
|
13226
|
+
const rootRecord = assertRootScoped(record, "events");
|
|
13227
|
+
await refreshPresence(rootRecord, record.docId);
|
|
13228
|
+
const response = await fetchPendingEvents(rootRecord, after);
|
|
12708
13229
|
await writeJoinRecord(root, {
|
|
12709
13230
|
...record,
|
|
12710
13231
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -12713,7 +13234,7 @@ async function remoteEvents(root, record, flags) {
|
|
|
12713
13234
|
if (response.truncated) {
|
|
12714
13235
|
return {
|
|
12715
13236
|
...response,
|
|
12716
|
-
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."
|
|
12717
13238
|
};
|
|
12718
13239
|
}
|
|
12719
13240
|
return response;
|
|
@@ -12722,11 +13243,11 @@ function parseShareUrl(value, flags) {
|
|
|
12722
13243
|
const url = new URL(value);
|
|
12723
13244
|
const route = parseSharePath(url.pathname);
|
|
12724
13245
|
if (!route) {
|
|
12725
|
-
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.", {
|
|
12726
13247
|
hint: "Copy the share link from the Magic Markdown share dialog."
|
|
12727
13248
|
});
|
|
12728
13249
|
}
|
|
12729
|
-
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";
|
|
12730
13251
|
const explicitDoc = typeof flags.doc === "string" ? flags.doc : typeof flags["doc-id"] === "string" ? flags["doc-id"] : void 0;
|
|
12731
13252
|
return {
|
|
12732
13253
|
joinId: joinIdFor(value),
|
|
@@ -12734,8 +13255,8 @@ function parseShareUrl(value, flags) {
|
|
|
12734
13255
|
shareUrl: value,
|
|
12735
13256
|
origin: url.origin,
|
|
12736
13257
|
workspaceId: route.workspaceId,
|
|
12737
|
-
rootId: route.rootId,
|
|
12738
|
-
docId: explicitDoc ?? route.docId,
|
|
13258
|
+
rootId: scope === "workspace" ? void 0 : route.rootId,
|
|
13259
|
+
docId: scope === "workspace" ? void 0 : explicitDoc ?? route.docId,
|
|
12739
13260
|
agentId: "",
|
|
12740
13261
|
agentName: "",
|
|
12741
13262
|
joinedAt: "",
|
|
@@ -12745,7 +13266,11 @@ function parseShareUrl(value, flags) {
|
|
|
12745
13266
|
}
|
|
12746
13267
|
function shareUrlForScope(value, record) {
|
|
12747
13268
|
const url = new URL(value);
|
|
12748
|
-
url.pathname = buildSharePath(
|
|
13269
|
+
url.pathname = buildSharePath(
|
|
13270
|
+
record.workspaceId,
|
|
13271
|
+
record.scope === "workspace" ? void 0 : record.rootId,
|
|
13272
|
+
record.scope === "file" ? record.docId : void 0
|
|
13273
|
+
);
|
|
12749
13274
|
return url.toString();
|
|
12750
13275
|
}
|
|
12751
13276
|
function joinSummary(record, document) {
|
|
@@ -12756,16 +13281,19 @@ function joinSummary(record, document) {
|
|
|
12756
13281
|
workspaceId: record.workspaceId,
|
|
12757
13282
|
rootId: record.rootId,
|
|
12758
13283
|
docId: record.docId,
|
|
12759
|
-
currentHead: document
|
|
12760
|
-
document
|
|
12761
|
-
|
|
12762
|
-
|
|
12763
|
-
|
|
12764
|
-
|
|
13284
|
+
currentHead: document?.currentSha,
|
|
13285
|
+
...document ? {
|
|
13286
|
+
document: {
|
|
13287
|
+
docId: document.docId,
|
|
13288
|
+
path: document.path,
|
|
13289
|
+
title: document.title
|
|
13290
|
+
}
|
|
13291
|
+
} : {},
|
|
12765
13292
|
nextCommands: [
|
|
12766
|
-
"mdocs remote context --json",
|
|
13293
|
+
record.scope === "workspace" ? "mdocs remote map --json" : "mdocs remote context --summary --json",
|
|
12767
13294
|
record.scope === "project" ? "mdocs remote map --json" : void 0,
|
|
12768
|
-
"mdocs remote
|
|
13295
|
+
record.scope === "workspace" ? "mdocs remote library --json" : void 0,
|
|
13296
|
+
record.scope === "workspace" ? void 0 : "mdocs remote events --json"
|
|
12769
13297
|
].filter(Boolean)
|
|
12770
13298
|
};
|
|
12771
13299
|
}
|
|
@@ -12775,6 +13303,22 @@ function assertProjectScope(record, command) {
|
|
|
12775
13303
|
hint: "Rejoin the share with `mdocs join <share-url> --scope project --json`, then retry."
|
|
12776
13304
|
});
|
|
12777
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
|
+
}
|
|
12778
13322
|
function emptySidecar(docId, path, title) {
|
|
12779
13323
|
return {
|
|
12780
13324
|
schemaVersion: SIDECAR_SCHEMA_VERSION,
|
|
@@ -13255,18 +13799,13 @@ async function main() {
|
|
|
13255
13799
|
}
|
|
13256
13800
|
case "context": {
|
|
13257
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
|
+
}
|
|
13258
13806
|
const startLine = parsed.flags["start-line"] !== void 0 ? Number(parsed.flags["start-line"]) : void 0;
|
|
13259
13807
|
const endLine = parsed.flags["end-line"] !== void 0 ? Number(parsed.flags["end-line"]) : void 0;
|
|
13260
|
-
const
|
|
13261
|
-
const totalLines = lines.length;
|
|
13262
|
-
const sl = startLine ?? 1;
|
|
13263
|
-
const el = endLine ?? totalLines;
|
|
13264
|
-
if (sl < 1 || el > totalLines || sl > el) {
|
|
13265
|
-
throw new CliError("invalid_range", `Line range ${sl}:${el} is out of bounds for a ${totalLines}-line document.`, {
|
|
13266
|
-
hint: `Use --start-line and --end-line with 1-based line numbers within 1:${totalLines}.`
|
|
13267
|
-
});
|
|
13268
|
-
}
|
|
13269
|
-
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);
|
|
13270
13809
|
print({
|
|
13271
13810
|
docId: state.docId,
|
|
13272
13811
|
path: state.path,
|
|
@@ -13280,12 +13819,19 @@ async function main() {
|
|
|
13280
13819
|
...image2.workspacePath ? { absolutePath: resolve5(cwd, image2.workspacePath) } : {}
|
|
13281
13820
|
})),
|
|
13282
13821
|
links: state.links,
|
|
13283
|
-
|
|
13284
|
-
|
|
13285
|
-
|
|
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
|
+
}
|
|
13286
13827
|
}, parsed.flags);
|
|
13287
13828
|
return;
|
|
13288
13829
|
}
|
|
13830
|
+
case "review": {
|
|
13831
|
+
const state = await getDocumentState(io, requiredArg(parsed.command[1], "path"));
|
|
13832
|
+
print(reviewStateForDocument(state), parsed.flags);
|
|
13833
|
+
return;
|
|
13834
|
+
}
|
|
13289
13835
|
case "comments": {
|
|
13290
13836
|
const state = await getDocumentState(io, requiredArg(parsed.command[1], "path"));
|
|
13291
13837
|
print(state.sidecar.comments, parsed.flags);
|
|
@@ -13546,6 +14092,8 @@ Commands:
|
|
|
13546
14092
|
graph --json Print workspace knowledge graph nodes and edges
|
|
13547
14093
|
state <path> --json Print merged Markdown and sidecar state
|
|
13548
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
|
|
13549
14097
|
comments <path> --json List comments
|
|
13550
14098
|
comment <path> --range 3:5 --body Add a sidecar comment
|
|
13551
14099
|
suggestions <path> --json List suggestions
|
|
@@ -13563,7 +14111,7 @@ Commands:
|
|
|
13563
14111
|
checkpoint create|list|restore Manage local reversible checkpoints
|
|
13564
14112
|
join <share-url> --json Join a Magic Markdown share through the CLI
|
|
13565
14113
|
joins --json List saved Magic Markdown remote joins
|
|
13566
|
-
remote map|graph|context|create-file|move-file
|
|
14114
|
+
remote map|graph|context|review|create-file|move-file
|
|
13567
14115
|
Work with documents in the active remote join
|
|
13568
14116
|
remote comment|suggest Add remote review comments and suggestions
|
|
13569
14117
|
remote reject <suggestionId> Withdraw a suggestion you submitted remotely
|