@tensorix/metahub 0.1.4 → 0.1.7
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/LICENSE +235 -0
- package/README.md +86 -166
- package/README.zh-CN.md +127 -0
- package/dist/cli.js +142 -73
- package/dist/cli.js.map +9 -9
- package/dist/core/blocks.d.ts +23 -4
- package/dist/core/blocks.d.ts.map +1 -1
- package/dist/core/db.d.ts +6 -0
- package/dist/core/db.d.ts.map +1 -1
- package/dist/core/documents.d.ts.map +1 -1
- package/dist/core/schema.d.ts +1 -1
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/sync/server.d.ts.map +1 -1
- package/dist/core/sync/webui.d.ts.map +1 -1
- package/dist/index.js +129 -73
- package/dist/index.js.map +9 -9
- package/dist/webui.js +30 -31
- package/package.json +5 -2
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# metahub
|
|
2
|
+
|
|
3
|
+
**给你的 AI 一个本地知识库。**
|
|
4
|
+
|
|
5
|
+
[English](./README.md) · 简体中文
|
|
6
|
+
|
|
7
|
+
metahub 是基于 Bun + SQLite 的本地优先知识库 CLI,给 AI agent 一份持久、可同步的工作记忆:用 notion 风格的**类型化数据表**装结构化数据,用 **markdown 文档**装长文本知识,再配一套像 `Read / Edit / Write` 的编辑接口。数据全部存在你本机的 `~/.metahub/`,单机离线可用,多机之间保持一致。
|
|
8
|
+
|
|
9
|
+
> 运行时依赖 [Bun](https://bun.sh)。用 `bunx`、全局安装后的 `mh`,或下载独立二进制都行。
|
|
10
|
+
|
|
11
|
+
## 为什么用它
|
|
12
|
+
|
|
13
|
+
- **为 AI 读写而设计** —— `doc read` 先返回正文 + 版本号,`doc edit --old/--new` 做锚定查找替换、只传增量;引用一律支持 id 前缀 / 名字,不必粘完整 id。
|
|
14
|
+
- **结构化 + 非结构化一处管** —— 类型化数据表(行 / 列,8 种属性类型)放任务、流水、联系人;markdown 文档放设计稿、笔记。
|
|
15
|
+
- **本地优先、可同步** —— 数据在你自己的磁盘上,离线可用;多台机器干净合并,改不同段落互不覆盖。
|
|
16
|
+
- **自带 GUI 与 API** —— `mh --server` 一条命令起浏览器 WebUI(浏览 / 行内编辑 / 全文搜索)+ `/api/*` REST 接口 + 自动生成的 OpenAPI 文档。
|
|
17
|
+
|
|
18
|
+
<!-- TODO 截图:docs/assets/webui.png(WebUI)、docs/assets/desktop.png(桌面端) -->
|
|
19
|
+
> 想看界面?装上[桌面 App](#获取--安装),或运行 `mh --server` 后打开 `http://localhost:7777/`。
|
|
20
|
+
|
|
21
|
+
## 快速开始
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bunx @tensorix/metahub init # 创建 ~/.metahub
|
|
25
|
+
mh db create "Tasks" # 建一张表 → 返回 id,如 db_tasks-k3f9c1
|
|
26
|
+
mh use tasks # 设为「当前库」,之后 record/prop 免带库参数
|
|
27
|
+
mh prop add Title --type text
|
|
28
|
+
mh prop add Status --type select --options "todo,doing,done"
|
|
29
|
+
mh record create --data '{"Title":"写设计稿","Status":"todo"}'
|
|
30
|
+
mh doc create --title "架构说明" --body @arch.md # @file / @- / 直接字符串
|
|
31
|
+
mh search "架构" # 全文检索文档 + 记录
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
引用不必粘完整 id:凡接受 id 的地方都接受**完整 id / 唯一前缀 / 名字**,歧义时报错列候选。
|
|
35
|
+
|
|
36
|
+
**给 AI 用的非交互增量编辑**(对标 `Read / Edit / Write`):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
mh doc read <ref> # 读正文 + version(改前先读)
|
|
40
|
+
mh doc edit <ref> --old "旧文本" --new "新文本" # 锚定查找替换,只传增量
|
|
41
|
+
mh doc append <ref> --body "追加段落" # 也有 prepend
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
输出按受众自动切换:终端(人)显示表格 / markdown,被管道或子进程调用(AI)输出 **JSON**;`--json` / `--pretty` 可强制。
|
|
45
|
+
|
|
46
|
+
## 获取 / 安装
|
|
47
|
+
|
|
48
|
+
| 形态 | 安装 | 适合 |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| **桌面 App**(GUI) | `brew install --cask tensorix/tap/metahub-app` | 想要图形界面、像 notion 那样用 |
|
|
51
|
+
| **CLI** | `brew install tensorix/tap/metahub-cli`,或 `npm i -g @tensorix/metahub`,或免安装 `bunx @tensorix/metahub <cmd>` | AI agent / 命令行 |
|
|
52
|
+
| **库**(Bun) | `bun add @tensorix/metahub` | 在自己的 Bun 程序里直接调用 |
|
|
53
|
+
| **独立二进制** | 下载对应平台文件,`chmod +x` 后直接运行 | 无运行时环境 |
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// 作为库使用:所有 core 能力从包根导出
|
|
57
|
+
import { openMetahub, createDatabase, createRecord, search } from "@tensorix/metahub";
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
支持平台:`darwin-arm64` / `darwin-x64` / `linux-x64` / `linux-arm64` / `windows-x64`。库 API 详见 [CONTRIBUTING.md](./CONTRIBUTING.md)。
|
|
61
|
+
|
|
62
|
+
## 多机同步
|
|
63
|
+
|
|
64
|
+
在一台机器起服务端,另一台与它同步。文档按段落级合并,所以并发改同一篇文档的不同部分能干净合并。
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# A 机:启动同步服务端(它本身也是一个 metahub 节点)
|
|
68
|
+
mh --server --port 7777
|
|
69
|
+
|
|
70
|
+
# B 机:与服务端推 / 拉一轮
|
|
71
|
+
mh sync http://a-host:7777
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
配对一次后即可后台**周期性自动双向同步**,不必每次手敲 `mh sync`。同一条 `sync` 命令也能在「单个文档 / 数据表」与本地文件之间导入导出(文档 ↔ markdown、数据表 ↔ CSV)。原理详见[系统设计文档](./docs/system-design/)。
|
|
75
|
+
|
|
76
|
+
## WebUI、API 与 Agent 站点
|
|
77
|
+
|
|
78
|
+
`mh --server` 在根路径 `/` 内置浏览器 **WebUI**(浏览 / 行内编辑数据表、块级所见即所得编辑文档、全文搜索)。同一服务端还暴露:
|
|
79
|
+
|
|
80
|
+
- `/api/*` —— REST 接口,读写本库的数据表与文档。
|
|
81
|
+
- `/docs` —— 自动生成的 OpenAPI 文档。
|
|
82
|
+
- `/sites/<name>/` —— `mh site publish` 托管 agent 生成的 HTML/CSS/JS;页面同源调用 `/api/*` 即可读本库数据(一个本地 mini-Supabase)。
|
|
83
|
+
|
|
84
|
+
每个请求由单 token 守护(持久化在 `~/.metahub`)。服务端默认只绑 `127.0.0.1`;`--host 0.0.0.0` 才对外,此时凭据以明文 Bearer 传输,请置于可信网络或前置 TLS。细节见[系统设计文档](./docs/system-design/)。
|
|
85
|
+
|
|
86
|
+
## 命令参考
|
|
87
|
+
|
|
88
|
+
<details>
|
|
89
|
+
<summary>展开完整命令表</summary>
|
|
90
|
+
|
|
91
|
+
| 命令 | 说明 |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| `mh init` | 创建 `~/.metahub` |
|
|
94
|
+
| `mh db create\|list\|get\|delete` | 管理数据库(表) |
|
|
95
|
+
| `mh use [<db>] [--clear]` | 设置/显示「当前库」(record/prop 默认作用于它) |
|
|
96
|
+
| `mh get <ref>` | 通用查找:按 id/前缀/名字解析,自动判别类型 |
|
|
97
|
+
| `mh prop add\|list\|update\|remove` | 管理属性(列);`add` 用 `--db` 指定库(默认当前库) |
|
|
98
|
+
| `mh record create\|list\|get\|update\|delete` | 管理记录(行) |
|
|
99
|
+
| `mh doc create\|list\|get\|update\|delete` | 管理 markdown 文档 |
|
|
100
|
+
| `mh doc read <id>` | 读正文 + version token(AI 改前先读) |
|
|
101
|
+
| `mh doc edit <id> --old --new` | 锚定查找替换(`--replace-all` / `--if-match`) |
|
|
102
|
+
| `mh doc append\|prepend <id> --body` | 在文档首/尾追加块 |
|
|
103
|
+
| `mh edit <id>` | 在 `$EDITOR` 中交互式编辑文档/记录(给人用) |
|
|
104
|
+
| `mh search <query>` | 全文检索(文档 + 记录) |
|
|
105
|
+
| `mh doctor` | 只读体检:列出逻辑完整性问题(孤儿引用/单元格、重复路径、文档环、重名等) |
|
|
106
|
+
| `mh repair [--dry-run]` | 确定性、幂等修复可自动修的问题(改动随 oplog 复制);`--dry-run` 仅预览(等价 doctor) |
|
|
107
|
+
| `mh site create\|put\|publish\|list\|files\|rm\|delete` | 托管 agent 生成的静态站点(HTML/CSS/JS),由 `--server` 在 `/sites/<name>/` serve 出去 |
|
|
108
|
+
| `mh token [show\|refresh]` | 查看 / 轮换持久化的服务器鉴权 token(存于 `~/.metahub`,默认 30 天到期轮换) |
|
|
109
|
+
| `mh completion <bash\|zsh\|fish>` | 打印补全脚本:`eval "$(mh completion zsh)"` |
|
|
110
|
+
| `mh sync <url>` | 与服务端同步一轮(CRDT 推/拉);`/sync` 受保护时按已存凭据直连,否则在交互终端提示输入 token 并记住(`--token` 非交互) |
|
|
111
|
+
| `mh sync <src> <dst>` | 单个文档/数据表与文件互导:文档↔markdown、数据表↔CSV;方向按参数判别(哪侧是库内实体) |
|
|
112
|
+
| `mh config` | 配置服务器与同步设备:无参进交互向导,`--flag` 直配(`--host/--port/--sync-interval/--auto-sync`) |
|
|
113
|
+
| `mh config peer code\|add\|list\|sync\|enable\|disable\|rm` | 多设备配对与管理:生成一次性配对码 / 配对 / 列出 / 立即同步 / 启停 / 移除(连带吊销签发给对方的凭据) |
|
|
114
|
+
| `mh config grant list\|revoke` | 列出 / 吊销本机签发的入站同步凭据(`revoke --token` 支持精确或前缀) |
|
|
115
|
+
| `mh --server [--port] [--host] [--debug] [--token] [--sync-interval] [--no-auto-sync]` | 启动服务端:`/sync`(主 token 或配对凭据)+ 根路径 WebUI + `/api/*` REST + `/docs`(OpenAPI)+ 静态站点 `/sites/<name>/` + token 交换 `/auth/token` + 配对 `/api/pair`;内置定时器自动同步已配对 peer |
|
|
116
|
+
|
|
117
|
+
</details>
|
|
118
|
+
|
|
119
|
+
## 文档
|
|
120
|
+
|
|
121
|
+
- [系统设计](./docs/system-design/) —— 架构、数据模型、能力清单、使用流程。
|
|
122
|
+
- [贡献指南](./CONTRIBUTING.md) —— 本地开发、构建、发布、目录结构、库 API。
|
|
123
|
+
- [桌面 App](./apps/desktop/README.md) —— Electron 壳 + Bun sidecar。
|
|
124
|
+
|
|
125
|
+
## 许可证
|
|
126
|
+
|
|
127
|
+
[AGPL-3.0-only](./LICENSE)。
|
package/dist/cli.js
CHANGED
|
@@ -5,25 +5,43 @@ var __getProtoOf = Object.getPrototypeOf;
|
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
function __accessProp(key) {
|
|
9
|
+
return this[key];
|
|
10
|
+
}
|
|
11
|
+
var __toESMCache_node;
|
|
12
|
+
var __toESMCache_esm;
|
|
8
13
|
var __toESM = (mod, isNodeMode, target) => {
|
|
14
|
+
var canCache = mod != null && typeof mod === "object";
|
|
15
|
+
if (canCache) {
|
|
16
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
17
|
+
var cached = cache.get(mod);
|
|
18
|
+
if (cached)
|
|
19
|
+
return cached;
|
|
20
|
+
}
|
|
9
21
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
22
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
23
|
for (let key of __getOwnPropNames(mod))
|
|
12
24
|
if (!__hasOwnProp.call(to, key))
|
|
13
25
|
__defProp(to, key, {
|
|
14
|
-
get: (
|
|
26
|
+
get: __accessProp.bind(mod, key),
|
|
15
27
|
enumerable: true
|
|
16
28
|
});
|
|
29
|
+
if (canCache)
|
|
30
|
+
cache.set(mod, to);
|
|
17
31
|
return to;
|
|
18
32
|
};
|
|
19
33
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
34
|
+
var __returnValue = (v) => v;
|
|
35
|
+
function __exportSetter(name, newValue) {
|
|
36
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
37
|
+
}
|
|
20
38
|
var __export = (target, all) => {
|
|
21
39
|
for (var name in all)
|
|
22
40
|
__defProp(target, name, {
|
|
23
41
|
get: all[name],
|
|
24
42
|
enumerable: true,
|
|
25
43
|
configurable: true,
|
|
26
|
-
set: (
|
|
44
|
+
set: __exportSetter.bind(all, name)
|
|
27
45
|
});
|
|
28
46
|
};
|
|
29
47
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -667,7 +685,7 @@ var init_prompt = __esm(() => {
|
|
|
667
685
|
Object.keys(r.modifier);
|
|
668
686
|
tD = Object.keys(r.color);
|
|
669
687
|
eD = Object.keys(r.bgColor);
|
|
670
|
-
[...tD];
|
|
688
|
+
[...tD, ...eD];
|
|
671
689
|
iD = sD();
|
|
672
690
|
v = new Set(["\x1B", "\x9B"]);
|
|
673
691
|
y = `${rD}8;;`;
|
|
@@ -891,7 +909,7 @@ function observeHlc(db, node, remote, now = Date.now()) {
|
|
|
891
909
|
}
|
|
892
910
|
|
|
893
911
|
// src/core/blocks.ts
|
|
894
|
-
function
|
|
912
|
+
function parseDocBlocks(md) {
|
|
895
913
|
if (!md)
|
|
896
914
|
return [];
|
|
897
915
|
const lines = md.replace(/\r\n?/g, `
|
|
@@ -901,24 +919,24 @@ function parseBlocks(md) {
|
|
|
901
919
|
let cur = [];
|
|
902
920
|
let fenceChar = null;
|
|
903
921
|
let fenceLen = 0;
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
cur.
|
|
907
|
-
|
|
908
|
-
cur
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
922
|
+
const closeBlock = () => {
|
|
923
|
+
if (cur.length) {
|
|
924
|
+
blocks.push({ text: cur.join(`
|
|
925
|
+
`), blankAfter: 0 });
|
|
926
|
+
cur = [];
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
const onBlank = () => {
|
|
930
|
+
closeBlock();
|
|
931
|
+
if (blocks.length)
|
|
932
|
+
blocks[blocks.length - 1].blankAfter++;
|
|
913
933
|
};
|
|
914
934
|
for (const raw of lines) {
|
|
915
935
|
if (fenceChar) {
|
|
916
936
|
cur.push(raw);
|
|
917
937
|
const m2 = raw.match(/^\s*(`{3,}|~{3,})\s*$/);
|
|
918
938
|
if (m2 && m2[1][0] === fenceChar && m2[1].length >= fenceLen) {
|
|
919
|
-
|
|
920
|
-
`));
|
|
921
|
-
cur = [];
|
|
939
|
+
closeBlock();
|
|
922
940
|
fenceChar = null;
|
|
923
941
|
fenceLen = 0;
|
|
924
942
|
}
|
|
@@ -926,28 +944,34 @@ function parseBlocks(md) {
|
|
|
926
944
|
}
|
|
927
945
|
const open = raw.match(/^\s*(`{3,}|~{3,})/);
|
|
928
946
|
if (open) {
|
|
929
|
-
|
|
947
|
+
closeBlock();
|
|
930
948
|
fenceChar = open[1][0];
|
|
931
949
|
fenceLen = open[1].length;
|
|
932
950
|
cur.push(raw);
|
|
933
951
|
} else if (raw.trim() === "") {
|
|
934
|
-
|
|
952
|
+
onBlank();
|
|
935
953
|
} else {
|
|
936
954
|
cur.push(raw);
|
|
937
955
|
}
|
|
938
956
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
`));
|
|
943
|
-
} else {
|
|
944
|
-
flushPara();
|
|
945
|
-
}
|
|
957
|
+
closeBlock();
|
|
958
|
+
for (let i2 = 0;i2 < blocks.length - 1; i2++)
|
|
959
|
+
blocks[i2].blankAfter = Math.max(0, blocks[i2].blankAfter - 1);
|
|
946
960
|
return blocks;
|
|
947
961
|
}
|
|
948
|
-
function
|
|
949
|
-
|
|
950
|
-
|
|
962
|
+
function serializeDocBlocks(blocks) {
|
|
963
|
+
const live = blocks.filter((b2) => !!b2.text && b2.text.length > 0);
|
|
964
|
+
const out = [];
|
|
965
|
+
live.forEach((b2, i2) => {
|
|
966
|
+
out.push(...b2.text.split(`
|
|
967
|
+
`));
|
|
968
|
+
const extra = Math.max(0, b2.blankAfter ?? 0);
|
|
969
|
+
if (i2 < live.length - 1)
|
|
970
|
+
out.push("");
|
|
971
|
+
for (let k2 = 0;k2 < extra; k2++)
|
|
972
|
+
out.push("");
|
|
973
|
+
});
|
|
974
|
+
return out.join(`
|
|
951
975
|
`);
|
|
952
976
|
}
|
|
953
977
|
function reconcile(oldTexts, newTexts) {
|
|
@@ -1004,9 +1028,9 @@ function isBlockManaged(db, docId) {
|
|
|
1004
1028
|
return db.query("SELECT 1 AS x FROM doc_blocks WHERE doc_id = ? LIMIT 1").get(docId) != null;
|
|
1005
1029
|
}
|
|
1006
1030
|
function recomputeDocBody(db, docId) {
|
|
1007
|
-
const rows = db.query("SELECT text FROM doc_blocks WHERE doc_id = ? AND __deleted = 0 ORDER BY order_key, id").all(docId);
|
|
1031
|
+
const rows = db.query("SELECT text, blank_after FROM doc_blocks WHERE doc_id = ? AND __deleted = 0 ORDER BY order_key, id").all(docId);
|
|
1008
1032
|
ensureRow(db, "documents", docId);
|
|
1009
|
-
db.query("UPDATE documents SET body = ? WHERE id = ?").run(
|
|
1033
|
+
db.query("UPDATE documents SET body = ? WHERE id = ?").run(serializeDocBlocks(rows.map((r3) => ({ text: r3.text, blankAfter: r3.blank_after ?? 0 }))), docId);
|
|
1010
1034
|
}
|
|
1011
1035
|
function materialize(db, dataset, rowId, col, valueJson) {
|
|
1012
1036
|
if (dataset === "records") {
|
|
@@ -1096,7 +1120,7 @@ var init_crdt = __esm(() => {
|
|
|
1096
1120
|
},
|
|
1097
1121
|
doc_blocks: {
|
|
1098
1122
|
table: "doc_blocks",
|
|
1099
|
-
cols: new Set(["doc_id", "text", "order_key", "__deleted"])
|
|
1123
|
+
cols: new Set(["doc_id", "text", "order_key", "blank_after", "__deleted"])
|
|
1100
1124
|
},
|
|
1101
1125
|
sites: {
|
|
1102
1126
|
table: "sites",
|
|
@@ -1390,6 +1414,7 @@ async function serveWebui(req) {
|
|
|
1390
1414
|
headers: { "content-type": "text/javascript; charset=utf-8" }
|
|
1391
1415
|
});
|
|
1392
1416
|
} catch (e3) {
|
|
1417
|
+
console.error("[webui] failed to serve /webui.js \u2014", e3);
|
|
1393
1418
|
return new Response(`webui build failed: ${e3}`, { status: 500 });
|
|
1394
1419
|
}
|
|
1395
1420
|
}
|
|
@@ -1423,6 +1448,7 @@ var HTML = `<!doctype html>
|
|
|
1423
1448
|
--radius:8px; --radius-sm:6px;
|
|
1424
1449
|
--ui:"Hanken Grotesk",-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
|
|
1425
1450
|
--mono:"JetBrains Mono",ui-monospace,SFMono-Regular,monospace;
|
|
1451
|
+
--note-lh:1.6;
|
|
1426
1452
|
--code-bg:#f8f8f6;
|
|
1427
1453
|
--hl-cmt:#9a9aa2; --hl-key:#a626a4; --hl-str:#28823f; --hl-num:#b76a00;
|
|
1428
1454
|
--hl-fn:#3f51d6; --hl-type:#0a7ea4; --hl-var:#c2410c;
|
|
@@ -1455,7 +1481,7 @@ var HTML = `<!doctype html>
|
|
|
1455
1481
|
* { box-sizing: border-box; }
|
|
1456
1482
|
html, body { margin: 0; height: 100%; }
|
|
1457
1483
|
body { font-family: var(--ui); font-size: 14px; line-height: 1.55; color: var(--fg); background: var(--bg);
|
|
1458
|
-
overflow: hidden;
|
|
1484
|
+
overflow: hidden; text-rendering: auto; }
|
|
1459
1485
|
button { font: inherit; color: inherit; cursor: pointer; background: none; border: 0; padding: 0; }
|
|
1460
1486
|
input, textarea { font: inherit; color: inherit; }
|
|
1461
1487
|
.muted { color: var(--muted); }
|
|
@@ -1511,6 +1537,8 @@ var HTML = `<!doctype html>
|
|
|
1511
1537
|
.dragging { opacity:.35; }
|
|
1512
1538
|
.sb-footer { flex:none; padding:6px 8px; border-top:1px solid var(--line); }
|
|
1513
1539
|
.sb-footer .navitem { font-size:13.5px; }
|
|
1540
|
+
.nav-dot { flex:none; width:7px; height:7px; border-radius:50%; background:var(--danger);
|
|
1541
|
+
box-shadow:0 0 0 2px var(--surface); margin-right:2px; }
|
|
1514
1542
|
|
|
1515
1543
|
/* settings page */
|
|
1516
1544
|
.set-page { max-width:680px; margin:0 auto; padding:40px 28px 64px; }
|
|
@@ -1520,8 +1548,19 @@ var HTML = `<!doctype html>
|
|
|
1520
1548
|
.set-section-head { font-size:12px; font-weight:600; text-transform:uppercase; letter-spacing:.07em; color:var(--muted); margin-bottom:4px; }
|
|
1521
1549
|
.set-section-desc { color:var(--fg-soft); margin-bottom:16px; }
|
|
1522
1550
|
.set-footer { margin-top:40px; padding-top:20px; border-top:1px solid var(--line); display:flex;
|
|
1523
|
-
justify-content:center; gap:
|
|
1524
|
-
.set-footer-sep { opacity:.
|
|
1551
|
+
flex-wrap:wrap; justify-content:center; align-items:center; gap:7px; color:var(--muted); font-size:12px; }
|
|
1552
|
+
.set-footer-sep { opacity:.45; }
|
|
1553
|
+
.ver-num { font-family:var(--mono); font-size:11px; color:var(--fg-soft); letter-spacing:-.01em; }
|
|
1554
|
+
.ver-up { display:inline-flex; align-items:center; gap:6px; }
|
|
1555
|
+
.ver-up.err .ver-msg { color:var(--danger); }
|
|
1556
|
+
.ver-act { background:none; border:0; padding:0; margin:0; font:inherit; font-size:12px; line-height:1;
|
|
1557
|
+
color:var(--muted); cursor:pointer; transition:color .12s; }
|
|
1558
|
+
.ver-act:hover { color:var(--accent); }
|
|
1559
|
+
.ver-act.accent { color:var(--accent); font-weight:600; }
|
|
1560
|
+
.ver-act[aria-disabled="true"] { opacity:.6; cursor:default; pointer-events:none; }
|
|
1561
|
+
.ver-dot { width:6px; height:6px; border-radius:50%; background:var(--accent); flex:none; }
|
|
1562
|
+
.ver-dot.pulse { animation:verpulse 1.8s ease-in-out infinite; }
|
|
1563
|
+
@keyframes verpulse { 0%,100% { opacity:1; } 50% { opacity:.4; } }
|
|
1525
1564
|
.theme-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; }
|
|
1526
1565
|
.theme-card { position:relative; display:flex; flex-direction:column; align-items:flex-start; gap:10px;
|
|
1527
1566
|
text-align:left; padding:16px; border:1px solid var(--line-strong); border-radius:var(--radius);
|
|
@@ -1614,8 +1653,10 @@ var HTML = `<!doctype html>
|
|
|
1614
1653
|
/* The list item's host gutter controls the whole unit, so suppress the nested
|
|
1615
1654
|
code block's own gutter (it would overlap the list marker on the same row). */
|
|
1616
1655
|
.block.list-code-host + .block-wrap.nested .gutter { display:none; }
|
|
1617
|
-
.editable { outline:none; flex:1; min-width:0; padding:3px 2px; line-height:
|
|
1656
|
+
.editable { outline:none; flex:1; min-width:0; padding:3px 2px; line-height:var(--note-lh); }
|
|
1618
1657
|
.editable:empty::before { content:attr(data-ph); color:var(--muted); pointer-events:none; }
|
|
1658
|
+
/* The generic "/" prompt only shows on the focused line; idle blank lines (paragraphs and list items, any depth) stay blank. */
|
|
1659
|
+
.editable[data-ph-hint="slash"]:empty:not(:focus)::before { content:""; }
|
|
1619
1660
|
.b-h1 .editable { font-size:28px; font-weight:700; letter-spacing:-.02em; padding-top:12px; }
|
|
1620
1661
|
.b-h2 .editable { font-size:22px; font-weight:650; letter-spacing:-.01em; padding-top:8px; }
|
|
1621
1662
|
.b-h3 .editable { font-size:18px; font-weight:600; padding-top:4px; }
|
|
@@ -1667,7 +1708,8 @@ var HTML = `<!doctype html>
|
|
|
1667
1708
|
.hljs-emphasis { font-style:italic; }
|
|
1668
1709
|
.hljs-strong { font-weight:700; }
|
|
1669
1710
|
.marker { width:24px; flex:none; text-align:center; color:var(--fg-soft); user-select:none; padding-top:4px; }
|
|
1670
|
-
.b-todo .marker { padding-top:
|
|
1711
|
+
.b-todo .marker { box-sizing:content-box; padding-top:3px; height:calc(1em * var(--note-lh));
|
|
1712
|
+
display:flex; align-items:center; justify-content:center; }
|
|
1671
1713
|
.b-todo .marker input { width:15px; height:15px; accent-color:var(--accent); cursor:pointer; }
|
|
1672
1714
|
.b-divider { padding:10px 0; }
|
|
1673
1715
|
.b-divider hr { border:0; border-top:1px solid var(--line-strong); margin:0; width:100%; }
|
|
@@ -1854,14 +1896,14 @@ var HTML = `<!doctype html>
|
|
|
1854
1896
|
.optrow .x:hover { background:var(--hover-2); color:var(--danger); }
|
|
1855
1897
|
|
|
1856
1898
|
/* modal */
|
|
1857
|
-
.modal-scrim { position:fixed; inset:0; background:rgba(15,15,25,.4); z-index:100; display:
|
|
1899
|
+
.modal-scrim { position:fixed; inset:0; background:rgba(15,15,25,.4); z-index:100; display:flex; align-items:center; justify-content:center;
|
|
1858
1900
|
opacity:0; pointer-events:none; transition:opacity .15s; padding:20px; }
|
|
1859
1901
|
.modal-scrim.open { opacity:1; pointer-events:auto; }
|
|
1860
|
-
.modal { background:var(--bg); border-radius:16px; box-shadow:var(--shadow-lg); width:440px; max-width:100%; overflow:hidden; }
|
|
1902
|
+
.modal { background:var(--bg); border-radius:16px; box-shadow:var(--shadow-lg); width:440px; max-width:100%; max-height:100%; display:flex; flex-direction:column; overflow:hidden; }
|
|
1861
1903
|
.modal-head { padding:20px 22px 0; }
|
|
1862
1904
|
.modal-head h3 { margin:0; font-size:18px; font-weight:700; letter-spacing:-.01em; }
|
|
1863
1905
|
.modal-head p { margin:6px 0 0; color:var(--muted); font-size:13.5px; }
|
|
1864
|
-
.modal-body { padding:18px 22px; }
|
|
1906
|
+
.modal-body { padding:18px 22px; overflow-y:auto; }
|
|
1865
1907
|
.modal-foot { display:flex; justify-content:flex-end; gap:8px; padding:14px 22px; background:var(--surface); border-top:1px solid var(--line); }
|
|
1866
1908
|
.field-label { font-size:12px; font-weight:600; color:var(--fg-soft); margin:12px 0 5px; }
|
|
1867
1909
|
.field-label:first-child { margin-top:0; }
|
|
@@ -2080,6 +2122,17 @@ var HTML = `<!doctype html>
|
|
|
2080
2122
|
.qn-body .doc { max-width:none; margin:0; padding:14px 22px 30vh; }
|
|
2081
2123
|
.qn-body .doc-title { font-size:24px; margin-bottom:2px; }
|
|
2082
2124
|
.qn-body .doc-meta { display:none; }
|
|
2125
|
+
/* A native checkbox paints an opaque white box when unchecked, which breaks
|
|
2126
|
+
the window's vibrancy blur. Draw a custom box so the unchecked background
|
|
2127
|
+
stays transparent; checked falls back to the accent fill + checkmark. */
|
|
2128
|
+
body.quicknote .b-todo .marker input {
|
|
2129
|
+
appearance:none; -webkit-appearance:none; box-sizing:border-box;
|
|
2130
|
+
border:1.5px solid color-mix(in srgb, var(--fg-soft) 55%, transparent);
|
|
2131
|
+
border-radius:4px; background:transparent; }
|
|
2132
|
+
body.quicknote .b-todo .marker input:checked {
|
|
2133
|
+
border-color:var(--accent);
|
|
2134
|
+
background:var(--accent) center/10px no-repeat
|
|
2135
|
+
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12l5 5L20 7'/%3E%3C/svg%3E"); }
|
|
2083
2136
|
/* Compact menus inside the quick-note window only (dropdown + slash command
|
|
2084
2137
|
share .pop/.item/.lico with the main app; scope under body.quicknote so the
|
|
2085
2138
|
main app keeps its roomier density). */
|
|
@@ -3540,8 +3593,9 @@ async function runMain(cmd, opts = {}) {
|
|
|
3540
3593
|
// package.json
|
|
3541
3594
|
var package_default = {
|
|
3542
3595
|
name: "@tensorix/metahub",
|
|
3543
|
-
version: "0.1.
|
|
3596
|
+
version: "0.1.7",
|
|
3544
3597
|
description: "Metahub core library and CLI",
|
|
3598
|
+
license: "AGPL-3.0-only",
|
|
3545
3599
|
type: "module",
|
|
3546
3600
|
main: "./dist/index.js",
|
|
3547
3601
|
module: "./dist/index.js",
|
|
@@ -3559,7 +3613,9 @@ var package_default = {
|
|
|
3559
3613
|
},
|
|
3560
3614
|
files: [
|
|
3561
3615
|
"dist",
|
|
3562
|
-
"README.md"
|
|
3616
|
+
"README.md",
|
|
3617
|
+
"README.zh-CN.md",
|
|
3618
|
+
"LICENSE"
|
|
3563
3619
|
],
|
|
3564
3620
|
scripts: {
|
|
3565
3621
|
dev: "bun --hot src/cli/index.ts",
|
|
@@ -3692,11 +3748,12 @@ CREATE TABLE IF NOT EXISTS documents (
|
|
|
3692
3748
|
-- independent CRDT register so concurrent edits to different blocks merge.
|
|
3693
3749
|
-- order_key is a fractional index (ties broken by id) giving display order.
|
|
3694
3750
|
CREATE TABLE IF NOT EXISTS doc_blocks (
|
|
3695
|
-
id
|
|
3696
|
-
doc_id
|
|
3697
|
-
text
|
|
3698
|
-
order_key
|
|
3699
|
-
|
|
3751
|
+
id TEXT PRIMARY KEY,
|
|
3752
|
+
doc_id TEXT,
|
|
3753
|
+
text TEXT,
|
|
3754
|
+
order_key TEXT,
|
|
3755
|
+
blank_after INTEGER NOT NULL DEFAULT 0, -- extra blank lines kept after this block (user spacing)
|
|
3756
|
+
__deleted INTEGER NOT NULL DEFAULT 0
|
|
3700
3757
|
);
|
|
3701
3758
|
CREATE INDEX IF NOT EXISTS idx_doc_blocks_doc ON doc_blocks(doc_id);
|
|
3702
3759
|
|
|
@@ -4384,6 +4441,10 @@ function migratePeers(db) {
|
|
|
4384
4441
|
db.exec(`ALTER TABLE peers ADD COLUMN ${col} ${decl}`);
|
|
4385
4442
|
}
|
|
4386
4443
|
}
|
|
4444
|
+
function migrateDocBlocks(db) {
|
|
4445
|
+
if (!hasColumn(db, "doc_blocks", "blank_after"))
|
|
4446
|
+
db.exec("ALTER TABLE doc_blocks ADD COLUMN blank_after INTEGER NOT NULL DEFAULT 0");
|
|
4447
|
+
}
|
|
4387
4448
|
function openMetahub() {
|
|
4388
4449
|
ensureDirs();
|
|
4389
4450
|
const db = new Database(dbPath(), { create: true });
|
|
@@ -4391,6 +4452,7 @@ function openMetahub() {
|
|
|
4391
4452
|
runSchema(db);
|
|
4392
4453
|
migrateRecords(db);
|
|
4393
4454
|
migratePeers(db);
|
|
4455
|
+
migrateDocBlocks(db);
|
|
4394
4456
|
return db;
|
|
4395
4457
|
}
|
|
4396
4458
|
|
|
@@ -4562,7 +4624,7 @@ var use_default = defineCommand({
|
|
|
4562
4624
|
init_ids();
|
|
4563
4625
|
init_crdt();
|
|
4564
4626
|
function liveBlocks(db, docId) {
|
|
4565
|
-
return db.query("SELECT id, text, order_key FROM doc_blocks WHERE doc_id = ? AND __deleted = 0 ORDER BY order_key, id").all(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);
|
|
4566
4628
|
}
|
|
4567
4629
|
function makeBlockId(text) {
|
|
4568
4630
|
return newId("blk", text.split(`
|
|
@@ -4572,29 +4634,37 @@ function emitBlock(db, docId, fields) {
|
|
|
4572
4634
|
const id = makeBlockId(fields.text);
|
|
4573
4635
|
emit(db, "doc_blocks", id, "doc_id", docId);
|
|
4574
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);
|
|
4575
4639
|
emit(db, "doc_blocks", id, "text", fields.text);
|
|
4576
4640
|
}
|
|
4577
|
-
function insertBlocks(db, docId,
|
|
4578
|
-
const keys = keysBetween(after, before,
|
|
4579
|
-
|
|
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 }));
|
|
4580
4644
|
}
|
|
4581
4645
|
function ensureBlocks(db, docId) {
|
|
4582
4646
|
if (db.query("SELECT 1 AS x FROM doc_blocks WHERE doc_id = ? LIMIT 1").get(docId))
|
|
4583
4647
|
return;
|
|
4584
4648
|
const row = db.query("SELECT body FROM documents WHERE id = ?").get(docId);
|
|
4585
|
-
const
|
|
4586
|
-
if (
|
|
4587
|
-
insertBlocks(db, docId,
|
|
4649
|
+
const blocks = parseDocBlocks(row?.body ?? "");
|
|
4650
|
+
if (blocks.length)
|
|
4651
|
+
insertBlocks(db, docId, blocks, null, null);
|
|
4588
4652
|
}
|
|
4589
4653
|
function reconcileBody(db, docId, body) {
|
|
4590
4654
|
const old = liveBlocks(db, docId);
|
|
4591
|
-
const
|
|
4655
|
+
const next = parseDocBlocks(body);
|
|
4656
|
+
const plan = reconcile(old.map((b2) => b2.text), next.map((b2) => b2.text));
|
|
4592
4657
|
for (const oi of plan.deleted)
|
|
4593
4658
|
emit(db, "doc_blocks", old[oi].id, "__deleted", 1);
|
|
4594
4659
|
const { items } = plan;
|
|
4595
4660
|
let i2 = 0;
|
|
4596
4661
|
while (i2 < items.length) {
|
|
4597
|
-
|
|
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);
|
|
4598
4668
|
i2++;
|
|
4599
4669
|
continue;
|
|
4600
4670
|
}
|
|
@@ -4603,8 +4673,7 @@ function reconcileBody(db, docId, body) {
|
|
|
4603
4673
|
j++;
|
|
4604
4674
|
const left = i2 > 0 ? old[items[i2 - 1].keep].order_key : null;
|
|
4605
4675
|
const right = j < items.length ? old[items[j].keep].order_key : null;
|
|
4606
|
-
|
|
4607
|
-
insertBlocks(db, docId, texts, left, right);
|
|
4676
|
+
insertBlocks(db, docId, next.slice(i2, j), left, right);
|
|
4608
4677
|
i2 = j;
|
|
4609
4678
|
}
|
|
4610
4679
|
}
|
|
@@ -4623,9 +4692,9 @@ function createDocument(db, opts) {
|
|
|
4623
4692
|
if (opts.parent_id !== undefined)
|
|
4624
4693
|
emit(db, "documents", id, "parent_id", opts.parent_id);
|
|
4625
4694
|
if (opts.body !== undefined) {
|
|
4626
|
-
const
|
|
4627
|
-
if (
|
|
4628
|
-
insertBlocks(db, id,
|
|
4695
|
+
const blocks = parseDocBlocks(opts.body);
|
|
4696
|
+
if (blocks.length)
|
|
4697
|
+
insertBlocks(db, id, blocks, null, null);
|
|
4629
4698
|
}
|
|
4630
4699
|
return getDocument(db, id);
|
|
4631
4700
|
}
|
|
@@ -4671,7 +4740,7 @@ function editDocument(db, id, opts) {
|
|
|
4671
4740
|
throw new Error("--old must not be empty");
|
|
4672
4741
|
ensureBlocks(db, id);
|
|
4673
4742
|
const blocks = liveBlocks(db, id);
|
|
4674
|
-
const body =
|
|
4743
|
+
const body = serializeDocBlocks(blocks.map((b2) => ({ text: b2.text, blankAfter: b2.blank_after })));
|
|
4675
4744
|
const count = countOccurrences(body, opts.old);
|
|
4676
4745
|
if (count === 0)
|
|
4677
4746
|
throw new Error(`anchor not found: no match for --old`);
|
|
@@ -4698,25 +4767,25 @@ function appendDocument(db, id, body) {
|
|
|
4698
4767
|
if (!getDocument(db, id))
|
|
4699
4768
|
throw new Error(`no such document: ${id}`);
|
|
4700
4769
|
ensureBlocks(db, id);
|
|
4701
|
-
const
|
|
4702
|
-
if (
|
|
4770
|
+
const next = parseDocBlocks(body);
|
|
4771
|
+
if (next.length) {
|
|
4703
4772
|
const blocks = liveBlocks(db, id);
|
|
4704
4773
|
const last = blocks.length ? blocks[blocks.length - 1].order_key : null;
|
|
4705
|
-
insertBlocks(db, id,
|
|
4774
|
+
insertBlocks(db, id, next, last, null);
|
|
4706
4775
|
}
|
|
4707
|
-
return { id, changed:
|
|
4776
|
+
return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
|
|
4708
4777
|
}
|
|
4709
4778
|
function prependDocument(db, id, body) {
|
|
4710
4779
|
if (!getDocument(db, id))
|
|
4711
4780
|
throw new Error(`no such document: ${id}`);
|
|
4712
4781
|
ensureBlocks(db, id);
|
|
4713
|
-
const
|
|
4714
|
-
if (
|
|
4782
|
+
const next = parseDocBlocks(body);
|
|
4783
|
+
if (next.length) {
|
|
4715
4784
|
const blocks = liveBlocks(db, id);
|
|
4716
4785
|
const first = blocks.length ? blocks[0].order_key : null;
|
|
4717
|
-
insertBlocks(db, id,
|
|
4786
|
+
insertBlocks(db, id, next, null, first);
|
|
4718
4787
|
}
|
|
4719
|
-
return { id, changed:
|
|
4788
|
+
return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
|
|
4720
4789
|
}
|
|
4721
4790
|
function deleteDocument(db, id) {
|
|
4722
4791
|
if (!getDocument(db, id))
|
|
@@ -17211,7 +17280,7 @@ function finalize(ctx, schema) {
|
|
|
17211
17280
|
result.$schema = "http://json-schema.org/draft-07/schema#";
|
|
17212
17281
|
} else if (ctx.target === "draft-04") {
|
|
17213
17282
|
result.$schema = "http://json-schema.org/draft-04/schema#";
|
|
17214
|
-
} else if (ctx.target === "openapi-3.0") {}
|
|
17283
|
+
} else if (ctx.target === "openapi-3.0") {}
|
|
17215
17284
|
if (ctx.external?.uri) {
|
|
17216
17285
|
const id = ctx.external.registry.get(schema)?.id;
|
|
17217
17286
|
if (!id)
|
|
@@ -17455,7 +17524,7 @@ var literalProcessor = (schema, ctx, json, _params) => {
|
|
|
17455
17524
|
if (val === undefined) {
|
|
17456
17525
|
if (ctx.unrepresentable === "throw") {
|
|
17457
17526
|
throw new Error("Literal `undefined` cannot be represented in JSON Schema");
|
|
17458
|
-
}
|
|
17527
|
+
}
|
|
17459
17528
|
} else if (typeof val === "bigint") {
|
|
17460
17529
|
if (ctx.unrepresentable === "throw") {
|
|
17461
17530
|
throw new Error("BigInt literals cannot be represented in JSON Schema");
|
|
@@ -23482,7 +23551,7 @@ function startServer(opts = {}) {
|
|
|
23482
23551
|
let timer = null;
|
|
23483
23552
|
if (autoSync && syncIntervalMs > 0) {
|
|
23484
23553
|
timer = setInterval(() => {
|
|
23485
|
-
syncAllPeers(db).catch(() =>
|
|
23554
|
+
syncAllPeers(db).catch((e3) => console.error("[sync] auto-sync tick failed \u2014", e3));
|
|
23486
23555
|
}, syncIntervalMs);
|
|
23487
23556
|
timer.unref?.();
|
|
23488
23557
|
}
|
|
@@ -23922,4 +23991,4 @@ if (argv2.includes("--server")) {
|
|
|
23922
23991
|
runMain(main, { showUsage: showUsage2 });
|
|
23923
23992
|
}
|
|
23924
23993
|
|
|
23925
|
-
//# debugId=
|
|
23994
|
+
//# debugId=6697905C98729D8364756E2164756E21
|