@tensorix/metahub 0.3.1 → 0.3.2

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 CHANGED
@@ -6,6 +6,10 @@ English · [简体中文](./README.zh-CN.md)
6
6
 
7
7
  metahub is a local-first knowledge base CLI built on Bun + SQLite. It gives an AI agent a durable, syncable working memory: Notion-style **typed tables** for structured data, **Markdown documents** for long-form knowledge, and a `Read / Edit / Write`-style editing interface. Everything lives on your own machine under `~/.metahub/` — offline on one machine, and consistent across many.
8
8
 
9
+ <p align="center">
10
+ <img src="./docs/assets/webui.png" alt="metahub WebUI — block-level document editing" width="100%">
11
+ </p>
12
+
9
13
  > Requires [Bun](https://bun.sh). Use `bunx`, a global `mh` install, or a standalone binary.
10
14
 
11
15
  ## Why metahub
@@ -15,7 +19,10 @@ metahub is a local-first knowledge base CLI built on Bun + SQLite. It gives an A
15
19
  - **Local-first and syncable** — your data sits on your own disk and works offline; multiple machines merge cleanly, and edits to different paragraphs don't clobber each other.
16
20
  - **GUI and API included** — `mh --server` starts a browser WebUI (browse, inline-edit, full-text search), a `/api/*` REST interface, and auto-generated OpenAPI docs.
17
21
 
18
- <!-- TODO screenshots: docs/assets/webui.png (WebUI), docs/assets/desktop.png (desktop app) -->
22
+ <p align="center">
23
+ <img src="./docs/assets/webui-table.png" alt="metahub WebUI — typed tables with select, multi-select, and checkbox columns" width="100%">
24
+ </p>
25
+
19
26
  > Want to see the UI? Install the [desktop app](#install) or run `mh --server` and open `http://localhost:7777/`.
20
27
 
21
28
  ## Quick start
package/dist/cli.js CHANGED
@@ -3099,16 +3099,22 @@ var init_documents = __esm(() => {
3099
3099
 
3100
3100
  `)) {
3101
3101
  const affected = blocks.filter((b2) => b2.text.includes(opts.old));
3102
- const next = affected.map((b2) => opts.replaceAll ? b2.text.replaceAll(opts.old, opts.new) : b2.text.replace(opts.old, opts.new));
3102
+ const next = affected.map((b2) => opts.replaceAll ? b2.text.replaceAll(opts.old, () => opts.new) : b2.text.replace(opts.old, () => opts.new));
3103
3103
  if (next.every((t2) => t2.length > 0 && !t2.includes(`
3104
3104
 
3105
3105
  `))) {
3106
3106
  const targets = opts.replaceAll ? affected : affected.slice(0, 1);
3107
- targets.forEach((b2, i2) => emit(db, "doc_blocks", b2.id, "text", next[i2]));
3108
- return { id, changed: true, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
3107
+ let wrote = false;
3108
+ targets.forEach((b2, i2) => {
3109
+ if (next[i2] !== b2.text) {
3110
+ emit(db, "doc_blocks", b2.id, "text", next[i2]);
3111
+ wrote = true;
3112
+ }
3113
+ });
3114
+ return { id, changed: wrote, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
3109
3115
  }
3110
3116
  }
3111
- const newBody = opts.replaceAll ? body.replaceAll(opts.old, opts.new) : body.replace(opts.old, opts.new);
3117
+ const newBody = opts.replaceAll ? body.replaceAll(opts.old, () => opts.new) : body.replace(opts.old, () => opts.new);
3112
3118
  reconcileBody(db, id, newBody);
3113
3119
  return { id, changed: newBody !== body, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
3114
3120
  });
@@ -3132,7 +3138,7 @@ var init_documents = __esm(() => {
3132
3138
  if (count > 1 && !e2.replaceAll)
3133
3139
  throw new MhError("ambiguous", `edit[${i2}]: ${count} matches for old; add surrounding context or set replaceAll`);
3134
3140
  const repl = e2.new ?? "";
3135
- body = e2.replaceAll ? body.replaceAll(e2.old, repl) : body.replace(e2.old, repl);
3141
+ body = e2.replaceAll ? body.replaceAll(e2.old, () => repl) : body.replace(e2.old, () => repl);
3136
3142
  total += e2.replaceAll ? count : 1;
3137
3143
  });
3138
3144
  if (body !== orig)
@@ -19690,6 +19696,70 @@ var init_sites_serve = __esm(() => {
19690
19696
  });
19691
19697
 
19692
19698
  // src/core/md/grammar.ts
19699
+ function safeUrl(url2, opts = {}) {
19700
+ const probe = url2.replace(/[\u0000-\u0020]+/g, "");
19701
+ const m3 = /^([a-z][a-z0-9+.-]*):/i.exec(probe);
19702
+ if (!m3)
19703
+ return url2;
19704
+ const scheme = m3[1].toLowerCase();
19705
+ if (scheme === "http" || scheme === "https" || scheme === "mailto" || scheme === "blob")
19706
+ return url2;
19707
+ if (scheme === "data" && opts.allowData)
19708
+ return url2;
19709
+ return "#";
19710
+ }
19711
+ function extOf(url2) {
19712
+ const clean = url2.split(/[?#]/, 1)[0] ?? url2;
19713
+ const base = clean.slice(clean.lastIndexOf("/") + 1);
19714
+ const dot = base.lastIndexOf(".");
19715
+ return dot >= 0 ? base.slice(dot + 1).toLowerCase() : "";
19716
+ }
19717
+ function imageSyntaxKind(ext2) {
19718
+ if (VIDEO_EXTS.has(ext2))
19719
+ return "video";
19720
+ if (AUDIO_EXTS.has(ext2))
19721
+ return "audio";
19722
+ return "image";
19723
+ }
19724
+ function parseMediaUrl(url2) {
19725
+ const hash3 = url2.indexOf("#");
19726
+ const frag = hash3 >= 0 ? url2.slice(hash3) : "";
19727
+ const noFrag = hash3 >= 0 ? url2.slice(0, hash3) : url2;
19728
+ const q3 = noFrag.indexOf("?");
19729
+ if (q3 < 0)
19730
+ return { src: url2 };
19731
+ const base = noFrag.slice(0, q3);
19732
+ let width;
19733
+ const kept = [];
19734
+ for (const p2 of noFrag.slice(q3 + 1).split("&").filter(Boolean)) {
19735
+ const m3 = p2.match(/^w=(\d+)$/);
19736
+ if (m3)
19737
+ width = parseInt(m3[1], 10);
19738
+ else
19739
+ kept.push(p2);
19740
+ }
19741
+ return { src: base + (kept.length ? "?" + kept.join("&") : "") + frag, width };
19742
+ }
19743
+ function matchMediaEmbed(line) {
19744
+ const t2 = line.trim();
19745
+ let m3 = t2.match(/^!\[([^\]]*)\]\(([^\s)]+)\)$/);
19746
+ if (m3) {
19747
+ const { src: src2, width } = parseMediaUrl(m3[2]);
19748
+ const kind = imageSyntaxKind(extOf(src2));
19749
+ const r3 = { kind, src: src2, name: m3[1] };
19750
+ if (kind === "image" && width)
19751
+ r3.width = width;
19752
+ return r3;
19753
+ }
19754
+ m3 = t2.match(/^\[([^\]]*)\]\(([^\s)]+)(?:\s+"([^"]*)")?\)$/);
19755
+ if (m3 && m3[2].startsWith("/blob/")) {
19756
+ const r3 = { kind: "file", src: m3[2], name: m3[1] };
19757
+ if (m3[3] != null && /^\d+$/.test(m3[3]))
19758
+ r3.size = parseInt(m3[3], 10);
19759
+ return r3;
19760
+ }
19761
+ return null;
19762
+ }
19693
19763
  function matchQuoteLine(text) {
19694
19764
  const m3 = text.match(RE.quote);
19695
19765
  return m3 ? m3[1] : null;
@@ -19793,7 +19863,7 @@ function splitTableRow(line) {
19793
19863
  cells.pop();
19794
19864
  return cells.map((c3) => c3.trim());
19795
19865
  }
19796
- var HTML_FENCE = "mh-html", RE, RE_DELIM_CELL;
19866
+ var HTML_FENCE = "mh-html", RE, VIDEO_EXTS, AUDIO_EXTS, RE_DELIM_CELL;
19797
19867
  var init_grammar = __esm(() => {
19798
19868
  RE = {
19799
19869
  fenceOpen: /^\s*(`{3,}|~{3,})\s*([^\s`]*)?.*$/,
@@ -19804,6 +19874,8 @@ var init_grammar = __esm(() => {
19804
19874
  quote: /^>[ \t](.*)$/,
19805
19875
  divider: /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/
19806
19876
  };
19877
+ VIDEO_EXTS = new Set(["mp4", "webm", "mov", "m4v", "ogv", "mkv", "ogg"]);
19878
+ AUDIO_EXTS = new Set(["mp3", "wav", "m4a", "aac", "flac", "opus", "oga", "weba"]);
19807
19879
  RE_DELIM_CELL = /^\s*:?-+:?\s*$/;
19808
19880
  });
19809
19881
 
@@ -19917,6 +19989,15 @@ function healLegacyMarkdown(src2) {
19917
19989
  changed = true;
19918
19990
  }
19919
19991
  }
19992
+ for (let i2 = lines.length - 1;i2 >= 0; i2--) {
19993
+ if (inFence[i2])
19994
+ continue;
19995
+ const line = lines[i2];
19996
+ if (BARE_QUOTE.test(line) && (quoteish(i2 - 1) || quoteish(i2 + 1))) {
19997
+ lines[i2] = line + " ";
19998
+ changed = true;
19999
+ }
20000
+ }
19920
20001
  return changed ? lines.join(`
19921
20002
  `) : src2;
19922
20003
  }
@@ -19939,7 +20020,7 @@ function renderInline(src2, opts = {}) {
19939
20020
  for (const t2 of tokens) {
19940
20021
  out += escapeHtml(src2.slice(pos, t2.start));
19941
20022
  const inner = src2.slice(t2.innerFrom, t2.innerTo);
19942
- const url2 = rewrite(t2.url ?? "", opts).replace(/"/g, "&quot;");
20023
+ const rawUrl = rewrite(t2.url ?? "", opts);
19943
20024
  switch (t2.kind) {
19944
20025
  case "code":
19945
20026
  out += `<code>${escapeHtml(inner)}</code>`;
@@ -19953,12 +20034,16 @@ function renderInline(src2, opts = {}) {
19953
20034
  case "del":
19954
20035
  out += `<del>${escapeHtml(inner)}</del>`;
19955
20036
  break;
19956
- case "link":
19957
- out += `<a href="${url2}" target="_blank" rel="noreferrer noopener">${escapeHtml(inner)}</a>`;
20037
+ case "link": {
20038
+ const href = escapeHtml(safeUrl(rawUrl));
20039
+ out += `<a href="${href}" target="_blank" rel="noreferrer noopener">${escapeHtml(inner)}</a>`;
19958
20040
  break;
19959
- case "image":
19960
- out += `<img src="${url2}" alt="${escapeHtml(t2.alt ?? "")}" loading="lazy">`;
20041
+ }
20042
+ case "image": {
20043
+ const imgSrc = escapeHtml(safeUrl(rawUrl, { allowData: true }));
20044
+ out += `<img src="${imgSrc}" alt="${escapeHtml(t2.alt ?? "")}" loading="lazy">`;
19961
20045
  break;
20046
+ }
19962
20047
  }
19963
20048
  pos = t2.end;
19964
20049
  }
@@ -19969,6 +20054,19 @@ function rewrite(url2, opts) {
19969
20054
  return opts.rewriteBlob(url2);
19970
20055
  return url2;
19971
20056
  }
20057
+ function renderMedia(m3, opts) {
20058
+ const raw = rewrite(m3.src, opts);
20059
+ if (m3.kind === "video") {
20060
+ const src2 = escapeHtml(safeUrl(raw, { allowData: true }));
20061
+ return `<p class="mh-media"><video src="${src2}" controls preload="metadata"></video></p>`;
20062
+ }
20063
+ if (m3.kind === "audio") {
20064
+ const src2 = escapeHtml(safeUrl(raw, { allowData: true }));
20065
+ return `<p class="mh-media"><audio src="${src2}" controls preload="metadata"></audio></p>`;
20066
+ }
20067
+ const href = escapeHtml(safeUrl(raw));
20068
+ return `<p class="mh-file"><a href="${href}" download>${escapeHtml(m3.name || "file")}</a></p>`;
20069
+ }
19972
20070
  function renderMarkdown(md, opts = {}) {
19973
20071
  const healed = healLegacyMarkdown((md ?? "").replace(/\r\n?/g, `
19974
20072
  `));
@@ -20080,9 +20178,10 @@ function renderMarkdown(md, opts = {}) {
20080
20178
  out.push(ordered ? `<ol>${lis.join("")}</ol>` : `<ul>${lis.join("")}</ul>`);
20081
20179
  continue;
20082
20180
  }
20083
- if (/^\s*!\[[^\]]*\]\([^)\s]+\)\s*$/.test(line)) {
20181
+ const media = matchMediaEmbed(line);
20182
+ if (media) {
20084
20183
  flushPara();
20085
- out.push(`<p class="mh-img">${renderInline(line.trim(), opts)}</p>`);
20184
+ out.push(media.kind === "image" ? `<p class="mh-img">${renderInline(line.trim(), opts)}</p>` : renderMedia(media, opts));
20086
20185
  i2++;
20087
20186
  continue;
20088
20187
  }
@@ -20517,6 +20616,8 @@ function pageShell(title, inner, opts = {}) {
20517
20616
  td[contenteditable]{outline:none} td.saving{opacity:.5} td.saved{background:rgba(46,160,67,.18)}
20518
20617
  .tag{display:inline-block;background:var(--card);border:1px solid var(--line);border-radius:999px;padding:1px 9px;font-size:12px}
20519
20618
  .muted{color:var(--muted)} .mh-img{text-align:center}
20619
+ .mh-media{text-align:center} .mh-media video,.mh-media audio{max-width:100%}
20620
+ .mh-file a{display:inline-block;background:var(--card);border:1px solid var(--line);border-radius:8px;padding:8px 14px;color:var(--accent);text-decoration:none}
20520
20621
  iframe.mh-embed{width:100%;border:1px solid var(--line);border-radius:8px;min-height:240px}
20521
20622
  textarea.mh-raw{width:100%;min-height:60vh;font:14px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;padding:14px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--fg)}
20522
20623
  ul.files{list-style:none;padding:0} ul.files li{padding:4px 0} button.file{background:none;border:0;color:var(--accent);cursor:pointer;font-size:15px;padding:0}
@@ -21955,7 +22056,7 @@ async function runMain(cmd, opts = {}) {
21955
22056
  // package.json
21956
22057
  var package_default = {
21957
22058
  name: "@tensorix/metahub",
21958
- version: "0.3.1",
22059
+ version: "0.3.2",
21959
22060
  description: "Metahub core library and CLI",
21960
22061
  license: "AGPL-3.0-only",
21961
22062
  type: "module",
@@ -23678,6 +23779,7 @@ var get_default = defineCommand({
23678
23779
  init_properties();
23679
23780
 
23680
23781
  // src/cli/input.ts
23782
+ init_errors();
23681
23783
  async function resolveValue2(raw) {
23682
23784
  if (raw == null)
23683
23785
  return;
@@ -23691,7 +23793,11 @@ async function resolveJson(raw) {
23691
23793
  const text = await resolveValue2(raw);
23692
23794
  if (text == null)
23693
23795
  return;
23694
- return JSON.parse(text);
23796
+ try {
23797
+ return JSON.parse(text);
23798
+ } catch (e2) {
23799
+ throw new MhError("invalid_input", `invalid JSON: ${e2 instanceof Error ? e2.message : String(e2)}`);
23800
+ }
23695
23801
  }
23696
23802
 
23697
23803
  // src/cli/commands/prop.ts
@@ -24191,7 +24297,7 @@ var edit = defineCommand({
24191
24297
  throw new MhError("invalid_input", "use --old/--new/--replace-all or --edits, not both");
24192
24298
  const edits = parseEditPairs(await resolveJson(args.edits));
24193
24299
  const r4 = editDocumentBatch(db, id, { edits, ifMatch: args["if-match"] });
24194
- print(r4, () => `edited ${r4.id} (${r4.replaced} replaced across ${edits.length} edit(s))`);
24300
+ print(r4, () => r4.changed ? `edited ${r4.id} (${r4.replaced} replaced across ${edits.length} edit(s))` : `no change: ${r4.id}`);
24195
24301
  return;
24196
24302
  }
24197
24303
  if (args.old == null)
@@ -24204,7 +24310,7 @@ var edit = defineCommand({
24204
24310
  replaceAll: args["replace-all"],
24205
24311
  ifMatch: args["if-match"]
24206
24312
  });
24207
- print(r3, () => `edited ${r3.id} (${r3.replaced} replaced)`);
24313
+ print(r3, () => r3.changed ? `edited ${r3.id} (${r3.replaced} replaced)` : `no change: ${r3.id}`);
24208
24314
  })
24209
24315
  });
24210
24316
  var append = defineCommand({
@@ -33005,4 +33111,4 @@ if (argv2.includes("--server")) {
33005
33111
  runMain(main, { showUsage: showUsage2 });
33006
33112
  }
33007
33113
 
33008
- //# debugId=6B3DFB027376050D64756E2164756E21
33114
+ //# debugId=65C9A394BDB4F1CE64756E2164756E21