@fromsko/obsidian-mcp-server 1.1.9 → 1.2.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.
@@ -0,0 +1,211 @@
1
+ import * as fs from "fs/promises";
2
+ import { glob } from "glob";
3
+ import matter from "gray-matter";
4
+ import * as path from "path";
5
+ export function createNoteService(vaultPath) {
6
+ async function parseNote(filePath) {
7
+ try {
8
+ const content = await fs.readFile(filePath, "utf-8");
9
+ const { data } = matter(content);
10
+ const relativePath = path.relative(vaultPath, filePath);
11
+ return {
12
+ path: relativePath,
13
+ name: path.basename(filePath, ".md"),
14
+ category: data.category,
15
+ tags: data.tags,
16
+ summary: data.summary,
17
+ folder: data.folder,
18
+ created: data.created,
19
+ };
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ async function getAllNotes() {
26
+ const files = await glob("**/*.md", {
27
+ cwd: vaultPath,
28
+ ignore: [".obsidian/**", ".smart-env/**", ".windsurf/**"],
29
+ });
30
+ const notes = [];
31
+ for (const file of files) {
32
+ const meta = await parseNote(path.join(vaultPath, file));
33
+ if (meta)
34
+ notes.push(meta);
35
+ }
36
+ return notes;
37
+ }
38
+ async function searchNotes(query, tag, category) {
39
+ const allNotes = await getAllNotes();
40
+ return allNotes.filter((note) => {
41
+ const matchesQuery = !query ||
42
+ note.name.toLowerCase().includes(query.toLowerCase()) ||
43
+ note.summary?.toLowerCase().includes(query.toLowerCase());
44
+ const matchesTag = !tag || note.tags?.includes(tag);
45
+ const matchesCategory = !category || note.category === category;
46
+ return matchesQuery && matchesTag && matchesCategory;
47
+ });
48
+ }
49
+ async function readNote(notePath) {
50
+ const fullPath = path.join(vaultPath, notePath);
51
+ try {
52
+ return await fs.readFile(fullPath, "utf-8");
53
+ }
54
+ catch {
55
+ throw new Error(`笔记不存在: ${notePath}`);
56
+ }
57
+ }
58
+ async function listFolder(folderPath = "") {
59
+ const targetPath = path.join(vaultPath, folderPath);
60
+ const entries = await fs.readdir(targetPath, { withFileTypes: true });
61
+ const folders = [];
62
+ const notes = [];
63
+ for (const entry of entries) {
64
+ if (entry.name.startsWith("."))
65
+ continue;
66
+ if (entry.isDirectory()) {
67
+ folders.push(entry.name);
68
+ }
69
+ else if (entry.name.endsWith(".md")) {
70
+ const meta = await parseNote(path.join(targetPath, entry.name));
71
+ if (meta)
72
+ notes.push(meta);
73
+ }
74
+ }
75
+ return { folders, notes };
76
+ }
77
+ async function getNoteStructure() {
78
+ const rootContent = await listFolder("");
79
+ const structure = {
80
+ _notes: rootContent.notes.map((n) => n.name),
81
+ };
82
+ for (const folder of rootContent.folders) {
83
+ try {
84
+ const subContent = await listFolder(folder);
85
+ structure[folder] = {
86
+ folders: subContent.folders,
87
+ notes: subContent.notes.map((n) => n.name),
88
+ };
89
+ }
90
+ catch {
91
+ structure[folder] = { error: "无法读取" };
92
+ }
93
+ }
94
+ return structure;
95
+ }
96
+ async function createNote(notePath, content) {
97
+ const fullPath = path.join(vaultPath, notePath);
98
+ const dir = path.dirname(fullPath);
99
+ await fs.mkdir(dir, { recursive: true });
100
+ try {
101
+ await fs.access(fullPath);
102
+ throw new Error(`笔记已存在: ${notePath}`);
103
+ }
104
+ catch (err) {
105
+ if (err.code !== "ENOENT")
106
+ throw err;
107
+ }
108
+ await fs.writeFile(fullPath, content, "utf-8");
109
+ return `笔记创建成功: ${notePath}`;
110
+ }
111
+ async function updateNote(notePath, content) {
112
+ const fullPath = path.join(vaultPath, notePath);
113
+ try {
114
+ await fs.access(fullPath);
115
+ }
116
+ catch {
117
+ throw new Error(`笔记不存在: ${notePath}`);
118
+ }
119
+ await fs.writeFile(fullPath, content, "utf-8");
120
+ return `笔记更新成功: ${notePath}`;
121
+ }
122
+ async function deleteNote(notePath) {
123
+ const fullPath = path.join(vaultPath, notePath);
124
+ try {
125
+ await fs.access(fullPath);
126
+ }
127
+ catch {
128
+ throw new Error(`笔记不存在: ${notePath}`);
129
+ }
130
+ await fs.unlink(fullPath);
131
+ return `笔记删除成功: ${notePath}`;
132
+ }
133
+ async function createFolder(folderPath) {
134
+ const fullPath = path.join(vaultPath, folderPath);
135
+ try {
136
+ const stat = await fs.stat(fullPath);
137
+ if (stat.isDirectory()) {
138
+ throw new Error(`文件夹已存在: ${folderPath}`);
139
+ }
140
+ throw new Error(`路径已存在但不是文件夹: ${folderPath}`);
141
+ }
142
+ catch (err) {
143
+ if (err.code !== "ENOENT")
144
+ throw err;
145
+ }
146
+ await fs.mkdir(fullPath, { recursive: true });
147
+ return `文件夹创建成功: ${folderPath}`;
148
+ }
149
+ async function fullTextSearch(keyword) {
150
+ const files = await glob("**/*.md", {
151
+ cwd: vaultPath,
152
+ ignore: [".obsidian/**", ".smart-env/**", ".windsurf/**"],
153
+ });
154
+ const results = [];
155
+ for (const file of files) {
156
+ try {
157
+ const content = await fs.readFile(path.join(vaultPath, file), "utf-8");
158
+ const lines = content.split("\n");
159
+ const matches = [];
160
+ lines.forEach((line, index) => {
161
+ if (line.toLowerCase().includes(keyword.toLowerCase())) {
162
+ matches.push(`L${index + 1}: ${line.trim().substring(0, 100)}`);
163
+ }
164
+ });
165
+ if (matches.length > 0) {
166
+ results.push({ path: file, matches: matches.slice(0, 5) });
167
+ }
168
+ }
169
+ catch {
170
+ // ignore
171
+ }
172
+ }
173
+ return results.slice(0, 20);
174
+ }
175
+ async function moveNote(oldPath, newPath) {
176
+ const fullOldPath = path.join(vaultPath, oldPath);
177
+ const fullNewPath = path.join(vaultPath, newPath);
178
+ try {
179
+ await fs.access(fullOldPath);
180
+ }
181
+ catch {
182
+ throw new Error(`原笔记不存在: ${oldPath}`);
183
+ }
184
+ const dir = path.dirname(fullNewPath);
185
+ await fs.mkdir(dir, { recursive: true });
186
+ await fs.rename(fullOldPath, fullNewPath);
187
+ return `笔记已移动: ${oldPath} -> ${newPath}`;
188
+ }
189
+ async function getTemplate(templateName) {
190
+ const templatePath = path.join(vaultPath, "Obsidian-体系", "Templates", `${templateName}模板.md`);
191
+ try {
192
+ return await fs.readFile(templatePath, "utf-8");
193
+ }
194
+ catch {
195
+ throw new Error(`模板不存在: ${templateName}`);
196
+ }
197
+ }
198
+ return {
199
+ searchNotes,
200
+ readNote,
201
+ listFolder,
202
+ getNoteStructure,
203
+ createNote,
204
+ updateNote,
205
+ deleteNote,
206
+ createFolder,
207
+ fullTextSearch,
208
+ moveNote,
209
+ getTemplate,
210
+ };
211
+ }
@@ -0,0 +1,3 @@
1
+ export declare function suggestFolder(category: string): string;
2
+ export declare function fillTemplate(template: string, data: Record<string, string>): string;
3
+ export declare function getPromptGuide(): Promise<string>;
@@ -0,0 +1,159 @@
1
+ const vaultConventions = `# Obsidian 知识库体系化写作规范
2
+
3
+ ## 1. 核心目标
4
+ 构建一个“高内聚、低耦合、自生长”的知识网络。
5
+ - **高内聚**:单一笔记只讲一个原子概念。
6
+ - **低耦合**:通过双向链接 [[笔记名称]] 建立关系,而非死板的文件夹层级。
7
+ - **自生长**:通过“问题链”驱动深度思考,通过“本质拆解”直击底层规律。
8
+
9
+ ---
10
+
11
+ ## 2. 笔记类型定义 (Type Specification)
12
+ 必须在 Frontmatter 的 \`type\` 字段中明确指定以下类型之一:
13
+
14
+ | 类型 (Type) | 核心用途 | 结构要求 |
15
+ | :--- | :--- | :--- |
16
+ | **概念卡** | 原子化的知识点、术语、工具 | 定义、要点、本质视角、反例、应用、关联链接 |
17
+ | **读书笔记** | 书籍、文章、课程的深度解析 | SQ3R框架、关键观点、原始来源、启发行动 |
18
+ | **问题链** | 驱动思考的连续提问 | 关键问题清单、逻辑推理链 (前提→推理→结论) |
19
+ | **本质拆解** | 对现象的第一性原理分析 | 现象、要素拆解、底层事实、重构方案 |
20
+ | **扩展思路** | 知识迁移、跨学科对比 | 对比视角、迁移路径、新假设 |
21
+ | **灵感启发** | 瞬间的火花、感悟、想法 | 触发点、新观点、立即行动 |
22
+ | **复盘行动** | 周期性总结、项目复盘 | 目标对照、收获、盲区、改进措施、Todo |
23
+
24
+ ---
25
+
26
+ ## 3. 存储与元数据规范 (Folder & Metadata)
27
+
28
+ ### 3.1 自动分类映射 (Folder Mapping)
29
+ 根据 \`category\` 字段,笔记应存放在以下对应路径:
30
+ - \`frontend\`: 知识点/02-前端知识/
31
+ - \`backend\`: 知识点/05-后端知识/
32
+ - \`hardware\`: 知识点/03-硬件学习/
33
+ - \`ai\`: 知识点/04-人工智能/
34
+ - \`docker\`: 知识点/01-Docker/
35
+ - \`devops\`: 知识点/00-闲置笔记/git/
36
+ - \`linux\`: 知识点/00-闲置笔记/Liunx/
37
+ - \`database\`: 知识点/00-闲置笔记/数据库/
38
+ - \`server\`: 知识点/00-闲置笔记/服务器安装环境/
39
+ - \`security\`: 知识点/密钥/
40
+ - \`music\`: 乐理/
41
+ - \`misc\`: 知识点/00-闲置笔记/
42
+
43
+ ### 3.2 标准 YAML Frontmatter
44
+ \`\`\`yaml
45
+ ---
46
+ type: <必填: 笔记类型>
47
+ status: <draft | active | archived>
48
+ created: YYYY-MM-DD
49
+ category: <映射分类>
50
+ tags: [标签1, 标签2]
51
+ summary: <一句话总结内容本质>
52
+ folder: <自动映射的路径>
53
+ source: <来源URL或书名>
54
+ ---
55
+ \`\`\`
56
+
57
+ ---
58
+
59
+ ## 4. 连接策略 (Linking Strategy)
60
+ - **索引连接**:概念卡必须链接 \`[[索引-概念卡]]\`,问题链链接 \`[[索引-问题链]]\`。
61
+ - **逻辑双链**:在文末设置 「🔗 Related Notes」 章节,推测 3-5 个逻辑关联的笔记。
62
+ - **反向链接**:在解释一个概念时,尽量链接其上位概念或基础原理。
63
+
64
+ ---
65
+
66
+ ## 5. 写作风格约束 (Writing Style)
67
+ - **去废话化**:删除所有礼貌用语、过渡段落。
68
+ - **结构化优先**:大量使用列表、任务列表 (\`- [ ]\`)、代码块。
69
+ - **代码规范**:代码块必须标注语言,且包含简短注释说明关键逻辑。
70
+ - **命名规范**:使用“主题-子主题”或“序号-主题”格式,如 \`React-Hooks深度分析.md\`。
71
+ `;
72
+ const guideHeader = `# 🗂️ Obsidian 知识库整理助手 - 终极提示词指南
73
+
74
+ ## 1. 提示词角色定位
75
+ 你不仅是一个整理者,更是一个**知识架构师**。
76
+ 你的任务是接收凌乱的输入(聊天记录、网页摘录、随手记),通过**深度学习框架**将其转化为具有**体系感**的 Obsidian 笔记。
77
+
78
+ ---
79
+
80
+ ## 2. 如何将此规范集成到 LLM
81
+
82
+ ### 场景 A:作为“系统提示词” (推荐)
83
+ 如果你使用 Claude Artifacts, ChatGPT Custom Instructions 或 Windsurf/Cursor:
84
+ 1. 调用 \`get_prompt_guide\` 获取下方全文。
85
+ 2. 将 **“三、提示词全文”** 部分复制到 AI 的 System Prompt 或角色设定中。
86
+
87
+ ### 场景 B:作为 Obsidian 模板使用
88
+ 1. 在 Obsidian 中创建 \`Templates/AI整理助手.md\`。
89
+ 2. 粘贴提示词全文。
90
+ 3. 需要整理内容时,呼出模板并全选复制给 AI。
91
+
92
+ ---
93
+
94
+ ## 3. 提示词全文 (复制到 AI 角色设定)
95
+
96
+ \`\`\`markdown
97
+ # Role: Obsidian 知识库架构师
98
+
99
+ ## Task
100
+ 将用户输入的原始、杂乱内容,转化为符合严格规范的 Obsidian 笔记。
101
+
102
+ ## Rules & Constraints
103
+ ${vaultConventions}
104
+
105
+ ## Workflow
106
+ 1. **分析意图**:判断内容属于哪种笔记类型(概念卡/问题链等)。
107
+ 2. **提取本质**:识别核心关键词和底层原理。
108
+ 3. **结构化构建**:填充对应的模板结构,生成 Frontmatter。
109
+ 4. **建立链接**:自动推测并添加相关联的笔记双链。
110
+ 5. **归档决策**:根据 category 确定 folder 路径。
111
+
112
+ ---
113
+ ## Start Processing
114
+ 请直接输出整理后的 Markdown 源代码,严禁输出任何开场白或解释。
115
+ \`\`\`
116
+
117
+ ---
118
+
119
+ ## 4. MCP 工具联动建议
120
+ 当 AI 生成内容后,建议按以下顺序调用 MCP 工具:
121
+ 1. **\`generate_from_template\`**: 使用对应模板生成文件。
122
+ 2. **\`auto_archive_note\`**: 确保文件存放在规范的路径。
123
+ 3. **\`search_notes\`**: 搜索 AI 建议的 \`Related Notes\`,验证链接是否已存在,若不存在则打上待创建标记。
124
+ `;
125
+ const CATEGORY_MAP = {
126
+ frontend: "知识点/02-前端知识/",
127
+ backend: "知识点/05-后端知识/",
128
+ hardware: "知识点/03-硬件学习/",
129
+ ai: "知识点/04-人工智能/",
130
+ docker: "知识点/01-Docker/",
131
+ devops: "知识点/00-闲置笔记/git/",
132
+ linux: "知识点/00-闲置笔记/Liunx/",
133
+ database: "知识点/00-闲置笔记/数据库/",
134
+ server: "知识点/00-闲置笔记/服务器安装环境/",
135
+ security: "知识点/密钥/",
136
+ music: "乐理/",
137
+ misc: "知识点/00-闲置笔记/",
138
+ };
139
+ export function suggestFolder(category) {
140
+ return CATEGORY_MAP[category.toLowerCase()] || CATEGORY_MAP["misc"];
141
+ }
142
+ export function fillTemplate(template, data) {
143
+ let result = template;
144
+ for (const [key, value] of Object.entries(data)) {
145
+ // 替换 {{key}}
146
+ result = result.replace(new RegExp(`{{${key}}}`, "g"), value);
147
+ // 同时也支持替换 <key>
148
+ result = result.replace(new RegExp(`<${key}>`, "g"), value);
149
+ }
150
+ // 自动填充日期
151
+ if (!data.date) {
152
+ const today = new Date().toISOString().split("T")[0];
153
+ result = result.replace(/{{date}}/g, today);
154
+ }
155
+ return result;
156
+ }
157
+ export async function getPromptGuide() {
158
+ return `${guideHeader}`;
159
+ }
@@ -0,0 +1,210 @@
1
+ export declare function buildTools(vaultPath: string): {
2
+ tools: ({
3
+ name: string;
4
+ description: string;
5
+ inputSchema: {
6
+ type: string;
7
+ properties: {
8
+ query: {
9
+ type: string;
10
+ description: string;
11
+ };
12
+ tag: {
13
+ type: string;
14
+ description: string;
15
+ };
16
+ category: {
17
+ type: string;
18
+ description: string;
19
+ };
20
+ path?: undefined;
21
+ folder?: undefined;
22
+ keyword?: undefined;
23
+ content?: undefined;
24
+ templateName?: undefined;
25
+ noteName?: undefined;
26
+ data?: undefined;
27
+ };
28
+ required?: undefined;
29
+ };
30
+ } | {
31
+ name: string;
32
+ description: string;
33
+ inputSchema: {
34
+ type: string;
35
+ properties: {
36
+ path: {
37
+ type: string;
38
+ description: string;
39
+ };
40
+ query?: undefined;
41
+ tag?: undefined;
42
+ category?: undefined;
43
+ folder?: undefined;
44
+ keyword?: undefined;
45
+ content?: undefined;
46
+ templateName?: undefined;
47
+ noteName?: undefined;
48
+ data?: undefined;
49
+ };
50
+ required: string[];
51
+ };
52
+ } | {
53
+ name: string;
54
+ description: string;
55
+ inputSchema: {
56
+ type: string;
57
+ properties: {
58
+ folder: {
59
+ type: string;
60
+ description: string;
61
+ };
62
+ query?: undefined;
63
+ tag?: undefined;
64
+ category?: undefined;
65
+ path?: undefined;
66
+ keyword?: undefined;
67
+ content?: undefined;
68
+ templateName?: undefined;
69
+ noteName?: undefined;
70
+ data?: undefined;
71
+ };
72
+ required?: undefined;
73
+ };
74
+ } | {
75
+ name: string;
76
+ description: string;
77
+ inputSchema: {
78
+ type: string;
79
+ properties: {
80
+ query?: undefined;
81
+ tag?: undefined;
82
+ category?: undefined;
83
+ path?: undefined;
84
+ folder?: undefined;
85
+ keyword?: undefined;
86
+ content?: undefined;
87
+ templateName?: undefined;
88
+ noteName?: undefined;
89
+ data?: undefined;
90
+ };
91
+ required?: undefined;
92
+ };
93
+ } | {
94
+ name: string;
95
+ description: string;
96
+ inputSchema: {
97
+ type: string;
98
+ properties: {
99
+ keyword: {
100
+ type: string;
101
+ description: string;
102
+ };
103
+ query?: undefined;
104
+ tag?: undefined;
105
+ category?: undefined;
106
+ path?: undefined;
107
+ folder?: undefined;
108
+ content?: undefined;
109
+ templateName?: undefined;
110
+ noteName?: undefined;
111
+ data?: undefined;
112
+ };
113
+ required: string[];
114
+ };
115
+ } | {
116
+ name: string;
117
+ description: string;
118
+ inputSchema: {
119
+ type: string;
120
+ properties: {
121
+ path: {
122
+ type: string;
123
+ description: string;
124
+ };
125
+ content: {
126
+ type: string;
127
+ description: string;
128
+ };
129
+ query?: undefined;
130
+ tag?: undefined;
131
+ category?: undefined;
132
+ folder?: undefined;
133
+ keyword?: undefined;
134
+ templateName?: undefined;
135
+ noteName?: undefined;
136
+ data?: undefined;
137
+ };
138
+ required: string[];
139
+ };
140
+ } | {
141
+ name: string;
142
+ description: string;
143
+ inputSchema: {
144
+ type: string;
145
+ properties: {
146
+ path: {
147
+ type: string;
148
+ description: string;
149
+ };
150
+ category: {
151
+ type: string;
152
+ description: string;
153
+ };
154
+ query?: undefined;
155
+ tag?: undefined;
156
+ folder?: undefined;
157
+ keyword?: undefined;
158
+ content?: undefined;
159
+ templateName?: undefined;
160
+ noteName?: undefined;
161
+ data?: undefined;
162
+ };
163
+ required: string[];
164
+ };
165
+ } | {
166
+ name: string;
167
+ description: string;
168
+ inputSchema: {
169
+ type: string;
170
+ properties: {
171
+ templateName: {
172
+ type: string;
173
+ description: string;
174
+ };
175
+ noteName: {
176
+ type: string;
177
+ description: string;
178
+ };
179
+ data: {
180
+ type: string;
181
+ description: string;
182
+ additionalProperties: {
183
+ type: string;
184
+ };
185
+ };
186
+ category: {
187
+ type: string;
188
+ description: string;
189
+ };
190
+ folder: {
191
+ type: string;
192
+ description: string;
193
+ };
194
+ query?: undefined;
195
+ tag?: undefined;
196
+ path?: undefined;
197
+ keyword?: undefined;
198
+ content?: undefined;
199
+ };
200
+ required: string[];
201
+ };
202
+ })[];
203
+ handleTool: (name: string, args?: Record<string, unknown>) => Promise<string | import("../services/notes.js").NoteMeta[] | {
204
+ folders: string[];
205
+ notes: import("../services/notes.js").NoteMeta[];
206
+ } | Record<string, any> | {
207
+ path: string;
208
+ matches: string[];
209
+ }[]>;
210
+ };