@magic-markdown/cli 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +774 -48
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -321,6 +321,17 @@ function fnv1a(value) {
|
|
|
321
321
|
return hash;
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
// ../core/src/wikilinks.ts
|
|
325
|
+
function parseWikilinkBody(value) {
|
|
326
|
+
const separatorIndex = value.indexOf("|");
|
|
327
|
+
const rawTarget = separatorIndex >= 0 ? value.slice(0, separatorIndex) : value;
|
|
328
|
+
const rawLabel = separatorIndex >= 0 ? value.slice(separatorIndex + 1) : void 0;
|
|
329
|
+
const target = rawTarget.trim();
|
|
330
|
+
if (!target) return void 0;
|
|
331
|
+
const label = rawLabel?.trim();
|
|
332
|
+
return label ? { target, label } : { target };
|
|
333
|
+
}
|
|
334
|
+
|
|
324
335
|
// ../core/src/markdown.ts
|
|
325
336
|
var POLLUTING_PATTERNS = [
|
|
326
337
|
{ name: "CriticMarkup addition", pattern: /\{\+\+[\s\S]*?\+\+\}/ },
|
|
@@ -469,6 +480,86 @@ function extractMarkdownImages(markdown, documentPath) {
|
|
|
469
480
|
}
|
|
470
481
|
return images;
|
|
471
482
|
}
|
|
483
|
+
function extractMarkdownLinks(markdown) {
|
|
484
|
+
const codeMasked = maskCode(markdown);
|
|
485
|
+
const referenceDefinitions = collectReferenceDefinitions(markdown, codeMasked);
|
|
486
|
+
const masked = maskReferenceDefinitions(markdown, codeMasked);
|
|
487
|
+
const lineStarts = getLineStarts(markdown);
|
|
488
|
+
const links = [];
|
|
489
|
+
let index = 0;
|
|
490
|
+
while (index < masked.length) {
|
|
491
|
+
const start = masked.indexOf("[", index);
|
|
492
|
+
if (start === -1) break;
|
|
493
|
+
if (masked[start + 1] === "[" || markdown[start - 1] === "!" || isEscaped(markdown, start)) {
|
|
494
|
+
index = start + 1;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
const labelClose = findClosingBracket(masked, start);
|
|
498
|
+
if (labelClose === -1) {
|
|
499
|
+
index = start + 1;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const label = markdown.slice(start + 1, labelClose);
|
|
503
|
+
const afterLabel = labelClose + 1;
|
|
504
|
+
const position = positionAt(lineStarts, start);
|
|
505
|
+
if (masked[afterLabel] === "(") {
|
|
506
|
+
const destinationClose = findClosingParen(markdown, afterLabel + 1);
|
|
507
|
+
if (destinationClose === -1) {
|
|
508
|
+
index = afterLabel + 1;
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const parsed = parseDestinationAndTitle(markdown.slice(afterLabel + 1, destinationClose));
|
|
512
|
+
if (parsed) {
|
|
513
|
+
links.push(toLinkReference({
|
|
514
|
+
label,
|
|
515
|
+
target: parsed.source,
|
|
516
|
+
title: parsed.title,
|
|
517
|
+
line: position.line,
|
|
518
|
+
column: position.column,
|
|
519
|
+
syntax: "inline"
|
|
520
|
+
}));
|
|
521
|
+
}
|
|
522
|
+
index = destinationClose + 1;
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (masked[afterLabel] === "[") {
|
|
526
|
+
const referenceClose = findClosingBracket(masked, afterLabel);
|
|
527
|
+
if (referenceClose === -1) {
|
|
528
|
+
index = afterLabel + 1;
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
const explicitLabel = markdown.slice(afterLabel + 1, referenceClose);
|
|
532
|
+
const definitionLabel = explicitLabel.length > 0 ? explicitLabel : label;
|
|
533
|
+
const definition = referenceDefinitions.get(normalizeReferenceLabel(definitionLabel));
|
|
534
|
+
if (definition) {
|
|
535
|
+
links.push(toLinkReference({
|
|
536
|
+
label,
|
|
537
|
+
target: definition.source,
|
|
538
|
+
title: definition.title,
|
|
539
|
+
line: position.line,
|
|
540
|
+
column: position.column,
|
|
541
|
+
syntax: explicitLabel.length > 0 ? "reference" : "collapsed_reference"
|
|
542
|
+
}));
|
|
543
|
+
}
|
|
544
|
+
index = referenceClose + 1;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
const shortcutDefinition = referenceDefinitions.get(normalizeReferenceLabel(label));
|
|
548
|
+
if (shortcutDefinition) {
|
|
549
|
+
links.push(toLinkReference({
|
|
550
|
+
label,
|
|
551
|
+
target: shortcutDefinition.source,
|
|
552
|
+
title: shortcutDefinition.title,
|
|
553
|
+
line: position.line,
|
|
554
|
+
column: position.column,
|
|
555
|
+
syntax: "shortcut_reference"
|
|
556
|
+
}));
|
|
557
|
+
}
|
|
558
|
+
index = labelClose + 1;
|
|
559
|
+
}
|
|
560
|
+
extractWikilinks(markdown, masked, lineStarts).forEach((link2) => links.push(link2));
|
|
561
|
+
return links.sort((left, right) => left.line - right.line || left.column - right.column);
|
|
562
|
+
}
|
|
472
563
|
function toImageReference(image2) {
|
|
473
564
|
const workspacePath = resolveWorkspaceImagePath(image2.source, image2.documentPath);
|
|
474
565
|
return {
|
|
@@ -483,6 +574,54 @@ function toImageReference(image2) {
|
|
|
483
574
|
...workspacePath ? { workspacePath } : {}
|
|
484
575
|
};
|
|
485
576
|
}
|
|
577
|
+
function toLinkReference(link2) {
|
|
578
|
+
return {
|
|
579
|
+
label: unescapeMarkdown(link2.label),
|
|
580
|
+
target: link2.target,
|
|
581
|
+
...link2.title ? { title: link2.title } : {},
|
|
582
|
+
line: link2.line,
|
|
583
|
+
column: link2.column,
|
|
584
|
+
syntax: link2.syntax
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function extractWikilinks(markdown, masked, lineStarts) {
|
|
588
|
+
const links = [];
|
|
589
|
+
let index = 0;
|
|
590
|
+
while (index < masked.length) {
|
|
591
|
+
const start = masked.indexOf("[[", index);
|
|
592
|
+
if (start === -1) break;
|
|
593
|
+
if (isEscaped(markdown, start)) {
|
|
594
|
+
index = start + 2;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
const close2 = masked.indexOf("]]", start + 2);
|
|
598
|
+
if (close2 === -1 || markdown.slice(start + 2, close2).includes("\n")) {
|
|
599
|
+
index = start + 2;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const parsed = parseWikilinkBody(markdown.slice(start + 2, close2));
|
|
603
|
+
if (parsed) {
|
|
604
|
+
const position = positionAt(lineStarts, start);
|
|
605
|
+
links.push({
|
|
606
|
+
label: parsed.label ?? parsed.target,
|
|
607
|
+
target: parsed.target,
|
|
608
|
+
line: position.line,
|
|
609
|
+
column: position.column,
|
|
610
|
+
syntax: "wikilink"
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
index = close2 + 2;
|
|
614
|
+
}
|
|
615
|
+
return links;
|
|
616
|
+
}
|
|
617
|
+
function maskReferenceDefinitions(markdown, masked) {
|
|
618
|
+
const markdownLines = markdown.split(/(?<=\n)/);
|
|
619
|
+
const maskedLines = masked.split(/(?<=\n)/);
|
|
620
|
+
return maskedLines.map((line, index) => {
|
|
621
|
+
const original = markdownLines[index] ?? "";
|
|
622
|
+
return parseReferenceDefinition(original.replace(/\r?\n$/, "")) ? maskLine(line) : line;
|
|
623
|
+
}).join("");
|
|
624
|
+
}
|
|
486
625
|
function collectReferenceDefinitions(markdown, masked) {
|
|
487
626
|
const definitions = /* @__PURE__ */ new Map();
|
|
488
627
|
const lines = getLines(markdown);
|
|
@@ -840,6 +979,184 @@ function applySuggestion(markdown, suggestion) {
|
|
|
840
979
|
return next;
|
|
841
980
|
}
|
|
842
981
|
|
|
982
|
+
// ../core/src/graph.ts
|
|
983
|
+
function resolveMarkdownLinks(links, currentDocument, documents) {
|
|
984
|
+
return links.map((link2) => resolveMarkdownLink(link2, currentDocument, documents));
|
|
985
|
+
}
|
|
986
|
+
function buildWorkspaceGraph(workspaceId, schemaVersion, documents) {
|
|
987
|
+
const edges = documents.flatMap((document) => document.links.map((link2, index) => toGraphEdge(document, link2, index)));
|
|
988
|
+
const incomingByDocId = /* @__PURE__ */ new Map();
|
|
989
|
+
for (const edge of edges) {
|
|
990
|
+
if (!edge.targetDocId) continue;
|
|
991
|
+
incomingByDocId.set(edge.targetDocId, (incomingByDocId.get(edge.targetDocId) ?? 0) + 1);
|
|
992
|
+
}
|
|
993
|
+
const nodes = documents.map((document) => ({
|
|
994
|
+
docId: document.docId,
|
|
995
|
+
path: document.path,
|
|
996
|
+
title: document.title,
|
|
997
|
+
openComments: document.openComments,
|
|
998
|
+
openSuggestions: document.openSuggestions,
|
|
999
|
+
anchorsNeedingReview: document.anchorsNeedingReview,
|
|
1000
|
+
outgoingLinks: document.links.length,
|
|
1001
|
+
incomingLinks: incomingByDocId.get(document.docId) ?? 0,
|
|
1002
|
+
unresolvedLinks: document.unresolvedLinks,
|
|
1003
|
+
externalLinks: document.externalLinks
|
|
1004
|
+
}));
|
|
1005
|
+
return {
|
|
1006
|
+
workspaceId,
|
|
1007
|
+
schemaVersion,
|
|
1008
|
+
nodes,
|
|
1009
|
+
edges
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
function resolveMarkdownLink(link2, currentDocument, documents) {
|
|
1013
|
+
const target = link2.target.trim();
|
|
1014
|
+
const parsed = parseInternalTarget(target);
|
|
1015
|
+
if (!target) return { ...link2, status: "unresolved" };
|
|
1016
|
+
if (isExternalTarget(target)) return { ...link2, status: "external" };
|
|
1017
|
+
if (!parsed) return { ...link2, status: "unresolved" };
|
|
1018
|
+
if (!parsed.path) {
|
|
1019
|
+
return parsed.fragment ? {
|
|
1020
|
+
...link2,
|
|
1021
|
+
status: "anchor",
|
|
1022
|
+
fragment: parsed.fragment,
|
|
1023
|
+
targetDocId: currentDocument.docId,
|
|
1024
|
+
targetPath: currentDocument.path,
|
|
1025
|
+
targetTitle: currentDocument.title
|
|
1026
|
+
} : { ...link2, status: "unresolved" };
|
|
1027
|
+
}
|
|
1028
|
+
if (link2.syntax === "wikilink" && !isPathLikeWikilinkTarget(parsed.path)) {
|
|
1029
|
+
const candidates = findBasenameWikilinkCandidates(parsed.path, parsed.fragment, documents);
|
|
1030
|
+
if (candidates.length === 1) return resolvedLink(link2, candidates[0]);
|
|
1031
|
+
if (candidates.length > 1) return { ...link2, status: "ambiguous", fragment: parsed.fragment, candidates };
|
|
1032
|
+
return { ...link2, status: "unresolved", fragment: parsed.fragment };
|
|
1033
|
+
}
|
|
1034
|
+
const linked = link2.syntax === "wikilink" ? resolvePathLikeWikilink(target, currentDocument.path, documents) : resolveWorkspaceDocumentLink(target, currentDocument.path, documents);
|
|
1035
|
+
return linked ? resolvedLink(link2, linked) : { ...link2, status: "unresolved", fragment: parsed.fragment };
|
|
1036
|
+
}
|
|
1037
|
+
function resolvedLink(link2, target) {
|
|
1038
|
+
return {
|
|
1039
|
+
...link2,
|
|
1040
|
+
status: "resolved",
|
|
1041
|
+
...target.fragment ? { fragment: target.fragment } : {},
|
|
1042
|
+
targetDocId: target.docId,
|
|
1043
|
+
targetPath: target.path,
|
|
1044
|
+
targetTitle: target.title
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
function toGraphEdge(document, link2, index) {
|
|
1048
|
+
return {
|
|
1049
|
+
id: `${document.docId}:${index}`,
|
|
1050
|
+
sourceDocId: document.docId,
|
|
1051
|
+
sourcePath: document.path,
|
|
1052
|
+
sourceTitle: document.title,
|
|
1053
|
+
label: link2.label,
|
|
1054
|
+
target: link2.target,
|
|
1055
|
+
...link2.title ? { title: link2.title } : {},
|
|
1056
|
+
line: link2.line,
|
|
1057
|
+
column: link2.column,
|
|
1058
|
+
syntax: link2.syntax,
|
|
1059
|
+
status: link2.status,
|
|
1060
|
+
...link2.fragment ? { fragment: link2.fragment } : {},
|
|
1061
|
+
...link2.targetDocId ? { targetDocId: link2.targetDocId } : {},
|
|
1062
|
+
...link2.targetPath ? { targetPath: link2.targetPath } : {},
|
|
1063
|
+
...link2.targetTitle ? { targetTitle: link2.targetTitle } : {},
|
|
1064
|
+
...link2.candidates ? { candidates: link2.candidates } : {}
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
function resolveWorkspaceDocumentLink(target, currentPath, documents) {
|
|
1068
|
+
const parsed = parseInternalTarget(target);
|
|
1069
|
+
if (!parsed || !parsed.path) return void 0;
|
|
1070
|
+
const workspacePath = resolveWorkspacePath(parsed.path, currentPath);
|
|
1071
|
+
if (!workspacePath) return void 0;
|
|
1072
|
+
const document = findLinkedDocument(candidateDocumentPaths(workspacePath), documents);
|
|
1073
|
+
return document ? { ...document, ...parsed.fragment ? { fragment: parsed.fragment } : {} } : void 0;
|
|
1074
|
+
}
|
|
1075
|
+
function resolvePathLikeWikilink(target, currentPath, documents) {
|
|
1076
|
+
const linked = resolveWorkspaceDocumentLink(target, currentPath, documents);
|
|
1077
|
+
if (linked) return linked;
|
|
1078
|
+
if (/^(?:\/|\.\/|\.\.\/)/.test(target)) return void 0;
|
|
1079
|
+
return resolveWorkspaceDocumentLink(`/${target}`, currentPath, documents);
|
|
1080
|
+
}
|
|
1081
|
+
function findBasenameWikilinkCandidates(name, fragment, documents) {
|
|
1082
|
+
const normalizedName = normalizeWikilinkName(name);
|
|
1083
|
+
return documents.filter((doc) => normalizeWikilinkName(markdownStem(doc.path)) === normalizedName).map((doc) => ({ ...doc, ...fragment ? { fragment } : {} })).sort((left, right) => left.path.localeCompare(right.path, void 0, { numeric: true, sensitivity: "base" }));
|
|
1084
|
+
}
|
|
1085
|
+
function findLinkedDocument(paths, documents) {
|
|
1086
|
+
for (const path of paths) {
|
|
1087
|
+
const document = documents.find((candidate) => candidate.path === path);
|
|
1088
|
+
if (document) return document;
|
|
1089
|
+
}
|
|
1090
|
+
return void 0;
|
|
1091
|
+
}
|
|
1092
|
+
function parseInternalTarget(target) {
|
|
1093
|
+
const trimmed = target.trim();
|
|
1094
|
+
if (!trimmed) return void 0;
|
|
1095
|
+
const hashIndex = trimmed.indexOf("#");
|
|
1096
|
+
const withoutFragment = hashIndex >= 0 ? trimmed.slice(0, hashIndex) : trimmed;
|
|
1097
|
+
const fragment = hashIndex >= 0 ? safeDecode(trimmed.slice(hashIndex + 1)) : void 0;
|
|
1098
|
+
const queryIndex = withoutFragment.indexOf("?");
|
|
1099
|
+
const path = safeDecode(queryIndex >= 0 ? withoutFragment.slice(0, queryIndex) : withoutFragment);
|
|
1100
|
+
return { path, ...fragment ? { fragment } : {} };
|
|
1101
|
+
}
|
|
1102
|
+
function resolveWorkspacePath(linkPath, currentPath) {
|
|
1103
|
+
const path = linkPath.replaceAll("\\", "/").trim();
|
|
1104
|
+
if (!path) return void 0;
|
|
1105
|
+
const source = path.startsWith("/") ? path.slice(1) : `${dirname2(currentPath)}/${path}`;
|
|
1106
|
+
return normalizeWorkspacePath2(source);
|
|
1107
|
+
}
|
|
1108
|
+
function normalizeWorkspacePath2(path) {
|
|
1109
|
+
const segments = [];
|
|
1110
|
+
for (const segment of path.split("/")) {
|
|
1111
|
+
if (!segment || segment === ".") continue;
|
|
1112
|
+
if (segment === "..") {
|
|
1113
|
+
if (segments.length === 0) return void 0;
|
|
1114
|
+
segments.pop();
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
segments.push(segment);
|
|
1118
|
+
}
|
|
1119
|
+
return segments.join("/");
|
|
1120
|
+
}
|
|
1121
|
+
function candidateDocumentPaths(path) {
|
|
1122
|
+
const candidates = [path];
|
|
1123
|
+
if (path.endsWith("/")) {
|
|
1124
|
+
candidates.push(`${path}README.md`, `${path}index.md`);
|
|
1125
|
+
} else if (!/\.[^/.]+$/.test(path)) {
|
|
1126
|
+
candidates.push(`${path}.md`, `${path}.mdx`, `${path}/README.md`, `${path}/index.md`);
|
|
1127
|
+
}
|
|
1128
|
+
return [...new Set(candidates)];
|
|
1129
|
+
}
|
|
1130
|
+
function isPathLikeWikilinkTarget(path) {
|
|
1131
|
+
return /^(?:\/|\.\/|\.\.\/)/.test(path) || path.includes("/") || path.includes("\\") || /\.(md|mdx)$/i.test(path);
|
|
1132
|
+
}
|
|
1133
|
+
function isExternalTarget(target) {
|
|
1134
|
+
const trimmed = target.trim();
|
|
1135
|
+
if (!trimmed || trimmed.startsWith("#")) return false;
|
|
1136
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(trimmed) || trimmed.startsWith("//") || looksLikeBareDomain(trimmed);
|
|
1137
|
+
}
|
|
1138
|
+
function looksLikeBareDomain(value) {
|
|
1139
|
+
return /^[a-z0-9-]+(?:\.[a-z0-9-]+)+(?:[/?#].*)?$/i.test(value) && !/\.(md|mdx)(?:[?#]|$)/i.test(value);
|
|
1140
|
+
}
|
|
1141
|
+
function markdownStem(path) {
|
|
1142
|
+
const basename3 = path.split("/").pop() ?? path;
|
|
1143
|
+
return basename3.replace(/\.mdx?$/i, "");
|
|
1144
|
+
}
|
|
1145
|
+
function normalizeWikilinkName(value) {
|
|
1146
|
+
return value.trim().toLowerCase();
|
|
1147
|
+
}
|
|
1148
|
+
function dirname2(path) {
|
|
1149
|
+
const index = path.lastIndexOf("/");
|
|
1150
|
+
return index >= 0 ? path.slice(0, index) : "";
|
|
1151
|
+
}
|
|
1152
|
+
function safeDecode(value) {
|
|
1153
|
+
try {
|
|
1154
|
+
return decodeURIComponent(value);
|
|
1155
|
+
} catch {
|
|
1156
|
+
return value;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
843
1160
|
// ../core/src/sidecar.ts
|
|
844
1161
|
function defaultActor(name = "mdocs") {
|
|
845
1162
|
return {
|
|
@@ -916,9 +1233,12 @@ async function indexWorkspace(io) {
|
|
|
916
1233
|
}
|
|
917
1234
|
async function getWorkspaceMap(io) {
|
|
918
1235
|
const manifest = await indexWorkspace(io);
|
|
1236
|
+
const graph = await workspaceGraphForManifest(io, manifest);
|
|
1237
|
+
const graphNodesByDocId = new Map(graph.nodes.map((node) => [node.docId, node]));
|
|
919
1238
|
const docs = await Promise.all(
|
|
920
1239
|
manifest.docs.map(async (entry) => {
|
|
921
1240
|
const sidecar = await readSidecar(io, entry.docId);
|
|
1241
|
+
const graphNode = graphNodesByDocId.get(entry.docId);
|
|
922
1242
|
return {
|
|
923
1243
|
docId: entry.docId,
|
|
924
1244
|
path: entry.path,
|
|
@@ -926,7 +1246,11 @@ async function getWorkspaceMap(io) {
|
|
|
926
1246
|
sidecarPath: sidecarPath(entry.docId),
|
|
927
1247
|
openComments: sidecar.comments.filter((comment2) => comment2.status === "open").length,
|
|
928
1248
|
openSuggestions: sidecar.suggestions.filter((suggestion) => suggestion.status === "open").length,
|
|
929
|
-
anchorsNeedingReview: sidecar.anchors.filter((anchor) => anchor.status !== "mapped").length
|
|
1249
|
+
anchorsNeedingReview: sidecar.anchors.filter((anchor) => anchor.status !== "mapped").length,
|
|
1250
|
+
outgoingLinks: graphNode?.outgoingLinks ?? 0,
|
|
1251
|
+
incomingLinks: graphNode?.incomingLinks ?? 0,
|
|
1252
|
+
unresolvedLinks: graphNode?.unresolvedLinks ?? 0,
|
|
1253
|
+
externalLinks: graphNode?.externalLinks ?? 0
|
|
930
1254
|
};
|
|
931
1255
|
})
|
|
932
1256
|
);
|
|
@@ -936,6 +1260,9 @@ async function getWorkspaceMap(io) {
|
|
|
936
1260
|
docs
|
|
937
1261
|
};
|
|
938
1262
|
}
|
|
1263
|
+
async function getWorkspaceGraph(io) {
|
|
1264
|
+
return workspaceGraphForManifest(io, await indexWorkspace(io));
|
|
1265
|
+
}
|
|
939
1266
|
async function getDocumentState(io, pathOrDocId) {
|
|
940
1267
|
const manifest = await indexWorkspace(io);
|
|
941
1268
|
const entry = findDoc(manifest, pathOrDocId);
|
|
@@ -964,10 +1291,31 @@ async function getDocumentState(io, pathOrDocId) {
|
|
|
964
1291
|
confidence: anchor.confidence
|
|
965
1292
|
})),
|
|
966
1293
|
images: extractMarkdownImages(markdown, entry.path),
|
|
1294
|
+
links: resolveMarkdownLinks(extractMarkdownLinks(markdown), entry, manifest.docs),
|
|
967
1295
|
openComments: nextSidecar.comments.filter((comment2) => comment2.status === "open").length,
|
|
968
1296
|
openSuggestions: nextSidecar.suggestions.filter((suggestion) => suggestion.status === "open").length
|
|
969
1297
|
};
|
|
970
1298
|
}
|
|
1299
|
+
async function workspaceGraphForManifest(io, manifest) {
|
|
1300
|
+
const documents = await Promise.all(
|
|
1301
|
+
manifest.docs.map(async (entry) => {
|
|
1302
|
+
const [markdown, sidecar] = await Promise.all([io.readText(entry.path), readSidecar(io, entry.docId)]);
|
|
1303
|
+
const links = resolveMarkdownLinks(extractMarkdownLinks(markdown), entry, manifest.docs);
|
|
1304
|
+
return {
|
|
1305
|
+
docId: entry.docId,
|
|
1306
|
+
path: entry.path,
|
|
1307
|
+
title: entry.title,
|
|
1308
|
+
openComments: sidecar.comments.filter((comment2) => comment2.status === "open").length,
|
|
1309
|
+
openSuggestions: sidecar.suggestions.filter((suggestion) => suggestion.status === "open").length,
|
|
1310
|
+
anchorsNeedingReview: sidecar.anchors.filter((anchor) => anchor.status !== "mapped").length,
|
|
1311
|
+
externalLinks: links.filter((link2) => link2.status === "external").length,
|
|
1312
|
+
unresolvedLinks: links.filter((link2) => link2.status === "unresolved" || link2.status === "ambiguous").length,
|
|
1313
|
+
links
|
|
1314
|
+
};
|
|
1315
|
+
})
|
|
1316
|
+
);
|
|
1317
|
+
return buildWorkspaceGraph(manifest.workspaceId, manifest.schemaVersion, documents);
|
|
1318
|
+
}
|
|
971
1319
|
async function addComment(io, pathOrDocId, range, body, author = defaultActor()) {
|
|
972
1320
|
const state = await getDocumentState(io, pathOrDocId);
|
|
973
1321
|
assertLineRangeWithin(state.markdown, range);
|
|
@@ -1234,7 +1582,7 @@ var DEFAULT_ROOT_PATH_RULES = {
|
|
|
1234
1582
|
maxFileSizeBytes: 1024 * 1024
|
|
1235
1583
|
};
|
|
1236
1584
|
var MARKDOWN_FILE_PATTERN = /\.mdx?$/i;
|
|
1237
|
-
function
|
|
1585
|
+
function normalizeWorkspacePath3(path) {
|
|
1238
1586
|
const normalized = path.replaceAll("\\", "/").replace(/\/+/g, "/").trim();
|
|
1239
1587
|
if (!normalized) throw new Error("Workspace path cannot be empty.");
|
|
1240
1588
|
if (normalized.startsWith("/") || /^[a-zA-Z]:\//.test(normalized)) {
|
|
@@ -1247,7 +1595,7 @@ function normalizeWorkspacePath2(path) {
|
|
|
1247
1595
|
return parts.filter((part) => part && part !== ".").join("/");
|
|
1248
1596
|
}
|
|
1249
1597
|
function assertWorkspacePathAllowed(path, rules = DEFAULT_ROOT_PATH_RULES, sizeBytes, ignoreMatcher) {
|
|
1250
|
-
const normalized =
|
|
1598
|
+
const normalized = normalizeWorkspacePath3(path);
|
|
1251
1599
|
if (isIgnoredWorkspacePath(normalized, rules)) {
|
|
1252
1600
|
throw new Error(`Workspace path is ignored by root policy: ${normalized}`);
|
|
1253
1601
|
}
|
|
@@ -1278,7 +1626,7 @@ function isWorkspaceMarkdownPathAllowed(path, rules = DEFAULT_ROOT_PATH_RULES, s
|
|
|
1278
1626
|
function createWorkspaceIgnoreMatcher(ignoreFiles) {
|
|
1279
1627
|
const rules = parseWorkspaceIgnoreRules(ignoreFiles);
|
|
1280
1628
|
return (path) => {
|
|
1281
|
-
const normalized =
|
|
1629
|
+
const normalized = normalizeWorkspacePath3(path);
|
|
1282
1630
|
let ignored = false;
|
|
1283
1631
|
for (const rule of rules) {
|
|
1284
1632
|
if (!matchesWorkspaceIgnoreRule(normalized, rule)) continue;
|
|
@@ -1288,7 +1636,7 @@ function createWorkspaceIgnoreMatcher(ignoreFiles) {
|
|
|
1288
1636
|
};
|
|
1289
1637
|
}
|
|
1290
1638
|
function isIgnoredWorkspacePath(path, rules = DEFAULT_ROOT_PATH_RULES) {
|
|
1291
|
-
const normalized =
|
|
1639
|
+
const normalized = normalizeWorkspacePath3(path);
|
|
1292
1640
|
const parts = normalized.split("/");
|
|
1293
1641
|
return parts.some((part) => rules.ignoredDirectories.includes(part));
|
|
1294
1642
|
}
|
|
@@ -1308,8 +1656,8 @@ function matchesAny(path, patterns) {
|
|
|
1308
1656
|
return patterns.some((pattern) => globToRegExp(pattern).test(path));
|
|
1309
1657
|
}
|
|
1310
1658
|
function parseWorkspaceIgnoreRules(ignoreFiles) {
|
|
1311
|
-
return [...ignoreFiles].sort((left, right) =>
|
|
1312
|
-
const normalizedPath =
|
|
1659
|
+
return [...ignoreFiles].sort((left, right) => normalizeWorkspacePath3(left.path).localeCompare(normalizeWorkspacePath3(right.path))).flatMap((file) => {
|
|
1660
|
+
const normalizedPath = normalizeWorkspacePath3(file.path);
|
|
1313
1661
|
const basePath = normalizedPath.endsWith("/.gitignore") ? normalizedPath.slice(0, -"/.gitignore".length) : "";
|
|
1314
1662
|
return file.content.split(/\r?\n/).flatMap((line) => {
|
|
1315
1663
|
const rule = parseWorkspaceIgnoreRule(basePath, line);
|
|
@@ -1416,14 +1764,14 @@ function createSourceMapping(input, updatedAt = (/* @__PURE__ */ new Date()).toI
|
|
|
1416
1764
|
};
|
|
1417
1765
|
}
|
|
1418
1766
|
function canonicalPathToReplicaPath(path, mapping) {
|
|
1419
|
-
if (path === ".mdocs" || path.startsWith(".mdocs/")) return
|
|
1420
|
-
const canonicalPath =
|
|
1767
|
+
if (path === ".mdocs" || path.startsWith(".mdocs/")) return normalizeWorkspacePath3(path);
|
|
1768
|
+
const canonicalPath = normalizeWorkspacePath3(path);
|
|
1421
1769
|
const relativePath = stripPrefix(canonicalPath, normalizeOptionalPrefix(mapping.canonicalPrefix), "canonical");
|
|
1422
1770
|
return joinWorkspacePath(normalizeOptionalPrefix(mapping.replicaPrefix), relativePath);
|
|
1423
1771
|
}
|
|
1424
1772
|
function replicaPathToCanonicalPath(path, mapping) {
|
|
1425
|
-
if (path === ".mdocs" || path.startsWith(".mdocs/")) return
|
|
1426
|
-
const replicaPath =
|
|
1773
|
+
if (path === ".mdocs" || path.startsWith(".mdocs/")) return normalizeWorkspacePath3(path);
|
|
1774
|
+
const replicaPath = normalizeWorkspacePath3(path);
|
|
1427
1775
|
const relativePath = stripPrefix(replicaPath, normalizeOptionalPrefix(mapping.replicaPrefix), "replica");
|
|
1428
1776
|
return joinWorkspacePath(normalizeOptionalPrefix(mapping.canonicalPrefix), relativePath);
|
|
1429
1777
|
}
|
|
@@ -1438,7 +1786,7 @@ function sourceMappingPathRules(mapping, base) {
|
|
|
1438
1786
|
function normalizeOptionalPrefix(prefix) {
|
|
1439
1787
|
const value = prefix?.trim();
|
|
1440
1788
|
if (!value || value === ".") return "";
|
|
1441
|
-
return
|
|
1789
|
+
return normalizeWorkspacePath3(value).replace(/\/+$/, "");
|
|
1442
1790
|
}
|
|
1443
1791
|
function stripPrefix(path, prefix, label) {
|
|
1444
1792
|
if (!prefix) return path;
|
|
@@ -1447,9 +1795,9 @@ function stripPrefix(path, prefix, label) {
|
|
|
1447
1795
|
throw new Error(`Path ${path} is outside ${label} prefix ${prefix}`);
|
|
1448
1796
|
}
|
|
1449
1797
|
function joinWorkspacePath(prefix, path) {
|
|
1450
|
-
if (!prefix) return
|
|
1798
|
+
if (!prefix) return normalizeWorkspacePath3(path);
|
|
1451
1799
|
if (!path) return prefix;
|
|
1452
|
-
return
|
|
1800
|
+
return normalizeWorkspacePath3(`${prefix}/${path}`);
|
|
1453
1801
|
}
|
|
1454
1802
|
|
|
1455
1803
|
// ../core/src/share-routes.ts
|
|
@@ -1462,6 +1810,10 @@ function parseSharePath(pathname) {
|
|
|
1462
1810
|
docId: docId ? decodeURIComponent(docId) : void 0
|
|
1463
1811
|
};
|
|
1464
1812
|
}
|
|
1813
|
+
function buildSharePath(workspaceId, rootId, docId) {
|
|
1814
|
+
const base = `/share/${encodeURIComponent(workspaceId)}/${encodeURIComponent(rootId)}`;
|
|
1815
|
+
return docId ? `${base}/${encodeURIComponent(docId)}` : base;
|
|
1816
|
+
}
|
|
1465
1817
|
|
|
1466
1818
|
// ../../node_modules/orderedmap/dist/index.js
|
|
1467
1819
|
function OrderedMap(content) {
|
|
@@ -3901,7 +4253,10 @@ function gatherMarks(schema2, marks) {
|
|
|
3901
4253
|
// ../core/src/pm-schema.ts
|
|
3902
4254
|
var canonicalSchema = new Schema({
|
|
3903
4255
|
nodes: {
|
|
3904
|
-
doc
|
|
4256
|
+
// The doc allows suggestion marks on its block children so pending
|
|
4257
|
+
// block-level suggestions (node marks) survive fromJSON and can be
|
|
4258
|
+
// structurally reverted before serialization.
|
|
4259
|
+
doc: { content: "block+", marks: "insertion deletion modification" },
|
|
3905
4260
|
paragraph: { group: "block", content: "inline*" },
|
|
3906
4261
|
heading: {
|
|
3907
4262
|
group: "block",
|
|
@@ -3969,14 +4324,45 @@ var canonicalSchema = new Schema({
|
|
|
3969
4324
|
},
|
|
3970
4325
|
inclusive: false
|
|
3971
4326
|
},
|
|
4327
|
+
wikilink: {
|
|
4328
|
+
attrs: {
|
|
4329
|
+
target: { default: null }
|
|
4330
|
+
},
|
|
4331
|
+
inclusive: false
|
|
4332
|
+
},
|
|
3972
4333
|
bold: {},
|
|
3973
4334
|
italic: {},
|
|
3974
4335
|
strike: {},
|
|
3975
|
-
code: {}
|
|
4336
|
+
code: {},
|
|
4337
|
+
// Pending-suggestion marks (mirroring the web editor's extensions). They
|
|
4338
|
+
// are never serialized — the canonical serializer reverts them first —
|
|
4339
|
+
// but the schema must know them so suggestion-marked docs load intact.
|
|
4340
|
+
insertion: {
|
|
4341
|
+
attrs: { id: { default: null } },
|
|
4342
|
+
inclusive: false,
|
|
4343
|
+
excludes: "deletion modification insertion"
|
|
4344
|
+
},
|
|
4345
|
+
deletion: {
|
|
4346
|
+
attrs: { id: { default: null } },
|
|
4347
|
+
inclusive: false,
|
|
4348
|
+
excludes: "insertion modification deletion"
|
|
4349
|
+
},
|
|
4350
|
+
modification: {
|
|
4351
|
+
attrs: {
|
|
4352
|
+
id: { default: null },
|
|
4353
|
+
type: { default: null },
|
|
4354
|
+
attrName: { default: null },
|
|
4355
|
+
previousValue: { default: null },
|
|
4356
|
+
newValue: { default: null }
|
|
4357
|
+
},
|
|
4358
|
+
inclusive: false,
|
|
4359
|
+
excludes: "deletion insertion"
|
|
4360
|
+
}
|
|
3976
4361
|
}
|
|
3977
4362
|
});
|
|
4363
|
+
var SUGGESTION_MARK_TYPES = /* @__PURE__ */ new Set(["insertion", "deletion", "modification"]);
|
|
3978
4364
|
var CANONICAL_NODE_TYPES = new Set(Object.keys(canonicalSchema.nodes));
|
|
3979
|
-
var CANONICAL_MARK_TYPES = new Set(Object.keys(canonicalSchema.marks));
|
|
4365
|
+
var CANONICAL_MARK_TYPES = new Set(Object.keys(canonicalSchema.marks).filter((name) => !SUGGESTION_MARK_TYPES.has(name)));
|
|
3980
4366
|
|
|
3981
4367
|
// ../../node_modules/markdown-it/lib/common/utils.mjs
|
|
3982
4368
|
var utils_exports = {};
|
|
@@ -10171,9 +10557,28 @@ var markdownSerializer = new MarkdownSerializer(
|
|
|
10171
10557
|
return `](${href.replace(/[()"]/g, "\\$&")})`;
|
|
10172
10558
|
},
|
|
10173
10559
|
mixable: false
|
|
10560
|
+
},
|
|
10561
|
+
wikilink: {
|
|
10562
|
+
open(_state, mark, parent, index) {
|
|
10563
|
+
const target = typeof mark.attrs.target === "string" ? mark.attrs.target : "";
|
|
10564
|
+
const label = markedRangeText(parent, index, mark);
|
|
10565
|
+
if (!target || label === target) return "[[";
|
|
10566
|
+
return `[[${target}|`;
|
|
10567
|
+
},
|
|
10568
|
+
close: "]]",
|
|
10569
|
+
mixable: false
|
|
10174
10570
|
}
|
|
10175
10571
|
}
|
|
10176
10572
|
);
|
|
10573
|
+
function markedRangeText(parent, index, mark) {
|
|
10574
|
+
let text2 = "";
|
|
10575
|
+
for (let childIndex = index; childIndex < parent.childCount; childIndex += 1) {
|
|
10576
|
+
const child = parent.child(childIndex);
|
|
10577
|
+
if (!mark.isInSet(child.marks)) break;
|
|
10578
|
+
text2 += child.textContent;
|
|
10579
|
+
}
|
|
10580
|
+
return text2;
|
|
10581
|
+
}
|
|
10177
10582
|
function escapeImageAlt(value) {
|
|
10178
10583
|
return value.replace(/\\/g, "\\\\").replace(/\[/g, "\\[").replace(/]/g, "\\]").replace(/\n+/g, " ");
|
|
10179
10584
|
}
|
|
@@ -10237,7 +10642,7 @@ var RemoteDocumentIO = class {
|
|
|
10237
10642
|
};
|
|
10238
10643
|
|
|
10239
10644
|
// src/agent.ts
|
|
10240
|
-
var CLI_VERSION = "0.3.
|
|
10645
|
+
var CLI_VERSION = "0.3.2";
|
|
10241
10646
|
var CLI_PACKAGE_NAME = "@magic-markdown/cli";
|
|
10242
10647
|
var AGENT_COMMANDS = [
|
|
10243
10648
|
{
|
|
@@ -10288,8 +10693,8 @@ var AGENT_COMMANDS = [
|
|
|
10288
10693
|
},
|
|
10289
10694
|
{
|
|
10290
10695
|
name: "remote",
|
|
10291
|
-
summary: "Read, comment on, suggest edits to, monitor, and restore the active Magic Markdown remote join.",
|
|
10292
|
-
usage: "mdocs remote map|context|comment|suggest|reject|events|history|restore --json",
|
|
10696
|
+
summary: "Read, organize, comment on, suggest edits to, monitor, and restore the active Magic Markdown remote join.",
|
|
10697
|
+
usage: "mdocs remote map|graph|context|create-file|move-file|comment|suggest|reject|events|history|restore|library|create-folder|update-folder|move-root|invite-folder --json",
|
|
10293
10698
|
output: "json",
|
|
10294
10699
|
mutates: true,
|
|
10295
10700
|
examples: [
|
|
@@ -10297,14 +10702,24 @@ var AGENT_COMMANDS = [
|
|
|
10297
10702
|
"mdocs remote context --start-line 1 --end-line 100 --json",
|
|
10298
10703
|
"mdocs remote context --start-line 101 --end-line 200 --json",
|
|
10299
10704
|
"mdocs remote map --json",
|
|
10705
|
+
"mdocs remote graph --json",
|
|
10706
|
+
"mdocs remote create-file docs/new-note.md --with-file /tmp/initial.md --json",
|
|
10707
|
+
"mdocs remote move-file docs/new-note.md --to archive/new-note.md --json",
|
|
10300
10708
|
"mdocs remote suggest --range 4:9 --with-file /tmp/replacement.md --message-file /tmp/message.txt --json",
|
|
10301
10709
|
"mdocs remote comment docs/example.md --range 3:5 --body-file /tmp/comment.txt --json",
|
|
10302
10710
|
"mdocs remote reject suggestion_abc123 --json",
|
|
10303
10711
|
"mdocs remote history --json",
|
|
10304
|
-
"mdocs remote restore head_abc123 --json"
|
|
10712
|
+
"mdocs remote restore head_abc123 --json",
|
|
10713
|
+
"mdocs remote library --json",
|
|
10714
|
+
"mdocs remote create-folder Research --json",
|
|
10715
|
+
"mdocs remote move-root --folder fold_abc123 --json",
|
|
10716
|
+
"mdocs remote invite-folder fold_abc123 --email person@example.com --role edit --json"
|
|
10305
10717
|
],
|
|
10306
10718
|
notes: [
|
|
10307
10719
|
"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>.",
|
|
10720
|
+
"remote graph requires a project-scoped join and returns the shared project's Obsidian-style Markdown/wikilink graph.",
|
|
10721
|
+
"remote create-file and remote move-file require a project-scoped join and edit access. They write through the root sync API, so version history and conflict handling stay intact.",
|
|
10722
|
+
"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.",
|
|
10308
10723
|
"remote comment/suggest never clobber concurrent edits: the server merges review additions, and the CLI rebases and retries automatically if a conflict still occurs.",
|
|
10309
10724
|
"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.",
|
|
10310
10725
|
"remote events returns truncated: true when older events were dropped from the bounded log; refetch full state with remote context instead of trusting the gap.",
|
|
@@ -10322,15 +10737,27 @@ var AGENT_COMMANDS = [
|
|
|
10322
10737
|
},
|
|
10323
10738
|
{
|
|
10324
10739
|
name: "map",
|
|
10325
|
-
summary: "Return the indexed Markdown documents and
|
|
10740
|
+
summary: "Return the indexed Markdown documents, review counts, and link counts.",
|
|
10326
10741
|
usage: "mdocs map --json",
|
|
10327
10742
|
output: "json",
|
|
10328
10743
|
mutates: true,
|
|
10329
10744
|
examples: ["mdocs map --json"]
|
|
10330
10745
|
},
|
|
10746
|
+
{
|
|
10747
|
+
name: "graph",
|
|
10748
|
+
summary: "Return the workspace knowledge graph as document nodes and Markdown/wikilink edges.",
|
|
10749
|
+
usage: "mdocs graph --json",
|
|
10750
|
+
output: "json",
|
|
10751
|
+
mutates: true,
|
|
10752
|
+
examples: ["mdocs graph --json"],
|
|
10753
|
+
notes: [
|
|
10754
|
+
"Edges include resolved document links, ambiguous wikilinks, unresolved links, external links, and same-document anchor links.",
|
|
10755
|
+
"Use this before broad agent edits to understand the Obsidian-style document graph and avoid changing related notes in isolation."
|
|
10756
|
+
]
|
|
10757
|
+
},
|
|
10331
10758
|
{
|
|
10332
10759
|
name: "context",
|
|
10333
|
-
summary: "Return agent-ready document Markdown, open review state, anchors, and
|
|
10760
|
+
summary: "Return agent-ready document Markdown, open review state, anchors, image references, and resolved outgoing links.",
|
|
10334
10761
|
usage: "mdocs context <path|docId> [--start-line N] [--end-line N] --json",
|
|
10335
10762
|
output: "json",
|
|
10336
10763
|
mutates: true,
|
|
@@ -10493,7 +10920,7 @@ function getAgentGuidePayload() {
|
|
|
10493
10920
|
function getAgentSkillMarkdown() {
|
|
10494
10921
|
return `---
|
|
10495
10922
|
name: magic-markdown
|
|
10496
|
-
description: Use the Magic Markdown CLI or MCP server to read, 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, comments, suggestions, image context, checkpoints, or local source sync.
|
|
10923
|
+
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.
|
|
10497
10924
|
metadata:
|
|
10498
10925
|
short-description: Operate Magic Markdown via CLI/MCP
|
|
10499
10926
|
---
|
|
@@ -10520,9 +10947,10 @@ Run \`mdocs help <command>\` (or \`mdocs <command> --help\`) for usage, examples
|
|
|
10520
10947
|
|
|
10521
10948
|
1. If given a Magic Markdown share URL, run \`mdocs join <share-url> --json\` first, and always include \`--name "<your agent name>"\` (for example \`--name "Claude Code"\`) so collaborators can see which agent is connected. Use \`mdocs remote ...\` commands after joining.
|
|
10522
10949
|
2. If working in a local workspace, run \`mdocs doctor --json\` to validate the workspace and learn recommended next commands.
|
|
10523
|
-
3. Run \`mdocs map --json\` or \`mdocs remote map --json\` to list documents, paths, docIds, open comments, open suggestions,
|
|
10524
|
-
4.
|
|
10525
|
-
5.
|
|
10950
|
+
3. Run \`mdocs map --json\` or \`mdocs remote map --json\` to list documents, paths, docIds, open comments, open suggestions, anchor review counts, and link counts. File-scoped joins (most share links) cover a single document, so \`mdocs remote map\` is unavailable for them \u2014 use \`mdocs remote context --json\` instead. To organize a shared project, rejoin with \`--scope project\` and use \`mdocs remote library --json\`.
|
|
10951
|
+
4. Run \`mdocs graph --json\` or project-scoped \`mdocs remote graph --json\` before broad edits to inspect the Obsidian-style document graph built from Markdown links and wikilinks.
|
|
10952
|
+
5. For a document, run \`mdocs context <path|docId> --json\` locally or \`mdocs remote context <path|docId> --json\` for a joined share.
|
|
10953
|
+
6. Use \`mdocs comment\` / \`mdocs remote comment\` for review notes and \`mdocs suggest\` / \`mdocs remote suggest\` for proposed replacements. Do not insert comments, CriticMarkup, directives, or Magic markers into Markdown files.
|
|
10526
10954
|
|
|
10527
10955
|
## Editing Rules
|
|
10528
10956
|
|
|
@@ -10547,6 +10975,7 @@ Run \`mdocs help <command>\` (or \`mdocs <command> --help\`) for usage, examples
|
|
|
10547
10975
|
## Access Roles and Version History
|
|
10548
10976
|
|
|
10549
10977
|
- Agents are **suggest-only by default**: comments and suggestions always work, but direct content rewrites are rejected with a \`forbidden_role\` error until a human grants edit access. Propose changes with \`mdocs remote suggest\` instead of rewriting content.
|
|
10978
|
+
- Project-scoped agents with edit access can create and move Markdown files and can move the joined root into library folders. Folder invites and folder hierarchy updates require admin access on the joined root.
|
|
10550
10979
|
- Every accepted change is a content-addressed commit with a full snapshot. \`mdocs remote history --json\` lists commits with changed paths; \`mdocs remote restore <headId> --json\` restores the joined document to that snapshot (as a new commit, so it is reversible). Restore requires edit access \u2014 only run it when the user asked.
|
|
10551
10980
|
|
|
10552
10981
|
## Core Commands
|
|
@@ -10561,7 +10990,7 @@ Start the local stdio MCP server with:
|
|
|
10561
10990
|
mdocs serve-mcp --cwd /path/to/workspace
|
|
10562
10991
|
\`\`\`
|
|
10563
10992
|
|
|
10564
|
-
The MCP server exposes document resources, image resources,
|
|
10993
|
+
The MCP server exposes document resources, image resources, workspace map and graph resources, an agent guide resource, typed tools for comments and suggestions, and the \`magic_markdown_agent_workflow\` prompt.
|
|
10565
10994
|
`;
|
|
10566
10995
|
}
|
|
10567
10996
|
function formatCommandReference() {
|
|
@@ -10618,7 +11047,7 @@ function toCliError(error) {
|
|
|
10618
11047
|
|
|
10619
11048
|
// src/checkpoints.ts
|
|
10620
11049
|
import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
10621
|
-
import { basename, dirname as
|
|
11050
|
+
import { basename, dirname as dirname3, join } from "node:path";
|
|
10622
11051
|
async function createCheckpoint(root, label) {
|
|
10623
11052
|
const id = createId("checkpoint");
|
|
10624
11053
|
const createdAt = nowIso();
|
|
@@ -10683,7 +11112,7 @@ async function restoreTree(snapshotRoot, root) {
|
|
|
10683
11112
|
await mkdir(dest, { recursive: true });
|
|
10684
11113
|
await restoreTree(source, dest);
|
|
10685
11114
|
} else if (entry.isFile()) {
|
|
10686
|
-
await mkdir(
|
|
11115
|
+
await mkdir(dirname3(dest), { recursive: true });
|
|
10687
11116
|
await cp(source, dest);
|
|
10688
11117
|
}
|
|
10689
11118
|
}
|
|
@@ -10739,7 +11168,7 @@ function nextCommands(map) {
|
|
|
10739
11168
|
|
|
10740
11169
|
// src/fs-io.ts
|
|
10741
11170
|
import { mkdir as mkdir2, readFile as readFile2, readdir as readdir2, realpath, rename, rm as rm2, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
|
|
10742
|
-
import { dirname as
|
|
11171
|
+
import { dirname as dirname4, join as join2, relative, resolve } from "node:path";
|
|
10743
11172
|
var NodeWorkspaceIO = class {
|
|
10744
11173
|
constructor(root, pathRules = DEFAULT_ROOT_PATH_RULES) {
|
|
10745
11174
|
this.root = root;
|
|
@@ -10752,12 +11181,12 @@ var NodeWorkspaceIO = class {
|
|
|
10752
11181
|
}
|
|
10753
11182
|
async writeText(path, content) {
|
|
10754
11183
|
const target = await this.resolveWritablePath(path, Buffer.byteLength(content, "utf8"));
|
|
10755
|
-
await mkdir2(
|
|
11184
|
+
await mkdir2(dirname4(target), { recursive: true });
|
|
10756
11185
|
await writeFile2(target, content, "utf8");
|
|
10757
11186
|
}
|
|
10758
11187
|
async writeTextAtomic(path, content) {
|
|
10759
11188
|
const target = await this.resolveWritablePath(path, Buffer.byteLength(content, "utf8"));
|
|
10760
|
-
await mkdir2(
|
|
11189
|
+
await mkdir2(dirname4(target), { recursive: true });
|
|
10761
11190
|
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
10762
11191
|
await writeFile2(temporary, content, "utf8");
|
|
10763
11192
|
await rename(temporary, target);
|
|
@@ -10794,12 +11223,12 @@ var NodeWorkspaceIO = class {
|
|
|
10794
11223
|
const normalized = assertWorkspacePathAllowed(path, this.pathRules, sizeBytes);
|
|
10795
11224
|
const root = await realpath(this.root);
|
|
10796
11225
|
const target = resolve(root, normalized);
|
|
10797
|
-
await mkdir2(
|
|
10798
|
-
assertPathInsideRoot(root, await realpath(
|
|
11226
|
+
await mkdir2(dirname4(target), { recursive: true });
|
|
11227
|
+
assertPathInsideRoot(root, await realpath(dirname4(target)));
|
|
10799
11228
|
return target;
|
|
10800
11229
|
}
|
|
10801
11230
|
async resolvePath(path) {
|
|
10802
|
-
const normalized =
|
|
11231
|
+
const normalized = normalizeWorkspacePath3(path);
|
|
10803
11232
|
const root = await realpath(this.root);
|
|
10804
11233
|
const target = resolve(root, normalized);
|
|
10805
11234
|
assertPathInsideRoot(root, target);
|
|
@@ -10836,7 +11265,7 @@ var PathMappedWorkspaceIO = class {
|
|
|
10836
11265
|
return files.map((path) => replicaPathToCanonicalPath(path, this.mapping)).sort();
|
|
10837
11266
|
}
|
|
10838
11267
|
toReplicaPath(path) {
|
|
10839
|
-
if (path === ".mdocs" || path.startsWith(".mdocs/")) return
|
|
11268
|
+
if (path === ".mdocs" || path.startsWith(".mdocs/")) return normalizeWorkspacePath3(path);
|
|
10840
11269
|
return canonicalPathToReplicaPath(path, this.mapping);
|
|
10841
11270
|
}
|
|
10842
11271
|
};
|
|
@@ -10857,14 +11286,14 @@ async function walk(root, output, base, rules, ignoreFiles) {
|
|
|
10857
11286
|
}
|
|
10858
11287
|
if (entry.isFile()) {
|
|
10859
11288
|
const stats = await stat2(absolute);
|
|
10860
|
-
if (isWorkspaceMarkdownPathAllowed(relativePath, rules, stats.size, ignoreMatcher)) output.push(
|
|
11289
|
+
if (isWorkspaceMarkdownPathAllowed(relativePath, rules, stats.size, ignoreMatcher)) output.push(normalizeWorkspacePath3(relativePath));
|
|
10861
11290
|
}
|
|
10862
11291
|
}
|
|
10863
11292
|
}
|
|
10864
11293
|
async function withDirectoryIgnoreFile(root, base, ignoreFiles) {
|
|
10865
11294
|
try {
|
|
10866
11295
|
const path = join2(relative(base, root).replaceAll("\\", "/"), ".gitignore").replace(/^\.\//, "");
|
|
10867
|
-
const normalizedPath = path === ".gitignore" ? path :
|
|
11296
|
+
const normalizedPath = path === ".gitignore" ? path : normalizeWorkspacePath3(path);
|
|
10868
11297
|
const content = await readFile2(join2(root, ".gitignore"), "utf8");
|
|
10869
11298
|
return [...ignoreFiles, { path: normalizedPath, content }];
|
|
10870
11299
|
} catch {
|
|
@@ -10997,6 +11426,7 @@ async function contextForDocument(io, path, startLine, endLine) {
|
|
|
10997
11426
|
endLine: el,
|
|
10998
11427
|
markdown,
|
|
10999
11428
|
images: withMcpImageResources(io, state).images,
|
|
11429
|
+
links: state.links,
|
|
11000
11430
|
comments: state.sidecar.comments.filter((comment2) => comment2.status === "open"),
|
|
11001
11431
|
suggestions: state.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
|
|
11002
11432
|
anchors: state.anchors
|
|
@@ -11074,7 +11504,12 @@ var actorProperties = {
|
|
|
11074
11504
|
var tools = [
|
|
11075
11505
|
{
|
|
11076
11506
|
name: "mdocs_map",
|
|
11077
|
-
description: "Return the workspace document map.",
|
|
11507
|
+
description: "Return the workspace document map with review and link counts.",
|
|
11508
|
+
inputSchema: noArgsSchema
|
|
11509
|
+
},
|
|
11510
|
+
{
|
|
11511
|
+
name: "mdocs_graph",
|
|
11512
|
+
description: "Return the workspace knowledge graph as document nodes and Markdown/wikilink edges. Edges include resolved, ambiguous, unresolved, external, and same-document anchor links.",
|
|
11078
11513
|
inputSchema: noArgsSchema
|
|
11079
11514
|
},
|
|
11080
11515
|
{
|
|
@@ -11084,7 +11519,7 @@ var tools = [
|
|
|
11084
11519
|
},
|
|
11085
11520
|
{
|
|
11086
11521
|
name: "mdocs_context",
|
|
11087
|
-
description: "Return agent-ready document Markdown, open review state, anchors, and
|
|
11522
|
+
description: "Return agent-ready document Markdown, open review state, anchors, image references, and resolved outgoing links. Use startLine/endLine to read a specific line range when the full document is too large for one context window. The response always includes totalLines so you know whether to page.",
|
|
11088
11523
|
inputSchema: {
|
|
11089
11524
|
type: "object",
|
|
11090
11525
|
properties: {
|
|
@@ -11223,6 +11658,11 @@ async function dispatch(io, method, params) {
|
|
|
11223
11658
|
uri: "mdocs://workspace/map",
|
|
11224
11659
|
name: "Workspace Map",
|
|
11225
11660
|
mimeType: "application/vnd.mdocs.workspace-map+json"
|
|
11661
|
+
},
|
|
11662
|
+
{
|
|
11663
|
+
uri: "mdocs://workspace/graph",
|
|
11664
|
+
name: "Workspace Knowledge Graph",
|
|
11665
|
+
mimeType: "application/vnd.mdocs.workspace-graph+json"
|
|
11226
11666
|
}
|
|
11227
11667
|
])
|
|
11228
11668
|
};
|
|
@@ -11315,6 +11755,17 @@ async function readResource(io, path) {
|
|
|
11315
11755
|
]
|
|
11316
11756
|
};
|
|
11317
11757
|
}
|
|
11758
|
+
if (path === "mdocs://workspace/graph") {
|
|
11759
|
+
return {
|
|
11760
|
+
contents: [
|
|
11761
|
+
{
|
|
11762
|
+
uri: path,
|
|
11763
|
+
mimeType: "application/vnd.mdocs.workspace-graph+json",
|
|
11764
|
+
text: JSON.stringify(await getWorkspaceGraph(io), null, 2)
|
|
11765
|
+
}
|
|
11766
|
+
]
|
|
11767
|
+
};
|
|
11768
|
+
}
|
|
11318
11769
|
const imageMatch = /^mdocs:\/\/documents\/([^/]+)\/images\/(\d+)$/.exec(path);
|
|
11319
11770
|
if (imageMatch) {
|
|
11320
11771
|
return readImageResource(io, path, imageMatch[1] ?? "", Number(imageMatch[2]));
|
|
@@ -11333,6 +11784,7 @@ async function readResource(io, path) {
|
|
|
11333
11784
|
}
|
|
11334
11785
|
async function callTool(io, name, args) {
|
|
11335
11786
|
if (name === "mdocs_map") return getWorkspaceMap(io);
|
|
11787
|
+
if (name === "mdocs_graph") return getWorkspaceGraph(io);
|
|
11336
11788
|
if (name === "mdocs_doctor") return runDoctor(io, io.root);
|
|
11337
11789
|
if (name === "mdocs_context") {
|
|
11338
11790
|
const startLine = typeof args.startLine === "number" ? args.startLine : void 0;
|
|
@@ -11459,6 +11911,42 @@ async function fetchTree(record) {
|
|
|
11459
11911
|
headers: shareHeaders(record.shareUrl)
|
|
11460
11912
|
});
|
|
11461
11913
|
}
|
|
11914
|
+
async function fetchLibrary(record) {
|
|
11915
|
+
return fetchJson(libraryUrl(record), {
|
|
11916
|
+
headers: shareHeaders(record.shareUrl)
|
|
11917
|
+
});
|
|
11918
|
+
}
|
|
11919
|
+
async function postLibraryFolder(record, input, actor) {
|
|
11920
|
+
return fetchJson(`${libraryUrl(record)}/folders`, {
|
|
11921
|
+
method: "POST",
|
|
11922
|
+
headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
|
|
11923
|
+
body: JSON.stringify({ ...input, actor })
|
|
11924
|
+
});
|
|
11925
|
+
}
|
|
11926
|
+
async function patchLibraryFolder(record, folderId, input, actor) {
|
|
11927
|
+
return fetchJson(`${libraryUrl(record)}/folders/${encodeURIComponent(folderId)}`, {
|
|
11928
|
+
method: "PATCH",
|
|
11929
|
+
headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
|
|
11930
|
+
body: JSON.stringify({ ...input, actor })
|
|
11931
|
+
});
|
|
11932
|
+
}
|
|
11933
|
+
async function postMoveRoot(record, folderId, actor) {
|
|
11934
|
+
return fetchJson(`${libraryUrl(record)}/move-root`, {
|
|
11935
|
+
method: "POST",
|
|
11936
|
+
headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
|
|
11937
|
+
body: JSON.stringify({ folderId, actor })
|
|
11938
|
+
});
|
|
11939
|
+
}
|
|
11940
|
+
async function postFolderInvite(record, folderId, input, actor) {
|
|
11941
|
+
return fetchJson(
|
|
11942
|
+
`${libraryUrl(record)}/folders/${encodeURIComponent(folderId)}/invites`,
|
|
11943
|
+
{
|
|
11944
|
+
method: "POST",
|
|
11945
|
+
headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
|
|
11946
|
+
body: JSON.stringify({ ...input, actor })
|
|
11947
|
+
}
|
|
11948
|
+
);
|
|
11949
|
+
}
|
|
11462
11950
|
async function fetchDocument(record, pathOrDocId) {
|
|
11463
11951
|
if (record.scope === "file" && record.docId) {
|
|
11464
11952
|
const document = await fetchDocumentById(record, record.docId);
|
|
@@ -11579,6 +12067,9 @@ function agentUrl(record, docId, action, query) {
|
|
|
11579
12067
|
const base = `${record.origin}/api/agent/${encodeURIComponent(record.workspaceId)}/${encodeURIComponent(record.rootId)}/${encodeURIComponent(docId)}/${action}`;
|
|
11580
12068
|
return query ? `${base}?${query}` : base;
|
|
11581
12069
|
}
|
|
12070
|
+
function libraryUrl(record) {
|
|
12071
|
+
return `${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/library`;
|
|
12072
|
+
}
|
|
11582
12073
|
function shareHeaders(shareUrl) {
|
|
11583
12074
|
return { Referer: shareUrl, Authorization: `Bearer ${shareUrl}` };
|
|
11584
12075
|
}
|
|
@@ -11758,12 +12249,13 @@ async function runJoinCommand(root, parsed) {
|
|
|
11758
12249
|
const agentName = resolveAgentName(parsed.flags);
|
|
11759
12250
|
const agentId = normalizeAgentId(String(parsed.flags["agent-id"] ?? parsed.flags.actor ?? agentName));
|
|
11760
12251
|
const docId = parsedShare.docId ?? await firstDocIdForProject(parsedShare);
|
|
11761
|
-
const
|
|
12252
|
+
const shareUrl = shareUrlForScope(target, { ...parsedShare, docId });
|
|
12253
|
+
const state = await postPresenceAndReadState(parsedShare, shareUrl, docId, agentId, agentName);
|
|
11762
12254
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
11763
12255
|
const record = {
|
|
11764
12256
|
joinId: joinIdFor(target),
|
|
11765
12257
|
scope: parsedShare.scope,
|
|
11766
|
-
shareUrl
|
|
12258
|
+
shareUrl,
|
|
11767
12259
|
origin: parsedShare.origin,
|
|
11768
12260
|
workspaceId: parsedShare.workspaceId,
|
|
11769
12261
|
rootId: parsedShare.rootId,
|
|
@@ -11782,7 +12274,7 @@ async function runJoinsCommand(root) {
|
|
|
11782
12274
|
return listJoinRecords(root);
|
|
11783
12275
|
}
|
|
11784
12276
|
async function runRemoteCommand(root, subcommand, parsed) {
|
|
11785
|
-
const record = await
|
|
12277
|
+
const record = await normalizedSelectedJoin(root, parsed.flags);
|
|
11786
12278
|
switch (subcommand) {
|
|
11787
12279
|
case "map":
|
|
11788
12280
|
case void 0:
|
|
@@ -11793,8 +12285,14 @@ async function runRemoteCommand(root, subcommand, parsed) {
|
|
|
11793
12285
|
}
|
|
11794
12286
|
await refreshPresence(record, record.docId);
|
|
11795
12287
|
return fetchTree(record);
|
|
12288
|
+
case "graph":
|
|
12289
|
+
return remoteGraph(record);
|
|
11796
12290
|
case "context":
|
|
11797
12291
|
return remoteContext(record, parsed.command[2], parsed.flags);
|
|
12292
|
+
case "create-file":
|
|
12293
|
+
return remoteCreateFile(root, record, parsed);
|
|
12294
|
+
case "move-file":
|
|
12295
|
+
return remoteMoveFile(root, record, parsed);
|
|
11798
12296
|
case "comment":
|
|
11799
12297
|
return remoteComment(root, record, parsed);
|
|
11800
12298
|
case "suggest":
|
|
@@ -11807,14 +12305,48 @@ async function runRemoteCommand(root, subcommand, parsed) {
|
|
|
11807
12305
|
return remoteHistory(record, parsed.flags);
|
|
11808
12306
|
case "restore":
|
|
11809
12307
|
return remoteRestore(record, parsed);
|
|
12308
|
+
case "library":
|
|
12309
|
+
assertProjectScope(record, "library");
|
|
12310
|
+
await refreshPresence(record, record.docId);
|
|
12311
|
+
return fetchLibrary(record);
|
|
12312
|
+
case "create-folder":
|
|
12313
|
+
return remoteCreateFolder(record, parsed);
|
|
12314
|
+
case "update-folder":
|
|
12315
|
+
return remoteUpdateFolder(record, parsed);
|
|
12316
|
+
case "move-root":
|
|
12317
|
+
return remoteMoveRoot(record, parsed);
|
|
12318
|
+
case "invite-folder":
|
|
12319
|
+
return remoteInviteFolder(record, parsed);
|
|
11810
12320
|
case "rejoin":
|
|
11811
12321
|
return rejoin(root, record.joinId, parsed.flags);
|
|
11812
12322
|
default:
|
|
11813
12323
|
throw new CliError("usage_error", `Unknown remote subcommand: ${subcommand}`, {
|
|
11814
|
-
hint: "Use mdocs remote map|context|comment|suggest|reject|events|history|restore|rejoin."
|
|
12324
|
+
hint: "Use mdocs remote map|graph|context|create-file|move-file|comment|suggest|reject|events|history|restore|library|create-folder|update-folder|move-root|invite-folder|rejoin."
|
|
11815
12325
|
});
|
|
11816
12326
|
}
|
|
11817
12327
|
}
|
|
12328
|
+
async function remoteGraph(record) {
|
|
12329
|
+
assertProjectScope(record, "graph");
|
|
12330
|
+
await refreshPresence(record, record.docId);
|
|
12331
|
+
const tree = await fetchTree(record);
|
|
12332
|
+
const documents = await Promise.all(tree.docs.map((doc) => fetchDocument(record, doc.docId)));
|
|
12333
|
+
const lookup = documents.map((doc) => ({ docId: doc.docId, path: doc.path, title: doc.title }));
|
|
12334
|
+
const graphDocuments = documents.map((doc) => {
|
|
12335
|
+
const links = doc.links ?? resolveMarkdownLinks(extractMarkdownLinks(doc.markdown), doc, lookup);
|
|
12336
|
+
return {
|
|
12337
|
+
docId: doc.docId,
|
|
12338
|
+
path: doc.path,
|
|
12339
|
+
title: doc.title,
|
|
12340
|
+
openComments: doc.openComments,
|
|
12341
|
+
openSuggestions: doc.openSuggestions,
|
|
12342
|
+
anchorsNeedingReview: doc.anchors.filter((anchor) => anchor.status !== "mapped").length,
|
|
12343
|
+
externalLinks: links.filter((link2) => link2.status === "external").length,
|
|
12344
|
+
unresolvedLinks: links.filter((link2) => link2.status === "unresolved" || link2.status === "ambiguous").length,
|
|
12345
|
+
links
|
|
12346
|
+
};
|
|
12347
|
+
});
|
|
12348
|
+
return buildWorkspaceGraph(record.workspaceId, SIDECAR_SCHEMA_VERSION, graphDocuments);
|
|
12349
|
+
}
|
|
11818
12350
|
async function remoteHistory(record, flags) {
|
|
11819
12351
|
await refreshPresence(record, record.docId);
|
|
11820
12352
|
const limit = typeof flags.limit === "string" ? Number(flags.limit) || 50 : 50;
|
|
@@ -11844,9 +12376,11 @@ async function remoteRestore(record, parsed) {
|
|
|
11844
12376
|
async function rejoin(root, joinId, flags) {
|
|
11845
12377
|
const record = await readSelectedJoin(root, { ...flags, ...joinId ? { join: joinId } : {} });
|
|
11846
12378
|
const docId = record.docId ?? await firstDocIdForProject(record);
|
|
11847
|
-
const
|
|
12379
|
+
const shareUrl = shareUrlForScope(record.shareUrl, { ...record, docId });
|
|
12380
|
+
const state = await postPresenceAndReadState(record, shareUrl, docId, record.agentId, record.agentName);
|
|
11848
12381
|
const next = {
|
|
11849
12382
|
...record,
|
|
12383
|
+
shareUrl,
|
|
11850
12384
|
docId,
|
|
11851
12385
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11852
12386
|
currentHead: state.document.currentSha
|
|
@@ -11854,6 +12388,15 @@ async function rejoin(root, joinId, flags) {
|
|
|
11854
12388
|
await writeJoinRecord(root, next);
|
|
11855
12389
|
return joinSummary(next, state.document);
|
|
11856
12390
|
}
|
|
12391
|
+
async function normalizedSelectedJoin(root, flags) {
|
|
12392
|
+
const record = await readSelectedJoin(root, flags);
|
|
12393
|
+
if (!record.docId && record.scope === "file") return record;
|
|
12394
|
+
const shareUrl = shareUrlForScope(record.shareUrl, record);
|
|
12395
|
+
if (shareUrl === record.shareUrl) return record;
|
|
12396
|
+
const next = { ...record, shareUrl, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
12397
|
+
await writeJoinRecord(root, next);
|
|
12398
|
+
return next;
|
|
12399
|
+
}
|
|
11857
12400
|
async function refreshPresence(record, docId) {
|
|
11858
12401
|
if (!docId) return;
|
|
11859
12402
|
await postPresence(record, record.shareUrl, docId, record.agentId, record.agentName).catch(() => void 0);
|
|
@@ -11887,8 +12430,74 @@ async function remoteContext(record, pathOrDocId, flags = {}) {
|
|
|
11887
12430
|
comments: document.sidecar.comments.filter((comment2) => comment2.status === "open"),
|
|
11888
12431
|
suggestions: document.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
|
|
11889
12432
|
anchors: document.anchors,
|
|
11890
|
-
images: document.images
|
|
12433
|
+
images: document.images,
|
|
12434
|
+
links: document.links
|
|
12435
|
+
};
|
|
12436
|
+
}
|
|
12437
|
+
async function remoteCreateFile(root, record, parsed) {
|
|
12438
|
+
assertProjectScope(record, "create-file");
|
|
12439
|
+
const path = parsed.command[2];
|
|
12440
|
+
if (!path) {
|
|
12441
|
+
throw new CliError("usage_error", "Missing file path.", {
|
|
12442
|
+
hint: "Run mdocs remote create-file <path> --with-file /tmp/initial.md --json."
|
|
12443
|
+
});
|
|
12444
|
+
}
|
|
12445
|
+
const tree = await fetchTree(record);
|
|
12446
|
+
const markdownInput = await readOptionalTextFlag(parsed.flags, root, ["with", "markdown", "body"]);
|
|
12447
|
+
const markdown = markdownInput ?? defaultMarkdown(path);
|
|
12448
|
+
const normalizedPath = normalizeRemoteWorkspacePath(path, tree.root.pathRules, Buffer.byteLength(markdown, "utf8"));
|
|
12449
|
+
if (tree.docs.some((doc) => doc.path === normalizedPath)) {
|
|
12450
|
+
throw new CliError("validation_error", `Document already exists: ${normalizedPath}`, {
|
|
12451
|
+
hint: "Choose a new path or use mdocs remote context to inspect the existing document."
|
|
12452
|
+
});
|
|
12453
|
+
}
|
|
12454
|
+
const docId = createDocId();
|
|
12455
|
+
const title = titleFromMarkdown(normalizedPath, markdown);
|
|
12456
|
+
const sidecar = emptySidecar(docId, normalizedPath, title);
|
|
12457
|
+
const baseDocument = {
|
|
12458
|
+
workspaceId: record.workspaceId,
|
|
12459
|
+
rootId: record.rootId,
|
|
12460
|
+
docId,
|
|
12461
|
+
path: normalizedPath,
|
|
12462
|
+
title,
|
|
12463
|
+
markdown,
|
|
12464
|
+
sidecar,
|
|
12465
|
+
anchors: [],
|
|
12466
|
+
images: [],
|
|
12467
|
+
links: [],
|
|
12468
|
+
openComments: 0,
|
|
12469
|
+
openSuggestions: 0,
|
|
12470
|
+
currentSha: tree.root.canonical.head
|
|
11891
12471
|
};
|
|
12472
|
+
const document = await pushDocument(record, baseDocument, markdown, sidecar);
|
|
12473
|
+
await recordHead(root, record, document);
|
|
12474
|
+
return { document };
|
|
12475
|
+
}
|
|
12476
|
+
async function remoteMoveFile(root, record, parsed) {
|
|
12477
|
+
assertProjectScope(record, "move-file");
|
|
12478
|
+
const pathOrDocId = parsed.command[2];
|
|
12479
|
+
if (!pathOrDocId) {
|
|
12480
|
+
throw new CliError("usage_error", "Missing source document path or docId.", {
|
|
12481
|
+
hint: "Run mdocs remote move-file <path|docId> --to <new-path> --json."
|
|
12482
|
+
});
|
|
12483
|
+
}
|
|
12484
|
+
const nextPathFlag = requiredFlag(parsed.flags, "to");
|
|
12485
|
+
const document = await fetchDocument(record, pathOrDocId);
|
|
12486
|
+
const tree = await fetchTree(record);
|
|
12487
|
+
const nextPath = normalizeRemoteWorkspacePath(nextPathFlag, tree.root.pathRules, Buffer.byteLength(document.markdown, "utf8"));
|
|
12488
|
+
if (tree.docs.some((doc) => doc.docId !== document.docId && doc.path === nextPath)) {
|
|
12489
|
+
throw new CliError("validation_error", `Document already exists: ${nextPath}`, {
|
|
12490
|
+
hint: "Choose a path that is not already in use."
|
|
12491
|
+
});
|
|
12492
|
+
}
|
|
12493
|
+
const moved = await pushDocument(
|
|
12494
|
+
record,
|
|
12495
|
+
{ ...document, path: nextPath, title: titleFromMarkdown(nextPath, document.markdown) },
|
|
12496
|
+
document.markdown,
|
|
12497
|
+
document.sidecar
|
|
12498
|
+
);
|
|
12499
|
+
await recordHead(root, record, moved);
|
|
12500
|
+
return { document: moved };
|
|
11892
12501
|
}
|
|
11893
12502
|
async function remoteComment(root, record, parsed) {
|
|
11894
12503
|
const body = await readRequiredTextFlag(parsed.flags, root, ["body", "message"]);
|
|
@@ -11913,6 +12522,62 @@ async function remoteSuggest(root, record, parsed) {
|
|
|
11913
12522
|
);
|
|
11914
12523
|
return { suggestion: result.created, document: result.document };
|
|
11915
12524
|
}
|
|
12525
|
+
async function remoteCreateFolder(record, parsed) {
|
|
12526
|
+
assertProjectScope(record, "create-folder");
|
|
12527
|
+
const name = folderNameFromArgs(parsed);
|
|
12528
|
+
const parentId = optionalFolderTarget(parsed.flags.parent);
|
|
12529
|
+
await refreshPresence(record, record.docId);
|
|
12530
|
+
return postLibraryFolder(record, { name, ...parentId ? { parentId } : {} }, actorForRecord(record));
|
|
12531
|
+
}
|
|
12532
|
+
async function remoteUpdateFolder(record, parsed) {
|
|
12533
|
+
assertProjectScope(record, "update-folder");
|
|
12534
|
+
const folderId = parsed.command[2];
|
|
12535
|
+
if (!folderId) {
|
|
12536
|
+
throw new CliError("usage_error", "Missing folder id.", {
|
|
12537
|
+
hint: "Run mdocs remote update-folder <folderId> --name <name> --json."
|
|
12538
|
+
});
|
|
12539
|
+
}
|
|
12540
|
+
const input = {};
|
|
12541
|
+
if (typeof parsed.flags.name === "string") input.name = parsed.flags.name;
|
|
12542
|
+
if (parsed.flags.home === true || parsed.flags.home === "true") input.parentId = null;
|
|
12543
|
+
else if (parsed.flags.parent !== void 0) input.parentId = optionalFolderTarget(parsed.flags.parent);
|
|
12544
|
+
if (input.name === void 0 && input.parentId === void 0) {
|
|
12545
|
+
throw new CliError("usage_error", "Nothing to update.", {
|
|
12546
|
+
hint: "Pass --name <name>, --parent <folderId>, or --home."
|
|
12547
|
+
});
|
|
12548
|
+
}
|
|
12549
|
+
await refreshPresence(record, record.docId);
|
|
12550
|
+
return patchLibraryFolder(record, folderId, input, actorForRecord(record));
|
|
12551
|
+
}
|
|
12552
|
+
async function remoteMoveRoot(record, parsed) {
|
|
12553
|
+
assertProjectScope(record, "move-root");
|
|
12554
|
+
const folderId = parsed.flags.home === true || parsed.flags.home === "true" ? null : optionalFolderTarget(parsed.flags.folder);
|
|
12555
|
+
if (folderId === void 0) {
|
|
12556
|
+
throw new CliError("usage_error", "Missing target folder.", {
|
|
12557
|
+
hint: "Pass --folder <folderId> to move into a folder, or --home to move to the top level."
|
|
12558
|
+
});
|
|
12559
|
+
}
|
|
12560
|
+
await refreshPresence(record, record.docId);
|
|
12561
|
+
return postMoveRoot(record, folderId, actorForRecord(record));
|
|
12562
|
+
}
|
|
12563
|
+
async function remoteInviteFolder(record, parsed) {
|
|
12564
|
+
assertProjectScope(record, "invite-folder");
|
|
12565
|
+
const folderId = parsed.command[2];
|
|
12566
|
+
if (!folderId) {
|
|
12567
|
+
throw new CliError("usage_error", "Missing folder id.", {
|
|
12568
|
+
hint: "Run mdocs remote invite-folder <folderId> --email person@example.com --role edit --json."
|
|
12569
|
+
});
|
|
12570
|
+
}
|
|
12571
|
+
const email = requiredFlag(parsed.flags, "email");
|
|
12572
|
+
const role = typeof parsed.flags.role === "string" ? parsed.flags.role : "edit";
|
|
12573
|
+
if (!["read", "suggest", "edit", "admin"].includes(role)) {
|
|
12574
|
+
throw new CliError("usage_error", "Invalid --role.", {
|
|
12575
|
+
hint: "Use --role read, suggest, edit, or admin."
|
|
12576
|
+
});
|
|
12577
|
+
}
|
|
12578
|
+
await refreshPresence(record, record.docId);
|
|
12579
|
+
return postFolderInvite(record, folderId, { email, role }, actorForRecord(record));
|
|
12580
|
+
}
|
|
11916
12581
|
async function remoteReject(root, record, parsed) {
|
|
11917
12582
|
const suggestionId = parsed.command[2];
|
|
11918
12583
|
if (!suggestionId) {
|
|
@@ -12078,6 +12743,11 @@ function parseShareUrl(value, flags) {
|
|
|
12078
12743
|
lastSeenEventId: 0
|
|
12079
12744
|
};
|
|
12080
12745
|
}
|
|
12746
|
+
function shareUrlForScope(value, record) {
|
|
12747
|
+
const url = new URL(value);
|
|
12748
|
+
url.pathname = buildSharePath(record.workspaceId, record.rootId, record.scope === "file" ? record.docId : void 0);
|
|
12749
|
+
return url.toString();
|
|
12750
|
+
}
|
|
12081
12751
|
function joinSummary(record, document) {
|
|
12082
12752
|
return {
|
|
12083
12753
|
connected: true,
|
|
@@ -12099,6 +12769,52 @@ function joinSummary(record, document) {
|
|
|
12099
12769
|
].filter(Boolean)
|
|
12100
12770
|
};
|
|
12101
12771
|
}
|
|
12772
|
+
function assertProjectScope(record, command) {
|
|
12773
|
+
if (record.scope === "project") return;
|
|
12774
|
+
throw new CliError("usage_error", `remote ${command} requires a project-scoped join.`, {
|
|
12775
|
+
hint: "Rejoin the share with `mdocs join <share-url> --scope project --json`, then retry."
|
|
12776
|
+
});
|
|
12777
|
+
}
|
|
12778
|
+
function emptySidecar(docId, path, title) {
|
|
12779
|
+
return {
|
|
12780
|
+
schemaVersion: SIDECAR_SCHEMA_VERSION,
|
|
12781
|
+
docId,
|
|
12782
|
+
path,
|
|
12783
|
+
title,
|
|
12784
|
+
anchors: [],
|
|
12785
|
+
comments: [],
|
|
12786
|
+
suggestions: [],
|
|
12787
|
+
changeSets: [],
|
|
12788
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12789
|
+
};
|
|
12790
|
+
}
|
|
12791
|
+
function defaultMarkdown(path) {
|
|
12792
|
+
return `# ${titleFromMarkdown(path, "")}
|
|
12793
|
+
`;
|
|
12794
|
+
}
|
|
12795
|
+
function normalizeRemoteWorkspacePath(path, rules, sizeBytes) {
|
|
12796
|
+
try {
|
|
12797
|
+
return assertWorkspacePathAllowed(path, rules, sizeBytes);
|
|
12798
|
+
} catch (error) {
|
|
12799
|
+
throw new CliError("validation_error", error instanceof Error ? error.message : String(error));
|
|
12800
|
+
}
|
|
12801
|
+
}
|
|
12802
|
+
function folderNameFromArgs(parsed) {
|
|
12803
|
+
const name = parsed.command[2] ?? (typeof parsed.flags.name === "string" ? parsed.flags.name : void 0);
|
|
12804
|
+
if (!name?.trim()) {
|
|
12805
|
+
throw new CliError("usage_error", "Missing folder name.", {
|
|
12806
|
+
hint: "Run mdocs remote create-folder <name> --json."
|
|
12807
|
+
});
|
|
12808
|
+
}
|
|
12809
|
+
return name;
|
|
12810
|
+
}
|
|
12811
|
+
function optionalFolderTarget(value) {
|
|
12812
|
+
if (value === void 0) return void 0;
|
|
12813
|
+
if (typeof value !== "string") return void 0;
|
|
12814
|
+
const trimmed = value.trim();
|
|
12815
|
+
if (!trimmed || trimmed === "home" || trimmed === "null" || trimmed === "top") return null;
|
|
12816
|
+
return trimmed;
|
|
12817
|
+
}
|
|
12102
12818
|
|
|
12103
12819
|
// src/bridge.ts
|
|
12104
12820
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -12529,6 +13245,10 @@ async function main() {
|
|
|
12529
13245
|
print(await getWorkspaceMap(io), parsed.flags);
|
|
12530
13246
|
return;
|
|
12531
13247
|
}
|
|
13248
|
+
case "graph": {
|
|
13249
|
+
print(await getWorkspaceGraph(io), parsed.flags);
|
|
13250
|
+
return;
|
|
13251
|
+
}
|
|
12532
13252
|
case "state": {
|
|
12533
13253
|
print(await getDocumentState(io, requiredArg(parsed.command[1], "path")), parsed.flags);
|
|
12534
13254
|
return;
|
|
@@ -12559,6 +13279,7 @@ async function main() {
|
|
|
12559
13279
|
...image2,
|
|
12560
13280
|
...image2.workspacePath ? { absolutePath: resolve5(cwd, image2.workspacePath) } : {}
|
|
12561
13281
|
})),
|
|
13282
|
+
links: state.links,
|
|
12562
13283
|
comments: state.sidecar.comments.filter((comment2) => comment2.status === "open"),
|
|
12563
13284
|
suggestions: state.sidecar.suggestions.filter((suggestion) => suggestion.status === "open"),
|
|
12564
13285
|
anchors: state.anchors
|
|
@@ -12822,6 +13543,7 @@ Commands:
|
|
|
12822
13543
|
agent commands --json Print machine-readable command metadata
|
|
12823
13544
|
init [path] Initialize .mdocs and index Markdown files
|
|
12824
13545
|
map --json Print workspace map
|
|
13546
|
+
graph --json Print workspace knowledge graph nodes and edges
|
|
12825
13547
|
state <path> --json Print merged Markdown and sidecar state
|
|
12826
13548
|
context <path> --json Print agent-ready context
|
|
12827
13549
|
comments <path> --json List comments
|
|
@@ -12841,9 +13563,13 @@ Commands:
|
|
|
12841
13563
|
checkpoint create|list|restore Manage local reversible checkpoints
|
|
12842
13564
|
join <share-url> --json Join a Magic Markdown share through the CLI
|
|
12843
13565
|
joins --json List saved Magic Markdown remote joins
|
|
12844
|
-
remote map|context|
|
|
13566
|
+
remote map|graph|context|create-file|move-file
|
|
13567
|
+
Work with documents in the active remote join
|
|
13568
|
+
remote comment|suggest Add remote review comments and suggestions
|
|
12845
13569
|
remote reject <suggestionId> Withdraw a suggestion you submitted remotely
|
|
12846
13570
|
remote events|history|restore Poll events, list commits, restore snapshots
|
|
13571
|
+
remote library|create-folder|update-folder|move-root|invite-folder
|
|
13572
|
+
Organize the joined project library
|
|
12847
13573
|
bridge --workspace <id> --root . --url <base-url> --token <bridge-token>
|
|
12848
13574
|
Sync an approved local root with the workspace
|
|
12849
13575
|
(--token from the web "Bind agent filesystem"
|
package/package.json
CHANGED