Skip to content

Commit 785b304

Browse files
ApeCodeLabclaude
andcommitted
feat: v0.3 — CLI-first 基线 + CLAUDE.md
- 新增 CLI 记录/查询命令:braindump init/add/list/search/show/stats --json - __version__ 修正为 0.2.0,与 README/pyproject 一致 - rebuild-index 识别 web/cli 文件名前缀,CLI 创建的笔记可被反向恢复 - config.example.toml 默认关闭 LLM/Review,未配置时不再失败 - 新增 DESIGN-v0.3.md 记录 v0.3 方向 - 新增 CLAUDE.md(仅技术参考,决议/路线放 second-brain projects/braindump/) - 解禁 CLAUDE.md(之前在 .gitignore) - 新增 tests/test_cli_first.py P0 CLI 与 v0.3 设计文档由 Codex 完成;CLAUDE.md / .gitignore 由 Claude Code 补齐。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1d08559 commit 785b304

10 files changed

Lines changed: 727 additions & 21 deletions

File tree

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,3 @@ test-data/
1616
TASK.md
1717
REVIEW.md
1818
data/
19-
CLAUDE.md

CLAUDE.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# braindump — Claude 协作指引
2+
3+
> 个人表达原材料库。文件即真相 + SQLite 索引;Telegram + CLI 入,Web / AI 取。
4+
>
5+
> **定位 / 当前阶段 / 路线 / 已决议事项**全部在 second-brain:
6+
> `~/work/second-brain/projects/braindump/``index.md` / `_ai-context.md` / `discussions/`
7+
> 跨工具决策流水:`~/work/second-brain/_ai/memory/daily/<YYYY-MM-DD>.md`
8+
9+
## 不变量
10+
11+
1. **文件即真相,DB 是索引**:原始文件按 `~/braindump-data/media/<type>/YYYY/MM/DD/` 存;SQLite 任何时候都能从文件 + `.md` frontmatter 重建(`rebuild-index`)。
12+
2. **三个目录 / 字段不能动**`media/` 原始文件、`transcripts/` 转写文本、`.md` 已落 frontmatter 字段名(`created / source / type / tags / title / summary / mood`)。改字段名 = 破坏向后兼容。
13+
3. **Secrets 不入 `config.toml`**:API Key、Bot Token 只通过环境变量;config 里存的是变量名。
14+
4. **Schema 改动 = migration**:在 `migrations/` 加新文件,并验证 `rebuild-index` 仍能从文件恢复出新 schema。
15+
5. **前端改后必须 `cd frontend && npm run build`**,否则 `frontend/dist/` 缺失 → 404。
16+
6. **CLI 命令必须有 `--json`**,退出码语义化(`0` = ok,非 `0` = 失败)。
17+
7. **CLI 输出格式用 `emit()` 统一**`braindump/cli.py`):默认人类可读,`--json` 走机器路径。
18+
19+
## 跑、测、构建
20+
21+
```bash
22+
# 后端 dev
23+
uv sync
24+
uv run python -m braindump web # 仅 Web (http://localhost:8080)
25+
uv run python -m braindump serve # 全部(Bot + Web + 转写 + 摘要 + Review)
26+
27+
# CLI 试一下
28+
uv run python -m braindump init --json
29+
uv run python -m braindump add "测试一下 CLI #test" --json
30+
uv run python -m braindump list --limit 5 --json
31+
32+
# 前端 dev
33+
cd frontend && npm install && npm run dev # http://localhost:5173(代理 API → :8080)
34+
35+
# 测试(绕开真实 Telegram + 前端 E2E)
36+
uv run python -m pytest tests/ -q \
37+
--ignore=tests/test_web_e2e.py \
38+
--ignore=tests/test_frontend_e2e.py \
39+
--ignore=tests/test_telegram_bot.py
40+
41+
# 前端构建(部署 / 完整 E2E 前必跑)
42+
cd frontend && npm run build
43+
```
44+
45+
> `tests/test_telegram_bot.py::test_get_updates` 真实访问 Telegram API,本机经 `127.0.0.1:7890` 代理 SSL 握手超时是已知现象,本地验证排除即可。
46+
47+
## AI 操作流程
48+
49+
- **复杂任务**:用 `braindump-dev` Skill(worktree → TASK.md → Claude Code PTY → 测试 → review → 合并)。
50+
- **简单改动**:直接 edit + 跑相关 pytest 即可。
51+
- **"盯着"任务**:定期读 PTY 日志并主动汇报,不要空泛说"在跑"。
52+
- **新增 CLI 命令**:默认人类输出 + 加 `--json` 走 AI 路径,复用 `emit()`;命令必须能被 `rebuild-index` 反向恢复。
53+
- **AI 调用 braindump**:优先 `braindump <cmd> --json`**不要**直连 SQLite 或绕过 CLI 写 `media/`
54+
55+
## 仓库结构
56+
57+
- `braindump/` — 后端 Python 包
58+
- `frontend/` — React 19 + Vite + Tailwind v4 + shadcn/ui
59+
- `migrations/` — SQLite schema 迁移
60+
- `tests/` — pytest
61+
- `DESIGN.md` / `DESIGN-v0.2.md` / `DESIGN-v0.3.md` — 历史 / 当前设计文档
62+
- `config.example.toml` — 配置模板
63+
- 数据目录(不入仓库):`~/braindump-data/`

