@abraca/cli 1.9.1 → 2.3.0

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.
@@ -37,6 +37,10 @@ node_fs = __toESM(node_fs);
37
37
  let node_os = require("node:os");
38
38
  let node_path = require("node:path");
39
39
  node_path = __toESM(node_path);
40
+ let wtf_wikipedia = require("wtf_wikipedia");
41
+ wtf_wikipedia = __toESM(wtf_wikipedia);
42
+ let wtf_plugin_api = require("wtf-plugin-api");
43
+ wtf_plugin_api = __toESM(wtf_plugin_api);
40
44
 
41
45
  //#region packages/cli/src/parser.ts
42
46
  /**
@@ -83,97 +87,205 @@ function parseArgs(argv) {
83
87
  }
84
88
 
85
89
  //#endregion
86
- //#region packages/cli/node_modules/@noble/hashes/esm/utils.js
87
- /** Checks if something is Uint8Array. Be careful: nodejs Buffer will return true. */
90
+ //#region node_modules/@noble/hashes/utils.js
91
+ /**
92
+ * Checks if something is Uint8Array. Be careful: nodejs Buffer will return true.
93
+ * @param a - value to test
94
+ * @returns `true` when the value is a Uint8Array-compatible view.
95
+ * @example
96
+ * Check whether a value is a Uint8Array-compatible view.
97
+ * ```ts
98
+ * isBytes(new Uint8Array([1, 2, 3]));
99
+ * ```
100
+ */
88
101
  function isBytes(a) {
89
- return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array";
102
+ return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
90
103
  }
