@magic-markdown/cli 0.3.20 → 0.3.24

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 (2) hide show
  1. package/dist/index.js +1128 -1639
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -270,13 +270,11 @@ var require_punycode = __commonJS({
270
270
  });
271
271
 
272
272
  // src/index.ts
273
- import { readFile as readFile8 } from "node:fs/promises";
274
273
  import { resolve as resolve6 } from "node:path";
275
274
 
276
275
  // ../core/src/types.ts
277
- var SIDECAR_SCHEMA_VERSION = 1;
276
+ var WORKSPACE_SCHEMA_VERSION = 1;
278
277
  var MANIFEST_PATH = ".mdocs/manifest.json";
279
- var SIDECAR_DIR = ".mdocs/docs";
280
278
  var CHECKPOINT_DIR = ".mdocs/checkpoints";
281
279
 
282
280
  // ../core/src/errors.ts
@@ -295,6 +293,7 @@ function isMdocsError(error) {
295
293
  }
296
294
 
297
295
  // ../core/src/ids.ts
296
+ import { createHash } from "node:crypto";
298
297
  function createId(prefix) {
299
298
  const id = globalThis.crypto?.randomUUID?.() ?? fallbackRandomId();
300
299
  return `${prefix}_${id.replaceAll("-", "").slice(0, 24)}`;
@@ -302,6 +301,11 @@ function createId(prefix) {
302
301
  function createDocId() {
303
302
  return createId("doc");
304
303
  }
304
+ var DOC_ID_SEPARATOR = "\0";
305
+ function deterministicDocId(rootId, path) {
306
+ const digest = createHash("sha256").update(`${rootId}${DOC_ID_SEPARATOR}${path}`).digest("hex");
307
+ return `doc_${digest.slice(0, 16)}`;
308
+ }
305
309
  function contentHashForText(value) {
306
310
  return `text_${fnv1a(value).toString(16).padStart(16, "0")}`;
307
311
  }
@@ -334,10 +338,10 @@ function parseWikilinkBody(value) {
334
338
 
335
339
  // ../core/src/markdown.ts
336
340
  var POLLUTING_PATTERNS = [
337
- { name: "CriticMarkup addition", pattern: /\{\+\+[\s\S]*?\+\+\}/ },
338
- { name: "CriticMarkup deletion", pattern: /\{--[\s\S]*?--\}/ },
339
- { name: "CriticMarkup substitution", pattern: /\{~~[\s\S]*?~~\}/ },
340
- { name: "CriticMarkup comment", pattern: /\{>>[\s\S]*?<<\}/ },
341
+ { name: "inline review addition marker", pattern: /\{\+\+[\s\S]*?\+\+\}/ },
342
+ { name: "inline review deletion marker", pattern: /\{--[\s\S]*?--\}/ },
343
+ { name: "inline review substitution marker", pattern: /\{~~[\s\S]*?~~\}/ },
344
+ { name: "inline review comment marker", pattern: /\{>>[\s\S]*?<<\}/ },
341
345
  { name: "mdocs HTML marker", pattern: /<!--\s*mdocs:/i },
342
346
  { name: "mdocs suggestion marker", pattern: /<!--\s*\/?\s*mdocs-suggestion\b/i },
343
347
  { name: "mdocs directive", pattern: /:{1,3}(?:comment|suggestion|anchor)\b/i }
@@ -355,7 +359,7 @@ function assertCleanMarkdown(markdown) {
355
359
  throw new MdocsError(
356
360
  "validation_error",
357
361
  `Markdown contains collaboration markup: ${names}`,
358
- "Keep Markdown clean; record comments and suggestions through the sidecar commands instead of inline markers."
362
+ "Keep Markdown clean; record comments and suggestions through the document authority instead of inline markers."
359
363
  );
360
364
  }
361
365
  }
@@ -368,10 +372,6 @@ function titleFromMarkdown(path, markdown) {
368
372
  function getLines(markdown) {
369
373
  return markdown.length === 0 ? [] : markdown.split(/\r?\n/);
370
374
  }
371
- function extractLineRange(markdown, range) {
372
- const lines = getLines(markdown);
373
- return lines.slice(range.startLine - 1, range.endLine).join("\n");
374
- }
375
375
  function assertLineRangeWithin(markdown, range) {
376
376
  if (!Number.isInteger(range.startLine) || !Number.isInteger(range.endLine) || range.startLine < 1 || range.endLine < range.startLine) {
377
377
  throw new MdocsError(
@@ -406,7 +406,7 @@ function replacementLinesForRange(replacement) {
406
406
  function extractMarkdownImages(markdown, documentPath) {
407
407
  const masked = maskCode(markdown);
408
408
  const referenceDefinitions = collectReferenceDefinitions(markdown, masked);
409
- const lineStarts2 = getLineStarts(markdown);
409
+ const lineStarts = getLineStarts(markdown);
410
410
  const images = [];
411
411
  let index = 0;
412
412
  while (index < masked.length) {
@@ -424,7 +424,7 @@ function extractMarkdownImages(markdown, documentPath) {
424
424
  }
425
425
  const alt = markdown.slice(labelOpen + 1, labelClose);
426
426
  const afterLabel = labelClose + 1;
427
- const position = positionAt(lineStarts2, start);
427
+ const position = positionAt(lineStarts, start);
428
428
  if (masked[afterLabel] === "(") {
429
429
  const destinationClose = findClosingParen(markdown, afterLabel + 1);
430
430
  if (destinationClose === -1) {
@@ -492,7 +492,7 @@ function extractMarkdownLinks(markdown) {
492
492
  const codeMasked = maskCode(markdown);
493
493
  const referenceDefinitions = collectReferenceDefinitions(markdown, codeMasked);
494
494
  const masked = maskReferenceDefinitions(markdown, codeMasked);
495
- const lineStarts2 = getLineStarts(markdown);
495
+ const lineStarts = getLineStarts(markdown);
496
496
  const links = [];
497
497
  let index = 0;
498
498
  while (index < masked.length) {
@@ -509,7 +509,7 @@ function extractMarkdownLinks(markdown) {
509
509
  }
510
510
  const label = markdown.slice(start + 1, labelClose);
511
511
  const afterLabel = labelClose + 1;
512
- const position = positionAt(lineStarts2, start);
512
+ const position = positionAt(lineStarts, start);
513
513
  if (masked[afterLabel] === "(") {
514
514
  const destinationClose = findClosingParen(markdown, afterLabel + 1);
515
515
  if (destinationClose === -1) {
@@ -565,7 +565,7 @@ function extractMarkdownLinks(markdown) {
565
565
  }
566
566
  index = labelClose + 1;
567
567
  }
568
- extractWikilinks(markdown, masked, lineStarts2).forEach((link2) => links.push(link2));
568
+ extractWikilinks(markdown, masked, lineStarts).forEach((link2) => links.push(link2));
569
569
  return links.sort((left, right) => left.line - right.line || left.column - right.column);
570
570
  }
571
571
  function toImageReference(image2) {
@@ -592,7 +592,7 @@ function toLinkReference(link2) {
592
592
  syntax: link2.syntax
593
593
  };
594
594
  }
595
- function extractWikilinks(markdown, masked, lineStarts2) {
595
+ function extractWikilinks(markdown, masked, lineStarts) {
596
596
  const links = [];
597
597
  let index = 0;
598
598
  while (index < masked.length) {
@@ -609,7 +609,7 @@ function extractWikilinks(markdown, masked, lineStarts2) {
609
609
  }
610
610
  const parsed = parseWikilinkBody(markdown.slice(start + 2, close2));
611
611
  if (parsed) {
612
- const position = positionAt(lineStarts2, start);
612
+ const position = positionAt(lineStarts, start);
613
613
  links.push({
614
614
  label: parsed.label ?? parsed.target,
615
615
  target: parsed.target,
@@ -889,12 +889,12 @@ function getLineStarts(value) {
889
889
  }
890
890
  return starts;
891
891
  }
892
- function positionAt(lineStarts2, index) {
892
+ function positionAt(lineStarts, index) {
893
893
  let low = 0;
894
- let high = lineStarts2.length - 1;
894
+ let high = lineStarts.length - 1;
895
895
  while (low <= high) {
896
896
  const middle = Math.floor((low + high) / 2);
897
- const start = lineStarts2[middle] ?? 0;
897
+ const start = lineStarts[middle] ?? 0;
898
898
  if (start <= index) {
899
899
  low = middle + 1;
900
900
  } else {
@@ -904,533 +904,10 @@ function positionAt(lineStarts2, index) {
904
904
  const lineIndex = Math.max(0, high);
905
905
  return {
906
906
  line: lineIndex + 1,
907
- column: index - (lineStarts2[lineIndex] ?? 0) + 1
907
+ column: index - (lineStarts[lineIndex] ?? 0) + 1
908
908
  };
909
909
  }
910
910
 
911
- // ../core/src/anchors.ts
912
- function selectorFromRange(markdown, range) {
913
- const lines = getLines(markdown);
914
- const selected = lines.slice(range.startLine - 1, range.endLine).join("\n");
915
- const prefix = lines.slice(Math.max(0, range.startLine - 4), range.startLine - 1).join("\n");
916
- const suffix = lines.slice(range.endLine, range.endLine + 3).join("\n");
917
- return { quote: selected, prefix, suffix };
918
- }
919
- function remapAnchor(markdown, anchor) {
920
- const lines = getLines(markdown);
921
- const quoteLines = anchor.selector.quote.split(/\r?\n/);
922
- const candidates = findQuoteCandidates(lines, quoteLines);
923
- const best = bestCandidate(lines, candidates, anchor.selector);
924
- if (!best) {
925
- return {
926
- ...anchor,
927
- status: "needs_review",
928
- confidence: 0,
929
- updatedAt: nowIso()
930
- };
931
- }
932
- const confidence = scoreContext(lines, best.startLine, best.endLine, anchor.selector);
933
- return {
934
- ...anchor,
935
- status: confidence >= 0.65 ? "mapped" : "needs_review",
936
- confidence,
937
- range: best,
938
- updatedAt: nowIso()
939
- };
940
- }
941
- function remapAnchors(markdown, anchors) {
942
- return anchors.map((anchor) => remapAnchor(markdown, anchor));
943
- }
944
- function findQuoteCandidates(lines, quoteLines) {
945
- if (quoteLines.length === 0) return [];
946
- const ranges = [];
947
- const quoteText = quoteLines.join("\n");
948
- for (let index = 0; index <= lines.length - quoteLines.length; index += 1) {
949
- const candidate = lines.slice(index, index + quoteLines.length);
950
- if (candidate.join("\n") === quoteText) ranges.push({ startLine: index + 1, endLine: index + quoteLines.length });
951
- }
952
- return ranges;
953
- }
954
- function bestCandidate(lines, candidates, selector) {
955
- if (candidates.length <= 1) return candidates[0];
956
- const scored = candidates.map((range) => ({ range, score: scoreContext(lines, range.startLine, range.endLine, selector) })).sort((left, right) => right.score - left.score);
957
- const best = scored[0];
958
- if (!best) return void 0;
959
- const tied = scored.filter((candidate) => candidate.score === best.score);
960
- return tied.length === 1 ? best.range : void 0;
961
- }
962
- function scoreContext(lines, startLine, endLine, selector) {
963
- let score = 0.7;
964
- const expectedPrefix = contextPrefix(selector.prefix);
965
- if (expectedPrefix) {
966
- const actualPrefix = contextPrefix(lines.slice(Math.max(0, startLine - 4), startLine - 1).join("\n"));
967
- if (expectedPrefix && actualPrefix.endsWith(expectedPrefix)) score += 0.15;
968
- }
969
- const expectedSuffix = contextSuffix(selector.suffix);
970
- if (expectedSuffix) {
971
- const actualSuffix = contextSuffix(lines.slice(endLine, endLine + 3).join("\n"));
972
- if (expectedSuffix && actualSuffix.startsWith(expectedSuffix)) score += 0.15;
973
- }
974
- return Math.min(1, score);
975
- }
976
- function contextPrefix(value) {
977
- return value?.split(/\r?\n/).filter(Boolean).slice(-3).join("\n") ?? "";
978
- }
979
- function contextSuffix(value) {
980
- return value?.split(/\r?\n/).filter(Boolean).slice(0, 3).join("\n") ?? "";
981
- }
982
-
983
- // ../core/src/review-markdown.ts
984
- var START_MARKER = "<!--mdocs-suggestion";
985
- var END_MARKER = "<!--/mdocs-suggestion-->";
986
- function parseReviewMarkdown(reviewMarkdown) {
987
- const segments = [];
988
- const suggestions = [];
989
- let cursor = 0;
990
- let canonicalOffset = 0;
991
- while (cursor < reviewMarkdown.length) {
992
- const markerStart = reviewMarkdown.indexOf(START_MARKER, cursor);
993
- if (markerStart === -1) {
994
- appendTextSegment(reviewMarkdown, segments, cursor, reviewMarkdown.length, canonicalOffset);
995
- break;
996
- }
997
- appendTextSegment(reviewMarkdown, segments, cursor, markerStart, canonicalOffset);
998
- canonicalOffset += markerStart - cursor;
999
- const startClose = reviewMarkdown.indexOf("-->", markerStart);
1000
- if (startClose === -1) throw invalidReviewMarkdown("Unclosed mdocs suggestion marker.");
1001
- const attrs2 = parseMarkerAttributes(reviewMarkdown.slice(markerStart + START_MARKER.length, startClose));
1002
- if (!attrs2.id) throw invalidReviewMarkdown("Suggestion marker is missing an id.");
1003
- if (attrs2.kind !== "insert" && attrs2.kind !== "delete" && attrs2.kind !== "replace") {
1004
- throw invalidReviewMarkdown(`Suggestion ${attrs2.id} has an invalid kind.`);
1005
- }
1006
- const bodyStart = startClose + 3;
1007
- const markerEnd = reviewMarkdown.indexOf(END_MARKER, bodyStart);
1008
- if (markerEnd === -1) throw invalidReviewMarkdown(`Suggestion ${attrs2.id} is missing its closing marker.`);
1009
- const body = reviewMarkdown.slice(bodyStart, markerEnd);
1010
- if (body.includes(START_MARKER)) throw invalidReviewMarkdown(`Suggestion ${attrs2.id} contains a nested suggestion.`);
1011
- const parsedBody = parseSuggestionBody(attrs2.kind, body, attrs2.id);
1012
- const token = {
1013
- id: attrs2.id,
1014
- kind: attrs2.kind,
1015
- form: parsedBody.before.includes("\n") || parsedBody.after.includes("\n") ? "block" : "inline",
1016
- author: actorFromAttributes(attrs2),
1017
- message: attrs2.message,
1018
- createdAt: attrs2.createdAt,
1019
- updatedAt: attrs2.updatedAt,
1020
- before: parsedBody.before,
1021
- after: parsedBody.after,
1022
- markerStart,
1023
- markerEnd: markerEnd + END_MARKER.length,
1024
- canonicalStart: canonicalOffset,
1025
- canonicalEnd: canonicalOffset + parsedBody.before.length,
1026
- raw: reviewMarkdown.slice(markerStart, markerEnd + END_MARKER.length)
1027
- };
1028
- segments.push({ type: "suggestion", token });
1029
- suggestions.push(token);
1030
- canonicalOffset += token.before.length;
1031
- cursor = token.markerEnd;
1032
- }
1033
- return {
1034
- reviewMarkdown,
1035
- canonicalMarkdown: canonicalFromSegments(segments, statusLookup(void 0)),
1036
- segments,
1037
- suggestions
1038
- };
1039
- }
1040
- function projectCanonicalMarkdown(reviewMarkdown, sidecarStatuses) {
1041
- return canonicalFromSegments(parseReviewMarkdown(reviewMarkdown).segments, statusLookup(sidecarStatuses));
1042
- }
1043
- function deriveSidecarFromReviewMarkdown(sidecar, reviewMarkdown, options) {
1044
- const parsed = parseReviewMarkdown(reviewMarkdown);
1045
- const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
1046
- const existingSuggestions = new Map(sidecar.suggestions.map((suggestion) => [suggestion.id, suggestion]));
1047
- const existingAnchors = new Map(sidecar.anchors.map((anchor) => [anchor.id, anchor]));
1048
- const anchors = sidecar.anchors.filter((anchor) => anchor.kind !== "suggestion");
1049
- const suggestions = [];
1050
- const base = { contentHash: contentHashForText(parsed.canonicalMarkdown), ...options.baseHead ? { head: options.baseHead } : {} };
1051
- for (const token of parsed.suggestions) {
1052
- const existing = existingSuggestions.get(token.id);
1053
- const anchorId = existing?.anchorId ?? stableAnchorIdForSuggestion(token.id);
1054
- const range = lineRangeForCanonicalSpan(parsed.canonicalMarkdown, token.canonicalStart, token.canonicalEnd);
1055
- const selector = selectorForCanonicalSpan(parsed.canonicalMarkdown, token.canonicalStart, token.canonicalEnd);
1056
- const anchor = {
1057
- ...existingAnchors.get(anchorId) ?? {},
1058
- id: anchorId,
1059
- kind: "suggestion",
1060
- status: "mapped",
1061
- selector,
1062
- range,
1063
- confidence: 1,
1064
- updatedAt: now
1065
- };
1066
- const author = token.author ?? existing?.author ?? options.author;
1067
- const message = token.message ?? existing?.message ?? "Suggested edit";
1068
- const suggestion = {
1069
- ...existing ?? {},
1070
- id: token.id,
1071
- anchorId,
1072
- status: "open",
1073
- kind: token.kind,
1074
- author,
1075
- message,
1076
- patch: {
1077
- type: "replace_lines",
1078
- range,
1079
- before: token.before,
1080
- after: token.after
1081
- },
1082
- base: existing?.base ?? base,
1083
- diff: {
1084
- before: token.before,
1085
- after: token.after,
1086
- summary: diffSummary(token.before, token.after)
1087
- },
1088
- projectionStatus: "clean",
1089
- reviewRange: { start: token.markerStart, end: token.markerEnd },
1090
- createdAt: token.createdAt ?? existing?.createdAt ?? now,
1091
- updatedAt: token.updatedAt ?? now
1092
- };
1093
- anchors.push(anchor);
1094
- suggestions.push(suggestion);
1095
- }
1096
- return {
1097
- ...sidecar,
1098
- reviewMarkdown,
1099
- anchors,
1100
- suggestions,
1101
- updatedAt: now
1102
- };
1103
- }
1104
- function createSuggestionMarkup(reviewMarkdown, patch, metadata) {
1105
- const parsed = parseReviewMarkdown(reviewMarkdown);
1106
- assertNoDuplicateSuggestionId(parsed, metadata.suggestionId);
1107
- const [contentStart, contentEnd] = lineRangeOffsets(parsed.canonicalMarkdown, patch.range);
1108
- const selected = parsed.canonicalMarkdown.slice(contentStart, contentEnd);
1109
- if (selected !== patch.before) {
1110
- throw new MdocsError(
1111
- "conflict",
1112
- "Suggestion target does not match the review document projection.",
1113
- "Re-read the document and create the suggestion against the current canonical text."
1114
- );
1115
- }
1116
- const [canonicalStart, canonicalEnd] = lineRangeReplacementOffsets(parsed.canonicalMarkdown, patch.range);
1117
- if (selectionTouchesSuggestion(parsed.suggestions, canonicalStart, canonicalEnd)) {
1118
- throw new MdocsError(
1119
- "conflict",
1120
- "Suggestion overlaps an unresolved suggestion.",
1121
- "Accept or reject the existing suggestion before creating another edit over the same text."
1122
- );
1123
- }
1124
- const rawStart = rawOffsetForCanonicalOffset(parsed.segments, canonicalStart);
1125
- const rawEnd = rawOffsetForCanonicalOffset(parsed.segments, canonicalEnd);
1126
- const before = parsed.canonicalMarkdown.slice(canonicalStart, canonicalEnd);
1127
- const after = replacementTextForSpan(parsed.canonicalMarkdown, patch, canonicalStart, canonicalEnd);
1128
- const marker = formatSuggestionMarker(metadata, kindForText(before, after), before, after);
1129
- const nextReviewMarkdown = `${reviewMarkdown.slice(0, rawStart)}${marker}${reviewMarkdown.slice(rawEnd)}`;
1130
- const token = parseReviewMarkdown(nextReviewMarkdown).suggestions.find((candidate) => candidate.id === metadata.suggestionId);
1131
- if (!token) throw invalidReviewMarkdown(`Failed to create suggestion ${metadata.suggestionId}.`);
1132
- return { reviewMarkdown: nextReviewMarkdown, token };
1133
- }
1134
- function projectReviewMarkdown(canonicalMarkdown, suggestions) {
1135
- let reviewMarkdown = canonicalMarkdown;
1136
- const open = suggestions.filter((suggestion) => suggestion.status === "open" && suggestion.patch?.type === "replace_lines").slice().sort((left, right) => left.patch.range.startLine - right.patch.range.startLine);
1137
- for (const suggestion of open) {
1138
- try {
1139
- reviewMarkdown = createSuggestionMarkup(reviewMarkdown, suggestion.patch, {
1140
- suggestionId: suggestion.id,
1141
- author: suggestion.author,
1142
- message: suggestion.message,
1143
- createdAt: suggestion.createdAt,
1144
- updatedAt: suggestion.updatedAt
1145
- }).reviewMarkdown;
1146
- } catch {
1147
- }
1148
- }
1149
- return reviewMarkdown;
1150
- }
1151
- function resolveSuggestionMarkup(reviewMarkdown, suggestionId, resolution) {
1152
- const parsed = parseReviewMarkdown(reviewMarkdown);
1153
- const token = parsed.suggestions.find((candidate) => candidate.id === suggestionId);
1154
- if (!token) {
1155
- throw new MdocsError("not_found", `Unknown suggestion: ${suggestionId}`, "Refresh review state and try again.");
1156
- }
1157
- const resolved = resolutionText(token, resolution);
1158
- return `${reviewMarkdown.slice(0, token.markerStart)}${resolved}${reviewMarkdown.slice(token.markerEnd)}`;
1159
- }
1160
- function escapeReviewSuggestionText(value) {
1161
- return value.replaceAll("\\", "\\\\").replaceAll("{++", "\\{++").replaceAll("++}", "\\++}").replaceAll("{--", "\\{--").replaceAll("--}", "\\--}").replaceAll("{~~", "\\{~~").replaceAll("~>", "\\~>").replaceAll("~~}", "\\~~}");
1162
- }
1163
- function lineRangeForCanonicalSpan(markdown, start, end) {
1164
- assertCanonicalSpan(markdown, start, end);
1165
- return lineRangeForOffsets(markdown, start, end);
1166
- }
1167
- function selectorForCanonicalSpan(markdown, start, end) {
1168
- assertCanonicalSpan(markdown, start, end);
1169
- const context = 160;
1170
- const prefix = markdown.slice(Math.max(0, start - context), start);
1171
- const suffix = markdown.slice(end, Math.min(markdown.length, end + context));
1172
- return {
1173
- quote: markdown.slice(start, end),
1174
- ...prefix ? { prefix } : {},
1175
- ...suffix ? { suffix } : {}
1176
- };
1177
- }
1178
- function appendTextSegment(source, segments, start, end, canonicalStart) {
1179
- if (end <= start) return;
1180
- segments.push({
1181
- type: "text",
1182
- text: source.slice(start, end),
1183
- start,
1184
- end,
1185
- canonicalStart,
1186
- canonicalEnd: canonicalStart + (end - start)
1187
- });
1188
- }
1189
- function canonicalFromSegments(segments, statuses) {
1190
- return segments.map((segment) => {
1191
- if (segment.type === "text") return segment.text;
1192
- const status = statuses(segment.token.id);
1193
- if (status === "accepted") return acceptedText(segment.token);
1194
- return originalText(segment.token);
1195
- }).join("");
1196
- }
1197
- function parseMarkerAttributes(raw) {
1198
- const attrs2 = {};
1199
- const pattern = /\s+([a-zA-Z][\w:-]*)="([^"]*)"/g;
1200
- let match2;
1201
- while ((match2 = pattern.exec(raw)) !== null) {
1202
- const key = match2[1];
1203
- if (key === "id" || key === "kind" || key === "authorId" || key === "authorKind" || key === "authorName" || key === "authorEmail" || key === "message" || key === "createdAt" || key === "updatedAt") {
1204
- attrs2[key] = decodeAttribute(match2[2] ?? "");
1205
- }
1206
- }
1207
- return attrs2;
1208
- }
1209
- function actorFromAttributes(attrs2) {
1210
- if (!attrs2.authorId || !attrs2.authorName) return void 0;
1211
- const kind = attrs2.authorKind === "agent" || attrs2.authorKind === "system" || attrs2.authorKind === "human" ? attrs2.authorKind : "human";
1212
- return {
1213
- id: attrs2.authorId,
1214
- kind,
1215
- name: attrs2.authorName,
1216
- ...attrs2.authorEmail ? { email: attrs2.authorEmail } : {}
1217
- };
1218
- }
1219
- function parseSuggestionBody(kind, body, id) {
1220
- if (kind === "insert") {
1221
- return { before: "", after: parseWrappedBody(body, "{++", "++}", id) };
1222
- }
1223
- if (kind === "delete") {
1224
- return { before: parseWrappedBody(body, "{--", "--}", id), after: "" };
1225
- }
1226
- if (!body.startsWith("{~~") || !body.endsWith("~~}")) {
1227
- throw invalidReviewMarkdown(`Replacement suggestion ${id} has invalid CriticMarkup payload.`);
1228
- }
1229
- const inner = body.slice(3, -3);
1230
- const separator = findUnescaped2(inner, "~>");
1231
- if (separator === -1) throw invalidReviewMarkdown(`Replacement suggestion ${id} is missing a separator.`);
1232
- return {
1233
- before: unescapeReviewSuggestionText(inner.slice(0, separator)),
1234
- after: unescapeReviewSuggestionText(inner.slice(separator + 2))
1235
- };
1236
- }
1237
- function parseWrappedBody(body, open, close2, id) {
1238
- if (!body.startsWith(open) || !body.endsWith(close2) || isEscapedAt(body, body.length - close2.length)) {
1239
- throw invalidReviewMarkdown(`Suggestion ${id} has invalid CriticMarkup payload.`);
1240
- }
1241
- return unescapeReviewSuggestionText(body.slice(open.length, -close2.length));
1242
- }
1243
- function formatSuggestionMarker(metadata, kind, before, after) {
1244
- const attrs2 = [
1245
- ["id", metadata.suggestionId],
1246
- ["kind", kind]
1247
- ];
1248
- if (metadata.author) {
1249
- attrs2.push(["authorId", metadata.author.id]);
1250
- attrs2.push(["authorKind", metadata.author.kind]);
1251
- attrs2.push(["authorName", metadata.author.name]);
1252
- if (metadata.author.email) attrs2.push(["authorEmail", metadata.author.email]);
1253
- }
1254
- if (metadata.message) attrs2.push(["message", metadata.message]);
1255
- if (metadata.createdAt) attrs2.push(["createdAt", metadata.createdAt]);
1256
- if (metadata.updatedAt) attrs2.push(["updatedAt", metadata.updatedAt]);
1257
- const attrText = attrs2.map(([key, value]) => `${key}="${encodeAttribute(value)}"`).join(" ");
1258
- const body = kind === "insert" ? `{++${escapeReviewSuggestionText(after)}++}` : kind === "delete" ? `{--${escapeReviewSuggestionText(before)}--}` : `{~~${escapeReviewSuggestionText(before)}~>${escapeReviewSuggestionText(after)}~~}`;
1259
- return `${START_MARKER} ${attrText} -->${body}${END_MARKER}`;
1260
- }
1261
- function stableAnchorIdForSuggestion(suggestionId) {
1262
- return `anchor_${suggestionId.replace(/[^a-zA-Z0-9_:-]/g, "_")}`;
1263
- }
1264
- function diffSummary(before, after) {
1265
- if (!before && after) return "Inserted text";
1266
- if (before && !after) return "Deleted text";
1267
- return "Replaced text";
1268
- }
1269
- function unescapeReviewSuggestionText(value) {
1270
- let result = "";
1271
- for (let index = 0; index < value.length; index += 1) {
1272
- if (value[index] !== "\\") {
1273
- result += value[index];
1274
- continue;
1275
- }
1276
- const next = value[index + 1];
1277
- if (next === void 0) {
1278
- result += "\\";
1279
- continue;
1280
- }
1281
- result += next;
1282
- index += 1;
1283
- }
1284
- return result;
1285
- }
1286
- function findUnescaped2(value, needle, from = 0) {
1287
- let index = value.indexOf(needle, from);
1288
- while (index !== -1) {
1289
- if (!isEscapedAt(value, index)) return index;
1290
- index = value.indexOf(needle, index + needle.length);
1291
- }
1292
- return -1;
1293
- }
1294
- function isEscapedAt(value, index) {
1295
- let slashes = 0;
1296
- for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1) slashes += 1;
1297
- return slashes % 2 === 1;
1298
- }
1299
- function statusLookup(statuses) {
1300
- if (!statuses) return () => "open";
1301
- if (isStatusMap(statuses)) return (suggestionId) => statuses.get(suggestionId) ?? "open";
1302
- if (Array.isArray(statuses)) {
1303
- const map = /* @__PURE__ */ new Map();
1304
- for (const item of statuses) {
1305
- const id = item.id ?? item.suggestionId;
1306
- if (id && item.status) map.set(id, item.status);
1307
- }
1308
- return (suggestionId) => map.get(suggestionId) ?? "open";
1309
- }
1310
- const record = statuses;
1311
- return (suggestionId) => record[suggestionId] ?? "open";
1312
- }
1313
- function isStatusMap(value) {
1314
- return typeof value.get === "function";
1315
- }
1316
- function acceptedText(token) {
1317
- return token.kind === "delete" ? "" : token.after;
1318
- }
1319
- function originalText(token) {
1320
- return token.kind === "insert" ? "" : token.before;
1321
- }
1322
- function resolutionText(token, resolution) {
1323
- return resolution === "accept" ? acceptedText(token) : originalText(token);
1324
- }
1325
- function kindForText(before, after) {
1326
- if (before.length === 0) return "insert";
1327
- if (after.length === 0) return "delete";
1328
- return "replace";
1329
- }
1330
- function assertNoDuplicateSuggestionId(parsed, suggestionId) {
1331
- if (parsed.suggestions.some((suggestion) => suggestion.id === suggestionId)) {
1332
- throw new MdocsError("validation_error", `Duplicate suggestion id: ${suggestionId}`, "Suggestion ids must be stable and unique per document.");
1333
- }
1334
- }
1335
- function assertCanonicalSpan(markdown, start, end) {
1336
- if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end > markdown.length) {
1337
- throw new MdocsError("invalid_range", `Invalid canonical span ${start}:${end}.`, "Re-read the document and try again.");
1338
- }
1339
- }
1340
- function selectionTouchesSuggestion(tokens, start, end) {
1341
- return tokens.some((token) => {
1342
- if (token.canonicalStart === token.canonicalEnd) return start <= token.canonicalStart && end >= token.canonicalEnd;
1343
- return start < token.canonicalEnd && end > token.canonicalStart;
1344
- });
1345
- }
1346
- function rawOffsetForCanonicalOffset(segments, offset) {
1347
- for (const segment of segments) {
1348
- if (segment.type === "text") {
1349
- if (offset >= segment.canonicalStart && offset <= segment.canonicalEnd) {
1350
- return segment.start + (offset - segment.canonicalStart);
1351
- }
1352
- continue;
1353
- }
1354
- const token = segment.token;
1355
- if (offset > token.canonicalStart && offset < token.canonicalEnd) {
1356
- throw new MdocsError(
1357
- "conflict",
1358
- "Selection falls inside an unresolved suggestion.",
1359
- "Accept or reject the existing suggestion before editing inside it."
1360
- );
1361
- }
1362
- }
1363
- if (offset === segmentsEndCanonicalOffset(segments)) return segmentsEndRawOffset(segments);
1364
- throw new MdocsError("invalid_range", "Could not map canonical offset into review Markdown.", "Re-read the document and try again.");
1365
- }
1366
- function segmentsEndCanonicalOffset(segments) {
1367
- const last = segments[segments.length - 1];
1368
- if (!last) return 0;
1369
- return last.type === "text" ? last.canonicalEnd : last.token.canonicalEnd;
1370
- }
1371
- function segmentsEndRawOffset(segments) {
1372
- const last = segments[segments.length - 1];
1373
- if (!last) return 0;
1374
- return last.type === "text" ? last.end : last.token.markerEnd;
1375
- }
1376
- function lineRangeOffsets(markdown, range) {
1377
- const lines = getLines(markdown);
1378
- if (range.startLine < 1 || range.endLine < range.startLine || range.endLine > lines.length) {
1379
- throw new MdocsError("invalid_range", `Invalid line range ${range.startLine}:${range.endLine}.`, "Re-read the document and try again.");
1380
- }
1381
- let start = 0;
1382
- for (let index = 0; index < range.startLine - 1; index += 1) start += lines[index].length + 1;
1383
- let end = start;
1384
- for (let index = range.startLine - 1; index < range.endLine; index += 1) {
1385
- if (index > range.startLine - 1) end += 1;
1386
- end += lines[index].length;
1387
- }
1388
- return [start, end];
1389
- }
1390
- function lineRangeReplacementOffsets(markdown, range) {
1391
- const [start, contentEnd] = lineRangeOffsets(markdown, range);
1392
- if (contentEnd < markdown.length && markdown[contentEnd] === "\n") return [start, contentEnd + 1];
1393
- return [start, contentEnd];
1394
- }
1395
- function replacementTextForSpan(markdown, patch, start, end) {
1396
- const applied = replaceLineRange(markdown, patch.range, patch.after);
1397
- const suffixLength = markdown.length - end;
1398
- const replacementEnd = applied.length - suffixLength;
1399
- if (replacementEnd < start) return "";
1400
- return applied.slice(start, replacementEnd);
1401
- }
1402
- function lineRangeForOffsets(markdown, start, end) {
1403
- const starts = lineStarts(markdown);
1404
- return {
1405
- startLine: lineNumberAt(starts, start),
1406
- endLine: lineNumberAt(starts, Math.max(start, end - 1))
1407
- };
1408
- }
1409
- function lineStarts(markdown) {
1410
- const starts = [0];
1411
- for (let index = 0; index < markdown.length; index += 1) {
1412
- if (markdown[index] === "\n") starts.push(index + 1);
1413
- }
1414
- return starts;
1415
- }
1416
- function lineNumberAt(starts, offset) {
1417
- let line = 1;
1418
- for (let index = 0; index < starts.length; index += 1) {
1419
- if (starts[index] <= offset) line = index + 1;
1420
- else break;
1421
- }
1422
- return line;
1423
- }
1424
- function encodeAttribute(value) {
1425
- return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1426
- }
1427
- function decodeAttribute(value) {
1428
- return value.replaceAll("&quot;", '"').replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
1429
- }
1430
- function invalidReviewMarkdown(message) {
1431
- return new MdocsError("validation_error", `Invalid review Markdown: ${message}`, "Repair or remove the malformed suggestion markup.");
1432
- }
1433
-
1434
911
  // ../core/src/graph.ts
1435
912
  function resolveMarkdownLinks(links, currentDocument, documents) {
1436
913
  return links.map((link2) => resolveMarkdownLink(link2, currentDocument, documents));
@@ -1609,23 +1086,16 @@ function safeDecode(value) {
1609
1086
  }
1610
1087
  }
1611
1088
 
1612
- // ../core/src/sidecar.ts
1613
- function defaultActor(name = "mdocs") {
1614
- return {
1615
- id: "actor_local",
1616
- kind: "human",
1617
- name
1618
- };
1619
- }
1089
+ // ../core/src/workspace-store.ts
1090
+ var cacheLookupSignatures = /* @__PURE__ */ new WeakMap();
1620
1091
  async function initWorkspace(io, workspaceId = createId("workspace")) {
1621
1092
  await io.mkdir(".mdocs");
1622
- await io.mkdir(SIDECAR_DIR);
1623
1093
  await io.mkdir(CHECKPOINT_DIR);
1624
1094
  const existing = await readManifestIfPresent(io);
1625
1095
  if (existing) return existing;
1626
1096
  const now = nowIso();
1627
1097
  const manifest = {
1628
- schemaVersion: SIDECAR_SCHEMA_VERSION,
1098
+ schemaVersion: WORKSPACE_SCHEMA_VERSION,
1629
1099
  workspaceId,
1630
1100
  createdAt: now,
1631
1101
  updatedAt: now,
@@ -1634,315 +1104,169 @@ async function initWorkspace(io, workspaceId = createId("workspace")) {
1634
1104
  await writeManifest(io, manifest);
1635
1105
  return manifest;
1636
1106
  }
1637
- async function indexWorkspace(io) {
1638
- const manifest = await initWorkspace(io);
1639
- const markdownFiles = await io.listMarkdownFiles();
1640
- const existingByPath = new Map(manifest.docs.map((doc) => [doc.path, doc]));
1641
- const markdownFileSet = new Set(markdownFiles);
1642
- const unmatchedExisting = manifest.docs.filter((doc) => !markdownFileSet.has(doc.path));
1643
- const claimedRenamedDocIds = /* @__PURE__ */ new Set();
1644
- const now = nowIso();
1645
- const docs = [];
1646
- const markdownByPath = /* @__PURE__ */ new Map();
1647
- const newPathCountByHash = /* @__PURE__ */ new Map();
1648
- for (const path of markdownFiles) {
1649
- const markdown = await io.readText(path);
1650
- assertCleanMarkdown(markdown);
1651
- const contentHash = contentHashForText(markdown);
1652
- markdownByPath.set(path, { contentHash, markdown });
1653
- newPathCountByHash.set(contentHash, (newPathCountByHash.get(contentHash) ?? 0) + 1);
1654
- }
1655
- for (const path of markdownFiles) {
1656
- const { contentHash, markdown } = markdownByPath.get(path);
1657
- const existing = existingByPath.get(path) ?? await findRenamedDocumentEntry(
1658
- io,
1659
- unmatchedExisting.filter((doc) => !claimedRenamedDocIds.has(doc.docId)),
1660
- markdown,
1661
- contentHash,
1662
- newPathCountByHash.get(contentHash) ?? 0
1663
- );
1664
- if (existing && existing.path !== path) claimedRenamedDocIds.add(existing.docId);
1665
- const docId = existing?.docId ?? createDocId();
1666
- const title = titleFromMarkdown(path, markdown);
1667
- const entry = {
1668
- docId,
1669
- path,
1670
- title,
1671
- contentHash,
1672
- currentSha: existing?.currentSha,
1673
- updatedAt: now
1674
- };
1675
- docs.push(entry);
1676
- await ensureDocumentSidecar(io, entry, markdown);
1677
- }
1678
- const next = {
1679
- ...manifest,
1680
- docs,
1681
- updatedAt: now
1682
- };
1683
- await writeManifest(io, next);
1684
- return next;
1107
+ async function indexWorkspace(io, options) {
1108
+ return (await computeWorkspaceIndex(io, options)).manifest;
1685
1109
  }
1686
- async function getWorkspaceMap(io) {
1687
- const manifest = await indexWorkspace(io);
1688
- const graph = await workspaceGraphForManifest(io, manifest);
1110
+ async function getWorkspaceMap(io, options) {
1111
+ const { manifest, graphDocuments } = await computeWorkspaceIndex(io, options);
1112
+ const graph = buildWorkspaceGraph(manifest.workspaceId, manifest.schemaVersion, graphDocuments);
1689
1113
  const graphNodesByDocId = new Map(graph.nodes.map((node) => [node.docId, node]));
1690
- const docs = await Promise.all(
1691
- manifest.docs.map(async (entry) => {
1692
- const sidecar = await readSidecar(io, entry.docId);
1114
+ return {
1115
+ workspaceId: manifest.workspaceId,
1116
+ schemaVersion: manifest.schemaVersion,
1117
+ docs: manifest.docs.map((entry) => {
1693
1118
  const graphNode = graphNodesByDocId.get(entry.docId);
1694
1119
  return {
1695
1120
  docId: entry.docId,
1696
1121
  path: entry.path,
1697
1122
  title: entry.title,
1698
- sidecarPath: sidecarPath(entry.docId),
1699
- openComments: sidecar.comments.filter((comment2) => comment2.status === "open").length,
1700
- openSuggestions: sidecar.suggestions.filter((suggestion) => suggestion.status === "open").length,
1701
- anchorsNeedingReview: sidecar.anchors.filter((anchor) => anchor.status !== "mapped").length,
1123
+ openComments: 0,
1124
+ openSuggestions: 0,
1125
+ anchorsNeedingReview: 0,
1702
1126
  outgoingLinks: graphNode?.outgoingLinks ?? 0,
1703
1127
  incomingLinks: graphNode?.incomingLinks ?? 0,
1704
1128
  unresolvedLinks: graphNode?.unresolvedLinks ?? 0,
1705
1129
  externalLinks: graphNode?.externalLinks ?? 0
1706
1130
  };
1707
1131
  })
1708
- );
1709
- return {
1710
- workspaceId: manifest.workspaceId,
1711
- schemaVersion: manifest.schemaVersion,
1712
- docs
1713
1132
  };
1714
1133
  }
1715
- async function getWorkspaceGraph(io) {
1716
- return workspaceGraphForManifest(io, await indexWorkspace(io));
1134
+ async function getWorkspaceGraph(io, options) {
1135
+ const { manifest, graphDocuments } = await computeWorkspaceIndex(io, options);
1136
+ return buildWorkspaceGraph(manifest.workspaceId, manifest.schemaVersion, graphDocuments);
1717
1137
  }
1718
- async function getDocumentState(io, pathOrDocId) {
1719
- const manifest = await indexWorkspace(io);
1138
+ async function getDocumentState(io, pathOrDocId, options) {
1139
+ const { manifest, graphDocuments, markdownByPath } = await computeWorkspaceIndex(io, options);
1720
1140
  const entry = findDoc(manifest, pathOrDocId);
1721
- const markdown = await io.readText(entry.path);
1141
+ const markdown = markdownByPath.get(entry.path) ?? await io.readText(entry.path);
1722
1142
  assertCleanMarkdown(markdown);
1723
- const sidecar = await readSidecar(io, entry.docId);
1724
- const remappedAnchors = remapAnchors(markdown, sidecar.anchors);
1725
- const nextSidecar = {
1726
- ...sidecar,
1727
- anchors: remappedAnchors,
1728
- updatedAt: nowIso()
1729
- };
1730
- await writeSidecar(io, nextSidecar);
1143
+ const source = graphDocuments.find((document) => document.docId === entry.docId);
1144
+ const links = source?.links ?? resolveMarkdownLinks(extractMarkdownLinks(markdown), entry, manifest.docs);
1731
1145
  return {
1732
1146
  docId: entry.docId,
1733
1147
  path: entry.path,
1734
1148
  title: entry.title,
1735
1149
  markdown,
1736
- reviewMarkdown: nextSidecar.reviewMarkdown ?? markdown,
1737
- sidecar: nextSidecar,
1738
- anchors: remappedAnchors.map((anchor) => ({
1739
- id: anchor.id,
1740
- kind: anchor.kind,
1741
- status: anchor.status,
1742
- range: anchor.range,
1743
- quote: anchor.selector.quote,
1744
- confidence: anchor.confidence
1745
- })),
1746
1150
  images: extractMarkdownImages(markdown, entry.path),
1747
- links: resolveMarkdownLinks(extractMarkdownLinks(markdown), entry, manifest.docs),
1748
- openComments: nextSidecar.comments.filter((comment2) => comment2.status === "open").length,
1749
- openSuggestions: nextSidecar.suggestions.filter((suggestion) => suggestion.status === "open").length
1750
- };
1751
- }
1752
- async function workspaceGraphForManifest(io, manifest) {
1753
- const documents = await Promise.all(
1754
- manifest.docs.map(async (entry) => {
1755
- const [markdown, sidecar] = await Promise.all([io.readText(entry.path), readSidecar(io, entry.docId)]);
1756
- const links = resolveMarkdownLinks(extractMarkdownLinks(markdown), entry, manifest.docs);
1757
- return {
1758
- docId: entry.docId,
1759
- path: entry.path,
1760
- title: entry.title,
1761
- openComments: sidecar.comments.filter((comment2) => comment2.status === "open").length,
1762
- openSuggestions: sidecar.suggestions.filter((suggestion) => suggestion.status === "open").length,
1763
- anchorsNeedingReview: sidecar.anchors.filter((anchor) => anchor.status !== "mapped").length,
1764
- externalLinks: links.filter((link2) => link2.status === "external").length,
1765
- unresolvedLinks: links.filter((link2) => link2.status === "unresolved" || link2.status === "ambiguous").length,
1766
- links
1767
- };
1768
- })
1769
- );
1770
- return buildWorkspaceGraph(manifest.workspaceId, manifest.schemaVersion, documents);
1771
- }
1772
- async function addComment(io, pathOrDocId, range, body, author = defaultActor()) {
1773
- const state = await getDocumentState(io, pathOrDocId);
1774
- assertLineRangeWithin(state.markdown, range);
1775
- const now = nowIso();
1776
- const anchor = {
1777
- id: createId("anchor"),
1778
- kind: "comment",
1779
- status: "mapped",
1780
- selector: selectorFromRange(state.markdown, range),
1781
- range,
1782
- confidence: 1,
1783
- updatedAt: now
1784
- };
1785
- const comment2 = {
1786
- id: createId("comment"),
1787
- anchorId: anchor.id,
1788
- status: "open",
1789
- author,
1790
- body,
1791
- replies: [],
1792
- createdAt: now,
1793
- updatedAt: now
1151
+ links,
1152
+ openComments: 0,
1153
+ openSuggestions: 0,
1154
+ anchorsNeedingReview: 0
1794
1155
  };
1795
- await writeSidecar(io, {
1796
- ...state.sidecar,
1797
- anchors: [...state.sidecar.anchors, anchor],
1798
- comments: [...state.sidecar.comments, comment2],
1799
- updatedAt: now
1800
- });
1801
- return comment2;
1802
1156
  }
1803
- async function addSuggestion(io, pathOrDocId, range, replacement, message, author = { id: "agent_local", kind: "agent", name: "Local Agent" }, changeSetId) {
1804
- const state = await getDocumentState(io, pathOrDocId);
1805
- const manifest = await indexWorkspace(io);
1806
- const entry = manifest.docs.find((doc) => doc.docId === state.docId);
1807
- assertLineRangeWithin(state.markdown, range);
1808
- assertCleanMarkdown(replacement);
1157
+ async function computeWorkspaceIndex(io, options) {
1158
+ const baseline = await initWorkspace(io);
1159
+ const cache = options?.cache;
1160
+ const markdownFiles = await io.listMarkdownFiles();
1161
+ const existingByPath = new Map(baseline.docs.map((doc) => [doc.path, doc]));
1809
1162
  const now = nowIso();
1810
- const before = extractLineRange(state.markdown, range);
1811
- const anchor = {
1812
- id: createId("anchor"),
1813
- kind: "suggestion",
1814
- status: "mapped",
1815
- selector: selectorFromRange(state.markdown, range),
1816
- range,
1817
- confidence: 1,
1818
- updatedAt: now
1819
- };
1820
- const suggestion = {
1821
- id: createId("suggestion"),
1822
- anchorId: anchor.id,
1823
- status: "open",
1824
- kind: replacement.length === 0 ? "delete" : before.length === 0 ? "insert" : "replace",
1825
- author,
1826
- message,
1827
- patch: {
1828
- type: "replace_lines",
1829
- range,
1830
- before,
1831
- after: replacement
1832
- },
1833
- base: {
1834
- contentHash: contentHashForText(state.markdown),
1835
- ...entry?.currentSha ? { head: entry.currentSha } : {}
1836
- },
1837
- changeSetId,
1838
- createdAt: now,
1839
- updatedAt: now
1840
- };
1841
- const review = createSuggestionMarkup(state.sidecar.reviewMarkdown ?? state.markdown, suggestion.patch, {
1842
- suggestionId: suggestion.id,
1843
- author,
1844
- message,
1845
- createdAt: now,
1846
- updatedAt: now
1847
- });
1848
- const nextSidecar = deriveSidecarFromReviewMarkdown(
1849
- { ...state.sidecar, reviewMarkdown: review.reviewMarkdown, anchors: [...state.sidecar.anchors, anchor] },
1850
- review.reviewMarkdown,
1851
- { author, baseHead: entry?.currentSha, now }
1852
- );
1853
- await writeSidecar(io, nextSidecar);
1854
- const enriched = nextSidecar.suggestions.find((candidate) => candidate.id === suggestion.id);
1855
- if (!enriched) throw new MdocsError("validation_error", `Could not derive suggestion ${suggestion.id}.`, "Refresh review state and try again.");
1856
- return enriched;
1857
- }
1858
- async function acceptSuggestion(io, suggestionId, actor = defaultActor()) {
1859
- const manifest = await indexWorkspace(io);
1860
- for (const entry of manifest.docs) {
1861
- const markdown = await io.readText(entry.path);
1862
- const sidecar = await readSidecar(io, entry.docId);
1863
- const suggestion = sidecar.suggestions.find((candidate) => candidate.id === suggestionId);
1864
- if (!suggestion) continue;
1865
- const now = nowIso();
1866
- const reviewMarkdown = sidecar.reviewMarkdown ?? markdown;
1867
- const resolvedReviewMarkdown = resolveSuggestionMarkup(reviewMarkdown, suggestionId, "accept");
1868
- const nextMarkdown = projectCanonicalMarkdown(resolvedReviewMarkdown);
1869
- assertCleanMarkdown(nextMarkdown);
1870
- const nextSidecar = deriveSidecarFromReviewMarkdown(
1871
- { ...sidecar, reviewMarkdown: resolvedReviewMarkdown },
1872
- resolvedReviewMarkdown,
1873
- { author: suggestion.author, baseHead: entry.currentSha, now }
1874
- );
1875
- await io.writeText(entry.path, nextMarkdown);
1876
- await writeSidecar(io, nextSidecar);
1877
- return resolvedSuggestion(suggestion, "accepted", actor, now);
1163
+ const drafts = [];
1164
+ const seenPaths = /* @__PURE__ */ new Set();
1165
+ for (const path of markdownFiles) {
1166
+ seenPaths.add(path);
1167
+ const existing = existingByPath.get(path);
1168
+ const stat4 = io.statFile ? await io.statFile(path).catch(() => void 0) : void 0;
1169
+ const cached = cache?.get(path);
1170
+ const statUnchanged = Boolean(stat4 && cached && cached.mtimeMs === stat4.mtimeMs && cached.size === stat4.size);
1171
+ if (statUnchanged && cached) {
1172
+ drafts.push({
1173
+ entry: manifestEntry(cached.docId, path, cached.title, cached.contentHash, existing, now, stat4),
1174
+ markdown: cached.markdown,
1175
+ reusedFromCache: true
1176
+ });
1177
+ continue;
1178
+ }
1179
+ let markdown;
1180
+ try {
1181
+ markdown = await io.readText(path);
1182
+ assertCleanMarkdown(markdown);
1183
+ } catch (error) {
1184
+ emitIndexDiagnostic(path, error);
1185
+ if (existing) drafts.push({ entry: { ...existing }, markdown: cached?.markdown ?? "", reusedFromCache: true });
1186
+ continue;
1187
+ }
1188
+ const contentHash = contentHashForText(markdown);
1189
+ const title = titleFromMarkdown(path, markdown);
1190
+ const docId = existing?.docId ?? mintDocId(options?.rootId, path);
1191
+ drafts.push({
1192
+ entry: manifestEntry(docId, path, title, contentHash, existing, now, stat4),
1193
+ markdown,
1194
+ reusedFromCache: false
1195
+ });
1878
1196
  }
1879
- throw new MdocsError("not_found", `Unknown suggestion: ${suggestionId}`, "List suggestion ids with the suggestions command.");
1880
- }
1881
- async function rejectSuggestion(io, suggestionId, actor = defaultActor()) {
1882
- const manifest = await indexWorkspace(io);
1883
- for (const entry of manifest.docs) {
1884
- const sidecar = await readSidecar(io, entry.docId);
1885
- const suggestion = sidecar.suggestions.find((candidate) => candidate.id === suggestionId);
1886
- if (!suggestion) continue;
1887
- const now = nowIso();
1888
- const updated = resolvedSuggestion(suggestion, "rejected", actor, now);
1889
- const reviewMarkdown = resolveSuggestionMarkup(sidecar.reviewMarkdown ?? "", suggestionId, "reject");
1890
- const nextSidecar = deriveSidecarFromReviewMarkdown({ ...sidecar, reviewMarkdown }, reviewMarkdown, {
1891
- author: suggestion.author,
1892
- now
1197
+ const docs = drafts.map((draft) => draft.entry);
1198
+ const lookup = docs.map((entry) => ({ docId: entry.docId, path: entry.path, title: entry.title }));
1199
+ const lookupSignature = JSON.stringify(lookup);
1200
+ const reuseLinks = cache ? cacheLookupSignatures.get(cache) === lookupSignature : false;
1201
+ const graphDocuments = [];
1202
+ const markdownByPath = /* @__PURE__ */ new Map();
1203
+ for (const draft of drafts) {
1204
+ const { entry, markdown, reusedFromCache } = draft;
1205
+ markdownByPath.set(entry.path, markdown);
1206
+ const cached = cache?.get(entry.path);
1207
+ const links = reusedFromCache && reuseLinks && cached?.links && cached.docId === entry.docId ? cached.links : resolveMarkdownLinks(extractMarkdownLinks(markdown), entry, lookup);
1208
+ graphDocuments.push({
1209
+ docId: entry.docId,
1210
+ path: entry.path,
1211
+ title: entry.title,
1212
+ openComments: 0,
1213
+ openSuggestions: 0,
1214
+ anchorsNeedingReview: 0,
1215
+ externalLinks: links.filter((link2) => link2.status === "external").length,
1216
+ unresolvedLinks: links.filter((link2) => link2.status === "unresolved" || link2.status === "ambiguous").length,
1217
+ links
1893
1218
  });
1894
- await writeSidecar(io, nextSidecar);
1895
- return updated;
1219
+ if (cache && entry.mtimeMs !== void 0 && entry.size !== void 0) {
1220
+ cache.set(entry.path, {
1221
+ mtimeMs: entry.mtimeMs,
1222
+ size: entry.size,
1223
+ contentHash: entry.contentHash ?? contentHashForText(markdown),
1224
+ markdown,
1225
+ title: entry.title,
1226
+ docId: entry.docId,
1227
+ links
1228
+ });
1229
+ }
1230
+ }
1231
+ if (cache) {
1232
+ cacheLookupSignatures.set(cache, lookupSignature);
1233
+ for (const key of [...cache.keys()]) {
1234
+ if (!seenPaths.has(key)) cache.delete(key);
1235
+ }
1896
1236
  }
1897
- throw new MdocsError("not_found", `Unknown suggestion: ${suggestionId}`, "List suggestion ids with the suggestions command.");
1237
+ const nextManifest = { ...baseline, docs, updatedAt: baseline.updatedAt };
1238
+ if (stableManifestKey(baseline) === stableManifestKey(nextManifest)) {
1239
+ return { manifest: baseline, graphDocuments, markdownByPath };
1240
+ }
1241
+ const written = { ...nextManifest, updatedAt: now };
1242
+ await writeManifest(io, written);
1243
+ return { manifest: written, graphDocuments, markdownByPath };
1898
1244
  }
1899
- function resolvedSuggestion(suggestion, status, actor, now) {
1245
+ function manifestEntry(docId, path, title, contentHash, existing, now, stat4) {
1900
1246
  return {
1901
- ...suggestion,
1902
- status,
1903
- updatedAt: now,
1904
- resolvedAt: now,
1905
- resolvedBy: actor
1247
+ docId,
1248
+ path,
1249
+ title,
1250
+ contentHash,
1251
+ currentSha: existing?.currentSha,
1252
+ // Preserve the prior timestamp when the content is unchanged so the manifest
1253
+ // stays byte-identical; only a real content change advances it.
1254
+ updatedAt: existing && existing.contentHash === contentHash ? existing.updatedAt : now,
1255
+ ...stat4 ? { mtimeMs: stat4.mtimeMs, size: stat4.size } : {}
1906
1256
  };
1907
1257
  }
1908
- async function resolveComment(io, commentId, actor = defaultActor()) {
1909
- const manifest = await indexWorkspace(io);
1910
- for (const entry of manifest.docs) {
1911
- const sidecar = await readSidecar(io, entry.docId);
1912
- const comment2 = sidecar.comments.find((candidate) => candidate.id === commentId);
1913
- if (!comment2) continue;
1914
- const now = nowIso();
1915
- const updated = {
1916
- ...comment2,
1917
- status: "resolved",
1918
- updatedAt: now,
1919
- resolvedAt: now,
1920
- resolvedBy: actor
1921
- };
1922
- await writeSidecar(io, {
1923
- ...sidecar,
1924
- comments: sidecar.comments.map((candidate) => candidate.id === commentId ? updated : candidate),
1925
- updatedAt: now
1926
- });
1927
- return updated;
1928
- }
1929
- throw new MdocsError("not_found", `Unknown comment: ${commentId}`, "List comment ids with the comments command.");
1258
+ function mintDocId(rootId, path) {
1259
+ return rootId ? deterministicDocId(rootId, path) : createDocId();
1930
1260
  }
1931
- function createEmptySidecar(entry) {
1932
- return {
1933
- schemaVersion: SIDECAR_SCHEMA_VERSION,
1934
- docId: entry.docId,
1935
- path: entry.path,
1936
- title: entry.title,
1937
- anchors: [],
1938
- comments: [],
1939
- suggestions: [],
1940
- changeSets: [],
1941
- updatedAt: entry.updatedAt
1942
- };
1261
+ function stableManifestKey(manifest) {
1262
+ return JSON.stringify({ ...manifest, updatedAt: "" });
1943
1263
  }
1944
- function sidecarPath(docId) {
1945
- return `${SIDECAR_DIR}/${docId}.json`;
1264
+ function emitIndexDiagnostic(path, error) {
1265
+ const detail = error instanceof Error ? error.message : String(error);
1266
+ if (typeof process !== "undefined" && process.stderr && typeof process.stderr.write === "function") {
1267
+ process.stderr.write(`mdocs index skipped ${path}: ${detail}
1268
+ `);
1269
+ }
1946
1270
  }
1947
1271
  async function readManifest(io) {
1948
1272
  const raw = await io.readText(MANIFEST_PATH);
@@ -1952,79 +1276,21 @@ async function writeManifest(io, manifest) {
1952
1276
  await writeJsonText(io, MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}
1953
1277
  `);
1954
1278
  }
1955
- async function readSidecar(io, docId) {
1956
- const raw = await io.readText(sidecarPath(docId));
1957
- return JSON.parse(raw);
1958
- }
1959
- async function writeSidecar(io, sidecar) {
1960
- await writeJsonText(io, sidecarPath(sidecar.docId), `${JSON.stringify(sidecar, null, 2)}
1961
- `);
1962
- }
1963
- async function writeJsonText(io, path, content) {
1964
- if (io.writeTextAtomic) {
1965
- await io.writeTextAtomic(path, content);
1966
- return;
1967
- }
1968
- await io.writeText(path, content);
1969
- }
1970
1279
  function findDoc(manifest, pathOrDocId) {
1971
1280
  const entry = manifest.docs.find((doc) => doc.path === pathOrDocId || doc.docId === pathOrDocId);
1972
- if (!entry) throw new Error(`Unknown document: ${pathOrDocId}`);
1281
+ if (!entry) throw new MdocsError("not_found", `Unknown document: ${pathOrDocId}`);
1973
1282
  return entry;
1974
1283
  }
1975
1284
  async function readManifestIfPresent(io) {
1976
1285
  if (!await io.exists(MANIFEST_PATH)) return void 0;
1977
1286
  return readManifest(io);
1978
1287
  }
1979
- async function ensureDocumentSidecar(io, entry, markdown) {
1980
- const path = sidecarPath(entry.docId);
1981
- if (await io.exists(path)) {
1982
- const sidecar = await readSidecar(io, entry.docId).catch(() => createEmptySidecar(entry));
1983
- await writeSidecar(io, {
1984
- ...sidecar,
1985
- path: entry.path,
1986
- title: entry.title,
1987
- anchors: remapAnchors(markdown, sidecar.anchors),
1988
- updatedAt: nowIso()
1989
- });
1990
- return;
1991
- }
1992
- await writeSidecar(io, createEmptySidecar(entry));
1993
- }
1994
- async function findRenamedDocumentEntry(io, candidates, markdown, contentHash, newPathCountForHash) {
1995
- if (newPathCountForHash === 1) {
1996
- const hashMatches = candidates.filter((entry) => entry.contentHash === contentHash);
1997
- if (hashMatches.length === 1) return hashMatches[0];
1998
- }
1999
- const scored = [];
2000
- for (const entry of candidates) {
2001
- const path = sidecarPath(entry.docId);
2002
- if (!await io.exists(path)) continue;
2003
- const sidecar = await readSidecar(io, entry.docId).catch(() => void 0);
2004
- if (!sidecar) continue;
2005
- const score = sidecarMarkdownMatchScore(sidecar, markdown);
2006
- if (score > 0) scored.push({ entry, score });
2007
- }
2008
- scored.sort((left, right) => right.score - left.score);
2009
- const best = scored[0];
2010
- const runnerUp = scored[1];
2011
- return best && best.score > (runnerUp?.score ?? 0) ? best.entry : void 0;
2012
- }
2013
- function sidecarMarkdownMatchScore(sidecar, markdown) {
2014
- const needles = /* @__PURE__ */ new Set();
2015
- for (const anchor of sidecar.anchors) {
2016
- const quote = anchor.selector.quote.trim();
2017
- if (quote) needles.add(quote);
2018
- }
2019
- for (const suggestion of sidecar.suggestions) {
2020
- if (suggestion.patch.before.trim()) needles.add(suggestion.patch.before.trim());
2021
- if (suggestion.patch.after.trim()) needles.add(suggestion.patch.after.trim());
2022
- }
2023
- let score = 0;
2024
- for (const needle of needles) {
2025
- if (markdown.includes(needle)) score += 1;
1288
+ async function writeJsonText(io, path, content) {
1289
+ if (io.writeTextAtomic) {
1290
+ await io.writeTextAtomic(path, content);
1291
+ return;
2026
1292
  }
2027
- return score;
1293
+ await io.writeText(path, content);
2028
1294
  }
2029
1295
 
2030
1296
  // ../core/src/context.ts
@@ -2043,8 +1309,6 @@ function sliceMarkdown(markdown, startLine, endLine) {
2043
1309
  return { markdown: lines.slice(sl - 1, el).join("\n"), totalLines, startLine: sl, endLine: el };
2044
1310
  }
2045
1311
  function summarizeDocumentContext(document) {
2046
- const comments = openComments(document);
2047
- const suggestions = openSuggestions(document);
2048
1312
  const currentSha = currentDocumentSha(document);
2049
1313
  return {
2050
1314
  docId: document.docId,
@@ -2055,9 +1319,9 @@ function summarizeDocumentContext(document) {
2055
1319
  byteLength: new TextEncoder().encode(document.markdown).byteLength,
2056
1320
  headings: extractMarkdownHeadings(document.markdown),
2057
1321
  reviewCounts: {
2058
- openComments: comments.length,
2059
- openSuggestions: suggestions.length,
2060
- anchorsNeedingReview: document.anchors.filter((anchor) => anchor.status !== "mapped").length
1322
+ openComments: document.openComments,
1323
+ openSuggestions: document.openSuggestions,
1324
+ anchorsNeedingReview: document.anchorsNeedingReview
2061
1325
  },
2062
1326
  imageCounts: imageCounts(document.images),
2063
1327
  linkCounts: linkCounts(document.links)
@@ -2070,9 +1334,12 @@ function reviewStateForDocument(document) {
2070
1334
  path: document.path,
2071
1335
  title: document.title,
2072
1336
  ...currentSha ? { currentSha } : {},
2073
- comments: openComments(document),
2074
- suggestions: openSuggestions(document),
2075
- anchors: document.anchors
1337
+ reviewCounts: {
1338
+ openComments: document.openComments,
1339
+ openSuggestions: document.openSuggestions,
1340
+ anchorsNeedingReview: document.anchorsNeedingReview
1341
+ },
1342
+ message: "Review state is authority-backed. Use remote authority commands or MCP endpoints for comments, suggestions, accept, and reject."
2076
1343
  };
2077
1344
  }
2078
1345
  function currentDocumentSha(document) {
@@ -2101,12 +1368,6 @@ function extractMarkdownHeadings(markdown) {
2101
1368
  });
2102
1369
  return headings;
2103
1370
  }
2104
- function openComments(document) {
2105
- return document.sidecar.comments.filter((comment2) => comment2.status === "open");
2106
- }
2107
- function openSuggestions(document) {
2108
- return document.sidecar.suggestions.filter((suggestion) => suggestion.status === "open");
2109
- }
2110
1371
  function imageCounts(images) {
2111
1372
  const local = images.filter((image2) => image2.isLocal).length;
2112
1373
  return {
@@ -4844,7 +4105,8 @@ var canonicalSchema = new Schema({
4844
4105
  nodes: {
4845
4106
  // The doc allows suggestion marks on its block children so pending
4846
4107
  // block-level suggestions (node marks) survive fromJSON and can be
4847
- // structurally reverted before serialization.
4108
+ // structurally resolved before serialization. Only inline insert/delete is
4109
+ // used today; block-level (modification) is reserved for a later pass.
4848
4110
  doc: { content: "block+", marks: "insertion deletion modification" },
4849
4111
  paragraph: { group: "block", content: "inline*", attrs: blockAttrs },
4850
4112
  heading: {
@@ -4926,11 +4188,14 @@ var canonicalSchema = new Schema({
4926
4188
  strike: {},
4927
4189
  code: {},
4928
4190
  // Pending-suggestion marks (mirroring the web editor's extensions). They
4929
- // are never serialized — the canonical serializer reverts them first —
4930
- // but the schema must know them so suggestion-marked docs load intact.
4191
+ // never appear in canonical bytes — the canonical serializer resolves them
4192
+ // first — but the schema must know them so suggestion-marked docs load and
4193
+ // the CriticMarkup codec can emit/parse them. `id` tags a rendered span
4194
+ // with its owning branch at overlay time; it is empty while editing one
4195
+ // branch.
4931
4196
  insertion: {
4932
4197
  attrs: { id: { default: null } },
4933
- inclusive: false,
4198
+ inclusive: true,
4934
4199
  excludes: "deletion modification insertion"
4935
4200
  },
4936
4201
  deletion: {
@@ -11016,6 +10281,69 @@ var MarkdownSerializerState = class {
11016
10281
  }
11017
10282
  };
11018
10283
 
10284
+ // ../core/src/suggestion-id.ts
10285
+ var SUGGESTION_ID_PREFIX = "suggestion:";
10286
+
10287
+ // ../core/src/critic-markup.ts
10288
+ var INSERTION_OPEN_SENTINEL = String.fromCharCode(57344);
10289
+ var INSERTION_CLOSE_SENTINEL = String.fromCharCode(57345);
10290
+ var DELETION_OPEN_SENTINEL = String.fromCharCode(57346);
10291
+ var DELETION_CLOSE_SENTINEL = String.fromCharCode(57347);
10292
+ var ID_OPEN_SENTINEL = String.fromCharCode(57348);
10293
+ var ID_CLOSE_SENTINEL = String.fromCharCode(57349);
10294
+ function suggestionSpanAt(src, start) {
10295
+ const open = src.slice(start, start + 3);
10296
+ if (open === "{++") return { markName: "insertion", closeSequence: "++}" };
10297
+ if (open === "{--") return { markName: "deletion", closeSequence: "--}" };
10298
+ return void 0;
10299
+ }
10300
+ function suggestionIdSuffixAt(src, pos) {
10301
+ if (!src.startsWith("{#", pos)) return void 0;
10302
+ const end = src.indexOf("}", pos + 2);
10303
+ if (end === -1) return void 0;
10304
+ const id = src.slice(pos + 2, end);
10305
+ return id.startsWith(SUGGESTION_ID_PREFIX) ? id : void 0;
10306
+ }
10307
+ function findUnescapedClose(src, from, closeSequence) {
10308
+ let pos = from;
10309
+ while (pos < src.length) {
10310
+ if (src.charCodeAt(pos) === 92) {
10311
+ pos += 2;
10312
+ continue;
10313
+ }
10314
+ if (src.startsWith(closeSequence, pos)) return pos;
10315
+ pos += 1;
10316
+ }
10317
+ return -1;
10318
+ }
10319
+ function criticMarkupPlugin(md) {
10320
+ md.inline.ruler.before("emphasis", "criticmarkup", (state, silent) => {
10321
+ const start = state.pos;
10322
+ if (state.src.charCodeAt(start) !== 123) return false;
10323
+ const span = suggestionSpanAt(state.src, start);
10324
+ if (!span) return false;
10325
+ const contentStart = start + 3;
10326
+ const closeStart = findUnescapedClose(state.src, contentStart, span.closeSequence);
10327
+ if (closeStart === -1) return false;
10328
+ if (silent) return true;
10329
+ const openToken = state.push(`${span.markName}_open`, "", 1);
10330
+ const savedMax = state.posMax;
10331
+ state.pos = contentStart;
10332
+ state.posMax = closeStart;
10333
+ state.md.inline.tokenize(state);
10334
+ state.posMax = savedMax;
10335
+ state.push(`${span.markName}_close`, "", -1);
10336
+ let nextPos = closeStart + span.closeSequence.length;
10337
+ const id = suggestionIdSuffixAt(state.src, nextPos);
10338
+ if (id !== void 0) {
10339
+ openToken.attrSet("id", id);
10340
+ nextPos += `{#${id}}`.length;
10341
+ }
10342
+ state.pos = nextPos;
10343
+ return true;
10344
+ });
10345
+ }
10346
+
11019
10347
  // ../core/src/pm-markdown.ts
11020
10348
  function serializeInlineFragmentToMarkdown(node) {
11021
10349
  const paragraph2 = canonicalSchema.nodes.paragraph;
@@ -11058,109 +10386,119 @@ function backtickFence(node) {
11058
10386
  const longest = matches.reduce((length, run) => Math.max(length, run.length), 2);
11059
10387
  return "`".repeat(longest + 1);
11060
10388
  }
11061
- var markdownSerializer = new MarkdownSerializer(
11062
- {
11063
- paragraph(state, node) {
11064
- state.renderInline(node);
11065
- state.closeBlock(node);
11066
- },
11067
- heading(state, node) {
11068
- state.write(`${"#".repeat(Number(node.attrs.level) || 1)} `);
11069
- state.renderInline(node, false);
11070
- state.closeBlock(node);
11071
- },
11072
- blockquote(state, node) {
11073
- state.wrapBlock("> ", null, node, () => state.renderContent(node));
11074
- },
11075
- codeBlock(state, node) {
11076
- const fence2 = backtickFence(node);
11077
- state.write(`${fence2}${typeof node.attrs.language === "string" ? node.attrs.language : ""}
10389
+ var NODE_SERIALIZERS = {
10390
+ paragraph(state, node) {
10391
+ state.renderInline(node);
10392
+ state.closeBlock(node);
10393
+ },
10394
+ heading(state, node) {
10395
+ state.write(`${"#".repeat(Number(node.attrs.level) || 1)} `);
10396
+ state.renderInline(node, false);
10397
+ state.closeBlock(node);
10398
+ },
10399
+ blockquote(state, node) {
10400
+ state.wrapBlock("> ", null, node, () => state.renderContent(node));
10401
+ },
10402
+ codeBlock(state, node) {
10403
+ const fence2 = backtickFence(node);
10404
+ state.write(`${fence2}${typeof node.attrs.language === "string" ? node.attrs.language : ""}
11078
10405
  `);
11079
- state.text(node.textContent, false);
11080
- state.ensureNewLine();
11081
- state.write(fence2);
11082
- state.closeBlock(node);
11083
- },
11084
- horizontalRule(state, node) {
11085
- state.write("---");
11086
- state.closeBlock(node);
11087
- },
11088
- bulletList(state, node) {
11089
- state.renderList(node, " ", () => "- ");
11090
- },
11091
- orderedList(state, node) {
11092
- const start = Number(node.attrs.start) || 1;
11093
- const maxWidth = String(start + node.childCount - 1).length;
11094
- const space = state.repeat(" ", maxWidth + 2);
11095
- state.renderList(node, space, (index) => {
11096
- const label = String(start + index);
11097
- return `${state.repeat(" ", maxWidth - label.length)}${label}. `;
11098
- });
11099
- },
11100
- listItem(state, node) {
11101
- state.renderContent(node);
11102
- },
11103
- taskList(state, node) {
11104
- state.renderList(node, " ", () => "- ");
11105
- },
11106
- taskItem(state, node) {
11107
- state.write(`[${node.attrs.checked ? "x" : " "}] `);
11108
- state.renderContent(node);
11109
- },
11110
- table: serializeTable,
11111
- image(state, node) {
11112
- const src = typeof node.attrs.src === "string" ? node.attrs.src : "";
11113
- const alt = typeof node.attrs.alt === "string" ? node.attrs.alt : "";
11114
- const title = typeof node.attrs.title === "string" ? node.attrs.title : "";
11115
- const destination = src.replace(/[()"]/g, "\\$&");
11116
- const titleSuffix = title ? ` "${title.replace(/["\\]/g, "\\$&")}"` : "";
11117
- state.write(`![${escapeImageAlt(alt)}](${destination}${titleSuffix})`);
11118
- },
11119
- hardBreak(state, node, parent, index) {
11120
- for (let after = index + 1; after < parent.childCount; after += 1) {
11121
- if (parent.child(after).type !== node.type) {
11122
- state.write("\\\n");
11123
- return;
11124
- }
10406
+ state.text(node.textContent, false);
10407
+ state.ensureNewLine();
10408
+ state.write(fence2);
10409
+ state.closeBlock(node);
10410
+ },
10411
+ horizontalRule(state, node) {
10412
+ state.write("---");
10413
+ state.closeBlock(node);
10414
+ },
10415
+ bulletList(state, node) {
10416
+ state.renderList(node, " ", () => "- ");
10417
+ },
10418
+ orderedList(state, node) {
10419
+ const start = Number(node.attrs.start) || 1;
10420
+ const maxWidth = String(start + node.childCount - 1).length;
10421
+ const space = state.repeat(" ", maxWidth + 2);
10422
+ state.renderList(node, space, (index) => {
10423
+ const label = String(start + index);
10424
+ return `${state.repeat(" ", maxWidth - label.length)}${label}. `;
10425
+ });
10426
+ },
10427
+ listItem(state, node) {
10428
+ state.renderContent(node);
10429
+ },
10430
+ taskList(state, node) {
10431
+ state.renderList(node, " ", () => "- ");
10432
+ },
10433
+ taskItem(state, node) {
10434
+ state.write(`[${node.attrs.checked ? "x" : " "}] `);
10435
+ state.renderContent(node);
10436
+ },
10437
+ table: serializeTable,
10438
+ image(state, node) {
10439
+ const src = typeof node.attrs.src === "string" ? node.attrs.src : "";
10440
+ const alt = typeof node.attrs.alt === "string" ? node.attrs.alt : "";
10441
+ const title = typeof node.attrs.title === "string" ? node.attrs.title : "";
10442
+ const destination = src.replace(/[()"]/g, "\\$&");
10443
+ const titleSuffix = title ? ` "${title.replace(/["\\]/g, "\\$&")}"` : "";
10444
+ state.write(`![${escapeImageAlt(alt)}](${destination}${titleSuffix})`);
10445
+ },
10446
+ hardBreak(state, node, parent, index) {
10447
+ for (let after = index + 1; after < parent.childCount; after += 1) {
10448
+ if (parent.child(after).type !== node.type) {
10449
+ state.write("\\\n");
10450
+ return;
11125
10451
  }
11126
- },
11127
- text(state, node) {
11128
- state.text(node.text ?? "");
11129
10452
  }
11130
10453
  },
11131
- {
11132
- bold: { open: "**", close: "**", mixable: true, expelEnclosingWhitespace: true },
11133
- italic: { open: "_", close: "_", mixable: true, expelEnclosingWhitespace: true },
11134
- strike: { open: "~~", close: "~~", mixable: true, expelEnclosingWhitespace: true },
11135
- code: {
11136
- open(_state, _mark, parent, index) {
11137
- return backticksFor2(parent.child(index), -1);
11138
- },
11139
- close(_state, _mark, parent, index) {
11140
- return backticksFor2(parent.child(index - 1), 1);
11141
- },
11142
- escape: false
10454
+ text(state, node) {
10455
+ state.text(node.text ?? "");
10456
+ }
10457
+ };
10458
+ var BASE_MARK_SERIALIZERS = {
10459
+ bold: { open: "**", close: "**", mixable: true, expelEnclosingWhitespace: true },
10460
+ italic: { open: "_", close: "_", mixable: true, expelEnclosingWhitespace: true },
10461
+ strike: { open: "~~", close: "~~", mixable: true, expelEnclosingWhitespace: true },
10462
+ code: {
10463
+ open(_state, _mark, parent, index) {
10464
+ return backticksFor2(parent.child(index), -1);
11143
10465
  },
11144
- link: {
11145
- open: "[",
11146
- close(_state, mark) {
11147
- const href = typeof mark.attrs.href === "string" ? mark.attrs.href : "";
11148
- return `](${href.replace(/[()"]/g, "\\$&")})`;
11149
- },
11150
- mixable: false
10466
+ close(_state, _mark, parent, index) {
10467
+ return backticksFor2(parent.child(index - 1), 1);
11151
10468
  },
11152
- wikilink: {
11153
- open(_state, mark, parent, index) {
11154
- const target = typeof mark.attrs.target === "string" ? mark.attrs.target : "";
11155
- const label = markedRangeText(parent, index, mark);
11156
- if (!target || label === target) return "[[";
11157
- return `[[${target}|`;
11158
- },
11159
- close: "]]",
11160
- mixable: false
11161
- }
10469
+ escape: false
10470
+ },
10471
+ link: {
10472
+ open: "[",
10473
+ close(_state, mark) {
10474
+ const href = typeof mark.attrs.href === "string" ? mark.attrs.href : "";
10475
+ return `](${href.replace(/[()"]/g, "\\$&")})`;
10476
+ },
10477
+ mixable: false
10478
+ },
10479
+ wikilink: {
10480
+ open(_state, mark, parent, index) {
10481
+ const target = typeof mark.attrs.target === "string" ? mark.attrs.target : "";
10482
+ const label = markedRangeText(parent, index, mark);
10483
+ if (!target || label === target) return "[[";
10484
+ return `[[${target}|`;
10485
+ },
10486
+ close: "]]",
10487
+ mixable: false
11162
10488
  }
11163
- );
10489
+ };
10490
+ var markdownSerializer = new MarkdownSerializer(NODE_SERIALIZERS, BASE_MARK_SERIALIZERS);
10491
+ function suggestionMarkClose(closeSentinel) {
10492
+ return (_state, mark) => {
10493
+ const id = typeof mark.attrs.id === "string" ? mark.attrs.id : "";
10494
+ return id ? `${closeSentinel}${ID_OPEN_SENTINEL}${id}${ID_CLOSE_SENTINEL}` : closeSentinel;
10495
+ };
10496
+ }
10497
+ var criticMarkupSerializer = new MarkdownSerializer(NODE_SERIALIZERS, {
10498
+ ...BASE_MARK_SERIALIZERS,
10499
+ insertion: { open: INSERTION_OPEN_SENTINEL, close: suggestionMarkClose(INSERTION_CLOSE_SENTINEL), mixable: true },
10500
+ deletion: { open: DELETION_OPEN_SENTINEL, close: suggestionMarkClose(DELETION_CLOSE_SENTINEL), mixable: true }
10501
+ });
11164
10502
  function markedRangeText(parent, index, mark) {
11165
10503
  let text2 = "";
11166
10504
  for (let childIndex = index; childIndex < parent.childCount; childIndex += 1) {
@@ -11187,7 +10525,7 @@ function backticksFor2(node, side) {
11187
10525
  }
11188
10526
 
11189
10527
  // ../core/src/pm-parse.ts
11190
- var parser = new MarkdownParser(canonicalSchema, new lib_default(), {
10528
+ var BASE_TOKEN_SPEC = {
11191
10529
  blockquote: { block: "blockquote" },
11192
10530
  paragraph: { block: "paragraph" },
11193
10531
  list_item: { block: "listItem" },
@@ -11214,56 +10552,246 @@ var parser = new MarkdownParser(canonicalSchema, new lib_default(), {
11214
10552
  title: tok.attrGet("title")
11215
10553
  })
11216
10554
  }
10555
+ };
10556
+ var parser = new MarkdownParser(canonicalSchema, new lib_default(), BASE_TOKEN_SPEC);
10557
+ var criticMarkupParser = new MarkdownParser(canonicalSchema, new lib_default().use(criticMarkupPlugin), {
10558
+ ...BASE_TOKEN_SPEC,
10559
+ insertion: { mark: "insertion", getAttrs: (tok) => ({ id: tok.attrGet("id") }) },
10560
+ deletion: { mark: "deletion", getAttrs: (tok) => ({ id: tok.attrGet("id") }) }
11217
10561
  });
11218
10562
 
11219
- // ../core/src/remote-document-io.ts
11220
- var RemoteDocumentIO = class {
11221
- constructor(document) {
11222
- this.document = document;
11223
- const now = (/* @__PURE__ */ new Date()).toISOString();
11224
- const manifest = {
11225
- schemaVersion: document.sidecar.schemaVersion,
11226
- workspaceId: document.workspaceId,
11227
- createdAt: now,
11228
- updatedAt: now,
11229
- docs: [{
11230
- docId: document.docId,
11231
- path: document.path,
11232
- title: document.title,
11233
- contentHash: contentHashForText(document.markdown),
11234
- currentSha: document.currentSha,
11235
- updatedAt: document.sidecar.updatedAt
11236
- }]
11237
- };
11238
- this.files.set(MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}
11239
- `);
11240
- this.files.set(document.path, document.markdown);
11241
- this.files.set(sidecarPath(document.docId), `${JSON.stringify(document.sidecar, null, 2)}
11242
- `);
10563
+ // ../core/src/patch.ts
10564
+ var PATCH_SCHEMA = "magic-markdown.patch.v1";
10565
+ function textPatchHash(text2) {
10566
+ return contentHashForText(text2);
10567
+ }
10568
+ function buildPatchEnvelope(options) {
10569
+ const ops = diffToPatchOps(options.base, options.result);
10570
+ return patchEnvelopeFromOps({
10571
+ baseVersion: options.baseVersion,
10572
+ base: options.base,
10573
+ ops,
10574
+ clientMutationId: options.clientMutationId,
10575
+ clientMeta: options.clientMeta
10576
+ });
10577
+ }
10578
+ function buildLineRangePatchEnvelope(options) {
10579
+ assertLineRangeWithin(options.base, options.range);
10580
+ const result = replaceLineRange(options.base, options.range, options.replacement);
10581
+ return buildPatchEnvelope({
10582
+ baseVersion: options.baseVersion,
10583
+ base: options.base,
10584
+ result,
10585
+ clientMutationId: options.clientMutationId,
10586
+ clientMeta: { ...options.clientMeta ?? {}, source: options.clientMeta?.source ?? "line_range" }
10587
+ });
10588
+ }
10589
+ function patchEnvelopeFromOps(options) {
10590
+ const ops = normalizePatchOps(options.ops);
10591
+ const result = applyPatchOps(options.base, ops);
10592
+ return {
10593
+ schema: PATCH_SCHEMA,
10594
+ baseVersion: options.baseVersion,
10595
+ baseHash: textPatchHash(options.base),
10596
+ baseLength: options.base.length,
10597
+ resultLength: result.length,
10598
+ resultHash: textPatchHash(result),
10599
+ ops,
10600
+ ...options.clientMutationId ? { clientMutationId: options.clientMutationId } : {},
10601
+ ...options.clientMeta ? { clientMeta: options.clientMeta } : {}
10602
+ };
10603
+ }
10604
+ function applyPatchOps(base, ops) {
10605
+ validatePatchOpsShape(ops);
10606
+ let baseCursor = 0;
10607
+ let result = "";
10608
+ for (const op of ops) {
10609
+ if (op.type === "retain") {
10610
+ const next = baseCursor + op.count;
10611
+ if (next > base.length) throw new MdocsError("validation_error", "Patch retain runs past the base document.");
10612
+ result += base.slice(baseCursor, next);
10613
+ baseCursor = next;
10614
+ continue;
10615
+ }
10616
+ if (op.type === "delete") {
10617
+ const actual = base.slice(baseCursor, baseCursor + op.text.length);
10618
+ if (actual !== op.text) throw new MdocsError("conflict", "Patch delete text does not match the base document.");
10619
+ baseCursor += op.text.length;
10620
+ continue;
10621
+ }
10622
+ result += op.text;
10623
+ }
10624
+ return result + base.slice(baseCursor);
10625
+ }
10626
+ function normalizePatchOps(ops) {
10627
+ validatePatchOpsShape(ops);
10628
+ const normalized = [];
10629
+ for (const op of ops) {
10630
+ if (op.type === "retain" && op.count === 0) continue;
10631
+ if ((op.type === "insert" || op.type === "delete") && op.text.length === 0) continue;
10632
+ const last = normalized.at(-1);
10633
+ if (last?.type === op.type) {
10634
+ if (op.type === "retain" && last.type === "retain") last.count += op.count;
10635
+ if (op.type === "insert" && last.type === "insert") last.text += op.text;
10636
+ if (op.type === "delete" && last.type === "delete") last.text += op.text;
10637
+ continue;
10638
+ }
10639
+ normalized.push({ ...op });
11243
10640
  }
11244
- document;
11245
- files = /* @__PURE__ */ new Map();
11246
- async readText(path) {
11247
- const content = this.files.get(path);
11248
- if (content === void 0) throw new Error(`Missing remote document file: ${path}`);
11249
- return content;
10641
+ return normalized;
10642
+ }
10643
+ function diffToPatchOps(base, result) {
10644
+ if (base === result) return [];
10645
+ let prefix = 0;
10646
+ while (prefix < base.length && prefix < result.length && base[prefix] === result[prefix]) prefix += 1;
10647
+ let suffix = 0;
10648
+ while (suffix < base.length - prefix && suffix < result.length - prefix && base[base.length - 1 - suffix] === result[result.length - 1 - suffix]) {
10649
+ suffix += 1;
10650
+ }
10651
+ const tokenRefined = diffMiddleByTokens(base, result, prefix, suffix);
10652
+ if (tokenRefined) return tokenRefined;
10653
+ const refined = diffMiddleByLines(base, result, prefix, suffix);
10654
+ if (refined) return refined;
10655
+ return singleWindowPatchOps(base, result, prefix, suffix);
10656
+ }
10657
+ function singleWindowPatchOps(base, result, prefix, suffix) {
10658
+ const ops = [];
10659
+ if (prefix > 0) ops.push({ type: "retain", count: prefix });
10660
+ const deleted = base.slice(prefix, base.length - suffix);
10661
+ const inserted = result.slice(prefix, result.length - suffix);
10662
+ if (deleted.length > 0) ops.push({ type: "delete", text: deleted });
10663
+ if (inserted.length > 0) ops.push({ type: "insert", text: inserted });
10664
+ if (suffix > 0) ops.push({ type: "retain", count: suffix });
10665
+ return normalizePatchOps(ops);
10666
+ }
10667
+ function diffMiddleByLines(base, result, prefix, suffix) {
10668
+ const baseMiddle = base.slice(prefix, base.length - suffix);
10669
+ const resultMiddle = result.slice(prefix, result.length - suffix);
10670
+ if (!baseMiddle.includes("\n") && !resultMiddle.includes("\n")) return void 0;
10671
+ const baseLines = splitLinesPreservingNewline(baseMiddle);
10672
+ const resultLines = splitLinesPreservingNewline(resultMiddle);
10673
+ if (baseLines.length + resultLines.length > 800) return void 0;
10674
+ if (!hasSharedLine(baseLines, resultLines)) return void 0;
10675
+ const middleOps = lineDiffOps(baseLines, resultLines);
10676
+ if (middleOps.length <= 2) return void 0;
10677
+ const ops = [];
10678
+ if (prefix > 0) ops.push({ type: "retain", count: prefix });
10679
+ ops.push(...middleOps);
10680
+ if (suffix > 0) ops.push({ type: "retain", count: suffix });
10681
+ return normalizePatchOps(ops);
10682
+ }
10683
+ function diffMiddleByTokens(base, result, prefix, suffix) {
10684
+ const baseMiddle = base.slice(prefix, base.length - suffix);
10685
+ const resultMiddle = result.slice(prefix, result.length - suffix);
10686
+ if (!baseMiddle || !resultMiddle) return void 0;
10687
+ const baseTokens = tokenizePatchMiddle(baseMiddle);
10688
+ const resultTokens = tokenizePatchMiddle(resultMiddle);
10689
+ if (baseTokens.length + resultTokens.length > 1200) return void 0;
10690
+ if (!hasSharedToken(baseTokens, resultTokens)) return void 0;
10691
+ const middleOps = tokenDiffOps(baseTokens, resultTokens);
10692
+ if (!middleOps.some((op) => op.type === "retain")) return void 0;
10693
+ const ops = [];
10694
+ if (prefix > 0) ops.push({ type: "retain", count: prefix });
10695
+ ops.push(...middleOps);
10696
+ if (suffix > 0) ops.push({ type: "retain", count: suffix });
10697
+ const normalized = normalizePatchOps(ops);
10698
+ return normalized.length > 3 ? normalized : void 0;
10699
+ }
10700
+ function tokenizePatchMiddle(text2) {
10701
+ return text2.match(/[\p{L}\p{N}_]+|\s+|[^\s\p{L}\p{N}_]+/gu) ?? [];
10702
+ }
10703
+ function hasSharedToken(first, second) {
10704
+ const secondSet = new Set(second);
10705
+ return first.some((token) => secondSet.has(token));
10706
+ }
10707
+ function tokenDiffOps(baseTokens, resultTokens) {
10708
+ const lcs = tokenLcsTable(baseTokens, resultTokens);
10709
+ const ops = [];
10710
+ let baseIndex = 0;
10711
+ let resultIndex = 0;
10712
+ while (baseIndex < baseTokens.length || resultIndex < resultTokens.length) {
10713
+ if (baseIndex < baseTokens.length && resultIndex < resultTokens.length && baseTokens[baseIndex] === resultTokens[resultIndex]) {
10714
+ ops.push({ type: "retain", count: baseTokens[baseIndex].length });
10715
+ baseIndex += 1;
10716
+ resultIndex += 1;
10717
+ continue;
10718
+ }
10719
+ if (resultIndex >= resultTokens.length || baseIndex < baseTokens.length && lcs[baseIndex + 1][resultIndex] >= lcs[baseIndex][resultIndex + 1]) {
10720
+ ops.push({ type: "delete", text: baseTokens[baseIndex] });
10721
+ baseIndex += 1;
10722
+ continue;
10723
+ }
10724
+ ops.push({ type: "insert", text: resultTokens[resultIndex] });
10725
+ resultIndex += 1;
11250
10726
  }
11251
- async writeText(path, content) {
11252
- this.files.set(path, content);
10727
+ return normalizePatchOps(ops);
10728
+ }
10729
+ function tokenLcsTable(baseTokens, resultTokens) {
10730
+ const table2 = Array.from({ length: baseTokens.length + 1 }, () => Array.from({ length: resultTokens.length + 1 }, () => 0));
10731
+ for (let baseIndex = baseTokens.length - 1; baseIndex >= 0; baseIndex -= 1) {
10732
+ for (let resultIndex = resultTokens.length - 1; resultIndex >= 0; resultIndex -= 1) {
10733
+ table2[baseIndex][resultIndex] = baseTokens[baseIndex] === resultTokens[resultIndex] ? table2[baseIndex + 1][resultIndex + 1] + 1 : Math.max(table2[baseIndex + 1][resultIndex], table2[baseIndex][resultIndex + 1]);
10734
+ }
11253
10735
  }
11254
- async exists(path) {
11255
- return this.files.has(path) || [...this.files.keys()].some((candidate) => candidate.startsWith(`${path}/`));
10736
+ return table2;
10737
+ }
10738
+ function splitLinesPreservingNewline(text2) {
10739
+ return text2.match(/[^\n]*\n|[^\n]+/g) ?? [];
10740
+ }
10741
+ function hasSharedLine(first, second) {
10742
+ const secondSet = new Set(second);
10743
+ return first.some((line) => secondSet.has(line));
10744
+ }
10745
+ function lineDiffOps(baseLines, resultLines) {
10746
+ const lcs = lineLcsTable(baseLines, resultLines);
10747
+ const ops = [];
10748
+ let baseIndex = 0;
10749
+ let resultIndex = 0;
10750
+ while (baseIndex < baseLines.length || resultIndex < resultLines.length) {
10751
+ if (baseIndex < baseLines.length && resultIndex < resultLines.length && baseLines[baseIndex] === resultLines[resultIndex]) {
10752
+ ops.push({ type: "retain", count: baseLines[baseIndex].length });
10753
+ baseIndex += 1;
10754
+ resultIndex += 1;
10755
+ continue;
10756
+ }
10757
+ if (resultIndex >= resultLines.length || baseIndex < baseLines.length && lcs[baseIndex + 1][resultIndex] >= lcs[baseIndex][resultIndex + 1]) {
10758
+ ops.push({ type: "delete", text: baseLines[baseIndex] });
10759
+ baseIndex += 1;
10760
+ continue;
10761
+ }
10762
+ ops.push({ type: "insert", text: resultLines[resultIndex] });
10763
+ resultIndex += 1;
11256
10764
  }
11257
- async mkdir() {
11258
- return;
10765
+ return normalizePatchOps(ops);
10766
+ }
10767
+ function lineLcsTable(baseLines, resultLines) {
10768
+ const table2 = Array.from({ length: baseLines.length + 1 }, () => Array.from({ length: resultLines.length + 1 }, () => 0));
10769
+ for (let baseIndex = baseLines.length - 1; baseIndex >= 0; baseIndex -= 1) {
10770
+ for (let resultIndex = resultLines.length - 1; resultIndex >= 0; resultIndex -= 1) {
10771
+ table2[baseIndex][resultIndex] = baseLines[baseIndex] === resultLines[resultIndex] ? table2[baseIndex + 1][resultIndex + 1] + 1 : Math.max(table2[baseIndex + 1][resultIndex], table2[baseIndex][resultIndex + 1]);
10772
+ }
11259
10773
  }
11260
- async listMarkdownFiles() {
11261
- return [this.document.path];
10774
+ return table2;
10775
+ }
10776
+ function validatePatchOpsShape(ops) {
10777
+ if (!Array.isArray(ops)) throw new MdocsError("validation_error", "Patch ops must be an array.");
10778
+ for (const op of ops) {
10779
+ if (op.type === "retain") {
10780
+ if (!Number.isInteger(op.count) || op.count < 0) {
10781
+ throw new MdocsError("validation_error", "Patch retain count must be a non-negative integer.");
10782
+ }
10783
+ continue;
10784
+ }
10785
+ if (op.type === "insert" || op.type === "delete") {
10786
+ if (typeof op.text !== "string") throw new MdocsError("validation_error", `Patch ${op.type} text must be a string.`);
10787
+ continue;
10788
+ }
10789
+ throw new MdocsError("validation_error", `Unknown patch op type: ${op.type}.`);
11262
10790
  }
11263
- };
10791
+ }
11264
10792
 
11265
10793
  // src/agent.ts
11266
- var CLI_VERSION = "0.3.20";
10794
+ var CLI_VERSION = "0.3.24";
11267
10795
  var CLI_PACKAGE_NAME = "@magic-markdown/cli";
11268
10796
  var AGENT_COMMANDS = [
11269
10797
  {
@@ -11351,12 +10879,12 @@ var AGENT_COMMANDS = [
11351
10879
  "Use remote status --json immediately after joining when the handoff includes expected scope/root/doc values. If it reports wrong_binding, stop and ask for the correct share or connector.",
11352
10880
  "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.",
11353
10881
  "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>.",
11354
- "Use remote context --no-review when reading document content only; use remote review to list open comments, open suggestions, and anchors separately.",
10882
+ "Use remote context --no-review when reading document content only; use remote review to list authority-projected open comments and suggestions separately.",
11355
10883
  "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>.",
11356
10884
  "remote graph requires a project-scoped join and returns the shared project's Obsidian-style Markdown/wikilink graph.",
11357
10885
  "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.",
11358
10886
  "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.",
11359
- "remote comment/suggest never clobber concurrent edits: the server merges review additions, and the CLI rebases and retries automatically if a conflict still occurs.",
10887
+ "remote comment/suggest never clobber concurrent edits: comments, suggestions, accept, and reject go through DocumentAuthority.",
11360
10888
  "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.",
11361
10889
  "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.",
11362
10890
  "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.",
@@ -11365,7 +10893,7 @@ var AGENT_COMMANDS = [
11365
10893
  },
11366
10894
  {
11367
10895
  name: "doctor",
11368
- summary: "Validate the workspace map, sidecars, and agent interface readiness.",
10896
+ summary: "Validate the workspace map and agent interface readiness.",
11369
10897
  usage: "mdocs doctor --json",
11370
10898
  output: "json",
11371
10899
  mutates: true,
@@ -11393,7 +10921,7 @@ var AGENT_COMMANDS = [
11393
10921
  },
11394
10922
  {
11395
10923
  name: "context",
11396
- summary: "Return agent-ready document Markdown, optional review state, image references, and resolved outgoing links.",
10924
+ summary: "Return agent-ready clean Markdown, image references, and resolved outgoing links.",
11397
10925
  usage: "mdocs context <path|docId> [--summary] [--no-review] [--start-line N] [--end-line N] --json",
11398
10926
  output: "json",
11399
10927
  mutates: true,
@@ -11407,12 +10935,12 @@ var AGENT_COMMANDS = [
11407
10935
  "--summary returns metadata, heading line numbers, review counts, image counts, and link counts without returning Markdown.",
11408
10936
  "The response always includes totalLines, startLine, and endLine. If totalLines > endLine, request the next page with --start-line <endLine+1>.",
11409
10937
  "--start-line and --end-line are 1-based and inclusive. Omitting both returns the full document.",
11410
- "--no-review omits comments, suggestions, and anchors from content reads; use review/comments/suggestions commands when you need review state."
10938
+ "--no-review omits authority-review redirect metadata from content reads. Use remote review for comments and suggestions."
11411
10939
  ]
11412
10940
  },
11413
10941
  {
11414
10942
  name: "review",
11415
- summary: "Return open comments, open suggestions, and anchors for one document without returning Markdown.",
10943
+ summary: "Return local authority-review redirect metadata for one document without returning Markdown.",
11416
10944
  usage: "mdocs review <path|docId> --json",
11417
10945
  output: "json",
11418
10946
  mutates: true,
@@ -11420,82 +10948,12 @@ var AGENT_COMMANDS = [
11420
10948
  },
11421
10949
  {
11422
10950
  name: "state",
11423
- summary: "Return full merged Markdown and sidecar state for one document.",
10951
+ summary: "Return clean Markdown document state for one document.",
11424
10952
  usage: "mdocs state <path|docId> --json",
11425
10953
  output: "json",
11426
10954
  mutates: true,
11427
10955
  examples: ["mdocs state docs/example.md --json"]
11428
10956
  },
11429
- {
11430
- name: "comment",
11431
- summary: "Create a sidecar comment without modifying clean Markdown.",
11432
- usage: "mdocs comment <path|docId> --range <start:end> --body <text> --json",
11433
- output: "json",
11434
- mutates: true,
11435
- examples: [
11436
- 'mdocs comment docs/example.md --range 3:5 --body "Check this claim." --json',
11437
- 'mdocs comment docs/example.md --range 3:5 --body-file /tmp/comment.txt --actor agent_local --actor-kind agent --actor-name "Local Agent" --json'
11438
- ],
11439
- notes: ["--range is 1-based and inclusive, validated against the current document line count."]
11440
- },
11441
- {
11442
- name: "suggest",
11443
- summary: "Create a reviewMarkdown replacement suggestion without modifying canonical Markdown.",
11444
- usage: "mdocs suggest <path|docId> --range <start:end> --with <markdown> --message <text> --json",
11445
- output: "json",
11446
- mutates: true,
11447
- examples: [
11448
- 'mdocs suggest docs/example.md --range 4:4 --with "Better line." --message "Tighten wording." --json',
11449
- 'mdocs suggest docs/example.md --range 4:9 --with-file /tmp/replacement.md --message-file /tmp/message.txt --actor agent_local --actor-kind agent --actor-name "Local Agent" --json'
11450
- ],
11451
- notes: [
11452
- "--range is 1-based and inclusive, validated against the current document line count.",
11453
- "The replacement text replaces the whole range; re-read context first so the range matches current content.",
11454
- "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."
11455
- ]
11456
- },
11457
- {
11458
- name: "comments",
11459
- summary: "List all comment threads for a document.",
11460
- usage: "mdocs comments <path|docId> --json",
11461
- output: "json",
11462
- mutates: true,
11463
- examples: ["mdocs comments docs/example.md --json"]
11464
- },
11465
- {
11466
- name: "suggestions",
11467
- summary: "List all suggestions for a document.",
11468
- usage: "mdocs suggestions <path|docId> --json",
11469
- output: "json",
11470
- mutates: true,
11471
- examples: ["mdocs suggestions docs/example.md --json"]
11472
- },
11473
- {
11474
- name: "accept",
11475
- summary: "Apply and accept an existing suggestion.",
11476
- usage: "mdocs accept <suggestionId> --json",
11477
- output: "json",
11478
- mutates: true,
11479
- examples: ["mdocs accept suggestion_abc123 --json"],
11480
- notes: ["Only use when the user explicitly asks to apply a suggestion."]
11481
- },
11482
- {
11483
- name: "reject",
11484
- summary: "Reject an existing suggestion.",
11485
- usage: "mdocs reject <suggestionId> --json",
11486
- output: "json",
11487
- mutates: true,
11488
- examples: ["mdocs reject suggestion_abc123 --json"],
11489
- notes: ["Only use when the user explicitly asks to reject a suggestion."]
11490
- },
11491
- {
11492
- name: "resolve-comment",
11493
- summary: "Resolve an existing comment thread.",
11494
- usage: "mdocs resolve-comment <commentId> --json",
11495
- output: "json",
11496
- mutates: true,
11497
- examples: ["mdocs resolve-comment comment_abc123 --json"]
11498
- },
11499
10957
  {
11500
10958
  name: "status",
11501
10959
  summary: "Return local Git status plus the Magic Markdown workspace map.",
@@ -11626,14 +11084,14 @@ function getAgentGuidePayload() {
11626
11084
  function getAgentSkillMarkdown() {
11627
11085
  return `---
11628
11086
  name: magic-markdown
11629
- description: Use the Magic Markdown CLI or MCP server to read, organize, review, comment on, suggest edits to, and sync clean Markdown workspaces without hand-editing .mdocs sidecars. Use when an agent needs to interact with Magic Markdown documents, library folders, comments, suggestions, image context, checkpoints, or local source sync.
11087
+ description: Use the Magic Markdown CLI or MCP server to read clean Markdown workspaces, and use remote authority commands for comments, suggestions, accept, and reject. Use when an agent needs to interact with Magic Markdown documents, library folders, image context, checkpoints, remote review state, or local source sync.
11630
11088
  metadata:
11631
11089
  short-description: Operate Magic Markdown via CLI/MCP
11632
11090
  ---
11633
11091
 
11634
11092
  # Magic Markdown Agent Workflow
11635
11093
 
11636
- Magic Markdown keeps Markdown files clean and stores collaboration metadata in .mdocs sidecars. Treat .mdocs as an implementation detail: use the CLI or MCP resources/tools instead of reading or editing sidecar JSON by hand.
11094
+ Magic Markdown keeps Markdown files clean. Review state lives in DocumentAuthority: comments, suggestions, accept, and reject must use \`mdocs remote ...\` or the remote MCP connector, not local \`.mdocs\` files.
11637
11095
 
11638
11096
  ## Install / Bootstrap
11639
11097
 
@@ -11653,7 +11111,7 @@ Run \`mdocs help <command>\` (or \`mdocs <command> --help\`) for usage, examples
11653
11111
  4. 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>\`.
11654
11112
  5. 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.
11655
11113
  6. 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.
11656
- 7. 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 hand-edit canonical Markdown files or \`.mdocs\` reviewMarkdown syntax directly.
11114
+ 7. 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 hand-edit canonical Markdown files or local review metadata directly.
11657
11115
 
11658
11116
  ## Filesystem Bridge / Resume
11659
11117
 
@@ -11693,7 +11151,7 @@ If Magic and the local root both changed the same document while you were offlin
11693
11151
 
11694
11152
  ## Editing Rules
11695
11153
 
11696
- - Comments are sidecar operations; suggestions are reviewMarkdown operations derived into sidecar state. They should not modify canonical Markdown until a suggestion is accepted.
11154
+ - Comments and suggestions are review operations. They should not modify canonical Markdown until a suggestion is accepted through authority.
11697
11155
  - \`--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.
11698
11156
  - 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.
11699
11157
  - 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.
@@ -11878,14 +11336,14 @@ async function runDoctor(io, workspaceRoot) {
11878
11336
  details: `${map.docs.length} Markdown document${map.docs.length === 1 ? "" : "s"} indexed.`
11879
11337
  },
11880
11338
  {
11881
- name: "sidecar_anchor_mapping",
11339
+ name: "authority_projection_mapping",
11882
11340
  ok: warnings.length === 0,
11883
- details: warnings.length === 0 ? "All sidecar anchors are mapped." : `${warnings.length} document(s) have anchors needing review.`
11341
+ details: warnings.length === 0 ? "No authority projection mapping warnings." : `${warnings.length} document(s) have review targets needing projection attention.`
11884
11342
  },
11885
11343
  {
11886
11344
  name: "agent_cli_contract",
11887
11345
  ok: AGENT_COMMANDS.length > 0,
11888
- details: "Agent guide, JSON commands, checkpoints, document context, suggestions, comments, and MCP stdio are available."
11346
+ details: "Agent guide, JSON commands, checkpoints, clean document context, and MCP stdio are available. Review mutations require remote authority commands."
11889
11347
  }
11890
11348
  ];
11891
11349
  return {
@@ -11947,6 +11405,14 @@ var NodeWorkspaceIO = class {
11947
11405
  return false;
11948
11406
  }
11949
11407
  }
11408
+ async statFile(path) {
11409
+ try {
11410
+ const stats = await stat2(await this.resolveExistingPath(path));
11411
+ return { mtimeMs: stats.mtimeMs, size: stats.size };
11412
+ } catch {
11413
+ return void 0;
11414
+ }
11415
+ }
11950
11416
  async mkdir(path) {
11951
11417
  const target = await this.resolvePath(path);
11952
11418
  await mkdir2(target, { recursive: true });
@@ -12001,6 +11467,9 @@ var PathMappedWorkspaceIO = class {
12001
11467
  async exists(path) {
12002
11468
  return this.local.exists(this.toReplicaPath(path));
12003
11469
  }
11470
+ async statFile(path) {
11471
+ return this.local.statFile(this.toReplicaPath(path));
11472
+ }
12004
11473
  async mkdir(path) {
12005
11474
  await this.local.mkdir(this.toReplicaPath(path));
12006
11475
  }
@@ -12098,28 +11567,6 @@ function optionalString(args, name) {
12098
11567
  const value = args[name];
12099
11568
  return typeof value === "string" ? value : void 0;
12100
11569
  }
12101
- function requiredNumber(args, name) {
12102
- const value = args[name];
12103
- const number = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
12104
- if (!Number.isFinite(number)) throw new Error(`Missing numeric tool argument: ${name}`);
12105
- return number;
12106
- }
12107
- function actorFromArgs(args, fallback) {
12108
- const id = optionalString(args, "actorId") ?? fallback.id;
12109
- const name = optionalString(args, "actorName") ?? fallback.name;
12110
- const email = optionalString(args, "actorEmail") ?? fallback.email;
12111
- const kind = actorKindFromArg(args.actorKind) ?? fallback.kind;
12112
- return {
12113
- id,
12114
- kind,
12115
- name,
12116
- ...email ? { email } : {}
12117
- };
12118
- }
12119
- function actorKindFromArg(value) {
12120
- if (value === "human" || value === "agent" || value === "system") return value;
12121
- return void 0;
12122
- }
12123
11570
 
12124
11571
  // src/mcp-resources.ts
12125
11572
  import { readFile as readFile3 } from "node:fs/promises";
@@ -12180,19 +11627,12 @@ async function contextForDocument(io, path, startLine, endLine, options = {}) {
12180
11627
  markdown,
12181
11628
  images: withMcpImageResources(io, state).images,
12182
11629
  links: state.links,
12183
- ...options.includeReview === false ? {} : reviewFields(state)
11630
+ ...options.includeReview === false ? {} : { review: reviewStateForDocument(state) }
12184
11631
  };
12185
11632
  }
12186
11633
  async function reviewForDocument(io, path) {
12187
11634
  return reviewStateForDocument(await getDocumentState(io, path));
12188
11635
  }
12189
- function reviewFields(state) {
12190
- return {
12191
- comments: state.sidecar.comments.filter((comment2) => comment2.status === "open"),
12192
- suggestions: state.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
12193
- anchors: state.anchors
12194
- };
12195
- }
12196
11636
  function imageResourceUri(docId, index) {
12197
11637
  return `mdocs://documents/${docId}/images/${index}`;
12198
11638
  }
@@ -12233,25 +11673,6 @@ var documentPathSchema = {
12233
11673
  required: ["path"],
12234
11674
  additionalProperties: false
12235
11675
  };
12236
- var actorProperties = {
12237
- actorId: {
12238
- type: "string",
12239
- description: "Optional actor id to record in sidecar metadata."
12240
- },
12241
- actorName: {
12242
- type: "string",
12243
- description: "Optional actor display name to record in sidecar metadata."
12244
- },
12245
- actorKind: {
12246
- type: "string",
12247
- enum: ["human", "agent", "system"],
12248
- description: "Optional actor kind to record in sidecar metadata."
12249
- },
12250
- actorEmail: {
12251
- type: "string",
12252
- description: "Optional actor email to record in sidecar metadata."
12253
- }
12254
- };
12255
11676
  var tools = [
12256
11677
  {
12257
11678
  name: "mdocs_map",
@@ -12266,105 +11687,45 @@ var tools = [
12266
11687
  {
12267
11688
  name: "mdocs_doctor",
12268
11689
  description: "Validate the workspace and return agent-readiness diagnostics.",
12269
- inputSchema: noArgsSchema
12270
- },
12271
- {
12272
- name: "mdocs_context",
12273
- 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.",
12274
- inputSchema: {
12275
- type: "object",
12276
- properties: {
12277
- path: documentPathSchema.properties.path,
12278
- startLine: {
12279
- type: "number",
12280
- description: "1-based first line to include (default: 1). The response startLine/endLine/totalLines fields tell you what was returned and whether more pages exist."
12281
- },
12282
- endLine: {
12283
- type: "number",
12284
- description: "1-based last line to include (default: last line of document)."
12285
- },
12286
- summary: {
12287
- type: "boolean",
12288
- description: "Return metadata, heading outline, current head, and counts instead of Markdown content."
12289
- },
12290
- includeReview: {
12291
- type: "boolean",
12292
- 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."
12293
- }
12294
- },
12295
- required: ["path"],
12296
- additionalProperties: false
12297
- }
12298
- },
12299
- {
12300
- name: "mdocs_review",
12301
- description: "Return open comments, open suggestions, and mapped anchors for one document without returning document Markdown.",
12302
- inputSchema: documentPathSchema
12303
- },
12304
- {
12305
- name: "mdocs_state",
12306
- description: "Return full merged Markdown and sidecar state for one document.",
12307
- inputSchema: documentPathSchema
12308
- },
12309
- {
12310
- name: "mdocs_comment",
12311
- description: "Create a sidecar comment without modifying clean Markdown.",
12312
- inputSchema: {
12313
- type: "object",
12314
- properties: {
12315
- path: documentPathSchema.properties.path,
12316
- startLine: { type: "number", description: "1-based start line." },
12317
- endLine: { type: "number", description: "1-based end line." },
12318
- body: { type: "string", description: "Comment body." },
12319
- ...actorProperties
12320
- },
12321
- required: ["path", "startLine", "endLine", "body"],
12322
- additionalProperties: false
12323
- }
12324
- },
12325
- {
12326
- name: "mdocs_suggest",
12327
- description: "Create a reviewMarkdown replacement suggestion without modifying canonical Markdown. Prefer the smallest coherent range; use longer ranges only when the broader rewrite is genuinely needed.",
12328
- inputSchema: {
12329
- type: "object",
12330
- properties: {
12331
- path: documentPathSchema.properties.path,
12332
- startLine: { type: "number", description: "1-based start line." },
12333
- endLine: { type: "number", description: "1-based end line." },
12334
- replacement: { type: "string", description: "Exact replacement Markdown for the line range; keep unchanged surrounding text identical for narrow edits." },
12335
- message: { type: "string", description: "Short explanation for the suggestion." },
12336
- ...actorProperties
12337
- },
12338
- required: ["path", "startLine", "endLine", "replacement"],
12339
- additionalProperties: false
12340
- }
11690
+ inputSchema: noArgsSchema
12341
11691
  },
12342
11692
  {
12343
- name: "mdocs_resolve_comment",
12344
- description: "Resolve a sidecar comment thread.",
11693
+ name: "mdocs_context",
11694
+ description: "Return agent-ready clean 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. Review mutations require the remote MCP connector or mdocs remote commands.",
12345
11695
  inputSchema: {
12346
11696
  type: "object",
12347
11697
  properties: {
12348
- commentId: { type: "string", description: "Comment thread id." },
12349
- ...actorProperties
11698
+ path: documentPathSchema.properties.path,
11699
+ startLine: {
11700
+ type: "number",
11701
+ description: "1-based first line to include (default: 1). The response startLine/endLine/totalLines fields tell you what was returned and whether more pages exist."
11702
+ },
11703
+ endLine: {
11704
+ type: "number",
11705
+ description: "1-based last line to include (default: last line of document)."
11706
+ },
11707
+ summary: {
11708
+ type: "boolean",
11709
+ description: "Return metadata, heading outline, current head, and counts instead of Markdown content."
11710
+ },
11711
+ includeReview: {
11712
+ type: "boolean",
11713
+ description: "Whether to include authority-review redirect metadata in the content response (default: true). Use false to keep document reads minimal."
11714
+ }
12350
11715
  },
12351
- required: ["commentId"],
11716
+ required: ["path"],
12352
11717
  additionalProperties: false
12353
11718
  }
12354
11719
  },
12355
11720
  {
12356
- name: "mdocs_resolve_suggestion",
12357
- description: "Accept or reject a suggestion.",
12358
- inputSchema: {
12359
- type: "object",
12360
- properties: {
12361
- suggestionId: { type: "string", description: "Suggestion id." },
12362
- status: { type: "string", enum: ["accepted", "rejected"], description: "Resolution status." },
12363
- ...actorProperties
12364
- },
12365
- required: ["suggestionId", "status"],
12366
- additionalProperties: false
12367
- }
11721
+ name: "mdocs_review",
11722
+ description: "Return local review redirect metadata for one document. Use remote MCP or mdocs remote review for authority-backed comments and suggestions.",
11723
+ inputSchema: documentPathSchema
11724
+ },
11725
+ {
11726
+ name: "mdocs_state",
11727
+ description: "Return clean Markdown document state and metadata for one document.",
11728
+ inputSchema: documentPathSchema
12368
11729
  },
12369
11730
  {
12370
11731
  name: "mdocs_diff",
@@ -12560,38 +11921,8 @@ async function callTool(io, name, args) {
12560
11921
  }
12561
11922
  if (name === "mdocs_review") return reviewForDocument(io, requiredString(args, "path"));
12562
11923
  if (name === "mdocs_state") return withMcpImageResources(io, await getDocumentState(io, requiredString(args, "path")));
12563
- if (name === "mdocs_comment") {
12564
- return addComment(
12565
- io,
12566
- requiredString(args, "path"),
12567
- { startLine: requiredNumber(args, "startLine"), endLine: requiredNumber(args, "endLine") },
12568
- requiredString(args, "body"),
12569
- actorFromArgs(args, defaultActor())
12570
- );
12571
- }
12572
- if (name === "mdocs_suggest") {
12573
- return addSuggestion(
12574
- io,
12575
- requiredString(args, "path"),
12576
- { startLine: requiredNumber(args, "startLine"), endLine: requiredNumber(args, "endLine") },
12577
- requiredString(args, "replacement"),
12578
- optionalString(args, "message") ?? "Suggested edit",
12579
- actorFromArgs(args, {
12580
- id: "agent_local",
12581
- kind: "agent",
12582
- name: "Local Agent"
12583
- })
12584
- );
12585
- }
12586
- if (name === "mdocs_resolve_suggestion") {
12587
- const suggestionId = requiredString(args, "suggestionId");
12588
- const status = requiredString(args, "status");
12589
- if (status === "accepted") return acceptSuggestion(io, suggestionId, actorFromArgs(args, defaultActor()));
12590
- if (status === "rejected") return rejectSuggestion(io, suggestionId, actorFromArgs(args, defaultActor()));
12591
- throw new Error("status must be accepted or rejected");
12592
- }
12593
- if (name === "mdocs_resolve_comment") {
12594
- return resolveComment(io, requiredString(args, "commentId"), actorFromArgs(args, defaultActor()));
11924
+ if (name === "mdocs_comment" || name === "mdocs_suggest" || name === "mdocs_resolve_suggestion" || name === "mdocs_resolve_comment") {
11925
+ throw new Error("Local MCP review mutations were removed. Use the remote MCP connector or `mdocs remote ...` so review state goes through DocumentAuthority.");
12595
11926
  }
12596
11927
  if (name === "mdocs_diff") {
12597
11928
  return { patch: await gitDiff(io.root, optionalString(args, "path")) };
@@ -12802,33 +12133,47 @@ function splitWorkspaceDocumentTarget(value, rootIds) {
12802
12133
  const target = value.slice(separator + 1);
12803
12134
  return target ? { rootId, target } : void 0;
12804
12135
  }
12805
- async function postReview(record, docId, additions, actor) {
12806
- const response = await fetchJson(
12807
- `${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/review`,
12136
+ async function postSuggestionBranch(record, docId, input, actor) {
12137
+ return fetchJson(
12138
+ `${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/suggestions`,
12139
+ {
12140
+ method: "POST",
12141
+ headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
12142
+ body: JSON.stringify({ actor, ...input })
12143
+ }
12144
+ );
12145
+ }
12146
+ async function fetchAuthorityState(record, docId) {
12147
+ return fetchJson(
12148
+ `${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/authority-state`,
12149
+ { headers: shareHeaders(record.shareUrl) }
12150
+ );
12151
+ }
12152
+ async function postResolveSuggestionBranch(record, docId, suggestionId, resolution, actor, changeId = randomUUID2()) {
12153
+ return fetchJson(
12154
+ `${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/suggestions/${encodeURIComponent(suggestionId)}/${resolution}`,
12808
12155
  {
12809
12156
  method: "POST",
12810
12157
  headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
12811
- body: JSON.stringify({ actor, changeId: randomUUID2(), ...additions })
12158
+ body: JSON.stringify({ actor, changeId })
12812
12159
  }
12813
12160
  );
12814
- return response.document;
12815
12161
  }
12816
- async function postReviewOperation(record, docId, operation, actor) {
12162
+ async function postAuthorityComment(record, docId, input, actor) {
12817
12163
  return fetchJson(
12818
- `${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/review-ops`,
12164
+ `${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/comments`,
12819
12165
  {
12820
12166
  method: "POST",
12821
12167
  headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
12822
- body: JSON.stringify({ actor, operation })
12168
+ body: JSON.stringify({ actor, ...input })
12823
12169
  }
12824
12170
  );
12825
12171
  }
12826
- async function pushDocument(record, baseDocument, markdown, sidecar) {
12172
+ async function pushDocument(record, baseDocument, markdown) {
12827
12173
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
12828
12174
  const payload = {
12829
12175
  ...baseDocument,
12830
12176
  markdown,
12831
- sidecar,
12832
12177
  baseHead: baseDocument.currentSha,
12833
12178
  currentSha: baseDocument.currentSha
12834
12179
  };
@@ -13027,7 +12372,7 @@ function parseRange(value) {
13027
12372
  }
13028
12373
 
13029
12374
  // src/remote-join-store.ts
13030
- import { createHash } from "node:crypto";
12375
+ import { createHash as createHash2 } from "node:crypto";
13031
12376
  import { mkdir as mkdir3, readFile as readFile5, readdir as readdir3, writeFile as writeFile3 } from "node:fs/promises";
13032
12377
  import { join as join3 } from "node:path";
13033
12378
  async function readSelectedJoin(root, flags) {
@@ -13061,7 +12406,7 @@ async function writeJoinRecord(root, record) {
13061
12406
  `, "utf8");
13062
12407
  }
13063
12408
  function joinIdFor(value) {
13064
- return `join_${createHash("sha256").update(value).digest("hex").slice(0, 16)}`;
12409
+ return `join_${createHash2("sha256").update(value).digest("hex").slice(0, 16)}`;
13065
12410
  }
13066
12411
  function looksLikeUrl(value) {
13067
12412
  try {
@@ -13081,6 +12426,7 @@ function joinStoreDir(root) {
13081
12426
  }
13082
12427
 
13083
12428
  // src/remote.ts
12429
+ var WORKSPACE_GRAPH_SCHEMA_VERSION = 1;
13084
12430
  async function runJoinCommand(root, parsed) {
13085
12431
  const target = parsed.command[1];
13086
12432
  const existingId = typeof parsed.flags.id === "string" ? parsed.flags.id : void 0;
@@ -13131,7 +12477,7 @@ async function runRemoteCommand(root, subcommand, parsed) {
13131
12477
  if (record.scope === "workspace") return remoteWorkspaceMap(record);
13132
12478
  const rootRecord = assertRootScoped(record, "map");
13133
12479
  await refreshPresence(rootRecord, record.docId);
13134
- return fetchTree(rootRecord);
12480
+ return treeWithAuthorityCounts(rootRecord, await fetchTree(rootRecord));
13135
12481
  }
13136
12482
  case "graph":
13137
12483
  return remoteGraph(record);
@@ -13225,28 +12571,35 @@ async function remoteGraph(record) {
13225
12571
  assertProjectScope(record, "graph");
13226
12572
  const rootRecord = assertRootScoped(record, "graph");
13227
12573
  await refreshPresence(rootRecord, record.docId);
13228
- const tree = await fetchTree(rootRecord);
12574
+ const tree = await treeWithAuthorityCounts(rootRecord, await fetchTree(rootRecord));
13229
12575
  const documents = await Promise.all(tree.docs.map((doc) => fetchDocument(rootScopedRecordFor(record, rootRecord.rootId), doc.docId)));
12576
+ const authorityCounts = new Map(tree.docs.map((doc) => [doc.docId, doc]));
13230
12577
  const lookup = documents.map((doc) => ({ docId: doc.docId, path: doc.path, title: doc.title }));
13231
12578
  const graphDocuments = documents.map((doc) => {
13232
12579
  const links = doc.links ?? resolveMarkdownLinks(extractMarkdownLinks(doc.markdown), doc, lookup);
12580
+ const counts = authorityCounts.get(doc.docId);
13233
12581
  return {
13234
12582
  docId: doc.docId,
13235
12583
  path: doc.path,
13236
12584
  title: doc.title,
13237
- openComments: doc.openComments,
13238
- openSuggestions: doc.openSuggestions,
13239
- anchorsNeedingReview: doc.anchors.filter((anchor) => anchor.status !== "mapped").length,
12585
+ openComments: counts?.openComments ?? 0,
12586
+ openSuggestions: counts?.openSuggestions ?? 0,
12587
+ anchorsNeedingReview: counts?.anchorsNeedingReview ?? 0,
13240
12588
  externalLinks: links.filter((link2) => link2.status === "external").length,
13241
12589
  unresolvedLinks: links.filter((link2) => link2.status === "unresolved" || link2.status === "ambiguous").length,
13242
12590
  links
13243
12591
  };
13244
12592
  });
13245
- return buildWorkspaceGraph(record.workspaceId, SIDECAR_SCHEMA_VERSION, graphDocuments);
12593
+ return buildWorkspaceGraph(record.workspaceId, WORKSPACE_GRAPH_SCHEMA_VERSION, graphDocuments);
13246
12594
  }
13247
12595
  async function remoteWorkspaceMap(record) {
13248
12596
  const roots = await fetchWorkspaceRoots(record);
13249
- const trees = await Promise.all(roots.map((root) => fetchTree(rootScopedRecordFor(record, root.rootId))));
12597
+ const trees = await Promise.all(
12598
+ roots.map(async (root) => {
12599
+ const rootRecord = rootScopedRecordFor(record, root.rootId);
12600
+ return treeWithAuthorityCounts(rootRecord, await fetchTree(rootRecord));
12601
+ })
12602
+ );
13250
12603
  const library = await fetchWorkspaceLibrary(record).catch(() => void 0);
13251
12604
  return {
13252
12605
  workspaceId: record.workspaceId,
@@ -13270,6 +12623,27 @@ async function remoteWorkspaceMap(record) {
13270
12623
  ...library ? { folders: library.folders } : {}
13271
12624
  };
13272
12625
  }
12626
+ async function treeWithAuthorityCounts(record, tree) {
12627
+ const counts = await Promise.all(
12628
+ tree.docs.map(async (doc) => ({
12629
+ docId: doc.docId,
12630
+ counts: authorityReviewCounts(await fetchAuthorityState(record, doc.docId).catch(() => void 0))
12631
+ }))
12632
+ );
12633
+ const countsByDocId = new Map(counts.map((entry) => [entry.docId, entry.counts]));
12634
+ return {
12635
+ ...tree,
12636
+ docs: tree.docs.map((doc) => {
12637
+ const review = countsByDocId.get(doc.docId);
12638
+ return review ? {
12639
+ ...doc,
12640
+ openComments: review.openComments,
12641
+ openSuggestions: review.openSuggestions,
12642
+ anchorsNeedingReview: review.anchorsNeedingReview
12643
+ } : doc;
12644
+ })
12645
+ };
12646
+ }
13273
12647
  async function remoteHistory(record, flags) {
13274
12648
  const rootRecord = assertRootScoped(record, "history");
13275
12649
  await refreshPresence(rootRecord, record.docId);
@@ -13329,9 +12703,11 @@ async function refreshPresence(record, docId) {
13329
12703
  }
13330
12704
  async function remoteContext(record, pathOrDocId, flags = {}) {
13331
12705
  const document = pathOrDocId ? await fetchDocument(record, pathOrDocId) : (await fetchState(record)).document;
13332
- await refreshPresence(rootScopedRecordFor(record, document.rootId), document.docId);
12706
+ const rootRecord = rootScopedRecordFor(record, document.rootId);
12707
+ await refreshPresence(rootRecord, document.docId);
12708
+ const authority = flags["no-review"] ? void 0 : await fetchAuthorityState(rootRecord, document.docId).catch(() => void 0);
13333
12709
  if (flags.summary || flags["metadata-only"]) {
13334
- return withRemoteSummaryNextCommands(summarizeDocumentContext(document), record, document, pathOrDocId);
12710
+ return withRemoteSummaryNextCommands(summarizeDocumentContextFromAuthority(document, authority), record, document, pathOrDocId);
13335
12711
  }
13336
12712
  const startLine = typeof flags["start-line"] === "string" ? Number(flags["start-line"]) : void 0;
13337
12713
  const endLine = typeof flags["end-line"] === "string" ? Number(flags["end-line"]) : void 0;
@@ -13347,22 +12723,24 @@ async function remoteContext(record, pathOrDocId, flags = {}) {
13347
12723
  startLine: sl,
13348
12724
  endLine: el,
13349
12725
  markdown,
13350
- ...flags["no-review"] ? {} : {
13351
- comments: document.sidecar.comments.filter((comment2) => comment2.status === "open"),
13352
- suggestions: document.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
13353
- anchors: document.anchors
13354
- },
12726
+ ...flags["no-review"] ? {} : authorityReviewPayload(authority),
13355
12727
  images: document.images,
13356
12728
  links: document.links
13357
12729
  };
13358
12730
  }
13359
12731
  async function remoteReview(record, pathOrDocId) {
13360
12732
  const document = pathOrDocId ? await fetchDocument(record, pathOrDocId) : (await fetchState(record)).document;
13361
- await refreshPresence(rootScopedRecordFor(record, document.rootId), document.docId);
12733
+ const rootRecord = rootScopedRecordFor(record, document.rootId);
12734
+ await refreshPresence(rootRecord, document.docId);
12735
+ const authority = await fetchAuthorityState(rootRecord, document.docId).catch(() => void 0);
13362
12736
  return {
13363
12737
  joinId: record.joinId,
13364
12738
  scope: record.scope,
13365
- ...reviewStateForDocument(document)
12739
+ docId: document.docId,
12740
+ path: document.path,
12741
+ title: document.title,
12742
+ currentSha: document.currentSha,
12743
+ ...authorityReviewPayload(authority)
13366
12744
  };
13367
12745
  }
13368
12746
  function withRemoteSummaryNextCommands(summary, record, document, requestedTarget) {
@@ -13382,6 +12760,48 @@ function remoteCommandTarget(record, document, requestedTarget) {
13382
12760
  if (record.scope === "workspace") return ` ${document.rootId}:${document.docId}`;
13383
12761
  return ` ${document.docId}`;
13384
12762
  }
12763
+ function summarizeDocumentContextFromAuthority(document, authority) {
12764
+ const summary = summarizeDocumentContext(document);
12765
+ return {
12766
+ ...summary,
12767
+ reviewCounts: authorityReviewCounts(authority)
12768
+ };
12769
+ }
12770
+ function authorityReviewPayload(authority) {
12771
+ return {
12772
+ comments: authorityOpenComments(authority),
12773
+ suggestions: authorityOpenSuggestions(authority),
12774
+ commentTargets: authority?.commentProjections ?? []
12775
+ };
12776
+ }
12777
+ function authorityReviewCounts(authority) {
12778
+ const suggestions = authorityOpenSuggestions(authority);
12779
+ const comments = authorityOpenComments(authority);
12780
+ return {
12781
+ openComments: comments.length,
12782
+ openSuggestions: suggestions.length,
12783
+ anchorsNeedingReview: (authority?.commentProjections ?? []).filter((projection) => authorityRecordStatus(projection) !== "mapped").length + suggestions.filter((suggestion) => {
12784
+ const projection = suggestion && typeof suggestion === "object" ? suggestion.projection : void 0;
12785
+ const status = authorityRecordStatus(projection);
12786
+ return status === "conflicted" || status === "stale";
12787
+ }).length
12788
+ };
12789
+ }
12790
+ function authorityOpenComments(authority) {
12791
+ return (authority?.comments ?? []).filter((comment2) => {
12792
+ const status = authorityRecordStatus(comment2);
12793
+ return status === void 0 || status === "open" || status === "displaced";
12794
+ });
12795
+ }
12796
+ function authorityOpenSuggestions(authority) {
12797
+ return (authority?.suggestions ?? []).filter((suggestion) => authorityRecordStatus(suggestion) === "open");
12798
+ }
12799
+ function authorityRecordId(value) {
12800
+ return value && typeof value === "object" && typeof value.id === "string" ? value.id : void 0;
12801
+ }
12802
+ function authorityRecordStatus(value) {
12803
+ return value && typeof value === "object" && typeof value.status === "string" ? value.status : void 0;
12804
+ }
13385
12805
  async function remoteCreateFile(root, record, parsed) {
13386
12806
  assertProjectScope(record, "create-file");
13387
12807
  const rootRecord = assertRootScoped(record, "create-file");
@@ -13402,7 +12822,7 @@ async function remoteCreateFile(root, record, parsed) {
13402
12822
  }
13403
12823
  const docId = createDocId();
13404
12824
  const title = titleFromMarkdown(normalizedPath, markdown);
13405
- const sidecar = emptySidecar(docId, normalizedPath, title);
12825
+ const now = (/* @__PURE__ */ new Date()).toISOString();
13406
12826
  const baseDocument = {
13407
12827
  workspaceId: rootRecord.workspaceId,
13408
12828
  rootId: rootRecord.rootId,
@@ -13410,16 +12830,15 @@ async function remoteCreateFile(root, record, parsed) {
13410
12830
  path: normalizedPath,
13411
12831
  title,
13412
12832
  markdown,
13413
- reviewMarkdown: markdown,
13414
- sidecar,
13415
- anchors: [],
13416
12833
  images: [],
13417
12834
  links: [],
13418
12835
  openComments: 0,
13419
12836
  openSuggestions: 0,
12837
+ anchorsNeedingReview: 0,
12838
+ updatedAt: now,
13420
12839
  currentSha: tree.root.canonical.head
13421
12840
  };
13422
- const document = await pushDocument(rootRecord, baseDocument, markdown, sidecar);
12841
+ const document = await pushDocument(rootRecord, baseDocument, markdown);
13423
12842
  await recordHead(root, record, document);
13424
12843
  return { document };
13425
12844
  }
@@ -13444,8 +12863,7 @@ async function remoteMoveFile(root, record, parsed) {
13444
12863
  const moved = await pushDocument(
13445
12864
  rootScopedRecordFor(record, document.rootId),
13446
12865
  { ...document, path: nextPath, title: titleFromMarkdown(nextPath, document.markdown) },
13447
- document.markdown,
13448
- document.sidecar
12866
+ document.markdown
13449
12867
  );
13450
12868
  await recordHead(root, record, moved);
13451
12869
  return { document: moved };
@@ -13453,13 +12871,17 @@ async function remoteMoveFile(root, record, parsed) {
13453
12871
  async function remoteComment(root, record, parsed) {
13454
12872
  const body = await readRequiredTextFlag(parsed.flags, root, ["body", "message"]);
13455
12873
  const range = parseRange(requiredFlag(parsed.flags, "range"));
13456
- const result = await submitReview(
13457
- root,
13458
- record,
13459
- parsed.command[2] ?? record.docId,
13460
- (io, docId) => addComment(io, docId, range, body, actorForRecord(record))
13461
- );
13462
- return { comment: result.created, document: result.document };
12874
+ const pathOrDocId = parsed.command[2] ?? record.docId;
12875
+ if (!pathOrDocId) {
12876
+ throw new CliError("usage_error", "Missing document path or docId.", {
12877
+ hint: "Pass a document path or docId, or join with --doc <docId>."
12878
+ });
12879
+ }
12880
+ const document = await fetchDocument(record, pathOrDocId);
12881
+ const documentRecord = rootScopedRecordFor(record, document.rootId);
12882
+ await refreshPresence(documentRecord, document.docId);
12883
+ const result = await postAuthorityComment(documentRecord, document.docId, { ...range, body }, actorForRecord(record));
12884
+ return { comment: result.thread, projection: result.projection, document: { docId: document.docId, path: document.path, currentSha: document.currentSha } };
13463
12885
  }
13464
12886
  async function remoteSuggest(root, record, parsed) {
13465
12887
  const replacement = await readRequiredTextFlag(parsed.flags, root, ["with", "replacement"]);
@@ -13474,15 +12896,35 @@ async function remoteSuggest(root, record, parsed) {
13474
12896
  const document = await fetchDocument(record, pathOrDocId);
13475
12897
  const documentRecord = rootScopedRecordFor(record, document.rootId);
13476
12898
  await refreshPresence(documentRecord, document.docId);
13477
- const result = await postReviewOperation(
12899
+ const authority = await fetchAuthorityState(documentRecord, document.docId);
12900
+ if (!authority.head) {
12901
+ throw new CliError("conflict", "Document authority is not initialized yet.", {
12902
+ hint: "Open the document once or retry after the worker initializes authority-state."
12903
+ });
12904
+ }
12905
+ const clientMutationId = typeof parsed.flags["client-mutation-id"] === "string" ? parsed.flags["client-mutation-id"] : typeof parsed.flags["mutation-id"] === "string" ? parsed.flags["mutation-id"] : void 0;
12906
+ const patch = buildLineRangePatchEnvelope({
12907
+ baseVersion: authority.head.headVersion,
12908
+ base: authority.markdown ?? document.markdown,
12909
+ range,
12910
+ replacement,
12911
+ clientMutationId,
12912
+ clientMeta: { source: "cli_remote_suggest" }
12913
+ });
12914
+ const result = await postSuggestionBranch(
13478
12915
  documentRecord,
13479
12916
  document.docId,
13480
- { kind: "create_suggestion", baseHead: document.currentSha, payload: { ...range, replacement, message } },
12917
+ { patch, title: message },
13481
12918
  actorForRecord(record)
13482
12919
  );
13483
- if (result.document) await recordHead(root, record, result.document);
13484
- const placementStatus = result.reviewRecord?.placement?.status;
13485
- return { suggestion: result.suggestion, reviewRecord: result.reviewRecord, placementStatus, projectionStatus: result.projectionStatus, document: result.document };
12920
+ return {
12921
+ suggestion: result.suggestion,
12922
+ suggestionUpdate: result.update,
12923
+ suggestionId: result.suggestion?.id,
12924
+ projectionStatus: result.projection?.status,
12925
+ replayed: result.replayed,
12926
+ document: { docId: document.docId, path: document.path, currentSha: document.currentSha }
12927
+ };
13486
12928
  }
13487
12929
  async function remoteCreateFolder(record, parsed) {
13488
12930
  assertProjectScope(record, "create-folder");
@@ -13561,53 +13003,14 @@ async function remoteReject(root, record, parsed) {
13561
13003
  let documentRecord = rootScopedRecordFor(record, document.rootId);
13562
13004
  await refreshPresence(documentRecord, document.docId);
13563
13005
  const actor = actorForRecord(record);
13564
- const suggestion = document.sidecar.suggestions.find((candidate) => candidate.id === suggestionId);
13565
- if (!suggestion) {
13566
- throw new CliError("not_found", `Unknown suggestion: ${suggestionId}`, {
13567
- hint: "List suggestion ids with mdocs remote review --json."
13568
- });
13569
- }
13570
- if (suggestion.status !== "open") {
13571
- throw new CliError("usage_error", `Suggestion ${suggestionId} is already ${suggestion.status}.`, {
13006
+ const authority = await fetchAuthorityState(documentRecord, document.docId).catch(() => void 0);
13007
+ const suggestionStatus = authorityRecordStatus(authority?.suggestions?.find((candidate) => authorityRecordId(candidate) === suggestionId));
13008
+ if (suggestionStatus && suggestionStatus !== "open") {
13009
+ throw new CliError("usage_error", `Suggestion ${suggestionId} is already ${suggestionStatus}.`, {
13572
13010
  hint: "Only open suggestions can be rejected."
13573
13011
  });
13574
13012
  }
13575
- const result = await postReviewOperation(
13576
- documentRecord,
13577
- document.docId,
13578
- { kind: "reject_suggestion", baseHead: document.currentSha, payload: { suggestionId } },
13579
- actor
13580
- );
13581
- if (result.document) await recordHead(root, record, result.document);
13582
- return { suggestion: result.suggestion ?? { ...suggestion, status: "rejected" }, reviewRecord: result.reviewRecord, document: result.document };
13583
- }
13584
- async function submitReview(root, record, pathOrDocId, mutate) {
13585
- if (!pathOrDocId) {
13586
- throw new CliError("usage_error", "Missing document path or docId.", {
13587
- hint: "Pass a document path or docId, or join with --doc <docId>."
13588
- });
13589
- }
13590
- let document = await fetchDocument(record, pathOrDocId);
13591
- let documentRecord = rootScopedRecordFor(record, document.rootId);
13592
- await refreshPresence(documentRecord, document.docId);
13593
- const build = async (base) => {
13594
- const io = new RemoteDocumentIO(base);
13595
- const created = await mutate(io, base.docId);
13596
- const state = await getDocumentState(io, base.docId);
13597
- return {
13598
- created,
13599
- state,
13600
- additions: {
13601
- anchors: state.sidecar.anchors.filter((anchor) => anchor.id === created.anchorId),
13602
- comments: state.sidecar.comments.filter((comment2) => comment2.id === created.id),
13603
- reviewMarkdown: state.sidecar.reviewMarkdown
13604
- }
13605
- };
13606
- };
13607
- const attempt = await build(document);
13608
- const pushed = await postReview(documentRecord, document.docId, attempt.additions, actorForRecord(record));
13609
- await recordHead(root, record, pushed);
13610
- return { created: attempt.created, document: pushed };
13013
+ return postResolveSuggestionBranch(documentRecord, document.docId, suggestionId, "reject", actor);
13611
13014
  }
13612
13015
  async function recordHead(root, record, document) {
13613
13016
  await writeJoinRecord(root, { ...record, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), currentHead: document.currentSha });
@@ -13737,19 +13140,6 @@ function rootScopedRecordFor(record, rootId) {
13737
13140
  scope: record.scope === "file" ? "file" : "project"
13738
13141
  };
13739
13142
  }
13740
- function emptySidecar(docId, path, title) {
13741
- return {
13742
- schemaVersion: SIDECAR_SCHEMA_VERSION,
13743
- docId,
13744
- path,
13745
- title,
13746
- anchors: [],
13747
- comments: [],
13748
- suggestions: [],
13749
- changeSets: [],
13750
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13751
- };
13752
- }
13753
13143
  function defaultMarkdown(path) {
13754
13144
  return `# ${titleFromMarkdown(path, "")}
13755
13145
  `;
@@ -13779,14 +13169,55 @@ function optionalFolderTarget(value) {
13779
13169
  }
13780
13170
 
13781
13171
  // src/bridge.ts
13782
- import { createHash as createHash2, randomUUID as randomUUID3 } from "node:crypto";
13783
- import { chmod, mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
13172
+ import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
13173
+ import { unlinkSync } from "node:fs";
13174
+ import { chmod, mkdir as mkdir4, readFile as readFile6, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
13784
13175
  import { homedir } from "node:os";
13785
13176
  import { basename as basename2, join as join4, resolve as resolve4 } from "node:path";
13786
13177
  var BRIDGE_CONFIG_PATH = ".mdocs/bridge.json";
13787
13178
  var BRIDGE_STATUS_PATH = ".mdocs/bridge-status.json";
13179
+ var BRIDGE_LOCK_PATH = ".mdocs/bridge.lock";
13788
13180
  var BRIDGE_HEARTBEAT_INTERVAL_MS = 1e4;
13789
13181
  var BRIDGE_TOKEN_STORE_PATH = join4(homedir(), ".mdocs", "bridge-tokens.json");
13182
+ var BRIDGE_MIN_INTERVAL_MS = 500;
13183
+ var BRIDGE_DEFAULT_INTERVAL_MS = 1e3;
13184
+ var BRIDGE_RECONNECT_BASE_MS = 1e3;
13185
+ var BRIDGE_RECONNECT_CAP_MS = 3e4;
13186
+ function createSerialQueue() {
13187
+ let tail = Promise.resolve();
13188
+ return (task) => {
13189
+ const run = tail.then(task, task);
13190
+ tail = run.then(
13191
+ () => void 0,
13192
+ () => void 0
13193
+ );
13194
+ return run;
13195
+ };
13196
+ }
13197
+ function validateBridgeIntervalMs(raw) {
13198
+ if (typeof raw === "boolean" || raw === void 0 || raw === null || raw === "") return BRIDGE_DEFAULT_INTERVAL_MS;
13199
+ const value = Number(raw);
13200
+ if (!Number.isFinite(value) || value <= 0) return BRIDGE_DEFAULT_INTERVAL_MS;
13201
+ return Math.max(BRIDGE_MIN_INTERVAL_MS, Math.floor(value));
13202
+ }
13203
+ function reconnectBackoffCeiling(attempt, baseMs = BRIDGE_RECONNECT_BASE_MS, capMs = BRIDGE_RECONNECT_CAP_MS) {
13204
+ const exponent = Math.max(0, Math.floor(attempt));
13205
+ return Math.min(capMs, baseMs * 2 ** exponent);
13206
+ }
13207
+ function reconnectDelayMs(attempt, baseMs = BRIDGE_RECONNECT_BASE_MS, capMs = BRIDGE_RECONNECT_CAP_MS, random = Math.random) {
13208
+ return Math.floor(random() * (reconnectBackoffCeiling(attempt, baseMs, capMs) + 1));
13209
+ }
13210
+ function isConfirmingEcho(params) {
13211
+ if (params.messageSourceId && params.messageSourceId === params.selfSourceId) return true;
13212
+ return params.sentSignature !== void 0 && params.sentSignature === params.payloadSignature;
13213
+ }
13214
+ function shouldConflictCopyOnDelete(localSignature, baseSignature) {
13215
+ return baseSignature === void 0 || localSignature !== baseSignature;
13216
+ }
13217
+ function conflictCopyStamp(date = /* @__PURE__ */ new Date(), entropy = randomUUID3().slice(0, 6)) {
13218
+ const compact = date.toISOString().replaceAll(/[-:.]/g, "");
13219
+ return `${compact.slice(0, 15)}-${entropy}`;
13220
+ }
13790
13221
  function bridgeSetupIdentity(input) {
13791
13222
  const actorName = input.actorName?.trim() || "Agent";
13792
13223
  return {
@@ -13857,7 +13288,7 @@ async function runBridge(options) {
13857
13288
  }
13858
13289
  const localManifestSource = await readLocalSource(root);
13859
13290
  const rootId = options.rootId ?? options.sourceId ?? localManifestSource?.sourceId ?? rootIdForPath(root);
13860
- const replicaId = `replica_${createHash2("sha256").update(`${root}:${options.actorId}`).digest("hex").slice(0, 12)}`;
13291
+ const replicaId = `replica_${createHash3("sha256").update(`${root}:${options.actorId}`).digest("hex").slice(0, 12)}`;
13861
13292
  const replicaKind = actorKindForBridge(options.actorId) === "agent" ? "agent_runtime" : "local";
13862
13293
  const mapping = createSourceMapping({
13863
13294
  sourceId: rootId,
@@ -13874,21 +13305,33 @@ async function runBridge(options) {
13874
13305
  const pendingDocs = /* @__PURE__ */ new Set();
13875
13306
  const pendingDeletes = /* @__PURE__ */ new Set();
13876
13307
  const seenDocs = /* @__PURE__ */ new Set();
13308
+ const sentSignatures = /* @__PURE__ */ new Map();
13877
13309
  const deniedSignatures = /* @__PURE__ */ new Map();
13310
+ const indexCache = /* @__PURE__ */ new Map();
13311
+ const indexOptions = { cache: indexCache, rootId };
13312
+ const runManifest = createSerialQueue();
13878
13313
  let lastTreeSignature = "";
13879
13314
  const claimMode = Boolean(options.claimToken || localManifestSource?.canonicalHead);
13880
13315
  let lastAppliedHead = localManifestSource?.canonicalHead;
13881
13316
  let socket;
13317
+ let pollTimer;
13318
+ let reconnectTimer;
13319
+ let ticking = false;
13320
+ let reconnectAttempt = 0;
13321
+ let suppressOpenResume = false;
13322
+ let claimPrimed = false;
13882
13323
  let lastSuccessfulSyncAt;
13883
13324
  let lastHeartbeatAt;
13884
13325
  let lastHeartbeatSentMs = 0;
13885
13326
  let lastError;
13327
+ const lock = await acquireBridgeLock(root);
13886
13328
  await writeCurrentBridgeStatus("starting");
13887
13329
  let token = options.token ?? await readStoredBridgeToken(options, rootId).catch(() => void 0);
13888
13330
  if (!token && options.resume && !options.requestToken) {
13889
13331
  const message = "Missing stored bridge token. Re-run mdocs bridge setup from a fresh Magic binding packet.";
13890
13332
  lastError = message;
13891
13333
  await writeCurrentBridgeStatus("error");
13334
+ await lock.release();
13892
13335
  throw new Error(message);
13893
13336
  }
13894
13337
  if (!token && (options.claimToken || options.requestToken)) {
@@ -13899,6 +13342,7 @@ async function runBridge(options) {
13899
13342
  } catch (error) {
13900
13343
  lastError = errorMessage(error);
13901
13344
  await writeCurrentBridgeStatus("error");
13345
+ await lock.release();
13902
13346
  throw error;
13903
13347
  }
13904
13348
  }
@@ -13908,6 +13352,7 @@ async function runBridge(options) {
13908
13352
  } catch (error) {
13909
13353
  lastError = errorMessage(error);
13910
13354
  await writeCurrentBridgeStatus("error");
13355
+ await lock.release();
13911
13356
  throw error;
13912
13357
  }
13913
13358
  lastAppliedHead = lastAppliedHead ?? registeredRoot?.canonical.head;
@@ -13927,32 +13372,54 @@ async function runBridge(options) {
13927
13372
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13928
13373
  });
13929
13374
  const backfilled = await resumeFromCanonical();
13375
+ suppressOpenResume = true;
13930
13376
  if (options.once) {
13931
13377
  lastSuccessfulSyncAt = (/* @__PURE__ */ new Date()).toISOString();
13932
13378
  await writeCurrentBridgeStatus("once_complete");
13379
+ await lock.release();
13933
13380
  return;
13934
13381
  }
13382
+ if (claimMode && !backfilled) {
13383
+ await primeLocalSignatures();
13384
+ claimPrimed = true;
13385
+ }
13935
13386
  connect();
13936
- if (claimMode && !backfilled) await primeLocalSignatures();
13937
- else await publishSnapshot("initial");
13938
- const timer = setInterval(() => {
13939
- void publishSnapshot("poll").catch((error) => {
13940
- lastError = errorMessage(error);
13941
- void writeCurrentBridgeStatus("error").catch(() => void 0);
13942
- sendHeartbeat("error", lastError);
13943
- process.stderr.write(`bridge poll failed: ${lastError}
13944
- `);
13945
- });
13946
- }, options.intervalMs);
13387
+ scheduleNextPoll();
13947
13388
  process.once("SIGINT", () => {
13948
- clearInterval(timer);
13389
+ shutdownTimers();
13949
13390
  sendHeartbeat("offline", void 0, false);
13950
13391
  void writeCurrentBridgeStatus("stopped").catch(() => void 0).finally(() => {
13951
13392
  socket?.close();
13952
- process.exit(0);
13393
+ void lock.release().finally(() => process.exit(0));
13953
13394
  });
13954
13395
  });
13955
13396
  await new Promise(() => void 0);
13397
+ function shutdownTimers() {
13398
+ if (pollTimer) clearTimeout(pollTimer);
13399
+ if (reconnectTimer) clearTimeout(reconnectTimer);
13400
+ }
13401
+ function scheduleNextPoll() {
13402
+ pollTimer = setTimeout(runPollTick, options.intervalMs);
13403
+ }
13404
+ async function runPollTick() {
13405
+ if (ticking) {
13406
+ scheduleNextPoll();
13407
+ return;
13408
+ }
13409
+ ticking = true;
13410
+ try {
13411
+ await publishSnapshot("poll");
13412
+ } catch (error) {
13413
+ lastError = errorMessage(error);
13414
+ void writeCurrentBridgeStatus("error").catch(() => void 0);
13415
+ sendHeartbeat("error", lastError);
13416
+ process.stderr.write(`bridge poll failed: ${lastError}
13417
+ `);
13418
+ } finally {
13419
+ ticking = false;
13420
+ scheduleNextPoll();
13421
+ }
13422
+ }
13956
13423
  function connect() {
13957
13424
  const url = new URL(`/api/workspaces/${encodeURIComponent(options.workspaceId)}/roots/${encodeURIComponent(rootId)}/sync`, options.baseUrl);
13958
13425
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
@@ -13961,11 +13428,10 @@ async function runBridge(options) {
13961
13428
  if (token) url.searchParams.set("token", token);
13962
13429
  socket = new WebSocket(url);
13963
13430
  socket.addEventListener("open", () => {
13964
- lastError = void 0;
13965
- process.stdout.write(`mdocs bridge connected ${root} -> ${options.workspaceId}/${rootId}
13431
+ void onSocketOpen().catch((error) => {
13432
+ process.stderr.write(`bridge open handler failed: ${errorMessage(error)}
13966
13433
  `);
13967
- sendHeartbeat("synced");
13968
- void writeCurrentBridgeStatus("connected").catch(() => void 0);
13434
+ });
13969
13435
  });
13970
13436
  socket.addEventListener("message", (event) => {
13971
13437
  if (typeof event.data !== "string") return;
@@ -13979,7 +13445,7 @@ async function runBridge(options) {
13979
13445
  socket.addEventListener("close", () => {
13980
13446
  socket = void 0;
13981
13447
  void writeCurrentBridgeStatus("offline").catch(() => void 0);
13982
- setTimeout(connect, 1e3);
13448
+ scheduleReconnect();
13983
13449
  });
13984
13450
  socket.addEventListener("error", () => {
13985
13451
  lastError = "WebSocket error";
@@ -13987,8 +13453,34 @@ async function runBridge(options) {
13987
13453
  socket?.close();
13988
13454
  });
13989
13455
  }
13456
+ function scheduleReconnect() {
13457
+ const delayMs = reconnectDelayMs(reconnectAttempt);
13458
+ reconnectAttempt += 1;
13459
+ reconnectTimer = setTimeout(connect, delayMs);
13460
+ }
13461
+ async function onSocketOpen() {
13462
+ lastError = void 0;
13463
+ reconnectAttempt = 0;
13464
+ process.stdout.write(`mdocs bridge connected ${root} -> ${options.workspaceId}/${rootId}
13465
+ `);
13466
+ sendHeartbeat("synced");
13467
+ void writeCurrentBridgeStatus("connected").catch(() => void 0);
13468
+ pendingDocs.clear();
13469
+ pendingDeletes.clear();
13470
+ sentSignatures.clear();
13471
+ lastTreeSignature = "";
13472
+ if (suppressOpenResume) suppressOpenResume = false;
13473
+ else await resumeFromCanonical();
13474
+ await publishSnapshot("open");
13475
+ }
13476
+ function indexedMap() {
13477
+ return runManifest(() => getWorkspaceMap(io, indexOptions));
13478
+ }
13479
+ function indexedDocState(pathOrDocId) {
13480
+ return runManifest(() => getDocumentState(io, pathOrDocId, indexOptions));
13481
+ }
13990
13482
  async function publishSnapshot(reason) {
13991
- const map = await getWorkspaceMap(io);
13483
+ const map = await indexedMap();
13992
13484
  const currentDocIds = new Set(map.docs.map((doc) => doc.docId));
13993
13485
  const treeSignature = hashJson(map.docs.map((doc) => ({ docId: doc.docId, path: doc.path })));
13994
13486
  if (treeSignature !== lastTreeSignature) {
@@ -13997,19 +13489,21 @@ async function runBridge(options) {
13997
13489
  }
13998
13490
  }
13999
13491
  for (const doc of map.docs) {
14000
- const state = await getDocumentState(io, doc.docId);
13492
+ const state = await indexedDocState(doc.docId);
14001
13493
  const payload = {
14002
13494
  ...state,
14003
13495
  workspaceId: options.workspaceId,
14004
13496
  rootId,
13497
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
14005
13498
  currentSha: lastAppliedHead
14006
13499
  };
14007
- const signature = documentSignature(state.markdown, state.sidecar);
13500
+ const signature = documentSignature(state.markdown);
14008
13501
  if (signatures.get(doc.docId) === signature || pendingDocs.has(doc.docId)) continue;
14009
13502
  if (deniedSignatures.get(doc.docId) === signature) continue;
14010
13503
  if (send("file-changed", { ...payload, baseHead: lastAppliedHead, replicaId })) {
14011
13504
  seenDocs.add(doc.docId);
14012
13505
  pendingDocs.add(doc.docId);
13506
+ sentSignatures.set(doc.docId, signature);
14013
13507
  }
14014
13508
  }
14015
13509
  for (const docId of seenDocs) {
@@ -14055,10 +13549,10 @@ async function runBridge(options) {
14055
13549
  return true;
14056
13550
  }
14057
13551
  async function reconcileRemoteDocument(remote, base) {
14058
- const remoteSignature = documentSignature(remote.markdown, remote.sidecar);
14059
- const baseSignature = base ? documentSignature(base.markdown, base.sidecar) : void 0;
13552
+ const remoteSignature = documentSignature(remote.markdown);
13553
+ const baseSignature = base ? documentSignature(base.markdown) : void 0;
14060
13554
  const localState = await localStateForCanonical(remote);
14061
- const localSignature = localState ? documentSignature(localState.markdown, localState.sidecar) : void 0;
13555
+ const localSignature = localState ? documentSignature(localState.markdown) : void 0;
14062
13556
  if (!localState) {
14063
13557
  await applyCanonicalDocument(remote);
14064
13558
  return "applied";
@@ -14086,24 +13580,37 @@ async function runBridge(options) {
14086
13580
  return "conflict";
14087
13581
  }
14088
13582
  async function reconcileRemoteDelete(docId, base) {
14089
- const localState = await getDocumentState(io, docId).catch(() => void 0);
14090
- if (!localState) return "noop";
14091
- const baseSignature = documentSignature(base.markdown, base.sidecar);
14092
- const localSignature = documentSignature(localState.markdown, localState.sidecar);
14093
- if (localSignature !== baseSignature) {
13583
+ return applyCanonicalDelete(docId, base.path, documentSignature(base.markdown));
13584
+ }
13585
+ async function applyCanonicalDelete(docId, path, baseSignature) {
13586
+ let localState = await indexedDocState(docId).catch(() => void 0);
13587
+ if (!localState && path) {
13588
+ const map = await indexedMap().catch(() => void 0);
13589
+ const byPath = map?.docs.find((doc) => doc.path === path);
13590
+ if (byPath) localState = await indexedDocState(byPath.docId).catch(() => void 0);
13591
+ }
13592
+ if (!localState) {
13593
+ signatures.delete(docId);
13594
+ seenDocs.delete(docId);
13595
+ pendingDeletes.delete(docId);
13596
+ return "noop";
13597
+ }
13598
+ const localSignature = documentSignature(localState.markdown);
13599
+ const diverged = shouldConflictCopyOnDelete(localSignature, baseSignature);
13600
+ if (diverged) {
14094
13601
  const copyPath = conflictCopyPath(localState.path);
14095
13602
  await io.writeTextAtomic(copyPath, localState.markdown);
14096
- await seedConflictCopySidecar(copyPath, localState);
13603
+ await seedConflictCopyDocument(copyPath, localState.markdown);
14097
13604
  process.stderr.write(`local conflict ${localState.path}; local version saved as ${copyPath}, canonical delete applied
14098
13605
  `);
14099
13606
  }
14100
13607
  await io.deleteFile(localState.path).catch(() => void 0);
14101
- await io.deleteFile(sidecarPath(docId)).catch(() => void 0);
14102
13608
  await removeManifestDocument(docId, localState.path);
14103
13609
  signatures.delete(docId);
14104
13610
  seenDocs.delete(docId);
13611
+ sentSignatures.delete(docId);
14105
13612
  pendingDeletes.delete(docId);
14106
- return localSignature === baseSignature ? "applied" : "conflict";
13613
+ return diverged ? "conflict" : "applied";
14107
13614
  }
14108
13615
  async function handleIncoming(message) {
14109
13616
  if (message.type === "agent-unbound") {
@@ -14112,23 +13619,36 @@ async function runBridge(options) {
14112
13619
  if (!targetsUs) return;
14113
13620
  process.stdout.write(`mdocs bridge unbound by workspace; stopping ${root}
14114
13621
  `);
13622
+ shutdownTimers();
14115
13623
  await writeCurrentBridgeStatus("stopped").catch(() => void 0);
14116
13624
  socket?.close();
13625
+ await lock.release();
14117
13626
  process.exit(0);
14118
13627
  }
14119
13628
  if (message.type === "file-changed") {
14120
- await applyCanonicalDocument(readDocumentPayload(message.payload));
13629
+ const payload = readDocumentPayload(message.payload);
13630
+ const payloadSignature = documentSignature(payload.markdown);
13631
+ if (isConfirmingEcho({
13632
+ messageSourceId: message.sourceId,
13633
+ selfSourceId: sourceId,
13634
+ payloadSignature,
13635
+ sentSignature: sentSignatures.get(payload.docId)
13636
+ })) {
13637
+ signatures.set(payload.docId, payloadSignature);
13638
+ sentSignatures.delete(payload.docId);
13639
+ pendingDocs.delete(payload.docId);
13640
+ deniedSignatures.delete(payload.docId);
13641
+ seenDocs.add(payload.docId);
13642
+ lastAppliedHead = payload.canonicalHead ?? payload.currentSha ?? lastAppliedHead;
13643
+ await writeSourceState(lastAppliedHead);
13644
+ return;
13645
+ }
13646
+ await applyCanonicalDocument(payload);
14121
13647
  }
14122
13648
  if (message.type === "file-deleted") {
14123
13649
  const payload = message.payload;
14124
13650
  if (!payload.docId) return;
14125
- const map = await getWorkspaceMap(io);
14126
- const doc = map.docs.find((candidate) => candidate.docId === payload.docId);
14127
- if (doc) await io.deleteFile(doc.path);
14128
- await io.deleteFile(sidecarPath(payload.docId)).catch(() => void 0);
14129
- signatures.delete(payload.docId);
14130
- seenDocs.delete(payload.docId);
14131
- pendingDeletes.delete(payload.docId);
13651
+ await applyCanonicalDelete(payload.docId, payload.path, signatures.get(payload.docId));
14132
13652
  lastAppliedHead = payload.canonicalHead ?? lastAppliedHead;
14133
13653
  await writeSourceState(lastAppliedHead);
14134
13654
  process.stdout.write(`applied canonical delete ${payload.docId} at ${lastAppliedHead ?? "unknown"}
@@ -14156,14 +13676,14 @@ async function runBridge(options) {
14156
13676
  }
14157
13677
  }
14158
13678
  async function applyCanonicalDocument(payload) {
14159
- const remoteSignature = documentSignature(payload.markdown, payload.sidecar);
13679
+ const remoteSignature = documentSignature(payload.markdown);
14160
13680
  const localState = await localStateForCanonical(payload);
14161
- const localSignature = localState ? documentSignature(localState.markdown, localState.sidecar) : void 0;
13681
+ const localSignature = localState ? documentSignature(localState.markdown) : void 0;
14162
13682
  if (localSignature && signatures.has(payload.docId) && localSignature !== signatures.get(payload.docId) && localSignature !== remoteSignature) {
14163
13683
  const copyPath = conflictCopyPath(payload.path);
14164
13684
  if (localState) {
14165
13685
  await io.writeTextAtomic(copyPath, localState.markdown);
14166
- await seedConflictCopySidecar(copyPath, localState);
13686
+ await seedConflictCopyDocument(copyPath, localState.markdown);
14167
13687
  }
14168
13688
  send("conflict", {
14169
13689
  docId: payload.docId,
@@ -14180,21 +13700,13 @@ async function runBridge(options) {
14180
13700
  await io.deleteFile(localState.path).catch(() => void 0);
14181
13701
  }
14182
13702
  if (localState && localState.docId !== payload.docId) {
14183
- await io.deleteFile(sidecarPath(localState.docId)).catch(() => void 0);
14184
13703
  await removeManifestDocument(localState.docId, localState.path);
14185
13704
  }
14186
13705
  await io.writeTextAtomic(payload.path, payload.markdown);
14187
- await io.writeTextAtomic(sidecarPath(payload.docId), `${JSON.stringify(payload.sidecar, null, 2)}
14188
- `);
14189
- const reviewMarkdown = projectReviewMarkdown(payload.markdown, payload.sidecar.suggestions);
14190
- if (reviewMarkdown === payload.markdown) {
14191
- await io.deleteFile(reviewFilePath(payload.path)).catch(() => void 0);
14192
- } else {
14193
- await io.writeTextAtomic(reviewFilePath(payload.path), reviewMarkdown);
14194
- }
14195
13706
  await upsertManifestDocument(payload);
14196
13707
  signatures.set(payload.docId, remoteSignature);
14197
13708
  deniedSignatures.delete(payload.docId);
13709
+ sentSignatures.delete(payload.docId);
14198
13710
  seenDocs.add(payload.docId);
14199
13711
  pendingDocs.delete(payload.docId);
14200
13712
  lastAppliedHead = payload.canonicalHead ?? payload.currentSha ?? lastAppliedHead;
@@ -14202,38 +13714,33 @@ async function runBridge(options) {
14202
13714
  process.stdout.write(`applied canonical change ${payload.path} at ${lastAppliedHead ?? "unknown"}
14203
13715
  `);
14204
13716
  }
14205
- async function seedConflictCopySidecar(copyPath, state) {
14206
- const manifest = await readManifest(io).catch(() => void 0);
14207
- if (!manifest) return;
14208
- const now = (/* @__PURE__ */ new Date()).toISOString();
14209
- const docId = createDocId();
14210
- const title = titleFromMarkdown(copyPath, state.markdown);
14211
- const entry = {
14212
- docId,
14213
- path: copyPath,
14214
- title,
14215
- contentHash: contentHashForText(state.markdown),
14216
- updatedAt: now
14217
- };
14218
- await writeSidecar(io, {
14219
- ...state.sidecar,
14220
- docId,
14221
- path: copyPath,
14222
- title,
14223
- updatedAt: now
14224
- });
14225
- await writeManifest(io, {
14226
- ...manifest,
14227
- docs: [...manifest.docs.filter((doc) => doc.path !== copyPath && doc.docId !== docId), entry],
14228
- updatedAt: now
13717
+ async function seedConflictCopyDocument(copyPath, markdown) {
13718
+ await runManifest(async () => {
13719
+ const manifest = await readManifest(io).catch(() => void 0);
13720
+ if (!manifest) return;
13721
+ const now = (/* @__PURE__ */ new Date()).toISOString();
13722
+ const docId = deterministicDocId(rootId, copyPath);
13723
+ const title = titleFromMarkdown(copyPath, markdown);
13724
+ const entry = {
13725
+ docId,
13726
+ path: copyPath,
13727
+ title,
13728
+ contentHash: contentHashForText(markdown),
13729
+ updatedAt: now
13730
+ };
13731
+ await writeManifest(io, {
13732
+ ...manifest,
13733
+ docs: [...manifest.docs.filter((doc) => doc.path !== copyPath && doc.docId !== docId), entry],
13734
+ updatedAt: now
13735
+ });
14229
13736
  });
14230
13737
  }
14231
13738
  async function localStateForCanonical(payload) {
14232
- const byDocId = await getDocumentState(io, payload.docId).catch(() => void 0);
13739
+ const byDocId = await indexedDocState(payload.docId).catch(() => void 0);
14233
13740
  if (byDocId) return byDocId;
14234
- const map = await getWorkspaceMap(io).catch(() => void 0);
13741
+ const map = await indexedMap().catch(() => void 0);
14235
13742
  const byPath = map?.docs.find((doc) => doc.path === payload.path);
14236
- return byPath ? getDocumentState(io, byPath.docId).catch(() => void 0) : void 0;
13743
+ return byPath ? indexedDocState(byPath.docId).catch(() => void 0) : void 0;
14237
13744
  }
14238
13745
  async function fetchCanonicalDocument2(docId) {
14239
13746
  try {
@@ -14330,59 +13837,65 @@ async function runBridge(options) {
14330
13837
  await writeBridgeStatus(root, currentBridgeStatus(state));
14331
13838
  }
14332
13839
  async function primeLocalSignatures() {
14333
- const map = await getWorkspaceMap(io);
13840
+ const map = await indexedMap();
14334
13841
  for (const doc of map.docs) {
14335
- const state = await getDocumentState(io, doc.docId);
14336
- signatures.set(doc.docId, documentSignature(state.markdown, state.sidecar));
13842
+ const state = await indexedDocState(doc.docId);
13843
+ signatures.set(doc.docId, documentSignature(state.markdown));
14337
13844
  seenDocs.add(doc.docId);
14338
13845
  }
14339
13846
  process.stdout.write(`mdocs bridge claimed ${root} as ${rootId} at ${lastAppliedHead ?? "unknown"}
14340
13847
  `);
14341
13848
  }
14342
13849
  async function localSignatureFor(docId) {
14343
- const state = await getDocumentState(io, docId);
14344
- return documentSignature(state.markdown, state.sidecar);
13850
+ const state = await indexedDocState(docId);
13851
+ return documentSignature(state.markdown);
14345
13852
  }
14346
13853
  async function writeSourceState(head) {
14347
- const manifest = await indexWorkspace(io);
14348
- await writeManifest(io, {
14349
- ...manifest,
14350
- source: {
14351
- sourceId: rootId,
14352
- sourceName: options.sourceName ?? (basename2(root) || rootId),
14353
- canonicalHead: head,
14354
- claimedAt: manifest.source?.claimedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
14355
- mappings: [{ ...mapping, lastAppliedHead: head, status: head ? "synced" : "connecting", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }]
14356
- },
14357
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13854
+ await runManifest(async () => {
13855
+ const manifest = await indexWorkspace(io, indexOptions);
13856
+ await writeManifest(io, {
13857
+ ...manifest,
13858
+ source: {
13859
+ sourceId: rootId,
13860
+ sourceName: options.sourceName ?? (basename2(root) || rootId),
13861
+ canonicalHead: head,
13862
+ claimedAt: manifest.source?.claimedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
13863
+ mappings: [{ ...mapping, lastAppliedHead: head, status: head ? "synced" : "connecting", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }]
13864
+ },
13865
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13866
+ });
14358
13867
  });
14359
13868
  }
14360
13869
  async function upsertManifestDocument(payload) {
14361
- const manifest = await readManifest(io).catch(() => void 0);
14362
- if (!manifest) return;
14363
- await writeManifest(io, {
14364
- ...manifest,
14365
- docs: [
14366
- ...manifest.docs.filter((doc) => doc.docId !== payload.docId && doc.path !== payload.path),
14367
- {
14368
- docId: payload.docId,
14369
- path: payload.path,
14370
- title: payload.sidecar.title || titleFromMarkdown(payload.path, payload.markdown),
14371
- contentHash: contentHashForText(payload.markdown),
14372
- currentSha: payload.canonicalHead ?? payload.currentSha,
14373
- updatedAt: payload.sidecar.updatedAt
14374
- }
14375
- ].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0),
14376
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13870
+ await runManifest(async () => {
13871
+ const manifest = await readManifest(io).catch(() => void 0);
13872
+ if (!manifest) return;
13873
+ await writeManifest(io, {
13874
+ ...manifest,
13875
+ docs: [
13876
+ ...manifest.docs.filter((doc) => doc.docId !== payload.docId && doc.path !== payload.path),
13877
+ {
13878
+ docId: payload.docId,
13879
+ path: payload.path,
13880
+ title: payload.title || titleFromMarkdown(payload.path, payload.markdown),
13881
+ contentHash: contentHashForText(payload.markdown),
13882
+ currentSha: payload.canonicalHead ?? payload.currentSha,
13883
+ updatedAt: payload.updatedAt
13884
+ }
13885
+ ].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0),
13886
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13887
+ });
14377
13888
  });
14378
13889
  }
14379
13890
  async function removeManifestDocument(docId, path) {
14380
- const manifest = await readManifest(io).catch(() => void 0);
14381
- if (!manifest) return;
14382
- await writeManifest(io, {
14383
- ...manifest,
14384
- docs: manifest.docs.filter((doc) => doc.docId !== docId && doc.path !== path),
14385
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13891
+ await runManifest(async () => {
13892
+ const manifest = await readManifest(io).catch(() => void 0);
13893
+ if (!manifest) return;
13894
+ await writeManifest(io, {
13895
+ ...manifest,
13896
+ docs: manifest.docs.filter((doc) => doc.docId !== docId && doc.path !== path),
13897
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13898
+ });
14386
13899
  });
14387
13900
  }
14388
13901
  }
@@ -14514,7 +14027,7 @@ async function readBridgeTokenStore() {
14514
14027
  return { schemaVersion: 1, tokens: {} };
14515
14028
  }
14516
14029
  function bridgeTokenStoreKey(options, rootId) {
14517
- return createHash2("sha256").update(JSON.stringify([normalizeBridgeBaseUrl(options.baseUrl), options.workspaceId, rootId, options.actorId])).digest("hex").slice(0, 32);
14030
+ return createHash3("sha256").update(JSON.stringify([normalizeBridgeBaseUrl(options.baseUrl), options.workspaceId, rootId, options.actorId])).digest("hex").slice(0, 32);
14518
14031
  }
14519
14032
  function normalizeBridgeBaseUrl(baseUrl) {
14520
14033
  try {
@@ -14573,7 +14086,7 @@ async function fetchCanonicalDocument(options, rootId, docId) {
14573
14086
  function readCommitDocumentPayload(payload, workspaceId, rootId, headId) {
14574
14087
  if (!payload || typeof payload !== "object") return void 0;
14575
14088
  const value = payload;
14576
- if (typeof value.markdown !== "string" || !value.sidecar || typeof value.sidecar !== "object") return void 0;
14089
+ if (typeof value.markdown !== "string") return void 0;
14577
14090
  return readDocumentPayload({
14578
14091
  workspaceId,
14579
14092
  rootId,
@@ -14581,7 +14094,7 @@ function readCommitDocumentPayload(payload, workspaceId, rootId, headId) {
14581
14094
  path: value.path,
14582
14095
  title: value.title,
14583
14096
  markdown: value.markdown,
14584
- sidecar: value.sidecar,
14097
+ updatedAt: value.updatedAt,
14585
14098
  currentSha: headId,
14586
14099
  canonicalHead: headId
14587
14100
  });
@@ -14702,20 +14215,67 @@ function mergeReplicas(replicas, replica) {
14702
14215
  return [...replicas.filter((candidate) => candidate.replicaId !== replica.replicaId), replica];
14703
14216
  }
14704
14217
  function rootIdForPath(path) {
14705
- return `root_${createHash2("sha256").update(path).digest("hex").slice(0, 12)}`;
14218
+ return `root_${createHash3("sha256").update(path).digest("hex").slice(0, 12)}`;
14706
14219
  }
14707
- function conflictCopyPath(path) {
14708
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replaceAll(/[:.]/g, "").slice(0, 15);
14220
+ function conflictCopyPath(path, stamp = conflictCopyStamp()) {
14709
14221
  return path.replace(/(\.[^./]+)?$/, (extension) => `.conflict-${stamp}${extension || ".md"}`);
14710
14222
  }
14711
- function reviewFilePath(path) {
14712
- return path.replace(/(\.md)?$/i, ".review.md");
14223
+ async function acquireBridgeLock(root) {
14224
+ const lockPath = join4(root, BRIDGE_LOCK_PATH);
14225
+ await mkdir4(join4(root, ".mdocs"), { recursive: true });
14226
+ const payload = `${JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
14227
+ `;
14228
+ try {
14229
+ await writeFile4(lockPath, payload, { flag: "wx" });
14230
+ } catch (error) {
14231
+ if (error.code !== "EEXIST") throw error;
14232
+ const holderPid = await readBridgeLockPid(lockPath);
14233
+ if (holderPid !== void 0 && isProcessAlive(holderPid)) {
14234
+ throw new Error(
14235
+ `Another mdocs bridge (pid ${holderPid}) is already running for ${root}. Stop it first or remove ${lockPath} if it is stale.`
14236
+ );
14237
+ }
14238
+ await writeFile4(lockPath, payload);
14239
+ }
14240
+ let released = false;
14241
+ const removeSync = () => {
14242
+ if (released) return;
14243
+ released = true;
14244
+ try {
14245
+ unlinkSync(lockPath);
14246
+ } catch {
14247
+ }
14248
+ };
14249
+ process.once("exit", removeSync);
14250
+ return {
14251
+ release: async () => {
14252
+ if (released) return;
14253
+ released = true;
14254
+ await rm3(lockPath, { force: true }).catch(() => void 0);
14255
+ }
14256
+ };
14257
+ }
14258
+ async function readBridgeLockPid(lockPath) {
14259
+ try {
14260
+ const parsed = JSON.parse(await readFile6(lockPath, "utf8"));
14261
+ return typeof parsed.pid === "number" && Number.isInteger(parsed.pid) ? parsed.pid : void 0;
14262
+ } catch {
14263
+ return void 0;
14264
+ }
14265
+ }
14266
+ function isProcessAlive(pid) {
14267
+ try {
14268
+ process.kill(pid, 0);
14269
+ return true;
14270
+ } catch (error) {
14271
+ return error.code === "EPERM";
14272
+ }
14713
14273
  }
14714
14274
  function hashJson(value) {
14715
- return createHash2("sha256").update(JSON.stringify(value)).digest("hex");
14275
+ return createHash3("sha256").update(JSON.stringify(value)).digest("hex");
14716
14276
  }
14717
- function documentSignature(markdown, sidecar) {
14718
- return hashJson({ markdown, sidecar: { ...sidecar, updatedAt: void 0 } });
14277
+ function documentSignature(markdown) {
14278
+ return hashJson({ markdown });
14719
14279
  }
14720
14280
  async function readLocalSource(root) {
14721
14281
  const io = new NodeWorkspaceIO(root);
@@ -14731,12 +14291,12 @@ function readDocumentPayload(payload) {
14731
14291
  if (typeof value.docId !== "string") throw new Error("file-changed payload missing docId");
14732
14292
  if (typeof value.path !== "string") throw new Error("file-changed payload missing path");
14733
14293
  if (typeof value.markdown !== "string") throw new Error("file-changed payload missing markdown");
14734
- if (!value.sidecar || typeof value.sidecar !== "object") throw new Error("file-changed payload missing sidecar");
14735
14294
  return {
14736
14295
  docId: value.docId,
14737
14296
  path: value.path,
14297
+ title: typeof value.title === "string" ? value.title : titleFromMarkdown(value.path, value.markdown),
14738
14298
  markdown: value.markdown,
14739
- sidecar: value.sidecar,
14299
+ updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : (/* @__PURE__ */ new Date()).toISOString(),
14740
14300
  currentSha: typeof value.currentSha === "string" ? value.currentSha : void 0,
14741
14301
  canonicalHead: typeof value.canonicalHead === "string" ? value.canonicalHead : void 0
14742
14302
  };
@@ -14744,7 +14304,7 @@ function readDocumentPayload(payload) {
14744
14304
 
14745
14305
  // src/bridge-startup.ts
14746
14306
  import { execFile as execFile2 } from "node:child_process";
14747
- import { createHash as createHash3 } from "node:crypto";
14307
+ import { createHash as createHash4 } from "node:crypto";
14748
14308
  import { chmod as chmod2, copyFile, mkdir as mkdir5, readFile as readFile7, stat as stat3, writeFile as writeFile5 } from "node:fs/promises";
14749
14309
  import { homedir as homedir2, platform } from "node:os";
14750
14310
  import { dirname as dirname5, join as join5, resolve as resolve5 } from "node:path";
@@ -14940,7 +14500,7 @@ WantedBy=default.target
14940
14500
  `;
14941
14501
  }
14942
14502
  function serviceLabel(root) {
14943
- const digest = createHash3("sha256").update(root).digest("hex").slice(0, 12);
14503
+ const digest = createHash4("sha256").update(root).digest("hex").slice(0, 12);
14944
14504
  return `com.magic-markdown.bridge.${digest}`;
14945
14505
  }
14946
14506
  async function runOptionalCommand(command, args) {
@@ -15036,11 +14596,7 @@ async function main() {
15036
14596
  ...image2.workspacePath ? { absolutePath: resolve6(cwd, image2.workspacePath) } : {}
15037
14597
  })),
15038
14598
  links: state.links,
15039
- ...parsed.flags["no-review"] ? {} : {
15040
- comments: state.sidecar.comments.filter((comment2) => comment2.status === "open"),
15041
- suggestions: state.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
15042
- anchors: state.anchors
15043
- }
14599
+ ...parsed.flags["no-review"] ? {} : { review: reviewStateForDocument(state) }
15044
14600
  }, parsed.flags);
15045
14601
  return;
15046
14602
  }
@@ -15050,49 +14606,25 @@ async function main() {
15050
14606
  return;
15051
14607
  }
15052
14608
  case "comments": {
15053
- const state = await getDocumentState(io, requiredArg(parsed.command[1], "path"));
15054
- print(state.sidecar.comments, parsed.flags);
15055
- return;
14609
+ throw localReviewCommandError("comments");
15056
14610
  }
15057
14611
  case "comment": {
15058
- const path = requiredArg(parsed.command[1], "path");
15059
- const range = parseRange2(requiredFlag2(parsed.flags, "range"));
15060
- const body = await readRequiredTextFlag2(parsed.flags, cwd, ["body", "message"]);
15061
- if (!body) throw new CliError("usage_error", "Comment body is empty.", { hint: "Pass --body <text> or a non-empty --body-file." });
15062
- print(await addComment(io, path, range, body, actorFromFlags(parsed.flags, defaultActor())), parsed.flags);
15063
- return;
14612
+ throw localReviewCommandError("comment");
15064
14613
  }
15065
14614
  case "suggestions": {
15066
- const state = await getDocumentState(io, requiredArg(parsed.command[1], "path"));
15067
- print(state.sidecar.suggestions, parsed.flags);
15068
- return;
14615
+ throw localReviewCommandError("suggestions");
15069
14616
  }
15070
14617
  case "suggest": {
15071
- const path = requiredArg(parsed.command[1], "path");
15072
- const range = parseRange2(requiredFlag2(parsed.flags, "range"));
15073
- const replacement = await readRequiredTextFlag2(parsed.flags, cwd, ["with", "replacement"]);
15074
- const message = await readOptionalTextFlag2(parsed.flags, cwd, ["message"]) ?? "Suggested edit";
15075
- print(await addSuggestion(io, path, range, replacement, message, actorFromFlags(parsed.flags, {
15076
- id: "agent_local",
15077
- kind: "agent",
15078
- name: "Local Agent"
15079
- })), parsed.flags);
15080
- return;
14618
+ throw localReviewCommandError("suggest");
15081
14619
  }
15082
14620
  case "accept": {
15083
- const suggestionId = requiredArg(parsed.command[1], "suggestionId");
15084
- print(await acceptSuggestion(io, suggestionId, actorFromFlags(parsed.flags, defaultActor())), parsed.flags);
15085
- return;
14621
+ throw localReviewCommandError("accept");
15086
14622
  }
15087
14623
  case "reject": {
15088
- const suggestionId = requiredArg(parsed.command[1], "suggestionId");
15089
- print(await rejectSuggestion(io, suggestionId, actorFromFlags(parsed.flags, defaultActor())), parsed.flags);
15090
- return;
14624
+ throw localReviewCommandError("reject");
15091
14625
  }
15092
14626
  case "resolve-comment": {
15093
- const commentId = requiredArg(parsed.command[1], "commentId");
15094
- print(await resolveComment(io, commentId, actorFromFlags(parsed.flags, defaultActor())), parsed.flags);
15095
- return;
14627
+ throw localReviewCommandError("resolve-comment");
15096
14628
  }
15097
14629
  case "sync":
15098
14630
  case "status": {
@@ -15149,7 +14681,7 @@ async function main() {
15149
14681
  claimToken: typeof parsed.flags.claim === "string" ? parsed.flags.claim : void 0,
15150
14682
  actorId: typeof parsed.flags.actor === "string" ? parsed.flags.actor : void 0,
15151
14683
  actorName: typeof parsed.flags["actor-name"] === "string" ? parsed.flags["actor-name"] : void 0,
15152
- intervalMs: Number(parsed.flags.interval ?? 1e3),
14684
+ intervalMs: validateBridgeIntervalMs(parsed.flags.interval),
15153
14685
  token: typeof parsed.flags.token === "string" ? parsed.flags.token : process.env.MDOCS_BRIDGE_TOKEN || void 0,
15154
14686
  requestToken: Boolean(parsed.flags["request-token"] || parsed.flags.pair),
15155
14687
  pairingTimeoutMs: typeof parsed.flags["pairing-timeout-ms"] === "string" ? Number(parsed.flags["pairing-timeout-ms"]) : void 0,
@@ -15176,9 +14708,15 @@ async function main() {
15176
14708
  });
15177
14709
  return;
15178
14710
  }
14711
+ const workspaceId = typeof parsed.flags.workspace === "string" && parsed.flags.workspace.trim() ? parsed.flags.workspace.trim() : void 0;
14712
+ if (!workspaceId) {
14713
+ throw new CliError("usage_error", "Missing --workspace <id> for mdocs bridge.", {
14714
+ hint: "Pass the workspace explicitly, for example --workspace ws_123. The bridge no longer assumes a demo workspace."
14715
+ });
14716
+ }
15179
14717
  const options = {
15180
14718
  ...baseOptions,
15181
- workspaceId: typeof parsed.flags.workspace === "string" ? parsed.flags.workspace : "workspace_demo",
14719
+ workspaceId,
15182
14720
  baseUrl: bridgeBaseUrl(parsed.flags)
15183
14721
  };
15184
14722
  if (subcommand === "setup") await runBridgeSetup(options);
@@ -15264,23 +14802,14 @@ function parseArgs(args) {
15264
14802
  }
15265
14803
  return { command, flags };
15266
14804
  }
15267
- function parseRange2(value) {
15268
- const [start, end = start] = value.split(":").map(Number);
15269
- if (!Number.isInteger(start) || !Number.isInteger(end) || !start || !end || start < 1 || end < start) {
15270
- throw new CliError("invalid_range", `Invalid --range value "${value}".`, {
15271
- hint: "Use --range <startLine>:<endLine> with 1-based inclusive line numbers, for example --range 4:9."
15272
- });
15273
- }
15274
- return { startLine: start, endLine: end };
15275
- }
15276
14805
  function requiredArg(value, name) {
15277
14806
  if (!value) throw new CliError("usage_error", `Missing <${name}> argument.`);
15278
14807
  return value;
15279
14808
  }
15280
- function requiredFlag2(flags, name) {
15281
- const value = flags[name];
15282
- if (typeof value !== "string") throw new CliError("usage_error", `Missing --${name}.`);
15283
- return value;
14809
+ function localReviewCommandError(command) {
14810
+ return new CliError("usage_error", `Local review command "${command}" was removed.`, {
14811
+ hint: "Use `mdocs remote ...` or the remote MCP connector so comments, suggestions, accept, and reject go through DocumentAuthority."
14812
+ });
15284
14813
  }
15285
14814
  function bridgeBaseUrl(flags) {
15286
14815
  const configured = optionalBridgeBaseUrl(flags);
@@ -15294,40 +14823,6 @@ function optionalBridgeBaseUrl(flags) {
15294
14823
  const configured = process.env.MDOCS_BASE_URL?.trim();
15295
14824
  return configured || void 0;
15296
14825
  }
15297
- async function readRequiredTextFlag2(flags, cwd, names) {
15298
- const value = await readOptionalTextFlag2(flags, cwd, names);
15299
- if (value === void 0) {
15300
- throw new CliError("usage_error", `Pass --${names[0]} <text> or --${names[0]}-file <path>.`, {
15301
- hint: `For multiline text, write it to a file and pass --${names[0]}-file.`
15302
- });
15303
- }
15304
- return value;
15305
- }
15306
- async function readOptionalTextFlag2(flags, cwd, names) {
15307
- for (const name of names) {
15308
- const fileValue = flags[`${name}-file`];
15309
- if (typeof fileValue === "string") return readFile8(resolve6(cwd, fileValue), "utf8");
15310
- const value = flags[name];
15311
- if (typeof value === "string") return value;
15312
- }
15313
- return void 0;
15314
- }
15315
- function actorFromFlags(flags, fallback) {
15316
- const id = typeof flags.actor === "string" ? flags.actor : fallback.id;
15317
- const name = typeof flags["actor-name"] === "string" ? flags["actor-name"] : fallback.name;
15318
- const email = typeof flags["actor-email"] === "string" ? flags["actor-email"] : fallback.email;
15319
- const kind = actorKindFromFlag(flags["actor-kind"]) ?? fallback.kind;
15320
- return {
15321
- id,
15322
- kind,
15323
- name,
15324
- ...email ? { email } : {}
15325
- };
15326
- }
15327
- function actorKindFromFlag(value) {
15328
- if (value === "human" || value === "agent" || value === "system") return value;
15329
- return void 0;
15330
- }
15331
14826
  function print(value, flags) {
15332
14827
  if (flags.json || typeof value !== "string") {
15333
14828
  process.stdout.write(`${JSON.stringify(value, null, 2)}
@@ -15347,20 +14842,14 @@ Commands:
15347
14842
  init [path] Initialize .mdocs and index Markdown files
15348
14843
  map --json Print workspace map
15349
14844
  graph --json Print workspace knowledge graph nodes and edges
15350
- state <path> --json Print merged Markdown and sidecar state
14845
+ state <path> --json Print clean Markdown document state
15351
14846
  context <path> --json Print agent-ready context
15352
14847
  context <path> --summary --json Print document metadata and headings
15353
- review <path> --json Print open comments, suggestions, anchors
15354
- comments <path> --json List comments
15355
- comment <path> --range 3:5 --body Add a sidecar comment
15356
- suggestions <path> --json List suggestions
15357
- suggest <path> --range 3:5 --with Add a reviewMarkdown suggestion
15358
- suggest <path> --range 3:5 --with-file replacement.md
15359
- Add a multiline reviewMarkdown suggestion
15360
- accept <suggestionId> Apply and accept a suggestion
15361
- reject <suggestionId> Reject a suggestion
15362
- resolve-comment <commentId> Resolve a comment thread
15363
- status --json Print Git status and sidecar map
14848
+ review <path> --json Print local authority-review redirect metadata
14849
+ comments|comment|suggestions Removed locally; use remote authority commands
14850
+ suggest|accept|reject|resolve-comment
14851
+ Removed locally; use remote authority commands
14852
+ status --json Print Git status and workspace map
15364
14853
  sync --json Alias for status until hosted sync is configured
15365
14854
  diff [path] Print local Git diff
15366
14855
  history [path] --json Print local Git history
@@ -15393,7 +14882,7 @@ Commands:
15393
14882
  (or set MDOCS_BRIDGE_TOKEN)
15394
14883
  bridge --claim <token> --root . --canonical-prefix prompts --replica-prefix packages/agent/prompts
15395
14884
  Claim an existing repo path as a Magic-canonical source mapping
15396
- doctor --json Validate sidecar mappings
14885
+ doctor --json Validate workspace map and agent readiness
15397
14886
  serve-mcp Start JSON-RPC MCP-compatible stdio server
15398
14887
  version [--json] Print CLI version
15399
14888
  help <command> Show usage, examples, and notes for one command