DESIGN-v0.3.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# braindump v0.3 初始化方向
2+
3+
> 2026-05-29 reboot note. 目标从“能收集并回看”推进到“CLI-first、AI-friendly 的个人表达原材料库”。
4+
5+
## 现状判断
6+
7+
当前代码已经完成 v0.2 的基础产品形态:
8+
9+
- 输入侧:Telegram Bot 支持文字、图片、视频、语音、文件,音视频可转写。
10+
- 存储侧:文件系统是真相,SQLite 是索引;文字笔记写入 Markdown + YAML frontmatter。
11+
- 消化侧:已有 LLM 摘要字段、摘要 worker、每日回顾 scheduler。
12+
- 浏览侧:React SPA 已有 Timeline / Note Detail / Dashboard / Calendar。
13+
- 运维侧:Docker、health check、rebuild-index、retry-summary、migrate-frontmatter 都已具备。
14+
15+
关键缺口也很明确:
16+
17+
- CLI 之前主要是运维入口,不是“记录 / 查询 / 让 AI 调用”的产品入口。
18+
- Web 和 CLI 都会创建文件,但 `rebuild-index` 只识别旧的 `tg/fl/mm/im` 文件名前缀,重建时会漏掉 `web` 文件。
19+
- `braindump.__version__` 仍停在 `0.1.1`,与 README / pyproject 的 `0.2.0` 不一致。
20+
- second-brain 里项目状态仍是暂停后的快照,需要补一份新的重启决策记录。
21+
22+
## v0.3 产品目标
23+
24+
braindump 不做通用笔记,也不先做复杂知识库。它的核心路径是:
25+
26+
1. **低摩擦记录**:人可以用 Telegram / CLI 快速记录,AI 也可以用 CLI 写入。
27+
2. **结构化留痕**:每条内容都有稳定 ID、创建时间、来源、标签、文件路径、frontmatter。
28+
3. **可被 AI 调用**:命令要支持 `--json`,输出可解析,错误码要有语义。
29+
4. **帮助表达**:先把原始材料收集、搜索、回看做好,再做主题聚类、观点挖掘、文章草稿生成。
30+
31+
## CLI-first 基线
32+
33+
第一步初始化以下命令:
34+
35+
```bash
36+
braindump init --json
37+
braindump add "今天想讲的一个观点 #表达" --json
38+
braindump add --stdin --tag article --json
39+
braindump list --limit 20 --json
40+
braindump search "agent 落地" --json
41+
braindump show 123 --json
42+
braindump stats --json
43+
```
44+
45+
这些命令是 AI 调用 braindump 的稳定表面。MCP 暂时不是必要项;CLI + JSON + 清晰退出码更轻。
46+
47+
## 下一阶段
48+
49+
### P0:记录闭环
50+
51+
- `init` 写安全默认配置:Telegram、LLM、Review 默认关闭,避免未配置就报错。
52+
- `add/list/search/show/stats` 支持 `--json`
53+
- CLI 创建的文件使用 `cli` source,并能被 `rebuild-index` 恢复。
54+
55+
### P1:表达挖掘
56+
57+
- `braindump digest`:按时间范围 / 标签 / 搜索结果汇总素材。
58+
- `braindump cluster`:按主题聚类,输出主题、代表笔记、可展开观点。
59+
- `braindump draft`:基于一组 note id 生成文章 / 视频脚本草稿。
60+
- `braindump review --json`:把每日回顾从 Telegram scheduler 抽成可主动调用的 CLI。
61+
62+
### P2:Web 设计
63+
64+
Web 不先做营销页,也不先重做视觉。等 P1 命令语义稳定后,再用 opendesign 讨论一个“表达工作台”:
65+
66+
- 左侧是时间线 / 搜索 / 标签。
67+
- 中间是当前素材列表。
68+
- 右侧是 AI 挖掘结果:主题、观点、可写方向、草稿入口。
69+
- 移动端优先保证快速回看和编辑,不承载复杂写作工作流。
70+
71+
## 设计原则
72+
73+
- 数据目录仍然是可备份、可迁移、可人工阅读的主资产。
74+
- SQLite 只做索引和查询加速。
75+
- 所有 AI 生成内容必须保留模型、生成时间和可重跑状态。
76+
- CLI 输出默认给人看,`--json` 给 AI / 脚本看。
77+
- Web 只是更好的工作台,不是 braindump 的唯一入口。

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,16 @@ cd braindump
9797
uv sync
9898

