@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.
- package/dist/index.js +336 -21
- 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.
|
|
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:
|
|
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:
|
|
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
|
|
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}_${
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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_${
|
|
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
|
|
14066
|
-
|
|
14067
|
-
|
|
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
|
-
|
|
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