@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.
Files changed (2) hide show
  1. package/dist/index.js +532 -60
  2. 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 best = findQuote(lines, quoteLines);
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 findQuote(lines, quoteLines) {
943
- if (quoteLines.length === 0) return void 0;
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.join("\n")) {
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 void 0;
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
- if (selector.prefix) {
955
- const expectedPrefix = selector.prefix.split(/\r?\n/).filter(Boolean).slice(-3).join("\n");
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
- if (selector.suffix) {
960
- const expectedSuffix = selector.suffix.split(/\r?\n/).filter(Boolean).slice(0, 3).join("\n");
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 = contextPrefix(anchor.selector.prefix);
1229
+ const expectedPrefix = contextPrefix2(anchor.selector.prefix);
1204
1230
  if (expectedPrefix) {
1205
- const actualPrefix = contextPrefix(lines.slice(Math.max(0, range.startLine - 4), range.startLine - 1).join("\n"));
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 = contextSuffix(anchor.selector.suffix);
1234
+ const expectedSuffix = contextSuffix2(anchor.selector.suffix);
1209
1235
  if (expectedSuffix) {
1210
- const actualSuffix = contextSuffix(lines.slice(range.endLine, range.endLine + 3).join("\n"));
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 contextPrefix(value) {
1241
+ function contextPrefix2(value) {
1216
1242
  return value?.split(/\r?\n/).filter(Boolean).slice(-3).join("\n") || void 0;
1217
1243
  }
1218
- function contextSuffix(value) {
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.11";
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 working in a local workspace, run \`mdocs doctor --json\` to validate the workspace and learn recommended next commands.
11345
- 3. Run \`mdocs map --json\` or \`mdocs remote map --json\` to list documents, paths, docIds, open comments, open suggestions, anchor review counts, and link counts. File-scoped joins (most share links) cover a single document, so \`mdocs remote map\` is unavailable for them \u2014 use \`mdocs remote context --summary --json\` instead. Project-scoped joins cover one root; workspace-scoped joins cover Home and can target duplicate paths as \`<rootId>:<path-or-docId>\`.
11346
- 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.
11347
- 5. For a document, run \`mdocs context <path|docId> --summary --json\` locally or \`mdocs remote context <path|docId> --summary --json\` for a joined share before reading full content. Then page Markdown with \`--start-line\` / \`--end-line\` and \`--no-review\` when you only need document text.
11348
- 6. Pull review state separately with \`mdocs review <path|docId> --json\` locally or \`mdocs remote review <path|docId> --json\` for a joined share. Use \`mdocs comment\` / \`mdocs remote comment\` for review notes and \`mdocs suggest\` / \`mdocs remote suggest\` for proposed replacements. Do not insert comments, CriticMarkup, directives, or Magic markers into Markdown files.
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 result = await submitReview(
13069
- root,
13070
- record,
13071
- parsed.command[2] ?? record.docId,
13072
- (io, docId) => addSuggestion(io, docId, range, replacement, message, actorForRecord(record))
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
- return { suggestion: result.created, document: result.document };
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 fetchCanonicalDocument(payload.docId);
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 getDocumentState(io, payload.docId).catch(() => void 0);
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 fetchCanonicalDocument(docId) {
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 options = {
14067
- root: resolve5(String(parsed.flags.root ?? cwd)),
14068
- workspaceId: String(parsed.flags.workspace ?? "workspace_demo"),
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
- if (typeof flags.url === "string" && flags.url.trim()) return flags.url.trim();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magic-markdown/cli",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
4
4
  "description": "Magic Markdown agent CLI (mdocs): read, review, comment on, suggest edits to, and sync clean Markdown workspaces.",
5
5
  "type": "module",
6
6
  "license": "MIT",