obsidian-vault-bridge 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.md +86 -2
- package/bin/bridge.mjs +150 -0
- package/package.json +39 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Strange Leo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
本包依赖并打包分发 obsidian-mcp:
|
|
26
|
+
https://github.com/StevenStavrakis/obsidian-mcp
|
|
27
|
+
MIT License, Copyright (c) 2024 Steven Stavrakis
|
|
28
|
+
上游的许可证条款同样适用于其自身代码。
|
package/README.md
CHANGED
|
@@ -1,3 +1,87 @@
|
|
|
1
|
-
#
|
|
1
|
+
# obsidian-vault-bridge
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
让 AI 助手读写你的 Obsidian 笔记库 —— **不用手填路径**,装好就能用。
|
|
4
|
+
|
|
5
|
+
它是 [obsidian-mcp](https://github.com/StevenStavrakis/obsidian-mcp) 的启动适配层。
|
|
6
|
+
真正的笔记读写能力全部来自上游;本包只解决一个问题:**库在哪**。
|
|
7
|
+
|
|
8
|
+
## 为什么需要它
|
|
9
|
+
|
|
10
|
+
`obsidian-mcp` 只接受命令行参数指定库:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
obsidian-mcp serve --vault notes=/absolute/path/to/vault
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
它不读环境变量。而 MCP 连接器的表单只能把用户填的值注入 `env`,塞不进命令行参数。
|
|
17
|
+
于是每个人库路径不同这件事,就卡死了开箱即用。
|
|
18
|
+
|
|
19
|
+
本包负责:自动找到你机器上的 Obsidian 库 → 生成合法参数 → 启动上游,stdio 原样透传。
|
|
20
|
+
|
|
21
|
+
## 库是怎么找的
|
|
22
|
+
|
|
23
|
+
1. 环境变量 `OBSIDIAN_VAULT_PATH` 优先(多个库用 `;` 分隔);
|
|
24
|
+
2. 没设就读 Obsidian 自己的库注册表:
|
|
25
|
+
- Windows:`%APPDATA%\obsidian\obsidian.json`
|
|
26
|
+
- macOS:`~/Library/Application Support/obsidian/obsidian.json`
|
|
27
|
+
- Linux:`~/.config/obsidian/obsidian.json`
|
|
28
|
+
|
|
29
|
+
只要你在 Obsidian 里打开过库,它就被登记在注册表里,本包能直接找到,一个字都不用填。
|
|
30
|
+
|
|
31
|
+
## 用法
|
|
32
|
+
|
|
33
|
+
作为 MCP server(由客户端拉起,不用手动跑):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx -y obsidian-vault-bridge
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
配置示例:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"mcpServers": {
|
|
44
|
+
"obsidian": {
|
|
45
|
+
"type": "stdio",
|
|
46
|
+
"command": "npx",
|
|
47
|
+
"args": ["-y", "obsidian-vault-bridge"],
|
|
48
|
+
"runtime": { "type": "node", "version": "22" }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
显式指定库(可选):
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
"env": {
|
|
58
|
+
"OBSIDIAN_VAULT_PATH": "D:\\Obsidian-Vault",
|
|
59
|
+
"OBSIDIAN_VAULT_IDS": "notes"
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
看诊断信息(会打印找到了哪些库,不启动服务):
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx -y obsidian-vault-bridge --doctor
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 环境变量
|
|
70
|
+
|
|
71
|
+
| 变量 | 说明 |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `OBSIDIAN_VAULT_PATH` | 库绝对路径,多个用 `;` 分隔。不设则自动探测 |
|
|
74
|
+
| `OBSIDIAN_VAULT_IDS` | 自定义库 id,逗号分隔,与上面一一对应。不设则按目录名自动生成 |
|
|
75
|
+
|
|
76
|
+
## 能力与边界
|
|
77
|
+
|
|
78
|
+
上游提供 12 个工具:列库、读笔记、新建、编辑、删除、移动、建目录、搜索、加标签、删标签、改标签、统一管理标签。
|
|
79
|
+
|
|
80
|
+
- 直接读写 Markdown 文件,**不需要 Obsidian 保持运行**
|
|
81
|
+
- 写操作带事务日志,删除默认进 `.obsidian-mcp/trash` 可恢复
|
|
82
|
+
- 编辑可用 etag 做并发冲突检测
|
|
83
|
+
|
|
84
|
+
## 许可
|
|
85
|
+
|
|
86
|
+
本包:MIT,Copyright (c) 2026 Strange Leo。
|
|
87
|
+
上游 `obsidian-mcp`:MIT,Copyright (c) 2024 Steven Stavrakis —— 见 LICENSE 末段。
|
package/bin/bridge.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// obsidian-vault-bridge —— 自动定位本机 Obsidian 库,再交给 obsidian-mcp 提供服务。
|
|
3
|
+
//
|
|
4
|
+
// 为什么需要这一层:obsidian-mcp 只接受 `--vault <id>=<绝对路径>` 命令行参数、不读环境变量,
|
|
5
|
+
// 而 MCP 连接器的「用户自填」只能把值注入 env。所以本包负责定位库、拼出参数,
|
|
6
|
+
// 然后把 stdio 原样透传给真正的 MCP 服务端。
|
|
7
|
+
//
|
|
8
|
+
// 上游实现与全部笔记读写能力来自 obsidian-mcp
|
|
9
|
+
// https://github.com/StevenStavrakis/obsidian-mcp (MIT, Copyright (c) 2024 Steven Stavrakis)
|
|
10
|
+
// 本包只是启动参数的适配层。
|
|
11
|
+
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
|
|
20
|
+
const log = (msg) => process.stderr.write(msg + '\n');
|
|
21
|
+
|
|
22
|
+
// ---------- 定位 Obsidian 的库注册表 ----------
|
|
23
|
+
function registryPath() {
|
|
24
|
+
const home = os.homedir();
|
|
25
|
+
if (process.platform === 'win32') {
|
|
26
|
+
const appdata = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
27
|
+
return path.join(appdata, 'obsidian', 'obsidian.json');
|
|
28
|
+
}
|
|
29
|
+
if (process.platform === 'darwin') {
|
|
30
|
+
return path.join(home, 'Library', 'Application Support', 'obsidian', 'obsidian.json');
|
|
31
|
+
}
|
|
32
|
+
const xdg = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
33
|
+
return path.join(xdg, 'obsidian', 'obsidian.json');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 读注册表里登记过的库。Obsidian 打开过的库都会留在这里。
|
|
37
|
+
function detectVaults() {
|
|
38
|
+
const rp = registryPath();
|
|
39
|
+
if (!fs.existsSync(rp)) return { file: rp, paths: [] };
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(fs.readFileSync(rp, 'utf8').replace(/^\uFEFF/, ''));
|
|
43
|
+
} catch {
|
|
44
|
+
return { file: rp, paths: [] };
|
|
45
|
+
}
|
|
46
|
+
const paths = [];
|
|
47
|
+
for (const v of Object.values(parsed?.vaults || {})) {
|
|
48
|
+
if (v && typeof v.path === 'string') paths.push(v.path);
|
|
49
|
+
}
|
|
50
|
+
return { file: rp, paths: [...new Set(paths)] };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------- 生成合法 vault id ----------
|
|
54
|
+
// obsidian-mcp 要求:小写字母开头,只含小写字母、数字、下划线、连字符。
|
|
55
|
+
function makeId(vaultPath, index, used) {
|
|
56
|
+
const base = path.basename(vaultPath.replace(/[\\/]+$/, '')) || 'vault';
|
|
57
|
+
let id = base.toLowerCase().replace(/[^a-z0-9_-]/g, '-').replace(/^-+|-+$/g, '');
|
|
58
|
+
if (!/^[a-z]/.test(id)) id = 'v' + id;
|
|
59
|
+
if (!id) id = 'vault';
|
|
60
|
+
let candidate = id;
|
|
61
|
+
let n = 2;
|
|
62
|
+
while (used.has(candidate)) candidate = id + '-' + n++;
|
|
63
|
+
used.add(candidate);
|
|
64
|
+
return candidate;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------- 决定最终用哪些库 ----------
|
|
68
|
+
function resolveVaults() {
|
|
69
|
+
const explicit = (process.env.OBSIDIAN_VAULT_PATH || '').trim();
|
|
70
|
+
const customIds = (process.env.OBSIDIAN_VAULT_IDS || '')
|
|
71
|
+
.split(',')
|
|
72
|
+
.map((s) => s.trim())
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
|
|
75
|
+
let dirs;
|
|
76
|
+
if (explicit) {
|
|
77
|
+
dirs = explicit.split(';').map((s) => s.trim()).filter(Boolean);
|
|
78
|
+
} else {
|
|
79
|
+
dirs = detectVaults().paths;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const used = new Set();
|
|
83
|
+
return dirs.map((p, i) => {
|
|
84
|
+
const abs = path.resolve(p);
|
|
85
|
+
const id = customIds[i] || makeId(abs, i, used);
|
|
86
|
+
return { id, path: abs, exists: fs.existsSync(abs), hasObsidianDir: fs.existsSync(path.join(abs, '.obsidian')) };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------- 入口 ----------
|
|
91
|
+
const argv = process.argv.slice(2);
|
|
92
|
+
const doctor = argv.includes('--doctor');
|
|
93
|
+
const passthrough = argv.filter((a) => a !== '--doctor');
|
|
94
|
+
|
|
95
|
+
const vaults = resolveVaults();
|
|
96
|
+
|
|
97
|
+
if (doctor) {
|
|
98
|
+
const reg = detectVaults();
|
|
99
|
+
log('obsidian-vault-bridge 诊断');
|
|
100
|
+
log(' 平台: ' + process.platform);
|
|
101
|
+
log(' 注册表: ' + reg.file + (fs.existsSync(reg.file) ? '(存在)' : '(不存在)'));
|
|
102
|
+
log(' 注册表内的库: ' + (reg.paths.length ? reg.paths.join(' , ') : '无'));
|
|
103
|
+
log(' 最终使用的库:');
|
|
104
|
+
for (const v of vaults) {
|
|
105
|
+
log(' ' + v.id + ' = ' + v.path + (v.exists ? '' : ' [不存在]') + (v.hasObsidianDir ? '' : ' [缺少 .obsidian 目录]'));
|
|
106
|
+
}
|
|
107
|
+
if (!vaults.length) log(' -> 没找到任何库,请设置 OBSIDIAN_VAULT_PATH');
|
|
108
|
+
process.exit(vaults.length ? 0 : 1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!vaults.length) {
|
|
112
|
+
log('obsidian-vault-bridge: 没找到 Obsidian 库。');
|
|
113
|
+
log(' 请至少打开一次 Obsidian 让它登记库,或设置环境变量 OBSIDIAN_VAULT_PATH 指向库目录。');
|
|
114
|
+
log(' 多个库用英文分号分隔,例如 OBSIDIAN_VAULT_PATH=D:\\MyVault;D:\\WorkVault');
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const v of vaults) {
|
|
119
|
+
if (!v.exists) {
|
|
120
|
+
log('obsidian-vault-bridge: 库路径不存在 -> ' + v.path);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
if (!v.hasObsidianDir) {
|
|
124
|
+
log('obsidian-vault-bridge: 该目录不是 Obsidian 库(缺少 .obsidian)-> ' + v.path);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let mainJs;
|
|
130
|
+
try {
|
|
131
|
+
mainJs = require.resolve('obsidian-mcp');
|
|
132
|
+
} catch {
|
|
133
|
+
log('obsidian-vault-bridge: 未能定位 obsidian-mcp,请重新安装本包。');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const args = [mainJs, 'serve', ...passthrough];
|
|
138
|
+
for (const v of vaults) args.push('--vault', v.id + '=' + v.path);
|
|
139
|
+
|
|
140
|
+
log('obsidian-vault-bridge: 启动 ' + vaults.map((v) => v.id + '=' + v.path).join(' , '));
|
|
141
|
+
|
|
142
|
+
const child = spawn(process.execPath, args, { stdio: 'inherit' });
|
|
143
|
+
child.on('error', (err) => {
|
|
144
|
+
log('obsidian-vault-bridge: 启动 obsidian-mcp 失败 -> ' + err.message);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
});
|
|
147
|
+
child.on('exit', (code, signal) => {
|
|
148
|
+
if (signal) process.kill(process.pid, signal);
|
|
149
|
+
else process.exit(code ?? 0);
|
|
150
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obsidian-vault-bridge",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP bridge that auto-detects your Obsidian vaults and serves them to AI assistants",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"obsidian-vault-bridge": "./bin/bridge.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"doctor": "node bin/bridge.mjs --doctor"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"obsidian-mcp": "^2.0.1"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"obsidian",
|
|
25
|
+
"mcp",
|
|
26
|
+
"model-context-protocol",
|
|
27
|
+
"vault",
|
|
28
|
+
"notes",
|
|
29
|
+
"pkm"
|
|
30
|
+
],
|
|
31
|
+
"author": "Strange Leo <strangeyu911@gmail.com>",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/strangeyu911-tech/obsidian-vault-bridge.git"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/strangeyu911-tech/obsidian-vault-bridge/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/strangeyu911-tech/obsidian-vault-bridge#readme"
|
|
41
|
+
}
|