@abraca/cli 2.4.0 → 2.6.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.
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # @abraca/cli
2
+
3
+ The `abracadabra` command-line client — read and mutate a space's tree, content, metadata, permissions, files, and presence from a terminal, plus a streaming Wikipedia importer.
4
+
5
+ ## Documentation
6
+
7
+ Full, code-derived documentation lives in [`docs/`](docs/) — parsing/dispatch,
8
+ connection & auth, the full command reference (read vs mutate), and the Wikipedia
9
+ importer. It is the source of truth.
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ ABRA_URL=https://my-server.example.com abracadabra tree
15
+ abracadabra read name="My Page"
16
+ abracadabra write name="My Page" content="# Hello" mode=replace
17
+ ```
18
+
19
+ `abracadabra [--flag] <command>[:<sub>] [key=value ...]`. Identity is an Ed25519 key file (`ABRA_KEY_FILE`, default `~/.abracadabra/cli.key`); auto-registers on first run. Docs are addressed via `id=`/`name=`/`path=`/positional. Output `--format=json|tsv|tree|text|md`.
20
+
21
+ ::
22
+
23
+ > `abracadabra version` reports a hardcoded `1.0.0` (≠ package `2.4.0`). `delete` is a reversible soft-delete; `duplicate` is shallow; `chat` is a stateless `messages:send`. See [`docs/2.commands/reference`](docs/2.commands/1.reference.md).
24
+
25
+ ## License
26
+
27
+ MIT.
@@ -1440,6 +1440,23 @@ function parseFrontmatter(markdown) {
1440
1440
  body
1441
1441
  };
1442
1442
  }
1443
+ function pushNested(out, inner, wrap) {
1444
+ const children = parseInline(inner);
1445
+ if (children.length === 0) {
1446
+ out.push({
1447
+ text: inner,
1448
+ attrs: { ...wrap }
1449
+ });
1450
+ return;
1451
+ }
1452
+ for (const child of children) out.push({
1453
+ text: child.text,
1454
+ attrs: {
1455
+ ...child.attrs ?? {},
1456
+ ...wrap
1457
+ }
1458
+ });
1459
+ }
1443
1460
  function parseInline(text) {
1444
1461
  const stripped = text.replace(/\{lang="[^"]*"\}/g, "").replace(/:(?!badge|icon|kbd)(\w[\w-]*)\[([^\]]*)\](\{[^}]*\})?/g, "$2").replace(/:(?!badge|icon|kbd)(\w[\w-]*)(\{[^}]*\})/g, "");
1445
1462
  const tokens = [];
@@ -1488,22 +1505,10 @@ function parseInline(text) {
1488
1505
  text: label,
1489
1506
  attrs: { docLink: { docId } }
1490
1507
  });
1491
- } else if (match[10] !== void 0) tokens.push({
1492
- text: match[10],
1493
- attrs: { strike: true }
1494
- });
1495
- else if (match[11] !== void 0) tokens.push({
1496
- text: match[11],
1497
- attrs: { bold: true }
1498
- });
1499
- else if (match[12] !== void 0) tokens.push({
1500
- text: match[12],
1501
- attrs: { italic: true }
1502
- });
1503
- else if (match[13] !== void 0) tokens.push({
1504
- text: match[13],
1505
- attrs: { italic: true }
1506
- });
1508
+ } else if (match[10] !== void 0) pushNested(tokens, match[10], { strike: true });
1509
+ else if (match[11] !== void 0) pushNested(tokens, match[11], { bold: true });
1510
+ else if (match[12] !== void 0) pushNested(tokens, match[12], { italic: true });
1511
+ else if (match[13] !== void 0) pushNested(tokens, match[13], { italic: true });
1507
1512
  else if (match[14] !== void 0) tokens.push({
1508
1513
  text: match[14],
1509
1514
  attrs: { code: true }
@@ -2039,11 +2044,19 @@ function parseBlocks(markdown) {
2039
2044
  function fillTextInto(el, tokens) {
2040
2045
  const filtered = tokens.filter((t) => t.text.length > 0);
2041
2046
  if (!filtered.length) return;
2042
- const xtNodes = filtered.map(() => new yjs.XmlText());
2043
- el.insert(0, xtNodes);
2047
+ const children = filtered.map((tok) => {
2048
+ return (tok.attrs?.docLink)?.docId ? new yjs.XmlElement("docLink") : new yjs.XmlText();
2049
+ });
2050
+ el.insert(0, children);
2044
2051
  filtered.forEach((tok, i) => {
2045
- if (tok.attrs) xtNodes[i].insert(0, tok.text, tok.attrs);
2046
- else xtNodes[i].insert(0, tok.text);
2052
+ const node = children[i];
2053
+ if (node instanceof yjs.XmlElement) {
2054
+ const dl = tok.attrs.docLink;
2055
+ node.setAttribute("docId", dl.docId);
2056
+ return;
2057
+ }
2058
+ if (tok.attrs) node.insert(0, tok.text, tok.attrs);
2059
+ else node.insert(0, tok.text);
2047
2060
  });
2048
2061
  }
2049
2062
  function blockElName(b) {
@@ -2399,6 +2412,15 @@ function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled"
2399
2412
 
2400
2413
  //#endregion
2401
2414
  //#region packages/convert/src/yjs-to-markdown.ts
2415
+ function isXElem(n) {
2416
+ return !!n && typeof n.nodeName === "string";
2417
+ }
2418
+ function isXText(n) {
2419
+ return !!n && typeof n.nodeName !== "string" && typeof n.toDelta === "function";
2420
+ }
2421
+ function localizeFragment(fragment) {
2422
+ return fragment;
2423
+ }
2402
2424
  function serializeDelta(delta) {
2403
2425
  let result = "";
2404
2426
  for (const op of delta) {
@@ -2459,12 +2481,15 @@ function serializeDelta(delta) {
2459
2481
  }
2460
2482
  function serializeInline(el) {
2461
2483
  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));
2484
+ for (const child of el.toArray()) if (isXText(child)) parts.push(serializeDelta(child.toDelta()));
2485
+ else if (isXElem(child)) if (child.nodeName === "docLink") {
2486
+ const docId = child.getAttribute("docId") ?? "";
2487
+ parts.push(`[[${docId}]]`);
2488
+ } else parts.push(serializeInline(child));
2464
2489
  return parts.join("");
2465
2490
  }
2466
2491
  function serializeBlock(el, indent = "") {
2467
- if (el instanceof yjs.XmlText) return serializeDelta(el.toDelta());
2492
+ if (isXText(el)) return serializeDelta(el.toDelta());
2468
2493
  switch (el.nodeName) {
2469
2494
  case "documentHeader":
2470
2495
  case "documentMeta": return "";
@@ -2484,7 +2509,7 @@ function serializeBlock(el, indent = "") {
2484
2509
  }
2485
2510
  case "blockquote": {
2486
2511
  const lines = [];
2487
- for (const child of el.toArray()) if (child instanceof yjs.XmlElement) {
2512
+ for (const child of el.toArray()) if (isXElem(child)) {
2488
2513
  const text = serializeBlock(child);
2489
2514
  for (const line of text.split("\n")) lines.push(`> ${line}`);
2490
2515
  }
@@ -2545,11 +2570,11 @@ function serializeBlock(el, indent = "") {
2545
2570
  if (to) props.push(`to="${to}"`);
2546
2571
  return `::card${props.length ? `{${props.join(" ")}}` : ""}\n${serializeChildren(el)}\n::`;
2547
2572
  }
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::`;
2573
+ case "cardGroup": return `::card-group\n${el.toArray().filter((c) => isXElem(c)).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
2574
+ case "codeCollapse": return `::code-collapse\n${el.toArray().filter((c) => isXElem(c) && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
2575
+ case "codeGroup": return `::code-group\n${el.toArray().filter((c) => isXElem(c) && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
2551
2576
  case "codePreview": {
2552
- const children = el.toArray().filter((c) => c instanceof yjs.XmlElement);
2577
+ const children = el.toArray().filter((c) => isXElem(c));
2553
2578
  const nonCode = children.filter((c) => c.nodeName !== "codeBlock").map((c) => serializeBlock(c)).join("\n\n");
2554
2579
  const code = children.filter((c) => c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n");
2555
2580
  const parts = [nonCode];
@@ -2567,16 +2592,16 @@ function serializeBlock(el, indent = "") {
2567
2592
  if (required === true || required === "true") props.push("required=\"true\"");
2568
2593
  return `::field{${props.join(" ")}}\n${serializeChildren(el)}\n::`;
2569
2594
  }
2570
- case "fieldGroup": return `::field-group\n${el.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
2595
+ case "fieldGroup": return `::field-group\n${el.toArray().filter((c) => isXElem(c)).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
2571
2596
  default: return serializeChildren(el);
2572
2597
  }
2573
2598
  }
2574
2599
  function serializeChildren(el) {
2575
2600
  const blocks = [];
2576
- for (const child of el.toArray()) if (child instanceof yjs.XmlElement) {
2601
+ for (const child of el.toArray()) if (isXElem(child)) {
2577
2602
  const text = serializeBlock(child);
2578
2603
  if (text) blocks.push(text);
2579
- } else if (child instanceof yjs.XmlText) {
2604
+ } else if (isXText(child)) {
2580
2605
  const text = serializeDelta(child.toDelta());
2581
2606
  if (text) blocks.push(text);
2582
2607
  }
@@ -2586,11 +2611,11 @@ function serializeListItems(el, type, indent) {
2586
2611
  const lines = [];
2587
2612
  let counter = 1;
2588
2613
  for (const child of el.toArray()) {
2589
- if (!(child instanceof yjs.XmlElement) || child.nodeName !== "listItem") continue;
2614
+ if (!isXElem(child) || child.nodeName !== "listItem") continue;
2590
2615
  const prefix = type === "bullet" ? "- " : `${counter++}. `;
2591
2616
  const subParts = [];
2592
2617
  for (const sub of child.toArray()) {
2593
- if (!(sub instanceof yjs.XmlElement)) continue;
2618
+ if (!isXElem(sub)) continue;
2594
2619
  if (sub.nodeName === "bulletList") subParts.push(serializeListItems(sub, "bullet", indent + " "));
2595
2620
  else if (sub.nodeName === "orderedList") subParts.push(serializeListItems(sub, "ordered", indent + " "));
2596
2621
  else subParts.push(serializeInline(sub));
@@ -2606,13 +2631,13 @@ function serializeListItems(el, type, indent) {
2606
2631
  function serializeTaskList(el, indent) {
2607
2632
  const lines = [];
2608
2633
  for (const child of el.toArray()) {
2609
- if (!(child instanceof yjs.XmlElement) || child.nodeName !== "taskItem") continue;
2634
+ if (!isXElem(child) || child.nodeName !== "taskItem") continue;
2610
2635
  const checked = child.getAttribute("checked");
2611
2636
  const marker = checked === true || checked === "true" ? "[x]" : "[ ]";
2612
2637
  let header = "";
2613
2638
  const nestedParts = [];
2614
2639
  for (const sub of child.toArray()) {
2615
- if (!(sub instanceof yjs.XmlElement)) continue;
2640
+ if (!isXElem(sub)) continue;
2616
2641
  if (sub.nodeName === "paragraph" && header === "") header = serializeInline(sub);
2617
2642
  else if (sub.nodeName === "bulletList") nestedParts.push(serializeListItems(sub, "bullet", indent + " "));
2618
2643
  else if (sub.nodeName === "orderedList") nestedParts.push(serializeListItems(sub, "ordered", indent + " "));
@@ -2625,16 +2650,16 @@ function serializeTaskList(el, indent) {
2625
2650
  return lines.join("\n");
2626
2651
  }
2627
2652
  function getCodeBlockText(el) {
2628
- for (const child of el.toArray()) if (child instanceof yjs.XmlText) return child.toString();
2653
+ for (const child of el.toArray()) if (isXText(child)) return child.toString();
2629
2654
  return "";
2630
2655
  }
2631
2656
  function serializeTable(el) {
2632
- const rows = el.toArray().filter((c) => c instanceof yjs.XmlElement);
2657
+ const rows = el.toArray().filter((c) => isXElem(c));
2633
2658
  if (!rows.length) return "";
2634
2659
  const serializedRows = [];
2635
2660
  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(" ");
2661
+ const cells = row.toArray().filter((c) => isXElem(c)).map((cell) => {
2662
+ return cell.toArray().filter((c) => isXElem(c)).map((c) => serializeInline(c)).join(" ");
2638
2663
  });
2639
2664
  serializedRows.push(cells);
2640
2665
  }
@@ -2653,7 +2678,7 @@ function serializeTable(el) {
2653
2678
  ].join("\n");
2654
2679
  }
2655
2680
  function serializeSlottedContainer(el, containerName, childName, slotPrefix) {
2656
- return `::${containerName}\n${el.toArray().filter((c) => c instanceof yjs.XmlElement && c.nodeName === childName).map((item) => {
2681
+ return `::${containerName}\n${el.toArray().filter((c) => isXElem(c) && c.nodeName === childName).map((item) => {
2657
2682
  const label = item.getAttribute("label") ?? "";
2658
2683
  const icon = item.getAttribute("icon") ?? "";
2659
2684
  const props = [];
@@ -2703,6 +2728,7 @@ function escapeYaml(s) {
2703
2728
  return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
2704
2729
  }
2705
2730
  function yjsToMarkdown(fragment, label, meta, type) {
2731
+ fragment = localizeFragment(fragment);
2706
2732
  const { text: headerText, source: titleSource } = readDocumentHeader(fragment);
2707
2733
  const effectiveTitle = headerText || label;
2708
2734
  const docMeta = readDocumentMeta(fragment);
@@ -2726,7 +2752,7 @@ function readDocumentMeta(fragment) {
2726
2752
  const meta = {};
2727
2753
  let type;
2728
2754
  for (const child of fragment.toArray()) {
2729
- if (!(child instanceof yjs.XmlElement) || child.nodeName !== "documentMeta") continue;
2755
+ if (!isXElem(child) || child.nodeName !== "documentMeta") continue;
2730
2756
  const attrs = child.getAttributes();
2731
2757
  for (const k of Object.keys(attrs)) {
2732
2758
  const v = attrs[k];
@@ -2746,8 +2772,8 @@ function readDocumentMeta(fragment) {
2746
2772
  }
2747
2773
  function readDocumentHeader(fragment) {
2748
2774
  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);
2775
+ if (!isXElem(child) || child.nodeName !== "documentHeader") continue;
2776
+ const text = child.toArray().find((c) => isXText(c));
2751
2777
  const src = child.getAttribute("titleSource");
2752
2778
  const source = src === "h1" || src === "frontmatter" ? src : void 0;
2753
2779
  return {
@@ -2760,7 +2786,7 @@ function readDocumentHeader(fragment) {
2760
2786
  function collectBodyBlocks(fragment) {
2761
2787
  const out = [];
2762
2788
  for (const child of fragment.toArray()) {
2763
- if (!(child instanceof yjs.XmlElement)) continue;
2789
+ if (!isXElem(child)) continue;
2764
2790
  if (child.nodeName === "documentHeader" || child.nodeName === "documentMeta") continue;
2765
2791
  out.push(child);
2766
2792
  }
@@ -3704,17 +3730,17 @@ registerCommand({
3704
3730
  const docId = resolveDocument(conn, args.params, args.positional);
3705
3731
  if (!docId) return "Document not found. Specify id=, name=, or path= to identify the document.";
3706
3732
  try {
3707
- const { title, markdown } = yjsToMarkdown((await conn.getChildProvider(docId)).document.getXmlFragment("default"));
3733
+ const markdown = yjsToMarkdown((await conn.getChildProvider(docId)).document.getXmlFragment("default"), "");
3708
3734
  if (args.flags.has("json") || args.params["format"] === "json") {
3709
3735
  const treeMap = conn.getTreeMap();
3710
- let label = title;
3736
+ let label = "";
3711
3737
  let type;
3712
3738
  let meta;
3713
3739
  let children = [];
3714
3740
  if (treeMap) {
3715
3741
  const entry = treeMap.get(docId);
3716
3742
  if (entry) {
3717
- label = entry.label || title;
3743
+ label = entry.label || label;
3718
3744
  type = entry.type;
3719
3745
  meta = entry.meta;
3720
3746
  }
@@ -4029,7 +4055,7 @@ registerCommand({
4029
4055
  try {
4030
4056
  const doc = (await conn.getChildProvider(docId)).document;
4031
4057
  const fragment = doc.getXmlFragment("default");
4032
- const { markdown: existing } = yjsToMarkdown(fragment);
4058
+ const existing = yjsToMarkdown(fragment, "");
4033
4059
  const text = content.replace(/\\n/g, "\n").replace(/\\t/g, " ");
4034
4060
  const combined = args.flags.has("inline") ? text + existing : text + "\n" + existing;
4035
4061
  doc.transact(() => {
@@ -4052,7 +4078,7 @@ registerCommand({
4052
4078
  const docId = resolveDocument(conn, args.params, args.positional);
4053
4079
  if (!docId) return "Document not found.";
4054
4080
  try {
4055
- const { markdown } = yjsToMarkdown((await conn.getChildProvider(docId)).document.getXmlFragment("default"));
4081
+ const markdown = yjsToMarkdown((await conn.getChildProvider(docId)).document.getXmlFragment("default"), "");
4056
4082
  const words = markdown.split(/\s+/).filter(Boolean).length;
4057
4083
  const chars = markdown.length;
4058
4084
  if (args.flags.has("words")) return String(words);
@@ -4074,7 +4100,9 @@ registerCommand({
4074
4100
  const outputPath = args.params["output"] || args.params["to"] || args.params["path"];
4075
4101
  if (!outputPath) return "Missing required parameter: output=<filepath>";
4076
4102
  try {
4077
- const { title, markdown } = yjsToMarkdown((await conn.getChildProvider(docId)).document.getXmlFragment("default"));
4103
+ const fragment = (await conn.getChildProvider(docId)).document.getXmlFragment("default");
4104
+ const title = (conn.getTreeMap()?.get(docId))?.label;
4105
+ const markdown = yjsToMarkdown(fragment, title ?? "");
4078
4106
  const resolvedPath = node_path.resolve(outputPath);
4079
4107
  node_fs.writeFileSync(resolvedPath, markdown, "utf-8");
4080
4108
  return `Exported "${title || "document"}" to ${resolvedPath} (${Buffer.byteLength(markdown)} bytes)`;