91
- /** Asserts something is Uint8Array. */
92
- function abytes(b, ...lengths) {
93
- if (!isBytes(b)) throw new Error("Uint8Array expected");
94
- if (lengths.length > 0 && !lengths.includes(b.length)) throw new Error("Uint8Array expected of length " + lengths + ", got length=" + b.length);
104
+ /**
105
+ * Asserts something is Uint8Array.
106
+ * @param value - value to validate
107
+ * @param length - optional exact length constraint
108
+ * @param title - label included in thrown errors
109
+ * @returns The validated byte array.
110
+ * @throws On wrong argument types. {@link TypeError}
111
+ * @throws On wrong argument ranges or values. {@link RangeError}
112
+ * @example
113
+ * Validate that a value is a byte array.
114
+ * ```ts
115
+ * abytes(new Uint8Array([1, 2, 3]));
116
+ * ```
117
+ */
118
+ function abytes(value, length, title = "") {
119
+ const bytes = isBytes(value);
120
+ const len = value?.length;
121
+ const needsLen = length !== void 0;
122
+ if (!bytes || needsLen && len !== length) {
123
+ const prefix = title && `"${title}" `;
124
+ const ofLen = needsLen ? ` of length ${length}` : "";
125
+ const got = bytes ? `length=${len}` : `type=${typeof value}`;
126
+ const message = prefix + "expected Uint8Array" + ofLen + ", got " + got;
127
+ if (!bytes) throw new TypeError(message);
128
+ throw new RangeError(message);
129
+ }
130
+ return value;
95
131
  }
96
- /** Asserts a hash instance has not been destroyed / finished */
132
+ /**
133
+ * Asserts a hash instance has not been destroyed or finished.
134
+ * @param instance - hash instance to validate
135
+ * @param checkFinished - whether to reject finalized instances
136
+ * @throws If the hash instance has already been destroyed or finalized. {@link Error}
137
+ * @example
138
+ * Validate that a hash instance is still usable.
139
+ * ```ts
140
+ * import { aexists } from '@noble/hashes/utils.js';
141
+ * import { sha256 } from '@noble/hashes/sha2.js';
142
+ * const hash = sha256.create();
143
+ * aexists(hash);
144
+ * ```
145
+ */
97
146
  function aexists(instance, checkFinished = true) {
98
147
  if (instance.destroyed) throw new Error("Hash instance has been destroyed");
99
148
  if (checkFinished && instance.finished) throw new Error("Hash#digest() has already been called");
100
149
  }
101
- /** Asserts output is properly-sized byte array */
150
+ /**
151
+ * Asserts output is a sufficiently-sized byte array.
152
+ * @param out - destination buffer
153
+ * @param instance - hash instance providing output length
154
+ * Oversized buffers are allowed; downstream code only promises to fill the first `outputLen` bytes.
155
+ * @throws On wrong argument types. {@link TypeError}
156
+ * @throws On wrong argument ranges or values. {@link RangeError}
157
+ * @example
158
+ * Validate a caller-provided digest buffer.
159
+ * ```ts
160
+ * import { aoutput } from '@noble/hashes/utils.js';
161
+ * import { sha256 } from '@noble/hashes/sha2.js';
162
+ * const hash = sha256.create();
163
+ * aoutput(new Uint8Array(hash.outputLen), hash);
164
+ * ```
165
+ */
102
166
  function aoutput(out, instance) {
103
- abytes(out);
167
+ abytes(out, void 0, "digestInto() output");
104
168
  const min = instance.outputLen;
105
- if (out.length < min) throw new Error("digestInto() expects output buffer of length at least " + min);
169
+ if (out.length < min) throw new RangeError("\"digestInto() output\" expected to be of length >=" + min);
106
170
  }
107
- /** Zeroize a byte array. Warning: JS provides no guarantees. */
171
+ /**
172
+ * Zeroizes typed arrays in place. Warning: JS provides no guarantees.
173
+ * @param arrays - arrays to overwrite with zeros
174
+ * @example
175
+ * Zeroize sensitive buffers in place.
176
+ * ```ts
177
+ * clean(new Uint8Array([1, 2, 3]));
178
+ * ```
179
+ */
108
180
  function clean(...arrays) {
109
181
  for (let i = 0; i < arrays.length; i++) arrays[i].fill(0);
110
182
  }
111
- /** Create DataView of an array for easy byte-level manipulation. */
183
+ /**
184
+ * Creates a DataView for byte-level manipulation.
185
+ * @param arr - source typed array
186
+ * @returns DataView over the same buffer region.
187
+ * @example
188
+ * Create a DataView over an existing buffer.
189
+ * ```ts
190
+ * createView(new Uint8Array(4));
191
+ * ```
192
+ */
112
193
  function createView(arr) {
113
194
  return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
114
195
  }
115
- /** Is current platform little-endian? Most are. Big-Endian platform: IBM */
196
+ /** Whether the current platform is little-endian. */
116
197
  const isLE = new Uint8Array(new Uint32Array([287454020]).buffer)[0] === 68;
117
198
  const hasHexBuiltin = typeof Uint8Array.from([]).toHex === "function" && typeof Uint8Array.fromHex === "function";
118
199
  /**
119
- * Converts string to bytes using UTF8 encoding.
120
- * @example utf8ToBytes('abc') // Uint8Array.from([97, 98, 99])
121
- */
122
- function utf8ToBytes(str) {
123
- if (typeof str !== "string") throw new Error("string expected");
124
- return new Uint8Array(new TextEncoder().encode(str));
125
- }
126
- /**
127
- * Normalizes (non-hex) string or Uint8Array to Uint8Array.
128
- * Warning: when Uint8Array is passed, it would NOT get copied.
129
- * Keep in mind for future mutable operations.
200
+ * Creates a callable hash function from a stateful class constructor.
201
+ * @param hashCons - hash constructor or factory
202
+ * @param info - optional metadata such as DER OID
203
+ * @returns Frozen callable hash wrapper with `.create()`.
204
+ * Wrapper construction eagerly calls `hashCons(undefined)` once to read
205
+ * `outputLen` / `blockLen`, so constructor side effects happen at module
206
+ * init time.
207
+ * @example
208
+ * Wrap a stateful hash constructor into a callable helper.
209
+ * ```ts
210
+ * import { createHasher } from '@noble/hashes/utils.js';
211
+ * import { sha256 } from '@noble/hashes/sha2.js';
212
+ * const wrapped = createHasher(sha256.create, { oid: sha256.oid });
213
+ * wrapped(new Uint8Array([1]));
214
+ * ```
130
215
  */
131
- function toBytes(data) {
132
- if (typeof data === "string") data = utf8ToBytes(data);
133
- abytes(data);
134
- return data;
135
- }
136
- /** For runtime check if class implements interface */
137
- var Hash = class {};
138
- /** Wraps hash function, creating an interface on top of it */
139
- function createHasher(hashCons) {
140
- const hashC = (msg) => hashCons().update(toBytes(msg)).digest();
141
- const tmp = hashCons();
216
+ function createHasher(hashCons, info = {}) {
217
+ const hashC = (msg, opts) => hashCons(opts).update(msg).digest();
218
+ const tmp = hashCons(void 0);
142
219
  hashC.outputLen = tmp.outputLen;
143
220
  hashC.blockLen = tmp.blockLen;
144
- hashC.create = () => hashCons();
145
- return hashC;
221
+ hashC.canXOF = tmp.canXOF;
222
+ hashC.create = (opts) => hashCons(opts);
223
+ Object.assign(hashC, info);
224
+ return Object.freeze(hashC);
146
225
  }
226
+ /**
227
+ * Creates OID metadata for NIST hashes with prefix `06 09 60 86 48 01 65 03 04 02`.
228
+ * @param suffix - final OID byte for the selected hash.
229
+ * The helper accepts any byte even though only the documented NIST hash
230
+ * suffixes are meaningful downstream.
231
+ * @returns Object containing the DER-encoded OID.
232
+ * @example
233
+ * Build OID metadata for a NIST hash.
234
+ * ```ts
235
+ * oidNist(0x01);
236
+ * ```
237
+ */
238
+ const oidNist = (suffix) => ({ oid: Uint8Array.from([
239
+ 6,
240
+ 9,
241
+ 96,
242
+ 134,
243
+ 72,
244
+ 1,
245
+ 101,
246
+ 3,
247
+ 4,
248
+ 2,
249
+ suffix
250
+ ]) });
147
251
 
148
252
  //#endregion
149
- //#region packages/cli/node_modules/@noble/hashes/esm/_md.js
253
+ //#region node_modules/@noble/hashes/_md.js
150
254
  /**
151
255
  * Internal Merkle-Damgard hash utils.
152
256
  * @module
153
257
  */
154
- /** Polyfill for Safari 14. https://caniuse.com/mdn-javascript_builtins_dataview_setbiguint64 */
155
- function setBigUint64(view, byteOffset, value, isLE) {
156
- if (typeof view.setBigUint64 === "function") return view.setBigUint64(byteOffset, value, isLE);
157
- const _32n = BigInt(32);
158
- const _u32_max = BigInt(4294967295);
159
- const wh = Number(value >> _32n & _u32_max);
160
- const wl = Number(value & _u32_max);
161
- const h = isLE ? 4 : 0;
162
- const l = isLE ? 0 : 4;
163
- view.setUint32(byteOffset + h, wh, isLE);
164
- view.setUint32(byteOffset + l, wl, isLE);
165
- }
166
258
  /**
167
259
  * Merkle-Damgard hash construction base class.
168
260
  * Could be used to create MD5, RIPEMD, SHA1, SHA2.
261
+ * Accepts only byte-aligned `Uint8Array` input, even when the underlying spec describes bit
262
+ * strings with partial-byte tails.
263
+ * @param blockLen - internal block size in bytes
264
+ * @param outputLen - digest size in bytes
265
+ * @param padOffset - trailing length field size in bytes
266
+ * @param isLE - whether length and state words are encoded in little-endian
267
+ * @example
268
+ * Use a concrete subclass to get the shared Merkle-Damgard update/digest flow.
269
+ * ```ts
270
+ * import { _SHA1 } from '@noble/hashes/legacy.js';
271
+ * const hash = new _SHA1();
272
+ * hash.update(new Uint8Array([97, 98, 99]));
273
+ * hash.digest();
274
+ * ```
169
275
  */
170
- var HashMD = class extends Hash {
276
+ var HashMD = class {
277
+ blockLen;
278
+ outputLen;
279
+ canXOF = false;
280
+ padOffset;
281
+ isLE;
282
+ buffer;
283
+ view;
284
+ finished = false;
285
+ length = 0;
286
+ pos = 0;
287
+ destroyed = false;
171
288
  constructor(blockLen, outputLen, padOffset, isLE) {
172
- super();
173
- this.finished = false;
174
- this.length = 0;
175
- this.pos = 0;
176
- this.destroyed = false;
177
289
  this.blockLen = blockLen;
178
290
  this.outputLen = outputLen;
179
291
  this.padOffset = padOffset;
@@ -183,7 +295,6 @@ var HashMD = class extends Hash {
183
295
  }
184
296
  update(data) {
185
297
  aexists(this);
186
- data = toBytes(data);
187
298
  abytes(data);
188
299
  const { view, buffer, blockLen } = this;
189
300
  const len = data.length;
@@ -219,11 +330,11 @@ var HashMD = class extends Hash {
219
330
  pos = 0;
220
331
  }
221
332
  for (let i = pos; i < blockLen; i++) buffer[i] = 0;
222
- setBigUint64(view, blockLen - 8, BigInt(this.length * 8), isLE);
333
+ view.setBigUint64(blockLen - 8, BigInt(this.length * 8), isLE);
223
334
  this.process(view, 0);
224
335
  const oview = createView(out);
225
336
  const len = this.outputLen;
226
- if (len % 4) throw new Error("_sha2: outputLen should be aligned to 32bit");
337
+ if (len % 4) throw new Error("_sha2: outputLen must be aligned to 32bit");
227
338
  const outLen = len / 4;
228
339
  const state = this.get();
229
340
  if (outLen > state.length) throw new Error("_sha2: outputLen bigger than state");
@@ -237,7 +348,7 @@ var HashMD = class extends Hash {
237
348
  return res;
238
349
  }
239
350
  _cloneInto(to) {
240
- to || (to = new this.constructor());
351
+ to ||= new this.constructor();
241
352
  to.set(...this.get());
242
353
  const { blockLen, buffer, length, finished, destroyed, pos } = this;
243
354
  to.destroyed = destroyed;
@@ -251,7 +362,10 @@ var HashMD = class extends Hash {
251
362
  return this._cloneInto();
252
363
  }
253
364
  };
254
- /** Initial SHA512 state. Bits 0..64 of frac part of sqrt of primes 2..19 */
365
+ /** Initial SHA512 state from RFC 6234 §6.3: eight RFC 64-bit `H(0)` words stored as sixteen
366
+ * big-endian 32-bit halves. Derived from the fractional parts of the square roots of the first
367
+ * eight prime numbers. Exported as a shared table; callers must treat it as read-only because
368
+ * constructors copy halves from it by index. */
255
369
  const SHA512_IV = /* @__PURE__ */ Uint32Array.from([
256
370
  1779033703,
257
371
  4089235720,
@@ -272,12 +386,7 @@ const SHA512_IV = /* @__PURE__ */ Uint32Array.from([
272
386
  ]);
273
387
 
274
388
  //#endregion
275
- //#region packages/cli/node_modules/@noble/hashes/esm/_u64.js
276
- /**
277
- * Internal helpers for u64. BigUint64Array is too slow as per 2025, so we implement it using Uint32Array.
278
- * @todo re-check https://issues.chromium.org/issues/42212588
279
- * @module
280
- */
389
+ //#region node_modules/@noble/hashes/_u64.js
281
390
  const U32_MASK64 = /* @__PURE__ */ BigInt(2 ** 32 - 1);
282
391
  const _32n = /* @__PURE__ */ BigInt(32);
283
392
  function fromBig(n, le = false) {
@@ -321,12 +430,12 @@ const add5L = (Al, Bl, Cl, Dl, El) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl
321
430
  const add5H = (low, Ah, Bh, Ch, Dh, Eh) => Ah + Bh + Ch + Dh + Eh + (low / 2 ** 32 | 0) | 0;
322
431
 
323
432
  //#endregion
324
- //#region packages/cli/node_modules/@noble/hashes/esm/sha2.js
433
+ //#region node_modules/@noble/hashes/sha2.js
325
434
  /**
326
435
  * SHA2 hash function. A.k.a. sha256, sha384, sha512, sha512_224, sha512_256.
327
436
  * SHA256 is the fastest hash implementable in JS, even faster than Blake3.
328
- * Check out [RFC 4634](https://datatracker.ietf.org/doc/html/rfc4634) and
329
- * [FIPS 180-4](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf).
437
+ * Check out {@link https://www.rfc-editor.org/rfc/rfc4634 | RFC 4634} and
438
+ * {@link https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf | FIPS 180-4}.
330
439
  * @module
331
440
  */
332
441
  const K512 = split([
@@ -415,25 +524,10 @@ const SHA512_Kh = K512[0];
415
524
  const SHA512_Kl = K512[1];
416
525
  const SHA512_W_H = /* @__PURE__ */ new Uint32Array(80);
417
526
  const SHA512_W_L = /* @__PURE__ */ new Uint32Array(80);
418
- var SHA512 = class extends HashMD {
419
- constructor(outputLen = 64) {
527
+ /** Internal SHA-384 / SHA-512 compression engine from RFC 6234 §6.4. */
528
+ var SHA2_64B = class extends HashMD {
529
+ constructor(outputLen) {
420
530
  super(128, outputLen, 16, false);
421
- this.Ah = SHA512_IV[0] | 0;
422
- this.Al = SHA512_IV[1] | 0;
423
- this.Bh = SHA512_IV[2] | 0;
424
- this.Bl = SHA512_IV[3] | 0;
425
- this.Ch = SHA512_IV[4] | 0;
426
- this.Cl = SHA512_IV[5] | 0;
427
- this.Dh = SHA512_IV[6] | 0;
428
- this.Dl = SHA512_IV[7] | 0;
429
- this.Eh = SHA512_IV[8] | 0;
430
- this.El = SHA512_IV[9] | 0;
431
- this.Fh = SHA512_IV[10] | 0;
432
- this.Fl = SHA512_IV[11] | 0;
433
- this.Gh = SHA512_IV[12] | 0;
434
- this.Gl = SHA512_IV[13] | 0;
435
- this.Hh = SHA512_IV[14] | 0;
436
- this.Hl = SHA512_IV[15] | 0;
437
531
  }
438
532
  get() {
439
533
  const { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this;
@@ -536,12 +630,44 @@ var SHA512 = class extends HashMD {
536
630
  clean(SHA512_W_H, SHA512_W_L);
537
631
  }
538
632
  destroy() {
633
+ this.destroyed = true;
539
634
  clean(this.buffer);
540
635
  this.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
541
636
  }
542
637
  };
543
- /** SHA2-512 hash function from RFC 4634. */
544
- const sha512 = /* @__PURE__ */ createHasher(() => new SHA512());
638
+ /** Internal SHA-512 hash class grounded in RFC 6234 §6.3 and §6.4. */
639
+ var _SHA512 = class extends SHA2_64B {
640
+ Ah = SHA512_IV[0] | 0;
641
+ Al = SHA512_IV[1] | 0;
642
+ Bh = SHA512_IV[2] | 0;
643
+ Bl = SHA512_IV[3] | 0;
644
+ Ch = SHA512_IV[4] | 0;
645
+ Cl = SHA512_IV[5] | 0;
646
+ Dh = SHA512_IV[6] | 0;
647
+ Dl = SHA512_IV[7] | 0;
648
+ Eh = SHA512_IV[8] | 0;
649
+ El = SHA512_IV[9] | 0;
650
+ Fh = SHA512_IV[10] | 0;
651
+ Fl = SHA512_IV[11] | 0;
652
+ Gh = SHA512_IV[12] | 0;
653
+ Gl = SHA512_IV[13] | 0;
654
+ Hh = SHA512_IV[14] | 0;
655
+ Hl = SHA512_IV[15] | 0;
656
+ constructor() {
657
+ super(64);
658
+ }
659
+ };
660
+ /**
661
+ * SHA2-512 hash function from RFC 4634.
662
+ * @param msg - message bytes to hash
663
+ * @returns Digest bytes.
664
+ * @example
665
+ * Hash a message with SHA2-512.
666
+ * ```ts
667
+ * sha512(new Uint8Array([97, 98, 99]));
668
+ * ```
669
+ */
670
+ const sha512 = /* @__PURE__ */ createHasher(() => new _SHA512(), /* @__PURE__ */ oidNist(3));
545
671
 
546
672
  //#endregion
547
673
  //#region packages/cli/src/crypto.ts
@@ -549,7 +675,8 @@ const sha512 = /* @__PURE__ */ createHasher(() => new SHA512());
549
675
  * Ed25519 key generation, persistence, and challenge signing for CLI auth.
550
676
  * Mirrors @abraca/mcp/src/crypto.ts — standalone to avoid MCP SDK dependency.
551
677
  */
552
- _noble_ed25519.etc.sha512Sync = (...msgs) => sha512(_noble_ed25519.etc.concatBytes(...msgs));
678
+ _noble_ed25519.hashes.sha512 = sha512;
679
+ _noble_ed25519.hashes.sha512Async = (m) => Promise.resolve(sha512(m));
553
680
  const DEFAULT_KEY_PATH = (0, node_path.join)((0, node_os.homedir)(), ".abracadabra", "cli.key");
554
681
  function toBase64url(bytes) {
555
682
  return Buffer.from(bytes).toString("base64url");
@@ -572,7 +699,7 @@ async function loadOrCreateKeypair(keyPath) {
572
699
  publicKeyB64: toBase64url(_noble_ed25519.getPublicKey(privateKey))
573
700
  };
574
701
  }
575
- const privateKey = _noble_ed25519.utils.randomPrivateKey();
702
+ const privateKey = _noble_ed25519.utils.randomSecretKey();
576
703
  const publicKey = _noble_ed25519.getPublicKey(privateKey);
577
704
  const dir = (0, node_path.dirname)(path);
578
705
  if (!(0, node_fs.existsSync)(dir)) await (0, node_fs_promises.mkdir)(dir, {
@@ -614,24 +741,6 @@ function waitForSync(provider, timeoutMs = 15e3) {
614
741
  provider.on("synced", handler);
615
742
  });
616
743
  }
617
- /** Map a DocumentMeta to SpaceMeta shape for display compatibility. */
618
- function docToSpaceMeta(doc) {
619
- const publicAccess = doc.public_access;
620
- let visibility = "private";
621
- if (publicAccess && publicAccess !== "none") visibility = "public";
622
- return {
623
- id: doc.id,
624
- doc_id: doc.id,
625
- name: doc.label ?? doc.id,
626
- description: doc.description ?? null,
627
- visibility,
628
- is_hub: doc.is_hub ?? false,
629
- owner_id: doc.owner_id ?? null,
630
- created_at: 0,
631
- updated_at: doc.updated_at ?? 0,
632
- public_access: publicAccess ?? null
633
- };
634
- }
635
744
  var CLIConnection = class {
636
745
  config;
637
746
  client;
@@ -701,29 +810,12 @@ var CLIConnection = class {
701
810
  }
702
811
  this.log(`Authenticated as ${this.displayName} (${keypair.publicKeyB64.slice(0, 12)}...)`);
703
812
  this._serverInfo = await this.client.serverInfo();
704
- let initialDocId = this._serverInfo.index_doc_id ?? null;
705
- try {
706
- const roots = await this.client.listRootDocuments();
707
- this._spaces = roots.map(docToSpaceMeta);
708
- const hub = roots.find((d) => d.is_hub);
709
- if (hub) {
710
- initialDocId = hub.id;
711
- this.log(`Hub document: ${hub.label ?? hub.id} (${hub.id})`);
712
- } else if (roots.length > 0) {
713
- initialDocId = roots[0].id;
714
- this.log(`No hub, using first root doc: ${roots[0].label ?? roots[0].id}`);
715
- }
716
- } catch {
717
- try {
718
- this._spaces = await this.client.listSpaces();
719
- const hub = this._spaces.find((s) => s.is_hub);
720
- if (hub) initialDocId = hub.doc_id;
721
- else if (this._spaces.length > 0) initialDocId = this._spaces[0].doc_id;
722
- } catch {
723
- this.log("Neither /docs?root=true nor /spaces available, using index_doc_id");
724
- }
725
- }
726
- if (!initialDocId) throw new Error("No entry point found: server has neither spaces nor index_doc_id configured.");
813
+ const roots = await this.client.listChildren();
814
+ this._spaces = roots.filter((d) => d.kind === _abraca_dabra.Kind.Space);
815
+ const first = this._spaces[0] ?? roots[0];
816
+ const initialDocId = first?.id ?? null;
817
+ if (first) this.log(`Entry document: ${first.label ?? first.id} (${first.id})`);
818
+ if (!initialDocId) throw new Error("No entry point found: server has no top-level documents. Create a Space first.");
727
819
  this._rootDocId = initialDocId;
728
820
  const doc = new yjs.Doc({ guid: initialDocId });
729
821
  const provider = new _abraca_dabra.AbracadabraProvider({
@@ -1098,7 +1190,7 @@ registerCommand({
1098
1190
  `URL: ${conn.config.url}`,
1099
1191
  `Version: ${si.version ?? "—"}`,
1100
1192
  `Protocol: ${si.protocol_version ?? "—"}`,
1101
- `Hub Doc: ${conn.rootDocId ?? "—"}`,
1193
+ `Active Doc: ${conn.rootDocId ?? "—"}`,
1102
1194
  `Auth: ${si.auth_methods?.join(", ") ?? "—"}`,
1103
1195
  `Registration:${si.registration_allowed ? " open" : " closed"}${si.invite_only ? " (invite only)" : ""}`,
1104
1196
  `Documents: ${docCount}`,
@@ -1108,7 +1200,7 @@ registerCommand({
1108
1200
  });
1109
1201
  registerCommand({
1110
1202
  name: "spaces",
1111
- description: "List available spaces/root documents.",
1203
+ description: "List available spaces (top-level documents).",
1112
1204
  usage: "spaces [--format=json|tsv]",
1113
1205
  async run(conn, args) {
1114
1206
  if (!conn) return "Not connected";
@@ -1118,19 +1210,20 @@ registerCommand({
1118
1210
  const active = conn.rootDocId;
1119
1211
  if (format === "json") return printJson(spaces.map((s) => ({
1120
1212
  ...s,
1121
- active: s.doc_id === active
1213
+ active: s.id === active
1122
1214
  })));
1123
- return printTable(spaces.map((s) => [
1124
- s.doc_id === active ? "" : " ",
1125
- s.doc_id.slice(0, 8) + "…",
1126
- s.name,
1127
- s.is_hub ? "hub" : "",
1128
- s.visibility
1129
- ]), [
1215
+ return printTable(spaces.map((s) => {
1216
+ const visibility = s.public_access === "observer" ? "public" : "private";
1217
+ return [
1218
+ s.id === active ? "▸" : " ",
1219
+ s.id.slice(0, 8) + "",
1220
+ s.label ?? s.id,
1221
+ visibility
1222
+ ];
1223
+ }), [
1130
1224
  "",
1131
1225
  "ID",
1132
1226
  "NAME",
1133
- "HUB",
1134
1227
  "VISIBILITY"
1135
1228
  ]);
1136
1229
  }
@@ -1148,12 +1241,12 @@ registerCommand({
1148
1241
  if (targetId) docId = targetId;
1149
1242
  else if (targetName) {
1150
1243
  const lower = targetName.toLowerCase();
1151
- const space = conn.spaces.find((s) => s.name.toLowerCase() === lower || s.doc_id === targetName);
1152
- if (space) docId = space.doc_id;
1244
+ const space = conn.spaces.find((s) => (s.label ?? s.id).toLowerCase() === lower || s.id === targetName);
1245
+ if (space) docId = space.id;
1153
1246
  }
1154
1247
  if (!docId) return "Space not found. Use \"abracadabra spaces\" to list available spaces.";
1155
1248
  await conn.switchSpace(docId);
1156
- return `Switched to space "${conn.spaces.find((s) => s.doc_id === docId)?.name ?? docId}"`;
1249
+ return `Switched to space "${conn.spaces.find((s) => s.id === docId)?.label ?? docId}"`;
1157
1250
  }
1158
1251
  });
1159
1252
 
@@ -1184,7 +1277,7 @@ registerCommand({
1184
1277
  const tree = buildTree(readEntries(treeMap), rootId, maxDepth);
1185
1278
  if (format === "json") return printJson(tree);
1186
1279
  if (tree.length === 0) return "(empty tree)";
1187
- return (conn.spaces.find((s) => s.doc_id === conn.rootDocId)?.name ?? conn.rootDocId ?? "Workspace") + "\n" + printTree(toTreeNodes(tree));
1280
+ return (conn.spaces.find((s) => s.id === conn.rootDocId)?.label ?? conn.rootDocId ?? "Workspace") + "\n" + printTree(toTreeNodes(tree));
1188
1281
  }
1189
1282
  });
1190
1283
  registerCommand({
@@ -1256,247 +1349,14 @@ registerCommand({
1256
1349
  });
1257
1350
 
1258
1351
  //#endregion
1259
- //#region packages/mcp/src/converters/yjsToMarkdown.ts
1260
- /**
1261
- * Y.XmlFragment → Markdown serializer.
1262
- * Walks the TipTap document structure and produces markdown text.
1263
- */
1264
- function deltaToMarkdown(delta) {
1265
- return delta.map((op) => {
1266
- let text = op.insert;
1267
- if (!op.attributes) return text;
1268
- const a = op.attributes;
1269
- if (a.code) text = `\`${text}\``;
1270
- if (a.bold) text = `**${text}**`;
1271
- if (a.italic) text = `*${text}*`;
1272
- if (a.strike) text = `~~${text}~~`;
1273
- if (a.link?.href) text = `[${text}](${a.link.href})`;
1274
- if (a.badge) text = `:badge[${a.badge.label || text}]`;
1275
- if (a.kbd) text = `:kbd{value="${a.kbd.value || text}"}`;
1276
- if (a.proseIcon) text = `:icon{name="${a.proseIcon.name}"}`;
1277
- return text;
1278
- }).join("");
1279
- }
1280
- function xmlTextToMarkdown(xmlText) {
1281
- return deltaToMarkdown(xmlText.toDelta());
1282
- }
1283
- function elementTextContent(el) {
1284
- const parts = [];
1285
- for (let i = 0; i < el.length; i++) {
1286
- const child = el.get(i);
1287
- if (child instanceof yjs.XmlText) parts.push(xmlTextToMarkdown(child));
1288
- else if (child instanceof yjs.XmlElement) {
1289
- if (child.nodeName === "docLink") {
1290
- const docId = child.getAttribute("docId");
1291
- if (docId) parts.push(`[[${docId}]]`);
1292
- continue;
1293
- }
1294
- parts.push(elementTextContent(child));
1295
- }
1296
- }
1297
- return parts.join("");
1298
- }
1299
- function serializeElement(el, indent = "") {
1300
- switch (el.nodeName) {
1301
- case "documentHeader":
1302
- case "documentMeta": return "";
1303
- case "heading": {
1304
- const level = Number(el.getAttribute("level")) || 1;
1305
- return `${"#".repeat(level)} ${elementTextContent(el)}`;
1306
- }
1307
- case "paragraph": return elementTextContent(el);
1308
- case "bulletList": return serializeList(el, "bullet", indent);
1309
- case "orderedList": return serializeList(el, "ordered", indent);
1310
- case "taskList": return serializeTaskList(el, indent);
1311
- case "codeBlock": return `\`\`\`${el.getAttribute("language") || ""}\n${elementTextContent(el)}\n\`\`\``;
1312
- case "blockquote": {
1313
- const lines = [];
1314
- for (let i = 0; i < el.length; i++) {
1315
- const child = el.get(i);
1316
- if (child instanceof yjs.XmlElement) lines.push(serializeElement(child, indent));
1317
- }
1318
- return lines.map((l) => `> ${l}`).join("\n");
1319
- }
1320
- case "horizontalRule": return "---";
1321
- case "table": return serializeTable(el);
1322
- case "docEmbed": {
1323
- const docId = el.getAttribute("docId");
1324
- if (!docId) return "";
1325
- const seamlessAttr = el.getAttribute("seamless");
1326
- return seamlessAttr === true || seamlessAttr === "true" ? `![[${docId}]]{seamless}` : `![[${docId}]]`;
1327
- }
1328
- case "svgEmbed": {
1329
- const svg = el.getAttribute("svg") || "";
1330
- const svgTitle = el.getAttribute("title") || "";
1331
- if (!svg) return "";
1332
- return `\`\`\`svg${svgTitle ? ` ${svgTitle}` : ""}\n${svg}\n\`\`\``;
1333
- }
1334
- case "image": {
1335
- const src = el.getAttribute("src") || "";
1336
- const alt = el.getAttribute("alt") || "";
1337
- const w = el.getAttribute("width");
1338
- const h = el.getAttribute("height");
1339
- let attrs = "";
1340
- if (w || h) {
1341
- const parts = [];
1342
- if (w) parts.push(`width="${w}"`);
1343
- if (h) parts.push(`height="${h}"`);
1344
- attrs = `{${parts.join(" ")}}`;
1345
- }
1346
- return `![${alt}](${src})${attrs}`;
1347
- }
1348
- case "callout": return `::${el.getAttribute("type") || "note"}\n${serializeChildren(el, indent)}\n::`;
1349
- case "collapsible": {
1350
- const label = el.getAttribute("label") || "Details";
1351
- const open = el.getAttribute("open");
1352
- const props = [`label="${label}"`];
1353
- if (open === true || open === "true") props.push("open=\"true\"");
1354
- const inner = serializeChildren(el, indent);
1355
- return `::collapsible{${props.join(" ")}}\n${inner}\n::`;
1356
- }
1357
- case "steps": return `::steps\n${serializeChildren(el, indent)}\n::`;
1358
- case "card": {
1359
- const title = el.getAttribute("title") || "";
1360
- const icon = el.getAttribute("icon") || "";
1361
- const to = el.getAttribute("to") || "";
1362
- const props = [];
1363
- if (title) props.push(`title="${title}"`);
1364
- if (icon) props.push(`icon="${icon}"`);
1365
- if (to) props.push(`to="${to}"`);
1366
- const inner = serializeChildren(el, indent);
1367
- return `::card{${props.join(" ")}}\n${inner}\n::`;
1368
- }
1369
- case "cardGroup": return `::card-group\n${serializeChildren(el, indent)}\n::`;
1370
- case "codeCollapse": return `::code-collapse\n${serializeChildren(el, indent)}\n::`;
1371
- case "codeGroup": return `::code-group\n${serializeChildren(el, indent)}\n::`;
1372
- case "codePreview": return `::code-preview\n${serializeChildren(el, indent)}\n::`;
1373
- case "codeTree": return `::code-tree{files="${el.getAttribute("files") || "[]"}"}\n::`;
1374
- case "accordion": return serializeSlottedComponent(el, "accordion", "accordionItem", "item");
1375
- case "tabs": return serializeSlottedComponent(el, "tabs", "tabsItem", "tab");
1376
- case "field": {
1377
- const fieldName = el.getAttribute("name") || "";
1378
- const fieldType = el.getAttribute("type") || "string";
1379
- const required = el.getAttribute("required");
1380
- const props = [];
1381
- if (fieldName) props.push(`name="${fieldName}"`);
1382
- props.push(`type="${fieldType}"`);
1383
- if (required === true || required === "true") props.push("required=\"true\"");
1384
- const inner = serializeChildren(el, indent);
1385
- return `::field{${props.join(" ")}}\n${inner}\n::`;
1386
- }
1387
- case "fieldGroup": return `::field-group\n${serializeChildren(el, indent)}\n::`;
1388
- default: return serializeChildren(el, indent);
1389
- }
1390
- }
1391
- function serializeList(el, type, indent) {
1392
- const lines = [];
1393
- for (let i = 0; i < el.length; i++) {
1394
- const item = el.get(i);
1395
- if (item instanceof yjs.XmlElement && item.nodeName === "listItem") {
1396
- const prefix = type === "bullet" ? "- " : `${i + 1}. `;
1397
- const content = elementTextContent(item);
1398
- lines.push(`${indent}${prefix}${content}`);
1399
- }
1400
- }
1401
- return lines.join("\n");
1402
- }
1403
- function serializeTaskList(el, indent) {
1404
- const lines = [];
1405
- for (let i = 0; i < el.length; i++) {
1406
- const item = el.get(i);
1407
- if (item instanceof yjs.XmlElement && item.nodeName === "taskItem") {
1408
- const checked = item.getAttribute("checked");
1409
- const marker = checked === true || checked === "true" ? "[x]" : "[ ]";
1410
- const content = elementTextContent(item);
1411
- lines.push(`${indent}- ${marker} ${content}`);
1412
- }
1413
- }
1414
- return lines.join("\n");
1415
- }
1416
- function serializeTable(el) {
1417
- const rows = [];
1418
- for (let i = 0; i < el.length; i++) {
1419
- const row = el.get(i);
1420
- if (!(row instanceof yjs.XmlElement) || row.nodeName !== "tableRow") continue;
1421
- const cells = [];
1422
- for (let j = 0; j < row.length; j++) {
1423
- const cell = row.get(j);
1424
- if (cell instanceof yjs.XmlElement) cells.push(elementTextContent(cell));
1425
- }
1426
- rows.push(cells);
1427
- }
1428
- if (!rows.length) return "";
1429
- const headerRow = rows[0];
1430
- const lines = [`| ${headerRow.join(" | ")} |`];
1431
- lines.push(`| ${headerRow.map(() => "---").join(" | ")} |`);
1432
- for (let i = 1; i < rows.length; i++) lines.push(`| ${rows[i].join(" | ")} |`);
1433
- return lines.join("\n");
1434
- }
1435
- function serializeSlottedComponent(el, componentName, childNodeName, slotName) {
1436
- const parts = [`::${componentName}`];
1437
- for (let i = 0; i < el.length; i++) {
1438
- const child = el.get(i);
1439
- if (!(child instanceof yjs.XmlElement) || child.nodeName !== childNodeName) continue;
1440
- const label = child.getAttribute("label") || `Item ${i + 1}`;
1441
- const icon = child.getAttribute("icon") || "";
1442
- const props = [`label="${label}"`];
1443
- if (icon) props.push(`icon="${icon}"`);
1444
- parts.push(`#${slotName}{${props.join(" ")}}`);
1445
- const inner = serializeChildren(child, "");
1446
- if (inner) parts.push(inner);
1447
- }
1448
- parts.push("::");
1449
- return parts.join("\n");
1450
- }
1451
- function serializeChildren(el, indent) {
1452
- const parts = [];
1453
- for (let i = 0; i < el.length; i++) {
1454
- const child = el.get(i);
1455
- if (child instanceof yjs.XmlElement) {
1456
- const serialized = serializeElement(child, indent);
1457
- if (serialized) parts.push(serialized);
1458
- } else if (child instanceof yjs.XmlText) {
1459
- const text = xmlTextToMarkdown(child);
1460
- if (text) parts.push(text);
1461
- }
1462
- }
1463
- return parts.join("\n\n");
1464
- }
1465
- /**
1466
- * Converts a Y.XmlFragment (TipTap document) to markdown.
1467
- * Extracts the title from the documentHeader element.
1468
- *
1469
- * @returns `{ title, markdown }` where title is the H1/header text
1470
- */
1471
- function yjsToMarkdown(fragment) {
1472
- let title = "Untitled";
1473
- const bodyParts = [];
1474
- for (let i = 0; i < fragment.length; i++) {
1475
- const child = fragment.get(i);
1476
- if (!(child instanceof yjs.XmlElement)) continue;
1477
- if (child.nodeName === "documentHeader") {
1478
- title = elementTextContent(child) || "Untitled";
1479
- continue;
1480
- }
1481
- if (child.nodeName === "documentMeta") continue;
1482
- const serialized = serializeElement(child);
1483
- if (serialized !== "") bodyParts.push(serialized);
1484
- }
1485
- return {
1486
- title,
1487
- markdown: bodyParts.join("\n\n")
1488
- };
1489
- }
1490
-
1491
- //#endregion
1492
- //#region packages/mcp/src/converters/markdownToYjs.ts
1493
- /**
1494
- * Markdown → Y.js converter.
1495
- * Ported from cou-sh/app/utils/markdownToYjs.ts with Vue dependency removed.
1496
- */
1352
+ //#region packages/convert/src/markdown-to-yjs.ts
1497
1353
  function parseInlineArray(raw) {
1498
1354
  return raw.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
1499
1355
  }
1356
+ function stripQuotes(s) {
1357
+ if (s.length >= 2 && (s.startsWith("\"") && s.endsWith("\"") || s.startsWith("'") && s.endsWith("'"))) return s.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
1358
+ return s;
1359
+ }
1500
1360
  function parseFrontmatter(markdown) {
1501
1361
  const noResult = {
1502
1362
  meta: {},
@@ -1527,8 +1387,8 @@ function parseFrontmatter(markdown) {
1527
1387
  if (kvMatch) {
1528
1388
  const key = kvMatch[1];
1529
1389
  const val = kvMatch[2].trim();
1530
- if (val.startsWith("[") && val.endsWith("]")) raw[key] = parseInlineArray(val);
1531
- else raw[key] = val;
1390
+ if (val.startsWith("[") && val.endsWith("]")) raw[key] = parseInlineArray(val).map(stripQuotes);
1391
+ else raw[key] = stripQuotes(val);
1532
1392
  }
1533
1393
  i++;
1534
1394
  }
@@ -1555,54 +1415,27 @@ function parseFrontmatter(markdown) {
1555
1415
  }[priorityRaw.toLowerCase()] ?? (Number(priorityRaw) || 0);
1556
1416
  const checkedRaw = raw["checked"] ?? raw["done"];
1557
1417
  if (checkedRaw !== void 0) meta.checked = checkedRaw === "true" || checkedRaw === true;
1558
- const dateStart = getStr(["date", "created"]);
1418
+ const dateStart = getStr([
1419
+ "dateStart",
1420
+ "date",
1421
+ "created"
1422
+ ]);
1559
1423
  if (dateStart) meta.dateStart = dateStart;
1560
- const dateEnd = getStr(["due"]);
1424
+ const dateEnd = getStr(["dateEnd", "due"]);
1561
1425
  if (dateEnd) meta.dateEnd = dateEnd;
1562
- const subtitle = getStr(["description", "subtitle"]);
1426
+ const subtitle = getStr(["subtitle", "description"]);
1563
1427
  if (subtitle) meta.subtitle = subtitle;
1564
1428
  const url = getStr(["url"]);
1565
1429
  if (url) meta.url = url;
1566
- const email = getStr(["email"]);
1567
- if (email) meta.email = email;
1568
- const phone = getStr(["phone"]);
1569
- if (phone) meta.phone = phone;
1570
1430
  const ratingRaw = getStr(["rating"]);
1571
1431
  if (ratingRaw !== void 0) {
1572
1432
  const n = Number(ratingRaw);
1573
1433
  if (!Number.isNaN(n)) meta.rating = Math.min(5, Math.max(0, n));
1574
1434
  }
1575
- const datetimeStart = getStr(["datetimeStart"]);
1576
- if (datetimeStart) meta.datetimeStart = datetimeStart;
1577
- const datetimeEnd = getStr(["datetimeEnd"]);
1578
- if (datetimeEnd) meta.datetimeEnd = datetimeEnd;
1579
- const allDayRaw = raw["allDay"];
1580
- if (allDayRaw !== void 0) meta.allDay = allDayRaw === "true" || allDayRaw === true;
1581
- const geoLatRaw = getStr(["geoLat"]);
1582
- if (geoLatRaw !== void 0) {
1583
- const n = Number(geoLatRaw);
1584
- if (!Number.isNaN(n)) meta.geoLat = n;
1585
- }
1586
- const geoLngRaw = getStr(["geoLng"]);
1587
- if (geoLngRaw !== void 0) {
1588
- const n = Number(geoLngRaw);
1589
- if (!Number.isNaN(n)) meta.geoLng = n;
1590
- }
1591
- const geoType = getStr(["geoType"]);
1592
- if (geoType && (geoType === "marker" || geoType === "line" || geoType === "measure")) meta.geoType = geoType;
1593
- const geoDescription = getStr(["geoDescription"]);
1594
- if (geoDescription) meta.geoDescription = geoDescription;
1595
- const numberRaw = getStr(["number"]);
1596
- if (numberRaw !== void 0) {
1597
- const n = Number(numberRaw);
1598
- if (!Number.isNaN(n)) meta.number = n;
1599
- }
1600
- const unit = getStr(["unit"]);
1601
- if (unit) meta.unit = unit;
1602
- const note = getStr(["note"]);
1603
- if (note) meta.note = note;
1435
+ const rawTitle = typeof raw["title"] === "string" ? raw["title"] : void 0;
1604
1436
  return {
1605
- title: typeof raw["title"] === "string" ? raw["title"] : void 0,
1437
+ title: rawTitle !== void 0 ? stripQuotes(rawTitle) : void 0,
1438
+ type: getStr(["type"]),
1606
1439
  meta,
1607
1440
  body
1608
1441
  };
@@ -1610,66 +1443,79 @@ function parseFrontmatter(markdown) {
1610
1443
  function parseInline(text) {
1611
1444
  const stripped = text.replace(/\{lang="[^"]*"\}/g, "").replace(/:(?!badge|icon|kbd)(\w[\w-]*)\[([^\]]*)\](\{[^}]*\})?/g, "$2").replace(/:(?!badge|icon|kbd)(\w[\w-]*)(\{[^}]*\})/g, "");
1612
1445
  const tokens = [];
1613
- const re = /:badge\[([^\]]*)\](\{[^}]*\})?|:icon\{([^}]*)\}|:kbd\{([^}]*)\}|!?\[\[([^\]|]+?)(?:\|([^\]]+?))?\]\]|~~(.+?)~~|\*\*(.+?)\*\*|\*(.+?)\*|_(.+?)_|`(.+?)`|\[(.+?)\]\((.+?)\)/g;
1446
+ const re = /\$([^$\n]+?)\$|@\[([^\]]+?)\]\(user:([^)]+?)\)|:badge\[([^\]]*)\](\{[^}]*\})?|:icon\{([^}]*)\}|:kbd\{([^}]*)\}|\[\[([0-9a-fA-F-]{36})(?:\|([^\]]+?))?\]\]|~~(.+?)~~|\*\*(.+?)\*\*|\*(.+?)\*|_(.+?)_|`(.+?)`|\[(.+?)\]\((.+?)\)/g;
1614
1447
  let lastIndex = 0;
1615
1448
  let match;
1616
1449
  while ((match = re.exec(stripped)) !== null) {
1617
1450
  if (match.index > lastIndex) tokens.push({ text: stripped.slice(lastIndex, match.index) });
1618
- if (match[1] !== void 0) {
1619
- const badgeProps = parseMdcProps(match[2]);
1451
+ if (match[1] !== void 0) tokens.push({
1452
+ text: match[1],
1453
+ attrs: { mathInline: { expression: match[1] } }
1454
+ });
1455
+ else if (match[2] !== void 0 && match[3] !== void 0) tokens.push({
1456
+ text: match[2],
1457
+ attrs: { mention: {
1458
+ userId: match[3],
1459
+ label: match[2]
1460
+ } }
1461
+ });
1462
+ else if (match[4] !== void 0) {
1463
+ const badgeProps = parseMdcProps(match[5]);
1620
1464
  tokens.push({
1621
- text: match[1] || "Badge",
1465
+ text: match[4] || "Badge",
1622
1466
  attrs: { badge: {
1623
- label: match[1] || "Badge",
1467
+ label: match[4] || "Badge",
1624
1468
  color: badgeProps["color"] || "neutral",
1625
1469
  variant: badgeProps["variant"] || "subtle"
1626
1470
  } }
1627
1471
  });
1628
- } else if (match[3] !== void 0) {
1629
- const iconProps = parseMdcProps(`{${match[3]}}`);
1472
+ } else if (match[6] !== void 0) {
1473
+ const iconProps = parseMdcProps(`{${match[6]}}`);
1630
1474
  tokens.push({
1631
1475
  text: "​",
1632
1476
  attrs: { proseIcon: { name: iconProps["name"] || "i-lucide-star" } }
1633
1477
  });
1634
- } else if (match[4] !== void 0) {
1635
- const kbdProps = parseMdcProps(`{${match[4]}}`);
1478
+ } else if (match[7] !== void 0) {
1479
+ const kbdProps = parseMdcProps(`{${match[7]}}`);
1636
1480
  tokens.push({
1637
1481
  text: kbdProps["value"] || "",
1638
1482
  attrs: { kbd: { value: kbdProps["value"] || "" } }
1639
1483
  });
1640
- } else if (match[5] !== void 0) tokens.push({
1641
- text: "",
1642
- node: "docLink",
1643
- nodeAttrs: { docId: match[5] }
1644
- });
1645
- else if (match[7] !== void 0) tokens.push({
1646
- text: match[7],
1484
+ } else if (match[8] !== void 0) {
1485
+ const docId = match[8];
1486
+ const label = match[9] ?? docId;
1487
+ tokens.push({
1488
+ text: label,
1489
+ attrs: { docLink: { docId } }
1490
+ });
1491
+ } else if (match[10] !== void 0) tokens.push({
1492
+ text: match[10],
1647
1493
  attrs: { strike: true }
1648
1494
  });
1649
- else if (match[8] !== void 0) tokens.push({
1650
- text: match[8],
1495
+ else if (match[11] !== void 0) tokens.push({
1496
+ text: match[11],
1651
1497
  attrs: { bold: true }
1652
1498
  });
1653
- else if (match[9] !== void 0) tokens.push({
1654
- text: match[9],
1499
+ else if (match[12] !== void 0) tokens.push({
1500
+ text: match[12],
1655
1501
  attrs: { italic: true }
1656
1502
  });
1657
- else if (match[10] !== void 0) tokens.push({
1658
- text: match[10],
1503
+ else if (match[13] !== void 0) tokens.push({
1504
+ text: match[13],
1659
1505
  attrs: { italic: true }
1660
1506
  });
1661
- else if (match[11] !== void 0) tokens.push({
1662
- text: match[11],
1507
+ else if (match[14] !== void 0) tokens.push({
1508
+ text: match[14],
1663
1509
  attrs: { code: true }
1664
1510
  });
1665
- else if (match[12] !== void 0 && match[13] !== void 0) tokens.push({
1666
- text: match[12],
1667
- attrs: { link: { href: match[13] } }
1511
+ else if (match[15] !== void 0 && match[16] !== void 0) tokens.push({
1512
+ text: match[15],
1513
+ attrs: { link: { href: match[16] } }
1668
1514
  });
1669
1515
  lastIndex = match.index + match[0].length;
1670
1516
  }
1671
1517
  if (lastIndex < stripped.length) tokens.push({ text: stripped.slice(lastIndex) });
1672
- return tokens.filter((t) => t.node || t.text.length > 0);
1518
+ return tokens.filter((t) => t.text.length > 0);
1673
1519
  }
1674
1520
  function parseTableRow(line) {
1675
1521
  const parts = line.split("|");
@@ -1678,6 +1524,7 @@ function parseTableRow(line) {
1678
1524
  function isTableSeparator(line) {
1679
1525
  return /^\|[\s|:-]+\|$/.test(line.trim());
1680
1526
  }
1527
+ /** Extract fenced code blocks from MDC #code slot lines. */
1681
1528
  function extractFencedCode(lines) {
1682
1529
  const result = [];
1683
1530
  let i = 0;
@@ -1704,14 +1551,23 @@ function extractFencedCode(lines) {
1704
1551
  }
1705
1552
  return result;
1706
1553
  }
1554
+ /** Extract key="value" pairs from MDC prop syntax `{key="value" other="x"}` */
1707
1555
  function parseMdcProps(propsStr) {
1708
1556
  if (!propsStr) return {};
1709
1557
  const result = {};
1710
- const re = /(\w[\w-]*)="([^"]*)"/g;
1558
+ let s = propsStr.trim();
1559
+ if (s.startsWith("{") && s.endsWith("}")) s = s.slice(1, -1);
1560
+ const re = /(\w[\w-]*)(?:=(?:"([^"]*)"|([^\s"}]+)))?/g;
1711
1561
  let m;
1712
- while ((m = re.exec(propsStr)) !== null) result[m[1]] = m[2];
1562
+ while ((m = re.exec(s)) !== null) {
1563
+ const key = m[1];
1564
+ if (m[2] !== void 0) result[key] = m[2];
1565
+ else if (m[3] !== void 0) result[key] = m[3];
1566
+ else result[key] = "true";
1567
+ }
1713
1568
  return result;
1714
1569
  }
1570
+ /** Parse named child MDC blocks from inner lines (e.g. #item for accordion, #tab for tabs) */
1715
1571
  function parseMdcChildren(innerLines, slotPrefix) {
1716
1572
  const items = [];
1717
1573
  let current = null;
@@ -1743,6 +1599,95 @@ function parseMdcChildren(innerLines, slotPrefix) {
1743
1599
  }));
1744
1600
  }
1745
1601
  const TASK_RE = /^[-*+]\s+\[([ xX])\]\s+(.*)/;
1602
+ /**
1603
+ * Consume a list (bullet / ordered / task) starting at `start`. Indented
1604
+ * continuation lines and nested lists are captured into each item's
1605
+ * `innerBlocks` so the parse → serialise → parse cycle preserves tree
1606
+ * structure instead of flattening nested lists onto a single line.
1607
+ *
1608
+ * `indent` is the column of the item marker for the current list. A
1609
+ * nested list starts ≥2 columns deeper. Lines with less indent than
1610
+ * `indent` belong to the outer block and stop consumption.
1611
+ */
1612
+ function consumeList(lines, start, indent, kind) {
1613
+ const items = [];
1614
+ let i = start;
1615
+ while (i < lines.length) {
1616
+ const line = lines[i];
1617
+ if (line.trim() === "") {
1618
+ let j = i + 1;
1619
+ while (j < lines.length && lines[j].trim() === "") j++;
1620
+ if (j >= lines.length) break;
1621
+ const lookahead = lines[j];
1622
+ if (leadingSpaces(lookahead) < indent) break;
1623
+ if (!matchMarker(lookahead.slice(indent), kind)) break;
1624
+ i = j;
1625
+ continue;
1626
+ }
1627
+ const leading = leadingSpaces(line);
1628
+ if (leading < indent) break;
1629
+ if (leading > indent) break;
1630
+ const m = matchMarker(line.slice(indent), kind);
1631
+ if (!m) break;
1632
+ const item = { text: m.text };
1633
+ if (kind === "task") item.checked = m.checked;
1634
+ i++;
1635
+ const contLines = [];
1636
+ while (i < lines.length) {
1637
+ const next = lines[i];
1638
+ if (next.trim() === "") {
1639
+ let k = i + 1;
1640
+ while (k < lines.length && lines[k].trim() === "") k++;
1641
+ if (k >= lines.length) break;
1642
+ if (leadingSpaces(lines[k]) <= indent) break;
1643
+ contLines.push("");
1644
+ i++;
1645
+ continue;
1646
+ }
1647
+ const nextIndent = leadingSpaces(next);
1648
+ if (nextIndent <= indent) break;
1649
+ const deindentBy = Math.min(nextIndent, indent + 2);
1650
+ contLines.push(next.slice(deindentBy));
1651
+ i++;
1652
+ }
1653
+ if (contLines.length > 0) item.innerBlocks = parseBlocks(contLines.join("\n"));
1654
+ items.push(item);
1655
+ }
1656
+ return {
1657
+ items,
1658
+ next: i
1659
+ };
1660
+ }
1661
+ function leadingSpaces(s) {
1662
+ let n = 0;
1663
+ while (n < s.length && s[n] === " ") n++;
1664
+ return n;
1665
+ }
1666
+ function matchMarker(s, kind) {
1667
+ if (kind === "task") {
1668
+ const m = s.match(TASK_RE);
1669
+ if (!m) return null;
1670
+ return {
1671
+ text: m[2],
1672
+ checked: m[1].toLowerCase() === "x"
1673
+ };
1674
+ }
1675
+ if (kind === "bullet") {
1676
+ if (TASK_RE.test(s)) return null;
1677
+ const m = s.match(/^[-*+]\s+(.*)$/);
1678
+ if (!m) return null;
1679
+ return {
1680
+ text: m[1],
1681
+ checked: false
1682
+ };
1683
+ }
1684
+ const m = s.match(/^\d+\.\s+(.*)$/);
1685
+ if (!m) return null;
1686
+ return {
1687
+ text: m[1],
1688
+ checked: false
1689
+ };
1690
+ }
1746
1691
  function parseBlocks(markdown) {
1747
1692
  const rawLines = markdown.split("\n");
1748
1693
  let firstContentLine = 0;
@@ -1768,17 +1713,15 @@ function parseBlocks(markdown) {
1768
1713
  i++;
1769
1714
  }
1770
1715
  i++;
1771
- if (lang === "svg" || lang.startsWith("svg ")) {
1772
- const svgTitle = lang === "svg" ? "" : lang.slice(4).trim();
1773
- blocks.push({
1774
- type: "svgEmbed",
1775
- svg: codeLines.join("\n"),
1776
- title: svgTitle
1777
- });
1778
- } else blocks.push({
1716
+ const code = codeLines.join("\n");
1717
+ if (lang === "math") blocks.push({
1718
+ type: "mathBlock",
1719
+ expression: code
1720
+ });
1721
+ else blocks.push({
1779
1722
  type: "codeBlock",
1780
1723
  lang,
1781
- code: codeLines.join("\n")
1724
+ code
1782
1725
  });
1783
1726
  continue;
1784
1727
  }
@@ -1797,14 +1740,16 @@ function parseBlocks(markdown) {
1797
1740
  i++;
1798
1741
  continue;
1799
1742
  }
1800
- const docEmbedMatch = line.match(/^!\[\[([^\]|]+?)(?:\|[^\]]*?)?\]\](\{[^}]*\})?\s*$/);
1801
- if (docEmbedMatch) {
1802
- const props = parseMdcProps(docEmbedMatch[2]);
1803
- const seamless = "seamless" in props || props["seamless"] === "true" || /\{[^}]*\bseamless\b[^}]*\}/.test(docEmbedMatch[2] ?? "");
1743
+ const embedMatch = line.match(/^!\[\[([0-9a-fA-F-]{36})(?:\|([^\]]+))?\]\](\{[^}]*\})?\s*$/);
1744
+ if (embedMatch) {
1745
+ const docId = embedMatch[1];
1746
+ const label = embedMatch[2] ?? "";
1747
+ const props = parseMdcProps(embedMatch[3]);
1804
1748
  blocks.push({
1805
1749
  type: "docEmbed",
1806
- docId: docEmbedMatch[1],
1807
- seamless: seamless || void 0
1750
+ docId,
1751
+ label,
1752
+ props
1808
1753
  });
1809
1754
  i++;
1810
1755
  continue;
@@ -1856,6 +1801,23 @@ function parseBlocks(markdown) {
1856
1801
  });
1857
1802
  continue;
1858
1803
  }
1804
+ const atomMatch = line.match(/^:(\w[\w-]*)(\{[^}]*\})?\s*$/);
1805
+ if (atomMatch && atomMatch[1] === "file") {
1806
+ const props = parseMdcProps(atomMatch[2]);
1807
+ const uploadId = props["upload-id"] ?? props["uploadId"] ?? "";
1808
+ const filename = props["filename"] ?? "";
1809
+ const mime = props["mime"] ?? "";
1810
+ const src = props["src"] ?? (uploadId && filename ? `.abracadabra/files/${uploadId}-${filename}` : "");
1811
+ blocks.push({
1812
+ type: "fileBlock",
1813
+ src,
1814
+ mime,
1815
+ uploadId,
1816
+ filename
1817
+ });
1818
+ i++;
1819
+ continue;
1820
+ }
1859
1821
  const MDC_OPEN = /^\s*(:{2,})(\w[\w-]*)(\{[^}]*\})?\s*$/;
1860
1822
  if (MDC_OPEN.test(line)) {
1861
1823
  const colons = line.match(/^\s*(:+)/)?.[1]?.length ?? 2;
@@ -2024,15 +1986,8 @@ function parseBlocks(markdown) {
2024
1986
  continue;
2025
1987
  }
2026
1988
  if (TASK_RE.test(line)) {
2027
- const items = [];
2028
- while (i < lines.length && TASK_RE.test(lines[i])) {
2029
- const m = lines[i].match(TASK_RE);
2030
- items.push({
2031
- checked: m[1].toLowerCase() === "x",
2032
- text: m[2]
2033
- });
2034
- i++;
2035
- }
1989
+ const { items, next } = consumeList(lines, i, 0, "task");
1990
+ i = next;
2036
1991
  blocks.push({
2037
1992
  type: "taskList",
2038
1993
  items
@@ -2040,12 +1995,9 @@ function parseBlocks(markdown) {
2040
1995
  continue;
2041
1996
  }
2042
1997
  if (/^[-*+]\s+/.test(line)) {
2043
- const items = [];
2044
- while (i < lines.length && /^[-*+]\s+/.test(lines[i]) && !TASK_RE.test(lines[i])) {
2045
- items.push(lines[i].replace(/^[-*+]\s+/, ""));
2046
- i++;
2047
- }
2048
- if (items.length) {
1998
+ const { items, next } = consumeList(lines, i, 0, "bullet");
1999
+ if (items.length > 0) {
2000
+ i = next;
2049
2001
  blocks.push({
2050
2002
  type: "bulletList",
2051
2003
  items
@@ -2054,16 +2006,15 @@ function parseBlocks(markdown) {
2054
2006
  }
2055
2007
  }
2056
2008
  if (/^\d+\.\s+/.test(line)) {
2057
- const items = [];
2058
- while (i < lines.length && /^\d+\.\s+/.test(lines[i])) {
2059
- items.push(lines[i].replace(/^\d+\.\s+/, ""));
2060
- i++;
2009
+ const { items, next } = consumeList(lines, i, 0, "ordered");
2010
+ if (items.length > 0) {
2011
+ i = next;
2012
+ blocks.push({
2013
+ type: "orderedList",
2014
+ items
2015
+ });
2016
+ continue;
2061
2017
  }
2062
- blocks.push({
2063
- type: "orderedList",
2064
- items
2065
- });
2066
- continue;
2067
2018
  }
2068
2019
  if (line.trim() === "") {
2069
2020
  i++;
@@ -2081,23 +2032,18 @@ function parseBlocks(markdown) {
2081
2032
  }
2082
2033
  return blocks;
2083
2034
  }
2035
+ /**
2036
+ * Insert formatted inline tokens into an already-attached Y.XmlElement.
2037
+ * Creates one Y.XmlText per token (attach first, fill second).
2038
+ */
2084
2039
  function fillTextInto(el, tokens) {
2085
- const filtered = tokens.filter((t) => t.node || t.text.length > 0);
2040
+ const filtered = tokens.filter((t) => t.text.length > 0);
2086
2041
  if (!filtered.length) return;
2087
- const children = filtered.map((tok) => {
2088
- if (tok.node) {
2089
- const xe = new yjs.XmlElement(tok.node);
2090
- if (tok.nodeAttrs) for (const [k, v] of Object.entries(tok.nodeAttrs)) xe.setAttribute(k, v);
2091
- return xe;
2092
- }
2093
- return new yjs.XmlText();
2094
- });
2095
- el.insert(0, children);
2042
+ const xtNodes = filtered.map(() => new yjs.XmlText());
2043
+ el.insert(0, xtNodes);
2096
2044
  filtered.forEach((tok, i) => {
2097
- if (tok.node) return;
2098
- const xt = children[i];
2099
- if (tok.attrs) xt.insert(0, tok.text, tok.attrs);
2100
- else xt.insert(0, tok.text);
2045
+ if (tok.attrs) xtNodes[i].insert(0, tok.text, tok.attrs);
2046
+ else xtNodes[i].insert(0, tok.text);
2101
2047
  });
2102
2048
  }
2103
2049
  function blockElName(b) {
@@ -2126,9 +2072,19 @@ function blockElName(b) {
2126
2072
  case "fieldGroup": return "fieldGroup";
2127
2073
  case "image": return "image";
2128
2074
  case "docEmbed": return "docEmbed";
2129
- case "svgEmbed": return "svgEmbed";
2075
+ case "mathBlock": return "mathBlock";
2076
+ case "fileBlock": return "fileBlock";
2130
2077
  }
2131
2078
  }
2079
+ function populateListItemChildren(itemEl, item, _itemKind) {
2080
+ const paraEl = new yjs.XmlElement("paragraph");
2081
+ itemEl.insert(itemEl.length, [paraEl]);
2082
+ fillTextInto(paraEl, parseInline(item.text));
2083
+ if (!item.innerBlocks?.length) return;
2084
+ const innerEls = item.innerBlocks.map((b) => new yjs.XmlElement(blockElName(b)));
2085
+ itemEl.insert(itemEl.length, innerEls);
2086
+ item.innerBlocks.forEach((b, i) => fillBlock(innerEls[i], b));
2087
+ }
2132
2088
  function fillBlock(el, block) {
2133
2089
  switch (block.type) {
2134
2090
  case "heading":
@@ -2142,10 +2098,8 @@ function fillBlock(el, block) {
2142
2098
  case "orderedList": {
2143
2099
  const listItemEls = block.items.map(() => new yjs.XmlElement("listItem"));
2144
2100
  el.insert(0, listItemEls);
2145
- block.items.forEach((text, i) => {
2146
- const paraEl = new yjs.XmlElement("paragraph");
2147
- listItemEls[i].insert(0, [paraEl]);
2148
- fillTextInto(paraEl, parseInline(text));
2101
+ block.items.forEach((item, i) => {
2102
+ populateListItemChildren(listItemEls[i], item, "listItem");
2149
2103
  });
2150
2104
  break;
2151
2105
  }
@@ -2153,10 +2107,8 @@ function fillBlock(el, block) {
2153
2107
  const taskItemEls = block.items.map(() => new yjs.XmlElement("taskItem"));
2154
2108
  el.insert(0, taskItemEls);
2155
2109
  block.items.forEach((item, i) => {
2156
- taskItemEls[i].setAttribute("checked", item.checked);
2157
- const paraEl = new yjs.XmlElement("paragraph");
2158
- taskItemEls[i].insert(0, [paraEl]);
2159
- fillTextInto(paraEl, parseInline(item.text));
2110
+ taskItemEls[i].setAttribute("checked", !!item.checked);
2111
+ populateListItemChildren(taskItemEls[i], item, "taskItem");
2160
2112
  });
2161
2113
  break;
2162
2114
  }
@@ -2344,32 +2296,55 @@ function fillBlock(el, block) {
2344
2296
  break;
2345
2297
  case "docEmbed":
2346
2298
  el.setAttribute("docId", block.docId);
2347
- if (block.seamless) el.setAttribute("seamless", "true");
2299
+ for (const flag of [
2300
+ "collapsed",
2301
+ "tall",
2302
+ "seamless"
2303
+ ]) if (block.props[flag] === "true" || block.props[flag] === "1") el.setAttribute(flag, true);
2348
2304
  break;
2349
- case "svgEmbed":
2350
- el.setAttribute("svg", block.svg);
2351
- if (block.title) el.setAttribute("title", block.title);
2305
+ case "mathBlock":
2306
+ el.setAttribute("expression", block.expression);
2307
+ break;
2308
+ case "fileBlock":
2309
+ if (block.src) el.setAttribute("src", block.src);
2310
+ if (block.mime) el.setAttribute("mime", block.mime);
2311
+ if (block.uploadId) el.setAttribute("uploadId", block.uploadId);
2312
+ if (block.filename) el.setAttribute("filename", block.filename);
2352
2313
  break;
2353
2314
  }
2354
2315
  }
2316
+ /**
2317
+ * Parses markdown text and writes the result into a Y.XmlFragment that
2318
+ * TipTap's Collaboration extension can read.
2319
+ *
2320
+ * Requires `fragment.doc` to be set (i.e. the fragment must already be
2321
+ * obtained from a live Y.Doc via `ydoc.getXmlFragment('default')`).
2322
+ *
2323
+ * @param fragment The target `Y.Doc.getXmlFragment('default')`
2324
+ * @param markdown Raw markdown string
2325
+ * @param fallbackTitle Used as the title when the markdown has no H1
2326
+ */
2355
2327
  function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled") {
2356
2328
  const ydoc = fragment.doc;
2357
2329
  if (!ydoc) {
2358
2330
  console.warn("[markdownToYjs] fragment has no doc — skipping population");
2359
2331
  return;
2360
2332
  }
2361
- const blocks = parseBlocks(markdown);
2333
+ const fm = parseFrontmatter(markdown);
2334
+ const blocks = parseBlocks(fm.body);
2362
2335
  let title = fallbackTitle;
2336
+ let titleSource;
2337
+ if (fm.title !== void 0) {
2338
+ title = fm.title;
2339
+ titleSource = "frontmatter";
2340
+ }
2363
2341
  let contentBlocks = blocks;
2364
2342
  const h1 = blocks.findIndex((b) => b.type === "heading" && b.level === 1);
2365
2343
  if (h1 !== -1) {
2366
2344
  title = blocks[h1].text;
2367
2345
  contentBlocks = blocks.filter((_, i) => i !== h1);
2346
+ titleSource = "h1";
2368
2347
  }
2369
- if (!contentBlocks.length) contentBlocks = [{
2370
- type: "paragraph",
2371
- text: ""
2372
- }];
2373
2348
  ydoc.transact(() => {
2374
2349
  const headerEl = new yjs.XmlElement("documentHeader");
2375
2350
  const metaEl = new yjs.XmlElement("documentMeta");
@@ -2399,7 +2374,8 @@ function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled"
2399
2374
  case "fieldGroup": return new yjs.XmlElement("fieldGroup");
2400
2375
  case "image": return new yjs.XmlElement("image");
2401
2376
  case "docEmbed": return new yjs.XmlElement("docEmbed");
2402
- case "svgEmbed": return new yjs.XmlElement("svgEmbed");
2377
+ case "mathBlock": return new yjs.XmlElement("mathBlock");
2378
+ case "fileBlock": return new yjs.XmlElement("fileBlock");
2403
2379
  }
2404
2380
  });
2405
2381
  fragment.insert(0, [
@@ -2407,53 +2383,1316 @@ function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled"
2407
2383
  metaEl,
2408
2384
  ...bodyEls
2409
2385
  ]);
2386
+ if (titleSource) headerEl.setAttribute("titleSource", titleSource);
2410
2387
  const headerXt = new yjs.XmlText();
2411
2388
  headerEl.insert(0, [headerXt]);
2412
2389
  headerXt.insert(0, title);
2390
+ for (const k of Object.keys(fm.meta)) {
2391
+ const v = fm.meta[k];
2392
+ if (v === void 0 || v === null) continue;
2393
+ metaEl.setAttribute(k, v);
2394
+ }
2395
+ if (fm.type) metaEl.setAttribute("type", fm.type);
2413
2396
  contentBlocks.forEach((block, i) => fillBlock(bodyEls[i], block));
2414
2397
  });
2415
2398
  }
2416
2399
 
2417
2400
  //#endregion
2418
- //#region packages/cli/src/commands/documents.ts
2419
- /**
2420
- * Document CRUD commands: read, create, rename, move, delete, type, doc.
2421
- */
2422
- /** Safely read a tree map value, converting Y.Map to plain object if needed. */
2423
- function toPlain$1(val) {
2424
- return val instanceof yjs.Map ? val.toJSON() : val;
2425
- }
2426
- registerCommand({
2427
- name: "doc",
2428
- aliases: ["info:doc"],
2429
- description: "Show document metadata (label, type, meta, dates).",
2430
- usage: "doc id=<docId> | name=<label> | path=<a/b/c>",
2431
- async run(conn, args) {
2432
- if (!conn) return "Not connected";
2433
- const docId = resolveDocument(conn, args.params, args.positional);
2434
- if (!docId) return "Document not found. Specify id=, name=, or path= to identify the document.";
2435
- const treeMap = conn.getTreeMap();
2436
- if (!treeMap) return "Not connected";
2437
- const raw = treeMap.get(docId);
2438
- if (!raw) return `Document ${docId} not found in tree.`;
2439
- const entry = toPlain$1(raw);
2440
- const lines = [
2441
- `id: ${docId}`,
2442
- `label: ${entry.label || "Untitled"}`,
2443
- `type: ${entry.type ?? "—"}`,
2444
- `parent: ${entry.parentId ?? "(root)"}`,
2445
- `order: ${entry.order ?? 0}`,
2446
- `created: ${entry.createdAt ? new Date(entry.createdAt).toISOString() : "—"}`,
2447
- `updated: ${entry.updatedAt ? new Date(entry.updatedAt).toISOString() : "—"} (${relativeTime(entry.updatedAt)})`
2448
- ];
2449
- if (entry.meta && Object.keys(entry.meta).length > 0) {
2450
- lines.push(`meta:`);
2451
- for (const [k, v] of Object.entries(entry.meta)) lines.push(` ${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`);
2401
+ //#region packages/convert/src/yjs-to-markdown.ts
2402
+ function serializeDelta(delta) {
2403
+ let result = "";
2404
+ for (const op of delta) {
2405
+ if (typeof op.insert !== "string") continue;
2406
+ let text = op.insert;
2407
+ const attrs = op.attributes ?? {};
2408
+ if (attrs.code) {
2409
+ result += `\`${text}\``;
2410
+ continue;
2452
2411
  }
2453
- const children = childrenOf(readEntries(treeMap), docId);
2454
- lines.push(`children: ${children.length}`);
2455
- return lines.join("\n");
2456
- }
2412
+ if (attrs.badge) {
2413
+ const b = attrs.badge;
2414
+ const props = [];
2415
+ if (b.color && b.color !== "neutral") props.push(`color="${b.color}"`);
2416
+ if (b.variant && b.variant !== "subtle") props.push(`variant="${b.variant}"`);
2417
+ result += `:badge[${b.label || text}]${props.length ? `{${props.join(" ")}}` : ""}`;
2418
+ continue;
2419
+ }
2420
+ if (attrs.proseIcon) {
2421
+ const icon = attrs.proseIcon.name || "i-lucide-star";
2422
+ result += `:icon{name="${icon}"}`;
2423
+ continue;
2424
+ }
2425
+ if (attrs.kbd) {
2426
+ const value = attrs.kbd.value || text;
2427
+ result += `:kbd{value="${value}"}`;
2428
+ continue;
2429
+ }
2430
+ if (attrs.docLink) {
2431
+ const docId = attrs.docLink.docId;
2432
+ if (docId) {
2433
+ result += text === docId ? `[[${docId}]]` : `[[${docId}|${text}]]`;
2434
+ continue;
2435
+ }
2436
+ }
2437
+ if (attrs.mention) {
2438
+ const { userId, label } = attrs.mention;
2439
+ if (userId) {
2440
+ result += `@[${label || text}](user:${userId})`;
2441
+ continue;
2442
+ }
2443
+ }
2444
+ if (attrs.mathInline) {
2445
+ const expr = attrs.mathInline.expression ?? text;
2446
+ result += `$${expr}$`;
2447
+ continue;
2448
+ }
2449
+ if (attrs.bold) text = `**${text}**`;
2450
+ if (attrs.italic) text = `*${text}*`;
2451
+ if (attrs.strike) text = `~~${text}~~`;
2452
+ if (attrs.link) {
2453
+ const href = attrs.link.href ?? "";
2454
+ text = `[${text}](${href})`;
2455
+ }
2456
+ result += text;
2457
+ }
2458
+ return result;
2459
+ }
2460
+ function serializeInline(el) {
2461
+ const parts = [];
2462
+ for (const child of el.toArray()) if (child instanceof yjs.XmlText) parts.push(serializeDelta(child.toDelta()));
2463
+ else if (child instanceof yjs.XmlElement) parts.push(serializeInline(child));
2464
+ return parts.join("");
2465
+ }
2466
+ function serializeBlock(el, indent = "") {
2467
+ if (el instanceof yjs.XmlText) return serializeDelta(el.toDelta());
2468
+ switch (el.nodeName) {
2469
+ case "documentHeader":
2470
+ case "documentMeta": return "";
2471
+ case "heading": {
2472
+ const level = Number(el.getAttribute("level") ?? 2);
2473
+ return `${"#".repeat(level)} ${serializeInline(el)}`;
2474
+ }
2475
+ case "paragraph": return serializeInline(el);
2476
+ case "bulletList": return serializeListItems(el, "bullet", indent);
2477
+ case "orderedList": return serializeListItems(el, "ordered", indent);
2478
+ case "taskList": return serializeTaskList(el, indent);
2479
+ case "codeBlock": {
2480
+ const lang = el.getAttribute("language") ?? "";
2481
+ const code = getCodeBlockText(el);
2482
+ if (code === "") return `\`\`\`${lang}\n\`\`\``;
2483
+ return `\`\`\`${lang}\n${code}\n\`\`\``;
2484
+ }
2485
+ case "blockquote": {
2486
+ const lines = [];
2487
+ for (const child of el.toArray()) if (child instanceof yjs.XmlElement) {
2488
+ const text = serializeBlock(child);
2489
+ for (const line of text.split("\n")) lines.push(`> ${line}`);
2490
+ }
2491
+ return lines.join("\n");
2492
+ }
2493
+ case "table": return serializeTable(el);
2494
+ case "horizontalRule": return "---";
2495
+ case "image": {
2496
+ const src = el.getAttribute("src") ?? "";
2497
+ const alt = el.getAttribute("alt") ?? "";
2498
+ const width = el.getAttribute("width");
2499
+ const height = el.getAttribute("height");
2500
+ const attrs = [];
2501
+ if (width) attrs.push(`width=${width}`);
2502
+ if (height) attrs.push(`height=${height}`);
2503
+ return `![${alt}](${src})${attrs.length ? `{${attrs.join(" ")}}` : ""}`;
2504
+ }
2505
+ case "docEmbed": {
2506
+ const docId = el.getAttribute("docId") ?? "";
2507
+ const collapsed = el.getAttribute("collapsed");
2508
+ const tall = el.getAttribute("tall");
2509
+ const seamless = el.getAttribute("seamless");
2510
+ const flags = [];
2511
+ if (collapsed === true || collapsed === "true") flags.push("collapsed");
2512
+ if (tall === true || tall === "true") flags.push("tall");
2513
+ if (seamless === true || seamless === "true") flags.push("seamless");
2514
+ return `![[${docId}]]${flags.length ? `{${flags.join(" ")}}` : ""}`;
2515
+ }
2516
+ case "mathBlock": return `\`\`\`math\n${el.getAttribute("expression") ?? ""}\n\`\`\``;
2517
+ case "fileBlock": {
2518
+ const uploadId = el.getAttribute("uploadId") ?? "";
2519
+ const filename = el.getAttribute("filename") ?? "";
2520
+ const mime = el.getAttribute("mime") ?? "";
2521
+ const src = el.getAttribute("src") ?? (uploadId && filename ? `.abracadabra/files/${uploadId}-${filename}` : "");
2522
+ const props = [];
2523
+ if (src) props.push(`src="${src}"`);
2524
+ if (mime) props.push(`mime="${mime}"`);
2525
+ if (uploadId) props.push(`upload-id="${uploadId}"`);
2526
+ if (filename) props.push(`filename="${filename}"`);
2527
+ return `:file{${props.join(" ")}}`;
2528
+ }
2529
+ case "callout": return `::${el.getAttribute("type") ?? "note"}\n${serializeChildren(el)}\n::`;
2530
+ case "collapsible": {
2531
+ const label = el.getAttribute("label") ?? "Details";
2532
+ const open = el.getAttribute("open");
2533
+ const props = [`label="${label}"`];
2534
+ if (open === true || open === "true") props.push("open=\"true\"");
2535
+ return `::collapsible{${props.join(" ")}}\n${serializeChildren(el)}\n::`;
2536
+ }
2537
+ case "steps": return `::steps\n${serializeChildren(el)}\n::`;
2538
+ case "card": {
2539
+ const props = [];
2540
+ const title = el.getAttribute("title");
2541
+ const icon = el.getAttribute("icon");
2542
+ const to = el.getAttribute("to");
2543
+ if (title) props.push(`title="${title}"`);
2544
+ if (icon) props.push(`icon="${icon}"`);
2545
+ if (to) props.push(`to="${to}"`);
2546
+ return `::card${props.length ? `{${props.join(" ")}}` : ""}\n${serializeChildren(el)}\n::`;
2547
+ }
2548
+ case "cardGroup": return `::card-group\n${el.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
2549
+ case "codeCollapse": return `::code-collapse\n${el.toArray().filter((c) => c instanceof yjs.XmlElement && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
2550
+ case "codeGroup": return `::code-group\n${el.toArray().filter((c) => c instanceof yjs.XmlElement && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
2551
+ case "codePreview": {
2552
+ const children = el.toArray().filter((c) => c instanceof yjs.XmlElement);
2553
+ const nonCode = children.filter((c) => c.nodeName !== "codeBlock").map((c) => serializeBlock(c)).join("\n\n");
2554
+ const code = children.filter((c) => c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n");
2555
+ const parts = [nonCode];
2556
+ if (code) parts.push(`#code\n${code}`);
2557
+ return `::code-preview\n${parts.filter(Boolean).join("\n\n")}\n::`;
2558
+ }
2559
+ case "codeTree": return `::code-tree{files="${el.getAttribute("files") ?? "[]"}"}\n::`;
2560
+ case "accordion": return serializeSlottedContainer(el, "accordion", "accordionItem", "item");
2561
+ case "tabs": return serializeSlottedContainer(el, "tabs", "tabsItem", "tab");
2562
+ case "field": {
2563
+ const fieldName = el.getAttribute("name") ?? "";
2564
+ const fieldType = el.getAttribute("type") ?? "string";
2565
+ const required = el.getAttribute("required");
2566
+ const props = [`name="${fieldName}"`, `type="${fieldType}"`];
2567
+ if (required === true || required === "true") props.push("required=\"true\"");
2568
+ return `::field{${props.join(" ")}}\n${serializeChildren(el)}\n::`;
2569
+ }
2570
+ case "fieldGroup": return `::field-group\n${el.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
2571
+ default: return serializeChildren(el);
2572
+ }
2573
+ }
2574
+ function serializeChildren(el) {
2575
+ const blocks = [];
2576
+ for (const child of el.toArray()) if (child instanceof yjs.XmlElement) {
2577
+ const text = serializeBlock(child);
2578
+ if (text) blocks.push(text);
2579
+ } else if (child instanceof yjs.XmlText) {
2580
+ const text = serializeDelta(child.toDelta());
2581
+ if (text) blocks.push(text);
2582
+ }
2583
+ return blocks.join("\n\n");
2584
+ }
2585
+ function serializeListItems(el, type, indent) {
2586
+ const lines = [];
2587
+ let counter = 1;
2588
+ for (const child of el.toArray()) {
2589
+ if (!(child instanceof yjs.XmlElement) || child.nodeName !== "listItem") continue;
2590
+ const prefix = type === "bullet" ? "- " : `${counter++}. `;
2591
+ const subParts = [];
2592
+ for (const sub of child.toArray()) {
2593
+ if (!(sub instanceof yjs.XmlElement)) continue;
2594
+ if (sub.nodeName === "bulletList") subParts.push(serializeListItems(sub, "bullet", indent + " "));
2595
+ else if (sub.nodeName === "orderedList") subParts.push(serializeListItems(sub, "ordered", indent + " "));
2596
+ else subParts.push(serializeInline(sub));
2597
+ }
2598
+ if (subParts.length <= 1) lines.push(`${indent}${prefix}${subParts[0] ?? ""}`);
2599
+ else {
2600
+ lines.push(`${indent}${prefix}${subParts[0] ?? ""}`);
2601
+ for (let i = 1; i < subParts.length; i++) lines.push(subParts[i]);
2602
+ }
2603
+ }
2604
+ return lines.join("\n");
2605
+ }
2606
+ function serializeTaskList(el, indent) {
2607
+ const lines = [];
2608
+ for (const child of el.toArray()) {
2609
+ if (!(child instanceof yjs.XmlElement) || child.nodeName !== "taskItem") continue;
2610
+ const checked = child.getAttribute("checked");
2611
+ const marker = checked === true || checked === "true" ? "[x]" : "[ ]";
2612
+ let header = "";
2613
+ const nestedParts = [];
2614
+ for (const sub of child.toArray()) {
2615
+ if (!(sub instanceof yjs.XmlElement)) continue;
2616
+ if (sub.nodeName === "paragraph" && header === "") header = serializeInline(sub);
2617
+ else if (sub.nodeName === "bulletList") nestedParts.push(serializeListItems(sub, "bullet", indent + " "));
2618
+ else if (sub.nodeName === "orderedList") nestedParts.push(serializeListItems(sub, "ordered", indent + " "));
2619
+ else if (sub.nodeName === "taskList") nestedParts.push(serializeTaskList(sub, indent + " "));
2620
+ else nestedParts.push(indent + " " + serializeBlock(sub, indent + " "));
2621
+ }
2622
+ lines.push(`${indent}- ${marker} ${header}`);
2623
+ for (const part of nestedParts) lines.push(part);
2624
+ }
2625
+ return lines.join("\n");
2626
+ }
2627
+ function getCodeBlockText(el) {
2628
+ for (const child of el.toArray()) if (child instanceof yjs.XmlText) return child.toString();
2629
+ return "";
2630
+ }
2631
+ function serializeTable(el) {
2632
+ const rows = el.toArray().filter((c) => c instanceof yjs.XmlElement);
2633
+ if (!rows.length) return "";
2634
+ const serializedRows = [];
2635
+ for (const row of rows) {
2636
+ const cells = row.toArray().filter((c) => c instanceof yjs.XmlElement).map((cell) => {
2637
+ return cell.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeInline(c)).join(" ");
2638
+ });
2639
+ serializedRows.push(cells);
2640
+ }
2641
+ if (!serializedRows.length) return "";
2642
+ const colCount = Math.max(...serializedRows.map((r) => r.length));
2643
+ const headerRow = serializedRows[0];
2644
+ const separator = Array(colCount).fill("---");
2645
+ const dataRows = serializedRows.slice(1);
2646
+ const formatRow = (cells) => {
2647
+ return `| ${Array(colCount).fill("").map((_, i) => cells[i] ?? "").join(" | ")} |`;
2648
+ };
2649
+ return [
2650
+ formatRow(headerRow),
2651
+ formatRow(separator),
2652
+ ...dataRows.map(formatRow)
2653
+ ].join("\n");
2654
+ }
2655
+ function serializeSlottedContainer(el, containerName, childName, slotPrefix) {
2656
+ return `::${containerName}\n${el.toArray().filter((c) => c instanceof yjs.XmlElement && c.nodeName === childName).map((item) => {
2657
+ const label = item.getAttribute("label") ?? "";
2658
+ const icon = item.getAttribute("icon") ?? "";
2659
+ const props = [];
2660
+ if (label) props.push(`label="${label}"`);
2661
+ if (icon) props.push(`icon="${icon}"`);
2662
+ const content = serializeChildren(item);
2663
+ return `#${slotPrefix}{${props.join(" ")}}\n${content}`;
2664
+ }).join("\n\n")}\n::`;
2665
+ }
2666
+ function generateFrontmatter(label, meta, type) {
2667
+ const lines = [];
2668
+ if (label !== void 0) lines.push(`title: "${escapeYaml(label)}"`);
2669
+ if (type && type !== "doc") lines.push(`type: ${type}`);
2670
+ if (!meta) return `---\n${lines.join("\n")}\n---`;
2671
+ if (meta.tags?.length) lines.push(`tags: [${meta.tags.join(", ")}]`);
2672
+ if (meta.color) lines.push(`color: ${yamlScalar(meta.color)}`);
2673
+ if (meta.icon) lines.push(`icon: ${yamlScalar(meta.icon)}`);
2674
+ if (meta.status) lines.push(`status: ${yamlScalar(meta.status)}`);
2675
+ if (meta.priority !== void 0 && meta.priority !== 0) lines.push(`priority: ${{
2676
+ 1: "low",
2677
+ 2: "medium",
2678
+ 3: "high",
2679
+ 4: "urgent"
2680
+ }[meta.priority] ?? meta.priority}`);
2681
+ if (meta.checked !== void 0) lines.push(`checked: ${meta.checked}`);
2682
+ if (meta.dateStart) lines.push(`dateStart: "${escapeYaml(meta.dateStart)}"`);
2683
+ if (meta.dateEnd) lines.push(`dateEnd: "${escapeYaml(meta.dateEnd)}"`);
2684
+ if (meta.subtitle) lines.push(`subtitle: "${escapeYaml(meta.subtitle)}"`);
2685
+ if (meta.url) lines.push(`url: ${meta.url}`);
2686
+ if (meta.rating !== void 0 && meta.rating !== 0) lines.push(`rating: ${meta.rating}`);
2687
+ return `---\n${lines.join("\n")}\n---`;
2688
+ }
2689
+ /**
2690
+ * Render a YAML scalar — bare when safe, double-quoted when the value
2691
+ * needs escaping. YAML treats `#`, `:`, leading whitespace, and a few
2692
+ * other characters as syntactically significant, so anything starting
2693
+ * with one of those gets quoted to stay round-trip safe.
2694
+ */
2695
+ function yamlScalar(s) {
2696
+ if (s === "") return "\"\"";
2697
+ if (/^[#&*!|>%@`]/.test(s)) return `"${escapeYaml(s)}"`;
2698
+ if (/[:"]/.test(s)) return `"${escapeYaml(s)}"`;
2699
+ if (/^\s|\s$/.test(s)) return `"${escapeYaml(s)}"`;
2700
+ return s;
2701
+ }
2702
+ function escapeYaml(s) {
2703
+ return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
2704
+ }
2705
+ function yjsToMarkdown(fragment, label, meta, type) {
2706
+ const { text: headerText, source: titleSource } = readDocumentHeader(fragment);
2707
+ const effectiveTitle = headerText || label;
2708
+ const docMeta = readDocumentMeta(fragment);
2709
+ const effectiveMeta = meta ?? docMeta.meta;
2710
+ const effectiveType = type ?? docMeta.type;
2711
+ const metaIsEmpty = isMetaEmpty(effectiveMeta);
2712
+ const typeIsDefault = !effectiveType || effectiveType === "doc";
2713
+ const bodyBlocks = collectBodyBlocks(fragment);
2714
+ let body;
2715
+ if (titleSource === "h1" && effectiveTitle) {
2716
+ const tail = serializeBlocksClean(bodyBlocks);
2717
+ body = tail === "" ? `# ${effectiveTitle}` : `# ${effectiveTitle}\n\n${tail}`;
2718
+ } else body = serializeBlocksClean(bodyBlocks);
2719
+ const wantFrontmatterTitle = titleSource === "frontmatter";
2720
+ if (!wantFrontmatterTitle && !(!metaIsEmpty || !typeIsDefault)) return body === "" ? "" : `${body}\n`;
2721
+ const frontmatter = generateFrontmatter(wantFrontmatterTitle ? effectiveTitle : void 0, effectiveMeta, effectiveType);
2722
+ if (body === "") return `${frontmatter}\n`;
2723
+ return `${frontmatter}\n\n${body}\n`;
2724
+ }
2725
+ function readDocumentMeta(fragment) {
2726
+ const meta = {};
2727
+ let type;
2728
+ for (const child of fragment.toArray()) {
2729
+ if (!(child instanceof yjs.XmlElement) || child.nodeName !== "documentMeta") continue;
2730
+ const attrs = child.getAttributes();
2731
+ for (const k of Object.keys(attrs)) {
2732
+ const v = attrs[k];
2733
+ if (v === void 0 || v === null) continue;
2734
+ if (k === "type" && typeof v === "string") {
2735
+ type = v;
2736
+ continue;
2737
+ }
2738
+ meta[k] = v;
2739
+ }
2740
+ break;
2741
+ }
2742
+ return {
2743
+ meta,
2744
+ type
2745
+ };
2746
+ }
2747
+ function readDocumentHeader(fragment) {
2748
+ for (const child of fragment.toArray()) {
2749
+ if (!(child instanceof yjs.XmlElement) || child.nodeName !== "documentHeader") continue;
2750
+ const text = child.toArray().find((c) => c instanceof yjs.XmlText);
2751
+ const src = child.getAttribute("titleSource");
2752
+ const source = src === "h1" || src === "frontmatter" ? src : void 0;
2753
+ return {
2754
+ text: text ? text.toString() : "",
2755
+ source
2756
+ };
2757
+ }
2758
+ return { text: "" };
2759
+ }
2760
+ function collectBodyBlocks(fragment) {
2761
+ const out = [];
2762
+ for (const child of fragment.toArray()) {
2763
+ if (!(child instanceof yjs.XmlElement)) continue;
2764
+ if (child.nodeName === "documentHeader" || child.nodeName === "documentMeta") continue;
2765
+ out.push(child);
2766
+ }
2767
+ return out;
2768
+ }
2769
+ function serializeBlocksClean(blocks) {
2770
+ const parts = [];
2771
+ for (const block of blocks) {
2772
+ if (block.nodeName === "paragraph" && block.length === 0) {
2773
+ parts.push("");
2774
+ continue;
2775
+ }
2776
+ parts.push(serializeBlock(block));
2777
+ }
2778
+ while (parts.length && parts[parts.length - 1] === "") parts.pop();
2779
+ return parts.join("\n\n");
2780
+ }
2781
+ function isMetaEmpty(meta) {
2782
+ if (!meta) return true;
2783
+ for (const key of Object.keys(meta)) {
2784
+ const v = meta[key];
2785
+ if (v === void 0 || v === null) continue;
2786
+ if (typeof v === "string" && v === "") continue;
2787
+ if (Array.isArray(v) && v.length === 0) continue;
2788
+ return false;
2789
+ }
2790
+ return true;
2791
+ }
2792
+
2793
+ //#endregion
2794
+ //#region packages/convert/src/spec/nodes.ts
2795
+ const BOOL_FALSE_DEFAULT = {
2796
+ key: "",
2797
+ type: "boolean",
2798
+ default: false,
2799
+ optional: true
2800
+ };
2801
+ const bool = (key) => ({
2802
+ ...BOOL_FALSE_DEFAULT,
2803
+ key
2804
+ });
2805
+ const str = (key, def) => ({
2806
+ key,
2807
+ type: "string",
2808
+ default: def,
2809
+ optional: true
2810
+ });
2811
+ const num = (key) => ({
2812
+ key,
2813
+ type: "number",
2814
+ optional: true
2815
+ });
2816
+ const int = (key) => ({
2817
+ key,
2818
+ type: "integer",
2819
+ optional: true
2820
+ });
2821
+ const VANILLA_BLOCKS = [
2822
+ {
2823
+ name: "documentHeader",
2824
+ group: "block",
2825
+ wire: "special",
2826
+ doc: "Holds the title; hoisted to frontmatter on serialise."
2827
+ },
2828
+ {
2829
+ name: "documentMeta",
2830
+ group: "block",
2831
+ wire: "special",
2832
+ doc: "Holds page-level meta; serialised into frontmatter."
2833
+ },
2834
+ {
2835
+ name: "paragraph",
2836
+ group: "block",
2837
+ wire: "vanilla",
2838
+ contentBearing: true
2839
+ },
2840
+ {
2841
+ name: "heading",
2842
+ group: "block",
2843
+ wire: "vanilla",
2844
+ attrs: [int("level")],
2845
+ contentBearing: true
2846
+ },
2847
+ {
2848
+ name: "blockquote",
2849
+ group: "block",
2850
+ wire: "vanilla",
2851
+ contentBearing: true
2852
+ },
2853
+ {
2854
+ name: "codeBlock",
2855
+ group: "block",
2856
+ wire: "fence",
2857
+ attrs: [str("language", "")]
2858
+ },
2859
+ {
2860
+ name: "bulletList",
2861
+ group: "block",
2862
+ wire: "vanilla"
2863
+ },
2864
+ {
2865
+ name: "orderedList",
2866
+ group: "block",
2867
+ wire: "vanilla"
2868
+ },
2869
+ {
2870
+ name: "listItem",
2871
+ group: "block",
2872
+ wire: "vanilla",
2873
+ contentBearing: true
2874
+ },
2875
+ {
2876
+ name: "taskList",
2877
+ group: "block",
2878
+ wire: "vanilla"
2879
+ },
2880
+ {
2881
+ name: "taskItem",
2882
+ group: "block",
2883
+ wire: "vanilla",
2884
+ attrs: [bool("checked")],
2885
+ contentBearing: true
2886
+ },
2887
+ {
2888
+ name: "table",
2889
+ group: "block",
2890
+ wire: "vanilla"
2891
+ },
2892
+ {
2893
+ name: "tableRow",
2894
+ group: "block",
2895
+ wire: "vanilla"
2896
+ },
2897
+ {
2898
+ name: "tableHeader",
2899
+ group: "block",
2900
+ wire: "vanilla",
2901
+ contentBearing: true
2902
+ },
2903
+ {
2904
+ name: "tableCell",
2905
+ group: "block",
2906
+ wire: "vanilla",
2907
+ contentBearing: true
2908
+ },
2909
+ {
2910
+ name: "horizontalRule",
2911
+ group: "block",
2912
+ wire: "vanilla"
2913
+ },
2914
+ {
2915
+ name: "image",
2916
+ group: "block",
2917
+ wire: "special",
2918
+ attrs: [
2919
+ str("src"),
2920
+ str("alt", ""),
2921
+ int("width"),
2922
+ int("height")
2923
+ ]
2924
+ },
2925
+ {
2926
+ name: "hardBreak",
2927
+ group: "inline",
2928
+ wire: "vanilla"
2929
+ }
2930
+ ];
2931
+ const MDC_CONTAINERS = [
2932
+ {
2933
+ name: "callout",
2934
+ group: "block",
2935
+ wire: "mdc-container",
2936
+ attrs: [
2937
+ {
2938
+ key: "type",
2939
+ type: "string",
2940
+ default: "note",
2941
+ optional: true,
2942
+ values: [
2943
+ "note",
2944
+ "tip",
2945
+ "warning",
2946
+ "danger",
2947
+ "info",
2948
+ "caution",
2949
+ "alert",
2950
+ "success",
2951
+ "error"
2952
+ ]
2953
+ },
2954
+ str("title"),
2955
+ str("icon")
2956
+ ]
2957
+ },
2958
+ {
2959
+ name: "collapsible",
2960
+ group: "block",
2961
+ wire: "mdc-container",
2962
+ attrs: [str("label", "Details"), bool("open")]
2963
+ },
2964
+ {
2965
+ name: "accordion",
2966
+ group: "block",
2967
+ wire: "mdc-slotted",
2968
+ slotChild: "accordionItem"
2969
+ },
2970
+ {
2971
+ name: "accordionItem",
2972
+ group: "block",
2973
+ wire: "mdc-container",
2974
+ mdcTag: "accordion-item",
2975
+ attrs: [str("label", "Item"), str("icon")]
2976
+ },
2977
+ {
2978
+ name: "tabs",
2979
+ group: "block",
2980
+ wire: "mdc-slotted",
2981
+ slotChild: "tabsItem"
2982
+ },
2983
+ {
2984
+ name: "tabsItem",
2985
+ group: "block",
2986
+ wire: "mdc-container",
2987
+ mdcTag: "tabs-item",
2988
+ attrs: [str("label"), str("icon")]
2989
+ },
2990
+ {
2991
+ name: "steps",
2992
+ group: "block",
2993
+ wire: "mdc-container"
2994
+ },
2995
+ {
2996
+ name: "card",
2997
+ group: "block",
2998
+ wire: "mdc-container",
2999
+ attrs: [
3000
+ str("title"),
3001
+ str("icon"),
3002
+ str("to")
3003
+ ]
3004
+ },
3005
+ {
3006
+ name: "cardGroup",
3007
+ group: "block",
3008
+ wire: "mdc-slotted",
3009
+ mdcTag: "card-group",
3010
+ slotChild: "card"
3011
+ },
3012
+ {
3013
+ name: "field",
3014
+ group: "block",
3015
+ wire: "mdc-container",
3016
+ attrs: [
3017
+ str("name"),
3018
+ str("type", "string"),
3019
+ bool("required")
3020
+ ]
3021
+ },
3022
+ {
3023
+ name: "fieldGroup",
3024
+ group: "block",
3025
+ wire: "mdc-slotted",
3026
+ mdcTag: "field-group",
3027
+ slotChild: "field"
3028
+ },
3029
+ {
3030
+ name: "codeGroup",
3031
+ group: "block",
3032
+ wire: "mdc-slotted",
3033
+ mdcTag: "code-group",
3034
+ slotChild: "codeBlock"
3035
+ },
3036
+ {
3037
+ name: "codeCollapse",
3038
+ group: "block",
3039
+ wire: "mdc-container",
3040
+ mdcTag: "code-collapse"
3041
+ },
3042
+ {
3043
+ name: "codePreview",
3044
+ group: "block",
3045
+ wire: "mdc-container",
3046
+ mdcTag: "code-preview"
3047
+ },
3048
+ {
3049
+ name: "codeTree",
3050
+ group: "block",
3051
+ wire: "mdc-atom-block",
3052
+ mdcTag: "code-tree",
3053
+ attrs: [{
3054
+ key: "files",
3055
+ type: "json"
3056
+ }]
3057
+ },
3058
+ {
3059
+ name: "figure",
3060
+ group: "block",
3061
+ wire: "mdc-container",
3062
+ attrs: [
3063
+ str("src"),
3064
+ str("alt", ""),
3065
+ str("caption")
3066
+ ]
3067
+ },
3068
+ {
3069
+ name: "video",
3070
+ group: "block",
3071
+ wire: "mdc-atom-block",
3072
+ attrs: [
3073
+ str("src"),
3074
+ str("poster"),
3075
+ bool("autoplay"),
3076
+ bool("loop"),
3077
+ bool("controls")
3078
+ ]
3079
+ },
3080
+ {
3081
+ name: "embed",
3082
+ group: "block",
3083
+ wire: "mdc-atom-block",
3084
+ attrs: [str("src"), str("title")]
3085
+ },
3086
+ {
3087
+ name: "svgEmbed",
3088
+ group: "block",
3089
+ wire: "fence",
3090
+ attrs: [str("title")],
3091
+ mdcTag: "svg",
3092
+ doc: "Serialised as a ```svg fenced block; the SVG markup is the body."
3093
+ },
3094
+ {
3095
+ name: "divider",
3096
+ group: "block",
3097
+ wire: "mdc-atom-block",
3098
+ attrs: [str("label"), str("icon")]
3099
+ },
3100
+ {
3101
+ name: "quote",
3102
+ group: "block",
3103
+ wire: "mdc-container",
3104
+ attrs: [str("cite")]
3105
+ },
3106
+ {
3107
+ name: "progress",
3108
+ group: "block",
3109
+ wire: "mdc-atom-block",
3110
+ attrs: [
3111
+ num("value"),
3112
+ num("max"),
3113
+ str("label")
3114
+ ]
3115
+ },
3116
+ {
3117
+ name: "spoiler",
3118
+ group: "block",
3119
+ wire: "mdc-container",
3120
+ attrs: [str("label")]
3121
+ },
3122
+ {
3123
+ name: "colorSwatch",
3124
+ group: "block",
3125
+ wire: "mdc-atom-block",
3126
+ mdcTag: "color-swatch",
3127
+ attrs: [str("color"), str("label")]
3128
+ },
3129
+ {
3130
+ name: "stat",
3131
+ group: "block",
3132
+ wire: "mdc-container",
3133
+ attrs: [
3134
+ str("label"),
3135
+ str("value"),
3136
+ str("icon")
3137
+ ]
3138
+ },
3139
+ {
3140
+ name: "statGroup",
3141
+ group: "block",
3142
+ wire: "mdc-slotted",
3143
+ mdcTag: "stat-group",
3144
+ slotChild: "stat"
3145
+ },
3146
+ {
3147
+ name: "button",
3148
+ group: "block",
3149
+ wire: "mdc-atom-block",
3150
+ attrs: [
3151
+ str("label"),
3152
+ str("to"),
3153
+ str("icon"),
3154
+ str("variant")
3155
+ ]
3156
+ },
3157
+ {
3158
+ name: "buttonGroup",
3159
+ group: "block",
3160
+ wire: "mdc-slotted",
3161
+ mdcTag: "button-group",
3162
+ slotChild: "button"
3163
+ },
3164
+ {
3165
+ name: "timeline",
3166
+ group: "block",
3167
+ wire: "mdc-slotted",
3168
+ slotChild: "timelineItem"
3169
+ },
3170
+ {
3171
+ name: "timelineItem",
3172
+ group: "block",
3173
+ wire: "mdc-container",
3174
+ mdcTag: "timeline-item",
3175
+ attrs: [
3176
+ str("label"),
3177
+ str("icon"),
3178
+ str("date")
3179
+ ]
3180
+ },
3181
+ {
3182
+ name: "diff",
3183
+ group: "block",
3184
+ wire: "mdc-atom-block",
3185
+ attrs: [str("language", ""), {
3186
+ key: "value",
3187
+ type: "string"
3188
+ }]
3189
+ }
3190
+ ];
3191
+ const INLINE_AND_SPECIAL = [
3192
+ {
3193
+ name: "docLink",
3194
+ group: "inline",
3195
+ wire: "special",
3196
+ attrs: [str("docId")],
3197
+ doc: "Wire form `[[uuid|label]]`; label regenerated on export."
3198
+ },
3199
+ {
3200
+ name: "docEmbed",
3201
+ group: "block",
3202
+ wire: "special",
3203
+ attrs: [
3204
+ str("docId"),
3205
+ bool("collapsed"),
3206
+ bool("tall"),
3207
+ bool("seamless")
3208
+ ],
3209
+ doc: "Wire form `![[uuid|label]]{collapsed tall seamless}`."
3210
+ },
3211
+ {
3212
+ name: "mention",
3213
+ group: "inline",
3214
+ wire: "special",
3215
+ attrs: [str("userId")],
3216
+ doc: "Wire form `@[label](user:uuid)`; label regenerated on export."
3217
+ },
3218
+ {
3219
+ name: "mathInline",
3220
+ group: "inline",
3221
+ wire: "special",
3222
+ attrs: [{
3223
+ key: "expression",
3224
+ type: "string"
3225
+ }],
3226
+ doc: "Wire form `$expression$`."
3227
+ },
3228
+ {
3229
+ name: "mathBlock",
3230
+ group: "block",
3231
+ wire: "fence",
3232
+ attrs: [{
3233
+ key: "expression",
3234
+ type: "string"
3235
+ }],
3236
+ mdcTag: "math",
3237
+ doc: "Wire form ``` ```math\\nexpression\\n``` ```."
3238
+ },
3239
+ {
3240
+ name: "fileBlock",
3241
+ group: "block",
3242
+ wire: "mdc-atom-block",
3243
+ mdcTag: "file",
3244
+ attrs: [
3245
+ str("src"),
3246
+ str("mime"),
3247
+ str("uploadId"),
3248
+ str("filename")
3249
+ ],
3250
+ doc: "Wire form `:file{src=… mime=… upload-id=… filename=…}`; binary in sidecar."
3251
+ },
3252
+ {
3253
+ name: "badge",
3254
+ group: "inline",
3255
+ wire: "mdc-atom-inl",
3256
+ attrs: [
3257
+ str("label"),
3258
+ str("color"),
3259
+ str("variant", "subtle")
3260
+ ],
3261
+ doc: "Wire form `:badge[Label]{color=… variant=…}`."
3262
+ },
3263
+ {
3264
+ name: "proseIcon",
3265
+ group: "inline",
3266
+ wire: "mdc-atom-inl",
3267
+ mdcTag: "icon",
3268
+ attrs: [str("name")],
3269
+ doc: "Wire form `:icon{name=…}`."
3270
+ },
3271
+ {
3272
+ name: "kbd",
3273
+ group: "inline",
3274
+ wire: "mdc-atom-inl",
3275
+ attrs: [str("value")],
3276
+ doc: "Wire form `:kbd{value=…}`."
3277
+ }
3278
+ ];
3279
+ const NODE_SPECS = [
3280
+ ...VANILLA_BLOCKS,
3281
+ ...MDC_CONTAINERS,
3282
+ ...INLINE_AND_SPECIAL
3283
+ ];
3284
+ const NODE_SPEC_BY_NAME = new Map(NODE_SPECS.map((spec) => [spec.name, spec]));
3285
+
3286
+ //#endregion
3287
+ //#region packages/convert/src/spec/marks.ts
3288
+ const MARK_SPECS = [
3289
+ {
3290
+ name: "bold",
3291
+ wire: "delimited",
3292
+ delim: "**"
3293
+ },
3294
+ {
3295
+ name: "italic",
3296
+ wire: "delimited",
3297
+ delim: "*"
3298
+ },
3299
+ {
3300
+ name: "strike",
3301
+ wire: "delimited",
3302
+ delim: "~~"
3303
+ },
3304
+ {
3305
+ name: "code",
3306
+ wire: "delimited",
3307
+ delim: "`"
3308
+ },
3309
+ {
3310
+ name: "link",
3311
+ wire: "link",
3312
+ attrs: [{
3313
+ key: "href",
3314
+ type: "string"
3315
+ }, {
3316
+ key: "title",
3317
+ type: "string",
3318
+ optional: true
3319
+ }]
3320
+ },
3321
+ {
3322
+ name: "underline",
3323
+ wire: "delimited",
3324
+ delim: "__",
3325
+ doc: "Disambiguated from bold by delimiter character. Two underscores = underline; two asterisks = bold."
3326
+ },
3327
+ {
3328
+ name: "highlight",
3329
+ wire: "delimited",
3330
+ delim: "==",
3331
+ doc: "Pandoc-style."
3332
+ },
3333
+ {
3334
+ name: "subscript",
3335
+ wire: "delimited",
3336
+ delim: "~",
3337
+ doc: "Single tilde; double tilde is strike."
3338
+ },
3339
+ {
3340
+ name: "superscript",
3341
+ wire: "delimited",
3342
+ delim: "^"
3343
+ },
3344
+ {
3345
+ name: "textStyle",
3346
+ wire: "mdc-span",
3347
+ attrs: [
3348
+ {
3349
+ key: "color",
3350
+ type: "string",
3351
+ optional: true
3352
+ },
3353
+ {
3354
+ key: "backgroundColor",
3355
+ type: "string",
3356
+ optional: true
3357
+ },
3358
+ {
3359
+ key: "fontSize",
3360
+ type: "string",
3361
+ optional: true
3362
+ },
3363
+ {
3364
+ key: "fontFamily",
3365
+ type: "string",
3366
+ optional: true
3367
+ }
3368
+ ],
3369
+ doc: "Wire form `:span[text]{color=\"…\" font-size=\"…\"}`. Any of the attrs may be set."
3370
+ }
3371
+ ];
3372
+ const MARK_SPEC_BY_NAME = new Map(MARK_SPECS.map((spec) => [spec.name, spec]));
3373
+ const MARK_SPEC_BY_DELIM = new Map(MARK_SPECS.filter((spec) => spec.wire === "delimited" && !!spec.delim).map((spec) => [spec.delim, spec]));
3374
+
3375
+ //#endregion
3376
+ //#region packages/convert/src/spec/universal-meta.ts
3377
+ const UNIVERSAL_META_KEYS = [
3378
+ {
3379
+ key: "title",
3380
+ type: "string",
3381
+ doc: "Display title; the first H1 is hoisted into this field on import."
3382
+ },
3383
+ {
3384
+ key: "type",
3385
+ type: "string",
3386
+ doc: "Page type (doc, kanban, table, …). Omitted on serialise when \"doc\"."
3387
+ },
3388
+ {
3389
+ key: "color",
3390
+ type: "string",
3391
+ doc: "Hex or CSS color name."
3392
+ },
3393
+ {
3394
+ key: "icon",
3395
+ type: "string",
3396
+ doc: "Lucide icon name in kebab-case."
3397
+ },
3398
+ {
3399
+ key: "datetimeStart",
3400
+ type: "iso-datetime"
3401
+ },
3402
+ {
3403
+ key: "datetimeEnd",
3404
+ type: "iso-datetime"
3405
+ },
3406
+ {
3407
+ key: "allDay",
3408
+ type: "boolean"
3409
+ },
3410
+ {
3411
+ key: "dateTaken",
3412
+ type: "iso-datetime"
3413
+ },
3414
+ {
3415
+ key: "dateStart",
3416
+ type: "iso-date",
3417
+ parseAliases: ["date", "created"]
3418
+ },
3419
+ {
3420
+ key: "dateEnd",
3421
+ type: "iso-date",
3422
+ parseAliases: ["due"]
3423
+ },
3424
+ {
3425
+ key: "timeStart",
3426
+ type: "hh-mm"
3427
+ },
3428
+ {
3429
+ key: "timeEnd",
3430
+ type: "hh-mm"
3431
+ },
3432
+ {
3433
+ key: "tags",
3434
+ type: "string[]"
3435
+ },
3436
+ {
3437
+ key: "checked",
3438
+ type: "boolean",
3439
+ parseAliases: ["done"]
3440
+ },
3441
+ {
3442
+ key: "priority",
3443
+ type: "integer",
3444
+ min: 0,
3445
+ max: 4,
3446
+ doc: "Numeric or named (low/medium/high/urgent → 1/2/3/4)."
3447
+ },
3448
+ {
3449
+ key: "status",
3450
+ type: "string"
3451
+ },
3452
+ {
3453
+ key: "rating",
3454
+ type: "number",
3455
+ min: 0,
3456
+ max: 5
3457
+ },
3458
+ {
3459
+ key: "url",
3460
+ type: "string"
3461
+ },
3462
+ {
3463
+ key: "email",
3464
+ type: "string"
3465
+ },
3466
+ {
3467
+ key: "phone",
3468
+ type: "string"
3469
+ },
3470
+ {
3471
+ key: "number",
3472
+ type: "number"
3473
+ },
3474
+ {
3475
+ key: "unit",
3476
+ type: "string"
3477
+ },
3478
+ {
3479
+ key: "subtitle",
3480
+ type: "string",
3481
+ parseAliases: ["description"]
3482
+ },
3483
+ {
3484
+ key: "note",
3485
+ type: "string"
3486
+ },
3487
+ {
3488
+ key: "taskProgress",
3489
+ type: "integer",
3490
+ min: 0,
3491
+ max: 100
3492
+ },
3493
+ {
3494
+ key: "members",
3495
+ type: "members"
3496
+ },
3497
+ {
3498
+ key: "coverUploadId",
3499
+ type: "string"
3500
+ },
3501
+ {
3502
+ key: "coverDocId",
3503
+ type: "string"
3504
+ },
3505
+ {
3506
+ key: "coverMimeType",
3507
+ type: "string"
3508
+ },
3509
+ {
3510
+ key: "geoType",
3511
+ type: "string-enum",
3512
+ values: [
3513
+ "marker",
3514
+ "line",
3515
+ "measure"
3516
+ ]
3517
+ },
3518
+ {
3519
+ key: "geoLat",
3520
+ type: "number"
3521
+ },
3522
+ {
3523
+ key: "geoLng",
3524
+ type: "number"
3525
+ },
3526
+ {
3527
+ key: "geoDescription",
3528
+ type: "string"
3529
+ },
3530
+ {
3531
+ key: "deskX",
3532
+ type: "number"
3533
+ },
3534
+ {
3535
+ key: "deskY",
3536
+ type: "number"
3537
+ },
3538
+ {
3539
+ key: "deskZ",
3540
+ type: "number"
3541
+ },
3542
+ {
3543
+ key: "deskMode",
3544
+ type: "string-enum",
3545
+ values: [
3546
+ "icon",
3547
+ "widget-sm",
3548
+ "widget-lg"
3549
+ ]
3550
+ },
3551
+ {
3552
+ key: "mmX",
3553
+ type: "number"
3554
+ },
3555
+ {
3556
+ key: "mmY",
3557
+ type: "number"
3558
+ },
3559
+ {
3560
+ key: "graphX",
3561
+ type: "number"
3562
+ },
3563
+ {
3564
+ key: "graphY",
3565
+ type: "number"
3566
+ },
3567
+ {
3568
+ key: "graphPinned",
3569
+ type: "boolean"
3570
+ },
3571
+ {
3572
+ key: "spX",
3573
+ type: "number"
3574
+ },
3575
+ {
3576
+ key: "spY",
3577
+ type: "number"
3578
+ },
3579
+ {
3580
+ key: "spZ",
3581
+ type: "number"
3582
+ },
3583
+ {
3584
+ key: "spRX",
3585
+ type: "number"
3586
+ },
3587
+ {
3588
+ key: "spRY",
3589
+ type: "number"
3590
+ },
3591
+ {
3592
+ key: "spRZ",
3593
+ type: "number"
3594
+ },
3595
+ {
3596
+ key: "spSX",
3597
+ type: "number"
3598
+ },
3599
+ {
3600
+ key: "spSY",
3601
+ type: "number"
3602
+ },
3603
+ {
3604
+ key: "spSZ",
3605
+ type: "number"
3606
+ },
3607
+ {
3608
+ key: "spShape",
3609
+ type: "string-enum",
3610
+ values: [
3611
+ "box",
3612
+ "sphere",
3613
+ "cylinder",
3614
+ "cone",
3615
+ "plane",
3616
+ "torus",
3617
+ "glb"
3618
+ ]
3619
+ },
3620
+ {
3621
+ key: "spOpacity",
3622
+ type: "integer",
3623
+ min: 0,
3624
+ max: 100
3625
+ },
3626
+ {
3627
+ key: "spModelUploadId",
3628
+ type: "string"
3629
+ },
3630
+ {
3631
+ key: "spModelDocId",
3632
+ type: "string"
3633
+ },
3634
+ {
3635
+ key: "slidesTransition",
3636
+ type: "string-enum",
3637
+ values: [
3638
+ "none",
3639
+ "fade",
3640
+ "slide"
3641
+ ]
3642
+ },
3643
+ {
3644
+ key: "slidesTheme",
3645
+ type: "string-enum",
3646
+ values: ["dark", "light"]
3647
+ },
3648
+ {
3649
+ key: "__schemaVersion",
3650
+ type: "integer",
3651
+ min: 0
3652
+ }
3653
+ ];
3654
+ const UNIVERSAL_META_KEY_NAMES = new Set(UNIVERSAL_META_KEYS.map((k) => k.key));
3655
+
3656
+ //#endregion
3657
+ //#region packages/cli/src/commands/documents.ts
3658
+ /**
3659
+ * Document CRUD commands: read, create, rename, move, delete, type, doc.
3660
+ */
3661
+ /** Safely read a tree map value, converting Y.Map to plain object if needed. */
3662
+ function toPlain$1(val) {
3663
+ return val instanceof yjs.Map ? val.toJSON() : val;
3664
+ }
3665
+ registerCommand({
3666
+ name: "doc",
3667
+ aliases: ["info:doc"],
3668
+ description: "Show document metadata (label, type, meta, dates).",
3669
+ usage: "doc id=<docId> | name=<label> | path=<a/b/c>",
3670
+ async run(conn, args) {
3671
+ if (!conn) return "Not connected";
3672
+ const docId = resolveDocument(conn, args.params, args.positional);
3673
+ if (!docId) return "Document not found. Specify id=, name=, or path= to identify the document.";
3674
+ const treeMap = conn.getTreeMap();
3675
+ if (!treeMap) return "Not connected";
3676
+ const raw = treeMap.get(docId);
3677
+ if (!raw) return `Document ${docId} not found in tree.`;
3678
+ const entry = toPlain$1(raw);
3679
+ const lines = [
3680
+ `id: ${docId}`,
3681
+ `label: ${entry.label || "Untitled"}`,
3682
+ `type: ${entry.type ?? "—"}`,
3683
+ `parent: ${entry.parentId ?? "(root)"}`,
3684
+ `order: ${entry.order ?? 0}`,
3685
+ `created: ${entry.createdAt ? new Date(entry.createdAt).toISOString() : "—"}`,
3686
+ `updated: ${entry.updatedAt ? new Date(entry.updatedAt).toISOString() : "—"} (${relativeTime(entry.updatedAt)})`
3687
+ ];
3688
+ if (entry.meta && Object.keys(entry.meta).length > 0) {
3689
+ lines.push(`meta:`);
3690
+ for (const [k, v] of Object.entries(entry.meta)) lines.push(` ${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`);
3691
+ }
3692
+ const children = childrenOf(readEntries(treeMap), docId);
3693
+ lines.push(`children: ${children.length}`);
3694
+ return lines.join("\n");
3695
+ }
2457
3696
  });
2458
3697
  registerCommand({
2459
3698
  name: "read",
@@ -3143,23 +4382,24 @@ registerCommand({
3143
4382
  registerCommand({
3144
4383
  name: "chat",
3145
4384
  aliases: ["send"],
3146
- description: "Send a chat message to a channel.",
3147
- usage: "chat channel=<group:docId> text=<message>",
4385
+ description: "Send a chat message to a channel doc.",
4386
+ usage: "chat channel_doc_id=<docId> text=<message>",
3148
4387
  async run(conn, args) {
3149
4388
  if (!conn) return "Not connected";
3150
4389
  const rootProvider = conn.rootProvider;
3151
4390
  if (!rootProvider) return "Not connected";
3152
- const channel = args.params["channel"];
3153
- if (!channel) return "Missing required parameter: channel=<group:docId>";
4391
+ const raw = args.params["channel_doc_id"] || args.params["channel"];
4392
+ if (!raw) return "Missing required parameter: channel_doc_id=<docId>";
4393
+ const channel_doc_id = raw.startsWith("group:") ? raw.slice(6) : raw;
3154
4394
  const text = args.params["text"] || args.params["content"] || args.positional[0];
3155
4395
  if (!text) return "Missing required parameter: text=<message>";
3156
4396
  rootProvider.sendStateless(JSON.stringify({
3157
- type: "chat:send",
3158
- channel,
4397
+ type: "messages:send",
4398
+ channel_doc_id,
3159
4399
  content: text,
3160
- sender_name: conn.displayName
4400
+ mentions: []
3161
4401
  }));
3162
- return `Sent to ${channel}`;
4402
+ return `Sent to ${channel_doc_id}`;
3163
4403
  }
3164
4404
  });
3165
4405
 
@@ -3329,6 +4569,16 @@ const PAGE_TYPES = {
3329
4569
  core: true,
3330
4570
  supportsChildren: true
3331
4571
  },
4572
+ prose: {
4573
+ key: "prose",
4574
+ label: "Prose",
4575
+ icon: "pen-tool",
4576
+ description: "Long-form prose with serif typography and a narrow readable measure",
4577
+ core: true,
4578
+ supportsChildren: true,
4579
+ childLabel: "Item",
4580
+ defaultDepth: -1
4581
+ },
3332
4582
  kanban: {
3333
4583
  key: "kanban",
3334
4584
  label: "Kanban",
@@ -4113,6 +5363,695 @@ registerCommand({
4113
5363
  }
4114
5364
  });
4115
5365
 
5366
+ //#endregion
5367
+ //#region packages/cli/src/commands/wiki/wikipedia.ts
5368
+ /**
5369
+ * Rate-limited wrapper around wtf_wikipedia + wtf-plugin-api.
5370
+ *
5371
+ * Responsibilities:
5372
+ * - Throttle requests to respect Wikimedia API etiquette
5373
+ * - Cache parsed Documents by canonical title
5374
+ * - Resolve redirects so callers always see the redirect target
5375
+ * - Expose getCategoryPages via wtf-plugin-api
5376
+ */
5377
+ let pluginExtended = false;
5378
+ function ensurePlugin() {
5379
+ if (pluginExtended) return;
5380
+ wtf_wikipedia.default.extend(wtf_plugin_api.default);
5381
+ pluginExtended = true;
5382
+ }
5383
+ /** A token-bucket-ish throttle: at most `rate` calls per second, FIFO. */
5384
+ var RateLimiter = class {
5385
+ lastTickMs = 0;
5386
+ constructor(intervalMs) {
5387
+ this.intervalMs = intervalMs;
5388
+ }
5389
+ async wait() {
5390
+ const now = Date.now();
5391
+ const earliest = this.lastTickMs + this.intervalMs;
5392
+ if (now < earliest) await new Promise((r) => setTimeout(r, earliest - now));
5393
+ this.lastTickMs = Math.max(now, earliest);
5394
+ }
5395
+ };
5396
+ var WikipediaClient = class {
5397
+ cache = /* @__PURE__ */ new Map();
5398
+ redirects = /* @__PURE__ */ new Map();
5399
+ limiter;
5400
+ fetchOpts;
5401
+ constructor(config) {
5402
+ this.config = config;
5403
+ ensurePlugin();
5404
+ this.limiter = new RateLimiter(Math.max(50, Math.floor(1e3 / Math.max(.1, config.rate))));
5405
+ this.fetchOpts = {
5406
+ lang: config.lang,
5407
+ "Api-User-Agent": config.userAgent,
5408
+ follow_redirects: true
5409
+ };
5410
+ if (config.domain) this.fetchOpts.domain = config.domain;
5411
+ }
5412
+ /**
5413
+ * Fetch and parse a Wikipedia article.
5414
+ * - Returns the cached Document if we've seen this title before.
5415
+ * - Follows redirects and caches under both source and target titles.
5416
+ * - Returns null when the page does not exist.
5417
+ */
5418
+ async fetchArticle(rawTitle) {
5419
+ const title = canonicalTitle(rawTitle);
5420
+ if (this.cache.has(title)) return this.cache.get(title);
5421
+ if (this.redirects.has(title)) {
5422
+ const target = this.redirects.get(title);
5423
+ return this.cache.get(target) ?? null;
5424
+ }
5425
+ await this.limiter.wait();
5426
+ let doc;
5427
+ try {
5428
+ doc = await wtf_wikipedia.default.fetch(title, this.fetchOpts);
5429
+ } catch (err) {
5430
+ throw new Error(`Wikipedia fetch failed for "${title}": ${err?.message ?? err}`);
5431
+ }
5432
+ if (!doc) return null;
5433
+ if (typeof doc.isRedirect === "function" && doc.isRedirect()) {
5434
+ const target = doc.redirectTo?.()?.page;
5435
+ if (typeof target === "string") {
5436
+ this.redirects.set(title, canonicalTitle(target));
5437
+ return await this.fetchArticle(target);
5438
+ }
5439
+ }
5440
+ const resolvedTitle = canonicalTitle(doc.title?.() ?? title);
5441
+ this.cache.set(resolvedTitle, doc);
5442
+ if (resolvedTitle !== title) this.redirects.set(title, resolvedTitle);
5443
+ return doc;
5444
+ }
5445
+ /**
5446
+ * Fetch the member pages of a category (and optionally sub-categories).
5447
+ * @param category Category title (with or without "Category:" prefix).
5448
+ * @param recursive Whether to traverse sub-categories.
5449
+ * @param maxDepth Recursion depth when recursive=true.
5450
+ */
5451
+ async fetchCategoryPages(category, recursive, maxDepth) {
5452
+ await this.limiter.wait();
5453
+ const opts = {
5454
+ lang: this.config.lang,
5455
+ "Api-User-Agent": this.config.userAgent,
5456
+ recursive,
5457
+ maxDepth
5458
+ };
5459
+ if (this.config.domain) opts.domain = this.config.domain;
5460
+ return (await wtf_wikipedia.default.getCategoryPages(category, opts) ?? []).map((m) => ({
5461
+ title: canonicalTitle(m.title),
5462
+ type: m.type === "subcat" ? "subcat" : "page"
5463
+ }));
5464
+ }
5465
+ };
5466
+ /** Normalize a Wikipedia title — trim, collapse spaces, strip leading/trailing colons. */
5467
+ function canonicalTitle(s) {
5468
+ return (s ?? "").toString().replace(/_/g, " ").replace(/\s+/g, " ").trim();
5469
+ }
5470
+ /** Detect a category-namespaced title. */
5471
+ const CATEGORY_PREFIX = /^(Category|Catégorie|Kategorie|Categoría|Categoria|Categorie|Kategoria):/i;
5472
+ function isCategoryTitle(title) {
5473
+ return CATEGORY_PREFIX.test(title);
5474
+ }
5475
+ /** Strip the "Category:" prefix for display. */
5476
+ function stripCategoryPrefix(title) {
5477
+ return title.replace(CATEGORY_PREFIX, "").trim();
5478
+ }
5479
+
5480
+ //#endregion
5481
+ //#region packages/cli/src/commands/wiki/snapshot.ts
5482
+ function snapshotArticle(doc, title) {
5483
+ return {
5484
+ title,
5485
+ linkTitles: collectLinkTitles(doc),
5486
+ categories: collectCategories(doc),
5487
+ sections: snapshotSections(doc.sections?.() ?? []),
5488
+ infobox: snapshotInfobox(doc.infobox?.()),
5489
+ lead: leadParagraph(doc),
5490
+ url: typeof doc.url === "function" ? doc.url() : null
5491
+ };
5492
+ }
5493
+ function prettyCategoryLabel(catTitle) {
5494
+ return stripCategoryPrefix(catTitle);
5495
+ }
5496
+ function collectLinkTitles(doc) {
5497
+ const links = doc.links?.() ?? [];
5498
+ const out = /* @__PURE__ */ new Set();
5499
+ for (const l of links) {
5500
+ if (!l) continue;
5501
+ const page = typeof l.page === "function" ? l.page() : null;
5502
+ if (typeof page !== "string" || page.length === 0) continue;
5503
+ if (isCategoryTitle(page)) continue;
5504
+ out.add(canonicalTitle(page));
5505
+ }
5506
+ return [...out];
5507
+ }
5508
+ function collectCategories(doc) {
5509
+ const out = [];
5510
+ for (const c of doc.categories?.() ?? []) {
5511
+ const norm = canonicalTitle(c);
5512
+ if (norm) out.push(norm);
5513
+ }
5514
+ return out;
5515
+ }
5516
+ function snapshotSections(rawSections) {
5517
+ const all = rawSections.map((s) => ({
5518
+ raw: s,
5519
+ title: s.title?.() || "",
5520
+ parentRef: typeof s.parent === "function" ? s.parent() : null,
5521
+ children: []
5522
+ }));
5523
+ const byRaw = /* @__PURE__ */ new Map();
5524
+ for (const s of all) byRaw.set(s.raw, s);
5525
+ const roots = [];
5526
+ for (const s of all) if (s.parentRef && byRaw.has(s.parentRef)) byRaw.get(s.parentRef).children.push(materialize(s));
5527
+ else roots.push(s);
5528
+ return roots.map(materialize);
5529
+ }
5530
+ function materialize(node) {
5531
+ const lists = node.raw.lists?.() ?? [];
5532
+ const paragraphs = node.raw.paragraphs?.() ?? [];
5533
+ let listLength = 0;
5534
+ for (const l of lists) {
5535
+ const lines = l.lines?.() ?? [];
5536
+ listLength += lines.length;
5537
+ }
5538
+ const isList = lists.length > 0 && (paragraphs.length === 0 || listLength >= paragraphs.length * 2);
5539
+ const bodyParts = [];
5540
+ for (const p of paragraphs) {
5541
+ const md = paragraphMarkdown(p);
5542
+ if (md) bodyParts.push(md);
5543
+ }
5544
+ for (const l of lists) {
5545
+ const lines = l.lines?.() ?? [];
5546
+ for (const line of lines) {
5547
+ const text = lineText(line);
5548
+ if (text) bodyParts.push(`- ${text}`);
5549
+ }
5550
+ }
5551
+ return {
5552
+ title: node.title,
5553
+ body: bodyParts.join("\n\n"),
5554
+ isList,
5555
+ listLength,
5556
+ children: node.children
5557
+ };
5558
+ }
5559
+ function snapshotInfobox(box) {
5560
+ if (!box) return void 0;
5561
+ const data = typeof box.json === "function" ? box.json() : null;
5562
+ if (!data || typeof data !== "object") return void 0;
5563
+ const rows = [];
5564
+ for (const [key, val] of Object.entries(data)) {
5565
+ const value = stringifyInfoboxValue(val);
5566
+ if (!value) continue;
5567
+ rows.push({
5568
+ key: humanKey(key),
5569
+ value
5570
+ });
5571
+ }
5572
+ return rows.length > 0 ? rows : void 0;
5573
+ }
5574
+ function stringifyInfoboxValue(val) {
5575
+ if (val == null) return "";
5576
+ if (typeof val === "string") return val;
5577
+ if (typeof val === "number" || typeof val === "boolean") return String(val);
5578
+ if (Array.isArray(val)) return val.map(stringifyInfoboxValue).filter(Boolean).join(", ");
5579
+ if (typeof val === "object") {
5580
+ const o = val;
5581
+ if (typeof o.text === "string") return o.text;
5582
+ if (typeof o.number === "number") return String(o.number);
5583
+ }
5584
+ return "";
5585
+ }
5586
+ function humanKey(k) {
5587
+ return k.replace(/_/g, " ").replace(/^./, (m) => m.toUpperCase());
5588
+ }
5589
+ function leadParagraph(doc) {
5590
+ const first = (doc.paragraphs?.() ?? [])[0];
5591
+ if (!first) return "";
5592
+ return paragraphMarkdown(first);
5593
+ }
5594
+ /**
5595
+ * Render a paragraph as markdown, replacing internal links with `[[Title]]`.
5596
+ * The streaming orchestrator's link rewriter later swaps `[[Title]]` →
5597
+ * `[[docId|label]]` once IDs are known.
5598
+ */
5599
+ function paragraphMarkdown(paragraph) {
5600
+ const sentences = paragraph.sentences?.() ?? [];
5601
+ const out = [];
5602
+ for (const s of sentences) out.push(sentenceWithWikilinks(s));
5603
+ return out.join(" ").trim();
5604
+ }
5605
+ function sentenceWithWikilinks(sentence) {
5606
+ const text = (sentence.text?.() ?? "").toString();
5607
+ const links = sentence.links?.() ?? [];
5608
+ if (links.length === 0) return text;
5609
+ let result = text;
5610
+ const replacements = links.map((l) => {
5611
+ const page = typeof l.page === "function" ? l.page() : null;
5612
+ const display = typeof l.text === "function" ? l.text() : null;
5613
+ if (typeof page !== "string" || page.length === 0) return null;
5614
+ if (isCategoryTitle(page)) return null;
5615
+ const shown = display && display.length > 0 ? display : page;
5616
+ return {
5617
+ page: canonicalTitle(page),
5618
+ shown
5619
+ };
5620
+ }).filter((x) => x !== null).sort((a, b) => b.shown.length - a.shown.length);
5621
+ for (const { page, shown } of replacements) {
5622
+ if (!result.includes(shown)) continue;
5623
+ const replacement = shown === page ? `[[${page}]]` : `[[${page}|${shown}]]`;
5624
+ result = result.replace(shown, replacement);
5625
+ }
5626
+ return result;
5627
+ }
5628
+ function lineText(line) {
5629
+ if (!line) return "";
5630
+ if (typeof line === "string") return line;
5631
+ if (typeof line.text === "string") return line.text;
5632
+ if (typeof line.text === "function") return line.text();
5633
+ return "";
5634
+ }
5635
+
5636
+ //#endregion
5637
+ //#region packages/cli/src/commands/wiki/render.ts
5638
+ const ICONS = {
5639
+ graph: "git-fork",
5640
+ article: "book-open",
5641
+ category: "tag",
5642
+ infobox: "info",
5643
+ outline: "list",
5644
+ gallery: "images",
5645
+ section: "pilcrow",
5646
+ categories: "tags"
5647
+ };
5648
+ /** Decide a page type for a section based on its shape. */
5649
+ function pickSectionType(section) {
5650
+ if (section.children.length > 0) return {
5651
+ type: "outline",
5652
+ icon: ICONS.outline
5653
+ };
5654
+ if (section.isList && section.listLength >= 5) return {
5655
+ type: "outline",
5656
+ icon: ICONS.outline
5657
+ };
5658
+ return {
5659
+ type: "doc",
5660
+ icon: ICONS.section
5661
+ };
5662
+ }
5663
+ /** Render the lead paragraph as the article-doc body. */
5664
+ function renderArticleLead(article) {
5665
+ return article.lead ?? "";
5666
+ }
5667
+ /** Render the article as a single doc, sections + infobox inlined. */
5668
+ function renderArticleSingleDoc(article) {
5669
+ const parts = [];
5670
+ if (article.lead) parts.push(article.lead);
5671
+ if (article.infobox && article.infobox.length > 0) parts.push("## Infobox", renderInfoboxBody(article.infobox));
5672
+ for (const section of article.sections) parts.push(...renderSectionInline(section, 2));
5673
+ return parts.join("\n\n");
5674
+ }
5675
+ function renderSectionInline(section, level) {
5676
+ const out = [];
5677
+ const prefix = "#".repeat(Math.min(6, level));
5678
+ if (section.title) out.push(`${prefix} ${section.title}`);
5679
+ if (section.body.trim()) out.push(section.body);
5680
+ for (const child of section.children) out.push(...renderSectionInline(child, level + 1));
5681
+ return out;
5682
+ }
5683
+ function renderInfoboxBody(rows) {
5684
+ return rows.map((r) => `- **${r.key}:** ${r.value}`).join("\n");
5685
+ }
5686
+ function renderCategoryBody(members, subcategories) {
5687
+ const parts = [];
5688
+ if (members.length > 0) {
5689
+ parts.push("## Pages");
5690
+ parts.push(members.map((m) => `- [[${m}]]`).join("\n"));
5691
+ }
5692
+ if (subcategories.length > 0) {
5693
+ parts.push("## Sub-categories");
5694
+ parts.push(subcategories.map((s) => `- ${s}`).join("\n"));
5695
+ }
5696
+ return parts.join("\n\n");
5697
+ }
5698
+ /**
5699
+ * Replace `[[Title]]` / `[[Title|Alias]]` in markdown with
5700
+ * `[[docId|label]]` using the title→docId map. Unresolved titles fall
5701
+ * back to plain text (their alias or original title).
5702
+ */
5703
+ function rewriteLinks(markdown, titleToDocId) {
5704
+ return markdown.replace(/\[\[([^\]|]+?)(?:\|([^\]]+?))?\]\]/g, (_match, target, alias) => {
5705
+ const title = target.trim();
5706
+ const docId = titleToDocId.get(title);
5707
+ const display = (alias && alias.trim().length > 0 ? alias : title).trim();
5708
+ if (!docId) return display;
5709
+ return `[[${docId}|${display}]]`;
5710
+ });
5711
+ }
5712
+
5713
+ //#endregion
5714
+ //#region packages/cli/src/commands/wiki/connect.ts
5715
+ /**
5716
+ * Open a DocumentManager session for the wiki command, mirroring the
5717
+ * auth/register flow that CLIConnection uses but using the modern public API.
5718
+ *
5719
+ * Reuses the CLI's Ed25519 keypair handling (loadOrCreateKeypair, signChallenge)
5720
+ * so the wiki command authenticates with the same identity as every other
5721
+ * subcommand.
5722
+ */
5723
+ async function openSession(config) {
5724
+ const keypair = await loadOrCreateKeypair(config.keyFile);
5725
+ const sign = (challenge) => Promise.resolve(signChallenge(challenge, keypair.privateKey));
5726
+ const dm = new _abraca_dabra.DocumentManager({
5727
+ url: config.url,
5728
+ name: config.name ?? "Wiki Extractor",
5729
+ color: config.color,
5730
+ quiet: config.quiet
5731
+ });
5732
+ try {
5733
+ await dm.client.loginWithKey(keypair.publicKeyB64, sign);
5734
+ } catch (err) {
5735
+ const status = err?.status ?? err?.response?.status;
5736
+ if (status === 404 || status === 422) {
5737
+ if (!config.quiet) console.error("[abracadabra] Key not registered, creating new account...");
5738
+ await dm.client.registerWithKey({
5739
+ publicKey: keypair.publicKeyB64,
5740
+ username: (config.name ?? "wiki-extractor").replace(/\s+/g, "-").toLowerCase(),
5741
+ displayName: config.name ?? "Wiki Extractor",
5742
+ deviceName: "CLI Wiki",
5743
+ inviteCode: config.inviteCode
5744
+ });
5745
+ await dm.client.loginWithKey(keypair.publicKeyB64, sign);
5746
+ } else throw err;
5747
+ }
5748
+ await dm.connect();
5749
+ const rootDocId = dm.rootDocId;
5750
+ if (!rootDocId) throw new Error("Connected but no rootDocId — server has no spaces.");
5751
+ return {
5752
+ dm,
5753
+ rootDocId
5754
+ };
5755
+ }
5756
+
5757
+ //#endregion
5758
+ //#region packages/cli/src/commands/wiki/index.ts
5759
+ registerCommand({
5760
+ name: "wiki",
5761
+ aliases: ["wikipedia"],
5762
+ description: "Fetch Wikipedia articles into a graph of docs (streaming).",
5763
+ usage: [
5764
+ "wiki \"<Article Title>\"",
5765
+ " mode=single|split single doc per article OR split into sections+infobox [split]",
5766
+ " depth=<n> follow internal links to depth N [1]",
5767
+ " category-depth=<n> recurse into sub-categories [1]",
5768
+ " lang=<code> wiki language [en]",
5769
+ " domain=<host> 3rd-party MediaWiki host (overrides lang)",
5770
+ " parent=<docId> parent doc for the new graph [active space root]",
5771
+ " user-agent=<str> Api-User-Agent header (REQUIRED by Wikimedia etiquette)",
5772
+ " rate=<rps> max wikipedia requests per second [3]",
5773
+ " --include-categories expand each article's categories into nested graphs",
5774
+ " --dry-run fetch only the entry article, print outline, no writes"
5775
+ ].join("\n"),
5776
+ async run(_conn, args) {
5777
+ const opts = parseOptions(args);
5778
+ if (typeof opts === "string") return opts;
5779
+ const log = (msg) => {
5780
+ if (!args.flags.has("quiet") && !args.flags.has("q")) console.error(`[wiki] ${msg}`);
5781
+ };
5782
+ const wp = new WikipediaClient({
5783
+ lang: opts.lang,
5784
+ domain: opts.domain,
5785
+ userAgent: opts.userAgent,
5786
+ rate: opts.rate
5787
+ });
5788
+ if (opts.dryRun) {
5789
+ log(`fetch ${opts.title}`);
5790
+ const doc = await wp.fetchArticle(opts.title);
5791
+ if (!doc) return `Article not found: "${opts.title}"`;
5792
+ const snap = snapshotArticle(doc, canonicalTitle(doc.title?.() ?? opts.title));
5793
+ return [
5794
+ `Entry: ${snap.title}`,
5795
+ `URL: ${snap.url ?? "(none)"}`,
5796
+ `Internal links: ${snap.linkTitles.length}`,
5797
+ `Categories: ${snap.categories.length}`,
5798
+ `Sections: ${snap.sections.length}`,
5799
+ `Has infobox: ${snap.infobox && snap.infobox.length > 0 ? "yes" : "no"}`,
5800
+ "",
5801
+ "── Sections ──",
5802
+ printSections(snap.sections, "")
5803
+ ].join("\n");
5804
+ }
5805
+ const env = globalThis.process?.env ?? {};
5806
+ const url = env["ABRA_URL"];
5807
+ if (!url) return "ABRA_URL is required to write to the server. Set it or pass --dry-run.";
5808
+ const { dm } = await openSession({
5809
+ url,
5810
+ name: env["ABRA_NAME"],
5811
+ color: env["ABRA_COLOR"],
5812
+ inviteCode: env["ABRA_INVITE_CODE"],
5813
+ keyFile: env["ABRA_KEY_FILE"],
5814
+ quiet: args.flags.has("quiet") || args.flags.has("q")
5815
+ });
5816
+ try {
5817
+ const result = await runStreaming(dm, wp, opts, log);
5818
+ return [`Done. Created ${result.articleCount} articles${result.categoryCount > 0 ? ` + ${result.categoryCount} categories` : ""}.`, `Root: ${result.rootDocId}`].join("\n");
5819
+ } finally {
5820
+ await dm.destroy().catch(() => {});
5821
+ }
5822
+ }
5823
+ });
5824
+ async function runStreaming(dm, wp, opts, log) {
5825
+ const titleToDocId = /* @__PURE__ */ new Map();
5826
+ const fetched = /* @__PURE__ */ new Map();
5827
+ const childrenCreated = /* @__PURE__ */ new Set();
5828
+ const categoryToDocId = /* @__PURE__ */ new Map();
5829
+ let categoriesContainerId = null;
5830
+ log(`fetch ${opts.title}`);
5831
+ const entryDoc = await wp.fetchArticle(opts.title);
5832
+ if (!entryDoc) throw new Error(`Article not found: "${opts.title}"`);
5833
+ const entryTitle = canonicalTitle(entryDoc.title?.() ?? opts.title);
5834
+ const entrySnap = snapshotArticle(entryDoc, entryTitle);
5835
+ fetched.set(entryTitle, entrySnap);
5836
+ const rootEntry = dm.tree.create({
5837
+ parentId: opts.parentDocId ?? null,
5838
+ label: entryTitle,
5839
+ type: "graph",
5840
+ meta: { icon: ICONS.graph }
5841
+ });
5842
+ log(`+ ${rootEntry.id.slice(0, 8)}… ${entryTitle} (graph)`);
5843
+ const entryArticleId = createArticleShell(dm, entrySnap, rootEntry.id, log);
5844
+ titleToDocId.set(entryTitle, entryArticleId);
5845
+ const queue = [{
5846
+ title: entryTitle,
5847
+ depth: 0
5848
+ }];
5849
+ let articleCount = 0;
5850
+ while (queue.length > 0) {
5851
+ const { title, depth } = queue.shift();
5852
+ const articleDocId = titleToDocId.get(title);
5853
+ let snap = fetched.get(title);
5854
+ if (!snap) {
5855
+ log(`fetch [d${depth}] ${title}`);
5856
+ try {
5857
+ const doc = await wp.fetchArticle(title);
5858
+ if (!doc) {
5859
+ log(` not found — leaving stub`);
5860
+ continue;
5861
+ }
5862
+ snap = snapshotArticle(doc, canonicalTitle(doc.title?.() ?? title));
5863
+ fetched.set(title, snap);
5864
+ } catch (err) {
5865
+ log(`! fetch failed: ${err?.message ?? err}`);
5866
+ continue;
5867
+ }
5868
+ }
5869
+ if (opts.mode === "split" && !childrenCreated.has(title)) {
5870
+ createArticleChildren(dm, snap, articleDocId, log);
5871
+ childrenCreated.add(title);
5872
+ }
5873
+ if (depth < opts.depth) for (const linkTitle of snap.linkTitles) {
5874
+ if (titleToDocId.has(linkTitle)) continue;
5875
+ const shell = dm.tree.create({
5876
+ parentId: rootEntry.id,
5877
+ label: linkTitle,
5878
+ type: "doc",
5879
+ meta: { icon: ICONS.article }
5880
+ });
5881
+ titleToDocId.set(linkTitle, shell.id);
5882
+ queue.push({
5883
+ title: linkTitle,
5884
+ depth: depth + 1
5885
+ });
5886
+ log(`+ ${shell.id.slice(0, 8)}… ${linkTitle} (doc, shell)`);
5887
+ }
5888
+ if (opts.includeCategories && snap.categories.length > 0) {
5889
+ if (!categoriesContainerId) {
5890
+ const c = dm.tree.create({
5891
+ parentId: rootEntry.id,
5892
+ label: "Categories",
5893
+ type: "graph",
5894
+ meta: { icon: ICONS.categories }
5895
+ });
5896
+ categoriesContainerId = c.id;
5897
+ log(`+ ${c.id.slice(0, 8)}… Categories (graph)`);
5898
+ }
5899
+ for (const catTitle of snap.categories) {
5900
+ if (categoryToDocId.has(catTitle)) continue;
5901
+ const cat = dm.tree.create({
5902
+ parentId: categoriesContainerId,
5903
+ label: prettyCategoryLabel(catTitle),
5904
+ type: "graph",
5905
+ meta: { icon: ICONS.category }
5906
+ });
5907
+ categoryToDocId.set(catTitle, cat.id);
5908
+ log(`+ ${cat.id.slice(0, 8)}… ${prettyCategoryLabel(catTitle)} (graph, cat)`);
5909
+ }
5910
+ }
5911
+ const body = opts.mode === "split" ? renderArticleLead(snap) : renderArticleSingleDoc(snap);
5912
+ if (body.trim().length > 0) {
5913
+ const rewritten = rewriteLinks(body, titleToDocId);
5914
+ try {
5915
+ await dm.content.write(articleDocId, rewritten);
5916
+ log(`✓ body ${title}`);
5917
+ } catch (err) {
5918
+ log(`! body write failed for ${title}: ${err?.message ?? err}`);
5919
+ }
5920
+ }
5921
+ if (opts.mode === "split") await writeChildrenBodies(dm, snap, articleDocId, titleToDocId, log);
5922
+ articleCount++;
5923
+ }
5924
+ let categoryCount = 0;
5925
+ if (opts.includeCategories && categoryToDocId.size > 0) for (const [catTitle, catDocId] of categoryToDocId) {
5926
+ log(`category ${catTitle}`);
5927
+ try {
5928
+ const members = await wp.fetchCategoryPages(catTitle, opts.categoryDepth > 0, Math.max(0, opts.categoryDepth));
5929
+ const memberArticles = [];
5930
+ const subcats = [];
5931
+ for (const m of members) if (m.type === "subcat") subcats.push(prettyCategoryLabel(m.title));
5932
+ else memberArticles.push(m.title);
5933
+ const rewritten = rewriteLinks(renderCategoryBody(memberArticles, subcats), titleToDocId);
5934
+ if (rewritten.trim().length > 0) {
5935
+ await dm.content.write(catDocId, rewritten);
5936
+ log(`✓ body category ${catTitle}`);
5937
+ }
5938
+ categoryCount++;
5939
+ } catch (err) {
5940
+ log(`! category ${catTitle}: ${err?.message ?? err}`);
5941
+ }
5942
+ }
5943
+ return {
5944
+ rootDocId: rootEntry.id,
5945
+ articleCount,
5946
+ categoryCount
5947
+ };
5948
+ }
5949
+ function createArticleShell(dm, article, parentId, log) {
5950
+ const meta = { icon: ICONS.article };
5951
+ if (article.url) meta.url = article.url;
5952
+ const entry = dm.tree.create({
5953
+ parentId,
5954
+ label: article.title,
5955
+ type: "doc",
5956
+ meta
5957
+ });
5958
+ log(`+ ${entry.id.slice(0, 8)}… ${article.title} (doc)`);
5959
+ return entry.id;
5960
+ }
5961
+ /**
5962
+ * Create section + infobox child docs for a split-mode article. Returns nothing
5963
+ * — children get bodies written later in writeChildrenBodies.
5964
+ */
5965
+ function createArticleChildren(dm, article, articleDocId, log) {
5966
+ if (article.infobox && article.infobox.length > 0) {
5967
+ const ib = dm.tree.create({
5968
+ parentId: articleDocId,
5969
+ label: "Infobox",
5970
+ type: "outline",
5971
+ meta: { icon: ICONS.infobox }
5972
+ });
5973
+ log(` + ${ib.id.slice(0, 8)}… Infobox (outline)`);
5974
+ article._infoboxDocId = ib.id;
5975
+ }
5976
+ for (const section of article.sections) createSectionShell(dm, section, articleDocId, log);
5977
+ }
5978
+ function createSectionShell(dm, section, parentDocId, log) {
5979
+ const hasChildren = section.children.length > 0;
5980
+ if (!section.body.trim() && !hasChildren) return;
5981
+ const { type, icon } = pickSectionType(section);
5982
+ const entry = dm.tree.create({
5983
+ parentId: parentDocId,
5984
+ label: section.title || "Untitled section",
5985
+ type,
5986
+ meta: { icon }
5987
+ });
5988
+ log(` + ${entry.id.slice(0, 8)}… ${entry.label} (${type})`);
5989
+ section._docId = entry.id;
5990
+ for (const child of section.children) createSectionShell(dm, child, entry.id, log);
5991
+ }
5992
+ async function writeChildrenBodies(dm, article, _articleDocId, titleToDocId, log) {
5993
+ const infoboxDocId = article._infoboxDocId;
5994
+ if (infoboxDocId && article.infobox && article.infobox.length > 0) try {
5995
+ await dm.content.write(infoboxDocId, renderInfoboxBody(article.infobox));
5996
+ } catch (err) {
5997
+ log(`! infobox body write failed: ${err?.message ?? err}`);
5998
+ }
5999
+ for (const section of article.sections) await writeSectionBody(dm, section, titleToDocId, log);
6000
+ }
6001
+ async function writeSectionBody(dm, section, titleToDocId, log) {
6002
+ const docId = section._docId;
6003
+ if (docId && section.body.trim().length > 0) try {
6004
+ await dm.content.write(docId, rewriteLinks(section.body, titleToDocId));
6005
+ } catch (err) {
6006
+ log(`! section body write failed for ${section.title}: ${err?.message ?? err}`);
6007
+ }
6008
+ for (const child of section.children) await writeSectionBody(dm, child, titleToDocId, log);
6009
+ }
6010
+ function parseOptions(args) {
6011
+ const title = args.positional[0]?.trim() || args.params["title"];
6012
+ if (!title) return "Missing required positional argument: <title>. Example: abracadabra wiki \"Toronto Raptors\"";
6013
+ const env = globalThis.process?.env ?? {};
6014
+ const userAgent = args.params["user-agent"] || args.params["userAgent"] || env["ABRA_WIKI_USER_AGENT"];
6015
+ if (!userAgent) return ["Missing required parameter: user-agent=\"your-name (you@example.com)\"", "(Wikimedia etiquette requires an Api-User-Agent header. Pass user-agent=... or set ABRA_WIKI_USER_AGENT.)"].join("\n");
6016
+ const mode = args.params["mode"] ?? "split";
6017
+ if (mode !== "single" && mode !== "split") return `Invalid mode "${mode}". Use mode=single or mode=split.`;
6018
+ const depth = parseIntOr(args.params["depth"], 1);
6019
+ const categoryDepth = parseIntOr(args.params["category-depth"] ?? args.params["categoryDepth"], 1);
6020
+ const rate = parseFloatOr(args.params["rate"], 3);
6021
+ return {
6022
+ title,
6023
+ mode,
6024
+ depth,
6025
+ categoryDepth,
6026
+ includeCategories: args.flags.has("include-categories") || args.flags.has("includeCategories"),
6027
+ lang: args.params["lang"] ?? "en",
6028
+ domain: args.params["domain"],
6029
+ parentDocId: args.params["parent"],
6030
+ userAgent,
6031
+ rate,
6032
+ dryRun: args.flags.has("dry-run") || args.flags.has("dryRun")
6033
+ };
6034
+ }
6035
+ function parseIntOr(s, fallback) {
6036
+ if (!s) return fallback;
6037
+ const n = Number.parseInt(s, 10);
6038
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
6039
+ }
6040
+ function parseFloatOr(s, fallback) {
6041
+ if (!s) return fallback;
6042
+ const n = Number.parseFloat(s);
6043
+ return Number.isFinite(n) && n > 0 ? n : fallback;
6044
+ }
6045
+ function printSections(sections, indent) {
6046
+ const lines = [];
6047
+ for (const s of sections) {
6048
+ const hint = s.body ? ` (${s.body.length}b)` : "";
6049
+ lines.push(`${indent}- ${s.title}${hint}${s.children.length > 0 ? ` [${s.children.length} sub]` : ""}`);
6050
+ if (s.children.length > 0) lines.push(printSections(s.children, indent + " "));
6051
+ }
6052
+ return lines.join("\n");
6053
+ }
6054
+
4116
6055
  //#endregion
4117
6056
  //#region packages/cli/src/index.ts
4118
6057
  /**
@@ -4136,7 +6075,9 @@ const NO_CONNECT_COMMANDS = new Set([
4136
6075
  "v",
4137
6076
  "page-types",
4138
6077
  "types",
4139
- "doctypes"
6078
+ "doctypes",
6079
+ "wiki",
6080
+ "wikipedia"
4140
6081
  ]);
4141
6082
  async function main() {
4142
6083
  const args = parseArgs(process.argv);