agent-wiki 0.1.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,16 @@
1
+ /**
2
+ * wiki_init — 首次初始化知识库
3
+ *
4
+ * 创建目录结构(raw/、wiki/、outputs/)
5
+ * 生成 SCHEMA.md(知识库说明书 + Agent 操作指南 + 写作规则)
6
+ * 生成 wiki/INDEX.md(空索引)
7
+ * 生成 wiki/LOG.md(空日志)
8
+ * 保存全局配置到 ~/.agent-wiki/config.json
9
+ * 初始化 state.json
10
+ */
11
+ import { ToolResult } from "../types.js";
12
+ export declare function handleInit(params: {
13
+ dir: string;
14
+ topics: string[];
15
+ interests: string[];
16
+ }): Promise<ToolResult>;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * wiki_init — 首次初始化知识库
3
+ *
4
+ * 创建目录结构(raw/、wiki/、outputs/)
5
+ * 生成 SCHEMA.md(知识库说明书 + Agent 操作指南 + 写作规则)
6
+ * 生成 wiki/INDEX.md(空索引)
7
+ * 生成 wiki/LOG.md(空日志)
8
+ * 保存全局配置到 ~/.agent-wiki/config.json
9
+ * 初始化 state.json
10
+ */
11
+ import * as path from "path";
12
+ import * as fs from "fs";
13
+ import { setConfig, getRawDir, getWikiPagesDir, getOutputsDir } from "../state.js";
14
+ import { ensureDir, todayStr, writeJSON } from "../utils.js";
15
+ import { textResult } from "../types.js";
16
+ /** SCHEMA.md 模板 */
17
+ function generateSchemaMd(topics, interests) {
18
+ return `# 知识库 Schema
19
+
20
+ ## 这是什么
21
+ 一个关于 ${topics.join("、")} 的个人知识库,基于 Karpathy LLM Wiki 方案。
22
+
23
+ ## 目录规则
24
+ - raw/ — 未处理的源材料,永远不要修改这些文件
25
+ - wiki/ — AI 维护的维基,用户只读
26
+ - outputs/ — 生成的原创文章,适合直接发微信公众号
27
+
28
+ ## 维基规则
29
+ - 每个主题在 wiki/ 中有自己的 .md 文件
30
+ - 每个维基文件以一段摘要开头
31
+ - 使用 [[topic-name]] 格式链接相关主题
32
+ - 维护 wiki/INDEX.md,列出每个主题及一行描述
33
+ - 每次操作追加到 wiki/LOG.md
34
+ - 当添加新的源材料时,更新相关的维基文章
35
+
36
+ ## Agent 操作指南
37
+ 当用户说以下内容时,你应该:
38
+ - "把这篇文章加入知识库" / "加个文章" → wiki_ingest → 用 LLM 提取标签 → wiki_tag → 用 LLM 生成摘要/洞察/分类 → wiki_compile
39
+ - "整理知识库" / "全部处理一下" → wiki_ingest(全部新文件)→ 逐篇 LLM 打标签 → wiki_tag → 逐篇 LLM 生成摘要 → wiki_compile
40
+ - "搜一下 XX" / "有没有关于 XX 的" → wiki_search
41
+ - "写一篇关于 XX 的文章" → wiki_search 找参考资料 → 按"写原创文章规则"用 LLM 写文 → wiki_article 保存
42
+ - "检查知识库" / "知识库健康吗" → wiki_lint
43
+ - "知识库状态" → wiki_status
44
+
45
+ ## 写原创文章规则
46
+
47
+ 文章风格:微信公众号适合的、有人味的中文写作。
48
+
49
+ ### 人味标准
50
+ - 像跟朋友聊天,不像写论文
51
+ - 有自己的观点和态度,不当理中客
52
+ - 短句为主,该断就断
53
+ - 敢用"我",敢说"我觉得"
54
+ - 该短就短,不是每篇都要 3000 字
55
+
56
+ ### 微信公众号适配
57
+ - 标题直接说事,不超过 20 字
58
+ - 开头 3 句话内抓住读者
59
+ - 每 3-4 段一个小标题
60
+ - 关键句加粗,方便快速扫读
61
+ - 结尾不要升华,可以留个问题或一个观点
62
+ - 正文纯 Markdown,用户复制到公众号编辑器即可
63
+
64
+ ### 禁止的 AI 味
65
+ - "值得注意的是""总而言之""让我们..."
66
+ - 破折号滥用
67
+ - "深入探讨""全面解析""深度剖析"
68
+ - "在当今...的时代""随着...的发展"开头
69
+ - "首先...其次...最后..."排比结构
70
+ - 夸张比喻("像灯塔一样""如同一把钥匙")
71
+ - 结尾升华("让我们携手共创美好未来")
72
+ - "一方面...另一方面..."两头讨好
73
+
74
+ ## 我的兴趣方向
75
+ ${interests}
76
+ `;
77
+ }
78
+ export async function handleInit(params) {
79
+ const { dir, topics, interests } = params;
80
+ // 展开用户目录(~ → 绝对路径)
81
+ const wikiDir = dir.replace(/^~/, process.env.HOME || "~");
82
+ const interestsStr = interests.length > 0 ? interests.join("、") : topics.join("、");
83
+ // 创建目录结构
84
+ const rawDir = getRawDir(wikiDir);
85
+ const wikiPagesDir = getWikiPagesDir(wikiDir);
86
+ const outputsDir = getOutputsDir(wikiDir);
87
+ ensureDir(rawDir);
88
+ ensureDir(wikiPagesDir);
89
+ ensureDir(outputsDir);
90
+ // 生成 SCHEMA.md
91
+ const schemaPath = path.join(wikiDir, "SCHEMA.md");
92
+ if (!fs.existsSync(schemaPath)) {
93
+ fs.writeFileSync(schemaPath, generateSchemaMd(topics, interestsStr), "utf-8");
94
+ }
95
+ // 生成 wiki/INDEX.md(空索引)
96
+ const indexPath = path.join(wikiPagesDir, "INDEX.md");
97
+ if (!fs.existsSync(indexPath)) {
98
+ fs.writeFileSync(indexPath, `---\ntitle: 知识库索引\ndate: ${todayStr()}\n---\n\n# 知识库索引\n\n---\n*共 0 篇 · 更新于 ${todayStr()}*\n`, "utf-8");
99
+ }
100
+ // 生成 wiki/LOG.md(空日志)
101
+ const logPath = path.join(wikiPagesDir, "LOG.md");
102
+ if (!fs.existsSync(logPath)) {
103
+ fs.writeFileSync(logPath, `---\ntitle: Wiki 操作日志\ntags: [log, wiki]\n---\n\n# Wiki Log\n\n> Append-only 时间线,记录 wiki 的每次变更。\n`, "utf-8");
104
+ }
105
+ // 初始化 state.json
106
+ const statePath = path.join(wikiDir, "state.json");
107
+ if (!fs.existsSync(statePath)) {
108
+ writeJSON(statePath, { ingested: [], tagged: [], compiled: [] });
109
+ }
110
+ // 保存全局配置
111
+ setConfig({ wikiDir });
112
+ return textResult(JSON.stringify({
113
+ status: "ok",
114
+ dir: wikiDir,
115
+ created: [
116
+ "raw/",
117
+ "wiki/",
118
+ "outputs/",
119
+ "SCHEMA.md",
120
+ "wiki/INDEX.md",
121
+ "wiki/LOG.md",
122
+ "state.json",
123
+ ],
124
+ topics,
125
+ interests: interestsStr,
126
+ }));
127
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * wiki_lint — 自检知识库健康状态
3
+ *
4
+ * 检测:
5
+ * 1. 死链接([[xxx]] 引用不存在的页面)
6
+ * 2. 缺失反向链接(被引用但未引用回的页面)
7
+ * 3. 缺摘要(## 摘要 后无内容)
8
+ * 4. 内容过短(文件 < 100 字符)
9
+ * 5. 缺 frontmatter tags
10
+ *
11
+ * 如果 check_only=false,自动修复能修的问题
12
+ */
13
+ import { ToolResult } from "../types.js";
14
+ export declare function handleLint(params: {
15
+ check_only?: boolean;
16
+ }): Promise<ToolResult>;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * wiki_lint — 自检知识库健康状态
3
+ *
4
+ * 检测:
5
+ * 1. 死链接([[xxx]] 引用不存在的页面)
6
+ * 2. 缺失反向链接(被引用但未引用回的页面)
7
+ * 3. 缺摘要(## 摘要 后无内容)
8
+ * 4. 内容过短(文件 < 100 字符)
9
+ * 5. 缺 frontmatter tags
10
+ *
11
+ * 如果 check_only=false,自动修复能修的问题
12
+ */
13
+ import * as path from "path";
14
+ import * as fs from "fs";
15
+ import { getWikiDir, getWikiPagesDir, getRawDir } from "../state.js";
16
+ import { parseFrontmatter } from "../utils.js";
17
+ import { textResult } from "../types.js";
18
+ export async function handleLint(params) {
19
+ const wikiDir = getWikiDir();
20
+ if (!wikiDir) {
21
+ return textResult(JSON.stringify({ error: "知识库未初始化" }));
22
+ }
23
+ const wikiPagesDir = getWikiPagesDir(wikiDir);
24
+ const rawDir = getRawDir(wikiDir);
25
+ const checkOnly = params.check_only ?? true;
26
+ const issues = [];
27
+ let autoFixed = 0;
28
+ if (!fs.existsSync(wikiPagesDir)) {
29
+ return textResult(JSON.stringify({ total_pages: 0, issues: [], auto_fixed: 0 }));
30
+ }
31
+ const excludeFiles = new Set(["INDEX.md", "LOG.md"]);
32
+ const wikiFiles = fs
33
+ .readdirSync(wikiPagesDir)
34
+ .filter((f) => f.endsWith(".md") && !excludeFiles.has(f));
35
+ // 收集所有 Wiki 页面标题(用于检测死链接)
36
+ const existingPages = new Set(wikiFiles.map((f) => f.replace(".md", "")));
37
+ // 收集所有 raw 文件(用于检测源文件引用)
38
+ const rawFiles = new Set(fs.existsSync(rawDir) ? fs.readdirSync(rawDir) : []);
39
+ for (const fileName of wikiFiles) {
40
+ const filePath = path.join(wikiPagesDir, fileName);
41
+ const content = fs.readFileSync(filePath, "utf-8");
42
+ const { meta, body } = parseFrontmatter(content);
43
+ // 1. 检测内容过短
44
+ if (content.length < 100) {
45
+ issues.push({
46
+ type: "too_short",
47
+ severity: "warning",
48
+ source: fileName,
49
+ detail: `文件仅 ${content.length} 字符,内容过短`,
50
+ });
51
+ }
52
+ // 2. 检测缺 frontmatter tags
53
+ if (!meta.tags || !Array.isArray(meta.tags) || meta.tags.length === 0) {
54
+ issues.push({
55
+ type: "missing_tags",
56
+ severity: "warning",
57
+ source: fileName,
58
+ detail: "缺少标签(frontmatter tags 字段为空或不存在)",
59
+ });
60
+ }
61
+ // 3. 检测缺摘要
62
+ const summaryMatch = body.match(/## 摘要\s*\n([\s\S]*?)(?=\n##|$)/);
63
+ if (!summaryMatch || summaryMatch[1].trim().length < 10) {
64
+ issues.push({
65
+ type: "missing_summary",
66
+ severity: "info",
67
+ source: fileName,
68
+ detail: "缺少摘要或摘要内容过短",
69
+ });
70
+ }
71
+ // 4. 检测死链接([[xxx]] 引用)
72
+ const wikilinks = body.matchAll(/\[\[(.+?)\]\]/g);
73
+ for (const link of wikilinks) {
74
+ const linkTarget = link[1];
75
+ // 检查是否是 raw/ 下的源文件引用
76
+ if (linkTarget.startsWith("raw/")) {
77
+ const rawFileName = linkTarget.replace("raw/", "");
78
+ if (!rawFiles.has(rawFileName)) {
79
+ issues.push({
80
+ type: "dead_link",
81
+ severity: "error",
82
+ source: fileName,
83
+ detail: `死链接: [[${linkTarget}]],源文件不存在`,
84
+ });
85
+ }
86
+ }
87
+ else {
88
+ // Wiki 页面间引用
89
+ if (!existingPages.has(linkTarget)) {
90
+ issues.push({
91
+ type: "dead_link",
92
+ severity: "error",
93
+ source: fileName,
94
+ detail: `死链接: [[${linkTarget}]],目标页面不存在`,
95
+ });
96
+ }
97
+ }
98
+ }
99
+ // 5. 检测源文件引用
100
+ const sources = meta.sources;
101
+ if (Array.isArray(sources)) {
102
+ for (const src of sources) {
103
+ const srcStr = String(src).replace(/\[\[|\]\]/g, "");
104
+ if (srcStr.startsWith("raw/")) {
105
+ const rawFileName = srcStr.replace("raw/", "");
106
+ if (!rawFiles.has(rawFileName)) {
107
+ issues.push({
108
+ type: "broken_source",
109
+ severity: "error",
110
+ source: fileName,
111
+ detail: `源文件引用不存在: ${srcStr}`,
112
+ });
113
+ }
114
+ }
115
+ }
116
+ }
117
+ }
118
+ return textResult(JSON.stringify({
119
+ total_pages: wikiFiles.length,
120
+ issues,
121
+ auto_fixed: autoFixed,
122
+ summary: {
123
+ errors: issues.filter((i) => i.severity === "error").length,
124
+ warnings: issues.filter((i) => i.severity === "warning").length,
125
+ info: issues.filter((i) => i.severity === "info").length,
126
+ },
127
+ }));
128
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * wiki_search — 标签匹配 + 全文关键词搜索
3
+ *
4
+ * 遍历 wiki/*.md,解析 frontmatter tags
5
+ * 标签匹配权重 0.6 + 全文匹配权重 0.4
6
+ * 支持匹配模式 any(任一)/ all(全部)
7
+ */
8
+ import { ToolResult } from "../types.js";
9
+ export declare function handleSearch(params: {
10
+ tags?: string[];
11
+ query?: string;
12
+ match?: "any" | "all";
13
+ top_k?: number;
14
+ }): Promise<ToolResult>;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * wiki_search — 标签匹配 + 全文关键词搜索
3
+ *
4
+ * 遍历 wiki/*.md,解析 frontmatter tags
5
+ * 标签匹配权重 0.6 + 全文匹配权重 0.4
6
+ * 支持匹配模式 any(任一)/ all(全部)
7
+ */
8
+ import * as path from "path";
9
+ import * as fs from "fs";
10
+ import { getWikiDir, getWikiPagesDir } from "../state.js";
11
+ import { parseFrontmatter } from "../utils.js";
12
+ import { textResult } from "../types.js";
13
+ export async function handleSearch(params) {
14
+ const wikiDir = getWikiDir();
15
+ if (!wikiDir) {
16
+ return textResult(JSON.stringify({ error: "知识库未初始化" }));
17
+ }
18
+ const wikiPagesDir = getWikiPagesDir(wikiDir);
19
+ if (!fs.existsSync(wikiPagesDir)) {
20
+ return textResult(JSON.stringify({ total: 0, results: [] }));
21
+ }
22
+ const { tags = [], query = "", match = "any", top_k = 10 } = params;
23
+ const searchTags = tags.map((t) => t.toLowerCase());
24
+ const searchQuery = query.toLowerCase();
25
+ // 排除索引和日志文件
26
+ const excludeFiles = new Set(["INDEX.md", "LOG.md"]);
27
+ const mdFiles = fs
28
+ .readdirSync(wikiPagesDir)
29
+ .filter((f) => f.endsWith(".md") && !excludeFiles.has(f));
30
+ const results = [];
31
+ for (const fileName of mdFiles) {
32
+ const filePath = path.join(wikiPagesDir, fileName);
33
+ const content = fs.readFileSync(filePath, "utf-8");
34
+ const { meta, body } = parseFrontmatter(content);
35
+ const fileTags = Array.isArray(meta.tags)
36
+ ? meta.tags.map((t) => String(t).toLowerCase())
37
+ : [];
38
+ // 提取摘要(## 摘要 后面的内容)
39
+ let summary = "";
40
+ const summaryMatch = body.match(/## 摘要\s*\n([\s\S]*?)(?=\n##|$)/);
41
+ if (summaryMatch) {
42
+ summary = summaryMatch[1].trim().substring(0, 200);
43
+ }
44
+ const title = String(meta.title || fileName.replace(".md", ""));
45
+ // 计算得分
46
+ let tagScore = 0;
47
+ let textScore = 0;
48
+ if (searchTags.length > 0) {
49
+ if (match === "any") {
50
+ // 任一标签匹配
51
+ const matchedCount = searchTags.filter((st) => fileTags.some((ft) => ft.includes(st))).length;
52
+ tagScore = matchedCount / searchTags.length;
53
+ }
54
+ else {
55
+ // 全部标签匹配
56
+ const allMatch = searchTags.every((st) => fileTags.some((ft) => ft.includes(st)));
57
+ tagScore = allMatch ? 1 : 0;
58
+ }
59
+ }
60
+ if (searchQuery) {
61
+ const bodyLower = body.toLowerCase();
62
+ // 统计关键词出现次数(最多计 5 次避免长文偏差)
63
+ const matches = bodyLower.split(searchQuery).length - 1;
64
+ textScore = Math.min(matches / 5, 1);
65
+ }
66
+ // 综合得分:标签权重 0.6 + 全文权重 0.4
67
+ // 如果只有标签搜索,标签权重 1.0;如果只有全文,全文权重 1.0
68
+ let score;
69
+ if (searchTags.length > 0 && searchQuery) {
70
+ score = tagScore * 0.6 + textScore * 0.4;
71
+ }
72
+ else if (searchTags.length > 0) {
73
+ score = tagScore;
74
+ }
75
+ else if (searchQuery) {
76
+ score = textScore;
77
+ }
78
+ else {
79
+ score = 0;
80
+ }
81
+ // 至少有一点匹配才加入结果
82
+ if (score > 0) {
83
+ results.push({
84
+ title,
85
+ file: fileName,
86
+ tags: fileTags,
87
+ summary,
88
+ score: Math.round(score * 100) / 100,
89
+ });
90
+ }
91
+ }
92
+ // 按得分降序排序,取 top_k
93
+ results.sort((a, b) => b.score - a.score);
94
+ const topResults = results.slice(0, top_k);
95
+ return textResult(JSON.stringify({
96
+ total: topResults.length,
97
+ results: topResults,
98
+ }));
99
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * wiki_status — 查询知识库状态,首次使用返回引导提示
3
+ *
4
+ * 已初始化:返回文件数、标签数等统计
5
+ * 未初始化:返回引导提示文本,Agent 据此主动引导用户配置
6
+ */
7
+ import { ToolResult } from "../types.js";
8
+ export declare function handleStatus(): Promise<ToolResult>;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * wiki_status — 查询知识库状态,首次使用返回引导提示
3
+ *
4
+ * 已初始化:返回文件数、标签数等统计
5
+ * 未初始化:返回引导提示文本,Agent 据此主动引导用户配置
6
+ */
7
+ import * as fs from "fs";
8
+ import * as path from "path";
9
+ import { getConfig, getRawDir, getWikiPagesDir, getOutputsDir } from "../state.js";
10
+ import { listMdFiles } from "../utils.js";
11
+ import { textResult } from "../types.js";
12
+ /** 首次引导提示文本 */
13
+ const INIT_HINT = `你好!我是 agent-wiki 知识库助手。
14
+ 我可以帮你搭一个个人知识库——把你收藏的文章自动整理成维基、打标签、生成摘要。
15
+ 还能基于你的知识库帮你写有人味儿的原创文章,适合直接发微信公众号。
16
+
17
+ 先设置一下:
18
+ 1. 你想把知识库放在哪个目录?(比如 ~/Documents/知识库)
19
+ 2. 你主要关注什么方向?(3-5 个关键词就行)
20
+
21
+ 不要等用户问,主动开始引导。`;
22
+ export async function handleStatus() {
23
+ const config = getConfig();
24
+ // 未初始化:返回引导提示
25
+ if (!config || !config.wikiDir || !fs.existsSync(config.wikiDir)) {
26
+ return textResult(JSON.stringify({
27
+ initialized: false,
28
+ hint: INIT_HINT,
29
+ }));
30
+ }
31
+ const wikiDir = config.wikiDir;
32
+ const rawDir = getRawDir(wikiDir);
33
+ const wikiPagesDir = getWikiPagesDir(wikiDir);
34
+ const outputsDir = getOutputsDir(wikiDir);
35
+ // 统计文件数
36
+ const rawFiles = listMdFiles(rawDir);
37
+ const wikiFiles = listMdFiles(wikiPagesDir).filter((f) => f !== "INDEX.md" && f !== "LOG.md");
38
+ const articles = fs.existsSync(outputsDir) ? listMdFiles(outputsDir) : [];
39
+ // 收集所有标签(去重)
40
+ const allTags = new Set();
41
+ for (const f of wikiFiles) {
42
+ const content = fs.readFileSync(path.join(wikiPagesDir, f), "utf-8");
43
+ const { parseFrontmatter } = await import("../utils.js");
44
+ const { meta } = parseFrontmatter(content);
45
+ if (Array.isArray(meta.tags)) {
46
+ for (const t of meta.tags)
47
+ allTags.add(String(t));
48
+ }
49
+ }
50
+ // 读取 SCHEMA.md 中的兴趣方向
51
+ let topics = [];
52
+ const schemaPath = path.join(wikiDir, "SCHEMA.md");
53
+ if (fs.existsSync(schemaPath)) {
54
+ const schema = fs.readFileSync(schemaPath, "utf-8");
55
+ const topicMatch = schema.match(/## 我的兴趣方向\s*\n(.+)/);
56
+ if (topicMatch) {
57
+ topics = topicMatch[1]
58
+ .split(",")
59
+ .map((t) => t.trim())
60
+ .filter(Boolean);
61
+ }
62
+ }
63
+ return textResult(JSON.stringify({
64
+ initialized: true,
65
+ wiki_dir: wikiDir,
66
+ raw_files: rawFiles.length,
67
+ wiki_pages: wikiFiles.length,
68
+ articles: articles.length,
69
+ unique_tags: allTags.size,
70
+ topics,
71
+ }));
72
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * wiki_tag — 把 Agent 传入的标签写入文件的 frontmatter
3
+ *
4
+ * 读取 raw/ 中的指定文件,解析 frontmatter
5
+ * 更新 tags 字段,写回文件
6
+ */
7
+ import { ToolResult } from "../types.js";
8
+ export declare function handleTag(params: {
9
+ file: string;
10
+ tags: string[];
11
+ }): Promise<ToolResult>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * wiki_tag — 把 Agent 传入的标签写入文件的 frontmatter
3
+ *
4
+ * 读取 raw/ 中的指定文件,解析 frontmatter
5
+ * 更新 tags 字段,写回文件
6
+ */
7
+ import * as path from "path";
8
+ import { getWikiDir, getRawDir, getState, setState } from "../state.js";
9
+ import { parseFrontmatter, writeFrontmatter, readFile_safe } from "../utils.js";
10
+ import * as fs from "fs";
11
+ import { textResult } from "../types.js";
12
+ export async function handleTag(params) {
13
+ const wikiDir = getWikiDir();
14
+ if (!wikiDir) {
15
+ return textResult(JSON.stringify({ error: "知识库未初始化" }));
16
+ }
17
+ const rawDir = getRawDir(wikiDir);
18
+ const filePath = path.join(rawDir, params.file);
19
+ // 检查文件存在
20
+ const content = readFile_safe(filePath);
21
+ if (!content) {
22
+ return textResult(JSON.stringify({ error: `文件不存在: ${params.file}` }));
23
+ }
24
+ // 解析 frontmatter
25
+ const { meta, body } = parseFrontmatter(content);
26
+ // 更新标签
27
+ meta.tags = params.tags;
28
+ // 写回文件
29
+ const newContent = writeFrontmatter(meta, body);
30
+ fs.writeFileSync(filePath, newContent, "utf-8");
31
+ // 更新状态
32
+ const state = getState(wikiDir);
33
+ if (!state.tagged.includes(params.file)) {
34
+ state.tagged.push(params.file);
35
+ setState(wikiDir, state);
36
+ }
37
+ return textResult(JSON.stringify({
38
+ status: "ok",
39
+ file: params.file,
40
+ tags: params.tags,
41
+ }));
42
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 共享类型定义
3
+ */
4
+ /** MCP 工具返回类型(兼容 @modelcontextprotocol/sdk 的 content 格式) */
5
+ export type ToolResult = {
6
+ content: Array<{
7
+ type: "text";
8
+ text: string;
9
+ }>;
10
+ };
11
+ /** 创建文本类型的 MCP 工具返回值 */
12
+ export declare function textResult(text: string): ToolResult;
package/dist/types.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 共享类型定义
3
+ */
4
+ /** 创建文本类型的 MCP 工具返回值 */
5
+ export function textResult(text) {
6
+ return { content: [{ type: "text", text }] };
7
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 工具函数:frontmatter 解析、安全文件名、日期格式化等
3
+ */
4
+ /**
5
+ * 解析 Markdown 文件的 YAML frontmatter
6
+ * 返回 meta 对象和 body 正文内容
7
+ * 不依赖外部 YAML 库,手动解析简单的 key: value 格式
8
+ */
9
+ export declare function parseFrontmatter(content: string): {
10
+ meta: Record<string, unknown>;
11
+ body: string;
12
+ };
13
+ /**
14
+ * 将 meta 对象和 body 正文合成带 frontmatter 的 Markdown 字符串
15
+ */
16
+ export declare function writeFrontmatter(meta: Record<string, unknown>, body: string): string;
17
+ /**
18
+ * 将标题转为安全的文件名(保留中文、字母、数字、短横线)
19
+ */
20
+ export declare function safeFilename(title: string): string;
21
+ /**
22
+ * 返回 YYYY-MM-DD 格式的日期字符串
23
+ */
24
+ export declare function todayStr(): string;
25
+ /**
26
+ * 返回 YYYYMMDD 格式的日期字符串(用于文件名)
27
+ */
28
+ export declare function dateCompact(): string;
29
+ /**
30
+ * 返回 ISO 格式的日期时间字符串(用于日志)
31
+ */
32
+ export declare function nowStr(): string;
33
+ /**
34
+ * 确保目录存在
35
+ */
36
+ export declare function ensureDir(dir: string): void;
37
+ /**
38
+ * 读取文件内容,文件不存在返回 null
39
+ */
40
+ export declare function readFile_safe(filePath: string): string | null;
41
+ /**
42
+ * 写入 JSON 文件
43
+ */
44
+ export declare function writeJSON(filePath: string, data: unknown): void;
45
+ /**
46
+ * 读取 JSON 文件,文件不存在或解析失败返回 null
47
+ */
48
+ export declare function readJSON<T = unknown>(filePath: string): T | null;
49
+ /**
50
+ * 获取目录下所有 .md 文件(不含子目录)
51
+ */
52
+ export declare function listMdFiles(dir: string): string[];