@magic-markdown/cli 0.3.2 → 0.3.6
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 +763 -152
- 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);
|
|
978
1116
|
assertCleanMarkdown(next);
|
|
979
1117
|
return next;
|
|
980
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);
|
|
1157
|
+
assertCleanMarkdown(next);
|
|
1158
|
+
return next;
|
|
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.6";
|
|
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
|
{
|
|
@@ -10887,11 +11252,15 @@ var AGENT_COMMANDS = [
|
|
|
10887
11252
|
{
|
|
10888
11253
|
name: "bridge",
|
|
10889
11254
|
summary: "Sync a local Markdown root with a Magic workspace over WebSocket (long-running).",
|
|
10890
|
-
usage: "mdocs bridge --workspace <id> --root <path> --url <base-url>",
|
|
11255
|
+
usage: "mdocs bridge --workspace <id> --root <path> --url <base-url> --request-token",
|
|
10891
11256
|
output: "long-running",
|
|
10892
11257
|
mutates: true,
|
|
10893
|
-
examples: ["mdocs bridge --workspace workspace_abc --root . --root-id root_abc --url https://magic.example.com"],
|
|
10894
|
-
notes: [
|
|
11258
|
+
examples: ["mdocs bridge --workspace workspace_abc --root . --root-id root_abc --url https://magic.example.com --request-token"],
|
|
11259
|
+
notes: [
|
|
11260
|
+
"--request-token opens a human approval URL and avoids pasting bridge secrets into an agent session.",
|
|
11261
|
+
"--token (or MDOCS_BRIDGE_TOKEN) still works when a scoped bridge token is already available.",
|
|
11262
|
+
"--url (or MDOCS_BASE_URL) is required; the bridge never assumes a localhost server."
|
|
11263
|
+
]
|
|
10895
11264
|
},
|
|
10896
11265
|
{
|
|
10897
11266
|
name: "serve-mcp",
|
|
@@ -10947,15 +11316,18 @@ Run \`mdocs help <command>\` (or \`mdocs <command> --help\`) for usage, examples
|
|
|
10947
11316
|
|
|
10948
11317
|
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
11318
|
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.
|
|
11319
|
+
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
11320
|
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.
|
|
11321
|
+
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.
|
|
11322
|
+
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
11323
|
|
|
10955
11324
|
## Editing Rules
|
|
10956
11325
|
|
|
10957
11326
|
- Comments and suggestions are sidecar operations. They should not modify clean Markdown until a suggestion is accepted.
|
|
10958
11327
|
- \`--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.
|
|
11328
|
+
- 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.
|
|
11329
|
+
- 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.
|
|
11330
|
+
- 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
11331
|
- Prefer \`--json\` for agent calls and parse stdout as JSON.
|
|
10960
11332
|
- For multiline suggestion text, write the exact replacement to a temporary file and pass \`--with-file <file>\`.
|
|
10961
11333
|
- For long comments or messages, use \`--body-file <file>\` or \`--message-file <file>\`.
|
|
@@ -10968,14 +11340,14 @@ Run \`mdocs help <command>\` (or \`mdocs <command> --help\`) for usage, examples
|
|
|
10968
11340
|
- Errors go to stderr. With \`--json\` they are structured: \`{ "ok": false, "error": { "code", "message", "hint" }, "cliVersion" }\`. Follow the \`hint\`.
|
|
10969
11341
|
- 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
11342
|
- \`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
|
|
11343
|
+
- \`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
11344
|
- Retries are safe: remote writes carry a changeId and the server deduplicates replays.
|
|
10973
11345
|
- 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
11346
|
|
|
10975
11347
|
## Access Roles and Version History
|
|
10976
11348
|
|
|
10977
11349
|
- 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.
|
|
11350
|
+
- 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
11351
|
- 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
11352
|
|
|
10981
11353
|
## Core Commands
|
|
@@ -10990,7 +11362,7 @@ Start the local stdio MCP server with:
|
|
|
10990
11362
|
mdocs serve-mcp --cwd /path/to/workspace
|
|
10991
11363
|
\`\`\`
|
|
10992
11364
|
|
|
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.
|
|
11365
|
+
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
11366
|
`;
|
|
10995
11367
|
}
|
|
10996
11368
|
function formatCommandReference() {
|
|
@@ -11414,8 +11786,9 @@ function withMcpImageResources(io, state) {
|
|
|
11414
11786
|
}))
|
|
11415
11787
|
};
|
|
11416
11788
|
}
|
|
11417
|
-
async function contextForDocument(io, path, startLine, endLine) {
|
|
11789
|
+
async function contextForDocument(io, path, startLine, endLine, options = {}) {
|
|
11418
11790
|
const state = await getDocumentState(io, path);
|
|
11791
|
+
if (options.summary) return summarizeDocumentContext(state);
|
|
11419
11792
|
const { markdown, totalLines, startLine: sl, endLine: el } = sliceMarkdown(state.markdown, startLine, endLine);
|
|
11420
11793
|
return {
|
|
11421
11794
|
path: state.path,
|
|
@@ -11427,21 +11800,19 @@ async function contextForDocument(io, path, startLine, endLine) {
|
|
|
11427
11800
|
markdown,
|
|
11428
11801
|
images: withMcpImageResources(io, state).images,
|
|
11429
11802
|
links: state.links,
|
|
11803
|
+
...options.includeReview === false ? {} : reviewFields(state)
|
|
11804
|
+
};
|
|
11805
|
+
}
|
|
11806
|
+
async function reviewForDocument(io, path) {
|
|
11807
|
+
return reviewStateForDocument(await getDocumentState(io, path));
|
|
11808
|
+
}
|
|
11809
|
+
function reviewFields(state) {
|
|
11810
|
+
return {
|
|
11430
11811
|
comments: state.sidecar.comments.filter((comment2) => comment2.status === "open"),
|
|
11431
11812
|
suggestions: state.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
|
|
11432
11813
|
anchors: state.anchors
|
|
11433
11814
|
};
|
|
11434
11815
|
}
|
|
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
11816
|
function imageResourceUri(docId, index) {
|
|
11446
11817
|
return `mdocs://documents/${docId}/images/${index}`;
|
|
11447
11818
|
}
|
|
@@ -11519,7 +11890,7 @@ var tools = [
|
|
|
11519
11890
|
},
|
|
11520
11891
|
{
|
|
11521
11892
|
name: "mdocs_context",
|
|
11522
|
-
description: "Return agent-ready document Markdown
|
|
11893
|
+
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
11894
|
inputSchema: {
|
|
11524
11895
|
type: "object",
|
|
11525
11896
|
properties: {
|
|
@@ -11531,12 +11902,25 @@ var tools = [
|
|
|
11531
11902
|
endLine: {
|
|
11532
11903
|
type: "number",
|
|
11533
11904
|
description: "1-based last line to include (default: last line of document)."
|
|
11905
|
+
},
|
|
11906
|
+
summary: {
|
|
11907
|
+
type: "boolean",
|
|
11908
|
+
description: "Return metadata, heading outline, current head, and counts instead of Markdown content."
|
|
11909
|
+
},
|
|
11910
|
+
includeReview: {
|
|
11911
|
+
type: "boolean",
|
|
11912
|
+
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
11913
|
}
|
|
11535
11914
|
},
|
|
11536
11915
|
required: ["path"],
|
|
11537
11916
|
additionalProperties: false
|
|
11538
11917
|
}
|
|
11539
11918
|
},
|
|
11919
|
+
{
|
|
11920
|
+
name: "mdocs_review",
|
|
11921
|
+
description: "Return open comments, open suggestions, and mapped anchors for one document without returning document Markdown.",
|
|
11922
|
+
inputSchema: documentPathSchema
|
|
11923
|
+
},
|
|
11540
11924
|
{
|
|
11541
11925
|
name: "mdocs_state",
|
|
11542
11926
|
description: "Return full merged Markdown and sidecar state for one document.",
|
|
@@ -11560,14 +11944,14 @@ var tools = [
|
|
|
11560
11944
|
},
|
|
11561
11945
|
{
|
|
11562
11946
|
name: "mdocs_suggest",
|
|
11563
|
-
description: "Create a sidecar replacement suggestion without modifying clean Markdown.",
|
|
11947
|
+
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
11948
|
inputSchema: {
|
|
11565
11949
|
type: "object",
|
|
11566
11950
|
properties: {
|
|
11567
11951
|
path: documentPathSchema.properties.path,
|
|
11568
11952
|
startLine: { type: "number", description: "1-based start line." },
|
|
11569
11953
|
endLine: { type: "number", description: "1-based end line." },
|
|
11570
|
-
replacement: { type: "string", description: "Exact replacement Markdown for the line range." },
|
|
11954
|
+
replacement: { type: "string", description: "Exact replacement Markdown for the line range; keep unchanged surrounding text identical for narrow edits." },
|
|
11571
11955
|
message: { type: "string", description: "Short explanation for the suggestion." },
|
|
11572
11956
|
...actorProperties
|
|
11573
11957
|
},
|
|
@@ -11789,8 +12173,12 @@ async function callTool(io, name, args) {
|
|
|
11789
12173
|
if (name === "mdocs_context") {
|
|
11790
12174
|
const startLine = typeof args.startLine === "number" ? args.startLine : void 0;
|
|
11791
12175
|
const endLine = typeof args.endLine === "number" ? args.endLine : void 0;
|
|
11792
|
-
return contextForDocument(io, requiredString(args, "path"), startLine, endLine
|
|
12176
|
+
return contextForDocument(io, requiredString(args, "path"), startLine, endLine, {
|
|
12177
|
+
summary: args.summary === true,
|
|
12178
|
+
includeReview: args.includeReview !== false
|
|
12179
|
+
});
|
|
11793
12180
|
}
|
|
12181
|
+
if (name === "mdocs_review") return reviewForDocument(io, requiredString(args, "path"));
|
|
11794
12182
|
if (name === "mdocs_state") return withMcpImageResources(io, await getDocumentState(io, requiredString(args, "path")));
|
|
11795
12183
|
if (name === "mdocs_comment") {
|
|
11796
12184
|
return addComment(
|
|
@@ -11897,12 +12285,22 @@ async function postPresenceAndReadState(share, shareUrl, docId, agentId, agentNa
|
|
|
11897
12285
|
});
|
|
11898
12286
|
}
|
|
11899
12287
|
async function fetchState(record) {
|
|
11900
|
-
if (!record.docId) {
|
|
12288
|
+
if (!record.rootId || !record.docId) {
|
|
11901
12289
|
throw new CliError("usage_error", "This join has no current document.", {
|
|
11902
12290
|
hint: "Pass a document path or docId, or list documents with mdocs remote map --json."
|
|
11903
12291
|
});
|
|
11904
12292
|
}
|
|
11905
|
-
return fetchJson(agentUrl(record, record.docId, "state"), {
|
|
12293
|
+
return fetchJson(agentUrl({ ...record, rootId: record.rootId }, record.docId, "state"), {
|
|
12294
|
+
headers: shareHeaders(record.shareUrl)
|
|
12295
|
+
});
|
|
12296
|
+
}
|
|
12297
|
+
async function fetchWorkspaceRoots(record) {
|
|
12298
|
+
return fetchJson(`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots`, {
|
|
12299
|
+
headers: shareHeaders(record.shareUrl)
|
|
12300
|
+
});
|
|
12301
|
+
}
|
|
12302
|
+
async function fetchWorkspaceLibrary(record) {
|
|
12303
|
+
return fetchJson(`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/library`, {
|
|
11906
12304
|
headers: shareHeaders(record.shareUrl)
|
|
11907
12305
|
});
|
|
11908
12306
|
}
|
|
@@ -11948,8 +12346,10 @@ async function postFolderInvite(record, folderId, input, actor) {
|
|
|
11948
12346
|
);
|
|
11949
12347
|
}
|
|
11950
12348
|
async function fetchDocument(record, pathOrDocId) {
|
|
12349
|
+
if (record.scope === "workspace") return fetchWorkspaceDocument(record, pathOrDocId);
|
|
12350
|
+
const rootRecord = requireRootRecord(record);
|
|
11951
12351
|
if (record.scope === "file" && record.docId) {
|
|
11952
|
-
const document = await fetchDocumentById(
|
|
12352
|
+
const document = await fetchDocumentById(rootRecord, record.docId);
|
|
11953
12353
|
if (pathOrDocId === document.docId || pathOrDocId === document.path) return document;
|
|
11954
12354
|
throw new CliError("usage_error", `This file-scoped join only covers ${document.path} (${document.docId}).`, {
|
|
11955
12355
|
hint: `Join a share link for ${pathOrDocId} to work on it.`
|
|
@@ -11957,19 +12357,19 @@ async function fetchDocument(record, pathOrDocId) {
|
|
|
11957
12357
|
}
|
|
11958
12358
|
if (pathOrDocId.startsWith("doc_") || pathOrDocId === record.docId) {
|
|
11959
12359
|
try {
|
|
11960
|
-
return await fetchDocumentById(
|
|
12360
|
+
return await fetchDocumentById(rootRecord, pathOrDocId);
|
|
11961
12361
|
} catch (error) {
|
|
11962
12362
|
if (!(error instanceof CliError) || error.code !== "not_found") throw error;
|
|
11963
12363
|
}
|
|
11964
12364
|
}
|
|
11965
|
-
const tree = await fetchTree(
|
|
12365
|
+
const tree = await fetchTree(rootRecord);
|
|
11966
12366
|
const docId = tree.docs.find((doc) => doc.docId === pathOrDocId || doc.path === pathOrDocId)?.docId;
|
|
11967
12367
|
if (!docId) {
|
|
11968
12368
|
throw new CliError("not_found", `No document matches "${pathOrDocId}" in the joined workspace.`, {
|
|
11969
12369
|
hint: "List documents and their paths/docIds with mdocs remote map --json."
|
|
11970
12370
|
});
|
|
11971
12371
|
}
|
|
11972
|
-
return fetchDocumentById(
|
|
12372
|
+
return fetchDocumentById(rootRecord, docId);
|
|
11973
12373
|
}
|
|
11974
12374
|
function fetchDocumentById(record, docId) {
|
|
11975
12375
|
return fetchJson(
|
|
@@ -11977,6 +12377,51 @@ function fetchDocumentById(record, docId) {
|
|
|
11977
12377
|
{ headers: shareHeaders(record.shareUrl) }
|
|
11978
12378
|
);
|
|
11979
12379
|
}
|
|
12380
|
+
async function fetchWorkspaceDocument(record, pathOrDocId) {
|
|
12381
|
+
if (!pathOrDocId) {
|
|
12382
|
+
throw new CliError("usage_error", "Workspace-scoped context requires a document path or docId.", {
|
|
12383
|
+
hint: "Run mdocs remote map --json, then pass a path, docId, or <rootId>:<path-or-docId>."
|
|
12384
|
+
});
|
|
12385
|
+
}
|
|
12386
|
+
const roots = await fetchWorkspaceRoots(record);
|
|
12387
|
+
const rootIds = new Set(roots.map((root) => root.rootId));
|
|
12388
|
+
const prefixed = splitWorkspaceDocumentTarget(pathOrDocId, rootIds);
|
|
12389
|
+
if (prefixed) {
|
|
12390
|
+
const rootRecord = { ...record, rootId: prefixed.rootId };
|
|
12391
|
+
const tree = await fetchTree(rootRecord);
|
|
12392
|
+
const match3 = tree.docs.find((doc) => doc.docId === prefixed.target || doc.path === prefixed.target);
|
|
12393
|
+
if (!match3) {
|
|
12394
|
+
throw new CliError("not_found", `No document matches "${prefixed.target}" in ${prefixed.rootId}.`, {
|
|
12395
|
+
hint: "Run mdocs remote map --json to verify the root id and document path."
|
|
12396
|
+
});
|
|
12397
|
+
}
|
|
12398
|
+
return fetchDocumentById(rootRecord, match3.docId);
|
|
12399
|
+
}
|
|
12400
|
+
const trees = await Promise.all(roots.map((root) => fetchTree({ ...record, rootId: root.rootId })));
|
|
12401
|
+
const matches = trees.flatMap(
|
|
12402
|
+
(tree) => tree.docs.filter((doc) => doc.docId === pathOrDocId || doc.path === pathOrDocId).map((doc) => ({ rootId: tree.root.rootId, doc }))
|
|
12403
|
+
);
|
|
12404
|
+
if (matches.length === 0) {
|
|
12405
|
+
throw new CliError("not_found", `No document matches "${pathOrDocId}" in the joined Home workspace.`, {
|
|
12406
|
+
hint: "Run mdocs remote map --json to list roots, paths, and docIds."
|
|
12407
|
+
});
|
|
12408
|
+
}
|
|
12409
|
+
if (matches.length > 1) {
|
|
12410
|
+
throw new CliError("usage_error", `Document target "${pathOrDocId}" is ambiguous across ${matches.length} roots.`, {
|
|
12411
|
+
hint: `Prefix the target with a root id, for example ${matches[0].rootId}:${pathOrDocId}.`
|
|
12412
|
+
});
|
|
12413
|
+
}
|
|
12414
|
+
const match2 = matches[0];
|
|
12415
|
+
return fetchDocumentById({ ...record, rootId: match2.rootId }, match2.doc.docId);
|
|
12416
|
+
}
|
|
12417
|
+
function splitWorkspaceDocumentTarget(value, rootIds) {
|
|
12418
|
+
const separator = value.indexOf(":");
|
|
12419
|
+
if (separator <= 0) return void 0;
|
|
12420
|
+
const rootId = value.slice(0, separator);
|
|
12421
|
+
if (!rootIds.has(rootId)) return void 0;
|
|
12422
|
+
const target = value.slice(separator + 1);
|
|
12423
|
+
return target ? { rootId, target } : void 0;
|
|
12424
|
+
}
|
|
11980
12425
|
async function postReview(record, docId, additions, actor) {
|
|
11981
12426
|
const response = await fetchJson(
|
|
11982
12427
|
`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/review`,
|
|
@@ -12059,7 +12504,7 @@ function conflictError(eventPayload) {
|
|
|
12059
12504
|
const conflict = eventPayload ?? {};
|
|
12060
12505
|
const head = typeof conflict.canonicalHead === "string" ? ` Current head: ${conflict.canonicalHead}.` : "";
|
|
12061
12506
|
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.",
|
|
12507
|
+
hint: "Refetch with mdocs remote context --summary --json, re-apply your change against the needed fresh content range, and retry.",
|
|
12063
12508
|
details: eventPayload
|
|
12064
12509
|
});
|
|
12065
12510
|
}
|
|
@@ -12070,6 +12515,14 @@ function agentUrl(record, docId, action, query) {
|
|
|
12070
12515
|
function libraryUrl(record) {
|
|
12071
12516
|
return `${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/library`;
|
|
12072
12517
|
}
|
|
12518
|
+
function requireRootRecord(record) {
|
|
12519
|
+
if (!record.rootId) {
|
|
12520
|
+
throw new CliError("usage_error", "This command needs a root-scoped join.", {
|
|
12521
|
+
hint: "For Home joins, pass a document target from `mdocs remote map --json`, or use a project share for root organization commands."
|
|
12522
|
+
});
|
|
12523
|
+
}
|
|
12524
|
+
return { origin: record.origin, workspaceId: record.workspaceId, rootId: record.rootId, shareUrl: record.shareUrl };
|
|
12525
|
+
}
|
|
12073
12526
|
function shareHeaders(shareUrl) {
|
|
12074
12527
|
return { Referer: shareUrl, Authorization: `Bearer ${shareUrl}` };
|
|
12075
12528
|
}
|
|
@@ -12107,7 +12560,7 @@ function remoteHttpError(status, value, text2) {
|
|
|
12107
12560
|
}
|
|
12108
12561
|
if (status === 409) {
|
|
12109
12562
|
return new CliError("conflict", message, {
|
|
12110
|
-
hint: "Refetch with mdocs remote context --json and retry.",
|
|
12563
|
+
hint: "Refetch with mdocs remote context --summary --json and retry.",
|
|
12111
12564
|
details: value
|
|
12112
12565
|
});
|
|
12113
12566
|
}
|
|
@@ -12248,9 +12701,10 @@ async function runJoinCommand(root, parsed) {
|
|
|
12248
12701
|
const parsedShare = parseShareUrl(target, parsed.flags);
|
|
12249
12702
|
const agentName = resolveAgentName(parsed.flags);
|
|
12250
12703
|
const agentId = normalizeAgentId(String(parsed.flags["agent-id"] ?? parsed.flags.actor ?? agentName));
|
|
12251
|
-
const
|
|
12704
|
+
const rootScopedShare = rootScopedRecord(parsedShare);
|
|
12705
|
+
const docId = parsedShare.docId ?? (rootScopedShare ? await firstDocIdForProject(rootScopedShare) : void 0);
|
|
12252
12706
|
const shareUrl = shareUrlForScope(target, { ...parsedShare, docId });
|
|
12253
|
-
const state = await postPresenceAndReadState(
|
|
12707
|
+
const state = rootScopedShare && docId ? await postPresenceAndReadState(rootScopedShare, shareUrl, docId, agentId, agentName) : void 0;
|
|
12254
12708
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
12255
12709
|
const record = {
|
|
12256
12710
|
joinId: joinIdFor(target),
|
|
@@ -12265,10 +12719,10 @@ async function runJoinCommand(root, parsed) {
|
|
|
12265
12719
|
joinedAt: now,
|
|
12266
12720
|
updatedAt: now,
|
|
12267
12721
|
lastSeenEventId: 0,
|
|
12268
|
-
currentHead: state
|
|
12722
|
+
currentHead: state?.document.currentSha
|
|
12269
12723
|
};
|
|
12270
12724
|
await writeJoinRecord(root, record);
|
|
12271
|
-
return joinSummary(record, state
|
|
12725
|
+
return joinSummary(record, state?.document);
|
|
12272
12726
|
}
|
|
12273
12727
|
async function runJoinsCommand(root) {
|
|
12274
12728
|
return listJoinRecords(root);
|
|
@@ -12277,18 +12731,23 @@ async function runRemoteCommand(root, subcommand, parsed) {
|
|
|
12277
12731
|
const record = await normalizedSelectedJoin(root, parsed.flags);
|
|
12278
12732
|
switch (subcommand) {
|
|
12279
12733
|
case "map":
|
|
12280
|
-
case void 0:
|
|
12734
|
+
case void 0: {
|
|
12281
12735
|
if (record.scope === "file") {
|
|
12282
12736
|
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
|
|
12737
|
+
hint: "Use `mdocs remote context --summary --json` to inspect the joined document."
|
|
12284
12738
|
});
|
|
12285
12739
|
}
|
|
12286
|
-
|
|
12287
|
-
|
|
12740
|
+
if (record.scope === "workspace") return remoteWorkspaceMap(record);
|
|
12741
|
+
const rootRecord = assertRootScoped(record, "map");
|
|
12742
|
+
await refreshPresence(rootRecord, record.docId);
|
|
12743
|
+
return fetchTree(rootRecord);
|
|
12744
|
+
}
|
|
12288
12745
|
case "graph":
|
|
12289
12746
|
return remoteGraph(record);
|
|
12290
12747
|
case "context":
|
|
12291
12748
|
return remoteContext(record, parsed.command[2], parsed.flags);
|
|
12749
|
+
case "review":
|
|
12750
|
+
return remoteReview(record, parsed.command[2]);
|
|
12292
12751
|
case "create-file":
|
|
12293
12752
|
return remoteCreateFile(root, record, parsed);
|
|
12294
12753
|
case "move-file":
|
|
@@ -12305,10 +12764,13 @@ async function runRemoteCommand(root, subcommand, parsed) {
|
|
|
12305
12764
|
return remoteHistory(record, parsed.flags);
|
|
12306
12765
|
case "restore":
|
|
12307
12766
|
return remoteRestore(record, parsed);
|
|
12308
|
-
case "library":
|
|
12767
|
+
case "library": {
|
|
12768
|
+
if (record.scope === "workspace") return fetchWorkspaceLibrary(record);
|
|
12309
12769
|
assertProjectScope(record, "library");
|
|
12310
|
-
|
|
12311
|
-
|
|
12770
|
+
const rootRecord = assertRootScoped(record, "library");
|
|
12771
|
+
await refreshPresence(rootRecord, record.docId);
|
|
12772
|
+
return fetchLibrary(rootRecord);
|
|
12773
|
+
}
|
|
12312
12774
|
case "create-folder":
|
|
12313
12775
|
return remoteCreateFolder(record, parsed);
|
|
12314
12776
|
case "update-folder":
|
|
@@ -12321,15 +12783,16 @@ async function runRemoteCommand(root, subcommand, parsed) {
|
|
|
12321
12783
|
return rejoin(root, record.joinId, parsed.flags);
|
|
12322
12784
|
default:
|
|
12323
12785
|
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."
|
|
12786
|
+
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
12787
|
});
|
|
12326
12788
|
}
|
|
12327
12789
|
}
|
|
12328
12790
|
async function remoteGraph(record) {
|
|
12329
12791
|
assertProjectScope(record, "graph");
|
|
12330
|
-
|
|
12331
|
-
|
|
12332
|
-
const
|
|
12792
|
+
const rootRecord = assertRootScoped(record, "graph");
|
|
12793
|
+
await refreshPresence(rootRecord, record.docId);
|
|
12794
|
+
const tree = await fetchTree(rootRecord);
|
|
12795
|
+
const documents = await Promise.all(tree.docs.map((doc) => fetchDocument(rootScopedRecordFor(record, rootRecord.rootId), doc.docId)));
|
|
12333
12796
|
const lookup = documents.map((doc) => ({ docId: doc.docId, path: doc.path, title: doc.title }));
|
|
12334
12797
|
const graphDocuments = documents.map((doc) => {
|
|
12335
12798
|
const links = doc.links ?? resolveMarkdownLinks(extractMarkdownLinks(doc.markdown), doc, lookup);
|
|
@@ -12347,10 +12810,37 @@ async function remoteGraph(record) {
|
|
|
12347
12810
|
});
|
|
12348
12811
|
return buildWorkspaceGraph(record.workspaceId, SIDECAR_SCHEMA_VERSION, graphDocuments);
|
|
12349
12812
|
}
|
|
12813
|
+
async function remoteWorkspaceMap(record) {
|
|
12814
|
+
const roots = await fetchWorkspaceRoots(record);
|
|
12815
|
+
const trees = await Promise.all(roots.map((root) => fetchTree(rootScopedRecordFor(record, root.rootId))));
|
|
12816
|
+
const library = await fetchWorkspaceLibrary(record).catch(() => void 0);
|
|
12817
|
+
return {
|
|
12818
|
+
workspaceId: record.workspaceId,
|
|
12819
|
+
scope: "workspace",
|
|
12820
|
+
roots: trees.map((tree) => ({
|
|
12821
|
+
root: tree.root,
|
|
12822
|
+
docs: tree.docs.map((doc) => ({
|
|
12823
|
+
rootId: tree.root.rootId,
|
|
12824
|
+
docId: doc.docId,
|
|
12825
|
+
path: doc.path,
|
|
12826
|
+
title: doc.title,
|
|
12827
|
+
openComments: doc.openComments,
|
|
12828
|
+
openSuggestions: doc.openSuggestions,
|
|
12829
|
+
anchorsNeedingReview: doc.anchorsNeedingReview,
|
|
12830
|
+
outgoingLinks: doc.outgoingLinks,
|
|
12831
|
+
incomingLinks: doc.incomingLinks,
|
|
12832
|
+
unresolvedLinks: doc.unresolvedLinks,
|
|
12833
|
+
externalLinks: doc.externalLinks
|
|
12834
|
+
}))
|
|
12835
|
+
})),
|
|
12836
|
+
...library ? { folders: library.folders } : {}
|
|
12837
|
+
};
|
|
12838
|
+
}
|
|
12350
12839
|
async function remoteHistory(record, flags) {
|
|
12351
|
-
|
|
12840
|
+
const rootRecord = assertRootScoped(record, "history");
|
|
12841
|
+
await refreshPresence(rootRecord, record.docId);
|
|
12352
12842
|
const limit = typeof flags.limit === "string" ? Number(flags.limit) || 50 : 50;
|
|
12353
|
-
return fetchCommits(
|
|
12843
|
+
return fetchCommits(rootRecord, limit);
|
|
12354
12844
|
}
|
|
12355
12845
|
async function remoteRestore(record, parsed) {
|
|
12356
12846
|
const headId = parsed.command[2];
|
|
@@ -12359,9 +12849,10 @@ async function remoteRestore(record, parsed) {
|
|
|
12359
12849
|
hint: "List restorable commits with mdocs remote history --json, then run mdocs remote restore <headId>."
|
|
12360
12850
|
});
|
|
12361
12851
|
}
|
|
12362
|
-
await refreshPresence(record, record.docId);
|
|
12363
12852
|
const restoreAll = parsed.flags.all === true || parsed.flags.all === "true";
|
|
12364
12853
|
let docId;
|
|
12854
|
+
const rootRecord = assertRootScoped(record, "restore");
|
|
12855
|
+
await refreshPresence(rootRecord, record.docId);
|
|
12365
12856
|
if (!restoreAll) {
|
|
12366
12857
|
const target = typeof parsed.flags.doc === "string" ? parsed.flags.doc : record.docId;
|
|
12367
12858
|
if (!target) {
|
|
@@ -12371,22 +12862,23 @@ async function remoteRestore(record, parsed) {
|
|
|
12371
12862
|
}
|
|
12372
12863
|
docId = (await fetchDocument(record, target)).docId;
|
|
12373
12864
|
}
|
|
12374
|
-
return postRestore(
|
|
12865
|
+
return postRestore(rootRecord, headId, docId);
|
|
12375
12866
|
}
|
|
12376
12867
|
async function rejoin(root, joinId, flags) {
|
|
12377
12868
|
const record = await readSelectedJoin(root, { ...flags, ...joinId ? { join: joinId } : {} });
|
|
12378
|
-
const
|
|
12869
|
+
const rootRecord = rootScopedRecord(record);
|
|
12870
|
+
const docId = record.docId ?? (rootRecord ? await firstDocIdForProject(rootRecord) : void 0);
|
|
12379
12871
|
const shareUrl = shareUrlForScope(record.shareUrl, { ...record, docId });
|
|
12380
|
-
const state = await postPresenceAndReadState(
|
|
12872
|
+
const state = rootRecord && docId ? await postPresenceAndReadState(rootRecord, shareUrl, docId, record.agentId, record.agentName) : void 0;
|
|
12381
12873
|
const next = {
|
|
12382
12874
|
...record,
|
|
12383
12875
|
shareUrl,
|
|
12384
12876
|
docId,
|
|
12385
12877
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12386
|
-
currentHead: state
|
|
12878
|
+
currentHead: state?.document.currentSha
|
|
12387
12879
|
};
|
|
12388
12880
|
await writeJoinRecord(root, next);
|
|
12389
|
-
return joinSummary(next, state
|
|
12881
|
+
return joinSummary(next, state?.document);
|
|
12390
12882
|
}
|
|
12391
12883
|
async function normalizedSelectedJoin(root, flags) {
|
|
12392
12884
|
const record = await readSelectedJoin(root, flags);
|
|
@@ -12403,19 +12895,13 @@ async function refreshPresence(record, docId) {
|
|
|
12403
12895
|
}
|
|
12404
12896
|
async function remoteContext(record, pathOrDocId, flags = {}) {
|
|
12405
12897
|
const document = pathOrDocId ? await fetchDocument(record, pathOrDocId) : (await fetchState(record)).document;
|
|
12406
|
-
await refreshPresence(record, document.docId);
|
|
12898
|
+
await refreshPresence(rootScopedRecordFor(record, document.rootId), document.docId);
|
|
12899
|
+
if (flags.summary || flags["metadata-only"]) {
|
|
12900
|
+
return withRemoteSummaryNextCommands(summarizeDocumentContext(document), record, document, pathOrDocId);
|
|
12901
|
+
}
|
|
12407
12902
|
const startLine = typeof flags["start-line"] === "string" ? Number(flags["start-line"]) : void 0;
|
|
12408
12903
|
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;
|
|
12904
|
+
const { markdown, totalLines, startLine: sl, endLine: el } = sliceMarkdown(document.markdown, startLine, endLine);
|
|
12419
12905
|
return {
|
|
12420
12906
|
joinId: record.joinId,
|
|
12421
12907
|
scope: record.scope,
|
|
@@ -12427,22 +12913,51 @@ async function remoteContext(record, pathOrDocId, flags = {}) {
|
|
|
12427
12913
|
startLine: sl,
|
|
12428
12914
|
endLine: el,
|
|
12429
12915
|
markdown,
|
|
12430
|
-
|
|
12431
|
-
|
|
12432
|
-
|
|
12916
|
+
...flags["no-review"] ? {} : {
|
|
12917
|
+
comments: document.sidecar.comments.filter((comment2) => comment2.status === "open"),
|
|
12918
|
+
suggestions: document.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
|
|
12919
|
+
anchors: document.anchors
|
|
12920
|
+
},
|
|
12433
12921
|
images: document.images,
|
|
12434
12922
|
links: document.links
|
|
12435
12923
|
};
|
|
12436
12924
|
}
|
|
12925
|
+
async function remoteReview(record, pathOrDocId) {
|
|
12926
|
+
const document = pathOrDocId ? await fetchDocument(record, pathOrDocId) : (await fetchState(record)).document;
|
|
12927
|
+
await refreshPresence(rootScopedRecordFor(record, document.rootId), document.docId);
|
|
12928
|
+
return {
|
|
12929
|
+
joinId: record.joinId,
|
|
12930
|
+
scope: record.scope,
|
|
12931
|
+
...reviewStateForDocument(document)
|
|
12932
|
+
};
|
|
12933
|
+
}
|
|
12934
|
+
function withRemoteSummaryNextCommands(summary, record, document, requestedTarget) {
|
|
12935
|
+
const target = remoteCommandTarget(record, document, requestedTarget);
|
|
12936
|
+
const firstEnd = Math.min(summary.totalLines, 120);
|
|
12937
|
+
return {
|
|
12938
|
+
...summary,
|
|
12939
|
+
nextCommands: [
|
|
12940
|
+
`mdocs remote context${target} --start-line 1 --end-line ${firstEnd} --no-review --json`,
|
|
12941
|
+
summary.totalLines > firstEnd ? `mdocs remote context${target} --start-line ${firstEnd + 1} --end-line ${Math.min(summary.totalLines, firstEnd + 120)} --no-review --json` : void 0,
|
|
12942
|
+
summary.reviewCounts.openComments || summary.reviewCounts.openSuggestions || summary.reviewCounts.anchorsNeedingReview ? `mdocs remote review${target} --json` : void 0
|
|
12943
|
+
].filter((command) => Boolean(command))
|
|
12944
|
+
};
|
|
12945
|
+
}
|
|
12946
|
+
function remoteCommandTarget(record, document, requestedTarget) {
|
|
12947
|
+
if (record.scope === "file" && (!requestedTarget || requestedTarget === document.docId || requestedTarget === document.path)) return "";
|
|
12948
|
+
if (record.scope === "workspace") return ` ${document.rootId}:${document.docId}`;
|
|
12949
|
+
return ` ${document.docId}`;
|
|
12950
|
+
}
|
|
12437
12951
|
async function remoteCreateFile(root, record, parsed) {
|
|
12438
12952
|
assertProjectScope(record, "create-file");
|
|
12953
|
+
const rootRecord = assertRootScoped(record, "create-file");
|
|
12439
12954
|
const path = parsed.command[2];
|
|
12440
12955
|
if (!path) {
|
|
12441
12956
|
throw new CliError("usage_error", "Missing file path.", {
|
|
12442
12957
|
hint: "Run mdocs remote create-file <path> --with-file /tmp/initial.md --json."
|
|
12443
12958
|
});
|
|
12444
12959
|
}
|
|
12445
|
-
const tree = await fetchTree(
|
|
12960
|
+
const tree = await fetchTree(rootRecord);
|
|
12446
12961
|
const markdownInput = await readOptionalTextFlag(parsed.flags, root, ["with", "markdown", "body"]);
|
|
12447
12962
|
const markdown = markdownInput ?? defaultMarkdown(path);
|
|
12448
12963
|
const normalizedPath = normalizeRemoteWorkspacePath(path, tree.root.pathRules, Buffer.byteLength(markdown, "utf8"));
|
|
@@ -12455,8 +12970,8 @@ async function remoteCreateFile(root, record, parsed) {
|
|
|
12455
12970
|
const title = titleFromMarkdown(normalizedPath, markdown);
|
|
12456
12971
|
const sidecar = emptySidecar(docId, normalizedPath, title);
|
|
12457
12972
|
const baseDocument = {
|
|
12458
|
-
workspaceId:
|
|
12459
|
-
rootId:
|
|
12973
|
+
workspaceId: rootRecord.workspaceId,
|
|
12974
|
+
rootId: rootRecord.rootId,
|
|
12460
12975
|
docId,
|
|
12461
12976
|
path: normalizedPath,
|
|
12462
12977
|
title,
|
|
@@ -12469,12 +12984,13 @@ async function remoteCreateFile(root, record, parsed) {
|
|
|
12469
12984
|
openSuggestions: 0,
|
|
12470
12985
|
currentSha: tree.root.canonical.head
|
|
12471
12986
|
};
|
|
12472
|
-
const document = await pushDocument(
|
|
12987
|
+
const document = await pushDocument(rootRecord, baseDocument, markdown, sidecar);
|
|
12473
12988
|
await recordHead(root, record, document);
|
|
12474
12989
|
return { document };
|
|
12475
12990
|
}
|
|
12476
12991
|
async function remoteMoveFile(root, record, parsed) {
|
|
12477
12992
|
assertProjectScope(record, "move-file");
|
|
12993
|
+
const rootRecord = assertRootScoped(record, "move-file");
|
|
12478
12994
|
const pathOrDocId = parsed.command[2];
|
|
12479
12995
|
if (!pathOrDocId) {
|
|
12480
12996
|
throw new CliError("usage_error", "Missing source document path or docId.", {
|
|
@@ -12483,7 +12999,7 @@ async function remoteMoveFile(root, record, parsed) {
|
|
|
12483
12999
|
}
|
|
12484
13000
|
const nextPathFlag = requiredFlag(parsed.flags, "to");
|
|
12485
13001
|
const document = await fetchDocument(record, pathOrDocId);
|
|
12486
|
-
const tree = await fetchTree(
|
|
13002
|
+
const tree = await fetchTree(rootRecord);
|
|
12487
13003
|
const nextPath = normalizeRemoteWorkspacePath(nextPathFlag, tree.root.pathRules, Buffer.byteLength(document.markdown, "utf8"));
|
|
12488
13004
|
if (tree.docs.some((doc) => doc.docId !== document.docId && doc.path === nextPath)) {
|
|
12489
13005
|
throw new CliError("validation_error", `Document already exists: ${nextPath}`, {
|
|
@@ -12491,7 +13007,7 @@ async function remoteMoveFile(root, record, parsed) {
|
|
|
12491
13007
|
});
|
|
12492
13008
|
}
|
|
12493
13009
|
const moved = await pushDocument(
|
|
12494
|
-
record,
|
|
13010
|
+
rootScopedRecordFor(record, document.rootId),
|
|
12495
13011
|
{ ...document, path: nextPath, title: titleFromMarkdown(nextPath, document.markdown) },
|
|
12496
13012
|
document.markdown,
|
|
12497
13013
|
document.sidecar
|
|
@@ -12524,13 +13040,15 @@ async function remoteSuggest(root, record, parsed) {
|
|
|
12524
13040
|
}
|
|
12525
13041
|
async function remoteCreateFolder(record, parsed) {
|
|
12526
13042
|
assertProjectScope(record, "create-folder");
|
|
13043
|
+
const rootRecord = assertRootScoped(record, "create-folder");
|
|
12527
13044
|
const name = folderNameFromArgs(parsed);
|
|
12528
13045
|
const parentId = optionalFolderTarget(parsed.flags.parent);
|
|
12529
|
-
await refreshPresence(
|
|
12530
|
-
return postLibraryFolder(
|
|
13046
|
+
await refreshPresence(rootRecord, record.docId);
|
|
13047
|
+
return postLibraryFolder(rootRecord, { name, ...parentId ? { parentId } : {} }, actorForRecord(record));
|
|
12531
13048
|
}
|
|
12532
13049
|
async function remoteUpdateFolder(record, parsed) {
|
|
12533
13050
|
assertProjectScope(record, "update-folder");
|
|
13051
|
+
const rootRecord = assertRootScoped(record, "update-folder");
|
|
12534
13052
|
const folderId = parsed.command[2];
|
|
12535
13053
|
if (!folderId) {
|
|
12536
13054
|
throw new CliError("usage_error", "Missing folder id.", {
|
|
@@ -12546,22 +13064,24 @@ async function remoteUpdateFolder(record, parsed) {
|
|
|
12546
13064
|
hint: "Pass --name <name>, --parent <folderId>, or --home."
|
|
12547
13065
|
});
|
|
12548
13066
|
}
|
|
12549
|
-
await refreshPresence(
|
|
12550
|
-
return patchLibraryFolder(
|
|
13067
|
+
await refreshPresence(rootRecord, record.docId);
|
|
13068
|
+
return patchLibraryFolder(rootRecord, folderId, input, actorForRecord(record));
|
|
12551
13069
|
}
|
|
12552
13070
|
async function remoteMoveRoot(record, parsed) {
|
|
12553
13071
|
assertProjectScope(record, "move-root");
|
|
13072
|
+
const rootRecord = assertRootScoped(record, "move-root");
|
|
12554
13073
|
const folderId = parsed.flags.home === true || parsed.flags.home === "true" ? null : optionalFolderTarget(parsed.flags.folder);
|
|
12555
13074
|
if (folderId === void 0) {
|
|
12556
13075
|
throw new CliError("usage_error", "Missing target folder.", {
|
|
12557
13076
|
hint: "Pass --folder <folderId> to move into a folder, or --home to move to the top level."
|
|
12558
13077
|
});
|
|
12559
13078
|
}
|
|
12560
|
-
await refreshPresence(
|
|
12561
|
-
return postMoveRoot(
|
|
13079
|
+
await refreshPresence(rootRecord, record.docId);
|
|
13080
|
+
return postMoveRoot(rootRecord, folderId, actorForRecord(record));
|
|
12562
13081
|
}
|
|
12563
13082
|
async function remoteInviteFolder(record, parsed) {
|
|
12564
13083
|
assertProjectScope(record, "invite-folder");
|
|
13084
|
+
const rootRecord = assertRootScoped(record, "invite-folder");
|
|
12565
13085
|
const folderId = parsed.command[2];
|
|
12566
13086
|
if (!folderId) {
|
|
12567
13087
|
throw new CliError("usage_error", "Missing folder id.", {
|
|
@@ -12575,14 +13095,14 @@ async function remoteInviteFolder(record, parsed) {
|
|
|
12575
13095
|
hint: "Use --role read, suggest, edit, or admin."
|
|
12576
13096
|
});
|
|
12577
13097
|
}
|
|
12578
|
-
await refreshPresence(
|
|
12579
|
-
return postFolderInvite(
|
|
13098
|
+
await refreshPresence(rootRecord, record.docId);
|
|
13099
|
+
return postFolderInvite(rootRecord, folderId, { email, role }, actorForRecord(record));
|
|
12580
13100
|
}
|
|
12581
13101
|
async function remoteReject(root, record, parsed) {
|
|
12582
13102
|
const suggestionId = parsed.command[2];
|
|
12583
13103
|
if (!suggestionId) {
|
|
12584
13104
|
throw new CliError("usage_error", "Missing suggestion id.", {
|
|
12585
|
-
hint: "List open suggestions with mdocs remote
|
|
13105
|
+
hint: "List open suggestions with mdocs remote review --json, then run mdocs remote reject <suggestionId>."
|
|
12586
13106
|
});
|
|
12587
13107
|
}
|
|
12588
13108
|
const pathOrDocId = parsed.command[3] ?? record.docId;
|
|
@@ -12592,12 +13112,13 @@ async function remoteReject(root, record, parsed) {
|
|
|
12592
13112
|
});
|
|
12593
13113
|
}
|
|
12594
13114
|
let document = await fetchDocument(record, pathOrDocId);
|
|
12595
|
-
|
|
13115
|
+
let documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
13116
|
+
await refreshPresence(documentRecord, document.docId);
|
|
12596
13117
|
const actor = actorForRecord(record);
|
|
12597
13118
|
const suggestion = document.sidecar.suggestions.find((candidate) => candidate.id === suggestionId);
|
|
12598
13119
|
if (!suggestion) {
|
|
12599
13120
|
throw new CliError("not_found", `Unknown suggestion: ${suggestionId}`, {
|
|
12600
|
-
hint: "List suggestion ids with mdocs remote
|
|
13121
|
+
hint: "List suggestion ids with mdocs remote review --json."
|
|
12601
13122
|
});
|
|
12602
13123
|
}
|
|
12603
13124
|
if (suggestion.status !== "open") {
|
|
@@ -12607,7 +13128,7 @@ async function remoteReject(root, record, parsed) {
|
|
|
12607
13128
|
}
|
|
12608
13129
|
try {
|
|
12609
13130
|
const additions = { anchors: [], comments: [], suggestions: [], withdrawSuggestionIds: [suggestionId] };
|
|
12610
|
-
const pushed = await postReview(
|
|
13131
|
+
const pushed = await postReview(documentRecord, document.docId, additions, actor);
|
|
12611
13132
|
await recordHead(root, record, pushed);
|
|
12612
13133
|
const updated = pushed.sidecar.suggestions.find((candidate) => candidate.id === suggestionId);
|
|
12613
13134
|
return { suggestion: updated ?? suggestion, document: pushed };
|
|
@@ -12621,11 +13142,12 @@ async function remoteReject(root, record, parsed) {
|
|
|
12621
13142
|
}
|
|
12622
13143
|
for (let index = 0; index < PUSH_REBASE_ATTEMPTS; index += 1) {
|
|
12623
13144
|
if (index > 0) document = await fetchDocument(record, document.docId);
|
|
13145
|
+
documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
12624
13146
|
const io = new RemoteDocumentIO(document);
|
|
12625
13147
|
const rejected = await rejectSuggestion(io, suggestionId, actor);
|
|
12626
13148
|
const state = await getDocumentState(io, document.docId);
|
|
12627
13149
|
try {
|
|
12628
|
-
const pushed = await pushDocument(
|
|
13150
|
+
const pushed = await pushDocument(documentRecord, document, state.markdown, state.sidecar);
|
|
12629
13151
|
await recordHead(root, record, pushed);
|
|
12630
13152
|
return { suggestion: rejected, document: pushed };
|
|
12631
13153
|
} catch (error) {
|
|
@@ -12634,7 +13156,7 @@ async function remoteReject(root, record, parsed) {
|
|
|
12634
13156
|
}
|
|
12635
13157
|
}
|
|
12636
13158
|
throw new CliError("conflict", "Remote change was not accepted after retries.", {
|
|
12637
|
-
hint: "The document is changing rapidly. Refetch with mdocs remote context --json
|
|
13159
|
+
hint: "The document is changing rapidly. Refetch with mdocs remote context --summary --json, then retry against the needed content range."
|
|
12638
13160
|
});
|
|
12639
13161
|
}
|
|
12640
13162
|
function isWithdrawalUnsupported(error) {
|
|
@@ -12650,7 +13172,8 @@ async function submitReview(root, record, pathOrDocId, mutate) {
|
|
|
12650
13172
|
});
|
|
12651
13173
|
}
|
|
12652
13174
|
let document = await fetchDocument(record, pathOrDocId);
|
|
12653
|
-
|
|
13175
|
+
let documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
13176
|
+
await refreshPresence(documentRecord, document.docId);
|
|
12654
13177
|
const build = async (base) => {
|
|
12655
13178
|
const io = new RemoteDocumentIO(base);
|
|
12656
13179
|
const created = await mutate(io, base.docId);
|
|
@@ -12667,7 +13190,7 @@ async function submitReview(root, record, pathOrDocId, mutate) {
|
|
|
12667
13190
|
};
|
|
12668
13191
|
let attempt = await build(document);
|
|
12669
13192
|
try {
|
|
12670
|
-
const pushed = await postReview(
|
|
13193
|
+
const pushed = await postReview(documentRecord, document.docId, attempt.additions, actorForRecord(record));
|
|
12671
13194
|
await recordHead(root, record, pushed);
|
|
12672
13195
|
return { created: attempt.created, document: pushed };
|
|
12673
13196
|
} catch (error) {
|
|
@@ -12676,10 +13199,11 @@ async function submitReview(root, record, pathOrDocId, mutate) {
|
|
|
12676
13199
|
for (let index = 0; index < PUSH_REBASE_ATTEMPTS; index += 1) {
|
|
12677
13200
|
if (index > 0) {
|
|
12678
13201
|
document = await fetchDocument(record, document.docId);
|
|
13202
|
+
documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
12679
13203
|
attempt = await build(document);
|
|
12680
13204
|
}
|
|
12681
13205
|
try {
|
|
12682
|
-
const pushed = await pushDocument(
|
|
13206
|
+
const pushed = await pushDocument(documentRecord, document, attempt.state.markdown, attempt.state.sidecar);
|
|
12683
13207
|
await recordHead(root, record, pushed);
|
|
12684
13208
|
return { created: attempt.created, document: pushed };
|
|
12685
13209
|
} catch (error) {
|
|
@@ -12688,7 +13212,7 @@ async function submitReview(root, record, pathOrDocId, mutate) {
|
|
|
12688
13212
|
}
|
|
12689
13213
|
}
|
|
12690
13214
|
throw new CliError("conflict", "Remote change was not accepted after retries.", {
|
|
12691
|
-
hint: "The document is changing rapidly. Refetch with mdocs remote context --json
|
|
13215
|
+
hint: "The document is changing rapidly. Refetch with mdocs remote context --summary --json, then retry against the needed content range."
|
|
12692
13216
|
});
|
|
12693
13217
|
}
|
|
12694
13218
|
function isReviewEndpointUnsupported(error) {
|
|
@@ -12703,8 +13227,9 @@ async function recordHead(root, record, document) {
|
|
|
12703
13227
|
}
|
|
12704
13228
|
async function remoteEvents(root, record, flags) {
|
|
12705
13229
|
const after = typeof flags.after === "string" ? Number(flags.after) : record.lastSeenEventId;
|
|
12706
|
-
|
|
12707
|
-
|
|
13230
|
+
const rootRecord = assertRootScoped(record, "events");
|
|
13231
|
+
await refreshPresence(rootRecord, record.docId);
|
|
13232
|
+
const response = await fetchPendingEvents(rootRecord, after);
|
|
12708
13233
|
await writeJoinRecord(root, {
|
|
12709
13234
|
...record,
|
|
12710
13235
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -12713,7 +13238,7 @@ async function remoteEvents(root, record, flags) {
|
|
|
12713
13238
|
if (response.truncated) {
|
|
12714
13239
|
return {
|
|
12715
13240
|
...response,
|
|
12716
|
-
hint: "Older events were dropped from the bounded event log. Refetch
|
|
13241
|
+
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
13242
|
};
|
|
12718
13243
|
}
|
|
12719
13244
|
return response;
|
|
@@ -12722,11 +13247,11 @@ function parseShareUrl(value, flags) {
|
|
|
12722
13247
|
const url = new URL(value);
|
|
12723
13248
|
const route = parseSharePath(url.pathname);
|
|
12724
13249
|
if (!route) {
|
|
12725
|
-
throw new CliError("usage_error", "Share URL must be a Magic Markdown /share/<workspace
|
|
13250
|
+
throw new CliError("usage_error", "Share URL must be a Magic Markdown /share/<workspace>[/<root>[/doc]] URL.", {
|
|
12726
13251
|
hint: "Copy the share link from the Magic Markdown share dialog."
|
|
12727
13252
|
});
|
|
12728
13253
|
}
|
|
12729
|
-
const scope = flags.scope === "project" || !route.docId && flags.scope !== "file" ? "project" : "file";
|
|
13254
|
+
const scope = flags.scope === "workspace" || !route.rootId ? "workspace" : flags.scope === "project" || !route.docId && flags.scope !== "file" ? "project" : "file";
|
|
12730
13255
|
const explicitDoc = typeof flags.doc === "string" ? flags.doc : typeof flags["doc-id"] === "string" ? flags["doc-id"] : void 0;
|
|
12731
13256
|
return {
|
|
12732
13257
|
joinId: joinIdFor(value),
|
|
@@ -12734,8 +13259,8 @@ function parseShareUrl(value, flags) {
|
|
|
12734
13259
|
shareUrl: value,
|
|
12735
13260
|
origin: url.origin,
|
|
12736
13261
|
workspaceId: route.workspaceId,
|
|
12737
|
-
rootId: route.rootId,
|
|
12738
|
-
docId: explicitDoc ?? route.docId,
|
|
13262
|
+
rootId: scope === "workspace" ? void 0 : route.rootId,
|
|
13263
|
+
docId: scope === "workspace" ? void 0 : explicitDoc ?? route.docId,
|
|
12739
13264
|
agentId: "",
|
|
12740
13265
|
agentName: "",
|
|
12741
13266
|
joinedAt: "",
|
|
@@ -12745,7 +13270,11 @@ function parseShareUrl(value, flags) {
|
|
|
12745
13270
|
}
|
|
12746
13271
|
function shareUrlForScope(value, record) {
|
|
12747
13272
|
const url = new URL(value);
|
|
12748
|
-
url.pathname = buildSharePath(
|
|
13273
|
+
url.pathname = buildSharePath(
|
|
13274
|
+
record.workspaceId,
|
|
13275
|
+
record.scope === "workspace" ? void 0 : record.rootId,
|
|
13276
|
+
record.scope === "file" ? record.docId : void 0
|
|
13277
|
+
);
|
|
12749
13278
|
return url.toString();
|
|
12750
13279
|
}
|
|
12751
13280
|
function joinSummary(record, document) {
|
|
@@ -12756,16 +13285,19 @@ function joinSummary(record, document) {
|
|
|
12756
13285
|
workspaceId: record.workspaceId,
|
|
12757
13286
|
rootId: record.rootId,
|
|
12758
13287
|
docId: record.docId,
|
|
12759
|
-
currentHead: document
|
|
12760
|
-
document
|
|
12761
|
-
|
|
12762
|
-
|
|
12763
|
-
|
|
12764
|
-
|
|
13288
|
+
currentHead: document?.currentSha,
|
|
13289
|
+
...document ? {
|
|
13290
|
+
document: {
|
|
13291
|
+
docId: document.docId,
|
|
13292
|
+
path: document.path,
|
|
13293
|
+
title: document.title
|
|
13294
|
+
}
|
|
13295
|
+
} : {},
|
|
12765
13296
|
nextCommands: [
|
|
12766
|
-
"mdocs remote context --json",
|
|
13297
|
+
record.scope === "workspace" ? "mdocs remote map --json" : "mdocs remote context --summary --json",
|
|
12767
13298
|
record.scope === "project" ? "mdocs remote map --json" : void 0,
|
|
12768
|
-
"mdocs remote
|
|
13299
|
+
record.scope === "workspace" ? "mdocs remote library --json" : void 0,
|
|
13300
|
+
record.scope === "workspace" ? void 0 : "mdocs remote events --json"
|
|
12769
13301
|
].filter(Boolean)
|
|
12770
13302
|
};
|
|
12771
13303
|
}
|
|
@@ -12775,6 +13307,22 @@ function assertProjectScope(record, command) {
|
|
|
12775
13307
|
hint: "Rejoin the share with `mdocs join <share-url> --scope project --json`, then retry."
|
|
12776
13308
|
});
|
|
12777
13309
|
}
|
|
13310
|
+
function assertRootScoped(record, command) {
|
|
13311
|
+
if (record.rootId) return rootScopedRecordFor(record, record.rootId);
|
|
13312
|
+
throw new CliError("usage_error", `remote ${command} needs a specific project root.`, {
|
|
13313
|
+
hint: "For Home workspace joins, run `mdocs remote map --json` and use a document target, or join a project share for root organization commands."
|
|
13314
|
+
});
|
|
13315
|
+
}
|
|
13316
|
+
function rootScopedRecord(record) {
|
|
13317
|
+
return record.rootId ? rootScopedRecordFor(record, record.rootId) : void 0;
|
|
13318
|
+
}
|
|
13319
|
+
function rootScopedRecordFor(record, rootId) {
|
|
13320
|
+
return {
|
|
13321
|
+
...record,
|
|
13322
|
+
rootId,
|
|
13323
|
+
scope: record.scope === "file" ? "file" : "project"
|
|
13324
|
+
};
|
|
13325
|
+
}
|
|
12778
13326
|
function emptySidecar(docId, path, title) {
|
|
12779
13327
|
return {
|
|
12780
13328
|
schemaVersion: SIDECAR_SCHEMA_VERSION,
|
|
@@ -12823,6 +13371,7 @@ async function runBridge(options) {
|
|
|
12823
13371
|
const root = resolve4(options.root);
|
|
12824
13372
|
const localManifestSource = await readLocalSource(root);
|
|
12825
13373
|
const rootId = options.rootId ?? options.sourceId ?? localManifestSource?.sourceId ?? rootIdForPath(root);
|
|
13374
|
+
const token = options.token ?? (options.requestToken ? await requestBridgeToken(options, rootId) : void 0);
|
|
12826
13375
|
const replicaId = `replica_${createHash2("sha256").update(`${root}:${options.actorId}`).digest("hex").slice(0, 12)}`;
|
|
12827
13376
|
const replicaKind = actorKindForBridge(options.actorId) === "agent" ? "agent_runtime" : "local";
|
|
12828
13377
|
const mapping = createSourceMapping({
|
|
@@ -12845,7 +13394,7 @@ async function runBridge(options) {
|
|
|
12845
13394
|
const claimMode = Boolean(options.claimToken || localManifestSource?.canonicalHead);
|
|
12846
13395
|
let lastAppliedHead = localManifestSource?.canonicalHead;
|
|
12847
13396
|
let socket;
|
|
12848
|
-
const registeredRoot =
|
|
13397
|
+
const registeredRoot = token ? await fetchScopedRoot({ ...options, token }, rootId) : await registerRoot(options, root, rootId, mapping);
|
|
12849
13398
|
lastAppliedHead = lastAppliedHead ?? registeredRoot?.canonical.head;
|
|
12850
13399
|
await writeSourceState(lastAppliedHead);
|
|
12851
13400
|
connect();
|
|
@@ -12868,7 +13417,7 @@ async function runBridge(options) {
|
|
|
12868
13417
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
12869
13418
|
url.searchParams.set("actorId", options.actorId);
|
|
12870
13419
|
url.searchParams.set("sourceId", sourceId);
|
|
12871
|
-
if (
|
|
13420
|
+
if (token) url.searchParams.set("token", token);
|
|
12872
13421
|
socket = new WebSocket(url);
|
|
12873
13422
|
socket.addEventListener("open", () => {
|
|
12874
13423
|
process.stdout.write(`mdocs bridge connected ${root} -> ${options.workspaceId}/${rootId}
|
|
@@ -13030,7 +13579,7 @@ async function runBridge(options) {
|
|
|
13030
13579
|
`/api/workspaces/${encodeURIComponent(options.workspaceId)}/roots/${encodeURIComponent(rootId)}/documents/${encodeURIComponent(docId)}`,
|
|
13031
13580
|
options.baseUrl
|
|
13032
13581
|
),
|
|
13033
|
-
{ headers: authHeaders(
|
|
13582
|
+
{ headers: authHeaders(token) }
|
|
13034
13583
|
);
|
|
13035
13584
|
if (!response.ok) return void 0;
|
|
13036
13585
|
const document = await response.json();
|
|
@@ -13089,6 +13638,60 @@ async function runBridge(options) {
|
|
|
13089
13638
|
});
|
|
13090
13639
|
}
|
|
13091
13640
|
}
|
|
13641
|
+
async function requestBridgeToken(options, rootId) {
|
|
13642
|
+
const response = await fetch(new URL("/api/bridge-requests", options.baseUrl), {
|
|
13643
|
+
method: "POST",
|
|
13644
|
+
headers: { "Content-Type": "application/json" },
|
|
13645
|
+
body: JSON.stringify({
|
|
13646
|
+
workspaceId: options.workspaceId,
|
|
13647
|
+
rootId,
|
|
13648
|
+
actorId: options.actorId,
|
|
13649
|
+
actorName: options.actorName,
|
|
13650
|
+
sourceName: options.sourceName,
|
|
13651
|
+
role: "edit"
|
|
13652
|
+
})
|
|
13653
|
+
}).catch((error) => {
|
|
13654
|
+
throw new Error(`Failed to create bridge approval request: ${error instanceof Error ? error.message : String(error)}`);
|
|
13655
|
+
});
|
|
13656
|
+
if (!response.ok) {
|
|
13657
|
+
throw new Error(`Failed to create bridge approval request: ${response.status} ${await response.text()}`);
|
|
13658
|
+
}
|
|
13659
|
+
const created = await response.json();
|
|
13660
|
+
if (!created.requestId || !created.pollToken || !created.approveUrl) {
|
|
13661
|
+
throw new Error("Bridge approval request response was missing requestId, pollToken, or approveUrl.");
|
|
13662
|
+
}
|
|
13663
|
+
process.stdout.write(`mdocs bridge approval needed: ${created.approveUrl}
|
|
13664
|
+
`);
|
|
13665
|
+
if (created.expiresAt) process.stdout.write(`Waiting for approval until ${created.expiresAt}.
|
|
13666
|
+
`);
|
|
13667
|
+
const deadline = Date.now() + (options.pairingTimeoutMs ?? 1e3 * 60 * 15);
|
|
13668
|
+
while (Date.now() < deadline) {
|
|
13669
|
+
await delay(2e3);
|
|
13670
|
+
const pollResponse = await fetch(new URL(`/api/bridge-requests/${encodeURIComponent(created.requestId)}/token`, options.baseUrl), {
|
|
13671
|
+
headers: { Authorization: `Bearer ${created.pollToken}` }
|
|
13672
|
+
}).catch((error) => {
|
|
13673
|
+
throw new Error(`Bridge approval polling failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
13674
|
+
});
|
|
13675
|
+
const payload = await pollResponse.json().catch(() => ({}));
|
|
13676
|
+
if (pollResponse.status === 202) continue;
|
|
13677
|
+
if (pollResponse.ok && payload.token) {
|
|
13678
|
+
process.stdout.write(`mdocs bridge approved${payload.expiresAt ? `; token expires ${payload.expiresAt}` : ""}.
|
|
13679
|
+
`);
|
|
13680
|
+
return payload.token;
|
|
13681
|
+
}
|
|
13682
|
+
if (pollResponse.status === 409 && payload.status === "rejected") {
|
|
13683
|
+
throw new Error("Bridge approval request was rejected.");
|
|
13684
|
+
}
|
|
13685
|
+
if (pollResponse.status === 410 || payload.status === "expired") {
|
|
13686
|
+
throw new Error("Bridge approval request expired before it was approved.");
|
|
13687
|
+
}
|
|
13688
|
+
throw new Error(`Bridge approval polling failed with status ${pollResponse.status}.`);
|
|
13689
|
+
}
|
|
13690
|
+
throw new Error("Bridge approval timed out before a token was issued.");
|
|
13691
|
+
}
|
|
13692
|
+
function delay(ms) {
|
|
13693
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
13694
|
+
}
|
|
13092
13695
|
function authHeaders(token) {
|
|
13093
13696
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
13094
13697
|
}
|
|
@@ -13255,18 +13858,13 @@ async function main() {
|
|
|
13255
13858
|
}
|
|
13256
13859
|
case "context": {
|
|
13257
13860
|
const state = await getDocumentState(io, requiredArg(parsed.command[1], "path"));
|
|
13861
|
+
if (parsed.flags.summary || parsed.flags["metadata-only"]) {
|
|
13862
|
+
print(summarizeDocumentContext(state), parsed.flags);
|
|
13863
|
+
return;
|
|
13864
|
+
}
|
|
13258
13865
|
const startLine = parsed.flags["start-line"] !== void 0 ? Number(parsed.flags["start-line"]) : void 0;
|
|
13259
13866
|
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;
|
|
13867
|
+
const { markdown, totalLines, startLine: sl, endLine: el } = sliceMarkdown(state.markdown, startLine, endLine);
|
|
13270
13868
|
print({
|
|
13271
13869
|
docId: state.docId,
|
|
13272
13870
|
path: state.path,
|
|
@@ -13280,12 +13878,19 @@ async function main() {
|
|
|
13280
13878
|
...image2.workspacePath ? { absolutePath: resolve5(cwd, image2.workspacePath) } : {}
|
|
13281
13879
|
})),
|
|
13282
13880
|
links: state.links,
|
|
13283
|
-
|
|
13284
|
-
|
|
13285
|
-
|
|
13881
|
+
...parsed.flags["no-review"] ? {} : {
|
|
13882
|
+
comments: state.sidecar.comments.filter((comment2) => comment2.status === "open"),
|
|
13883
|
+
suggestions: state.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
|
|
13884
|
+
anchors: state.anchors
|
|
13885
|
+
}
|
|
13286
13886
|
}, parsed.flags);
|
|
13287
13887
|
return;
|
|
13288
13888
|
}
|
|
13889
|
+
case "review": {
|
|
13890
|
+
const state = await getDocumentState(io, requiredArg(parsed.command[1], "path"));
|
|
13891
|
+
print(reviewStateForDocument(state), parsed.flags);
|
|
13892
|
+
return;
|
|
13893
|
+
}
|
|
13289
13894
|
case "comments": {
|
|
13290
13895
|
const state = await getDocumentState(io, requiredArg(parsed.command[1], "path"));
|
|
13291
13896
|
print(state.sidecar.comments, parsed.flags);
|
|
@@ -13383,7 +13988,9 @@ async function main() {
|
|
|
13383
13988
|
actorName: typeof parsed.flags["actor-name"] === "string" ? parsed.flags["actor-name"] : void 0,
|
|
13384
13989
|
baseUrl: bridgeBaseUrl(parsed.flags),
|
|
13385
13990
|
intervalMs: Number(parsed.flags.interval ?? 1e3),
|
|
13386
|
-
token: typeof parsed.flags.token === "string" ? parsed.flags.token : process.env.MDOCS_BRIDGE_TOKEN || void 0
|
|
13991
|
+
token: typeof parsed.flags.token === "string" ? parsed.flags.token : process.env.MDOCS_BRIDGE_TOKEN || void 0,
|
|
13992
|
+
requestToken: Boolean(parsed.flags["request-token"] || parsed.flags.pair),
|
|
13993
|
+
pairingTimeoutMs: typeof parsed.flags["pairing-timeout-ms"] === "string" ? Number(parsed.flags["pairing-timeout-ms"]) : void 0
|
|
13387
13994
|
});
|
|
13388
13995
|
return;
|
|
13389
13996
|
}
|
|
@@ -13546,6 +14153,8 @@ Commands:
|
|
|
13546
14153
|
graph --json Print workspace knowledge graph nodes and edges
|
|
13547
14154
|
state <path> --json Print merged Markdown and sidecar state
|
|
13548
14155
|
context <path> --json Print agent-ready context
|
|
14156
|
+
context <path> --summary --json Print document metadata and headings
|
|
14157
|
+
review <path> --json Print open comments, suggestions, anchors
|
|
13549
14158
|
comments <path> --json List comments
|
|
13550
14159
|
comment <path> --range 3:5 --body Add a sidecar comment
|
|
13551
14160
|
suggestions <path> --json List suggestions
|
|
@@ -13563,17 +14172,19 @@ Commands:
|
|
|
13563
14172
|
checkpoint create|list|restore Manage local reversible checkpoints
|
|
13564
14173
|
join <share-url> --json Join a Magic Markdown share through the CLI
|
|
13565
14174
|
joins --json List saved Magic Markdown remote joins
|
|
13566
|
-
remote map|graph|context|create-file|move-file
|
|
14175
|
+
remote map|graph|context|review|create-file|move-file
|
|
13567
14176
|
Work with documents in the active remote join
|
|
13568
14177
|
remote comment|suggest Add remote review comments and suggestions
|
|
13569
14178
|
remote reject <suggestionId> Withdraw a suggestion you submitted remotely
|
|
13570
14179
|
remote events|history|restore Poll events, list commits, restore snapshots
|
|
13571
14180
|
remote library|create-folder|update-folder|move-root|invite-folder
|
|
13572
14181
|
Organize the joined project library
|
|
14182
|
+
bridge --workspace <id> --root . --url <base-url> --request-token
|
|
14183
|
+
Request human approval, then sync an
|
|
14184
|
+
approved local root with the workspace
|
|
13573
14185
|
bridge --workspace <id> --root . --url <base-url> --token <bridge-token>
|
|
13574
14186
|
Sync an approved local root with the workspace
|
|
13575
|
-
(
|
|
13576
|
-
dialog, or MDOCS_BRIDGE_TOKEN)
|
|
14187
|
+
(or set MDOCS_BRIDGE_TOKEN)
|
|
13577
14188
|
bridge --claim <token> --root . --canonical-prefix prompts --replica-prefix packages/agent/prompts
|
|
13578
14189
|
Claim an existing repo path as a Magic-canonical source mapping
|
|
13579
14190
|
doctor --json Validate sidecar mappings
|