@magic-markdown/cli 0.3.11 → 0.3.13
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 +532 -60
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -918,7 +918,8 @@ function selectorFromRange(markdown, range) {
|
|
|
918
918
|
function remapAnchor(markdown, anchor) {
|
|
919
919
|
const lines = getLines(markdown);
|
|
920
920
|
const quoteLines = anchor.selector.quote.split(/\r?\n/);
|
|
921
|
-
const
|
|
921
|
+
const candidates = findQuoteCandidates(lines, quoteLines);
|
|
922
|
+
const best = bestCandidate(lines, candidates, anchor.selector);
|
|
922
923
|
if (!best) {
|
|
923
924
|
return {
|
|
924
925
|
...anchor,
|
|
@@ -939,30 +940,44 @@ function remapAnchor(markdown, anchor) {
|
|
|
939
940
|
function remapAnchors(markdown, anchors) {
|
|
940
941
|
return anchors.map((anchor) => remapAnchor(markdown, anchor));
|
|
941
942
|
}
|
|
942
|
-
function
|
|
943
|
-
if (quoteLines.length === 0) return
|
|
943
|
+
function findQuoteCandidates(lines, quoteLines) {
|
|
944
|
+
if (quoteLines.length === 0) return [];
|
|
945
|
+
const ranges = [];
|
|
946
|
+
const quoteText = quoteLines.join("\n");
|
|
944
947
|
for (let index = 0; index <= lines.length - quoteLines.length; index += 1) {
|
|
945
948
|
const candidate = lines.slice(index, index + quoteLines.length);
|
|
946
|
-
if (candidate.join("\n") === quoteLines.
|
|
947
|
-
return { startLine: index + 1, endLine: index + quoteLines.length };
|
|
948
|
-
}
|
|
949
|
+
if (candidate.join("\n") === quoteText) ranges.push({ startLine: index + 1, endLine: index + quoteLines.length });
|
|
949
950
|
}
|
|
950
|
-
return
|
|
951
|
+
return ranges;
|
|
952
|
+
}
|
|
953
|
+
function bestCandidate(lines, candidates, selector) {
|
|
954
|
+
if (candidates.length <= 1) return candidates[0];
|
|
955
|
+
const scored = candidates.map((range) => ({ range, score: scoreContext(lines, range.startLine, range.endLine, selector) })).sort((left, right) => right.score - left.score);
|
|
956
|
+
const best = scored[0];
|
|
957
|
+
if (!best) return void 0;
|
|
958
|
+
const tied = scored.filter((candidate) => candidate.score === best.score);
|
|
959
|
+
return tied.length === 1 ? best.range : void 0;
|
|
951
960
|
}
|
|
952
961
|
function scoreContext(lines, startLine, endLine, selector) {
|
|
953
962
|
let score = 0.7;
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
const actualPrefix = lines.slice(Math.max(0, startLine - 4), startLine - 1).join("\n");
|
|
963
|
+
const expectedPrefix = contextPrefix(selector.prefix);
|
|
964
|
+
if (expectedPrefix) {
|
|
965
|
+
const actualPrefix = contextPrefix(lines.slice(Math.max(0, startLine - 4), startLine - 1).join("\n"));
|
|
957
966
|
if (expectedPrefix && actualPrefix.endsWith(expectedPrefix)) score += 0.15;
|
|
958
967
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
const actualSuffix = lines.slice(endLine, endLine + 3).join("\n");
|
|
968
|
+
const expectedSuffix = contextSuffix(selector.suffix);
|
|
969
|
+
if (expectedSuffix) {
|
|
970
|
+
const actualSuffix = contextSuffix(lines.slice(endLine, endLine + 3).join("\n"));
|
|
962
971
|
if (expectedSuffix && actualSuffix.startsWith(expectedSuffix)) score += 0.15;
|
|
963
972
|
}
|
|
964
973
|
return Math.min(1, score);
|
|
965
974
|
}
|
|
975
|
+
function contextPrefix(value) {
|
|
976
|
+
return value?.split(/\r?\n/).filter(Boolean).slice(-3).join("\n") ?? "";
|
|
977
|
+
}
|
|
978
|
+
function contextSuffix(value) {
|
|
979
|
+
return value?.split(/\r?\n/).filter(Boolean).slice(0, 3).join("\n") ?? "";
|
|
980
|
+
}
|
|
966
981
|
|
|
967
982
|
// ../core/src/text-merge.ts
|
|
968
983
|
var MAX_DIFF_CELLS = 25e6;
|
|
@@ -1163,6 +1178,9 @@ function regionTouchesSuggestionRange(region, baseStart, baseEnd) {
|
|
|
1163
1178
|
return region.baseStart < baseEnd && regionEnd > baseStart;
|
|
1164
1179
|
}
|
|
1165
1180
|
function currentSuggestionRange(markdown, suggestion, anchor) {
|
|
1181
|
+
const anchoredRange = currentAnchorRange(markdown, suggestion, anchor);
|
|
1182
|
+
if (anchoredRange) return anchoredRange;
|
|
1183
|
+
if (anchor && !anchorIsReliable(anchor)) return void 0;
|
|
1166
1184
|
const hintedRange = suggestion.patch.range;
|
|
1167
1185
|
if (rangeIsWithin(markdown, hintedRange) && extractLineRange(markdown, hintedRange) === suggestion.patch.before) return hintedRange;
|
|
1168
1186
|
const candidates = findBeforeCandidates(markdown, suggestion.patch.before);
|
|
@@ -1177,6 +1195,14 @@ function currentSuggestionRange(markdown, suggestion, anchor) {
|
|
|
1177
1195
|
if (best.score < 0.7) return void 0;
|
|
1178
1196
|
return best.range;
|
|
1179
1197
|
}
|
|
1198
|
+
function currentAnchorRange(markdown, suggestion, anchor) {
|
|
1199
|
+
if (!anchor || !anchorIsReliable(anchor) || !anchor.range) return void 0;
|
|
1200
|
+
if (!rangeIsWithin(markdown, anchor.range)) return void 0;
|
|
1201
|
+
return extractLineRange(markdown, anchor.range) === suggestion.patch.before ? anchor.range : void 0;
|
|
1202
|
+
}
|
|
1203
|
+
function anchorIsReliable(anchor) {
|
|
1204
|
+
return anchor.status === "mapped" && anchor.confidence >= 0.65;
|
|
1205
|
+
}
|
|
1180
1206
|
function findBeforeCandidates(markdown, before) {
|
|
1181
1207
|
const lines = getLines(markdown);
|
|
1182
1208
|
const beforeLines = linePattern(before);
|
|
@@ -1200,22 +1226,22 @@ function linePattern(value) {
|
|
|
1200
1226
|
function scoreContext2(markdown, range, anchor) {
|
|
1201
1227
|
const lines = getLines(markdown);
|
|
1202
1228
|
let score = anchor.selector.quote === extractLineRange(markdown, range) ? 0.7 : 0.65;
|
|
1203
|
-
const expectedPrefix =
|
|
1229
|
+
const expectedPrefix = contextPrefix2(anchor.selector.prefix);
|
|
1204
1230
|
if (expectedPrefix) {
|
|
1205
|
-
const actualPrefix =
|
|
1231
|
+
const actualPrefix = contextPrefix2(lines.slice(Math.max(0, range.startLine - 4), range.startLine - 1).join("\n"));
|
|
1206
1232
|
if (actualPrefix?.endsWith(expectedPrefix)) score += 0.15;
|
|
1207
1233
|
}
|
|
1208
|
-
const expectedSuffix =
|
|
1234
|
+
const expectedSuffix = contextSuffix2(anchor.selector.suffix);
|
|
1209
1235
|
if (expectedSuffix) {
|
|
1210
|
-
const actualSuffix =
|
|
1236
|
+
const actualSuffix = contextSuffix2(lines.slice(range.endLine, range.endLine + 3).join("\n"));
|
|
1211
1237
|
if (actualSuffix?.startsWith(expectedSuffix)) score += 0.15;
|
|
1212
1238
|
}
|
|
1213
1239
|
return Math.min(1, score);
|
|
1214
1240
|
}
|
|
1215
|
-
function
|
|
1241
|
+
function contextPrefix2(value) {
|
|
1216
1242
|
return value?.split(/\r?\n/).filter(Boolean).slice(-3).join("\n") || void 0;
|
|
1217
1243
|
}
|
|
1218
|
-
function
|
|
1244
|
+
function contextSuffix2(value) {
|
|
1219
1245
|
return value?.split(/\r?\n/).filter(Boolean).slice(0, 3).join("\n") || void 0;
|
|
1220
1246
|
}
|
|
1221
1247
|
|
|
@@ -4606,44 +4632,46 @@ function gatherMarks(schema2, marks) {
|
|
|
4606
4632
|
}
|
|
4607
4633
|
|
|
4608
4634
|
// ../core/src/pm-schema.ts
|
|
4635
|
+
var blockAttrs = { blockId: { default: null } };
|
|
4609
4636
|
var canonicalSchema = new Schema({
|
|
4610
4637
|
nodes: {
|
|
4611
4638
|
// The doc allows suggestion marks on its block children so pending
|
|
4612
4639
|
// block-level suggestions (node marks) survive fromJSON and can be
|
|
4613
4640
|
// structurally reverted before serialization.
|
|
4614
4641
|
doc: { content: "block+", marks: "insertion deletion modification" },
|
|
4615
|
-
paragraph: { group: "block", content: "inline*" },
|
|
4642
|
+
paragraph: { group: "block", content: "inline*", attrs: blockAttrs },
|
|
4616
4643
|
heading: {
|
|
4617
4644
|
group: "block",
|
|
4618
4645
|
content: "inline*",
|
|
4619
|
-
attrs: { level: { default: 1 } }
|
|
4646
|
+
attrs: { ...blockAttrs, level: { default: 1 } }
|
|
4620
4647
|
},
|
|
4621
|
-
blockquote: { group: "block", content: "block+" },
|
|
4648
|
+
blockquote: { group: "block", content: "block+", attrs: blockAttrs },
|
|
4622
4649
|
codeBlock: {
|
|
4623
4650
|
group: "block",
|
|
4624
4651
|
content: "text*",
|
|
4625
4652
|
marks: "",
|
|
4626
4653
|
code: true,
|
|
4627
|
-
attrs: { language: { default: null } }
|
|
4654
|
+
attrs: { ...blockAttrs, language: { default: null } }
|
|
4628
4655
|
},
|
|
4629
|
-
horizontalRule: { group: "block" },
|
|
4630
|
-
bulletList: { group: "block", content: "listItem+" },
|
|
4656
|
+
horizontalRule: { group: "block", attrs: blockAttrs },
|
|
4657
|
+
bulletList: { group: "block", content: "listItem+", attrs: blockAttrs },
|
|
4631
4658
|
orderedList: {
|
|
4632
4659
|
group: "block",
|
|
4633
4660
|
content: "listItem+",
|
|
4634
|
-
attrs: { start: { default: 1 } }
|
|
4661
|
+
attrs: { ...blockAttrs, start: { default: 1 } }
|
|
4635
4662
|
},
|
|
4636
|
-
listItem: { content: "paragraph block*" },
|
|
4637
|
-
taskList: { group: "block", content: "taskItem+" },
|
|
4663
|
+
listItem: { content: "paragraph block*", attrs: blockAttrs },
|
|
4664
|
+
taskList: { group: "block", content: "taskItem+", attrs: blockAttrs },
|
|
4638
4665
|
taskItem: {
|
|
4639
4666
|
content: "paragraph block*",
|
|
4640
|
-
attrs: { checked: { default: false } }
|
|
4667
|
+
attrs: { ...blockAttrs, checked: { default: false } }
|
|
4641
4668
|
},
|
|
4642
|
-
table: { group: "block", content: "tableRow+" },
|
|
4643
|
-
tableRow: { content: "(tableCell | tableHeader)+" },
|
|
4669
|
+
table: { group: "block", content: "tableRow+", attrs: blockAttrs },
|
|
4670
|
+
tableRow: { content: "(tableCell | tableHeader)+", attrs: blockAttrs },
|
|
4644
4671
|
tableHeader: {
|
|
4645
4672
|
content: "block+",
|
|
4646
4673
|
attrs: {
|
|
4674
|
+
...blockAttrs,
|
|
4647
4675
|
colspan: { default: 1 },
|
|
4648
4676
|
rowspan: { default: 1 },
|
|
4649
4677
|
colwidth: { default: null }
|
|
@@ -4652,6 +4680,7 @@ var canonicalSchema = new Schema({
|
|
|
4652
4680
|
tableCell: {
|
|
4653
4681
|
content: "block+",
|
|
4654
4682
|
attrs: {
|
|
4683
|
+
...blockAttrs,
|
|
4655
4684
|
colspan: { default: 1 },
|
|
4656
4685
|
rowspan: { default: 1 },
|
|
4657
4686
|
colwidth: { default: null }
|
|
@@ -10950,6 +10979,36 @@ function backticksFor2(node, side) {
|
|
|
10950
10979
|
return result;
|
|
10951
10980
|
}
|
|
10952
10981
|
|
|
10982
|
+
// ../core/src/pm-parse.ts
|
|
10983
|
+
var parser = new MarkdownParser(canonicalSchema, new lib_default(), {
|
|
10984
|
+
blockquote: { block: "blockquote" },
|
|
10985
|
+
paragraph: { block: "paragraph" },
|
|
10986
|
+
list_item: { block: "listItem" },
|
|
10987
|
+
bullet_list: { block: "bulletList" },
|
|
10988
|
+
ordered_list: {
|
|
10989
|
+
block: "orderedList",
|
|
10990
|
+
getAttrs: (tok) => ({ start: Number(tok.attrGet("start")) || 1 })
|
|
10991
|
+
},
|
|
10992
|
+
heading: { block: "heading", getAttrs: (tok) => ({ level: Number(tok.tag.slice(1)) || 1 }) },
|
|
10993
|
+
code_block: { block: "codeBlock", noCloseToken: true },
|
|
10994
|
+
fence: { block: "codeBlock", getAttrs: (tok) => ({ language: tok.info || null }), noCloseToken: true },
|
|
10995
|
+
hr: { node: "horizontalRule" },
|
|
10996
|
+
hardbreak: { node: "hardBreak" },
|
|
10997
|
+
em: { mark: "italic" },
|
|
10998
|
+
strong: { mark: "bold" },
|
|
10999
|
+
s: { mark: "strike" },
|
|
11000
|
+
link: { mark: "link", getAttrs: (tok) => ({ href: tok.attrGet("href") }) },
|
|
11001
|
+
code_inline: { mark: "code", noCloseToken: true },
|
|
11002
|
+
image: {
|
|
11003
|
+
node: "image",
|
|
11004
|
+
getAttrs: (tok) => ({
|
|
11005
|
+
alt: tok.content,
|
|
11006
|
+
src: tok.attrGet("src"),
|
|
11007
|
+
title: tok.attrGet("title")
|
|
11008
|
+
})
|
|
11009
|
+
}
|
|
11010
|
+
});
|
|
11011
|
+
|
|
10953
11012
|
// ../core/src/remote-document-io.ts
|
|
10954
11013
|
var RemoteDocumentIO = class {
|
|
10955
11014
|
constructor(document) {
|
|
@@ -10997,7 +11056,7 @@ var RemoteDocumentIO = class {
|
|
|
10997
11056
|
};
|
|
10998
11057
|
|
|
10999
11058
|
// src/agent.ts
|
|
11000
|
-
var CLI_VERSION = "0.3.
|
|
11059
|
+
var CLI_VERSION = "0.3.13";
|
|
11001
11060
|
var CLI_PACKAGE_NAME = "@magic-markdown/cli";
|
|
11002
11061
|
var AGENT_COMMANDS = [
|
|
11003
11062
|
{
|
|
@@ -11058,10 +11117,11 @@ var AGENT_COMMANDS = [
|
|
|
11058
11117
|
{
|
|
11059
11118
|
name: "remote",
|
|
11060
11119
|
summary: "Read, organize, comment on, suggest edits to, monitor, and restore the active Magic Markdown remote join.",
|
|
11061
|
-
usage: "mdocs remote map|graph|context|review|create-file|move-file|comment|suggest|reject|events|history|restore|library|create-folder|update-folder|move-root|invite-folder --json",
|
|
11120
|
+
usage: "mdocs remote status|map|graph|context|review|create-file|move-file|comment|suggest|reject|events|history|restore|library|create-folder|update-folder|move-root|invite-folder --json",
|
|
11062
11121
|
output: "json",
|
|
11063
11122
|
mutates: true,
|
|
11064
11123
|
examples: [
|
|
11124
|
+
"mdocs remote status --expect-scope file --expect-root root_abc --expect-doc doc_abc --json",
|
|
11065
11125
|
"mdocs remote context --summary --json",
|
|
11066
11126
|
"mdocs remote context --start-line 1 --end-line 100 --no-review --json",
|
|
11067
11127
|
"mdocs remote context --start-line 101 --end-line 200 --no-review --json",
|
|
@@ -11081,6 +11141,7 @@ var AGENT_COMMANDS = [
|
|
|
11081
11141
|
"mdocs remote invite-folder fold_abc123 --email person@example.com --role edit --json"
|
|
11082
11142
|
],
|
|
11083
11143
|
notes: [
|
|
11144
|
+
"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.",
|
|
11084
11145
|
"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.",
|
|
11085
11146
|
"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>.",
|
|
11086
11147
|
"Use remote context --no-review when reading document content only; use remote review to list open comments, open suggestions, and anchors separately.",
|
|
@@ -11279,6 +11340,23 @@ var AGENT_COMMANDS = [
|
|
|
11279
11340
|
"--token (or MDOCS_BRIDGE_TOKEN) still works when a scoped bridge token is already available."
|
|
11280
11341
|
]
|
|
11281
11342
|
},
|
|
11343
|
+
{
|
|
11344
|
+
name: "bridge resume",
|
|
11345
|
+
summary: "Backfill missed Magic changes after an agent restart, then keep the bridge running.",
|
|
11346
|
+
usage: "mdocs bridge resume --root <path> [--url <base-url>] [--workspace <id>] [--root-id <id>] [--request-token] [--once]",
|
|
11347
|
+
output: "long-running",
|
|
11348
|
+
mutates: true,
|
|
11349
|
+
examples: [
|
|
11350
|
+
"mdocs bridge resume --root . --request-token",
|
|
11351
|
+
"mdocs bridge resume --root . --once --request-token"
|
|
11352
|
+
],
|
|
11353
|
+
notes: [
|
|
11354
|
+
"Reads non-secret connection defaults from .mdocs/bridge.json written by bridge setup.",
|
|
11355
|
+
"Use this after a sandbox/container/agent session restarts. It backfills canonical Magic changes before publishing local edits.",
|
|
11356
|
+
"--request-token opens a human approval URL when no MDOCS_BRIDGE_TOKEN is available; do not ask users to paste bridge tokens by default.",
|
|
11357
|
+
"--once performs only the backfill/reconcile step and exits."
|
|
11358
|
+
]
|
|
11359
|
+
},
|
|
11282
11360
|
{
|
|
11283
11361
|
name: "bridge",
|
|
11284
11362
|
summary: "Sync a local Markdown root with a Magic workspace over WebSocket (long-running).",
|
|
@@ -11341,11 +11419,38 @@ Run \`mdocs help <command>\` (or \`mdocs <command> --help\`) for usage, examples
|
|
|
11341
11419
|
## Start Here
|
|
11342
11420
|
|
|
11343
11421
|
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.
|
|
11344
|
-
2. If
|
|
11345
|
-
3.
|
|
11346
|
-
4. Run \`mdocs
|
|
11347
|
-
5.
|
|
11348
|
-
6.
|
|
11422
|
+
2. If the handoff includes expected binding values, run \`mdocs remote status --expect-scope <file|project> --expect-root <rootId> --expect-doc <docId> --json\` after joining. If it reports \`wrong_binding\`, stop and ask for the correct share or connector.
|
|
11423
|
+
3. If working in a local workspace, run \`mdocs doctor --json\` to validate the workspace and learn recommended next commands.
|
|
11424
|
+
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>\`.
|
|
11425
|
+
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.
|
|
11426
|
+
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.
|
|
11427
|
+
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 insert comments, CriticMarkup, directives, or Magic markers into Markdown files.
|
|
11428
|
+
|
|
11429
|
+
## Filesystem Bridge / Resume
|
|
11430
|
+
|
|
11431
|
+
When a human asks you to bind an editable local Markdown filesystem to Magic Markdown, use the latest official package and the human-approval token flow:
|
|
11432
|
+
|
|
11433
|
+
\`\`\`bash
|
|
11434
|
+
npx --yes --package=@magic-markdown/cli@latest mdocs bridge setup \\
|
|
11435
|
+
--root . \\
|
|
11436
|
+
--workspace <workspace-id> \\
|
|
11437
|
+
--root-id <root-id> \\
|
|
11438
|
+
--url <magic-url> \\
|
|
11439
|
+
--actor-name "<agent name>" \\
|
|
11440
|
+
--request-token
|
|
11441
|
+
\`\`\`
|
|
11442
|
+
|
|
11443
|
+
Return the Magic approval link and keep the bridge process running after approval. Success looks like \`mdocs bridge connected ...\`.
|
|
11444
|
+
|
|
11445
|
+
If your sandbox, container, terminal, or agent session restarts after setup, do not ask for a raw bridge token. Start the latest CLI again and resume from the same root:
|
|
11446
|
+
|
|
11447
|
+
\`\`\`bash
|
|
11448
|
+
npx --yes --package=@magic-markdown/cli@latest mdocs bridge resume --root . --request-token
|
|
11449
|
+
\`\`\`
|
|
11450
|
+
|
|
11451
|
+
\`bridge resume\` reads non-secret defaults from \`.mdocs/bridge.json\`, backfills missed Magic changes before publishing local edits, and then keeps polling/watching. Use \`--once\` only when the user asked for a one-shot backfill rather than a live bridge.
|
|
11452
|
+
|
|
11453
|
+
If Magic and the local root both changed the same document while you were offline, the bridge keeps the Magic canonical path, saves your local divergent version as a \`.conflict-...\` Markdown copy, and lets the conflict copy sync as a normal file. If Magic reports a server-side conflict, wait for the human to resolve it in Magic Markdown before retrying content pushes.
|
|
11349
11454
|
|
|
11350
11455
|
## Editing Rules
|
|
11351
11456
|
|
|
@@ -11364,7 +11469,7 @@ Run \`mdocs help <command>\` (or \`mdocs <command> --help\`) for usage, examples
|
|
|
11364
11469
|
## Errors, Conflicts, and Exit Codes
|
|
11365
11470
|
|
|
11366
11471
|
- Errors go to stderr. With \`--json\` they are structured: \`{ "ok": false, "error": { "code", "message", "hint" }, "cliVersion" }\`. Follow the \`hint\`.
|
|
11367
|
-
- Exit codes: 0 ok, 1 internal, 2 usage/invalid range, 3 conflict, 4 not found, 5 network, 6 unauthorized (share link revoked or expired).
|
|
11472
|
+
- Exit codes: 0 ok, 1 internal, 2 usage/invalid range, 3 conflict, 4 not found or wrong binding, 5 network, 6 unauthorized (share link revoked or expired).
|
|
11368
11473
|
- \`remote comment\` and \`remote suggest\` are concurrency-safe: the server merges review additions without clobbering concurrent edits, and the CLI rebases and retries automatically. A surviving \`conflict\` error means the document is changing rapidly \u2014 refetch context and retry.
|
|
11369
11474
|
- \`remote events --json\` may return \`"truncated": true\` when older events were dropped from the bounded event log. Do not assume nothing happened; refetch with \`mdocs remote context --summary --json\`, then read needed pages and review state.
|
|
11370
11475
|
- Retries are safe: remote writes carry a changeId and the server deduplicates replays.
|
|
@@ -11388,7 +11493,7 @@ Start the local stdio MCP server with:
|
|
|
11388
11493
|
mdocs serve-mcp --cwd /path/to/workspace
|
|
11389
11494
|
\`\`\`
|
|
11390
11495
|
|
|
11391
|
-
The MCP server exposes document resources, image resources, workspace map and graph resources, an agent guide resource, typed tools for comments and suggestions, and the \`magic_markdown_agent_workflow\` prompt. Use \`mdocs_context\` / \`magic_context\` with \`summary: true\` before dumping Markdown; use \`includeReview: false\` for content-only reads and \`mdocs_review\` / \`magic_review\` for review state.
|
|
11496
|
+
The MCP server exposes document resources, image resources, workspace map and graph resources, an agent guide resource, typed tools for comments and suggestions, and the \`magic_markdown_agent_workflow\` prompt. Use \`magic_status\` first when expected binding values are available; stop if it returns \`wrong_binding\`. Then use \`mdocs_context\` / \`magic_context\` with \`summary: true\` before dumping Markdown; use \`includeReview: false\` for content-only reads and \`mdocs_review\` / \`magic_review\` for review state.
|
|
11392
11497
|
`;
|
|
11393
11498
|
}
|
|
11394
11499
|
function formatCommandReference() {
|
|
@@ -11416,6 +11521,7 @@ var CLI_EXIT_CODES = {
|
|
|
11416
11521
|
invalid_range: 2,
|
|
11417
11522
|
conflict: 3,
|
|
11418
11523
|
not_found: 4,
|
|
11524
|
+
wrong_binding: 4,
|
|
11419
11525
|
network_error: 5,
|
|
11420
11526
|
unauthorized: 6
|
|
11421
11527
|
};
|
|
@@ -12468,6 +12574,16 @@ async function postReview(record, docId, additions, actor) {
|
|
|
12468
12574
|
);
|
|
12469
12575
|
return response.document;
|
|
12470
12576
|
}
|
|
12577
|
+
async function postReviewOperation(record, docId, operation, actor) {
|
|
12578
|
+
return fetchJson(
|
|
12579
|
+
`${record.origin}/api/workspaces/${encodeURIComponent(record.workspaceId)}/roots/${encodeURIComponent(record.rootId)}/documents/${encodeURIComponent(docId)}/review-ops`,
|
|
12580
|
+
{
|
|
12581
|
+
method: "POST",
|
|
12582
|
+
headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
|
|
12583
|
+
body: JSON.stringify({ actor, operation })
|
|
12584
|
+
}
|
|
12585
|
+
);
|
|
12586
|
+
}
|
|
12471
12587
|
async function pushDocument(record, baseDocument, markdown, sidecar) {
|
|
12472
12588
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12473
12589
|
const payload = {
|
|
@@ -12765,6 +12881,8 @@ async function runJoinsCommand(root) {
|
|
|
12765
12881
|
async function runRemoteCommand(root, subcommand, parsed) {
|
|
12766
12882
|
const record = await normalizedSelectedJoin(root, parsed.flags);
|
|
12767
12883
|
switch (subcommand) {
|
|
12884
|
+
case "status":
|
|
12885
|
+
return remoteStatus(record, parsed.flags);
|
|
12768
12886
|
case "map":
|
|
12769
12887
|
case void 0: {
|
|
12770
12888
|
if (record.scope === "file") {
|
|
@@ -12818,10 +12936,53 @@ async function runRemoteCommand(root, subcommand, parsed) {
|
|
|
12818
12936
|
return rejoin(root, record.joinId, parsed.flags);
|
|
12819
12937
|
default:
|
|
12820
12938
|
throw new CliError("usage_error", `Unknown remote subcommand: ${subcommand}`, {
|
|
12821
|
-
hint: "Use mdocs remote map|graph|context|review|create-file|move-file|comment|suggest|reject|events|history|restore|library|create-folder|update-folder|move-root|invite-folder|rejoin."
|
|
12939
|
+
hint: "Use mdocs remote status|map|graph|context|review|create-file|move-file|comment|suggest|reject|events|history|restore|library|create-folder|update-folder|move-root|invite-folder|rejoin."
|
|
12822
12940
|
});
|
|
12823
12941
|
}
|
|
12824
12942
|
}
|
|
12943
|
+
async function remoteStatus(record, flags) {
|
|
12944
|
+
const document = record.rootId && record.docId ? (await fetchState(record)).document : void 0;
|
|
12945
|
+
const expected = expectedBinding(flags);
|
|
12946
|
+
const actual = {
|
|
12947
|
+
joinId: record.joinId,
|
|
12948
|
+
scope: record.scope,
|
|
12949
|
+
workspaceId: record.workspaceId,
|
|
12950
|
+
rootId: record.rootId,
|
|
12951
|
+
docId: document?.docId ?? record.docId,
|
|
12952
|
+
path: document?.path,
|
|
12953
|
+
currentHead: document?.currentSha ?? record.currentHead,
|
|
12954
|
+
agent: { id: record.agentId, name: record.agentName },
|
|
12955
|
+
currentDocument: document ? {
|
|
12956
|
+
docId: document.docId,
|
|
12957
|
+
path: document.path,
|
|
12958
|
+
title: document.title,
|
|
12959
|
+
currentSha: document.currentSha
|
|
12960
|
+
} : void 0
|
|
12961
|
+
};
|
|
12962
|
+
const mismatches = bindingMismatches(expected, actual);
|
|
12963
|
+
if (mismatches.length > 0) {
|
|
12964
|
+
throw new CliError("wrong_binding", "This Magic binding is for a different document or scope.", {
|
|
12965
|
+
hint: "Use the connector URL or CLI join command from the handoff copy.",
|
|
12966
|
+
details: {
|
|
12967
|
+
expected,
|
|
12968
|
+
actual,
|
|
12969
|
+
verification: { matches: false, mismatches }
|
|
12970
|
+
}
|
|
12971
|
+
});
|
|
12972
|
+
}
|
|
12973
|
+
return {
|
|
12974
|
+
ok: true,
|
|
12975
|
+
connected: true,
|
|
12976
|
+
...actual,
|
|
12977
|
+
expected,
|
|
12978
|
+
verification: { matches: true, mismatches: [] },
|
|
12979
|
+
nextCommands: [
|
|
12980
|
+
record.scope === "workspace" ? "mdocs remote map --json" : void 0,
|
|
12981
|
+
record.scope === "project" ? "mdocs remote map --json" : void 0,
|
|
12982
|
+
record.scope === "workspace" ? void 0 : "mdocs remote context --summary --json"
|
|
12983
|
+
].filter(Boolean)
|
|
12984
|
+
};
|
|
12985
|
+
}
|
|
12825
12986
|
async function remoteGraph(record) {
|
|
12826
12987
|
assertProjectScope(record, "graph");
|
|
12827
12988
|
const rootRecord = assertRootScoped(record, "graph");
|
|
@@ -13065,13 +13226,24 @@ async function remoteSuggest(root, record, parsed) {
|
|
|
13065
13226
|
const replacement = await readRequiredTextFlag(parsed.flags, root, ["with", "replacement"]);
|
|
13066
13227
|
const message = await readOptionalTextFlag(parsed.flags, root, ["message"]) ?? "Suggested edit";
|
|
13067
13228
|
const range = parseRange(requiredFlag(parsed.flags, "range"));
|
|
13068
|
-
const
|
|
13069
|
-
|
|
13070
|
-
|
|
13071
|
-
|
|
13072
|
-
|
|
13229
|
+
const pathOrDocId = parsed.command[2] ?? record.docId;
|
|
13230
|
+
if (!pathOrDocId) {
|
|
13231
|
+
throw new CliError("usage_error", "Missing document path or docId.", {
|
|
13232
|
+
hint: "Pass a document path or docId, or join with --doc <docId>."
|
|
13233
|
+
});
|
|
13234
|
+
}
|
|
13235
|
+
const document = await fetchDocument(record, pathOrDocId);
|
|
13236
|
+
const documentRecord = rootScopedRecordFor(record, document.rootId);
|
|
13237
|
+
await refreshPresence(documentRecord, document.docId);
|
|
13238
|
+
const result = await postReviewOperation(
|
|
13239
|
+
documentRecord,
|
|
13240
|
+
document.docId,
|
|
13241
|
+
{ kind: "create_suggestion", payload: { ...range, replacement, message } },
|
|
13242
|
+
actorForRecord(record)
|
|
13073
13243
|
);
|
|
13074
|
-
|
|
13244
|
+
if (result.document) await recordHead(root, record, result.document);
|
|
13245
|
+
const placementStatus = result.reviewRecord?.placement?.status;
|
|
13246
|
+
return { suggestion: result.suggestion, reviewRecord: result.reviewRecord, placementStatus, projectionStatus: result.projectionStatus, document: result.document };
|
|
13075
13247
|
}
|
|
13076
13248
|
async function remoteCreateFolder(record, parsed) {
|
|
13077
13249
|
assertProjectScope(record, "create-folder");
|
|
@@ -13336,6 +13508,33 @@ function joinSummary(record, document) {
|
|
|
13336
13508
|
].filter(Boolean)
|
|
13337
13509
|
};
|
|
13338
13510
|
}
|
|
13511
|
+
function expectedBinding(flags) {
|
|
13512
|
+
const scope = stringFlag(flags, "expect-scope", "expected-scope");
|
|
13513
|
+
const rootId = stringFlag(flags, "expect-root", "expected-root");
|
|
13514
|
+
const docId = stringFlag(flags, "expect-doc", "expected-doc");
|
|
13515
|
+
const path = stringFlag(flags, "expect-path", "expected-path");
|
|
13516
|
+
return {
|
|
13517
|
+
...scope ? { scope } : {},
|
|
13518
|
+
...rootId ? { rootId } : {},
|
|
13519
|
+
...docId ? { docId } : {},
|
|
13520
|
+
...path ? { path } : {}
|
|
13521
|
+
};
|
|
13522
|
+
}
|
|
13523
|
+
function stringFlag(flags, primary, alias) {
|
|
13524
|
+
const value = flags[primary] ?? flags[alias];
|
|
13525
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
13526
|
+
}
|
|
13527
|
+
function bindingMismatches(expected, actual) {
|
|
13528
|
+
const checks = [
|
|
13529
|
+
["scope", expected.scope, actual.scope],
|
|
13530
|
+
["rootId", expected.rootId, actual.rootId],
|
|
13531
|
+
["docId", expected.docId, actual.docId],
|
|
13532
|
+
["path", expected.path, actual.path]
|
|
13533
|
+
];
|
|
13534
|
+
return checks.flatMap(
|
|
13535
|
+
([field, expectedValue, actualValue]) => expectedValue && expectedValue !== actualValue ? [{ field, expected: expectedValue, actual: actualValue }] : []
|
|
13536
|
+
);
|
|
13537
|
+
}
|
|
13339
13538
|
function assertProjectScope(record, command) {
|
|
13340
13539
|
if (record.scope === "project") return;
|
|
13341
13540
|
throw new CliError("usage_error", `remote ${command} requires a project-scoped join.`, {
|
|
@@ -13402,6 +13601,7 @@ function optionalFolderTarget(value) {
|
|
|
13402
13601
|
// src/bridge.ts
|
|
13403
13602
|
import { createHash as createHash2, randomUUID as randomUUID3 } from "node:crypto";
|
|
13404
13603
|
import { basename as basename2, resolve as resolve4 } from "node:path";
|
|
13604
|
+
var BRIDGE_CONFIG_PATH = ".mdocs/bridge.json";
|
|
13405
13605
|
function bridgeSetupIdentity(input) {
|
|
13406
13606
|
const actorName = input.actorName?.trim() || "Agent";
|
|
13407
13607
|
return {
|
|
@@ -13426,7 +13626,43 @@ async function runBridgeSetup(options) {
|
|
|
13426
13626
|
actorId: identity.actorId,
|
|
13427
13627
|
actorName: identity.actorName,
|
|
13428
13628
|
sourceName: identity.sourceName,
|
|
13429
|
-
requestToken: options.requestToken || !options.token
|
|
13629
|
+
requestToken: options.requestToken || !options.token,
|
|
13630
|
+
resume: true
|
|
13631
|
+
});
|
|
13632
|
+
}
|
|
13633
|
+
async function runBridgeResume(options) {
|
|
13634
|
+
const root = resolve4(options.root);
|
|
13635
|
+
const config2 = await readBridgeConfig(root);
|
|
13636
|
+
const identity = bridgeSetupIdentity({
|
|
13637
|
+
actorId: options.actorId ?? config2?.actorId,
|
|
13638
|
+
actorName: options.actorName ?? config2?.actorName,
|
|
13639
|
+
sourceName: options.sourceName ?? config2?.sourceName
|
|
13640
|
+
});
|
|
13641
|
+
const workspaceId = options.workspaceId ?? config2?.workspaceId;
|
|
13642
|
+
const rootId = options.rootId ?? options.sourceId ?? config2?.rootId;
|
|
13643
|
+
const baseUrl = options.baseUrl ?? config2?.baseUrl;
|
|
13644
|
+
if (!workspaceId || !rootId || !baseUrl) {
|
|
13645
|
+
throw new Error("Missing bridge resume configuration. Run mdocs bridge setup --workspace <id> --root <path> --root-id <id> --url <base-url> first.");
|
|
13646
|
+
}
|
|
13647
|
+
await runBridge({
|
|
13648
|
+
root,
|
|
13649
|
+
workspaceId,
|
|
13650
|
+
rootId,
|
|
13651
|
+
sourceId: options.sourceId,
|
|
13652
|
+
sourceName: identity.sourceName,
|
|
13653
|
+
folderId: options.folderId ?? config2?.folderId,
|
|
13654
|
+
canonicalPrefix: options.canonicalPrefix ?? config2?.canonicalPrefix,
|
|
13655
|
+
replicaPrefix: options.replicaPrefix ?? config2?.replicaPrefix,
|
|
13656
|
+
claimToken: options.claimToken,
|
|
13657
|
+
actorId: identity.actorId,
|
|
13658
|
+
actorName: identity.actorName,
|
|
13659
|
+
baseUrl,
|
|
13660
|
+
intervalMs: options.intervalMs ?? config2?.intervalMs ?? 1e3,
|
|
13661
|
+
token: options.token,
|
|
13662
|
+
requestToken: options.requestToken || !options.token,
|
|
13663
|
+
pairingTimeoutMs: options.pairingTimeoutMs,
|
|
13664
|
+
once: options.once,
|
|
13665
|
+
resume: true
|
|
13430
13666
|
});
|
|
13431
13667
|
}
|
|
13432
13668
|
async function runBridge(options) {
|
|
@@ -13459,8 +13695,24 @@ async function runBridge(options) {
|
|
|
13459
13695
|
const registeredRoot = token ? await fetchScopedRoot({ ...options, token }, rootId) : await registerRoot(options, root, rootId, mapping);
|
|
13460
13696
|
lastAppliedHead = lastAppliedHead ?? registeredRoot?.canonical.head;
|
|
13461
13697
|
await writeSourceState(lastAppliedHead);
|
|
13698
|
+
await writeBridgeConfig(root, {
|
|
13699
|
+
schemaVersion: 1,
|
|
13700
|
+
workspaceId: options.workspaceId,
|
|
13701
|
+
rootId,
|
|
13702
|
+
baseUrl: options.baseUrl,
|
|
13703
|
+
actorId: options.actorId,
|
|
13704
|
+
actorName: options.actorName,
|
|
13705
|
+
sourceName: options.sourceName,
|
|
13706
|
+
folderId: options.folderId,
|
|
13707
|
+
canonicalPrefix: options.canonicalPrefix,
|
|
13708
|
+
replicaPrefix: options.replicaPrefix,
|
|
13709
|
+
intervalMs: options.intervalMs,
|
|
13710
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13711
|
+
});
|
|
13712
|
+
const backfilled = await resumeFromCanonical();
|
|
13713
|
+
if (options.once) return;
|
|
13462
13714
|
connect();
|
|
13463
|
-
if (claimMode) await primeLocalSignatures();
|
|
13715
|
+
if (claimMode && !backfilled) await primeLocalSignatures();
|
|
13464
13716
|
else await publishSnapshot("initial");
|
|
13465
13717
|
const timer = setInterval(() => {
|
|
13466
13718
|
void publishSnapshot("poll").catch((error) => {
|
|
@@ -13536,6 +13788,87 @@ async function runBridge(options) {
|
|
|
13536
13788
|
}
|
|
13537
13789
|
}
|
|
13538
13790
|
}
|
|
13791
|
+
async function resumeFromCanonical() {
|
|
13792
|
+
if (!options.resume && !token) return false;
|
|
13793
|
+
const remote = await fetchCanonicalSnapshot({ ...options, token }, rootId).catch(() => void 0);
|
|
13794
|
+
if (!remote) return false;
|
|
13795
|
+
const baseDocs = lastAppliedHead ? await fetchCommitDocuments({ ...options, token }, rootId, lastAppliedHead).catch(() => /* @__PURE__ */ new Map()) : /* @__PURE__ */ new Map();
|
|
13796
|
+
const remoteDocIds = new Set(remote.docs.map((doc) => doc.docId));
|
|
13797
|
+
let applied = 0;
|
|
13798
|
+
let keptLocal = 0;
|
|
13799
|
+
let conflicted = 0;
|
|
13800
|
+
for (const doc of remote.docs) {
|
|
13801
|
+
const base = baseDocs.get(doc.docId);
|
|
13802
|
+
const decision = await reconcileRemoteDocument(doc, base);
|
|
13803
|
+
if (decision === "applied") applied += 1;
|
|
13804
|
+
if (decision === "kept_local") keptLocal += 1;
|
|
13805
|
+
if (decision === "conflict") conflicted += 1;
|
|
13806
|
+
}
|
|
13807
|
+
for (const [docId, base] of baseDocs) {
|
|
13808
|
+
if (remoteDocIds.has(docId)) continue;
|
|
13809
|
+
const decision = await reconcileRemoteDelete(docId, base);
|
|
13810
|
+
if (decision === "applied") applied += 1;
|
|
13811
|
+
if (decision === "conflict") conflicted += 1;
|
|
13812
|
+
}
|
|
13813
|
+
lastAppliedHead = remote.head ?? lastAppliedHead;
|
|
13814
|
+
await writeSourceState(lastAppliedHead);
|
|
13815
|
+
process.stdout.write(
|
|
13816
|
+
`mdocs bridge resume ${root}: applied ${applied}, kept local ${keptLocal}, conflicts ${conflicted}, head ${lastAppliedHead ?? "unknown"}
|
|
13817
|
+
`
|
|
13818
|
+
);
|
|
13819
|
+
return true;
|
|
13820
|
+
}
|
|
13821
|
+
async function reconcileRemoteDocument(remote, base) {
|
|
13822
|
+
const remoteSignature = documentSignature(remote.markdown, remote.sidecar);
|
|
13823
|
+
const baseSignature = base ? documentSignature(base.markdown, base.sidecar) : void 0;
|
|
13824
|
+
const localState = await localStateForCanonical(remote);
|
|
13825
|
+
const localSignature = localState ? documentSignature(localState.markdown, localState.sidecar) : void 0;
|
|
13826
|
+
if (!localState) {
|
|
13827
|
+
await applyCanonicalDocument(remote);
|
|
13828
|
+
return "applied";
|
|
13829
|
+
}
|
|
13830
|
+
if (localSignature === remoteSignature && localState.path === remote.path && localState.docId === remote.docId) {
|
|
13831
|
+
signatures.set(remote.docId, remoteSignature);
|
|
13832
|
+
seenDocs.add(remote.docId);
|
|
13833
|
+
return "noop";
|
|
13834
|
+
}
|
|
13835
|
+
if (localSignature === remoteSignature && localState.path === remote.path) {
|
|
13836
|
+
await applyCanonicalDocument(remote);
|
|
13837
|
+
return "applied";
|
|
13838
|
+
}
|
|
13839
|
+
if (baseSignature && localSignature === baseSignature) {
|
|
13840
|
+
await applyCanonicalDocument(remote);
|
|
13841
|
+
return "applied";
|
|
13842
|
+
}
|
|
13843
|
+
if (baseSignature && remoteSignature === baseSignature) {
|
|
13844
|
+
signatures.set(remote.docId, baseSignature);
|
|
13845
|
+
seenDocs.add(remote.docId);
|
|
13846
|
+
return "kept_local";
|
|
13847
|
+
}
|
|
13848
|
+
signatures.set(remote.docId, baseSignature ?? remoteSignature);
|
|
13849
|
+
await applyCanonicalDocument(remote);
|
|
13850
|
+
return "conflict";
|
|
13851
|
+
}
|
|
13852
|
+
async function reconcileRemoteDelete(docId, base) {
|
|
13853
|
+
const localState = await getDocumentState(io, docId).catch(() => void 0);
|
|
13854
|
+
if (!localState) return "noop";
|
|
13855
|
+
const baseSignature = documentSignature(base.markdown, base.sidecar);
|
|
13856
|
+
const localSignature = documentSignature(localState.markdown, localState.sidecar);
|
|
13857
|
+
if (localSignature !== baseSignature) {
|
|
13858
|
+
const copyPath = conflictCopyPath(localState.path);
|
|
13859
|
+
await io.writeTextAtomic(copyPath, localState.markdown);
|
|
13860
|
+
await seedConflictCopySidecar(copyPath, localState);
|
|
13861
|
+
process.stderr.write(`local conflict ${localState.path}; local version saved as ${copyPath}, canonical delete applied
|
|
13862
|
+
`);
|
|
13863
|
+
}
|
|
13864
|
+
await io.deleteFile(localState.path).catch(() => void 0);
|
|
13865
|
+
await io.deleteFile(sidecarPath(docId)).catch(() => void 0);
|
|
13866
|
+
await removeManifestDocument(docId, localState.path);
|
|
13867
|
+
signatures.delete(docId);
|
|
13868
|
+
seenDocs.delete(docId);
|
|
13869
|
+
pendingDeletes.delete(docId);
|
|
13870
|
+
return localSignature === baseSignature ? "applied" : "conflict";
|
|
13871
|
+
}
|
|
13539
13872
|
async function handleIncoming(message) {
|
|
13540
13873
|
if (message.type === "file-changed") {
|
|
13541
13874
|
await applyCanonicalDocument(readDocumentPayload(message.payload));
|
|
@@ -13561,7 +13894,7 @@ async function runBridge(options) {
|
|
|
13561
13894
|
`);
|
|
13562
13895
|
if (!payload.docId) return;
|
|
13563
13896
|
pendingDocs.delete(payload.docId);
|
|
13564
|
-
const canonical = await
|
|
13897
|
+
const canonical = await fetchCanonicalDocument2(payload.docId);
|
|
13565
13898
|
if (canonical) await applyCanonicalDocument(canonical);
|
|
13566
13899
|
}
|
|
13567
13900
|
if (message.type === "sync-error") {
|
|
@@ -13576,7 +13909,7 @@ async function runBridge(options) {
|
|
|
13576
13909
|
}
|
|
13577
13910
|
async function applyCanonicalDocument(payload) {
|
|
13578
13911
|
const remoteSignature = documentSignature(payload.markdown, payload.sidecar);
|
|
13579
|
-
const localState = await
|
|
13912
|
+
const localState = await localStateForCanonical(payload);
|
|
13580
13913
|
const localSignature = localState ? documentSignature(localState.markdown, localState.sidecar) : void 0;
|
|
13581
13914
|
if (localSignature && signatures.has(payload.docId) && localSignature !== signatures.get(payload.docId) && localSignature !== remoteSignature) {
|
|
13582
13915
|
const copyPath = conflictCopyPath(payload.path);
|
|
@@ -13598,9 +13931,14 @@ async function runBridge(options) {
|
|
|
13598
13931
|
if (localState && localState.path !== payload.path) {
|
|
13599
13932
|
await io.deleteFile(localState.path).catch(() => void 0);
|
|
13600
13933
|
}
|
|
13934
|
+
if (localState && localState.docId !== payload.docId) {
|
|
13935
|
+
await io.deleteFile(sidecarPath(localState.docId)).catch(() => void 0);
|
|
13936
|
+
await removeManifestDocument(localState.docId, localState.path);
|
|
13937
|
+
}
|
|
13601
13938
|
await io.writeTextAtomic(payload.path, payload.markdown);
|
|
13602
13939
|
await io.writeTextAtomic(sidecarPath(payload.docId), `${JSON.stringify(payload.sidecar, null, 2)}
|
|
13603
13940
|
`);
|
|
13941
|
+
await upsertManifestDocument(payload);
|
|
13604
13942
|
signatures.set(payload.docId, remoteSignature);
|
|
13605
13943
|
deniedSignatures.delete(payload.docId);
|
|
13606
13944
|
seenDocs.add(payload.docId);
|
|
@@ -13636,7 +13974,14 @@ async function runBridge(options) {
|
|
|
13636
13974
|
updatedAt: now
|
|
13637
13975
|
});
|
|
13638
13976
|
}
|
|
13639
|
-
async function
|
|
13977
|
+
async function localStateForCanonical(payload) {
|
|
13978
|
+
const byDocId = await getDocumentState(io, payload.docId).catch(() => void 0);
|
|
13979
|
+
if (byDocId) return byDocId;
|
|
13980
|
+
const map = await getWorkspaceMap(io).catch(() => void 0);
|
|
13981
|
+
const byPath = map?.docs.find((doc) => doc.path === payload.path);
|
|
13982
|
+
return byPath ? getDocumentState(io, byPath.docId).catch(() => void 0) : void 0;
|
|
13983
|
+
}
|
|
13984
|
+
async function fetchCanonicalDocument2(docId) {
|
|
13640
13985
|
try {
|
|
13641
13986
|
const response = await fetch(
|
|
13642
13987
|
new URL(
|
|
@@ -13701,6 +14046,34 @@ async function runBridge(options) {
|
|
|
13701
14046
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
13702
14047
|
});
|
|
13703
14048
|
}
|
|
14049
|
+
async function upsertManifestDocument(payload) {
|
|
14050
|
+
const manifest = await readManifest(io).catch(() => void 0);
|
|
14051
|
+
if (!manifest) return;
|
|
14052
|
+
await writeManifest(io, {
|
|
14053
|
+
...manifest,
|
|
14054
|
+
docs: [
|
|
14055
|
+
...manifest.docs.filter((doc) => doc.docId !== payload.docId && doc.path !== payload.path),
|
|
14056
|
+
{
|
|
14057
|
+
docId: payload.docId,
|
|
14058
|
+
path: payload.path,
|
|
14059
|
+
title: payload.sidecar.title || titleFromMarkdown(payload.path, payload.markdown),
|
|
14060
|
+
contentHash: contentHashForText(payload.markdown),
|
|
14061
|
+
currentSha: payload.canonicalHead ?? payload.currentSha,
|
|
14062
|
+
updatedAt: payload.sidecar.updatedAt
|
|
14063
|
+
}
|
|
14064
|
+
].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0),
|
|
14065
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14066
|
+
});
|
|
14067
|
+
}
|
|
14068
|
+
async function removeManifestDocument(docId, path) {
|
|
14069
|
+
const manifest = await readManifest(io).catch(() => void 0);
|
|
14070
|
+
if (!manifest) return;
|
|
14071
|
+
await writeManifest(io, {
|
|
14072
|
+
...manifest,
|
|
14073
|
+
docs: manifest.docs.filter((doc) => doc.docId !== docId && doc.path !== path),
|
|
14074
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14075
|
+
});
|
|
14076
|
+
}
|
|
13704
14077
|
}
|
|
13705
14078
|
async function requestBridgeToken(options, rootId) {
|
|
13706
14079
|
const response = await fetch(new URL("/api/bridge-requests", options.baseUrl), {
|
|
@@ -13754,6 +14127,83 @@ async function requestBridgeToken(options, rootId) {
|
|
|
13754
14127
|
}
|
|
13755
14128
|
throw new Error("Bridge approval timed out before a token was issued.");
|
|
13756
14129
|
}
|
|
14130
|
+
async function readBridgeConfig(root) {
|
|
14131
|
+
const io = new NodeWorkspaceIO(root);
|
|
14132
|
+
try {
|
|
14133
|
+
const config2 = JSON.parse(await io.readText(BRIDGE_CONFIG_PATH));
|
|
14134
|
+
if (config2.schemaVersion !== 1 || typeof config2.workspaceId !== "string" || typeof config2.rootId !== "string" || typeof config2.baseUrl !== "string" || typeof config2.actorId !== "string") {
|
|
14135
|
+
return void 0;
|
|
14136
|
+
}
|
|
14137
|
+
return config2;
|
|
14138
|
+
} catch {
|
|
14139
|
+
return void 0;
|
|
14140
|
+
}
|
|
14141
|
+
}
|
|
14142
|
+
async function writeBridgeConfig(root, config2) {
|
|
14143
|
+
const io = new NodeWorkspaceIO(root);
|
|
14144
|
+
await io.mkdir(".mdocs");
|
|
14145
|
+
await io.writeTextAtomic(BRIDGE_CONFIG_PATH, `${JSON.stringify(config2, null, 2)}
|
|
14146
|
+
`);
|
|
14147
|
+
}
|
|
14148
|
+
async function fetchCanonicalSnapshot(options, rootId) {
|
|
14149
|
+
const response = await fetch(new URL(`/api/workspaces/${encodeURIComponent(options.workspaceId)}/roots/${encodeURIComponent(rootId)}/sync`, options.baseUrl), {
|
|
14150
|
+
headers: authHeaders(options.token)
|
|
14151
|
+
});
|
|
14152
|
+
if (!response.ok) return void 0;
|
|
14153
|
+
const snapshot = await response.json();
|
|
14154
|
+
const docs = await Promise.all(
|
|
14155
|
+
(snapshot.tree?.docs ?? []).map((doc) => typeof doc.docId === "string" ? doc.docId : void 0).filter((docId) => Boolean(docId)).map((docId) => fetchCanonicalDocument(options, rootId, docId))
|
|
14156
|
+
);
|
|
14157
|
+
return {
|
|
14158
|
+
head: snapshot.tree?.root?.canonical.head,
|
|
14159
|
+
docs: docs.filter((doc) => Boolean(doc))
|
|
14160
|
+
};
|
|
14161
|
+
}
|
|
14162
|
+
async function fetchCommitDocuments(options, rootId, headId) {
|
|
14163
|
+
const response = await fetch(
|
|
14164
|
+
new URL(
|
|
14165
|
+
`/api/workspaces/${encodeURIComponent(options.workspaceId)}/roots/${encodeURIComponent(rootId)}/commits/${encodeURIComponent(headId)}?content=1`,
|
|
14166
|
+
options.baseUrl
|
|
14167
|
+
),
|
|
14168
|
+
{ headers: authHeaders(options.token) }
|
|
14169
|
+
);
|
|
14170
|
+
if (!response.ok) return /* @__PURE__ */ new Map();
|
|
14171
|
+
const payload = await response.json();
|
|
14172
|
+
const docs = (payload.docs ?? []).map((doc) => readCommitDocumentPayload(doc, options.workspaceId, rootId, headId)).filter((doc) => Boolean(doc));
|
|
14173
|
+
return new Map(docs.map((doc) => [doc.docId, doc]));
|
|
14174
|
+
}
|
|
14175
|
+
async function fetchCanonicalDocument(options, rootId, docId) {
|
|
14176
|
+
try {
|
|
14177
|
+
const response = await fetch(
|
|
14178
|
+
new URL(
|
|
14179
|
+
`/api/workspaces/${encodeURIComponent(options.workspaceId)}/roots/${encodeURIComponent(rootId)}/documents/${encodeURIComponent(docId)}`,
|
|
14180
|
+
options.baseUrl
|
|
14181
|
+
),
|
|
14182
|
+
{ headers: authHeaders(options.token) }
|
|
14183
|
+
);
|
|
14184
|
+
if (!response.ok) return void 0;
|
|
14185
|
+
const document = await response.json();
|
|
14186
|
+
return readDocumentPayload({ ...document, canonicalHead: document.currentSha });
|
|
14187
|
+
} catch {
|
|
14188
|
+
return void 0;
|
|
14189
|
+
}
|
|
14190
|
+
}
|
|
14191
|
+
function readCommitDocumentPayload(payload, workspaceId, rootId, headId) {
|
|
14192
|
+
if (!payload || typeof payload !== "object") return void 0;
|
|
14193
|
+
const value = payload;
|
|
14194
|
+
if (typeof value.markdown !== "string" || !value.sidecar || typeof value.sidecar !== "object") return void 0;
|
|
14195
|
+
return readDocumentPayload({
|
|
14196
|
+
workspaceId,
|
|
14197
|
+
rootId,
|
|
14198
|
+
docId: value.docId,
|
|
14199
|
+
path: value.path,
|
|
14200
|
+
title: value.title,
|
|
14201
|
+
markdown: value.markdown,
|
|
14202
|
+
sidecar: value.sidecar,
|
|
14203
|
+
currentSha: headId,
|
|
14204
|
+
canonicalHead: headId
|
|
14205
|
+
});
|
|
14206
|
+
}
|
|
13757
14207
|
function parseBridgeSyncMessage(data) {
|
|
13758
14208
|
if (!data.trim()) return void 0;
|
|
13759
14209
|
try {
|
|
@@ -14063,9 +14513,9 @@ async function main() {
|
|
|
14063
14513
|
return;
|
|
14064
14514
|
}
|
|
14065
14515
|
case "bridge": {
|
|
14066
|
-
const
|
|
14067
|
-
|
|
14068
|
-
|
|
14516
|
+
const root = resolve5(String(parsed.flags.root ?? cwd));
|
|
14517
|
+
const baseOptions = {
|
|
14518
|
+
root,
|
|
14069
14519
|
rootId: typeof parsed.flags["root-id"] === "string" ? parsed.flags["root-id"] : void 0,
|
|
14070
14520
|
sourceId: typeof parsed.flags.source === "string" ? parsed.flags.source : void 0,
|
|
14071
14521
|
sourceName: typeof parsed.flags["source-name"] === "string" ? parsed.flags["source-name"] : void 0,
|
|
@@ -14075,11 +14525,24 @@ async function main() {
|
|
|
14075
14525
|
claimToken: typeof parsed.flags.claim === "string" ? parsed.flags.claim : void 0,
|
|
14076
14526
|
actorId: typeof parsed.flags.actor === "string" ? parsed.flags.actor : void 0,
|
|
14077
14527
|
actorName: typeof parsed.flags["actor-name"] === "string" ? parsed.flags["actor-name"] : void 0,
|
|
14078
|
-
baseUrl: bridgeBaseUrl(parsed.flags),
|
|
14079
14528
|
intervalMs: Number(parsed.flags.interval ?? 1e3),
|
|
14080
14529
|
token: typeof parsed.flags.token === "string" ? parsed.flags.token : process.env.MDOCS_BRIDGE_TOKEN || void 0,
|
|
14081
14530
|
requestToken: Boolean(parsed.flags["request-token"] || parsed.flags.pair),
|
|
14082
|
-
pairingTimeoutMs: typeof parsed.flags["pairing-timeout-ms"] === "string" ? Number(parsed.flags["pairing-timeout-ms"]) : void 0
|
|
14531
|
+
pairingTimeoutMs: typeof parsed.flags["pairing-timeout-ms"] === "string" ? Number(parsed.flags["pairing-timeout-ms"]) : void 0,
|
|
14532
|
+
once: Boolean(parsed.flags.once)
|
|
14533
|
+
};
|
|
14534
|
+
if (subcommand === "resume") {
|
|
14535
|
+
await runBridgeResume({
|
|
14536
|
+
...baseOptions,
|
|
14537
|
+
workspaceId: typeof parsed.flags.workspace === "string" ? parsed.flags.workspace : void 0,
|
|
14538
|
+
baseUrl: optionalBridgeBaseUrl(parsed.flags)
|
|
14539
|
+
});
|
|
14540
|
+
return;
|
|
14541
|
+
}
|
|
14542
|
+
const options = {
|
|
14543
|
+
...baseOptions,
|
|
14544
|
+
workspaceId: typeof parsed.flags.workspace === "string" ? parsed.flags.workspace : "workspace_demo",
|
|
14545
|
+
baseUrl: bridgeBaseUrl(parsed.flags)
|
|
14083
14546
|
};
|
|
14084
14547
|
if (subcommand === "setup") await runBridgeSetup(options);
|
|
14085
14548
|
else await runBridge({ ...options, actorId: options.actorId ?? "actor_local" });
|
|
@@ -14183,13 +14646,17 @@ function requiredFlag2(flags, name) {
|
|
|
14183
14646
|
return value;
|
|
14184
14647
|
}
|
|
14185
14648
|
function bridgeBaseUrl(flags) {
|
|
14186
|
-
|
|
14187
|
-
const configured = process.env.MDOCS_BASE_URL?.trim();
|
|
14649
|
+
const configured = optionalBridgeBaseUrl(flags);
|
|
14188
14650
|
if (configured) return configured;
|
|
14189
14651
|
throw new CliError("usage_error", "Missing --url <base-url> for mdocs bridge.", {
|
|
14190
14652
|
hint: "Pass the Magic Markdown origin explicitly (for example --url https://magic.example.com) or set MDOCS_BASE_URL. The bridge does not assume a localhost server."
|
|
14191
14653
|
});
|
|
14192
14654
|
}
|
|
14655
|
+
function optionalBridgeBaseUrl(flags) {
|
|
14656
|
+
if (typeof flags.url === "string" && flags.url.trim()) return flags.url.trim();
|
|
14657
|
+
const configured = process.env.MDOCS_BASE_URL?.trim();
|
|
14658
|
+
return configured || void 0;
|
|
14659
|
+
}
|
|
14193
14660
|
async function readRequiredTextFlag2(flags, cwd, names) {
|
|
14194
14661
|
const value = await readOptionalTextFlag2(flags, cwd, names);
|
|
14195
14662
|
if (value === void 0) {
|
|
@@ -14264,6 +14731,7 @@ Commands:
|
|
|
14264
14731
|
checkpoint create|list|restore Manage local reversible checkpoints
|
|
14265
14732
|
join <share-url> --json Join a Magic Markdown share through the CLI
|
|
14266
14733
|
joins --json List saved Magic Markdown remote joins
|
|
14734
|
+
remote status --json Verify the active remote join binding
|
|
14267
14735
|
remote map|graph|context|review|create-file|move-file
|
|
14268
14736
|
Work with documents in the active remote join
|
|
14269
14737
|
remote comment|suggest Add remote review comments and suggestions
|
|
@@ -14274,6 +14742,9 @@ Commands:
|
|
|
14274
14742
|
bridge setup --workspace <id> --root . --url <base-url> [--folder-id <id>] --request-token
|
|
14275
14743
|
Initialize, validate, request approval,
|
|
14276
14744
|
and start an agent filesystem bridge
|
|
14745
|
+
bridge resume --root . --request-token [--once]
|
|
14746
|
+
Backfill missed Magic changes, then keep
|
|
14747
|
+
the bridge running unless --once is set
|
|
14277
14748
|
bridge --workspace <id> --root . --url <base-url> --request-token
|
|
14278
14749
|
Request human approval, then sync an
|
|
14279
14750
|
approved local root with the workspace
|
|
@@ -14329,7 +14800,8 @@ main().catch((error) => {
|
|
|
14329
14800
|
code: cliError.code,
|
|
14330
14801
|
message: cliError.message,
|
|
14331
14802
|
...hint ? { hint } : {},
|
|
14332
|
-
...usage && cliError.hint ? { usage } : {}
|
|
14803
|
+
...usage && cliError.hint ? { usage } : {},
|
|
14804
|
+
...cliError.details !== void 0 ? { details: cliError.details } : {}
|
|
14333
14805
|
},
|
|
14334
14806
|
cliVersion: CLI_VERSION
|
|
14335
14807
|
};
|
package/package.json
CHANGED