@magic-markdown/cli 0.3.10 → 0.3.12

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 +336 -21
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10997,7 +10997,7 @@ var RemoteDocumentIO = class {
10997
10997
  };
10998
10998
 
10999
10999
  // src/agent.ts
11000
- var CLI_VERSION = "0.3.10";
11000
+ var CLI_VERSION = "0.3.12";
11001
11001
  var CLI_PACKAGE_NAME = "@magic-markdown/cli";
11002
11002
  var AGENT_COMMANDS = [
11003
11003
  {
@@ -11279,6 +11279,23 @@ var AGENT_COMMANDS = [
11279
11279
  "--token (or MDOCS_BRIDGE_TOKEN) still works when a scoped bridge token is already available."
11280
11280
  ]
11281
11281
  },
11282
+ {
11283
+ name: "bridge resume",
11284
+ summary: "Backfill missed Magic changes after an agent restart, then keep the bridge running.",
11285
+ usage: "mdocs bridge resume --root <path> [--url <base-url>] [--workspace <id>] [--root-id <id>] [--request-token] [--once]",
11286
+ output: "long-running",
11287
+ mutates: true,
11288
+ examples: [
11289
+ "mdocs bridge resume --root . --request-token",
11290
+ "mdocs bridge resume --root . --once --request-token"
11291
+ ],
11292
+ notes: [
11293
+ "Reads non-secret connection defaults from .mdocs/bridge.json written by bridge setup.",
11294
+ "Use this after a sandbox/container/agent session restarts. It backfills canonical Magic changes before publishing local edits.",
11295
+ "--request-token opens a human approval URL when no MDOCS_BRIDGE_TOKEN is available; do not ask users to paste bridge tokens by default.",
11296
+ "--once performs only the backfill/reconcile step and exits."
11297
+ ]
11298
+ },
11282
11299
  {
11283
11300
  name: "bridge",
11284
11301
  summary: "Sync a local Markdown root with a Magic workspace over WebSocket (long-running).",
@@ -11347,6 +11364,32 @@ Run \`mdocs help <command>\` (or \`mdocs <command> --help\`) for usage, examples
11347
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.
11348
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.
11349
11366
 
11367
+ ## Filesystem Bridge / Resume
11368
+
11369
+ 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:
11370
+
11371
+ \`\`\`bash
11372
+ npx --yes --package=@magic-markdown/cli@latest mdocs bridge setup \\
11373
+ --root . \\
11374
+ --workspace <workspace-id> \\
11375
+ --root-id <root-id> \\
11376
+ --url <magic-url> \\
11377
+ --actor-name "<agent name>" \\
11378
+ --request-token
11379
+ \`\`\`
11380
+
11381
+ Return the Magic approval link and keep the bridge process running after approval. Success looks like \`mdocs bridge connected ...\`.
11382
+
11383
+ 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:
11384
+
11385
+ \`\`\`bash
11386
+ npx --yes --package=@magic-markdown/cli@latest mdocs bridge resume --root . --request-token
11387
+ \`\`\`
11388
+
11389
+ \`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.
11390
+
11391
+ 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.
11392
+
11350
11393
  ## Editing Rules
11351
11394
 
11352
11395
  - Comments and suggestions are sidecar operations. They should not modify clean Markdown until a suggestion is accepted.
@@ -11565,6 +11608,7 @@ function nextCommands(map) {
11565
11608
  }
11566
11609
 
11567
11610
  // src/fs-io.ts
11611
+ import { randomUUID } from "node:crypto";
11568
11612
  import { mkdir as mkdir2, readFile as readFile2, readdir as readdir2, realpath, rename, rm as rm2, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
11569
11613
  import { dirname as dirname4, join as join2, relative, resolve } from "node:path";
11570
11614
  var NodeWorkspaceIO = class {
@@ -11585,7 +11629,7 @@ var NodeWorkspaceIO = class {
11585
11629
  async writeTextAtomic(path, content) {
11586
11630
  const target = await this.resolveWritablePath(path, Buffer.byteLength(content, "utf8"));
11587
11631
  await mkdir2(dirname4(target), { recursive: true });
11588
- const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
11632
+ const temporary = `${target}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
11589
11633
  await writeFile2(temporary, content, "utf8");
11590
11634
  await rename(temporary, target);
11591
11635
  }
@@ -12304,7 +12348,7 @@ function respond(payload) {
12304
12348
  }
12305
12349
 
12306
12350
  // src/remote-api.ts
12307
- import { randomUUID } from "node:crypto";
12351
+ import { randomUUID as randomUUID2 } from "node:crypto";
12308
12352
  async function postPresence(share, shareUrl, docId, agentId, agentName) {
12309
12353
  await fetchJson(agentUrl({ ...share, shareUrl }, docId, "presence"), {
12310
12354
  method: "POST",
@@ -12462,7 +12506,7 @@ async function postReview(record, docId, additions, actor) {
12462
12506
  {
12463
12507
  method: "POST",
12464
12508
  headers: { ...shareHeaders(record.shareUrl), "Content-Type": "application/json" },
12465
- body: JSON.stringify({ actor, changeId: randomUUID(), ...additions })
12509
+ body: JSON.stringify({ actor, changeId: randomUUID2(), ...additions })
12466
12510
  }
12467
12511
  );
12468
12512
  return response.document;
@@ -12487,7 +12531,7 @@ async function pushDocument(record, baseDocument, markdown, sidecar) {
12487
12531
  // Full identity so version-history commits show the agent's name.
12488
12532
  actor: { id: record.agentId, name: record.agentName, kind: "agent" },
12489
12533
  sourceId: record.agentId,
12490
- changeId: randomUUID(),
12534
+ changeId: randomUUID2(),
12491
12535
  payload,
12492
12536
  createdAt
12493
12537
  })
@@ -13399,8 +13443,9 @@ function optionalFolderTarget(value) {
13399
13443
  }
13400
13444
 
13401
13445
  // src/bridge.ts
13402
- import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
13446
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "node:crypto";
13403
13447
  import { basename as basename2, resolve as resolve4 } from "node:path";
13448
+ var BRIDGE_CONFIG_PATH = ".mdocs/bridge.json";
13404
13449
  function bridgeSetupIdentity(input) {
13405
13450
  const actorName = input.actorName?.trim() || "Agent";
13406
13451
  return {
@@ -13425,7 +13470,43 @@ async function runBridgeSetup(options) {
13425
13470
  actorId: identity.actorId,
13426
13471
  actorName: identity.actorName,
13427
13472
  sourceName: identity.sourceName,
13428
- requestToken: options.requestToken || !options.token
13473
+ requestToken: options.requestToken || !options.token,
13474
+ resume: true
13475
+ });
13476
+ }
13477
+ async function runBridgeResume(options) {
13478
+ const root = resolve4(options.root);
13479
+ const config2 = await readBridgeConfig(root);
13480
+ const identity = bridgeSetupIdentity({
13481
+ actorId: options.actorId ?? config2?.actorId,
13482
+ actorName: options.actorName ?? config2?.actorName,
13483
+ sourceName: options.sourceName ?? config2?.sourceName
13484
+ });
13485
+ const workspaceId = options.workspaceId ?? config2?.workspaceId;
13486
+ const rootId = options.rootId ?? options.sourceId ?? config2?.rootId;
13487
+ const baseUrl = options.baseUrl ?? config2?.baseUrl;
13488
+ if (!workspaceId || !rootId || !baseUrl) {
13489
+ throw new Error("Missing bridge resume configuration. Run mdocs bridge setup --workspace <id> --root <path> --root-id <id> --url <base-url> first.");
13490
+ }
13491
+ await runBridge({
13492
+ root,
13493
+ workspaceId,
13494
+ rootId,
13495
+ sourceId: options.sourceId,
13496
+ sourceName: identity.sourceName,
13497
+ folderId: options.folderId ?? config2?.folderId,
13498
+ canonicalPrefix: options.canonicalPrefix ?? config2?.canonicalPrefix,
13499
+ replicaPrefix: options.replicaPrefix ?? config2?.replicaPrefix,
13500
+ claimToken: options.claimToken,
13501
+ actorId: identity.actorId,
13502
+ actorName: identity.actorName,
13503
+ baseUrl,
13504
+ intervalMs: options.intervalMs ?? config2?.intervalMs ?? 1e3,
13505
+ token: options.token,
13506
+ requestToken: options.requestToken || !options.token,
13507
+ pairingTimeoutMs: options.pairingTimeoutMs,
13508
+ once: options.once,
13509
+ resume: true
13429
13510
  });
13430
13511
  }
13431
13512
  async function runBridge(options) {
@@ -13444,7 +13525,7 @@ async function runBridge(options) {
13444
13525
  replicaPrefix: options.replicaPrefix,
13445
13526
  lastAppliedHead: localManifestSource?.canonicalHead
13446
13527
  });
13447
- const sourceId = `bridge_${replicaId}_${randomUUID2().replaceAll("-", "").slice(0, 8)}`;
13528
+ const sourceId = `bridge_${replicaId}_${randomUUID3().replaceAll("-", "").slice(0, 8)}`;
13448
13529
  const io = new PathMappedWorkspaceIO(root, mapping);
13449
13530
  const signatures = /* @__PURE__ */ new Map();
13450
13531
  const pendingDocs = /* @__PURE__ */ new Set();
@@ -13458,8 +13539,24 @@ async function runBridge(options) {
13458
13539
  const registeredRoot = token ? await fetchScopedRoot({ ...options, token }, rootId) : await registerRoot(options, root, rootId, mapping);
13459
13540
  lastAppliedHead = lastAppliedHead ?? registeredRoot?.canonical.head;
13460
13541
  await writeSourceState(lastAppliedHead);
13542
+ await writeBridgeConfig(root, {
13543
+ schemaVersion: 1,
13544
+ workspaceId: options.workspaceId,
13545
+ rootId,
13546
+ baseUrl: options.baseUrl,
13547
+ actorId: options.actorId,
13548
+ actorName: options.actorName,
13549
+ sourceName: options.sourceName,
13550
+ folderId: options.folderId,
13551
+ canonicalPrefix: options.canonicalPrefix,
13552
+ replicaPrefix: options.replicaPrefix,
13553
+ intervalMs: options.intervalMs,
13554
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13555
+ });
13556
+ const backfilled = await resumeFromCanonical();
13557
+ if (options.once) return;
13461
13558
  connect();
13462
- if (claimMode) await primeLocalSignatures();
13559
+ if (claimMode && !backfilled) await primeLocalSignatures();
13463
13560
  else await publishSnapshot("initial");
13464
13561
  const timer = setInterval(() => {
13465
13562
  void publishSnapshot("poll").catch((error) => {
@@ -13535,6 +13632,87 @@ async function runBridge(options) {
13535
13632
  }
13536
13633
  }
13537
13634
  }
13635
+ async function resumeFromCanonical() {
13636
+ if (!options.resume && !token) return false;
13637
+ const remote = await fetchCanonicalSnapshot({ ...options, token }, rootId).catch(() => void 0);
13638
+ if (!remote) return false;
13639
+ const baseDocs = lastAppliedHead ? await fetchCommitDocuments({ ...options, token }, rootId, lastAppliedHead).catch(() => /* @__PURE__ */ new Map()) : /* @__PURE__ */ new Map();
13640
+ const remoteDocIds = new Set(remote.docs.map((doc) => doc.docId));
13641
+ let applied = 0;
13642
+ let keptLocal = 0;
13643
+ let conflicted = 0;
13644
+ for (const doc of remote.docs) {
13645
+ const base = baseDocs.get(doc.docId);
13646
+ const decision = await reconcileRemoteDocument(doc, base);
13647
+ if (decision === "applied") applied += 1;
13648
+ if (decision === "kept_local") keptLocal += 1;
13649
+ if (decision === "conflict") conflicted += 1;
13650
+ }
13651
+ for (const [docId, base] of baseDocs) {
13652
+ if (remoteDocIds.has(docId)) continue;
13653
+ const decision = await reconcileRemoteDelete(docId, base);
13654
+ if (decision === "applied") applied += 1;
13655
+ if (decision === "conflict") conflicted += 1;
13656
+ }
13657
+ lastAppliedHead = remote.head ?? lastAppliedHead;
13658
+ await writeSourceState(lastAppliedHead);
13659
+ process.stdout.write(
13660
+ `mdocs bridge resume ${root}: applied ${applied}, kept local ${keptLocal}, conflicts ${conflicted}, head ${lastAppliedHead ?? "unknown"}
13661
+ `
13662
+ );
13663
+ return true;
13664
+ }
13665
+ async function reconcileRemoteDocument(remote, base) {
13666
+ const remoteSignature = documentSignature(remote.markdown, remote.sidecar);
13667
+ const baseSignature = base ? documentSignature(base.markdown, base.sidecar) : void 0;
13668
+ const localState = await localStateForCanonical(remote);
13669
+ const localSignature = localState ? documentSignature(localState.markdown, localState.sidecar) : void 0;
13670
+ if (!localState) {
13671
+ await applyCanonicalDocument(remote);
13672
+ return "applied";
13673
+ }
13674
+ if (localSignature === remoteSignature && localState.path === remote.path && localState.docId === remote.docId) {
13675
+ signatures.set(remote.docId, remoteSignature);
13676
+ seenDocs.add(remote.docId);
13677
+ return "noop";
13678
+ }
13679
+ if (localSignature === remoteSignature && localState.path === remote.path) {
13680
+ await applyCanonicalDocument(remote);
13681
+ return "applied";
13682
+ }
13683
+ if (baseSignature && localSignature === baseSignature) {
13684
+ await applyCanonicalDocument(remote);
13685
+ return "applied";
13686
+ }
13687
+ if (baseSignature && remoteSignature === baseSignature) {
13688
+ signatures.set(remote.docId, baseSignature);
13689
+ seenDocs.add(remote.docId);
13690
+ return "kept_local";
13691
+ }
13692
+ signatures.set(remote.docId, baseSignature ?? remoteSignature);
13693
+ await applyCanonicalDocument(remote);
13694
+ return "conflict";
13695
+ }
13696
+ async function reconcileRemoteDelete(docId, base) {
13697
+ const localState = await getDocumentState(io, docId).catch(() => void 0);
13698
+ if (!localState) return "noop";
13699
+ const baseSignature = documentSignature(base.markdown, base.sidecar);
13700
+ const localSignature = documentSignature(localState.markdown, localState.sidecar);
13701
+ if (localSignature !== baseSignature) {
13702
+ const copyPath = conflictCopyPath(localState.path);
13703
+ await io.writeTextAtomic(copyPath, localState.markdown);
13704
+ await seedConflictCopySidecar(copyPath, localState);
13705
+ process.stderr.write(`local conflict ${localState.path}; local version saved as ${copyPath}, canonical delete applied
13706
+ `);
13707
+ }
13708
+ await io.deleteFile(localState.path).catch(() => void 0);
13709
+ await io.deleteFile(sidecarPath(docId)).catch(() => void 0);
13710
+ await removeManifestDocument(docId, localState.path);
13711
+ signatures.delete(docId);
13712
+ seenDocs.delete(docId);
13713
+ pendingDeletes.delete(docId);
13714
+ return localSignature === baseSignature ? "applied" : "conflict";
13715
+ }
13538
13716
  async function handleIncoming(message) {
13539
13717
  if (message.type === "file-changed") {
13540
13718
  await applyCanonicalDocument(readDocumentPayload(message.payload));
@@ -13560,7 +13738,7 @@ async function runBridge(options) {
13560
13738
  `);
13561
13739
  if (!payload.docId) return;
13562
13740
  pendingDocs.delete(payload.docId);
13563
- const canonical = await fetchCanonicalDocument(payload.docId);
13741
+ const canonical = await fetchCanonicalDocument2(payload.docId);
13564
13742
  if (canonical) await applyCanonicalDocument(canonical);
13565
13743
  }
13566
13744
  if (message.type === "sync-error") {
@@ -13575,7 +13753,7 @@ async function runBridge(options) {
13575
13753
  }
13576
13754
  async function applyCanonicalDocument(payload) {
13577
13755
  const remoteSignature = documentSignature(payload.markdown, payload.sidecar);
13578
- const localState = await getDocumentState(io, payload.docId).catch(() => void 0);
13756
+ const localState = await localStateForCanonical(payload);
13579
13757
  const localSignature = localState ? documentSignature(localState.markdown, localState.sidecar) : void 0;
13580
13758
  if (localSignature && signatures.has(payload.docId) && localSignature !== signatures.get(payload.docId) && localSignature !== remoteSignature) {
13581
13759
  const copyPath = conflictCopyPath(payload.path);
@@ -13597,9 +13775,14 @@ async function runBridge(options) {
13597
13775
  if (localState && localState.path !== payload.path) {
13598
13776
  await io.deleteFile(localState.path).catch(() => void 0);
13599
13777
  }
13778
+ if (localState && localState.docId !== payload.docId) {
13779
+ await io.deleteFile(sidecarPath(localState.docId)).catch(() => void 0);
13780
+ await removeManifestDocument(localState.docId, localState.path);
13781
+ }
13600
13782
  await io.writeTextAtomic(payload.path, payload.markdown);
13601
13783
  await io.writeTextAtomic(sidecarPath(payload.docId), `${JSON.stringify(payload.sidecar, null, 2)}
13602
13784
  `);
13785
+ await upsertManifestDocument(payload);
13603
13786
  signatures.set(payload.docId, remoteSignature);
13604
13787
  deniedSignatures.delete(payload.docId);
13605
13788
  seenDocs.add(payload.docId);
@@ -13635,7 +13818,14 @@ async function runBridge(options) {
13635
13818
  updatedAt: now
13636
13819
  });
13637
13820
  }
13638
- async function fetchCanonicalDocument(docId) {
13821
+ async function localStateForCanonical(payload) {
13822
+ const byDocId = await getDocumentState(io, payload.docId).catch(() => void 0);
13823
+ if (byDocId) return byDocId;
13824
+ const map = await getWorkspaceMap(io).catch(() => void 0);
13825
+ const byPath = map?.docs.find((doc) => doc.path === payload.path);
13826
+ return byPath ? getDocumentState(io, byPath.docId).catch(() => void 0) : void 0;
13827
+ }
13828
+ async function fetchCanonicalDocument2(docId) {
13639
13829
  try {
13640
13830
  const response = await fetch(
13641
13831
  new URL(
@@ -13665,7 +13855,7 @@ async function runBridge(options) {
13665
13855
  kind: actorKindForBridge(options.actorId)
13666
13856
  },
13667
13857
  sourceId,
13668
- changeId: randomUUID2(),
13858
+ changeId: randomUUID3(),
13669
13859
  payload,
13670
13860
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
13671
13861
  })
@@ -13700,6 +13890,34 @@ async function runBridge(options) {
13700
13890
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13701
13891
  });
13702
13892
  }
13893
+ async function upsertManifestDocument(payload) {
13894
+ const manifest = await readManifest(io).catch(() => void 0);
13895
+ if (!manifest) return;
13896
+ await writeManifest(io, {
13897
+ ...manifest,
13898
+ docs: [
13899
+ ...manifest.docs.filter((doc) => doc.docId !== payload.docId && doc.path !== payload.path),
13900
+ {
13901
+ docId: payload.docId,
13902
+ path: payload.path,
13903
+ title: payload.sidecar.title || titleFromMarkdown(payload.path, payload.markdown),
13904
+ contentHash: contentHashForText(payload.markdown),
13905
+ currentSha: payload.canonicalHead ?? payload.currentSha,
13906
+ updatedAt: payload.sidecar.updatedAt
13907
+ }
13908
+ ].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0),
13909
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13910
+ });
13911
+ }
13912
+ async function removeManifestDocument(docId, path) {
13913
+ const manifest = await readManifest(io).catch(() => void 0);
13914
+ if (!manifest) return;
13915
+ await writeManifest(io, {
13916
+ ...manifest,
13917
+ docs: manifest.docs.filter((doc) => doc.docId !== docId && doc.path !== path),
13918
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13919
+ });
13920
+ }
13703
13921
  }
13704
13922
  async function requestBridgeToken(options, rootId) {
13705
13923
  const response = await fetch(new URL("/api/bridge-requests", options.baseUrl), {
@@ -13753,6 +13971,83 @@ async function requestBridgeToken(options, rootId) {
13753
13971
  }
13754
13972
  throw new Error("Bridge approval timed out before a token was issued.");
13755
13973
  }
13974
+ async function readBridgeConfig(root) {
13975
+ const io = new NodeWorkspaceIO(root);
13976
+ try {
13977
+ const config2 = JSON.parse(await io.readText(BRIDGE_CONFIG_PATH));
13978
+ if (config2.schemaVersion !== 1 || typeof config2.workspaceId !== "string" || typeof config2.rootId !== "string" || typeof config2.baseUrl !== "string" || typeof config2.actorId !== "string") {
13979
+ return void 0;
13980
+ }
13981
+ return config2;
13982
+ } catch {
13983
+ return void 0;
13984
+ }
13985
+ }
13986
+ async function writeBridgeConfig(root, config2) {
13987
+ const io = new NodeWorkspaceIO(root);
13988
+ await io.mkdir(".mdocs");
13989
+ await io.writeTextAtomic(BRIDGE_CONFIG_PATH, `${JSON.stringify(config2, null, 2)}
13990
+ `);
13991
+ }
13992
+ async function fetchCanonicalSnapshot(options, rootId) {
13993
+ const response = await fetch(new URL(`/api/workspaces/${encodeURIComponent(options.workspaceId)}/roots/${encodeURIComponent(rootId)}/sync`, options.baseUrl), {
13994
+ headers: authHeaders(options.token)
13995
+ });
13996
+ if (!response.ok) return void 0;
13997
+ const snapshot = await response.json();
13998
+ const docs = await Promise.all(
13999
+ (snapshot.tree?.docs ?? []).map((doc) => typeof doc.docId === "string" ? doc.docId : void 0).filter((docId) => Boolean(docId)).map((docId) => fetchCanonicalDocument(options, rootId, docId))
14000
+ );
14001
+ return {
14002
+ head: snapshot.tree?.root?.canonical.head,
14003
+ docs: docs.filter((doc) => Boolean(doc))
14004
+ };
14005
+ }
14006
+ async function fetchCommitDocuments(options, rootId, headId) {
14007
+ const response = await fetch(
14008
+ new URL(
14009
+ `/api/workspaces/${encodeURIComponent(options.workspaceId)}/roots/${encodeURIComponent(rootId)}/commits/${encodeURIComponent(headId)}?content=1`,
14010
+ options.baseUrl
14011
+ ),
14012
+ { headers: authHeaders(options.token) }
14013
+ );
14014
+ if (!response.ok) return /* @__PURE__ */ new Map();
14015
+ const payload = await response.json();
14016
+ const docs = (payload.docs ?? []).map((doc) => readCommitDocumentPayload(doc, options.workspaceId, rootId, headId)).filter((doc) => Boolean(doc));
14017
+ return new Map(docs.map((doc) => [doc.docId, doc]));
14018
+ }
14019
+ async function fetchCanonicalDocument(options, rootId, docId) {
14020
+ try {
14021
+ const response = await fetch(
14022
+ new URL(
14023
+ `/api/workspaces/${encodeURIComponent(options.workspaceId)}/roots/${encodeURIComponent(rootId)}/documents/${encodeURIComponent(docId)}`,
14024
+ options.baseUrl
14025
+ ),
14026
+ { headers: authHeaders(options.token) }
14027
+ );
14028
+ if (!response.ok) return void 0;
14029
+ const document = await response.json();
14030
+ return readDocumentPayload({ ...document, canonicalHead: document.currentSha });
14031
+ } catch {
14032
+ return void 0;
14033
+ }
14034
+ }
14035
+ function readCommitDocumentPayload(payload, workspaceId, rootId, headId) {
14036
+ if (!payload || typeof payload !== "object") return void 0;
14037
+ const value = payload;
14038
+ if (typeof value.markdown !== "string" || !value.sidecar || typeof value.sidecar !== "object") return void 0;
14039
+ return readDocumentPayload({
14040
+ workspaceId,
14041
+ rootId,
14042
+ docId: value.docId,
14043
+ path: value.path,
14044
+ title: value.title,
14045
+ markdown: value.markdown,
14046
+ sidecar: value.sidecar,
14047
+ currentSha: headId,
14048
+ canonicalHead: headId
14049
+ });
14050
+ }
13756
14051
  function parseBridgeSyncMessage(data) {
13757
14052
  if (!data.trim()) return void 0;
13758
14053
  try {
@@ -13784,7 +14079,7 @@ async function fetchScopedRoot(options, rootId) {
13784
14079
  async function registerRoot(options, root, rootId, mapping) {
13785
14080
  const now = (/* @__PURE__ */ new Date()).toISOString();
13786
14081
  const existingRoot = await fetchExistingRoot(options, rootId);
13787
- const canonicalHead = existingRoot?.canonical.head ?? mapping.lastAppliedHead ?? `head_${randomUUID2().replaceAll("-", "").slice(0, 16)}`;
14082
+ const canonicalHead = existingRoot?.canonical.head ?? mapping.lastAppliedHead ?? `head_${randomUUID3().replaceAll("-", "").slice(0, 16)}`;
13788
14083
  const rootName = options.sourceName ?? existingRoot?.name ?? (basename2(root) || rootId);
13789
14084
  const owner = ownerForBridge(options.actorId, rootName, options.actorName);
13790
14085
  const replica = { ...mapping, lastAppliedHead: canonicalHead, status: "synced", updatedAt: now };
@@ -14062,9 +14357,9 @@ async function main() {
14062
14357
  return;
14063
14358
  }
14064
14359
  case "bridge": {
14065
- const options = {
14066
- root: resolve5(String(parsed.flags.root ?? cwd)),
14067
- workspaceId: String(parsed.flags.workspace ?? "workspace_demo"),
14360
+ const root = resolve5(String(parsed.flags.root ?? cwd));
14361
+ const baseOptions = {
14362
+ root,
14068
14363
  rootId: typeof parsed.flags["root-id"] === "string" ? parsed.flags["root-id"] : void 0,
14069
14364
  sourceId: typeof parsed.flags.source === "string" ? parsed.flags.source : void 0,
14070
14365
  sourceName: typeof parsed.flags["source-name"] === "string" ? parsed.flags["source-name"] : void 0,
@@ -14074,11 +14369,24 @@ async function main() {
14074
14369
  claimToken: typeof parsed.flags.claim === "string" ? parsed.flags.claim : void 0,
14075
14370
  actorId: typeof parsed.flags.actor === "string" ? parsed.flags.actor : void 0,
14076
14371
  actorName: typeof parsed.flags["actor-name"] === "string" ? parsed.flags["actor-name"] : void 0,
14077
- baseUrl: bridgeBaseUrl(parsed.flags),
14078
14372
  intervalMs: Number(parsed.flags.interval ?? 1e3),
14079
14373
  token: typeof parsed.flags.token === "string" ? parsed.flags.token : process.env.MDOCS_BRIDGE_TOKEN || void 0,
14080
14374
  requestToken: Boolean(parsed.flags["request-token"] || parsed.flags.pair),
14081
- pairingTimeoutMs: typeof parsed.flags["pairing-timeout-ms"] === "string" ? Number(parsed.flags["pairing-timeout-ms"]) : void 0
14375
+ pairingTimeoutMs: typeof parsed.flags["pairing-timeout-ms"] === "string" ? Number(parsed.flags["pairing-timeout-ms"]) : void 0,
14376
+ once: Boolean(parsed.flags.once)
14377
+ };
14378
+ if (subcommand === "resume") {
14379
+ await runBridgeResume({
14380
+ ...baseOptions,
14381
+ workspaceId: typeof parsed.flags.workspace === "string" ? parsed.flags.workspace : void 0,
14382
+ baseUrl: optionalBridgeBaseUrl(parsed.flags)
14383
+ });
14384
+ return;
14385
+ }
14386
+ const options = {
14387
+ ...baseOptions,
14388
+ workspaceId: typeof parsed.flags.workspace === "string" ? parsed.flags.workspace : "workspace_demo",
14389
+ baseUrl: bridgeBaseUrl(parsed.flags)
14082
14390
  };
14083
14391
  if (subcommand === "setup") await runBridgeSetup(options);
14084
14392
  else await runBridge({ ...options, actorId: options.actorId ?? "actor_local" });
@@ -14182,13 +14490,17 @@ function requiredFlag2(flags, name) {
14182
14490
  return value;
14183
14491
  }
14184
14492
  function bridgeBaseUrl(flags) {
14185
- if (typeof flags.url === "string" && flags.url.trim()) return flags.url.trim();
14186
- const configured = process.env.MDOCS_BASE_URL?.trim();
14493
+ const configured = optionalBridgeBaseUrl(flags);
14187
14494
  if (configured) return configured;
14188
14495
  throw new CliError("usage_error", "Missing --url <base-url> for mdocs bridge.", {
14189
14496
  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."
14190
14497
  });
14191
14498
  }
14499
+ function optionalBridgeBaseUrl(flags) {
14500
+ if (typeof flags.url === "string" && flags.url.trim()) return flags.url.trim();
14501
+ const configured = process.env.MDOCS_BASE_URL?.trim();
14502
+ return configured || void 0;
14503
+ }
14192
14504
  async function readRequiredTextFlag2(flags, cwd, names) {
14193
14505
  const value = await readOptionalTextFlag2(flags, cwd, names);
14194
14506
  if (value === void 0) {
@@ -14273,6 +14585,9 @@ Commands:
14273
14585
  bridge setup --workspace <id> --root . --url <base-url> [--folder-id <id>] --request-token
14274
14586
  Initialize, validate, request approval,
14275
14587
  and start an agent filesystem bridge
14588
+ bridge resume --root . --request-token [--once]
14589
+ Backfill missed Magic changes, then keep
14590
+ the bridge running unless --once is set
14276
14591
  bridge --workspace <id> --root . --url <base-url> --request-token
14277
14592
  Request human approval, then sync an
14278
14593
  approved local root with the workspace
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magic-markdown/cli",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
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",