read-cli 0.2.0__py3-none-any.whl

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.
read/core/models.py ADDED
@@ -0,0 +1,80 @@
1
+ """数据模型
2
+
3
+ Core Library 的数据结构定义。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Optional
10
+
11
+
12
+ @dataclass
13
+ class Item:
14
+ """摘录数据模型
15
+
16
+ Attributes:
17
+ id: 主键 ID
18
+ content: 摘录内容
19
+ url: 链接
20
+ source: 来源备注
21
+ type: 数据类型(quote/article/code)
22
+ metadata: JSON 扩展字段
23
+ created_at: 创建时间(ISO 8601)
24
+ updated_at: 更新时间(ISO 8601)
25
+ """
26
+
27
+ id: int
28
+ content: Optional[str] = None
29
+ url: Optional[str] = None
30
+ source: Optional[str] = None
31
+ type: str = "quote"
32
+ metadata: Optional[str] = None
33
+ created_at: Optional[str] = None
34
+ updated_at: Optional[str] = None
35
+
36
+ @property
37
+ def is_quote(self) -> bool:
38
+ """是否为文字摘录"""
39
+ return self.type == "quote" and self.content is not None
40
+
41
+ @property
42
+ def is_link(self) -> bool:
43
+ """是否为链接"""
44
+ return self.url is not None
45
+
46
+ @property
47
+ def display_text(self) -> str:
48
+ """显示文本(优先 content,其次 url)"""
49
+ if self.content:
50
+ return self.content
51
+ if self.url:
52
+ return self.url
53
+ return "(空)"
54
+
55
+ def to_dict(self) -> dict:
56
+ """转换为字典"""
57
+ return {
58
+ "id": self.id,
59
+ "content": self.content,
60
+ "url": self.url,
61
+ "source": self.source,
62
+ "type": self.type,
63
+ "metadata": self.metadata,
64
+ "created_at": self.created_at,
65
+ "updated_at": self.updated_at,
66
+ }
67
+
68
+ @classmethod
69
+ def from_dict(cls, data: dict) -> "Item":
70
+ """从字典创建"""
71
+ return cls(
72
+ id=data["id"],
73
+ content=data.get("content"),
74
+ url=data.get("url"),
75
+ source=data.get("source"),
76
+ type=data.get("type", "quote"),
77
+ metadata=data.get("metadata"),
78
+ created_at=data.get("created_at"),
79
+ updated_at=data.get("updated_at"),
80
+ )
read/db/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """数据库层"""
2
+
3
+ from .connection import ReadDatabase, get_connection, close_connection, get_cursor, get_db_path
4
+ from .schema import ReadSchemaManager, SCHEMA_VERSION, get_schema_version, set_schema_version, is_initialized, init_database
5
+ from .utils import to_cents, from_cents
6
+
7
+ __all__ = [
8
+ "ReadDatabase", "ReadSchemaManager",
9
+ "get_connection", "close_connection", "get_cursor", "get_db_path",
10
+ "SCHEMA_VERSION", "get_schema_version", "set_schema_version", "is_initialized", "init_database",
11
+ "to_cents", "from_cents",
12
+ ]
read/db/connection.py ADDED
@@ -0,0 +1,37 @@
1
+ """数据库连接管理模块
2
+
3
+ 继承 dong.db.Database,提供 read-cli 专用数据库访问。
4
+ """
5
+
6
+ import sqlite3
7
+ from typing import Iterator
8
+ from contextlib import contextmanager
9
+
10
+ from dong.db import Database as DongDatabase
11
+
12
+
13
+ class ReadDatabase(DongDatabase):
14
+ """读咚咚数据库类 - 继承自 dong.db.Database
15
+
16
+ 数据库路径: ~/.read/read.db
17
+ """
18
+
19
+ @classmethod
20
+ def get_name(cls) -> str:
21
+ return "read"
22
+
23
+
24
+ # 兼容性函数
25
+ def get_connection(db_path=None):
26
+ return ReadDatabase.get_connection()
27
+
28
+ def close_connection():
29
+ ReadDatabase.close_connection()
30
+
31
+ @contextmanager
32
+ def get_cursor() -> Iterator[sqlite3.Cursor]:
33
+ with ReadDatabase.get_cursor() as cur:
34
+ yield cur
35
+
36
+ def get_db_path():
37
+ return ReadDatabase.get_db_path()
read/db/schema.py ADDED
@@ -0,0 +1,60 @@
1
+ """数据库 Schema 定义和版本管理
2
+
3
+ 继承 dong.db.SchemaManager,管理 read-cli 的数据库 schema。
4
+ """
5
+
6
+ from dong.db import SchemaManager
7
+ from .connection import ReadDatabase
8
+
9
+ SCHEMA_VERSION = "1.0.0"
10
+
11
+
12
+ class ReadSchemaManager(SchemaManager):
13
+ """读咚咚 Schema 管理器"""
14
+
15
+ def __init__(self):
16
+ super().__init__(
17
+ db_class=ReadDatabase,
18
+ current_version=SCHEMA_VERSION
19
+ )
20
+
21
+ def init_schema(self) -> None:
22
+ self._create_articles_table()
23
+ self._create_indexes()
24
+
25
+ def _create_articles_table(self) -> None:
26
+ with ReadDatabase.get_cursor() as cur:
27
+ cur.execute("""
28
+ CREATE TABLE IF NOT EXISTS articles (
29
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
30
+ title TEXT NOT NULL,
31
+ author TEXT,
32
+ url TEXT,
33
+ notes TEXT,
34
+ status TEXT DEFAULT 'reading',
35
+ rating INTEGER,
36
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
37
+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP
38
+ )
39
+ """)
40
+
41
+ def _create_indexes(self) -> None:
42
+ with ReadDatabase.get_cursor() as cur:
43
+ cur.execute("CREATE INDEX IF NOT EXISTS idx_articles_status ON articles(status)")
44
+ cur.execute("CREATE INDEX IF NOT EXISTS idx_articles_author ON articles(author)")
45
+
46
+
47
+ # 兼容性函数
48
+ def get_schema_version() -> str | None:
49
+ return ReadSchemaManager().get_stored_version()
50
+
51
+ def set_schema_version(version: str) -> None:
52
+ ReadDatabase.set_meta(ReadSchemaManager.VERSION_KEY, version)
53
+
54
+ def is_initialized() -> bool:
55
+ return ReadSchemaManager().is_initialized()
56
+
57
+ def init_database() -> None:
58
+ schema = ReadSchemaManager()
59
+ if not schema.is_initialized():
60
+ schema.initialize()
read/db/utils.py ADDED
@@ -0,0 +1,272 @@
1
+ """CRUD 操作封装
2
+
3
+ 职责:
4
+ - 封装所有数据库操作
5
+ - 提供类型友好的接口
6
+ - 处理业务逻辑验证
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Optional
12
+
13
+ from read.const import DEFAULT_LIMIT, DEFAULT_TYPE, get_timestamp
14
+
15
+
16
+ def add_item(
17
+ content: Optional[str] = None,
18
+ url: Optional[str] = None,
19
+ source: Optional[str] = None,
20
+ item_type: str = DEFAULT_TYPE,
21
+ metadata: Optional[str] = None,
22
+ ) -> int:
23
+ """添加摘录,返回新 ID
24
+
25
+ Args:
26
+ content: 摘录内容
27
+ url: 链接
28
+ source: 来源备注
29
+ item_type: 数据类型
30
+ metadata: JSON 扩展字段
31
+
32
+ Returns:
33
+ 新插入记录的 ID
34
+
35
+ Raises:
36
+ ValueError: content 和 url 都为空
37
+ """
38
+ from read.db.connection import get_cursor
39
+
40
+ # 验证
41
+ if not content and not url:
42
+ raise ValueError("content 和 url 至少需要一个不为空")
43
+
44
+ now = get_timestamp()
45
+
46
+ with get_cursor() as cursor:
47
+ cursor.execute(
48
+ """
49
+ INSERT INTO items (content, url, source, type, metadata, created_at, updated_at)
50
+ VALUES (?, ?, ?, ?, ?, ?, ?)
51
+ """,
52
+ (content, url, source, item_type, metadata, now, now),
53
+ )
54
+ return cursor.lastrowid
55
+
56
+
57
+ def list_items(
58
+ limit: int = DEFAULT_LIMIT,
59
+ offset: int = 0,
60
+ item_type: Optional[str] = None,
61
+ order: str = "desc",
62
+ ) -> list[dict]:
63
+ """列出摘录
64
+
65
+ Args:
66
+ limit: 返回数量限制
67
+ offset: 偏移量
68
+ item_type: 筛选类型
69
+ order: 排序方向(desc/asc)
70
+
71
+ Returns:
72
+ 摘录列表
73
+ """
74
+ from read.db.connection import get_cursor
75
+
76
+ order_sql = "DESC" if order.lower() == "desc" else "ASC"
77
+
78
+ with get_cursor() as cursor:
79
+ if item_type:
80
+ cursor.execute(
81
+ f"""
82
+ SELECT id, content, url, source, type, metadata, created_at, updated_at
83
+ FROM items
84
+ WHERE type = ?
85
+ ORDER BY created_at {order_sql}
86
+ LIMIT ? OFFSET ?
87
+ """,
88
+ (item_type, limit, offset),
89
+ )
90
+ else:
91
+ cursor.execute(
92
+ f"""
93
+ SELECT id, content, url, source, type, metadata, created_at, updated_at
94
+ FROM items
95
+ ORDER BY created_at {order_sql}
96
+ LIMIT ? OFFSET ?
97
+ """,
98
+ (limit, offset),
99
+ )
100
+
101
+ rows = cursor.fetchall()
102
+ return [dict(row) for row in rows]
103
+
104
+
105
+ def get_item(item_id: int) -> Optional[dict]:
106
+ """获取单条摘录
107
+
108
+ Args:
109
+ item_id: 摘录 ID
110
+
111
+ Returns:
112
+ 摘录数据,不存在返回 None
113
+ """
114
+ from read.db.connection import get_cursor
115
+
116
+ with get_cursor() as cursor:
117
+ cursor.execute(
118
+ """
119
+ SELECT id, content, url, source, type, metadata, created_at, updated_at
120
+ FROM items
121
+ WHERE id = ?
122
+ """,
123
+ (item_id,),
124
+ )
125
+ row = cursor.fetchone()
126
+ return dict(row) if row else None
127
+
128
+
129
+ def delete_item(item_id: int) -> bool:
130
+ """删除摘录
131
+
132
+ Args:
133
+ item_id: 摘录 ID
134
+
135
+ Returns:
136
+ 是否删除成功
137
+ """
138
+ from read.db.connection import get_cursor
139
+
140
+ with get_cursor() as cursor:
141
+ cursor.execute("DELETE FROM items WHERE id = ?", (item_id,))
142
+ return cursor.rowcount > 0
143
+
144
+
145
+ def search_items(
146
+ query: str,
147
+ field: Optional[str] = None,
148
+ limit: int = DEFAULT_LIMIT,
149
+ ) -> list[dict]:
150
+ """搜索摘录
151
+
152
+ Args:
153
+ query: 搜索关键词
154
+ field: 搜索字段(content/url/source),None 表示全部
155
+ limit: 返回数量限制
156
+
157
+ Returns:
158
+ 匹配的摘录列表
159
+ """
160
+ from read.db.connection import get_cursor
161
+
162
+ pattern = f"%{query}%"
163
+
164
+ with get_cursor() as cursor:
165
+ if field == "content":
166
+ cursor.execute(
167
+ """
168
+ SELECT id, content, url, source, type, metadata, created_at, updated_at
169
+ FROM items
170
+ WHERE content LIKE ?
171
+ ORDER BY created_at DESC
172
+ LIMIT ?
173
+ """,
174
+ (pattern, limit),
175
+ )
176
+ elif field == "url":
177
+ cursor.execute(
178
+ """
179
+ SELECT id, content, url, source, type, metadata, created_at, updated_at
180
+ FROM items
181
+ WHERE url LIKE ?
182
+ ORDER BY created_at DESC
183
+ LIMIT ?
184
+ """,
185
+ (pattern, limit),
186
+ )
187
+ elif field == "source":
188
+ cursor.execute(
189
+ """
190
+ SELECT id, content, url, source, type, metadata, created_at, updated_at
191
+ FROM items
192
+ WHERE source LIKE ?
193
+ ORDER BY created_at DESC
194
+ LIMIT ?
195
+ """,
196
+ (pattern, limit),
197
+ )
198
+ else:
199
+ # 全部字段
200
+ cursor.execute(
201
+ """
202
+ SELECT id, content, url, source, type, metadata, created_at, updated_at
203
+ FROM items
204
+ WHERE content LIKE ? OR url LIKE ? OR source LIKE ?
205
+ ORDER BY created_at DESC
206
+ LIMIT ?
207
+ """,
208
+ (pattern, pattern, pattern, limit),
209
+ )
210
+
211
+ rows = cursor.fetchall()
212
+ return [dict(row) for row in rows]
213
+
214
+
215
+ def count_total() -> int:
216
+ """获取摘录总数"""
217
+ from read.db.connection import get_cursor
218
+
219
+ with get_cursor() as cursor:
220
+ cursor.execute("SELECT COUNT(*) as count FROM items")
221
+ row = cursor.fetchone()
222
+ return row["count"] if row else 0
223
+
224
+
225
+ def update_item(
226
+ item_id: int,
227
+ content: Optional[str] = None,
228
+ url: Optional[str] = None,
229
+ source: Optional[str] = None,
230
+ metadata: Optional[str] = None,
231
+ ) -> bool:
232
+ """更新摘录(预留,v0.1 暂不通过 CLI 暴露)
233
+
234
+ Args:
235
+ item_id: 摘录 ID
236
+ content: 新内容
237
+ url: 新链接
238
+ source: 新来源
239
+ metadata: 新元数据
240
+
241
+ Returns:
242
+ 是否更新成功
243
+ """
244
+ from read.db.connection import get_cursor
245
+
246
+ updates = []
247
+ params = []
248
+
249
+ if content is not None:
250
+ updates.append("content = ?")
251
+ params.append(content)
252
+ if url is not None:
253
+ updates.append("url = ?")
254
+ params.append(url)
255
+ if source is not None:
256
+ updates.append("source = ?")
257
+ params.append(source)
258
+ if metadata is not None:
259
+ updates.append("metadata = ?")
260
+ params.append(metadata)
261
+
262
+ if not updates:
263
+ return False
264
+
265
+ updates.append("updated_at = ?")
266
+ params.append(get_timestamp())
267
+ params.append(item_id)
268
+
269
+ with get_cursor() as cursor:
270
+ sql = f"UPDATE items SET {', '.join(updates)} WHERE id = ?"
271
+ cursor.execute(sql, params)
272
+ return cursor.rowcount > 0
read/mcp/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """MCP Server - Model Context Protocol 服务端
2
+
3
+ v0.2 版本功能,为 Claude/ChatGPT 等 Agent 提供原生集成。
4
+ """
5
+
6
+ # v0.1 预留,v0.2 实现
7
+ __all__ = []
@@ -0,0 +1,218 @@
1
+ Metadata-Version: 2.4
2
+ Name: read-cli
3
+ Version: 0.2.0
4
+ Summary: 读咚咚 (Read) - 个人知识数据层的命令行接口
5
+ Author: gudong
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: agent,cli,knowledge,mcp
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: dong-core>=0.3.0
17
+ Requires-Dist: typer>=0.12.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: mypy>=1.0; extra == 'dev'
20
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
21
+ Requires-Dist: pytest>=7.0; extra == 'dev'
22
+ Provides-Extra: mcp
23
+ Requires-Dist: mcp>=0.1.0; extra == 'mcp'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # 读咚咚 (Read) - 个人知识数据层
27
+
28
+ > 读咚咚是个人知识数据层的 Python 库,提供 CLI、SDK 和 MCP Server 等多种访问方式。
29
+ >
30
+ > 本地、私有、可编程的个人知识基础设施。
31
+
32
+ [![Python](https://img.shields.io/badge/Python-3.11+-blue.svg)](https://www.python.org/)
33
+ [![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
34
+ [![Version](https://img.shields.io/badge/Version-0.1.0-orange.svg)](https://github.com/gudong/read)
35
+
36
+ ---
37
+
38
+ ## 简介
39
+
40
+ **读咚咚** 是一个个人知识数据层,以 **Core Library** 为核心资产,支持多种客户端访问。
41
+
42
+ 当你看到一句有启发的话、一篇好文章,快速存下来。CLI、浏览器插件、Agent 都可以访问这些数据。
43
+
44
+ ### 核心特点
45
+
46
+ - **数据层优先** - Core Library 是核心,CLI/插件/SDK 都是客户端
47
+ - **本地私有** - 数据存放在 `~/.read/read.db`,不上云、不同步、不追踪
48
+ - **Agent 友好** - JSON 输出 + Python SDK + MCP Server(v0.2)
49
+ - **极简核心** - 只做收集,不做整理
50
+
51
+ ---
52
+
53
+ ## 安装
54
+
55
+ ### 方式一:从 PyPI 安装(推荐)
56
+
57
+ ```bash
58
+ pip install read-cli
59
+ ```
60
+
61
+ ### 方式二:从源码安装
62
+
63
+ ```bash
64
+ git clone https://github.com/gudong/read.git
65
+ cd read
66
+ pip install -e .
67
+ ```
68
+
69
+ ### 初始化
70
+
71
+ ```bash
72
+ read init
73
+ ```
74
+
75
+ ---
76
+
77
+ ## 安装 Agent Workspace
78
+
79
+ 如果你使用 OpenClaw,可以把 agent 目录复制到工作区:
80
+
81
+ ```bash
82
+ # 复制 agent workspace
83
+ mkdir -p ~/.openclaw/agents/read
84
+ cp -r agent/* ~/.openclaw/agents/read/
85
+
86
+ # 从模板创建 MEMORY.md
87
+ cp agent/MEMORY.md.template ~/.openclaw/agents/read/MEMORY.md
88
+ ```
89
+
90
+ ---
91
+
92
+ ## 快速开始
93
+
94
+ ```bash
95
+ # 添加摘录
96
+ read add "开始,就是最好的时机"
97
+
98
+ # 收藏文章
99
+ read add --url "https://mp.weixin.qq.com/s/xxx"
100
+
101
+ # 列出所有
102
+ read ls
103
+
104
+ # 搜索
105
+ read search "AI"
106
+
107
+ # 删除
108
+ read delete 123 --force
109
+ ```
110
+
111
+ ---
112
+
113
+ ## 项目结构
114
+
115
+ ```
116
+ read/
117
+ ├── src/read/ # CLI 源码
118
+ ├── agent/ # Agent workspace(OpenClaw 使用)
119
+ │ ├── IDENTITY.md # Agent 身份
120
+ │ ├── SOUL.md # Agent 性格
121
+ │ ├── TOOLS.md # CLI 工具定义
122
+ │ └── MEMORY.md.template # 记忆模板
123
+ ├── docs/ # 文档
124
+ ├── tests/ # 测试
125
+ ├── pyproject.toml # Python 包配置
126
+ └── README.md # 本文件
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Python SDK
132
+
133
+ ```python
134
+ from read import Client
135
+
136
+ client = Client()
137
+
138
+ # 添加
139
+ item = client.add("开始,就是最好的时机")
140
+
141
+ # 列出
142
+ items = client.list(limit=10)
143
+
144
+ # 搜索
145
+ results = client.search("AI")
146
+ ```
147
+
148
+ ---
149
+
150
+ ## 命令参考
151
+
152
+ | 命令 | 说明 |
153
+ |------|------|
154
+ | `read init` | 初始化数据库 |
155
+ | `read add "内容"` | 添加摘录 |
156
+ | `read add --url "..."` | 收藏链接 |
157
+ | `read ls` | 列出所有 |
158
+ | `read search "关键词"` | 搜索 |
159
+ | `read get 123` | 获取单条 |
160
+ | `read delete 123` | 删除 |
161
+
162
+ ---
163
+
164
+ ## 架构设计
165
+
166
+ ```
167
+ ┌─────────────────────────────────────────────────────────┐
168
+ │ 客户端层 │
169
+ ├──────────────┬──────────────┬──────────────┬────────────┤
170
+ │ CLI │ Browser │ Python SDK │ MCP Server │
171
+ │ (read add) │ Extension │ (import) │ (Agent) │
172
+ └──────────────┴──────────────┴──────────────┴────────────┘
173
+
174
+ ┌────────▼────────┐
175
+ │ Core Library │
176
+ │ (read.core.Client)│
177
+ └────────┬────────┘
178
+
179
+ ┌────────▼────────┐
180
+ │ SQLite DB │
181
+ │ ~/.read/read.db │
182
+ └─────────────────┘
183
+ ```
184
+
185
+ ---
186
+
187
+ ## 路线图
188
+
189
+ | 版本 | 核心资产 | 客户端 | 状态 |
190
+ |------|----------|--------|------|
191
+ | v0.1 | Core Library v0.1 | CLI | ✅ 完成 |
192
+ | v0.2 | Core Library v0.1 | **MCP Server** | 🚧 开发中 |
193
+ | v0.3 | Core Library v0.1 | Python SDK 增强 | 📋 计划中 |
194
+ | v0.4 | Core Library v0.1 | Browser Extension | 📋 计划中 |
195
+
196
+ ---
197
+
198
+ ## 文档
199
+
200
+ - [API 参考文档](docs/API_REFERENCE.md)
201
+ - [架构设计](ARCHITECTURE.md)
202
+ - [产品理念](WHY.md)
203
+
204
+ ---
205
+
206
+ ## License
207
+
208
+ [MIT](LICENSE)
209
+
210
+ ---
211
+
212
+ ## 作者
213
+
214
+ [@gudong](https://github.com/gudong)
215
+
216
+ ---
217
+
218
+ **让 AI 看见你的知识。**