@magic-markdown/cli 0.3.12 → 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 +206 -48
  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.12";
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.",
@@ -11358,11 +11419,12 @@ Run \`mdocs help <command>\` (or \`mdocs <command> --help\`) for usage, examples
11358
11419
  ## Start Here
11359
11420
 
11360
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.
11361
- 2. If working in a local workspace, run \`mdocs doctor --json\` to validate the workspace and learn recommended next commands.
11362
- 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>\`.
11363
- 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.
11364
- 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.
11365
- 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.
11366
11428
 
11367
11429
  ## Filesystem Bridge / Resume
11368
11430
 
@@ -11407,7 +11469,7 @@ If Magic and the local root both changed the same document while you were offlin
11407
11469
  ## Errors, Conflicts, and Exit Codes
11408
11470
 
11409
11471
  - Errors go to stderr. With \`--json\` they are structured: \`{ "ok": false, "error": { "code", "message", "hint" }, "cliVersion" }\`. Follow the \`hint\`.
11410
- - 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).
11411
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.
11412
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.
11413
11475
  - Retries are safe: remote writes carry a changeId and the server deduplicates replays.
@@ -11431,7 +11493,7 @@ Start the local stdio MCP server with:
11431
11493
  mdocs serve-mcp --cwd /path/to/workspace
11432
11494
  \`\`\`
11433
11495
 
11434
- 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.
11435
11497
  `;
11436
11498
  }
11437
11499
  function formatCommandReference() {
@@ -11459,6 +11521,7 @@ var CLI_EXIT_CODES = {
11459
11521
  invalid_range: 2,
11460
11522
  conflict: 3,
11461
11523
  not_found: 4,
11524
+ wrong_binding: 4,
11462
11525
  network_error: 5,
11463
11526
  unauthorized: 6
11464
11527
  };
@@ -12511,6 +12574,16 @@ async function postReview(record, docId, additions, actor) {
12511
12574
  );
12512
12575
  return response.document;
12513
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
+ }
12514
12587
  async function pushDocument(record, baseDocument, markdown, sidecar) {
12515
12588
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
12516
12589
  const payload = {
@@ -12808,6 +12881,8 @@ async function runJoinsCommand(root) {
12808
12881
  async function runRemoteCommand(root, subcommand, parsed) {
12809
12882
  const record = await normalizedSelectedJoin(root, parsed.flags);
12810
12883
  switch (subcommand) {
12884
+ case "status":
12885
+ return remoteStatus(record, parsed.flags);
12811
12886
  case "map":
12812
12887
  case void 0: {
12813
12888
  if (record.scope === "file") {
@@ -12861,10 +12936,53 @@ async function runRemoteCommand(root, subcommand, parsed) {
12861
12936
  return rejoin(root, record.joinId, parsed.flags);
12862
12937
  default:
12863
12938
  throw new CliError("usage_error", `Unknown remote subcommand: ${subcommand}`, {
12864
- 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."
12865
12940
  });
12866
12941
  }
12867
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
+ }
12868
12986
  async function remoteGraph(record) {
12869
12987
  assertProjectScope(record, "graph");
12870
12988
  const rootRecord = assertRootScoped(record, "graph");
@@ -13108,13 +13226,24 @@ async function remoteSuggest(root, record, parsed) {
13108
13226
  const replacement = await readRequiredTextFlag(parsed.flags, root, ["with", "replacement"]);
13109
13227
  const message = await readOptionalTextFlag(parsed.flags, root, ["message"]) ?? "Suggested edit";
13110
13228
  const range = parseRange(requiredFlag(parsed.flags, "range"));
13111
- const result = await submitReview(
13112
- root,
13113
- record,
13114
- parsed.command[2] ?? record.docId,
13115
- (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)
13116
13243
  );
13117
- 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 };
13118
13247
  }
13119
13248
  async function remoteCreateFolder(record, parsed) {
13120
13249
  assertProjectScope(record, "create-folder");
@@ -13379,6 +13508,33 @@ function joinSummary(record, document) {
13379
13508
  ].filter(Boolean)
13380
13509
  };
13381
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
+ }
13382
13538
  function assertProjectScope(record, command) {
13383
13539
  if (record.scope === "project") return;
13384
13540
  throw new CliError("usage_error", `remote ${command} requires a project-scoped join.`, {
@@ -14575,6 +14731,7 @@ Commands:
14575
14731
  checkpoint create|list|restore Manage local reversible checkpoints
14576
14732
  join <share-url> --json Join a Magic Markdown share through the CLI
14577
14733
  joins --json List saved Magic Markdown remote joins
14734
+ remote status --json Verify the active remote join binding
14578
14735
  remote map|graph|context|review|create-file|move-file
14579
14736
  Work with documents in the active remote join
14580
14737
  remote comment|suggest Add remote review comments and suggestions
@@ -14643,7 +14800,8 @@ main().catch((error) => {
14643
14800
  code: cliError.code,
14644
14801
  message: cliError.message,
14645
14802
  ...hint ? { hint } : {},
14646
- ...usage && cliError.hint ? { usage } : {}
14803
+ ...usage && cliError.hint ? { usage } : {},
14804
+ ...cliError.details !== void 0 ? { details: cliError.details } : {}
14647
14805
  },
14648
14806
  cliVersion: CLI_VERSION
14649
14807
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magic-markdown/cli",
3
- "version": "0.3.12",
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",