@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.
Files changed (3) hide show
  1. package/README.md +6 -2
  2. package/dist/index.js +763 -152
  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);
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 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.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 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
  {
@@ -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: ["--url (or MDOCS_BASE_URL) is required; the bridge never assumes a localhost server."]
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. To organize a shared project, rejoin with \`--scope project\` and use \`mdocs remote library --json\`.
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 full state with \`mdocs remote context --json\`.
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, 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.",
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(record, record.docId);
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(record, pathOrDocId);
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(record);
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(record, docId);
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 docId = parsedShare.docId ?? await firstDocIdForProject(parsedShare);
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(parsedShare, shareUrl, docId, agentId, agentName);
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.document.currentSha
12722
+ currentHead: state?.document.currentSha
12269
12723
  };
12270
12724
  await writeJoinRecord(root, record);
12271
- return joinSummary(record, state.document);
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 read the joined document."
12737
+ hint: "Use `mdocs remote context --summary --json` to inspect the joined document."
12284
12738
  });
12285
12739
  }
12286
- await refreshPresence(record, record.docId);
12287
- return fetchTree(record);
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
- await refreshPresence(record, record.docId);
12311
- return fetchLibrary(record);
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
- 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)));
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
- await refreshPresence(record, record.docId);
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(record, limit);
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(record, headId, docId);
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 docId = record.docId ?? await firstDocIdForProject(record);
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(record, shareUrl, docId, record.agentId, record.agentName);
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.document.currentSha
12878
+ currentHead: state?.document.currentSha
12387
12879
  };
12388
12880
  await writeJoinRecord(root, next);
12389
- return joinSummary(next, state.document);
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 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;
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
- comments: document.sidecar.comments.filter((comment2) => comment2.status === "open"),
12431
- suggestions: document.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
12432
- anchors: document.anchors,
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(record);
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: record.workspaceId,
12459
- rootId: record.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(record, baseDocument, markdown, sidecar);
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(record);
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(record, record.docId);
12530
- return postLibraryFolder(record, { name, ...parentId ? { parentId } : {} }, actorForRecord(record));
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(record, record.docId);
12550
- return patchLibraryFolder(record, folderId, input, actorForRecord(record));
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(record, record.docId);
12561
- return postMoveRoot(record, folderId, actorForRecord(record));
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(record, record.docId);
12579
- return postFolderInvite(record, folderId, { email, role }, actorForRecord(record));
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 context --json, then run mdocs remote reject <suggestionId>."
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
- await refreshPresence(record, document.docId);
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 context --json."
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(record, document.docId, additions, actor);
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(record, document, state.markdown, state.sidecar);
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 and retry."
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
- await refreshPresence(record, document.docId);
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(record, document.docId, attempt.additions, actorForRecord(record));
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(record, document, attempt.state.markdown, attempt.state.sidecar);
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 and retry."
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
- await refreshPresence(record, record.docId);
12707
- const response = await fetchPendingEvents(record, after);
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 full state with mdocs remote context --json instead of relying on the event stream."
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>/<root>[/doc] URL.", {
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(record.workspaceId, record.rootId, record.scope === "file" ? record.docId : void 0);
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.currentSha,
12760
- document: {
12761
- docId: document.docId,
12762
- path: document.path,
12763
- title: document.title
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 events --json"
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 = options.token ? await fetchScopedRoot(options, rootId) : await registerRoot(options, root, rootId, mapping);
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 (options.token) url.searchParams.set("token", options.token);
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(options.token) }
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 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;
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
- comments: state.sidecar.comments.filter((comment2) => comment2.status === "open"),
13284
- suggestions: state.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
13285
- anchors: state.anchors
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
- (--token from the web "Bind agent filesystem"
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