@tensorix/metahub 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1116,7 +1116,7 @@ var init_crdt = __esm(() => {
1116
1116
  },
1117
1117
  documents: {
1118
1118
  table: "documents",
1119
- cols: new Set(["title", "body", "database_id", "parent_id", "created_hlc", "__deleted"])
1119
+ cols: new Set(["title", "body", "database_id", "parent_id", "created_hlc", "order_key", "__deleted"])
1120
1120
  },
1121
1121
  doc_blocks: {
1122
1122
  table: "doc_blocks",
@@ -1172,6 +1172,24 @@ function inferContentType(path) {
1172
1172
  const ext = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
1173
1173
  return MIME[ext] ?? "application/octet-stream";
1174
1174
  }
1175
+ function normalizeSiteName(raw) {
1176
+ const slug = raw.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
1177
+ if (!slug)
1178
+ throw new Error(`invalid site name: ${JSON.stringify(raw)}`);
1179
+ return slug;
1180
+ }
1181
+ function normalizeSitePath(raw) {
1182
+ const segs = [];
1183
+ for (const seg of raw.split("/")) {
1184
+ if (seg === "" || seg === ".")
1185
+ continue;
1186
+ if (seg === "..")
1187
+ segs.pop();
1188
+ else
1189
+ segs.push(seg);
1190
+ }
1191
+ return segs.join("/");
1192
+ }
1175
1193
  function isTextType(contentType) {
1176
1194
  const ct = contentType.toLowerCase();
1177
1195
  return ct.startsWith("text/") || ct.startsWith("application/json") || ct.startsWith("application/manifest+json") || ct.startsWith("application/xml") || ct.startsWith("text/javascript") || ct.startsWith("application/javascript") || ct.startsWith("image/svg+xml");
@@ -1182,10 +1200,11 @@ function toBytes(data) {
1182
1200
  return data instanceof Uint8Array ? data : new Uint8Array(data);
1183
1201
  }
1184
1202
  function createSite(db, opts) {
1185
- if (getSiteByName(db, opts.name))
1186
- throw new Error(`site name already exists: ${opts.name}`);
1187
- const id = newId("site", opts.name);
1188
- const first = emit(db, "sites", id, "name", opts.name);
1203
+ const name = normalizeSiteName(opts.name);
1204
+ if (getSiteByName(db, name))
1205
+ throw new Error(`site name already exists: ${name}`);
1206
+ const id = newId("site", name);
1207
+ const first = emit(db, "sites", id, "name", name);
1189
1208
  emit(db, "sites", id, "created_hlc", first.hlc);
1190
1209
  if (opts.title !== undefined)
1191
1210
  emit(db, "sites", id, "title", opts.title);
@@ -1195,7 +1214,13 @@ function getSite(db, id) {
1195
1214
  return db.query("SELECT id, name, title, created_hlc FROM sites WHERE id = ? AND __deleted = 0").get(id);
1196
1215
  }
1197
1216
  function getSiteByName(db, name) {
1198
- return db.query("SELECT id, name, title, created_hlc FROM sites WHERE name = ? AND __deleted = 0 ORDER BY created_hlc DESC LIMIT 1").get(name);
1217
+ let slug;
1218
+ try {
1219
+ slug = normalizeSiteName(name);
1220
+ } catch {
1221
+ return null;
1222
+ }
1223
+ return db.query("SELECT id, name, title, created_hlc FROM sites WHERE name = ? AND __deleted = 0 ORDER BY created_hlc DESC LIMIT 1").get(slug);
1199
1224
  }
1200
1225
  function listSites(db) {
1201
1226
  return db.query("SELECT id, name, title, created_hlc FROM sites WHERE __deleted = 0 ORDER BY created_hlc").all();
@@ -1207,6 +1232,20 @@ function resolveSite(db, ref) {
1207
1232
  throw new Error(`no such site: ${ref}`);
1208
1233
  return site;
1209
1234
  }
1235
+ function updateSite(db, id, opts) {
1236
+ if (!getSite(db, id))
1237
+ throw new Error(`no such site: ${id}`);
1238
+ if (opts.name !== undefined) {
1239
+ const name = normalizeSiteName(opts.name);
1240
+ const dup = getSiteByName(db, name);
1241
+ if (dup && dup.id !== id)
1242
+ throw new Error(`site name already exists: ${name}`);
1243
+ emit(db, "sites", id, "name", name);
1244
+ }
1245
+ if (opts.title !== undefined)
1246
+ emit(db, "sites", id, "title", opts.title);
1247
+ return getSite(db, id);
1248
+ }
1210
1249
  function deleteSite(db, id) {
1211
1250
  if (!getSite(db, id))
1212
1251
  return false;
@@ -1221,7 +1260,10 @@ function fileIdFor(db, siteId, path) {
1221
1260
  return row?.id ?? null;
1222
1261
  }
1223
1262
  async function putFile(db, siteId, path, opts) {
1224
- const contentType = opts.contentType ?? inferContentType(path);
1263
+ const cleanPath = normalizeSitePath(path);
1264
+ if (!cleanPath)
1265
+ throw new Error(`invalid file path: ${JSON.stringify(path)}`);
1266
+ const contentType = opts.contentType ?? inferContentType(cleanPath);
1225
1267
  const bytes = toBytes(opts.data);
1226
1268
  let encoding;
1227
1269
  let content;
@@ -1235,11 +1277,11 @@ async function putFile(db, siteId, path, opts) {
1235
1277
  encoding = "blob";
1236
1278
  content = (await putBlob(bytes)).hash;
1237
1279
  }
1238
- const existing = fileIdFor(db, siteId, path);
1239
- const id = existing ?? newId("sf", path);
1280
+ const existing = fileIdFor(db, siteId, cleanPath);
1281
+ const id = existing ?? newId("sf", cleanPath);
1240
1282
  if (!existing) {
1241
1283
  const first = emit(db, "site_files", id, "site_id", siteId);
1242
- emit(db, "site_files", id, "path", path);
1284
+ emit(db, "site_files", id, "path", cleanPath);
1243
1285
  emit(db, "site_files", id, "created_hlc", first.hlc);
1244
1286
  }
1245
1287
  emit(db, "site_files", id, "content_type", contentType);
@@ -1252,7 +1294,7 @@ function listFiles(db, siteId) {
1252
1294
  return db.query("SELECT id, site_id, path, content_type, encoding FROM site_files WHERE site_id = ? AND __deleted = 0 ORDER BY path").all(siteId);
1253
1295
  }
1254
1296
  function deleteFile(db, siteId, path) {
1255
- const id = fileIdFor(db, siteId, path);
1297
+ const id = fileIdFor(db, siteId, normalizeSitePath(path));
1256
1298
  if (!id)
1257
1299
  return false;
1258
1300
  const live = db.query("SELECT __deleted AS d FROM site_files WHERE id = ?").get(id);
@@ -1262,7 +1304,10 @@ function deleteFile(db, siteId, path) {
1262
1304
  return true;
1263
1305
  }
1264
1306
  async function getFileForServe(db, siteId, path) {
1265
- const p = path === "" || path.endsWith("/") ? `${path}index.html` : path;
1307
+ const withIndex = path === "" || path.endsWith("/") ? `${path}index.html` : path;
1308
+ const p = normalizeSitePath(withIndex);
1309
+ if (!p)
1310
+ return null;
1266
1311
  const row = db.query("SELECT content_type, encoding, content FROM site_files WHERE site_id = ? AND path = ? AND __deleted = 0 ORDER BY created_hlc LIMIT 1").get(siteId, p);
1267
1312
  if (!row)
1268
1313
  return null;
@@ -1516,7 +1561,7 @@ var HTML = `<!doctype html>
1516
1561
  .sb-section-head .add { opacity:0; padding:3px; border-radius:5px; color:var(--muted); transition:opacity .12s; }
1517
1562
  .sb-section:hover .add { opacity:1; }
1518
1563
  .sb-section-head .add:hover { background:var(--hover-2); color:var(--fg); }
1519
- .navitem { display:flex; align-items:center; gap:3px; width:100%; text-align:left; padding:5px 6px; border-radius:var(--radius-sm);
1564
+ .navitem { display:flex; align-items:center; gap:3px; width:100%; text-align:left; padding:5px 6px; margin-bottom:2px; border-radius:var(--radius-sm);
1520
1565
  color:var(--fg-soft); font-size:13.5px; position:relative; white-space:nowrap; transition:background .1s; }
1521
1566
  .navitem:hover { background:var(--hover-2); }
1522
1567
  .navitem.active { background:var(--bg); color:var(--fg); font-weight:600; box-shadow:var(--shadow-sm); }
@@ -1616,6 +1661,21 @@ var HTML = `<!doctype html>
1616
1661
  .btn-primary:hover { filter:brightness(1.07); }
1617
1662
  .btn-danger { background:var(--danger); color:#fff; } .btn-danger:hover { filter:brightness(1.05); }
1618
1663
 
1664
+ /* desktop app (frameless): drop the sidebar logo; on macOS reserve a draggable
1665
+ strip for the inset traffic lights so they never overlap our controls */
1666
+ body.desktop .brand { display:none; }
1667
+ /* logo gone \u2192 keep the collapse button on the right (where .brand's flex used to push it) */
1668
+ body.desktop .sb-head .iconbtn { margin-left:auto; }
1669
+ body.desktop-mac .sb-head { -webkit-app-region:drag; }
1670
+ body.desktop-mac .sb-head .iconbtn { -webkit-app-region:no-drag; }
1671
+ body.desktop-mac .topbar { -webkit-app-region:drag; }
1672
+ body.desktop-mac .topbar .iconbtn, body.desktop-mac .topbar .btn { -webkit-app-region:no-drag; }
1673
+ /* peek drawer header overlaps the topbar drag region \u2192 clear its buttons so they stay clickable */
1674
+ body.desktop-mac .peek-head { -webkit-app-region:drag; }
1675
+ body.desktop-mac .peek-head .iconbtn, body.desktop-mac .peek-head .btn { -webkit-app-region:no-drag; }
1676
+ /* sidebar collapsed \u2192 traffic lights sit over the main header; clear the hamburger */
1677
+ body.desktop-mac .sidebar.collapsed ~ .main .topbar { padding-left:80px; }
1678
+
1619
1679
  /* document editor */
1620
1680
  .doc { max-width:740px; margin:0 auto; padding:60px 60px 36vh; }
1621
1681
  .doc-title { font-size:38px; font-weight:700; letter-spacing:-.02em; outline:none; line-height:1.15; margin-bottom:4px; }
@@ -1847,6 +1907,83 @@ var HTML = `<!doctype html>
1847
1907
  .gridfoot { display:flex; gap:18px; color:var(--muted); font-size:12px; padding:8px 2px; }
1848
1908
  input.inlineedit { width:100%; border:0; outline:2px solid var(--accent); border-radius:4px; padding:7px 9px; background:var(--bg); color:var(--fg); font:inherit; }
1849
1909
 
1910
+ /* ---- sites (\u7AD9\u70B9\u7BA1\u7406) ---- */
1911
+ .sites-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(264px, 1fr)); gap:14px; margin-top:6px; }
1912
+ .site-card { display:flex; flex-direction:column; gap:13px; padding:16px 16px 14px; background:var(--bg); border:1px solid var(--line);
1913
+ border-radius:var(--radius); box-shadow:var(--shadow-sm); cursor:pointer; transition:box-shadow .16s, border-color .16s, transform .16s; }
1914
+ .site-card:hover { box-shadow:var(--shadow-md); border-color:var(--line-strong); transform:translateY(-1px); }
1915
+ .site-card-head { display:flex; align-items:center; gap:11px; min-width:0; }
1916
+ .site-card .si { width:40px; height:40px; border-radius:11px; background:var(--accent-soft); color:var(--accent); display:grid; place-items:center; flex:none; }
1917
+ .site-card .si svg { width:20px; height:20px; }
1918
+ .site-card-id { min-width:0; }
1919
+ .site-card .slug { font-family:var(--mono); font-weight:600; font-size:14.5px; color:var(--fg); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1920
+ .site-card .ttl { font-size:12px; color:var(--fg-soft); margin-top:1px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1921
+ .site-card .ttl.muted { color:var(--muted); font-style:italic; }
1922
+ .site-addr { display:flex; align-items:center; gap:6px; width:100%; text-align:left; background:var(--surface); border:1px solid var(--line);
1923
+ border-radius:var(--radius-sm); padding:6px 9px; color:var(--muted); font-family:var(--mono); font-size:11.5px; transition:border-color .12s, color .12s; }
1924
+ .site-addr svg { flex:none; }
1925
+ .site-addr span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1926
+ .site-addr:hover { border-color:var(--accent); color:var(--accent); }
1927
+ .site-card-meta { display:flex; align-items:center; gap:7px; font-size:12px; color:var(--muted); }
1928
+ .site-card-meta b { color:var(--fg-soft); font-weight:600; font-variant-numeric:tabular-nums; }
1929
+ .site-card-meta .dot { color:var(--line-strong); }
1930
+ .site-card-foot { display:flex; align-items:center; gap:6px; padding-top:12px; margin-top:1px; border-top:1px solid var(--line); }
1931
+ .site-card-foot .btn-secondary { padding:6px 12px; justify-content:center; }
1932
+
1933
+ .site-empty { text-align:center; color:var(--muted); border:1px dashed var(--line-strong); border-radius:var(--radius); padding:48px 24px; margin-top:8px; }
1934
+ .site-empty .ei { font-size:30px; margin-bottom:10px; }
1935
+ .site-empty .et { font-size:14px; color:var(--fg-soft); margin-bottom:4px; font-weight:600; }
1936
+ .site-empty .ed { font-size:12.5px; margin-bottom:16px; }
1937
+
1938
+ /* site peek: access bar + file manager */
1939
+ .acc-link { display:flex; align-items:center; gap:6px; background:var(--surface); border:1px solid var(--line); border-radius:var(--radius-sm); padding:7px 8px 7px 11px; margin-bottom:16px; }
1940
+ .acc-link .url { flex:1; font-family:var(--mono); font-size:12.5px; color:var(--fg-soft); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1941
+ .acc-link button { width:28px; height:28px; display:grid; place-items:center; border-radius:6px; color:var(--muted); flex:none; }
1942
+ .acc-link button:hover { background:var(--hover-2); color:var(--fg); }
1943
+ .acc-link button.accent { color:var(--accent); }
1944
+ .site-meta { display:flex; gap:18px; color:var(--muted); font-size:12px; margin-bottom:20px; }
1945
+ .site-meta b { color:var(--fg); font-weight:600; }
1946
+ .files-head { display:flex; align-items:center; justify-content:space-between; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); margin:0 0 4px; }
1947
+ .filerow { display:flex; align-items:center; gap:10px; padding:9px 8px; border-radius:var(--radius-sm); cursor:pointer; border-bottom:1px solid var(--line); }
1948
+ .filerow:last-child { border-bottom:0; }
1949
+ .filerow:hover { background:var(--hover); }
1950
+ .filerow .fi { color:var(--muted); display:grid; place-items:center; flex:none; }
1951
+ .filerow .fpath { flex:1; font-family:var(--mono); font-size:12.5px; color:var(--fg); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1952
+ .filerow .facts { display:flex; gap:1px; opacity:0; flex:none; }
1953
+ .filerow:hover .facts { opacity:1; }
1954
+ .filerow .facts button { width:27px; height:27px; display:grid; place-items:center; border-radius:6px; color:var(--muted); }
1955
+ .filerow .facts button:hover { background:var(--hover-2); color:var(--fg); }
1956
+ .filerow .facts button.del:hover { color:var(--danger); }
1957
+ .enc-badge { display:inline-flex; align-items:center; font-family:var(--mono); font-size:10.5px; font-weight:500; letter-spacing:.02em;
1958
+ padding:2px 6px; border-radius:5px; background:var(--surface-2); color:var(--fg-soft); border:1px solid var(--line); }
1959
+ .enc-badge.blob { color:var(--accent); background:var(--accent-soft); border-color:transparent; }
1960
+
1961
+ /* file preview (inside modal) */
1962
+ .preview-box { font-family:var(--mono); font-size:12.5px; background:var(--surface-2); border:1px solid var(--line); border-radius:var(--radius);
1963
+ padding:14px 16px; max-height:52vh; overflow:auto; white-space:pre-wrap; line-height:1.6; color:var(--fg); }
1964
+ .preview-bin { text-align:center; color:var(--muted); border:1px dashed var(--line-strong); border-radius:var(--radius); padding:40px 20px; }
1965
+ .preview-bin .pi { font-size:28px; margin-bottom:8px; }
1966
+ .preview-bin .pt { color:var(--fg-soft); font-size:13px; }
1967
+
1968
+ /* in-app site preview (browser-framed iframe) */
1969
+ .spv-scrim { position:fixed; inset:0; background:rgba(15,15,25,.5); z-index:85; display:grid; place-items:center; padding:28px;
1970
+ opacity:0; pointer-events:none; transition:opacity .16s; animation:spv-in .16s forwards; }
1971
+ @keyframes spv-in { to { opacity:1; pointer-events:auto; } }
1972
+ .spv-scrim.open { opacity:1; pointer-events:auto; }
1973
+ .spv { width:min(1100px, 92vw); height:88vh; background:var(--bg); border-radius:14px; box-shadow:var(--shadow-lg);
1974
+ display:flex; flex-direction:column; overflow:hidden; }
1975
+ .spv-chrome { display:flex; align-items:center; gap:10px; padding:9px 12px; border-bottom:1px solid var(--line); background:var(--surface); }
1976
+ .spv-dots { display:flex; gap:6px; flex:none; }
1977
+ .spv-dots i { width:11px; height:11px; border-radius:50%; background:var(--line-strong); }
1978
+ .spv-url { flex:1; display:flex; align-items:center; gap:6px; min-width:0; background:var(--bg); border:1px solid var(--line);
1979
+ border-radius:7px; padding:5px 10px; color:var(--fg-soft); font-family:var(--mono); font-size:12px; }
1980
+ .spv-url svg { color:var(--muted); flex:none; }
1981
+ .spv-url span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1982
+ .spv-btn { display:inline-flex; align-items:center; gap:5px; flex:none; padding:6px 10px; border-radius:7px; color:var(--fg-soft); font-size:12.5px; font-weight:500; }
1983
+ .spv-btn:hover { background:var(--hover-2); color:var(--fg); }
1984
+ .spv-btn.icon { padding:6px; }
1985
+ .spv-frame { flex:1; width:100%; border:0; background:#fff; }
1986
+
1850
1987
  .selbar { position:fixed; bottom:24px; left:50%; transform:translateX(-50%); z-index:65; display:flex; align-items:center; gap:4px;
1851
1988
  background:var(--fg); color:var(--bg); padding:7px 8px 7px 14px; border-radius:11px; box-shadow:var(--shadow-lg); }
1852
1989
  .selbar .cnt { font-size:13px; font-weight:600; margin-right:6px; }
@@ -3593,7 +3730,7 @@ async function runMain(cmd, opts = {}) {
3593
3730
  // package.json
3594
3731
  var package_default = {
3595
3732
  name: "@tensorix/metahub",
3596
- version: "0.1.7",
3733
+ version: "0.1.9",
3597
3734
  description: "Metahub core library and CLI",
3598
3735
  license: "AGPL-3.0-only",
3599
3736
  type: "module",
@@ -3630,10 +3767,13 @@ var package_default = {
3630
3767
  citty: "^0.1.6",
3631
3768
  "highlight.js": "^11.11.1",
3632
3769
  preact: "^10.29.2",
3770
+ turndown: "^7.2.4",
3771
+ "turndown-plugin-gfm": "^1.0.2",
3633
3772
  zod: "^4.4.3"
3634
3773
  },
3635
3774
  devDependencies: {
3636
3775
  "@types/bun": "latest",
3776
+ "@types/turndown": "^5.0.6",
3637
3777
  typescript: "^5"
3638
3778
  }
3639
3779
  };
@@ -3741,6 +3881,7 @@ CREATE TABLE IF NOT EXISTS documents (
3741
3881
  database_id TEXT,
3742
3882
  parent_id TEXT,
3743
3883
  created_hlc TEXT,
3884
+ order_key TEXT, -- fractional index among siblings (same parent_id); ties broken by created_hlc,id
3744
3885
  __deleted INTEGER NOT NULL DEFAULT 0
3745
3886
  );
3746
3887
 
@@ -4383,6 +4524,291 @@ function deleteRecord(db, id) {
4383
4524
  return true;
4384
4525
  }
4385
4526
 
4527
+ // src/core/documents.ts
4528
+ init_ids();
4529
+ init_crdt();
4530
+ function liveBlocks(db, docId) {
4531
+ return db.query("SELECT id, text, order_key, blank_after FROM doc_blocks WHERE doc_id = ? AND __deleted = 0 ORDER BY order_key, id").all(docId);
4532
+ }
4533
+ function makeBlockId(text) {
4534
+ return newId("blk", text.split(`
4535
+ `, 1)[0] ?? "");
4536
+ }
4537
+ function emitBlock(db, docId, fields) {
4538
+ const id = makeBlockId(fields.text);
4539
+ emit(db, "doc_blocks", id, "doc_id", docId);
4540
+ emit(db, "doc_blocks", id, "order_key", fields.order_key);
4541
+ if (fields.blankAfter)
4542
+ emit(db, "doc_blocks", id, "blank_after", fields.blankAfter);
4543
+ emit(db, "doc_blocks", id, "text", fields.text);
4544
+ }
4545
+ function insertBlocks(db, docId, blocks, after, before) {
4546
+ const keys = keysBetween(after, before, blocks.length);
4547
+ blocks.forEach((b2, i2) => emitBlock(db, docId, { text: b2.text, order_key: keys[i2], blankAfter: b2.blankAfter }));
4548
+ }
4549
+ function ensureBlocks(db, docId) {
4550
+ if (db.query("SELECT 1 AS x FROM doc_blocks WHERE doc_id = ? LIMIT 1").get(docId))
4551
+ return;
4552
+ const row = db.query("SELECT body FROM documents WHERE id = ?").get(docId);
4553
+ const blocks = parseDocBlocks(row?.body ?? "");
4554
+ if (blocks.length)
4555
+ insertBlocks(db, docId, blocks, null, null);
4556
+ }
4557
+ function reconcileBody(db, docId, body) {
4558
+ const old = liveBlocks(db, docId);
4559
+ const next = parseDocBlocks(body);
4560
+ const plan = reconcile(old.map((b2) => b2.text), next.map((b2) => b2.text));
4561
+ for (const oi of plan.deleted)
4562
+ emit(db, "doc_blocks", old[oi].id, "__deleted", 1);
4563
+ const { items } = plan;
4564
+ let i2 = 0;
4565
+ while (i2 < items.length) {
4566
+ const it = items[i2];
4567
+ if ("keep" in it) {
4568
+ const oldB = old[it.keep];
4569
+ const want = next[i2].blankAfter;
4570
+ if ((oldB.blank_after ?? 0) !== want)
4571
+ emit(db, "doc_blocks", oldB.id, "blank_after", want);
4572
+ i2++;
4573
+ continue;
4574
+ }
4575
+ let j = i2;
4576
+ while (j < items.length && "insert" in items[j])
4577
+ j++;
4578
+ const left = i2 > 0 ? old[items[i2 - 1].keep].order_key : null;
4579
+ const right = j < items.length ? old[items[j].keep].order_key : null;
4580
+ insertBlocks(db, docId, next.slice(i2, j), left, right);
4581
+ i2 = j;
4582
+ }
4583
+ }
4584
+ function documentVersion(db, id) {
4585
+ const row = db.query(`SELECT MAX(hlc) AS h FROM crdt_changes
4586
+ WHERE (dataset = 'documents' AND row_id = ?)
4587
+ OR (dataset = 'doc_blocks' AND row_id IN (SELECT id FROM doc_blocks WHERE doc_id = ?))`).get(id, id);
4588
+ return row.h ?? "";
4589
+ }
4590
+ function createDocument(db, opts) {
4591
+ const id = newId("doc", opts.title);
4592
+ const first = emit(db, "documents", id, "title", opts.title);
4593
+ emit(db, "documents", id, "created_hlc", first.hlc);
4594
+ if (opts.database_id !== undefined)
4595
+ emit(db, "documents", id, "database_id", opts.database_id);
4596
+ if (opts.parent_id !== undefined)
4597
+ emit(db, "documents", id, "parent_id", opts.parent_id);
4598
+ placeInSiblings(db, id, opts.parent_id ?? null);
4599
+ if (opts.body !== undefined) {
4600
+ const blocks = parseDocBlocks(opts.body);
4601
+ if (blocks.length)
4602
+ insertBlocks(db, id, blocks, null, null);
4603
+ }
4604
+ return getDocument(db, id);
4605
+ }
4606
+ function getDocument(db, id) {
4607
+ return db.query("SELECT id, title, body, database_id, parent_id, created_hlc, order_key FROM documents WHERE id = ? AND __deleted = 0").get(id);
4608
+ }
4609
+ var ORDER_BY = "ORDER BY order_key IS NULL, order_key, created_hlc, id";
4610
+ function listDocuments(db, opts = {}) {
4611
+ const cols = "SELECT id, title, database_id, parent_id, created_hlc, order_key FROM documents WHERE __deleted = 0";
4612
+ if (opts.parent_id !== undefined)
4613
+ return db.query(`${cols} AND parent_id = ? ${ORDER_BY}`).all(opts.parent_id);
4614
+ if (opts.database_id)
4615
+ return db.query(`${cols} AND database_id = ? ${ORDER_BY}`).all(opts.database_id);
4616
+ return db.query(`${cols} ${ORDER_BY}`).all();
4617
+ }
4618
+ function siblingWhere(parentId) {
4619
+ return parentId === null ? { clause: "parent_id IS NULL", args: [] } : { clause: "parent_id = ?", args: [parentId] };
4620
+ }
4621
+ function canEmitOrderKeys2(db) {
4622
+ return tableExists2(db, "meta") && tableExists2(db, "crdt_changes");
4623
+ }
4624
+ function tableExists2(db, table) {
4625
+ return db.query("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) != null;
4626
+ }
4627
+ function lastSiblingOrderKey(db, parentId, excludeId) {
4628
+ const w2 = siblingWhere(parentId);
4629
+ const exclude = excludeId ? " AND id <> ?" : "";
4630
+ const row = db.query(`SELECT order_key FROM documents WHERE ${w2.clause}${exclude}
4631
+ AND __deleted = 0 AND order_key IS NOT NULL
4632
+ ORDER BY order_key DESC, id DESC LIMIT 1`).get(...w2.args, ...excludeId ? [excludeId] : []);
4633
+ return row?.order_key ?? null;
4634
+ }
4635
+ function orderedSiblings(db, parentId) {
4636
+ const w2 = siblingWhere(parentId);
4637
+ return db.query(`SELECT id, order_key FROM documents WHERE ${w2.clause} AND __deleted = 0 ${ORDER_BY}`).all(...w2.args);
4638
+ }
4639
+ function rebalanceSiblings(db, parentId) {
4640
+ const rows = orderedSiblings(db, parentId);
4641
+ const keys = keysBetween(null, null, rows.length);
4642
+ rows.forEach((row, i2) => {
4643
+ if (row.order_key !== keys[i2])
4644
+ emit(db, "documents", row.id, "order_key", keys[i2]);
4645
+ });
4646
+ }
4647
+ function backfillSiblings(db, parentId) {
4648
+ const emitChange = canEmitOrderKeys2(db);
4649
+ const w2 = siblingWhere(parentId);
4650
+ const missing = db.query(`SELECT id FROM documents WHERE ${w2.clause} AND __deleted = 0 AND order_key IS NULL
4651
+ ORDER BY created_hlc, id`).all(...w2.args);
4652
+ if (!missing.length)
4653
+ return;
4654
+ const keys = keysBetween(lastSiblingOrderKey(db, parentId), null, missing.length);
4655
+ missing.forEach((row, i2) => {
4656
+ if (emitChange)
4657
+ emit(db, "documents", row.id, "order_key", keys[i2]);
4658
+ else
4659
+ db.query("UPDATE documents SET order_key = ? WHERE id = ?").run(keys[i2], row.id);
4660
+ });
4661
+ }
4662
+ function backfillDocumentOrderKeys(db) {
4663
+ const parents = db.query("SELECT DISTINCT parent_id FROM documents WHERE __deleted = 0 AND order_key IS NULL").all();
4664
+ for (const p of parents)
4665
+ backfillSiblings(db, p.parent_id);
4666
+ }
4667
+ function placeInSiblings(db, id, parentId, anchor) {
4668
+ let cur = parentId;
4669
+ while (cur) {
4670
+ if (cur === id)
4671
+ throw new Error(`cannot set parent_id: would create a cycle (${id})`);
4672
+ cur = getDocument(db, cur)?.parent_id ?? null;
4673
+ }
4674
+ const current = getDocument(db, id);
4675
+ if (current && (current.parent_id ?? null) !== parentId)
4676
+ emit(db, "documents", id, "parent_id", parentId);
4677
+ let key;
4678
+ if (!anchor) {
4679
+ key = keyBetween(lastSiblingOrderKey(db, parentId, id), null);
4680
+ } else {
4681
+ backfillSiblings(db, parentId);
4682
+ const neighbors = () => {
4683
+ const rows = orderedSiblings(db, parentId).filter((r3) => r3.id !== id);
4684
+ const to = rows.findIndex((r3) => r3.id === anchor.targetId);
4685
+ if (to < 0)
4686
+ throw new Error(`no such target document: ${anchor.targetId}`);
4687
+ return anchor.where === "before" ? [rows[to - 1]?.order_key ?? null, rows[to].order_key] : [rows[to].order_key, rows[to + 1]?.order_key ?? null];
4688
+ };
4689
+ let [left, right] = neighbors();
4690
+ if (left !== null && right !== null && left >= right) {
4691
+ rebalanceSiblings(db, parentId);
4692
+ [left, right] = neighbors();
4693
+ }
4694
+ key = keyBetween(left, right);
4695
+ }
4696
+ emit(db, "documents", id, "order_key", key);
4697
+ }
4698
+ function moveDocument(db, id, targetId, where) {
4699
+ const src2 = getDocument(db, id);
4700
+ if (!src2)
4701
+ throw new Error(`no such document: ${id}`);
4702
+ if (id === targetId)
4703
+ return src2;
4704
+ const target = getDocument(db, targetId);
4705
+ if (!target)
4706
+ throw new Error(`no such target document: ${targetId}`);
4707
+ if (where === "into")
4708
+ placeInSiblings(db, id, targetId);
4709
+ else
4710
+ placeInSiblings(db, id, target.parent_id, { targetId, where });
4711
+ return getDocument(db, id);
4712
+ }
4713
+ function updateDocument(db, id, fields) {
4714
+ if (!getDocument(db, id))
4715
+ throw new Error(`no such document: ${id}`);
4716
+ if (fields.title !== undefined)
4717
+ emit(db, "documents", id, "title", fields.title);
4718
+ if (fields.database_id !== undefined)
4719
+ emit(db, "documents", id, "database_id", fields.database_id);
4720
+ if (fields.parent_id !== undefined) {
4721
+ const next = fields.parent_id ?? null;
4722
+ if ((getDocument(db, id).parent_id ?? null) !== next)
4723
+ placeInSiblings(db, id, next);
4724
+ }
4725
+ if (fields.body !== undefined) {
4726
+ ensureBlocks(db, id);
4727
+ reconcileBody(db, id, fields.body);
4728
+ }
4729
+ return getDocument(db, id);
4730
+ }
4731
+ function editDocument(db, id, opts) {
4732
+ if (!getDocument(db, id))
4733
+ throw new Error(`no such document: ${id}`);
4734
+ if (opts.ifMatch !== undefined && documentVersion(db, id) !== opts.ifMatch)
4735
+ throw new Error(`stale: document changed since ${opts.ifMatch}; re-read first`);
4736
+ if (opts.old === "")
4737
+ throw new Error("--old must not be empty");
4738
+ ensureBlocks(db, id);
4739
+ const blocks = liveBlocks(db, id);
4740
+ const body = serializeDocBlocks(blocks.map((b2) => ({ text: b2.text, blankAfter: b2.blank_after })));
4741
+ const count = countOccurrences(body, opts.old);
4742
+ if (count === 0)
4743
+ throw new Error(`anchor not found: no match for --old`);
4744
+ if (count > 1 && !opts.replaceAll)
4745
+ throw new Error(`${count} matches for --old; add surrounding context or use --replace-all`);
4746
+ if (!opts.old.includes(`
4747
+
4748
+ `)) {
4749
+ const affected = blocks.filter((b2) => b2.text.includes(opts.old));
4750
+ const next = affected.map((b2) => opts.replaceAll ? b2.text.replaceAll(opts.old, opts.new) : b2.text.replace(opts.old, opts.new));
4751
+ if (next.every((t2) => t2.length > 0 && !t2.includes(`
4752
+
4753
+ `))) {
4754
+ const targets = opts.replaceAll ? affected : affected.slice(0, 1);
4755
+ targets.forEach((b2, i2) => emit(db, "doc_blocks", b2.id, "text", next[i2]));
4756
+ return { id, changed: true, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
4757
+ }
4758
+ }
4759
+ const newBody = opts.replaceAll ? body.replaceAll(opts.old, opts.new) : body.replace(opts.old, opts.new);
4760
+ reconcileBody(db, id, newBody);
4761
+ return { id, changed: newBody !== body, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
4762
+ }
4763
+ function appendDocument(db, id, body) {
4764
+ if (!getDocument(db, id))
4765
+ throw new Error(`no such document: ${id}`);
4766
+ ensureBlocks(db, id);
4767
+ const next = parseDocBlocks(body);
4768
+ if (next.length) {
4769
+ const blocks = liveBlocks(db, id);
4770
+ const last = blocks.length ? blocks[blocks.length - 1].order_key : null;
4771
+ insertBlocks(db, id, next, last, null);
4772
+ }
4773
+ return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
4774
+ }
4775
+ function prependDocument(db, id, body) {
4776
+ if (!getDocument(db, id))
4777
+ throw new Error(`no such document: ${id}`);
4778
+ ensureBlocks(db, id);
4779
+ const next = parseDocBlocks(body);
4780
+ if (next.length) {
4781
+ const blocks = liveBlocks(db, id);
4782
+ const first = blocks.length ? blocks[0].order_key : null;
4783
+ insertBlocks(db, id, next, null, first);
4784
+ }
4785
+ return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
4786
+ }
4787
+ function deleteDocument(db, id) {
4788
+ if (!getDocument(db, id))
4789
+ return false;
4790
+ emit(db, "documents", id, "__deleted", 1);
4791
+ for (const b2 of liveChildIds(db, "doc_blocks", "doc_id", id))
4792
+ emit(db, "doc_blocks", b2, "__deleted", 1);
4793
+ for (const c3 of liveChildIds(db, "documents", "parent_id", id))
4794
+ emit(db, "documents", c3, "parent_id", null);
4795
+ return true;
4796
+ }
4797
+ function liveChildIds(db, table, col, parentId) {
4798
+ return db.query(`SELECT id FROM ${table} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r3) => r3.id);
4799
+ }
4800
+ function countOccurrences(haystack, needle) {
4801
+ if (needle === "")
4802
+ return 0;
4803
+ let n2 = 0;
4804
+ let i2 = haystack.indexOf(needle);
4805
+ while (i2 !== -1) {
4806
+ n2++;
4807
+ i2 = haystack.indexOf(needle, i2 + needle.length);
4808
+ }
4809
+ return n2;
4810
+ }
4811
+
4386
4812
  // src/core/db.ts
4387
4813
  function ensureDirs() {
4388
4814
  mkdirSync(metahubHome(), { recursive: true });
@@ -4402,7 +4828,7 @@ function hasColumn(db, table, column) {
4402
4828
  const cols = db.query(`PRAGMA table_info(${table})`).all();
4403
4829
  return cols.some((c3) => c3.name === column);
4404
4830
  }
4405
- function tableExists2(db, table) {
4831
+ function tableExists3(db, table) {
4406
4832
  return db.query("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) != null;
4407
4833
  }
4408
4834
  function migrateRecords(db) {
@@ -4410,7 +4836,7 @@ function migrateRecords(db) {
4410
4836
  db.exec("ALTER TABLE records ADD COLUMN data TEXT NOT NULL DEFAULT '{}'");
4411
4837
  if (!hasColumn(db, "records", "order_key"))
4412
4838
  db.exec("ALTER TABLE records ADD COLUMN order_key TEXT");
4413
- if (!tableExists2(db, "record_values"))
4839
+ if (!tableExists3(db, "record_values"))
4414
4840
  return;
4415
4841
  const tx = db.transaction(() => {
4416
4842
  db.exec(`
@@ -4445,6 +4871,11 @@ function migrateDocBlocks(db) {
4445
4871
  if (!hasColumn(db, "doc_blocks", "blank_after"))
4446
4872
  db.exec("ALTER TABLE doc_blocks ADD COLUMN blank_after INTEGER NOT NULL DEFAULT 0");
4447
4873
  }
4874
+ function migrateDocuments(db) {
4875
+ if (!hasColumn(db, "documents", "order_key"))
4876
+ db.exec("ALTER TABLE documents ADD COLUMN order_key TEXT");
4877
+ backfillDocumentOrderKeys(db);
4878
+ }
4448
4879
  function openMetahub() {
4449
4880
  ensureDirs();
4450
4881
  const db = new Database(dbPath(), { create: true });
@@ -4453,6 +4884,7 @@ function openMetahub() {
4453
4884
  migrateRecords(db);
4454
4885
  migratePeers(db);
4455
4886
  migrateDocBlocks(db);
4887
+ migrateDocuments(db);
4456
4888
  return db;
4457
4889
  }
4458
4890
 
@@ -4620,198 +5052,6 @@ var use_default = defineCommand({
4620
5052
  })
4621
5053
  });
4622
5054
 
4623
- // src/core/documents.ts
4624
- init_ids();
4625
- init_crdt();
4626
- function liveBlocks(db, docId) {
4627
- return db.query("SELECT id, text, order_key, blank_after FROM doc_blocks WHERE doc_id = ? AND __deleted = 0 ORDER BY order_key, id").all(docId);
4628
- }
4629
- function makeBlockId(text) {
4630
- return newId("blk", text.split(`
4631
- `, 1)[0] ?? "");
4632
- }
4633
- function emitBlock(db, docId, fields) {
4634
- const id = makeBlockId(fields.text);
4635
- emit(db, "doc_blocks", id, "doc_id", docId);
4636
- emit(db, "doc_blocks", id, "order_key", fields.order_key);
4637
- if (fields.blankAfter)
4638
- emit(db, "doc_blocks", id, "blank_after", fields.blankAfter);
4639
- emit(db, "doc_blocks", id, "text", fields.text);
4640
- }
4641
- function insertBlocks(db, docId, blocks, after, before) {
4642
- const keys = keysBetween(after, before, blocks.length);
4643
- blocks.forEach((b2, i2) => emitBlock(db, docId, { text: b2.text, order_key: keys[i2], blankAfter: b2.blankAfter }));
4644
- }
4645
- function ensureBlocks(db, docId) {
4646
- if (db.query("SELECT 1 AS x FROM doc_blocks WHERE doc_id = ? LIMIT 1").get(docId))
4647
- return;
4648
- const row = db.query("SELECT body FROM documents WHERE id = ?").get(docId);
4649
- const blocks = parseDocBlocks(row?.body ?? "");
4650
- if (blocks.length)
4651
- insertBlocks(db, docId, blocks, null, null);
4652
- }
4653
- function reconcileBody(db, docId, body) {
4654
- const old = liveBlocks(db, docId);
4655
- const next = parseDocBlocks(body);
4656
- const plan = reconcile(old.map((b2) => b2.text), next.map((b2) => b2.text));
4657
- for (const oi of plan.deleted)
4658
- emit(db, "doc_blocks", old[oi].id, "__deleted", 1);
4659
- const { items } = plan;
4660
- let i2 = 0;
4661
- while (i2 < items.length) {
4662
- const it = items[i2];
4663
- if ("keep" in it) {
4664
- const oldB = old[it.keep];
4665
- const want = next[i2].blankAfter;
4666
- if ((oldB.blank_after ?? 0) !== want)
4667
- emit(db, "doc_blocks", oldB.id, "blank_after", want);
4668
- i2++;
4669
- continue;
4670
- }
4671
- let j = i2;
4672
- while (j < items.length && "insert" in items[j])
4673
- j++;
4674
- const left = i2 > 0 ? old[items[i2 - 1].keep].order_key : null;
4675
- const right = j < items.length ? old[items[j].keep].order_key : null;
4676
- insertBlocks(db, docId, next.slice(i2, j), left, right);
4677
- i2 = j;
4678
- }
4679
- }
4680
- function documentVersion(db, id) {
4681
- const row = db.query(`SELECT MAX(hlc) AS h FROM crdt_changes
4682
- WHERE (dataset = 'documents' AND row_id = ?)
4683
- OR (dataset = 'doc_blocks' AND row_id IN (SELECT id FROM doc_blocks WHERE doc_id = ?))`).get(id, id);
4684
- return row.h ?? "";
4685
- }
4686
- function createDocument(db, opts) {
4687
- const id = newId("doc", opts.title);
4688
- const first = emit(db, "documents", id, "title", opts.title);
4689
- emit(db, "documents", id, "created_hlc", first.hlc);
4690
- if (opts.database_id !== undefined)
4691
- emit(db, "documents", id, "database_id", opts.database_id);
4692
- if (opts.parent_id !== undefined)
4693
- emit(db, "documents", id, "parent_id", opts.parent_id);
4694
- if (opts.body !== undefined) {
4695
- const blocks = parseDocBlocks(opts.body);
4696
- if (blocks.length)
4697
- insertBlocks(db, id, blocks, null, null);
4698
- }
4699
- return getDocument(db, id);
4700
- }
4701
- function getDocument(db, id) {
4702
- return db.query("SELECT id, title, body, database_id, parent_id, created_hlc FROM documents WHERE id = ? AND __deleted = 0").get(id);
4703
- }
4704
- function listDocuments(db, opts = {}) {
4705
- const cols = "SELECT id, title, database_id, parent_id, created_hlc FROM documents WHERE __deleted = 0";
4706
- if (opts.parent_id !== undefined)
4707
- return db.query(`${cols} AND parent_id = ? ORDER BY created_hlc`).all(opts.parent_id);
4708
- if (opts.database_id)
4709
- return db.query(`${cols} AND database_id = ? ORDER BY created_hlc`).all(opts.database_id);
4710
- return db.query(`${cols} ORDER BY created_hlc`).all();
4711
- }
4712
- function updateDocument(db, id, fields) {
4713
- if (!getDocument(db, id))
4714
- throw new Error(`no such document: ${id}`);
4715
- if (fields.title !== undefined)
4716
- emit(db, "documents", id, "title", fields.title);
4717
- if (fields.database_id !== undefined)
4718
- emit(db, "documents", id, "database_id", fields.database_id);
4719
- if (fields.parent_id !== undefined) {
4720
- let cur = fields.parent_id;
4721
- while (cur) {
4722
- if (cur === id)
4723
- throw new Error(`cannot set parent_id: would create a cycle (${id})`);
4724
- cur = getDocument(db, cur)?.parent_id ?? null;
4725
- }
4726
- emit(db, "documents", id, "parent_id", fields.parent_id);
4727
- }
4728
- if (fields.body !== undefined) {
4729
- ensureBlocks(db, id);
4730
- reconcileBody(db, id, fields.body);
4731
- }
4732
- return getDocument(db, id);
4733
- }
4734
- function editDocument(db, id, opts) {
4735
- if (!getDocument(db, id))
4736
- throw new Error(`no such document: ${id}`);
4737
- if (opts.ifMatch !== undefined && documentVersion(db, id) !== opts.ifMatch)
4738
- throw new Error(`stale: document changed since ${opts.ifMatch}; re-read first`);
4739
- if (opts.old === "")
4740
- throw new Error("--old must not be empty");
4741
- ensureBlocks(db, id);
4742
- const blocks = liveBlocks(db, id);
4743
- const body = serializeDocBlocks(blocks.map((b2) => ({ text: b2.text, blankAfter: b2.blank_after })));
4744
- const count = countOccurrences(body, opts.old);
4745
- if (count === 0)
4746
- throw new Error(`anchor not found: no match for --old`);
4747
- if (count > 1 && !opts.replaceAll)
4748
- throw new Error(`${count} matches for --old; add surrounding context or use --replace-all`);
4749
- if (!opts.old.includes(`
4750
-
4751
- `)) {
4752
- const affected = blocks.filter((b2) => b2.text.includes(opts.old));
4753
- const next = affected.map((b2) => opts.replaceAll ? b2.text.replaceAll(opts.old, opts.new) : b2.text.replace(opts.old, opts.new));
4754
- if (next.every((t2) => t2.length > 0 && !t2.includes(`
4755
-
4756
- `))) {
4757
- const targets = opts.replaceAll ? affected : affected.slice(0, 1);
4758
- targets.forEach((b2, i2) => emit(db, "doc_blocks", b2.id, "text", next[i2]));
4759
- return { id, changed: true, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
4760
- }
4761
- }
4762
- const newBody = opts.replaceAll ? body.replaceAll(opts.old, opts.new) : body.replace(opts.old, opts.new);
4763
- reconcileBody(db, id, newBody);
4764
- return { id, changed: newBody !== body, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
4765
- }
4766
- function appendDocument(db, id, body) {
4767
- if (!getDocument(db, id))
4768
- throw new Error(`no such document: ${id}`);
4769
- ensureBlocks(db, id);
4770
- const next = parseDocBlocks(body);
4771
- if (next.length) {
4772
- const blocks = liveBlocks(db, id);
4773
- const last = blocks.length ? blocks[blocks.length - 1].order_key : null;
4774
- insertBlocks(db, id, next, last, null);
4775
- }
4776
- return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
4777
- }
4778
- function prependDocument(db, id, body) {
4779
- if (!getDocument(db, id))
4780
- throw new Error(`no such document: ${id}`);
4781
- ensureBlocks(db, id);
4782
- const next = parseDocBlocks(body);
4783
- if (next.length) {
4784
- const blocks = liveBlocks(db, id);
4785
- const first = blocks.length ? blocks[0].order_key : null;
4786
- insertBlocks(db, id, next, null, first);
4787
- }
4788
- return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
4789
- }
4790
- function deleteDocument(db, id) {
4791
- if (!getDocument(db, id))
4792
- return false;
4793
- emit(db, "documents", id, "__deleted", 1);
4794
- for (const b2 of liveChildIds(db, "doc_blocks", "doc_id", id))
4795
- emit(db, "doc_blocks", b2, "__deleted", 1);
4796
- for (const c3 of liveChildIds(db, "documents", "parent_id", id))
4797
- emit(db, "documents", c3, "parent_id", null);
4798
- return true;
4799
- }
4800
- function liveChildIds(db, table2, col, parentId) {
4801
- return db.query(`SELECT id FROM ${table2} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r3) => r3.id);
4802
- }
4803
- function countOccurrences(haystack, needle) {
4804
- if (needle === "")
4805
- return 0;
4806
- let n2 = 0;
4807
- let i2 = haystack.indexOf(needle);
4808
- while (i2 !== -1) {
4809
- n2++;
4810
- i2 = haystack.indexOf(needle, i2 + needle.length);
4811
- }
4812
- return n2;
4813
- }
4814
-
4815
5055
  // src/cli/commands/get.ts
4816
5056
  var get_default = defineCommand({
4817
5057
  meta: { name: "get", description: "Look up any entity by ref (id/prefix/name), type auto-detected" },
@@ -22755,7 +22995,7 @@ var PropertySchema = exports_external.object({
22755
22995
  var RecordSchema = exports_external.object({
22756
22996
  id: exports_external.string(),
22757
22997
  database_id: exports_external.string(),
22758
- values: exports_external.record(exports_external.string(), exports_external.any())
22998
+ values: exports_external.record(exports_external.string(), exports_external.any()).describe('Field values keyed by property name (e.g. {"Amount":35}). Reads nest cells here; writes take a flat {column: value} map.')
22759
22999
  });
22760
23000
  var MoveRecordReq = exports_external.object({
22761
23001
  target: exports_external.string(),
@@ -22766,7 +23006,12 @@ var DocumentSummarySchema = exports_external.object({
22766
23006
  title: exports_external.string(),
22767
23007
  database_id: exports_external.string().nullable(),
22768
23008
  parent_id: exports_external.string().nullable(),
22769
- created_hlc: exports_external.string()
23009
+ created_hlc: exports_external.string(),
23010
+ order_key: exports_external.string().nullable()
23011
+ });
23012
+ var MoveDocumentReq = exports_external.object({
23013
+ target: exports_external.string(),
23014
+ where: exports_external.enum(["before", "after", "into"])
22770
23015
  });
22771
23016
  var DocumentSchema = DocumentSummarySchema.extend({ body: exports_external.string().nullable() });
22772
23017
  var SearchHitSchema = exports_external.object({
@@ -22795,7 +23040,7 @@ var UpdatePropertyReq = exports_external.object({
22795
23040
  position: exports_external.number().optional()
22796
23041
  });
22797
23042
  var SetWidthReq = exports_external.object({ width: exports_external.number() });
22798
- var RecordValuesReq = exports_external.record(exports_external.string(), exports_external.any());
23043
+ var RecordValuesReq = exports_external.record(exports_external.string(), exports_external.any()).describe("Flat { column: value } cell patch (keyed by property name).");
22799
23044
  var CreateDocumentReq = exports_external.object({
22800
23045
  title: exports_external.string(),
22801
23046
  body: exports_external.string().optional(),
@@ -23022,6 +23267,17 @@ var webuiRoutes = [
23022
23267
  return updateDocument(db, need(req, "id"), body);
23023
23268
  })
23024
23269
  },
23270
+ {
23271
+ method: "PATCH",
23272
+ path: "/api/document/move",
23273
+ summary: "Move a document before/after another, or into it as a child. Query: ?id=<id>",
23274
+ request: MoveDocumentReq,
23275
+ response: DocumentSchema,
23276
+ handler: handle(async (req, { db }) => {
23277
+ const body = await req.json();
23278
+ return moveDocument(db, need(req, "id"), body.target, body.where);
23279
+ })
23280
+ },
23025
23281
  {
23026
23282
  method: "DELETE",
23027
23283
  path: "/api/document",
@@ -23049,6 +23305,7 @@ var SiteSchema = exports_external.object({
23049
23305
  title: exports_external.string().nullable(),
23050
23306
  created_hlc: exports_external.string()
23051
23307
  });
23308
+ var SiteWithCountSchema = SiteSchema.extend({ file_count: exports_external.number() });
23052
23309
  var SiteFileSchema = exports_external.object({
23053
23310
  id: exports_external.string(),
23054
23311
  site_id: exports_external.string(),
@@ -23056,6 +23313,8 @@ var SiteFileSchema = exports_external.object({
23056
23313
  content_type: exports_external.string(),
23057
23314
  encoding: exports_external.string()
23058
23315
  });
23316
+ var CreateSiteBody = exports_external.object({ name: exports_external.string(), title: exports_external.string().optional() });
23317
+ var UpdateSiteBody = exports_external.object({ name: exports_external.string().optional(), title: exports_external.string().optional() });
23059
23318
  function need2(req, key) {
23060
23319
  const v3 = new URL(req.url).searchParams.get(key);
23061
23320
  if (!v3)
@@ -23075,9 +23334,9 @@ var sitesRoutes = [
23075
23334
  {
23076
23335
  method: "GET",
23077
23336
  path: "/api/sites",
23078
- summary: "List published sites",
23079
- response: exports_external.array(SiteSchema),
23080
- handler: handle2((_req, { db }) => listSites(db))
23337
+ summary: "List published sites (with file counts)",
23338
+ response: exports_external.array(SiteWithCountSchema),
23339
+ handler: handle2((_req, { db }) => listSites(db).map((s2) => ({ ...s2, file_count: listFiles(db, s2.id).length })))
23081
23340
  },
23082
23341
  {
23083
23342
  method: "GET",
@@ -23085,6 +23344,60 @@ var sitesRoutes = [
23085
23344
  summary: "List a site's files (manifest). Query: ?site=<id|name>",
23086
23345
  response: exports_external.array(SiteFileSchema),
23087
23346
  handler: handle2((req, { db }) => listFiles(db, resolveSite(db, need2(req, "site")).id))
23347
+ },
23348
+ {
23349
+ method: "POST",
23350
+ path: "/api/sites",
23351
+ summary: "Create a site",
23352
+ request: CreateSiteBody,
23353
+ response: SiteSchema,
23354
+ handler: handle2(async (req, { db }) => {
23355
+ const body = CreateSiteBody.parse(await req.json());
23356
+ return createSite(db, body);
23357
+ })
23358
+ },
23359
+ {
23360
+ method: "PATCH",
23361
+ path: "/api/site",
23362
+ summary: "Rename a site or change its title. Query: ?id=<id>",
23363
+ request: UpdateSiteBody,
23364
+ response: SiteSchema,
23365
+ handler: handle2(async (req, { db }) => {
23366
+ const body = UpdateSiteBody.parse(await req.json());
23367
+ return updateSite(db, need2(req, "id"), body);
23368
+ })
23369
+ },
23370
+ {
23371
+ method: "DELETE",
23372
+ path: "/api/site",
23373
+ summary: "Delete a site and its files. Query: ?id=<id>",
23374
+ response: exports_external.object({ ok: exports_external.boolean() }),
23375
+ handler: handle2((req, { db }) => ({ ok: deleteSite(db, need2(req, "id")) }))
23376
+ },
23377
+ {
23378
+ method: "POST",
23379
+ path: "/api/site/file",
23380
+ summary: "Upload/replace a file. Query: ?site=<id|name>&path=<path>; body is raw bytes",
23381
+ response: SiteFileSchema,
23382
+ handler: handle2(async (req, { db }) => {
23383
+ const site = resolveSite(db, need2(req, "site"));
23384
+ const ct3 = req.headers.get("content-type");
23385
+ const file2 = await putFile(db, site.id, need2(req, "path"), {
23386
+ data: await req.arrayBuffer(),
23387
+ contentType: ct3 && ct3 !== "application/octet-stream" ? ct3 : undefined
23388
+ });
23389
+ const { content: _content, ...summary } = file2;
23390
+ return summary;
23391
+ })
23392
+ },
23393
+ {
23394
+ method: "DELETE",
23395
+ path: "/api/site/file",
23396
+ summary: "Delete a file. Query: ?site=<id|name>&path=<path>",
23397
+ response: exports_external.object({ ok: exports_external.boolean() }),
23398
+ handler: handle2((req, { db }) => ({
23399
+ ok: deleteFile(db, resolveSite(db, need2(req, "site")).id, need2(req, "path"))
23400
+ }))
23088
23401
  }
23089
23402
  ];
23090
23403
 
@@ -23711,7 +24024,9 @@ USAGE
23711
24024
  CONCEPTS
23712
24025
  database (db) A typed table. Owns properties and records.
23713
24026
  property (prop) A column with a type (text|number|...). Belongs to a db.
23714
- record (rec) A row: a { column: value } map inside a db.
24027
+ record (rec) A row of cells. Write a flat { column: value } map; reads
24028
+ wrap them as { id, database_id, values: { column: value } }
24029
+ \u2014 field values live under "values", keyed by property name.
23715
24030
  document (doc) A markdown doc, stored as ordered blocks (block-level CRDT).
23716
24031
  site (site) A named bucket of files (HTML/CSS/JS) served at /sites/<name>/.
23717
24032
  Every id is "<kind>_<slug>-<rand>", e.g. db_tasks-k3f9c1, rec_fix-login-7j02an.
@@ -23808,6 +24123,13 @@ AI WORKFLOW (edit a doc without clobbering concurrent changes)
23808
24123
  mh doc edit <ref> --old "old text" --new "new text" --if-match <version>
23809
24124
  mh doc append <ref> --body "a new paragraph"
23810
24125
 
24126
+ SITE DATA (read a table from a hosted page \u2014 field values nest under "values")
24127
+ // a page served at /sites/<name>/ reads this library same-origin:
24128
+ const rs = await fetch('/api/records?db=tasks').then(r => r.json());
24129
+ // rs.records: [{ id, database_id, values: { Title, Status } }]
24130
+ rs.records.forEach(r => render(r.values.Title, r.values.Status));
24131
+ // NOTE: writes take a flat { column: value } map; reads wrap cells in .values
24132
+
23811
24133
  Run 'mh <command> --help' for arguments and examples of a specific command.`;
23812
24134
  var EXAMPLES = {
23813
24135
  init: ["mh init", "METAHUB_HOME=/tmp/mh mh init # use an alternate home"],
@@ -23991,4 +24313,4 @@ if (argv2.includes("--server")) {
23991
24313
  runMain(main, { showUsage: showUsage2 });
23992
24314
  }
23993
24315
 
23994
- //# debugId=6697905C98729D8364756E2164756E21
24316
+ //# debugId=740ECE016A65928164756E2164756E21