@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 +8 -1
- package/dist/cli.js +124 -18
- package/dist/cli.js.map +9 -9
- package/dist/core/documents.d.ts +9 -6
- package/dist/core/documents.d.ts.map +1 -1
- package/dist/core/md/grammar.d.ts +37 -0
- package/dist/core/md/grammar.d.ts.map +1 -1
- package/dist/core/md/heal.d.ts.map +1 -1
- package/dist/core/sync/share-render.d.ts.map +1 -1
- package/dist/db-worker.js +2 -2
- package/dist/index.js +116 -15
- package/dist/index.js.map +7 -7
- package/dist/webui-fmt.js +1 -1
- package/dist/webui.js +108 -108
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
3108
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
19960
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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=
|
|
33114
|
+
//# debugId=65C9A394BDB4F1CE64756E2164756E21
|