@magic-markdown/cli 0.3.2 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +6 -2
  2. package/dist/index.js +691 -143
  3. 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.length === 0 ? [] : replacement.split(/\r?\n/);
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 current = extractLineRange(markdown, suggestion.patch.range);
970
- if (current !== suggestion.patch.before) {
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, suggestion.patch.range, suggestion.patch.after);
1115
+ const next = replaceLineRange(markdown, range, suggestion.patch.after);
1116
+ assertCleanMarkdown(next);
1117
+ return next;
1118
+ }
1119
+ function applySuggestionContext(input) {
1120
+ if (!input) return {};
1121
+ if ("selector" in input) return { anchor: input };
1122
+ return input;
1123
+ }
1124
+ function applySuggestionFromBase(markdown, suggestion, baseMarkdown) {
1125
+ if (!baseMarkdown || !suggestion.base?.contentHash) return { kind: "unavailable" };
1126
+ if (contentHashForText(baseMarkdown) !== suggestion.base.contentHash) return { kind: "unavailable" };
1127
+ if (!rangeIsWithin(baseMarkdown, suggestion.patch.range) || extractLineRange(baseMarkdown, suggestion.patch.range) !== suggestion.patch.before) {
1128
+ return { kind: "conflict" };
1129
+ }
1130
+ const rebased = rebaseSuggestionPatch(baseMarkdown, markdown, suggestion);
1131
+ if (rebased !== void 0) return { kind: "applied", markdown: rebased };
1132
+ const suggestedMarkdown = replaceLineRange(baseMarkdown, suggestion.patch.range, suggestion.patch.after);
1133
+ assertCleanMarkdown(suggestedMarkdown);
1134
+ const merged = mergeText(baseMarkdown, markdown, suggestedMarkdown);
1135
+ if (!merged.ok) return { kind: "conflict" };
1136
+ assertCleanMarkdown(merged.text);
1137
+ return { kind: "applied", markdown: merged.text };
1138
+ }
1139
+ function rebaseSuggestionPatch(baseMarkdown, markdown, suggestion) {
1140
+ const baseLines = getLines(baseMarkdown);
1141
+ const currentLines = getLines(markdown);
1142
+ let regions;
1143
+ try {
1144
+ regions = diffTextRegions(baseLines, currentLines);
1145
+ } catch {
1146
+ return void 0;
1147
+ }
1148
+ const baseStart = suggestion.patch.range.startLine - 1;
1149
+ const baseEnd = suggestion.patch.range.endLine;
1150
+ if (regions.some((region) => regionTouchesSuggestionRange(region, baseStart, baseEnd))) return void 0;
1151
+ const currentStart = baseStart + sideDeltaBefore(regions, baseStart, true);
1152
+ const currentEnd = baseEnd + sideDeltaBefore(regions, baseEnd, false);
1153
+ const currentRange = { startLine: currentStart + 1, endLine: currentEnd };
1154
+ if (!rangeIsWithin(markdown, currentRange)) return void 0;
1155
+ if (extractLineRange(markdown, currentRange) !== suggestion.patch.before) return void 0;
1156
+ const next = replaceLineRange(markdown, currentRange, suggestion.patch.after);
978
1157
  assertCleanMarkdown(next);
979
1158
  return next;
980
1159
  }
1160
+ function regionTouchesSuggestionRange(region, baseStart, baseEnd) {
1161
+ if (region.baseLength === 0) return region.baseStart > baseStart && region.baseStart < baseEnd;
1162
+ const regionEnd = region.baseStart + region.baseLength;
1163
+ return region.baseStart < baseEnd && regionEnd > baseStart;
1164
+ }
1165
+ function currentSuggestionRange(markdown, suggestion, anchor) {
1166
+ const hintedRange = suggestion.patch.range;
1167
+ if (rangeIsWithin(markdown, hintedRange) && extractLineRange(markdown, hintedRange) === suggestion.patch.before) return hintedRange;
1168
+ const candidates = findBeforeCandidates(markdown, suggestion.patch.before);
1169
+ if (candidates.length === 0) return void 0;
1170
+ if (candidates.length === 1) return candidates[0];
1171
+ if (!anchor) return void 0;
1172
+ const scored = candidates.map((range) => ({ range, score: scoreContext2(markdown, range, anchor) })).sort((left, right) => right.score - left.score);
1173
+ const best = scored[0];
1174
+ if (!best) return void 0;
1175
+ const tied = scored.filter((candidate) => candidate.score === best.score);
1176
+ if (tied.length > 1) return void 0;
1177
+ if (best.score < 0.7) return void 0;
1178
+ return best.range;
1179
+ }
1180
+ function findBeforeCandidates(markdown, before) {
1181
+ const lines = getLines(markdown);
1182
+ const beforeLines = linePattern(before);
1183
+ if (beforeLines.length === 0 || beforeLines.length > lines.length) return [];
1184
+ const ranges = [];
1185
+ const beforeText = beforeLines.join("\n");
1186
+ for (let index = 0; index <= lines.length - beforeLines.length; index += 1) {
1187
+ if (lines.slice(index, index + beforeLines.length).join("\n") === beforeText) {
1188
+ ranges.push({ startLine: index + 1, endLine: index + beforeLines.length });
1189
+ }
1190
+ }
1191
+ return ranges;
1192
+ }
1193
+ function rangeIsWithin(markdown, range) {
1194
+ const lineCount = getLines(markdown).length;
1195
+ return range.startLine >= 1 && range.endLine >= range.startLine && range.endLine <= lineCount;
1196
+ }
1197
+ function linePattern(value) {
1198
+ return value.replace(/\r\n?/g, "\n").split("\n");
1199
+ }
1200
+ function scoreContext2(markdown, range, anchor) {
1201
+ const lines = getLines(markdown);
1202
+ let score = anchor.selector.quote === extractLineRange(markdown, range) ? 0.7 : 0.65;
1203
+ const expectedPrefix = contextPrefix(anchor.selector.prefix);
1204
+ if (expectedPrefix) {
1205
+ const actualPrefix = contextPrefix(lines.slice(Math.max(0, range.startLine - 4), range.startLine - 1).join("\n"));
1206
+ if (actualPrefix?.endsWith(expectedPrefix)) score += 0.15;
1207
+ }
1208
+ const expectedSuffix = contextSuffix(anchor.selector.suffix);
1209
+ if (expectedSuffix) {
1210
+ const actualSuffix = contextSuffix(lines.slice(range.endLine, range.endLine + 3).join("\n"));
1211
+ if (actualSuffix?.startsWith(expectedSuffix)) score += 0.15;
1212
+ }
1213
+ return Math.min(1, score);
1214
+ }
1215
+ function contextPrefix(value) {
1216
+ return value?.split(/\r?\n/).filter(Boolean).slice(-3).join("\n") || void 0;
1217
+ }
1218
+ function contextSuffix(value) {
1219
+ return value?.split(/\r?\n/).filter(Boolean).slice(0, 3).join("\n") || void 0;
1220
+ }
981
1221
 
982
1222
  // ../core/src/graph.ts
983
1223
  function resolveMarkdownLinks(links, currentDocument, documents) {
@@ -1349,6 +1589,8 @@ async function addComment(io, pathOrDocId, range, body, author = defaultActor())
1349
1589
  }