9999
# 配置
100-
mkdir -p ~/braindump-data
101-
cp config.example.toml ~/braindump-data/config.toml
102-
# 编辑 config.toml 填入你的 Telegram 凭证
100+
uv run python -m braindump init
101+
# 按需编辑 ~/braindump-data/config.toml 填入 Telegram / LLM / Review 配置
103102

104103
# 可选:启用 AI 摘要(也可以改成任意 OpenAI-compatible 服务)
105104
export MOONSHOT_API_KEY="your_api_key"
106105

106+
# CLI 记录(AI / 脚本友好)
107+
uv run python -m braindump add "今天想表达的一个观点 #表达" --json
108+
uv run python -m braindump search "观点" --json
109+
107110
# 导入 Flomo(可选)
108111
uv run python -m braindump import flomo /path/to/flomo-export/
109112

@@ -150,6 +153,13 @@ Docker 镜像特点:
150153
## 常用命令
151154

152155
```bash
156+
uv run python -m braindump init --json
157+
uv run python -m braindump add "一条文字记录 #tag" --json
158+
uv run python -m braindump add --stdin --tag draft --json
159+
uv run python -m braindump list --limit 20 --json
160+
uv run python -m braindump search "agent 落地" --json
161+
uv run python -m braindump show 123 --json
162+
uv run python -m braindump stats --json
153163
uv run python -m braindump rebuild-index
154164
uv run python -m braindump retry-transcribe --all
155165
uv run python -m braindump retry-summary --all
@@ -251,6 +261,7 @@ port = 8080
251261

252262
## 下一步
253263

264+
- [ ] 表达挖掘 CLI:`digest` / `cluster` / `draft`
254265
- [ ] UI 再打磨一轮,尤其是移动端时间线和详情页体验
255266
- [ ] 更多导入源(Day One、Apple Notes...)
256267
- [ ] GPU / 远程 ASR 的配置再做顺手一点

braindump/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import logging
44

5-
__version__ = "0.1.1"
5+
__version__ = "0.2.0"
66

77
# Configure root logger for braindump
88
logger = logging.getLogger("braindump")

braindump/__main__.py

Lines changed: 132 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,40 @@ def main():
305305
parser = argparse.ArgumentParser(prog="braindump", description="Personal expression material library")
306306
sub = parser.add_subparsers(dest="command")
307307

308+
# init
309+
init_p = sub.add_parser("init", help="Initialize data directory, config, and database")
310+
init_p.add_argument("--data-dir", default=None, help="Data directory (default: ~/braindump-data)")
311+
init_p.add_argument("--force", action="store_true", help="Overwrite config.toml if it already exists")
312+
init_p.add_argument("--json", action="store_true", help="Print machine-readable JSON")
313+
314+
# add
315+
add_p = sub.add_parser("add", help="Create a text note from CLI")
316+
add_p.add_argument("text", nargs="?", help="Text content. Use --stdin to read from stdin")
317+
add_p.add_argument("--stdin", action="store_true", help="Read note content from stdin")
318+
add_p.add_argument("--tag", action="append", default=[], help="Add a tag (repeatable)")
319+
add_p.add_argument("--tags", default="", help="Comma-separated tags")
320+
add_p.add_argument("--json", action="store_true", help="Print machine-readable JSON")
321+
322+
# list
323+
list_p = sub.add_parser("list", help="List recent notes")
324+
list_p.add_argument("--limit", type=int, default=20)
325+
list_p.add_argument("--offset", type=int, default=0)
326+
list_p.add_argument("--type", dest="media_type", default=None, help="Filter by media type")
327+
list_p.add_argument("--tag", default=None, help="Filter by tag")
328+
list_p.add_argument("--q", default=None, help="Full-text search query")
329+
list_p.add_argument("--json", action="store_true", help="Print machine-readable JSON")
330+
331+
# search
332+
search_p = sub.add_parser("search", help="Search notes")
333+
search_p.add_argument("query", help="Search query")
334+
search_p.add_argument("--limit", type=int, default=20)
335+
search_p.add_argument("--json", action="store_true", help="Print machine-readable JSON")
336+
337+
# show
338+
show_p = sub.add_parser("show", help="Show a note")
339+
show_p.add_argument("note_id", type=int)
340+
show_p.add_argument("--json", action="store_true", help="Print machine-readable JSON")
341+
308342
# serve
309343
sub.add_parser("serve", help="Start all services (Bot + Web + Transcribe + Summary)")
310344

@@ -323,7 +357,8 @@ def main():
323357
sub.add_parser("bot", help="Start Telegram Bot only")
324358

325359
# stats
326-
sub.add_parser("stats", help="Show statistics")
360+
stats_p = sub.add_parser("stats", help="Show statistics")
361+
stats_p.add_argument("--json", action="store_true", help="Print machine-readable JSON")
327362

328363
# upgrade
329364
sub.add_parser("upgrade", help="Run database migrations")
@@ -355,7 +390,94 @@ def main():
355390
parser.print_help()
356391
sys.exit(1)
357392

358-
if args.command == "web":
393+
if args.command == "init":
394+
from pathlib import Path
395+
from braindump.cli import emit, init_project
396+
397+
result = asyncio.run(init_project(Path(args.data_dir) if args.data_dir else None, force=args.force))
398+
emit(
399+
result,
400+
args.json,
401+
[
402+
f"Initialized braindump data dir: {result['data_dir']}",
403+
f"Config: {result['config_path']}",
404+
f"Database: {result['db_path']}",
405+
"Next: braindump add \"your thought\" --json",
406+
],
407+
)
408+
409+
elif args.command == "add":
410+
from braindump.cli import add_text_note, emit, read_stdin_or_arg
411+
412+
try:
413+
content = read_stdin_or_arg(args.text, args.stdin)
414+
result = asyncio.run(add_text_note(content, args.tag, args.tags))
415+
except ValueError as exc:
416+
print(f"error: {exc}", file=sys.stderr)
417+
sys.exit(2)
418+
emit(
419+
result,
420+
args.json,
421+
[
422+
f"Saved note #{result['id']}",
423+
f"File: {result['file_path']}",
424+
],
425+
)
426+
427+
elif args.command == "list":
428+
from braindump.cli import emit, list_notes
429+
430+
result = asyncio.run(
431+
list_notes(
432+
limit=args.limit,
433+
offset=args.offset,
434+
media_type=args.media_type,
435+
tag=args.tag,
436+
query=args.q,
437+
)
438+
)
439+
emit(
440+
result,
441+
args.json,
442+
[
443+
f"{note['id']}\t{note['created_at']}\t{note['type']}\t{note['title']}"
444+
for note in result["notes"]
445+
],
446+
)
447+
448+
elif args.command == "search":
449+
from braindump.cli import emit, search_notes
450+
451+
result = asyncio.run(search_notes(args.query, limit=args.limit))
452+
emit(
453+
result,
454+
args.json,
455+
[
456+
f"{note['id']}\t{note['created_at']}\t{note['type']}\t{note['title']}"
457+
for note in result["notes"]
458+
],
459+
)
460+
461+
elif args.command == "show":
462+
from braindump.cli import emit, show_note
463+
464+
try:
465+
result = asyncio.run(show_note(args.note_id))
466+
except LookupError as exc:
467+
print(f"error: {exc}", file=sys.stderr)
468+
sys.exit(3)
469+
note = result["note"]
470+
content = note.get("content") or note.get("transcript") or ""
471+
emit(
472+
result,
473+
args.json,
474+
[
475+
f"#{note['id']} {note['created_at']} [{note['media_type']}]",
476+
content,
477+
],
478+
)
479+
480+
elif args.command == "web":
359481
from braindump.web.app import run_web
360482
run_web(host=args.host, port=args.port)
361483

@@ -391,8 +513,14 @@ def main():
391513
asyncio.run(run_migrations())
392514

393515
elif args.command == "stats":
394-
from braindump.database import show_stats
395-
asyncio.run(show_stats())
516+
if args.json:
517+
from braindump.cli import emit
518+
from braindump.database import get_stats
519+
520+
emit(asyncio.run(get_stats()), True)
521+
else:
522+
from braindump.database import show_stats
523+
asyncio.run(show_stats())
396524

397525
elif args.command == "rebuild-index":
398526
from braindump.database import rebuild_index

0 commit comments

Comments
 (0)