@codehourra/llm-iwiki 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CodeHourra
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.
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # llm-iwiki
2
+
3
+ 面向 AI Agent 的本地知识库 CLI。
4
+
5
+ `llm-iwiki` 会采集 Claude Code、Cursor、Codex、CodeBuddy 等 AI 编程工具的本地会话记录,按项目归一化到 SQLite,再通过 AI 工具生成结构化 YAML 摘要和经验候选,最终导出到 Obsidian。
6
+
7
+ ## 安装
8
+
9
+ > 运行时依赖 [Bun](https://bun.sh)(CLI 入口为 TypeScript,使用 `bun` shebang)。
10
+
11
+ ```bash
12
+ # 全局安装
13
+ npm install -g @codehourra/llm-iwiki
14
+ # 或使用 bun
15
+ bun add -g @codehourra/llm-iwiki
16
+
17
+ llm-iwiki init # 初始化配置与状态库
18
+ llm-iwiki doctor # 自检
19
+ ```
20
+
21
+ 也可以免安装、用 `npx` / `bunx` 直接运行(CLI 入口为 TypeScript,需本机已安装 Bun):
22
+
23
+ ```bash
24
+ # 一次性运行(不全局安装)
25
+ npx @codehourra/llm-iwiki init
26
+ npx @codehourra/llm-iwiki sync
27
+
28
+ # 或使用 bunx
29
+ bunx @codehourra/llm-iwiki doctor
30
+ ```
31
+
32
+ ## 当前状态
33
+
34
+ - Milestone 1(CLI 骨架与状态库)已完成:`init` / `doctor` / `projects resolve` / `projects rename`。
35
+ - Milestone 2(Collectors)基本完成:已实现 **Claude Code / Codex / Cursor / Gemini / CodeBuddy** 五个 collector 与 `sync`,可把本地会话按项目归一化入库,并通过 `projects list` / `projects inspect` 按项目维度跨工具聚合查看。
36
+ - Milestone 4(AI 协作总结)已完成:`summarize prepare` / `experiences prepare` 生成压缩后的 AI 任务,`summarize apply` / `experiences propose` 把外部 AI 产出的 YAML 落库到 `session_summaries` / `experience_candidates`。
37
+ - Milestone 5(Obsidian 导出)已完成:`config set obsidian.vault <dir>` 配置库路径,`obsidian export` 将会话总结、已 accept 的经验和项目索引写成带 frontmatter + managed block 的 Markdown 笔记。更新采用非破坏式协议——保留用户手写区,托管块被手动改动时标记冲突并跳过,`--force` 才覆盖;`obsidian check` 只读扫描 vault 报告 drift/missing。
38
+ - 经验生命周期:`experiences propose` 落库为候选(`experience_candidates`),`experiences candidates` 列出待审,`experiences accept/reject` 决定去留;accept 会把候选提升为正式 `experiences` 并建立 `session_experience_links`,再由 `obsidian export` 写出。
39
+
40
+ ```bash
41
+ llm-iwiki sync # 采集本地 AI 工具会话
42
+ llm-iwiki projects list # 按项目查看会话数
43
+ llm-iwiki projects inspect . # 查看某项目下各工具的会话
44
+ llm-iwiki summarize prepare changed --project . # 生成会话总结任务
45
+ llm-iwiki summarize apply --project . --file summaries.yaml
46
+ llm-iwiki experiences prepare --project . --from changed-summaries
47
+ llm-iwiki experiences propose --project . --file experiences.yaml
48
+ llm-iwiki experiences candidates --project . # 查看经验候选
49
+ llm-iwiki experiences accept <candidate-id> # 采纳为正式经验
50
+ llm-iwiki config set obsidian.vault ~/Obsidian/Vault # 配置 Obsidian 库
51
+ llm-iwiki obsidian export --project . # 导出为 Markdown 笔记
52
+ llm-iwiki obsidian check # 检查笔记是否漂移
53
+ ```
54
+
55
+ ## 待完成能力地图
56
+
57
+ 以下为已在设计文档中规划、但尚未实现的进阶能力(按优先级粗排):
58
+
59
+ | 能力 | 命令 | 说明 | 状态 |
60
+ | --- | --- | --- | --- |
61
+ | 经验融合 | `experiences merge <candidate-id> <experience-id>` | 人工确认后把候选正文融合进已有经验,先生成 `merge-preview-*.md` 预览,确认后改写 managed block,合并 `source_sessions` / `evidence`,保留用户手写区 | 待开发 |
62
+ | 项目目录迁移 | `obsidian move-project <project-id>` | `projects rename` 后按 `obsidian_notes` 映射与 frontmatter 中的 `aiwiki_project_id` 迁移 vault 目录,目标已存在时先做冲突检查 | 待开发 |
63
+ | 结构化检索 | `search [sessions\|experiences] <query>` | SQLite FTS5 为主检索(projects / sessions / messages / summaries / experiences),可选 ripgrep 检索已导出 Markdown,`--index sqlite\|obsidian\|all` 切换 | 待开发 |
64
+ | 打开笔记 | `obsidian open <note-id>` | 在 Obsidian 中直接打开对应笔记 | 待开发 |
65
+ | 路径冲突批处理 | `obsidian export --overwrite-conflicts \| --skip-conflicts` | 目录迁移 / 导出时对路径冲突批量选择,跳过的标记 `conflict_status=path_conflict` 供 `obsidian check` 持续报告 | 待开发 |
66
+ | 增量范围导出 | `obsidian export --changed \| --all` | 按变更范围而非单项目导出 | 待开发 |
67
+ | 时间窗筛选 | `experiences prepare --since 30d` | 经验提炼任务按时间窗口限定来源摘要 | 待开发 |
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@codehourra/llm-iwiki",
3
+ "version": "0.1.0",
4
+ "description": "面向 AI Agent 的本地知识库 CLI:采集 Claude Code / Cursor / Codex / CodeBuddy / Gemini 会话,按项目归一化,生成总结与经验并导出到 Obsidian。",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "CodeHourra",
8
+ "homepage": "https://github.com/CodeHourra/llm-iwiki#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/CodeHourra/llm-iwiki.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/CodeHourra/llm-iwiki/issues"
15
+ },
16
+ "keywords": [
17
+ "ai",
18
+ "cli",
19
+ "knowledge-base",
20
+ "obsidian",
21
+ "claude-code",
22
+ "cursor",
23
+ "codex",
24
+ "bun"
25
+ ],
26
+ "bin": {
27
+ "llm-iwiki": "./src/index.ts"
28
+ },
29
+ "files": [
30
+ "src",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "engines": {
35
+ "bun": ">=1.0.0"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "scripts": {
41
+ "dev": "bun run src/index.ts",
42
+ "test": "bun test --pass-with-no-tests",
43
+ "typecheck": "tsc --noEmit",
44
+ "build": "bun build src/index.ts --compile --outfile dist/llm-iwiki"
45
+ },
46
+ "dependencies": {
47
+ "yaml": "^2.7.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/bun": "^1.3.11",
51
+ "typescript": "~5.7"
52
+ }
53
+ }
package/src/ai-yaml.ts ADDED
@@ -0,0 +1,82 @@
1
+ import { parse } from 'yaml'
2
+
3
+ import type { Confidence, ParsedExperiencesYaml, ParsedSummariesYaml, SummaryValue } from './types'
4
+
5
+ const SUMMARY_VALUES = new Set<SummaryValue>(['none', 'low', 'medium', 'high'])
6
+ const CONFIDENCE_VALUES = new Set<Confidence>(['low', 'medium', 'high'])
7
+
8
+ function asRecord(value: unknown, label: string): Record<string, unknown> {
9
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
10
+ throw new Error(`${label} must be an object`)
11
+ }
12
+ return value as Record<string, unknown>
13
+ }
14
+
15
+ function requiredString(record: Record<string, unknown>, key: string, label = key): string {
16
+ const value = record[key]
17
+ if (typeof value !== 'string' || value.trim() === '') {
18
+ throw new Error(`Missing required string: ${label}`)
19
+ }
20
+ return value
21
+ }
22
+
23
+ function optionalConfidence(record: Record<string, unknown>): Confidence | null {
24
+ if (!('confidence' in record)) return null
25
+ const confidence = record.confidence
26
+ if (typeof confidence !== 'string' || !CONFIDENCE_VALUES.has(confidence as Confidence)) {
27
+ throw new Error(`Invalid confidence: ${String(confidence)}`)
28
+ }
29
+ return confidence as Confidence
30
+ }
31
+
32
+ export function parseSummariesYaml(source: string): ParsedSummariesYaml {
33
+ const root = asRecord(parse(source), 'summaries.yaml')
34
+ const projectId = requiredString(root, 'project_id')
35
+ if (!Array.isArray(root.summaries)) throw new Error('summaries must be an array')
36
+
37
+ return {
38
+ projectId,
39
+ summaries: root.summaries.map((item, index) => {
40
+ const itemLabel = `summaries[${index}]`
41
+ const record = asRecord(item, `summaries[${index}]`)
42
+ const value = requiredString(record, 'value', `${itemLabel}.value`)
43
+ if (!SUMMARY_VALUES.has(value as SummaryValue)) throw new Error(`Invalid summary value: ${value}`)
44
+ optionalConfidence(record)
45
+ return {
46
+ sessionId: requiredString(record, 'session_id', `${itemLabel}.session_id`),
47
+ title: requiredString(record, 'title', `${itemLabel}.title`),
48
+ value: value as SummaryValue,
49
+ summaryMarkdown: requiredString(record, 'summary_markdown', `${itemLabel}.summary_markdown`),
50
+ metadata: record,
51
+ }
52
+ }),
53
+ }
54
+ }
55
+
56
+ export function parseExperiencesYaml(source: string): ParsedExperiencesYaml {
57
+ const root = asRecord(parse(source), 'experiences.yaml')
58
+ const projectId = requiredString(root, 'project_id')
59
+ if (!Array.isArray(root.experiences)) throw new Error('experiences must be an array')
60
+
61
+ return {
62
+ projectId,
63
+ experiences: root.experiences.map((item, index) => {
64
+ const itemLabel = `experiences[${index}]`
65
+ const record = asRecord(item, itemLabel)
66
+ const sourceSessions = record.source_sessions
67
+ if (!Array.isArray(sourceSessions) || sourceSessions.some((value) => typeof value !== 'string')) {
68
+ throw new Error(`experiences[${index}].source_sessions must be a string array`)
69
+ }
70
+ const confidence = optionalConfidence(record)
71
+ return {
72
+ title: requiredString(record, 'title', `${itemLabel}.title`),
73
+ slug: typeof record.slug === 'string' ? record.slug : null,
74
+ summary: requiredString(record, 'summary', `${itemLabel}.summary`),
75
+ bodyMarkdown: requiredString(record, 'body_markdown', `${itemLabel}.body_markdown`),
76
+ sourceSessions,
77
+ confidence: confidence as Confidence | null,
78
+ metadata: record,
79
+ }
80
+ }),
81
+ }
82
+ }