1350
1590
  async function addSuggestion(io, pathOrDocId, range, replacement, message, author = { id: "agent_local", kind: "agent", name: "Local Agent" }, changeSetId) {
1351
1591
  const state = await getDocumentState(io, pathOrDocId);
1592
+ const manifest = await indexWorkspace(io);
1593
+ const entry = manifest.docs.find((doc) => doc.docId === state.docId);
1352
1594
  assertLineRangeWithin(state.markdown, range);
1353
1595
  assertCleanMarkdown(replacement);
1354
1596
  const now = nowIso();
@@ -1374,6 +1616,10 @@ async function addSuggestion(io, pathOrDocId, range, replacement, message, autho
1374
1616
  before,
1375
1617
  after: replacement
1376
1618
  },
1619
+ base: {
1620
+ contentHash: contentHashForText(state.markdown),
1621
+ ...entry?.currentSha ? { head: entry.currentSha } : {}
1622
+ },
1377
1623
  changeSetId,
1378
1624
  createdAt: now,
1379
1625
  updatedAt: now
@@ -1416,7 +1662,8 @@ async function acceptSuggestion(io, suggestionId, actor = defaultActor()) {
1416
1662
  const sidecar = await readSidecar(io, entry.docId);
1417
1663
  const suggestion = sidecar.suggestions.find((candidate) => candidate.id === suggestionId);
1418
1664
  if (!suggestion) continue;
1419
- const nextMarkdown = applySuggestion(markdown, suggestion);
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 || !rootId) return void 0;
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 base = `/share/${encodeURIComponent(workspaceId)}/${encodeURIComponent(rootId)}`;
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.2";
10993
+ var CLI_VERSION = "0.3.5";
10646
10994
  var CLI_PACKAGE_NAME = "@magic-markdown/cli";
10647
10995
  var AGENT_COMMANDS = [
10648
10996
  {
@@ -10672,11 +11020,12 @@ var AGENT_COMMANDS = [
10672
11020
  {
10673
11021
  name: "join",
10674
11022
  summary: "Join a Magic Markdown share URL through the CLI, announce presence, and remember the session for later rejoin.",
10675
- usage: "mdocs join <share-url> --scope file|project --doc <docId> --name <agent-name> --json",
11023
+ usage: "mdocs join <share-url> --scope file|project|workspace --doc <docId> --name <agent-name> --json",
10676
11024
  output: "json",
10677
11025
  mutates: true,
10678
11026
  examples: [
10679
11027
  'mdocs join https://example.com/share/workspace/root/doc --scope file --doc doc_abc123 --name "Claude Code" --json',
11028
+ 'mdocs join https://example.com/share/workspace --scope workspace --name "Claude Code" --json',
10680
11029
  "mdocs join --json"
10681
11030
  ],
10682
11031
  notes: [
@@ -10694,13 +11043,14 @@ var AGENT_COMMANDS = [
10694
11043
  {
10695
11044
  name: "remote",
10696
11045
  summary: "Read, organize, comment on, suggest edits to, monitor, and restore the active Magic Markdown remote join.",
10697
- usage: "mdocs remote map|graph|context|create-file|move-file|comment|suggest|reject|events|history|restore|library|create-folder|update-folder|move-root|invite-folder --json",
11046
+ usage: "mdocs remote map|graph|context|review|create-file|move-file|comment|suggest|reject|events|history|restore|library|create-folder|update-folder|move-root|invite-folder --json",
10698
11047
  output: "json",
10699
11048
  mutates: true,
10700
11049
  examples: [
10701
- "mdocs remote context --json",
10702
- "mdocs remote context --start-line 1 --end-line 100 --json",
10703
- "mdocs remote context --start-line 101 --end-line 200 --json",
11050
+ "mdocs remote context --summary --json",
11051
+ "mdocs remote context --start-line 1 --end-line 100 --no-review --json",
11052
+ "mdocs remote context --start-line 101 --end-line 200 --no-review --json",
11053
+ "mdocs remote review --json",
10704
11054
  "mdocs remote map --json",
10705
11055
  "mdocs remote graph --json",
10706
11056
  "mdocs remote create-file docs/new-note.md --with-file /tmp/initial.md --json",
@@ -10716,13 +11066,16 @@ var AGENT_COMMANDS = [
10716
11066
  "mdocs remote invite-folder fold_abc123 --email person@example.com --role edit --json"
10717
11067
  ],
10718
11068
  notes: [
11069
+ "Use remote context --summary --json first on file-scoped joins and before reading large documents. It returns metadata, heading line numbers, current head, review counts, and suggested next commands without dumping Markdown.",
10719
11070
  "remote context accepts --start-line and --end-line (1-based, inclusive) to page through large documents. The response always includes totalLines, startLine, and endLine \u2014 if totalLines > endLine, request the next page with --start-line <endLine+1>.",
11071
+ "Use remote context --no-review when reading document content only; use remote review to list open comments, open suggestions, and anchors separately.",
11072
+ "remote map works on project and workspace-scoped joins. Workspace-scoped map lists every shared Home root; target duplicate paths as <rootId>:<path-or-docId>.",
10720
11073
  "remote graph requires a project-scoped join and returns the shared project's Obsidian-style Markdown/wikilink graph.",
10721
11074
  "remote create-file and remote move-file require a project-scoped join and edit access. They write through the root sync API, so version history and conflict handling stay intact.",
10722
11075
  "remote library, create-folder, update-folder, move-root, and invite-folder use the organization suite on top of the joined project. Folder invites require admin access on the joined root and can only target folders containing that root.",
10723
11076
  "remote comment/suggest never clobber concurrent edits: the server merges review additions, and the CLI rebases and retries automatically if a conflict still occurs.",
10724
11077
  "remote reject <suggestionId> withdraws a suggestion you submitted (for example, a duplicate). You can always withdraw your own; rejecting another author's suggestion requires edit access, and resolving a human's review work needs the user's explicit ask.",
10725
- "remote events returns truncated: true when older events were dropped from the bounded log; refetch full state with remote context instead of trusting the gap.",
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, open review state, anchors, image references, and resolved outgoing links.",
10761
- usage: "mdocs context <path|docId> [--start-line N] [--end-line N] --json",
11113
+ summary: "Return agent-ready document Markdown, optional review state, image references, and resolved outgoing links.",
11114
+ usage: "mdocs context <path|docId> [--summary] [--no-review] [--start-line N] [--end-line N] --json",
10762
11115
  output: "json",
10763
11116
  mutates: true,
10764
11117
  examples: [
11118
+ "mdocs context docs/example.md --summary --json",
10765
11119
  "mdocs context docs/example.md --json",
10766
- "mdocs context docs/example.md --start-line 1 --end-line 100 --json",
10767
- "mdocs context docs/example.md --start-line 101 --end-line 200 --json"
11120
+ "mdocs context docs/example.md --start-line 1 --end-line 100 --no-review --json",
11121
+ "mdocs context docs/example.md --start-line 101 --end-line 200 --no-review --json"
10768
11122
  ],
10769
11123
  notes: [
11124
+ "--summary returns metadata, heading line numbers, review counts, image counts, and link counts without returning Markdown.",
10770
11125
  "The response always includes totalLines, startLine, and endLine. If totalLines > endLine, request the next page with --start-line <endLine+1>.",
10771
- "--start-line and --end-line are 1-based and inclusive. Omitting both returns the full document."
11126
+ "--start-line and --end-line are 1-based and inclusive. Omitting both returns the full document.",
11127
+ "--no-review omits comments, suggestions, and anchors from content reads; use review/comments/suggestions commands when you need review state."
10772
11128
  ]
10773
11129
  },
11130
+ {
11131
+ name: "review",
11132
+ summary: "Return open comments, open suggestions, and anchors for one document without returning Markdown.",
11133
+ usage: "mdocs review <path|docId> --json",
11134
+ output: "json",
11135
+ mutates: true,
11136
+ examples: ["mdocs review docs/example.md --json"]
11137
+ },
10774
11138
  {
10775
11139
  name: "state",
10776
11140
  summary: "Return full merged Markdown and sidecar state for one document.",
@@ -10803,7 +11167,8 @@ var AGENT_COMMANDS = [
10803
11167
  ],
10804
11168
  notes: [
10805
11169
  "--range is 1-based and inclusive, validated against the current document line count.",
10806
- "The replacement text replaces the whole range; re-read context first so the range matches current content."
11170
+ "The replacement text replaces the whole range; re-read context first so the range matches current content.",
11171
+ "Prefer the smallest coherent range for the intended revision. If only one sentence inside a line changes, keep the rest of that line identical; use longer ranges only when the broader rewrite is genuinely needed."
10807
11172
  ]
10808
11173
  },
10809
11174
  {
@@ -10947,15 +11312,18 @@ Run \`mdocs help <command>\` (or \`mdocs <command> --help\`) for usage, examples
10947
11312
 
10948
11313
  1. If given a Magic Markdown share URL, run \`mdocs join <share-url> --json\` first, and always include \`--name "<your agent name>"\` (for example \`--name "Claude Code"\`) so collaborators can see which agent is connected. Use \`mdocs remote ...\` commands after joining.
10949
11314
  2. If working in a local workspace, run \`mdocs doctor --json\` to validate the workspace and learn recommended next commands.
10950
- 3. Run \`mdocs map --json\` or \`mdocs remote map --json\` to list documents, paths, docIds, open comments, open suggestions, anchor review counts, and link counts. File-scoped joins (most share links) cover a single document, so \`mdocs remote map\` is unavailable for them \u2014 use \`mdocs remote context --json\` instead. To organize a shared project, rejoin with \`--scope project\` and use \`mdocs remote library --json\`.
11315
+ 3. Run \`mdocs map --json\` or \`mdocs remote map --json\` to list documents, paths, docIds, open comments, open suggestions, anchor review counts, and link counts. File-scoped joins (most share links) cover a single document, so \`mdocs remote map\` is unavailable for them \u2014 use \`mdocs remote context --summary --json\` instead. Project-scoped joins cover one root; workspace-scoped joins cover Home and can target duplicate paths as \`<rootId>:<path-or-docId>\`.
10951
11316
  4. Run \`mdocs graph --json\` or project-scoped \`mdocs remote graph --json\` before broad edits to inspect the Obsidian-style document graph built from Markdown links and wikilinks.
10952
- 5. For a document, run \`mdocs context <path|docId> --json\` locally or \`mdocs remote context <path|docId> --json\` for a joined share.
10953
- 6. Use \`mdocs comment\` / \`mdocs remote comment\` for review notes and \`mdocs suggest\` / \`mdocs remote suggest\` for proposed replacements. Do not insert comments, CriticMarkup, directives, or Magic markers into Markdown files.
11317
+ 5. For a document, run \`mdocs context <path|docId> --summary --json\` locally or \`mdocs remote context <path|docId> --summary --json\` for a joined share before reading full content. Then page Markdown with \`--start-line\` / \`--end-line\` and \`--no-review\` when you only need document text.
11318
+ 6. Pull review state separately with \`mdocs review <path|docId> --json\` locally or \`mdocs remote review <path|docId> --json\` for a joined share. Use \`mdocs comment\` / \`mdocs remote comment\` for review notes and \`mdocs suggest\` / \`mdocs remote suggest\` for proposed replacements. Do not insert comments, CriticMarkup, directives, or Magic markers into Markdown files.
10954
11319
 
10955
11320
  ## Editing Rules
10956
11321
 
10957
11322
  - Comments and suggestions are sidecar operations. They should not modify clean Markdown until a suggestion is accepted.
10958
11323
  - \`--range <start:end>\` is 1-based and inclusive, and validated against the current document: read the document first and quote line numbers from what you just read.
11324
+ - Prefer the smallest coherent suggestion range that contains the actual change. If one sentence, list item, table row, or short paragraph changes, suggest that unit rather than replacing surrounding unchanged paragraphs or sections.
11325
+ - Broader paragraph, section, or multi-section rewrites are appropriate when the edit genuinely changes structure, ordering, transitions, or multiple interdependent ideas. Do not avoid a long edit when it is the clearest correct revision.
11326
+ - When the narrowest Markdown line contains unchanged surrounding text, keep that unchanged text byte-for-byte identical in the replacement instead of rewording the whole paragraph.
10959
11327
  - Prefer \`--json\` for agent calls and parse stdout as JSON.
10960
11328
  - For multiline suggestion text, write the exact replacement to a temporary file and pass \`--with-file <file>\`.
10961
11329
  - For long comments or messages, use \`--body-file <file>\` or \`--message-file <file>\`.
@@ -10968,14 +11336,14 @@ Run \`mdocs help <command>\` (or \`mdocs <command> --help\`) for usage, examples
10968
11336
  - Errors go to stderr. With \`--json\` they are structured: \`{ "ok": false, "error": { "code", "message", "hint" }, "cliVersion" }\`. Follow the \`hint\`.
10969
11337
  - Exit codes: 0 ok, 1 internal, 2 usage/invalid range, 3 conflict, 4 not found, 5 network, 6 unauthorized (share link revoked or expired).
10970
11338
  - \`remote comment\` and \`remote suggest\` are concurrency-safe: the server merges review additions without clobbering concurrent edits, and the CLI rebases and retries automatically. A surviving \`conflict\` error means the document is changing rapidly \u2014 refetch context and retry.
10971
- - \`remote events --json\` may return \`"truncated": true\` when older events were dropped from the bounded event log. Do not assume nothing happened; refetch full state with \`mdocs remote context --json\`.
11339
+ - \`remote events --json\` may return \`"truncated": true\` when older events were dropped from the bounded event log. Do not assume nothing happened; refetch with \`mdocs remote context --summary --json\`, then read needed pages and review state.
10972
11340
  - Retries are safe: remote writes carry a changeId and the server deduplicates replays.
10973
11341
  - Stale reads are safe too: the server merges your change three-way against the version you read, so non-overlapping concurrent edits never conflict. Only true overlaps return \`conflict\`.
10974
11342
 
10975
11343
  ## Access Roles and Version History
10976
11344
 
10977
11345
  - Agents are **suggest-only by default**: comments and suggestions always work, but direct content rewrites are rejected with a \`forbidden_role\` error until a human grants edit access. Propose changes with \`mdocs remote suggest\` instead of rewriting content.
10978
- - Project-scoped agents with edit access can create and move Markdown files and can move the joined root into library folders. Folder invites and folder hierarchy updates require admin access on the joined root.
11346
+ - Project-scoped agents with edit access can create and move Markdown files and can move the joined root into library folders. Workspace-scoped agents can read/comment/suggest across Home roots; root organization commands still require a project-scoped join. Folder invites and folder hierarchy updates require admin access on the joined root.
10979
11347
  - Every accepted change is a content-addressed commit with a full snapshot. \`mdocs remote history --json\` lists commits with changed paths; \`mdocs remote restore <headId> --json\` restores the joined document to that snapshot (as a new commit, so it is reversible). Restore requires edit access \u2014 only run it when the user asked.
10980
11348
 
10981
11349
  ## Core Commands
@@ -10990,7 +11358,7 @@ Start the local stdio MCP server with:
10990
11358
  mdocs serve-mcp --cwd /path/to/workspace
10991
11359
  \`\`\`
10992
11360
 
10993
- The MCP server exposes document resources, image resources, workspace map and graph resources, an agent guide resource, typed tools for comments and suggestions, and the \`magic_markdown_agent_workflow\` prompt.
11361
+ The MCP server exposes document resources, image resources, workspace map and graph resources, an agent guide resource, typed tools for comments and suggestions, and the \`magic_markdown_agent_workflow\` prompt. Use \`mdocs_context\` / \`magic_context\` with \`summary: true\` before dumping Markdown; use \`includeReview: false\` for content-only reads and \`mdocs_review\` / \`magic_review\` for review state.
10994
11362
  `;
10995
11363
  }
10996
11364
  function formatCommandReference() {
@@ -11414,8 +11782,9 @@ function withMcpImageResources(io, state) {
11414
11782
  }))
11415
11783
  };
11416
11784
  }
11417
- async function contextForDocument(io, path, startLine, endLine) {
11785
+ async function contextForDocument(io, path, startLine, endLine, options = {}) {
11418
11786
  const state = await getDocumentState(io, path);
11787
+ if (options.summary) return summarizeDocumentContext(state);
11419
11788
  const { markdown, totalLines, startLine: sl, endLine: el } = sliceMarkdown(state.markdown, startLine, endLine);
11420
11789
  return {
11421
11790
  path: state.path,
@@ -11427,21 +11796,19 @@ async function contextForDocument(io, path, startLine, endLine) {
11427
11796
  markdown,
11428
11797
  images: withMcpImageResources(io, state).images,
11429
11798
  links: state.links,
11799
+ ...options.includeReview === false ? {} : reviewFields(state)
11800
+ };
11801
+ }
11802
+ async function reviewForDocument(io, path) {
11803
+ return reviewStateForDocument(await getDocumentState(io, path));
11804
+ }
11805
+ function reviewFields(state) {
11806
+ return {
11430
11807
  comments: state.sidecar.comments.filter((comment2) => comment2.status === "open"),
11431
11808
  suggestions: state.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
11432
11809
  anchors: state.anchors
11433
11810
  };
11434
11811
  }
11435
- function sliceMarkdown(markdown, startLine, endLine) {
11436
- const lines = markdown.split("\n");
11437
- const totalLines = lines.length;
11438
- const sl = startLine ?? 1;
11439
- const el = endLine ?? totalLines;
11440
- if (sl < 1 || el > totalLines || sl > el) {
11441
- throw new Error(`Line range ${sl}:${el} is out of bounds for a ${totalLines}-line document.`);
11442
- }
11443
- return { markdown: lines.slice(sl - 1, el).join("\n"), totalLines, startLine: sl, endLine: el };
11444
- }
11445
11812
  function imageResourceUri(docId, index) {
11446
11813
  return `mdocs://documents/${docId}/images/${index}`;
11447
11814
  }
@@ -11519,7 +11886,7 @@ var tools = [
11519
11886
  },
11520
11887
  {
11521
11888
  name: "mdocs_context",
11522
- description: "Return agent-ready document Markdown, open review state, anchors, image references, and resolved outgoing links. Use startLine/endLine to read a specific line range when the full document is too large for one context window. The response always includes totalLines so you know whether to page.",
11889
+ description: "Return agent-ready document Markdown and resolved outgoing links. Use summary=true first to inspect title, line count, headings, current head, and review/link/image counts without dumping the full document. Use includeReview=false when reading content only, and mdocs_review for comments, suggestions, and anchors.",
11523
11890
  inputSchema: {
11524
11891
  type: "object",
11525
11892
  properties: {
@@ -11531,12 +11898,25 @@ var tools = [
11531
11898
  endLine: {
11532
11899
  type: "number",
11533
11900
  description: "1-based last line to include (default: last line of document)."
11901
+ },
11902
+ summary: {
11903
+ type: "boolean",
11904
+ description: "Return metadata, heading outline, current head, and counts instead of Markdown content."
11905
+ },
11906
+ includeReview: {
11907
+ type: "boolean",
11908
+ description: "Whether to include open comments, suggestions, and anchors in the content response (default: true). Use false to keep document reading separate from review state."
11534
11909
  }
11535
11910
  },
11536
11911
  required: ["path"],
11537
11912
  additionalProperties: false
11538
11913
  }
11539
11914
  },
11915
+ {
11916
+ name: "mdocs_review",
11917
+ description: "Return open comments, open suggestions, and mapped anchors for one document without returning document Markdown.",
11918
+ inputSchema: documentPathSchema
11919
+ },
11540
11920
  {
11541
11921
  name: "mdocs_state",
11542
11922
  description: "Return full merged Markdown and sidecar state for one document.",
@@ -11560,14 +11940,14 @@ var tools = [
11560
11940
  },
11561
11941
  {
11562
11942
  name: "mdocs_suggest",
11563
- description: "Create a sidecar replacement suggestion without modifying clean Markdown.",
11943
+ description: "Create a sidecar replacement suggestion without modifying clean Markdown. Prefer the smallest coherent range; use longer ranges only when the broader rewrite is genuinely needed.",
11564
11944
  inputSchema: {
11565
11945
  type: "object",
11566
11946
  properties: {
11567
11947
  path: documentPathSchema.properties.path,
11568
11948
  startLine: { type: "number", description: "1-based start line." },
11569
11949
  endLine: { type: "number", description: "1-based end line." },
11570
- replacement: { type: "string", description: "Exact replacement Markdown for the line range." },
11950
+ replacement: { type: "string", description: "Exact replacement Markdown for the line range; keep unchanged surrounding text identical for narrow edits." },
11571
11951
  message: { type: "string", description: "Short explanation for the suggestion." },
11572
11952
  ...actorProperties
11573
11953
  },
@@ -11789,8 +12169,12 @@ async function callTool(io, name, args) {
11789
12169
  if (name === "mdocs_context") {
11790
12170
  const startLine = typeof args.startLine === "number" ? args.startLine : void 0;
11791
12171
  const endLine = typeof args.endLine === "number" ? args.endLine : void 0;
11792
- return contextForDocument(io, requiredString(args, "path"), startLine, endLine);
12172
+ return contextForDocument(io, requiredString(args, "path"), startLine, endLine, {
12173
+ summary: args.summary === true,
12174
+ includeReview: args.includeReview !== false
12175
+ });
11793
12176
  }
12177
+ if (name === "mdocs_review") return reviewForDocument(io, requiredString(args, "path"));
11794
12178
  if (name === "mdocs_state") return withMcpImageResources(io, await getDocumentState(io, requiredString(args, "path")));
11795
12179
  if (name === "mdocs_comment") {
11796
12180
  return addComment(
@@ -11897,12 +12281,22 @@ async function postPresenceAndReadState(share, shareUrl, docId, agentId, agentNa
11897
12281
  });
11898
12282
  }
11899
12283
  async function fetchState(record) {
11900
- if (!record.docId) {
12284
+ if (!record.rootId || !record.docId) {
11901
12285
  throw new CliError("usage_error", "This join has no current document.", {
11902
12286
  hint: "Pass a document path or docId, or list documents with mdocs remote map --json."
11903
12287
  });
11904
12288
  }
11905
- return fetchJson(agentUrl(record, record.docId, "state"), {
12289
+ return fetchJson(agentUrl({ ...record, rootId: record.rootId }, record.docId, "state"), {
12290
+ headers: shareHeaders(record.shareUrl)
12291
+ });
12292
+ }
12293
+ async function fetchWorkspaceRoots(record) {
12294
+ return fetchJson(`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots`, {
12295
+ headers: shareHeaders(record.shareUrl)
12296
+ });
12297
+ }
12298
+ async function fetchWorkspaceLibrary(record) {
12299
+ return fetchJson(`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/library`, {
11906
12300
  headers: shareHeaders(record.shareUrl)
11907
12301
  });
11908
12302
  }
@@ -11948,8 +12342,10 @@ async function postFolderInvite(record, folderId, input, actor) {
11948
12342
  );
11949
12343
  }
11950
12344
  async function fetchDocument(record, pathOrDocId) {
12345
+ if (record.scope === "workspace") return fetchWorkspaceDocument(record, pathOrDocId);
12346
+ const rootRecord = requireRootRecord(record);
11951
12347
  if (record.scope === "file" && record.docId) {
11952
- const document = await fetchDocumentById(record, record.docId);
12348
+ const document = await fetchDocumentById(rootRecord, record.docId);
11953
12349
  if (pathOrDocId === document.docId || pathOrDocId === document.path) return document;
11954
12350
  throw new CliError("usage_error", `This file-scoped join only covers ${document.path} (${document.docId}).`, {
11955
12351
  hint: `Join a share link for ${pathOrDocId} to work on it.`
@@ -11957,19 +12353,19 @@ async function fetchDocument(record, pathOrDocId) {
11957
12353
  }
11958
12354
  if (pathOrDocId.startsWith("doc_") || pathOrDocId === record.docId) {
11959
12355
  try {
11960
- return await fetchDocumentById(record, pathOrDocId);
12356
+ return await fetchDocumentById(rootRecord, pathOrDocId);
11961
12357
  } catch (error) {
11962
12358
  if (!(error instanceof CliError) || error.code !== "not_found") throw error;
11963
12359
  }
11964
12360
  }
11965
- const tree = await fetchTree(record);
12361
+ const tree = await fetchTree(rootRecord);
11966
12362
  const docId = tree.docs.find((doc) => doc.docId === pathOrDocId || doc.path === pathOrDocId)?.docId;
11967
12363
  if (!docId) {
11968
12364
  throw new CliError("not_found", `No document matches "${pathOrDocId}" in the joined workspace.`, {
11969
12365
  hint: "List documents and their paths/docIds with mdocs remote map --json."
11970
12366
  });
11971
12367
  }
11972
- return fetchDocumentById(record, docId);
12368
+ return fetchDocumentById(rootRecord, docId);
11973
12369
  }
11974
12370
  function fetchDocumentById(record, docId) {
11975
12371
  return fetchJson(
@@ -11977,6 +12373,51 @@ function fetchDocumentById(record, docId) {
11977
12373
  { headers: shareHeaders(record.shareUrl) }
11978
12374
  );
11979
12375
  }
12376
+ async function fetchWorkspaceDocument(record, pathOrDocId) {
12377
+ if (!pathOrDocId) {
12378
+ throw new CliError("usage_error", "Workspace-scoped context requires a document path or docId.", {
12379
+ hint: "Run mdocs remote map --json, then pass a path, docId, or <rootId>:<path-or-docId>."
12380
+ });
12381
+ }
12382
+ const roots = await fetchWorkspaceRoots(record);
12383
+ const rootIds = new Set(roots.map((root) => root.rootId));
12384
+ const prefixed = splitWorkspaceDocumentTarget(pathOrDocId, rootIds);
12385
+ if (prefixed) {
12386
+ const rootRecord = { ...record, rootId: prefixed.rootId };
12387
+ const tree = await fetchTree(rootRecord);
12388
+ const match3 = tree.docs.find((doc) => doc.docId === prefixed.target || doc.path === prefixed.target);
12389
+ if (!match3) {
12390
+ throw new CliError("not_found", `No document matches "${prefixed.target}" in ${prefixed.rootId}.`, {
12391
+ hint: "Run mdocs remote map --json to verify the root id and document path."
12392
+ });
12393
+ }
12394
+ return fetchDocumentById(rootRecord, match3.docId);
12395
+ }
12396
+ const trees = await Promise.all(roots.map((root) => fetchTree({ ...record, rootId: root.rootId })));
12397
+ const matches = trees.flatMap(
12398
+ (tree) => tree.docs.filter((doc) => doc.docId === pathOrDocId || doc.path === pathOrDocId).map((doc) => ({ rootId: tree.root.rootId, doc }))
12399
+ );
12400
+ if (matches.length === 0) {
12401
+ throw new CliError("not_found", `No document matches "${pathOrDocId}" in the joined Home workspace.`, {
12402
+ hint: "Run mdocs remote map --json to list roots, paths, and docIds."
12403
+ });
12404
+ }
12405
+ if (matches.length > 1) {
12406
+ throw new CliError("usage_error", `Document target "${pathOrDocId}" is ambiguous across ${matches.length} roots.`, {
12407
+ hint: `Prefix the target with a root id, for example ${matches[0].rootId}:${pathOrDocId}.`
12408
+ });
12409
+ }
12410
+ const match2 = matches[0];
12411
+ return fetchDocumentById({ ...record, rootId: match2.rootId }, match2.doc.docId);
12412
+ }
12413
+ function splitWorkspaceDocumentTarget(value, rootIds) {
12414
+ const separator = value.indexOf(":");
12415
+ if (separator <= 0) return void 0;
12416
+ const rootId = value.slice(0, separator);
12417
+ if (!rootIds.has(rootId)) return void 0;
12418
+ const target = value.slice(separator + 1);
12419
+ return target ? { rootId, target } : void 0;
12420
+ }
11980
12421
  async function postReview(record, docId, additions, actor) {
11981
12422
  const response = await fetchJson(
11982
12423
  `${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/review`,
@@ -12059,7 +12500,7 @@ function conflictError(eventPayload) {
12059
12500
  const conflict = eventPayload ?? {};
12060
12501
  const head = typeof conflict.canonicalHead === "string" ? ` Current head: ${conflict.canonicalHead}.` : "";
12061
12502
  return new CliError("conflict", `Remote change was not accepted: the document changed since it was read.${head}`, {
12062
- hint: "Refetch with mdocs remote context --json, re-apply your change against the fresh content, and retry.",
12503
+ hint: "Refetch with mdocs remote context --summary --json, re-apply your change against the needed fresh content range, and retry.",
12063
12504
  details: eventPayload
12064
12505
  });
12065
12506
  }
@@ -12070,6 +12511,14 @@ function agentUrl(record, docId, action, query) {
12070
12511
  function libraryUrl(record) {
12071
12512
  return `${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/library`;
12072
12513
  }
12514
+ function requireRootRecord(record) {
12515
+ if (!record.rootId) {
12516
+ throw new CliError("usage_error", "This command needs a root-scoped join.", {
12517
+ hint: "For Home joins, pass a document target from `mdocs remote map --json`, or use a project share for root organization commands."
12518
+ });
12519
+ }
12520
+ return { origin: record.origin, workspaceId: record.workspaceId, rootId: record.rootId, shareUrl: record.shareUrl };
12521
+ }
12073
12522
  function shareHeaders(shareUrl) {
12074
12523
  return { Referer: shareUrl, Authorization: `Bearer ${shareUrl}` };
12075
12524
  }
@@ -12107,7 +12556,7 @@ function remoteHttpError(status, value, text2) {
12107
12556
  }
12108
12557
  if (status === 409) {
12109
12558
  return new CliError("conflict", message, {
12110
- hint: "Refetch with mdocs remote context --json and retry.",
12559
+ hint: "Refetch with mdocs remote context --summary --json and retry.",
12111
12560
  details: value
12112
12561
  });
12113
12562
  }
@@ -12248,9 +12697,10 @@ async function runJoinCommand(root, parsed) {
12248
12697
  const parsedShare = parseShareUrl(target, parsed.flags);
12249
12698
  const agentName = resolveAgentName(parsed.flags);
12250
12699
  const agentId = normalizeAgentId(String(parsed.flags["agent-id"] ?? parsed.flags.actor ?? agentName));
12251
- const docId = parsedShare.docId ?? await firstDocIdForProject(parsedShare);
12700
+ const rootScopedShare = rootScopedRecord(parsedShare);
12701
+ const docId = parsedShare.docId ?? (rootScopedShare ? await firstDocIdForProject(rootScopedShare) : void 0);
12252
12702
  const shareUrl = shareUrlForScope(target, { ...parsedShare, docId });
12253
- const state = await postPresenceAndReadState(parsedShare, shareUrl, docId, agentId, agentName);
12703
+ const state = rootScopedShare && docId ? await postPresenceAndReadState(rootScopedShare, shareUrl, docId, agentId, agentName) : void 0;
12254
12704
  const now = (/* @__PURE__ */ new Date()).toISOString();
12255
12705
  const record = {
12256
12706
  joinId: joinIdFor(target),
@@ -12265,10 +12715,10 @@ async function runJoinCommand(root, parsed) {
12265
12715
  joinedAt: now,
12266
12716
  updatedAt: now,
12267
12717
  lastSeenEventId: 0,
12268
- currentHead: state.document.currentSha
12718
+ currentHead: state?.document.currentSha
12269
12719
  };
12270
12720
  await writeJoinRecord(root, record);
12271
- return joinSummary(record, state.document);
12721
+ return joinSummary(record, state?.document);
12272
12722
  }
12273
12723
  async function runJoinsCommand(root) {
12274
12724
  return listJoinRecords(root);
@@ -12277,18 +12727,23 @@ async function runRemoteCommand(root, subcommand, parsed) {
12277
12727
  const record = await normalizedSelectedJoin(root, parsed.flags);
12278
12728
  switch (subcommand) {
12279
12729
  case "map":
12280
- case void 0:
12730
+ case void 0: {
12281
12731
  if (record.scope === "file") {
12282
12732
  throw new CliError("usage_error", "This join is file-scoped, so the project tree is not accessible.", {
12283
- hint: "Use `mdocs remote context --json` to read the joined document."
12733
+ hint: "Use `mdocs remote context --summary --json` to inspect the joined document."
12284
12734
  });
12285
12735
  }
12286
- await refreshPresence(record, record.docId);
12287
- return fetchTree(record);
12736
+ if (record.scope === "workspace") return remoteWorkspaceMap(record);
12737
+ const rootRecord = assertRootScoped(record, "map");
12738
+ await refreshPresence(rootRecord, record.docId);
12739
+ return fetchTree(rootRecord);
12740
+ }
12288
12741
  case "graph":
12289
12742
  return remoteGraph(record);
12290
12743
  case "context":
12291
12744
  return remoteContext(record, parsed.command[2], parsed.flags);
12745
+ case "review":
12746
+ return remoteReview(record, parsed.command[2]);
12292
12747
  case "create-file":
12293
12748
  return remoteCreateFile(root, record, parsed);
12294
12749
  case "move-file":
@@ -12305,10 +12760,13 @@ async function runRemoteCommand(root, subcommand, parsed) {
12305
12760
  return remoteHistory(record, parsed.flags);
12306
12761
  case "restore":
12307
12762
  return remoteRestore(record, parsed);
12308
- case "library":
12763
+ case "library": {
12764
+ if (record.scope === "workspace") return fetchWorkspaceLibrary(record);
12309
12765
  assertProjectScope(record, "library");
12310
- await refreshPresence(record, record.docId);
12311
- return fetchLibrary(record);
12766
+ const rootRecord = assertRootScoped(record, "library");
12767
+ await refreshPresence(rootRecord, record.docId);
12768
+ return fetchLibrary(rootRecord);
12769
+ }
12312
12770
  case "create-folder":
12313
12771
  return remoteCreateFolder(record, parsed);
12314
12772
  case "update-folder":
@@ -12321,15 +12779,16 @@ async function runRemoteCommand(root, subcommand, parsed) {
12321
12779
  return rejoin(root, record.joinId, parsed.flags);
12322
12780
  default:
12323
12781
  throw new CliError("usage_error", `Unknown remote subcommand: ${subcommand}`, {
12324
- hint: "Use mdocs remote map|graph|context|create-file|move-file|comment|suggest|reject|events|history|restore|library|create-folder|update-folder|move-root|invite-folder|rejoin."
12782
+ hint: "Use mdocs remote map|graph|context|review|create-file|move-file|comment|suggest|reject|events|history|restore|library|create-folder|update-folder|move-root|invite-folder|rejoin."
12325
12783
  });
12326
12784
  }
12327
12785
  }
12328
12786
  async function remoteGraph(record) {
12329
12787
  assertProjectScope(record, "graph");
12330
- await refreshPresence(record, record.docId);
12331
- const tree = await fetchTree(record);
12332
- const documents = await Promise.all(tree.docs.map((doc) => fetchDocument(record, doc.docId)));
12788
+ const rootRecord = assertRootScoped(record, "graph");
12789
+ await refreshPresence(rootRecord, record.docId);
12790
+ const tree = await fetchTree(rootRecord);
12791
+ const documents = await Promise.all(tree.docs.map((doc) => fetchDocument(rootScopedRecordFor(record, rootRecord.rootId), doc.docId)));
12333
12792
  const lookup = documents.map((doc) => ({ docId: doc.docId, path: doc.path, title: doc.title }));
12334
12793
  const graphDocuments = documents.map((doc) => {
12335
12794
  const links = doc.links ?? resolveMarkdownLinks(extractMarkdownLinks(doc.markdown), doc, lookup);
@@ -12347,10 +12806,37 @@ async function remoteGraph(record) {
12347
12806
  });
12348
12807
  return buildWorkspaceGraph(record.workspaceId, SIDECAR_SCHEMA_VERSION, graphDocuments);
12349
12808
  }
12809
+ async function remoteWorkspaceMap(record) {
12810
+ const roots = await fetchWorkspaceRoots(record);
12811
+ const trees = await Promise.all(roots.map((root) => fetchTree(rootScopedRecordFor(record, root.rootId))));
12812
+ const library = await fetchWorkspaceLibrary(record).catch(() => void 0);
12813
+ return {
12814
+ workspaceId: record.workspaceId,
12815
+ scope: "workspace",
12816
+ roots: trees.map((tree) => ({
12817
+ root: tree.root,
12818
+ docs: tree.docs.map((doc) => ({
12819
+ rootId: tree.root.rootId,
12820
+ docId: doc.docId,
12821
+ path: doc.path,
12822
+ title: doc.title,
12823
+ openComments: doc.openComments,
12824
+ openSuggestions: doc.openSuggestions,
12825
+ anchorsNeedingReview: doc.anchorsNeedingReview,
12826
+ outgoingLinks: doc.outgoingLinks,
12827
+ incomingLinks: doc.incomingLinks,
12828
+ unresolvedLinks: doc.unresolvedLinks,
12829
+ externalLinks: doc.externalLinks
12830
+ }))
12831
+ })),
12832
+ ...library ? { folders: library.folders } : {}
12833
+ };
12834
+ }
12350
12835
  async function remoteHistory(record, flags) {
12351
- await refreshPresence(record, record.docId);
12836
+ const rootRecord = assertRootScoped(record, "history");
12837
+ await refreshPresence(rootRecord, record.docId);
12352
12838
  const limit = typeof flags.limit === "string" ? Number(flags.limit) || 50 : 50;
12353
- return fetchCommits(record, limit);
12839
+ return fetchCommits(rootRecord, limit);
12354
12840
  }
12355
12841
  async function remoteRestore(record, parsed) {
12356
12842
  const headId = parsed.command[2];
@@ -12359,9 +12845,10 @@ async function remoteRestore(record, parsed) {
12359
12845
  hint: "List restorable commits with mdocs remote history --json, then run mdocs remote restore <headId>."
12360
12846
  });
12361
12847
  }
12362
- await refreshPresence(record, record.docId);
12363
12848
  const restoreAll = parsed.flags.all === true || parsed.flags.all === "true";
12364
12849
  let docId;
12850
+ const rootRecord = assertRootScoped(record, "restore");
12851
+ await refreshPresence(rootRecord, record.docId);
12365
12852
  if (!restoreAll) {
12366
12853
  const target = typeof parsed.flags.doc === "string" ? parsed.flags.doc : record.docId;
12367
12854
  if (!target) {
@@ -12371,22 +12858,23 @@ async function remoteRestore(record, parsed) {
12371
12858
  }
12372
12859
  docId = (await fetchDocument(record, target)).docId;
12373
12860
  }
12374
- return postRestore(record, headId, docId);
12861
+ return postRestore(rootRecord, headId, docId);
12375
12862
  }
12376
12863
  async function rejoin(root, joinId, flags) {
12377
12864
  const record = await readSelectedJoin(root, { ...flags, ...joinId ? { join: joinId } : {} });
12378
- const docId = record.docId ?? await firstDocIdForProject(record);
12865
+ const rootRecord = rootScopedRecord(record);
12866
+ const docId = record.docId ?? (rootRecord ? await firstDocIdForProject(rootRecord) : void 0);
12379
12867
  const shareUrl = shareUrlForScope(record.shareUrl, { ...record, docId });
12380
- const state = await postPresenceAndReadState(record, shareUrl, docId, record.agentId, record.agentName);
12868
+ const state = rootRecord && docId ? await postPresenceAndReadState(rootRecord, shareUrl, docId, record.agentId, record.agentName) : void 0;
12381
12869
  const next = {
12382
12870
  ...record,
12383
12871
  shareUrl,
12384
12872
  docId,
12385
12873
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
12386
- currentHead: state.document.currentSha
12874
+ currentHead: state?.document.currentSha
12387
12875
  };
12388
12876
  await writeJoinRecord(root, next);
12389
- return joinSummary(next, state.document);
12877
+ return joinSummary(next, state?.document);
12390
12878
  }
12391
12879
  async function normalizedSelectedJoin(root, flags) {
12392
12880
  const record = await readSelectedJoin(root, flags);
@@ -12403,19 +12891,13 @@ async function refreshPresence(record, docId) {
12403
12891
  }
12404
12892
  async function remoteContext(record, pathOrDocId, flags = {}) {
12405
12893
  const document = pathOrDocId ? await fetchDocument(record, pathOrDocId) : (await fetchState(record)).document;
12406
- await refreshPresence(record, document.docId);
12894
+ await refreshPresence(rootScopedRecordFor(record, document.rootId), document.docId);
12895
+ if (flags.summary || flags["metadata-only"]) {
12896
+ return withRemoteSummaryNextCommands(summarizeDocumentContext(document), record, document, pathOrDocId);
12897
+ }
12407
12898
  const startLine = typeof flags["start-line"] === "string" ? Number(flags["start-line"]) : void 0;
12408
12899
  const endLine = typeof flags["end-line"] === "string" ? Number(flags["end-line"]) : void 0;
12409
- const lines = document.markdown.split("\n");
12410
- const totalLines = lines.length;
12411
- const sl = startLine ?? 1;
12412
- const el = endLine ?? totalLines;
12413
- if (sl < 1 || el > totalLines || sl > el) {
12414
- throw new CliError("invalid_range", `Line range ${sl}:${el} is out of bounds for a ${totalLines}-line document.`, {
12415
- hint: `Use --start-line and --end-line with 1-based line numbers within 1:${totalLines}.`
12416
- });
12417
- }
12418
- const markdown = startLine !== void 0 || endLine !== void 0 ? lines.slice(sl - 1, el).join("\n") : document.markdown;
12900
+ const { markdown, totalLines, startLine: sl, endLine: el } = sliceMarkdown(document.markdown, startLine, endLine);
12419
12901
  return {
12420
12902
  joinId: record.joinId,
12421
12903
  scope: record.scope,
@@ -12427,22 +12909,51 @@ async function remoteContext(record, pathOrDocId, flags = {}) {
12427
12909
  startLine: sl,
12428
12910
  endLine: el,
12429
12911
  markdown,
12430
- comments: document.sidecar.comments.filter((comment2) => comment2.status === "open"),
12431
- suggestions: document.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
12432
- anchors: document.anchors,
12912
+ ...flags["no-review"] ? {} : {
12913
+ comments: document.sidecar.comments.filter((comment2) => comment2.status === "open"),
12914
+ suggestions: document.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
12915
+ anchors: document.anchors
12916
+ },
12433
12917
  images: document.images,
12434
12918
  links: document.links
12435
12919
  };
12436
12920
  }
12921
+ async function remoteReview(record, pathOrDocId) {
12922
+ const document = pathOrDocId ? await fetchDocument(record, pathOrDocId) : (await fetchState(record)).document;
12923
+ await refreshPresence(rootScopedRecordFor(record, document.rootId), document.docId);
12924
+ return {
12925
+ joinId: record.joinId,
12926
+ scope: record.scope,
12927
+ ...reviewStateForDocument(document)
12928
+ };
12929
+ }
12930
+ function withRemoteSummaryNextCommands(summary, record, document, requestedTarget) {
12931
+ const target = remoteCommandTarget(record, document, requestedTarget);
12932
+ const firstEnd = Math.min(summary.totalLines, 120);
12933
+ return {
12934
+ ...summary,
12935
+ nextCommands: [
12936
+ `mdocs remote context${target} --start-line 1 --end-line ${firstEnd} --no-review --json`,
12937
+ summary.totalLines > firstEnd ? `mdocs remote context${target} --start-line ${firstEnd + 1} --end-line ${Math.min(summary.totalLines, firstEnd + 120)} --no-review --json` : void 0,
12938
+ summary.reviewCounts.openComments || summary.reviewCounts.openSuggestions || summary.reviewCounts.anchorsNeedingReview ? `mdocs remote review${target} --json` : void 0
12939
+ ].filter((command) => Boolean(command))
12940
+ };
12941
+ }
12942
+ function remoteCommandTarget(record, document, requestedTarget) {
12943
+ if (record.scope === "file" && (!requestedTarget || requestedTarget === document.docId || requestedTarget === document.path)) return "";
12944
+ if (record.scope === "workspace") return ` ${document.rootId}:${document.docId}`;
12945
+ return ` ${document.docId}`;
12946
+ }
12437
12947
  async function remoteCreateFile(root, record, parsed) {
12438
12948
  assertProjectScope(record, "create-file");
12949
+ const rootRecord = assertRootScoped(record, "create-file");
12439
12950
  const path = parsed.command[2];
12440
12951
  if (!path) {
12441
12952
  throw new CliError("usage_error", "Missing file path.", {
12442
12953
  hint: "Run mdocs remote create-file <path> --with-file /tmp/initial.md --json."
12443
12954
  });
12444
12955
  }
12445
- const tree = await fetchTree(record);
12956
+ const tree = await fetchTree(rootRecord);
12446
12957
  const markdownInput = await readOptionalTextFlag(parsed.flags, root, ["with", "markdown", "body"]);
12447
12958
  const markdown = markdownInput ?? defaultMarkdown(path);
12448
12959
  const normalizedPath = normalizeRemoteWorkspacePath(path, tree.root.pathRules, Buffer.byteLength(markdown, "utf8"));
@@ -12455,8 +12966,8 @@ async function remoteCreateFile(root, record, parsed) {
12455
12966
  const title = titleFromMarkdown(normalizedPath, markdown);
12456
12967
  const sidecar = emptySidecar(docId, normalizedPath, title);
12457
12968
  const baseDocument = {
12458
- workspaceId: record.workspaceId,
12459
- rootId: record.rootId,
12969
+ workspaceId: rootRecord.workspaceId,
12970
+ rootId: rootRecord.rootId,
12460
12971
  docId,
12461
12972
  path: normalizedPath,
12462
12973
  title,
@@ -12469,12 +12980,13 @@ async function remoteCreateFile(root, record, parsed) {
12469
12980
  openSuggestions: 0,
12470
12981
  currentSha: tree.root.canonical.head
12471
12982
  };
12472
- const document = await pushDocument(record, baseDocument, markdown, sidecar);
12983
+ const document = await pushDocument(rootRecord, baseDocument, markdown, sidecar);
12473
12984
  await recordHead(root, record, document);
12474
12985
  return { document };
12475
12986
  }
12476
12987
  async function remoteMoveFile(root, record, parsed) {
12477
12988
  assertProjectScope(record, "move-file");
12989
+ const rootRecord = assertRootScoped(record, "move-file");
12478
12990
  const pathOrDocId = parsed.command[2];
12479
12991
  if (!pathOrDocId) {
12480
12992
  throw new CliError("usage_error", "Missing source document path or docId.", {
@@ -12483,7 +12995,7 @@ async function remoteMoveFile(root, record, parsed) {
12483
12995
  }
12484
12996
  const nextPathFlag = requiredFlag(parsed.flags, "to");
12485
12997
  const document = await fetchDocument(record, pathOrDocId);
12486
- const tree = await fetchTree(record);
12998
+ const tree = await fetchTree(rootRecord);
12487
12999
  const nextPath = normalizeRemoteWorkspacePath(nextPathFlag, tree.root.pathRules, Buffer.byteLength(document.markdown, "utf8"));
12488
13000
  if (tree.docs.some((doc) => doc.docId !== document.docId && doc.path === nextPath)) {
12489
13001
  throw new CliError("validation_error", `Document already exists: ${nextPath}`, {
@@ -12491,7 +13003,7 @@ async function remoteMoveFile(root, record, parsed) {
12491
13003
  });
12492
13004
  }
12493
13005
  const moved = await pushDocument(
12494
- record,
13006
+ rootScopedRecordFor(record, document.rootId),
12495
13007
  { ...document, path: nextPath, title: titleFromMarkdown(nextPath, document.markdown) },
12496
13008
  document.markdown,
12497
13009
  document.sidecar
@@ -12524,13 +13036,15 @@ async function remoteSuggest(root, record, parsed) {
12524
13036
  }
12525
13037
  async function remoteCreateFolder(record, parsed) {
12526
13038
  assertProjectScope(record, "create-folder");
13039
+ const rootRecord = assertRootScoped(record, "create-folder");
12527
13040
  const name = folderNameFromArgs(parsed);
12528
13041
  const parentId = optionalFolderTarget(parsed.flags.parent);
12529
- await refreshPresence(record, record.docId);
12530
- return postLibraryFolder(record, { name, ...parentId ? { parentId } : {} }, actorForRecord(record));
13042
+ await refreshPresence(rootRecord, record.docId);
13043
+ return postLibraryFolder(rootRecord, { name, ...parentId ? { parentId } : {} }, actorForRecord(record));
12531
13044
  }
12532
13045
  async function remoteUpdateFolder(record, parsed) {
12533
13046
  assertProjectScope(record, "update-folder");
13047
+ const rootRecord = assertRootScoped(record, "update-folder");
12534
13048
  const folderId = parsed.command[2];
12535
13049
  if (!folderId) {
12536
13050
  throw new CliError("usage_error", "Missing folder id.", {
@@ -12546,22 +13060,24 @@ async function remoteUpdateFolder(record, parsed) {
12546
13060
  hint: "Pass --name <name>, --parent <folderId>, or --home."
12547
13061
  });
12548
13062
  }
12549
- await refreshPresence(record, record.docId);
12550
- return patchLibraryFolder(record, folderId, input, actorForRecord(record));
13063
+ await refreshPresence(rootRecord, record.docId);
13064
+ return patchLibraryFolder(rootRecord, folderId, input, actorForRecord(record));
12551
13065
  }
12552
13066
  async function remoteMoveRoot(record, parsed) {
12553
13067
  assertProjectScope(record, "move-root");
13068
+ const rootRecord = assertRootScoped(record, "move-root");
12554
13069
  const folderId = parsed.flags.home === true || parsed.flags.home === "true" ? null : optionalFolderTarget(parsed.flags.folder);
12555
13070
  if (folderId === void 0) {
12556
13071
  throw new CliError("usage_error", "Missing target folder.", {
12557
13072
  hint: "Pass --folder <folderId> to move into a folder, or --home to move to the top level."
12558
13073
  });
12559
13074
  }
12560
- await refreshPresence(record, record.docId);
12561
- return postMoveRoot(record, folderId, actorForRecord(record));
13075
+ await refreshPresence(rootRecord, record.docId);
13076
+ return postMoveRoot(rootRecord, folderId, actorForRecord(record));
12562
13077
  }
12563
13078
  async function remoteInviteFolder(record, parsed) {
12564
13079
  assertProjectScope(record, "invite-folder");
13080
+ const rootRecord = assertRootScoped(record, "invite-folder");
12565
13081
  const folderId = parsed.command[2];
12566
13082
  if (!folderId) {
12567
13083
  throw new CliError("usage_error", "Missing folder id.", {
@@ -12575,14 +13091,14 @@ async function remoteInviteFolder(record, parsed) {
12575
13091
  hint: "Use --role read, suggest, edit, or admin."
12576
13092
  });
12577
13093
  }
12578
- await refreshPresence(record, record.docId);
12579
- return postFolderInvite(record, folderId, { email, role }, actorForRecord(record));
13094
+ await refreshPresence(rootRecord, record.docId);
13095
+ return postFolderInvite(rootRecord, folderId, { email, role }, actorForRecord(record));
12580
13096
  }
12581
13097
  async function remoteReject(root, record, parsed) {
12582
13098
  const suggestionId = parsed.command[2];
12583
13099
  if (!suggestionId) {
12584
13100
  throw new CliError("usage_error", "Missing suggestion id.", {
12585
- hint: "List open suggestions with mdocs remote context --json, then run mdocs remote reject <suggestionId>."
13101
+ hint: "List open suggestions with mdocs remote review --json, then run mdocs remote reject <suggestionId>."
12586
13102
  });
12587
13103
  }
12588
13104
  const pathOrDocId = parsed.command[3] ?? record.docId;
@@ -12592,12 +13108,13 @@ async function remoteReject(root, record, parsed) {
12592
13108
  });
12593
13109
  }
12594
13110
  let document = await fetchDocument(record, pathOrDocId);
12595
- await refreshPresence(record, document.docId);
13111
+ let documentRecord = rootScopedRecordFor(record, document.rootId);
13112
+ await refreshPresence(documentRecord, document.docId);
12596
13113
  const actor = actorForRecord(record);
12597
13114
  const suggestion = document.sidecar.suggestions.find((candidate) => candidate.id === suggestionId);
12598
13115
  if (!suggestion) {
12599
13116
  throw new CliError("not_found", `Unknown suggestion: ${suggestionId}`, {
12600
- hint: "List suggestion ids with mdocs remote context --json."
13117
+ hint: "List suggestion ids with mdocs remote review --json."
12601
13118
  });
12602
13119
  }
12603
13120
  if (suggestion.status !== "open") {
@@ -12607,7 +13124,7 @@ async function remoteReject(root, record, parsed) {
12607
13124
  }
12608
13125
  try {
12609
13126
  const additions = { anchors: [], comments: [], suggestions: [], withdrawSuggestionIds: [suggestionId] };
12610
- const pushed = await postReview(record, document.docId, additions, actor);
13127
+ const pushed = await postReview(documentRecord, document.docId, additions, actor);
12611
13128
  await recordHead(root, record, pushed);
12612
13129
  const updated = pushed.sidecar.suggestions.find((candidate) => candidate.id === suggestionId);
12613
13130
  return { suggestion: updated ?? suggestion, document: pushed };
@@ -12621,11 +13138,12 @@ async function remoteReject(root, record, parsed) {
12621
13138
  }
12622
13139
  for (let index = 0; index < PUSH_REBASE_ATTEMPTS; index += 1) {
12623
13140
  if (index > 0) document = await fetchDocument(record, document.docId);
13141
+ documentRecord = rootScopedRecordFor(record, document.rootId);
12624
13142
  const io = new RemoteDocumentIO(document);
12625
13143
  const rejected = await rejectSuggestion(io, suggestionId, actor);
12626
13144
  const state = await getDocumentState(io, document.docId);
12627
13145
  try {
12628
- const pushed = await pushDocument(record, document, state.markdown, state.sidecar);
13146
+ const pushed = await pushDocument(documentRecord, document, state.markdown, state.sidecar);
12629
13147
  await recordHead(root, record, pushed);
12630
13148
  return { suggestion: rejected, document: pushed };
12631
13149
  } catch (error) {
@@ -12634,7 +13152,7 @@ async function remoteReject(root, record, parsed) {
12634
13152
  }
12635
13153
  }
12636
13154
  throw new CliError("conflict", "Remote change was not accepted after retries.", {
12637
- hint: "The document is changing rapidly. Refetch with mdocs remote context --json and retry."
13155
+ hint: "The document is changing rapidly. Refetch with mdocs remote context --summary --json, then retry against the needed content range."
12638
13156
  });
12639
13157
  }
12640
13158
  function isWithdrawalUnsupported(error) {
@@ -12650,7 +13168,8 @@ async function submitReview(root, record, pathOrDocId, mutate) {
12650
13168
  });
12651
13169
  }
12652
13170
  let document = await fetchDocument(record, pathOrDocId);
12653
- await refreshPresence(record, document.docId);
13171
+ let documentRecord = rootScopedRecordFor(record, document.rootId);
13172
+ await refreshPresence(documentRecord, document.docId);
12654
13173
  const build = async (base) => {
12655
13174
  const io = new RemoteDocumentIO(base);
12656
13175
  const created = await mutate(io, base.docId);
@@ -12667,7 +13186,7 @@ async function submitReview(root, record, pathOrDocId, mutate) {
12667
13186
  };
12668
13187
  let attempt = await build(document);
12669
13188
  try {
12670
- const pushed = await postReview(record, document.docId, attempt.additions, actorForRecord(record));
13189
+ const pushed = await postReview(documentRecord, document.docId, attempt.additions, actorForRecord(record));
12671
13190
  await recordHead(root, record, pushed);
12672
13191
  return { created: attempt.created, document: pushed };
12673
13192
  } catch (error) {
@@ -12676,10 +13195,11 @@ async function submitReview(root, record, pathOrDocId, mutate) {
12676
13195
  for (let index = 0; index < PUSH_REBASE_ATTEMPTS; index += 1) {
12677
13196
  if (index > 0) {
12678
13197
  document = await fetchDocument(record, document.docId);
13198
+ documentRecord = rootScopedRecordFor(record, document.rootId);
12679
13199
  attempt = await build(document);
12680
13200
  }
12681
13201
  try {
12682
- const pushed = await pushDocument(record, document, attempt.state.markdown, attempt.state.sidecar);
13202
+ const pushed = await pushDocument(documentRecord, document, attempt.state.markdown, attempt.state.sidecar);
12683
13203
  await recordHead(root, record, pushed);
12684
13204
  return { created: attempt.created, document: pushed };
12685
13205
  } catch (error) {
@@ -12688,7 +13208,7 @@ async function submitReview(root, record, pathOrDocId, mutate) {
12688
13208
  }
12689
13209
  }
12690
13210
  throw new CliError("conflict", "Remote change was not accepted after retries.", {
12691
- hint: "The document is changing rapidly. Refetch with mdocs remote context --json and retry."
13211
+ hint: "The document is changing rapidly. Refetch with mdocs remote context --summary --json, then retry against the needed content range."
12692
13212
  });
12693
13213
  }
12694
13214
  function isReviewEndpointUnsupported(error) {
@@ -12703,8 +13223,9 @@ async function recordHead(root, record, document) {
12703
13223
  }
12704
13224
  async function remoteEvents(root, record, flags) {
12705
13225
  const after = typeof flags.after === "string" ? Number(flags.after) : record.lastSeenEventId;
12706
- await refreshPresence(record, record.docId);
12707
- const response = await fetchPendingEvents(record, after);
13226
+ const rootRecord = assertRootScoped(record, "events");
13227
+ await refreshPresence(rootRecord, record.docId);
13228
+ const response = await fetchPendingEvents(rootRecord, after);
12708
13229
  await writeJoinRecord(root, {
12709
13230
  ...record,
12710
13231
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -12713,7 +13234,7 @@ async function remoteEvents(root, record, flags) {
12713
13234
  if (response.truncated) {
12714
13235
  return {
12715
13236
  ...response,
12716
- hint: "Older events were dropped from the bounded event log. Refetch full state with mdocs remote context --json instead of relying on the event stream."
13237
+ hint: "Older events were dropped from the bounded event log. Refetch with mdocs remote context --summary --json, then read needed content pages and review state."
12717
13238
  };
12718
13239
  }
12719
13240
  return response;
@@ -12722,11 +13243,11 @@ function parseShareUrl(value, flags) {
12722
13243
  const url = new URL(value);
12723
13244
  const route = parseSharePath(url.pathname);
12724
13245
  if (!route) {
12725
- throw new CliError("usage_error", "Share URL must be a Magic Markdown /share/<workspace>/<root>[/doc] URL.", {
13246
+ throw new CliError("usage_error", "Share URL must be a Magic Markdown /share/<workspace>[/<root>[/doc]] URL.", {
12726
13247
  hint: "Copy the share link from the Magic Markdown share dialog."
12727
13248
  });
12728
13249
  }
12729
- const scope = flags.scope === "project" || !route.docId && flags.scope !== "file" ? "project" : "file";
13250
+ const scope = flags.scope === "workspace" || !route.rootId ? "workspace" : flags.scope === "project" || !route.docId && flags.scope !== "file" ? "project" : "file";
12730
13251
  const explicitDoc = typeof flags.doc === "string" ? flags.doc : typeof flags["doc-id"] === "string" ? flags["doc-id"] : void 0;
12731
13252
  return {
12732
13253
  joinId: joinIdFor(value),
@@ -12734,8 +13255,8 @@ function parseShareUrl(value, flags) {
12734
13255
  shareUrl: value,
12735
13256
  origin: url.origin,
12736
13257
  workspaceId: route.workspaceId,
12737
- rootId: route.rootId,
12738
- docId: explicitDoc ?? route.docId,
13258
+ rootId: scope === "workspace" ? void 0 : route.rootId,
13259
+ docId: scope === "workspace" ? void 0 : explicitDoc ?? route.docId,
12739
13260
  agentId: "",
12740
13261
  agentName: "",
12741
13262
  joinedAt: "",
@@ -12745,7 +13266,11 @@ function parseShareUrl(value, flags) {
12745
13266
  }
12746
13267
  function shareUrlForScope(value, record) {
12747
13268
  const url = new URL(value);
12748
- url.pathname = buildSharePath(record.workspaceId, record.rootId, record.scope === "file" ? record.docId : void 0);
13269
+ url.pathname = buildSharePath(
13270
+ record.workspaceId,
13271
+ record.scope === "workspace" ? void 0 : record.rootId,
13272
+ record.scope === "file" ? record.docId : void 0
13273
+ );
12749
13274
  return url.toString();
12750
13275
  }
12751
13276
  function joinSummary(record, document) {
@@ -12756,16 +13281,19 @@ function joinSummary(record, document) {
12756
13281
  workspaceId: record.workspaceId,
12757
13282
  rootId: record.rootId,
12758
13283
  docId: record.docId,
12759
- currentHead: document.currentSha,
12760
- document: {
12761
- docId: document.docId,
12762
- path: document.path,
12763
- title: document.title
12764
- },
13284
+ currentHead: document?.currentSha,
13285
+ ...document ? {
13286
+ document: {
13287
+ docId: document.docId,
13288
+ path: document.path,
13289
+ title: document.title
13290
+ }
13291
+ } : {},
12765
13292
  nextCommands: [
12766
- "mdocs remote context --json",
13293
+ record.scope === "workspace" ? "mdocs remote map --json" : "mdocs remote context --summary --json",
12767
13294
  record.scope === "project" ? "mdocs remote map --json" : void 0,
12768
- "mdocs remote events --json"
13295
+ record.scope === "workspace" ? "mdocs remote library --json" : void 0,
13296
+ record.scope === "workspace" ? void 0 : "mdocs remote events --json"
12769
13297
  ].filter(Boolean)
12770
13298
  };
12771
13299
  }
@@ -12775,6 +13303,22 @@ function assertProjectScope(record, command) {
12775
13303
  hint: "Rejoin the share with `mdocs join <share-url> --scope project --json`, then retry."
12776
13304
  });
12777
13305
  }
13306
+ function assertRootScoped(record, command) {
13307
+ if (record.rootId) return rootScopedRecordFor(record, record.rootId);
13308
+ throw new CliError("usage_error", `remote ${command} needs a specific project root.`, {
13309
+ hint: "For Home workspace joins, run `mdocs remote map --json` and use a document target, or join a project share for root organization commands."
13310
+ });
13311
+ }
13312
+ function rootScopedRecord(record) {
13313
+ return record.rootId ? rootScopedRecordFor(record, record.rootId) : void 0;
13314
+ }
13315
+ function rootScopedRecordFor(record, rootId) {
13316
+ return {
13317
+ ...record,
13318
+ rootId,
13319
+ scope: record.scope === "file" ? "file" : "project"
13320
+ };
13321
+ }
12778
13322
  function emptySidecar(docId, path, title) {
12779
13323
  return {
12780
13324
  schemaVersion: SIDECAR_SCHEMA_VERSION,
@@ -13255,18 +13799,13 @@ async function main() {
13255
13799
  }
13256
13800
  case "context": {
13257
13801
  const state = await getDocumentState(io, requiredArg(parsed.command[1], "path"));
13802
+ if (parsed.flags.summary || parsed.flags["metadata-only"]) {
13803
+ print(summarizeDocumentContext(state), parsed.flags);
13804
+ return;
13805
+ }
13258
13806
  const startLine = parsed.flags["start-line"] !== void 0 ? Number(parsed.flags["start-line"]) : void 0;
13259
13807
  const endLine = parsed.flags["end-line"] !== void 0 ? Number(parsed.flags["end-line"]) : void 0;
13260
- const lines = state.markdown.split("\n");
13261
- const totalLines = lines.length;
13262
- const sl = startLine ?? 1;
13263
- const el = endLine ?? totalLines;
13264
- if (sl < 1 || el > totalLines || sl > el) {
13265
- throw new CliError("invalid_range", `Line range ${sl}:${el} is out of bounds for a ${totalLines}-line document.`, {
13266
- hint: `Use --start-line and --end-line with 1-based line numbers within 1:${totalLines}.`
13267
- });
13268
- }
13269
- const markdown = startLine !== void 0 || endLine !== void 0 ? lines.slice(sl - 1, el).join("\n") : state.markdown;
13808
+ const { markdown, totalLines, startLine: sl, endLine: el } = sliceMarkdown(state.markdown, startLine, endLine);
13270
13809
  print({
13271
13810
  docId: state.docId,
13272
13811
  path: state.path,
@@ -13280,12 +13819,19 @@ async function main() {
13280
13819
  ...image2.workspacePath ? { absolutePath: resolve5(cwd, image2.workspacePath) } : {}
13281
13820
  })),
13282
13821
  links: state.links,
13283
- comments: state.sidecar.comments.filter((comment2) => comment2.status === "open"),
13284
- suggestions: state.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
13285
- anchors: state.anchors
13822
+ ...parsed.flags["no-review"] ? {} : {
13823
+ comments: state.sidecar.comments.filter((comment2) => comment2.status === "open"),
13824
+ suggestions: state.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
13825
+ anchors: state.anchors
13826
+ }
13286
13827
  }, parsed.flags);
13287
13828
  return;
13288
13829
  }
13830
+ case "review": {
13831
+ const state = await getDocumentState(io, requiredArg(parsed.command[1], "path"));
13832
+ print(reviewStateForDocument(state), parsed.flags);
13833
+ return;
13834
+ }
13289
13835
  case "comments": {
13290
13836
  const state = await getDocumentState(io, requiredArg(parsed.command[1], "path"));
13291
13837
  print(state.sidecar.comments, parsed.flags);
@@ -13546,6 +14092,8 @@ Commands:
13546
14092
  graph --json Print workspace knowledge graph nodes and edges
13547
14093
  state <path> --json Print merged Markdown and sidecar state
13548
14094
  context <path> --json Print agent-ready context
14095
+ context <path> --summary --json Print document metadata and headings
14096
+ review <path> --json Print open comments, suggestions, anchors
13549
14097
  comments <path> --json List comments
13550
14098
  comment <path> --range 3:5 --body Add a sidecar comment
13551
14099
  suggestions <path> --json List suggestions
@@ -13563,7 +14111,7 @@ Commands:
13563
14111
  checkpoint create|list|restore Manage local reversible checkpoints
13564
14112
  join <share-url> --json Join a Magic Markdown share through the CLI
13565
14113
  joins --json List saved Magic Markdown remote joins
13566
- remote map|graph|context|create-file|move-file
14114
+ remote map|graph|context|review|create-file|move-file
13567
14115
  Work with documents in the active remote join
13568
14116
  remote comment|suggest Add remote review comments and suggestions
13569
14117
  remote reject <suggestionId> Withdraw a suggestion you submitted remotely