agent-dump 0.1.0__tar.gz

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,42 @@
1
+ # python
2
+ .venv
3
+ .ruff_cache
4
+ __pycache__/
5
+ *.py[cod]
6
+ *$py.class
7
+ *.so
8
+ .Python
9
+ build/
10
+ develop-eggs/
11
+ dist/
12
+ downloads/
13
+ eggs/
14
+ .eggs/
15
+ lib/
16
+ lib64/
17
+ parts/
18
+ sdist/
19
+ var/
20
+ wheels/
21
+ *.egg-info/
22
+ .installed.cfg
23
+ *.egg
24
+
25
+ # pyright
26
+ .pyright/
27
+
28
+ # IDE
29
+ .vscode/
30
+ .idea/
31
+ *.swp
32
+ *.swo
33
+ *~
34
+ .DS_Store
35
+
36
+ # data
37
+ /data
38
+ /sessions
39
+
40
+ # uv
41
+ *.lock
42
+ !uv.lock
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,237 @@
1
+ # AGENTS.md
2
+
3
+ > 本文档用于帮助 AI Agents 快速理解本项目结构和开发规范
4
+
5
+ ## 项目概述
6
+
7
+ **agent-dump** 是一个 AI 编码助手会话导出工具,支持从 OpenCode 等 AI 编码工具中导出会话数据为 JSON 格式。
8
+
9
+ - **语言**: Python 3.14+
10
+ - **包管理**: uv
11
+ - **代码规范**: Ruff
12
+ - **构建工具**: Hatchling
13
+
14
+ ## 项目结构
15
+
16
+ ```
17
+ agent-dump/
18
+ ├── src/agent_dump/ # 主包目录
19
+ │ ├── __init__.py # 包初始化,公开 API
20
+ │ ├── __main__.py # python -m agent_dump 入口
21
+ │ ├── cli.py # 命令行接口和参数解析
22
+ │ ├── db.py # 数据库连接和查询操作
23
+ │ ├── exporter.py # JSON 导出逻辑
24
+ │ └── selector.py # 交互式会话选择
25
+ ├── tests/ # 测试目录(待填充)
26
+ ├── data/opencode/ # 本地数据库
27
+ ├── sessions/ # 导出目录
28
+ ├── pyproject.toml # 项目配置
29
+ ├── ruff.toml # 代码风格配置
30
+ └── Makefile # 自动化命令
31
+ ```
32
+
33
+ ## 核心模块
34
+
35
+ ### cli.py
36
+ 命令行入口模块,处理参数解析和主流程控制。
37
+
38
+ ```python
39
+ from agent_dump.cli import main
40
+ # 主函数入口
41
+ main()
42
+ ```
43
+
44
+ **关键函数:**
45
+ - `main()` - 主入口,处理参数解析和工作流调度
46
+
47
+ ### db.py
48
+ 数据库操作模块,处理 SQLite 数据库连接和查询。
49
+
50
+ **关键函数:**
51
+ - `find_db_path() -> Path` - 自动查找数据库路径
52
+ - `get_recent_sessions(db_path, days=7) -> List[Dict]` - 获取最近 N 天的会话
53
+
54
+ ### exporter.py
55
+ 导出逻辑模块,处理会话数据的 JSON 序列化。
56
+
57
+ **关键函数:**
58
+ - `export_session(db_path, session, output_dir) -> Path` - 导出单个会话
59
+ - `export_sessions(db_path, sessions, output_dir) -> List[Path]` - 批量导出
60
+
61
+ ### selector.py
62
+ 交互式选择模块,提供终端和简单两种选择模式。
63
+
64
+ **关键函数:**
65
+ - `select_sessions_interactive(sessions) -> List[Dict]` - 交互式选择(questionary)
66
+ - `select_sessions_simple(sessions) -> List[Dict]` - 简单选择(stdin)
67
+ - `is_terminal() -> bool` - 检查是否在终端环境
68
+
69
+ ## 使用方式
70
+
71
+ ### 命令行
72
+
73
+ ```bash
74
+ # 开发时
75
+ uv run agent-dump # 交互式导出
76
+ uv run agent-dump --days 3 # 导出最近 3 天
77
+ uv run agent-dump --list # 仅列出会话
78
+ uv run agent-dump --export id1,id2 # 指定 ID 导出
79
+
80
+ # 模块方式
81
+ uv run python -m agent_dump
82
+ ```
83
+
84
+ ### 作为库使用
85
+
86
+ ```python
87
+ from agent_dump import find_db_path, get_recent_sessions, export_session
88
+ from pathlib import Path
89
+
90
+ # 查找数据库
91
+ db_path = find_db_path()
92
+
93
+ # 获取最近 7 天的会话
94
+ sessions = get_recent_sessions(db_path, days=7)
95
+
96
+ # 导出第一个会话
97
+ output_dir = Path("./my-sessions")
98
+ export_session(db_path, sessions[0], output_dir)
99
+ ```
100
+
101
+ ## 开发规范
102
+
103
+ ### 代码风格
104
+ - 使用 Ruff 进行代码检查和格式化
105
+ - 配置位于 `ruff.toml`
106
+ - 单行最大长度 100
107
+ - 使用双引号
108
+
109
+ ### 命令
110
+
111
+ ```bash
112
+ # 代码检查
113
+ make lint # ruff check
114
+ make lint.fix # ruff check --fix
115
+ make lint.fmt # ruff format
116
+
117
+ # 类型检查(待配置)
118
+ make check # ty check
119
+ ```
120
+
121
+ ### 添加依赖
122
+
123
+ ```bash
124
+ # 生产依赖
125
+ uv add package-name
126
+
127
+ # 开发依赖
128
+ uv add --dev package-name
129
+ ```
130
+
131
+ ## 扩展指南
132
+
133
+ ### 支持新的 AI 工具
134
+
135
+ 要支持新的 AI 编码工具(如 Claude Code、Copilot Chat):
136
+
137
+ 1. **在 `db.py` 中添加新工具的数据库查找路径**
138
+
139
+ ```python
140
+ def find_db_path():
141
+ paths = [
142
+ # OpenCode
143
+ os.path.expanduser("~/.local/share/opencode/opencode.db"),
144
+ # 新工具
145
+ os.path.expanduser("~/.config/newtool/database.db"),
146
+ ]
147
+ ```
148
+
149
+ 2. **在 `cli.py` 中添加新的 agent 选项**
150
+
151
+ ```python
152
+ parser.add_argument(
153
+ "--agent",
154
+ type=str,
155
+ default="opencode",
156
+ choices=["opencode", "newtool"], # 添加新选项
157
+ help="Agent tool name",
158
+ )
159
+ ```
160
+
161
+ 3. **如有需要,创建新的数据解析模块**
162
+
163
+ 如果新工具的数据库结构与 OpenCode 不同,建议:
164
+ - 创建 `parsers/` 子目录
165
+ - 为每个工具创建解析器类
166
+ - 在 `exporter.py` 中根据 agent 类型选择解析器
167
+
168
+ ### 添加新的导出格式
169
+
170
+ 1. 在 `exporter.py` 中创建新的导出函数
171
+ 2. 在 `cli.py` 中添加格式选项
172
+
173
+ ```python
174
+ # exporter.py
175
+ def export_session_markdown(db_path, session, output_dir) -> Path:
176
+ """导出为 Markdown 格式"""
177
+ # 实现导出逻辑
178
+ pass
179
+ ```
180
+
181
+ ## 数据库结构
182
+
183
+ ### OpenCode 数据库表
184
+
185
+ ```sql
186
+ -- session 表
187
+ CREATE TABLE session (
188
+ id TEXT PRIMARY KEY,
189
+ title TEXT,
190
+ time_created INTEGER, -- 毫秒时间戳
191
+ time_updated INTEGER,
192
+ slug TEXT,
193
+ directory TEXT,
194
+ version INTEGER,
195
+ summary_files TEXT
196
+ );
197
+
198
+ -- message 表
199
+ CREATE TABLE message (
200
+ id TEXT PRIMARY KEY,
201
+ session_id TEXT,
202
+ time_created INTEGER,
203
+ data TEXT -- JSON 格式
204
+ );
205
+
206
+ -- part 表
207
+ CREATE TABLE part (
208
+ id TEXT PRIMARY KEY,
209
+ message_id TEXT,
210
+ time_created INTEGER,
211
+ data TEXT -- JSON 格式
212
+ );
213
+ ```
214
+
215
+ ## 注意事项
216
+
217
+ 1. **数据库路径**: `find_db_path()` 会尝试多个路径,新工具需要添加对应路径
218
+ 2. **时间戳**: 数据库存储的是毫秒时间戳,Python datetime 需要转换
219
+ 3. **JSON 数据**: message 和 part 表中的 data 字段是 JSON 字符串
220
+ 4. **交互模式**: `selector.py` 会根据是否在终端自动选择合适的模式
221
+
222
+ ## 发布
223
+
224
+ ```bash
225
+ # 构建包
226
+ uv build
227
+
228
+ # 发布到 PyPI
229
+ uv publish
230
+ ```
231
+
232
+ ## 相关文件
233
+
234
+ - `pyproject.toml` - 项目配置和依赖
235
+ - `ruff.toml` - 代码风格配置
236
+ - `README.md` - 用户文档(中文)
237
+ - `Makefile` - 自动化命令
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 XingKaiXin contributors
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, standard 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.
@@ -0,0 +1,12 @@
1
+ lint:
2
+ uv run ruff check .
3
+
4
+ lint.fix:
5
+ uv run ruff check . --fix
6
+
7
+ lint.fmt:
8
+ uv run ruff format .
9
+
10
+ check:
11
+ uv run pyright
12
+ uv run ty check .
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-dump
3
+ Version: 0.1.0
4
+ Summary: AI Coding Assistant Session Export Tool
5
+ Project-URL: Homepage, https://github.com/xingkaixin/agent-dump
6
+ Project-URL: Repository, https://github.com/xingkaixin/agent-dump
7
+ Project-URL: Issues, https://github.com/xingkaixin/agent-dump/issues
8
+ Author-email: XingKaiXin <xingkaixin@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,chat,cli,export,opencode
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.14
21
+ Requires-Dist: prompt-toolkit>=3.0.0
22
+ Requires-Dist: questionary>=2.1.1
23
+ Description-Content-Type: text/markdown
24
+
25
+ # Agent Dump
26
+
27
+ AI 编码助手会话导出工具 - 支持从多种 AI 编码工具的会话数据导出会话为 JSON 格式。
28
+
29
+ ## 支持的 AI 工具
30
+
31
+ - **OpenCode** - 开源 AI 编程助手
32
+ - **Claude Code** - Anthropic 的 AI 编码工具 *(计划中)*
33
+ - **Code X** - GitHub Copilot Chat *(计划中)*
34
+ - **更多工具** - 欢迎提交 PR 支持其他 AI 编码工具
35
+
36
+ ## 功能特性
37
+
38
+ - **交互式选择**: 使用 questionary 提供友好的命令行交互界面
39
+ - **批量导出**: 支持导出最近 N 天的所有会话
40
+ - **指定导出**: 通过会话 ID 导出特定会话
41
+ - **会话列表**: 仅列出会话而不导出
42
+ - **统计数据**: 导出包含 tokens 使用量、成本等统计信息
43
+ - **消息详情**: 完整保留会话消息、工具调用等详细信息
44
+
45
+ ## 安装
46
+
47
+ ### 方式一:使用 uv tool 安装(推荐)
48
+
49
+ ```bash
50
+ # 从 PyPI 安装(发布后可使用)
51
+ uv tool install agent-dump
52
+
53
+ # 从 GitHub 直接安装
54
+ uv tool install git+https://github.com/xingkaixin/agent-dump
55
+ ```
56
+
57
+ ### 方式二:使用 uvx 直接运行(无需安装)
58
+
59
+ ```bash
60
+ # 从 PyPI 运行(发布后可使用)
61
+ uvx agent-dump --help
62
+
63
+ # 从 GitHub 直接运行
64
+ uvx --from git+https://github.com/xingkaixin/agent-dump agent-dump --help
65
+ ```
66
+
67
+ ### 方式三:本地开发
68
+
69
+ ```bash
70
+ # 克隆仓库
71
+ git clone https://github.com/xingkaixin/agent-dump.git
72
+ cd agent-dump
73
+
74
+ # 使用 uv 安装依赖
75
+ uv sync
76
+
77
+ # 本地安装测试
78
+ uv tool install . --force
79
+ ```
80
+
81
+ ## 使用方法
82
+
83
+ ### 交互式导出(默认)
84
+
85
+ ```bash
86
+ # 方式一:使用命令行入口
87
+ uv run agent-dump
88
+
89
+ # 方式二:使用模块运行
90
+ uv run python -m agent_dump
91
+ ```
92
+
93
+ 运行后会显示最近 7 天的会话列表,使用空格选择/取消,回车确认导出。
94
+
95
+ ### 命令行参数
96
+
97
+ ```bash
98
+ uv run agent-dump --days 3 # 导出最近 3 天的会话
99
+ uv run agent-dump --agent claude # 指定 Agent 工具名称
100
+ uv run agent-dump --output ./my-sessions # 指定输出目录
101
+ uv run agent-dump --list # 仅列出会话
102
+ uv run agent-dump --export ses_abc,ses_xyz # 导出指定 ID 的会话
103
+ ```
104
+
105
+ ### 完整参数说明
106
+
107
+ | 参数 | 说明 | 默认值 |
108
+ |------|------|--------|
109
+ | `--days` | 查询最近 N 天的会话 | 7 |
110
+ | `--agent` | Agent 工具名称 | opencode |
111
+ | `--output` | 输出目录 | ./sessions |
112
+ | `--export` | 导出指定会话 ID(逗号分隔) | - |
113
+ | `--list` | 仅列出会话,不导出 | - |
114
+
115
+ ## 项目结构
116
+
117
+ ```
118
+ .
119
+ ├── src/
120
+ │ └── agent_dump/ # 主包目录
121
+ │ ├── __init__.py # 包初始化
122
+ │ ├── __main__.py # python -m agent_dump 入口
123
+ │ ├── cli.py # 命令行接口
124
+ │ ├── db.py # 数据库操作
125
+ │ ├── exporter.py # 导出逻辑
126
+ │ └── selector.py # 交互式选择
127
+ ├── tests/ # 测试目录
128
+ ├── pyproject.toml # 项目配置
129
+ ├── Makefile # 自动化命令
130
+ ├── ruff.toml # 代码风格配置
131
+ ├── data/ # 数据库目录
132
+ │ └── opencode/
133
+ │ └── opencode.db
134
+ └── sessions/ # 导出目录
135
+ └── {agent-name}/ # 按工具分类的导出文件
136
+ └── ses_xxx.json
137
+ ```
138
+
139
+ ## 开发
140
+
141
+ ```bash
142
+ # 代码检查
143
+ make lint
144
+
145
+ # 自动修复
146
+ make lint.fix
147
+
148
+ # 代码格式化
149
+ make lint.fmt
150
+
151
+ # 类型检查
152
+ make check
153
+ ```
154
+
155
+ ## 依赖
156
+
157
+ - Python >= 3.14
158
+ - prompt-toolkit >= 3.0.0
159
+ - questionary >= 2.1.1
160
+ - ruff >= 0.15.2 (开发)
161
+ - ty >= 0.0.18 (开发)
162
+
163
+ ## 许可证
164
+
165
+ MIT
@@ -0,0 +1,141 @@
1
+ # Agent Dump
2
+
3
+ AI 编码助手会话导出工具 - 支持从多种 AI 编码工具的会话数据导出会话为 JSON 格式。
4
+
5
+ ## 支持的 AI 工具
6
+
7
+ - **OpenCode** - 开源 AI 编程助手
8
+ - **Claude Code** - Anthropic 的 AI 编码工具 *(计划中)*
9
+ - **Code X** - GitHub Copilot Chat *(计划中)*
10
+ - **更多工具** - 欢迎提交 PR 支持其他 AI 编码工具
11
+
12
+ ## 功能特性
13
+
14
+ - **交互式选择**: 使用 questionary 提供友好的命令行交互界面
15
+ - **批量导出**: 支持导出最近 N 天的所有会话
16
+ - **指定导出**: 通过会话 ID 导出特定会话
17
+ - **会话列表**: 仅列出会话而不导出
18
+ - **统计数据**: 导出包含 tokens 使用量、成本等统计信息
19
+ - **消息详情**: 完整保留会话消息、工具调用等详细信息
20
+
21
+ ## 安装
22
+
23
+ ### 方式一:使用 uv tool 安装(推荐)
24
+
25
+ ```bash
26
+ # 从 PyPI 安装(发布后可使用)
27
+ uv tool install agent-dump
28
+
29
+ # 从 GitHub 直接安装
30
+ uv tool install git+https://github.com/xingkaixin/agent-dump
31
+ ```
32
+
33
+ ### 方式二:使用 uvx 直接运行(无需安装)
34
+
35
+ ```bash
36
+ # 从 PyPI 运行(发布后可使用)
37
+ uvx agent-dump --help
38
+
39
+ # 从 GitHub 直接运行
40
+ uvx --from git+https://github.com/xingkaixin/agent-dump agent-dump --help
41
+ ```
42
+
43
+ ### 方式三:本地开发
44
+
45
+ ```bash
46
+ # 克隆仓库
47
+ git clone https://github.com/xingkaixin/agent-dump.git
48
+ cd agent-dump
49
+
50
+ # 使用 uv 安装依赖
51
+ uv sync
52
+
53
+ # 本地安装测试
54
+ uv tool install . --force
55
+ ```
56
+
57
+ ## 使用方法
58
+
59
+ ### 交互式导出(默认)
60
+
61
+ ```bash
62
+ # 方式一:使用命令行入口
63
+ uv run agent-dump
64
+
65
+ # 方式二:使用模块运行
66
+ uv run python -m agent_dump
67
+ ```
68
+
69
+ 运行后会显示最近 7 天的会话列表,使用空格选择/取消,回车确认导出。
70
+
71
+ ### 命令行参数
72
+
73
+ ```bash
74
+ uv run agent-dump --days 3 # 导出最近 3 天的会话
75
+ uv run agent-dump --agent claude # 指定 Agent 工具名称
76
+ uv run agent-dump --output ./my-sessions # 指定输出目录
77
+ uv run agent-dump --list # 仅列出会话
78
+ uv run agent-dump --export ses_abc,ses_xyz # 导出指定 ID 的会话
79
+ ```
80
+
81
+ ### 完整参数说明
82
+
83
+ | 参数 | 说明 | 默认值 |
84
+ |------|------|--------|
85
+ | `--days` | 查询最近 N 天的会话 | 7 |
86
+ | `--agent` | Agent 工具名称 | opencode |
87
+ | `--output` | 输出目录 | ./sessions |
88
+ | `--export` | 导出指定会话 ID(逗号分隔) | - |
89
+ | `--list` | 仅列出会话,不导出 | - |
90
+
91
+ ## 项目结构
92
+
93
+ ```
94
+ .
95
+ ├── src/
96
+ │ └── agent_dump/ # 主包目录
97
+ │ ├── __init__.py # 包初始化
98
+ │ ├── __main__.py # python -m agent_dump 入口
99
+ │ ├── cli.py # 命令行接口
100
+ │ ├── db.py # 数据库操作
101
+ │ ├── exporter.py # 导出逻辑
102
+ │ └── selector.py # 交互式选择
103
+ ├── tests/ # 测试目录
104
+ ├── pyproject.toml # 项目配置
105
+ ├── Makefile # 自动化命令
106
+ ├── ruff.toml # 代码风格配置
107
+ ├── data/ # 数据库目录
108
+ │ └── opencode/
109
+ │ └── opencode.db
110
+ └── sessions/ # 导出目录
111
+ └── {agent-name}/ # 按工具分类的导出文件
112
+ └── ses_xxx.json
113
+ ```
114
+
115
+ ## 开发
116
+
117
+ ```bash
118
+ # 代码检查
119
+ make lint
120
+
121
+ # 自动修复
122
+ make lint.fix
123
+
124
+ # 代码格式化
125
+ make lint.fmt
126
+
127
+ # 类型检查
128
+ make check
129
+ ```
130
+
131
+ ## 依赖
132
+
133
+ - Python >= 3.14
134
+ - prompt-toolkit >= 3.0.0
135
+ - questionary >= 2.1.1
136
+ - ruff >= 0.15.2 (开发)
137
+ - ty >= 0.0.18 (开发)
138
+
139
+ ## 许可证
140
+
141
+ MIT
@@ -0,0 +1,63 @@
1
+ [project]
2
+ name = "agent-dump"
3
+ version = "0.1.0"
4
+ description = "AI Coding Assistant Session Export Tool"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.14"
8
+ authors = [
9
+ { name = "XingKaiXin", email = "xingkaixin@gmail.com" },
10
+ ]
11
+ keywords = ["ai", "chat", "export", "opencode", "cli"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Environment :: Console",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.14",
20
+ "Topic :: Utilities",
21
+ ]
22
+ dependencies = [
23
+ "prompt-toolkit>=3.0.0",
24
+ "questionary>=2.1.1",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/xingkaixin/agent-dump"
29
+ Repository = "https://github.com/xingkaixin/agent-dump"
30
+ Issues = "https://github.com/xingkaixin/agent-dump/issues"
31
+
32
+ [project.scripts]
33
+ agent-dump = "agent_dump.cli:main"
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "pyright>=1.1.408",
38
+ "ruff>=0.15.2",
39
+ "ty>=0.0.18",
40
+ ]
41
+
42
+ [build-system]
43
+ requires = ["hatchling"]
44
+ build-backend = "hatchling.build"
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/agent_dump"]
48
+
49
+ [tool.pyright]
50
+ pythonVersion = "3.14"
51
+ include = ["src"]
52
+ exclude = [
53
+ "**/node_modules",
54
+ "**/__pycache__",
55
+ ".git",
56
+ ".venv",
57
+ ]
58
+ defineConstant = { DEBUG = true }
59
+ venvPath = "."
60
+ venv = ".venv"
61
+ typeCheckingMode = "standard"
62
+ reportMissingImports = true
63
+ reportMissingTypeStubs = false
@@ -0,0 +1,41 @@
1
+ # ruff.toml
2
+ line-length = 120
3
+ target-version = "py314"
4
+ exclude = [
5
+ ".git",
6
+ ".venv",
7
+ "__pycache__",
8
+ "tests",
9
+ ]
10
+
11
+ [lint]
12
+ select = [
13
+ "E", # pycodestyle
14
+ "F", # pyflakes
15
+ "I", # isort
16
+ "UP", # pyupgrade
17
+ "B", # bugbear
18
+ "A", # builtins
19
+ "S", # bandit(安全检查,轻量但非常有用)
20
+ "C4", # flake8-comprehensions(提升可读性)
21
+ "SIM", # flake8-simplify(简化代码)
22
+ ]
23
+ ignore = [
24
+ "E501", # line-length
25
+ ]
26
+
27
+ # --- Import Sorting ---
28
+ [lint.isort]
29
+ known-first-party = ["agent-dump"]
30
+ combine-as-imports = true
31
+ force-sort-within-sections = true
32
+
33
+ [format]
34
+ quote-style = "double"
35
+ indent-style = "space"
36
+ line-ending = "lf"
37
+
38
+ # --- 可选:对特定目录放宽规则 ---
39
+ [lint.per-file-ignores]
40
+ "tests/*" = ["S101"] # 测试里允许 assert
41
+ "scripts/*" = ["SIM"] # 脚本可以更随意
@@ -0,0 +1,9 @@
1
+ """
2
+ Agent Dump - AI Coding Assistant Session Export Tool
3
+ """
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["find_db_path", "get_recent_sessions", "export_session", "export_sessions"]
7
+
8
+ from agent_dump.db import find_db_path, get_recent_sessions
9
+ from agent_dump.exporter import export_session, export_sessions
@@ -0,0 +1,8 @@
1
+ """
2
+ Entry point for python -m agent_dump
3
+ """
4
+
5
+ from agent_dump.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ main()
@@ -0,0 +1,91 @@
1
+ """
2
+ Command-line interface for agent-dump
3
+ """
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+
8
+ from agent_dump.db import find_db_path, get_recent_sessions
9
+ from agent_dump.exporter import export_sessions
10
+ from agent_dump.selector import select_sessions_interactive
11
+
12
+
13
+ def main():
14
+ """Main entry point"""
15
+ parser = argparse.ArgumentParser(description="Export agent sessions to JSON")
16
+ parser.add_argument("--days", type=int, default=7, help="Number of days to look back (default: 7)")
17
+ parser.add_argument(
18
+ "--agent",
19
+ type=str,
20
+ default="opencode",
21
+ help="Agent tool name (default: opencode)",
22
+ )
23
+ parser.add_argument(
24
+ "--output",
25
+ type=str,
26
+ default="./sessions",
27
+ help="Output base directory (default: ./sessions)",
28
+ )
29
+ parser.add_argument(
30
+ "--export",
31
+ type=str,
32
+ metavar="IDS",
33
+ help="Export specific session IDs (comma-separated)",
34
+ )
35
+ parser.add_argument("--list", action="store_true", help="List sessions without exporting")
36
+ args = parser.parse_args()
37
+
38
+ print(f"🔍 {args.agent.title()} Session Exporter\n")
39
+
40
+ # Find database
41
+ try:
42
+ db_path = find_db_path()
43
+ print(f"📁 Database: {db_path}\n")
44
+ except FileNotFoundError as e:
45
+ print(f"❌ Error: {e}")
46
+ return
47
+
48
+ # Get recent sessions
49
+ print(f"📊 Loading sessions from the last {args.days} days...")
50
+ sessions = get_recent_sessions(db_path, days=args.days)
51
+ print(f"✓ Found {len(sessions)} sessions\n")
52
+
53
+ if not sessions:
54
+ print("No sessions found.")
55
+ return
56
+
57
+ # List mode
58
+ if args.list:
59
+ print("Available sessions:")
60
+ print("-" * 80)
61
+ for i, session in enumerate(sessions, 1):
62
+ print(f"{i}. {session['title']}")
63
+ print(f" Time: {session['created_formatted']}")
64
+ print(f" ID: {session['id']}")
65
+ print()
66
+ return
67
+
68
+ # Export specific IDs
69
+ if args.export:
70
+ target_ids = [sid.strip() for sid in args.export.split(",")]
71
+ selected = [s for s in sessions if s["id"] in target_ids]
72
+ if not selected:
73
+ print(f"❌ No sessions found with IDs: {args.export}")
74
+ return
75
+ print(f"✓ Selected {len(selected)} session(s) by ID\n")
76
+ else:
77
+ # Interactive selection
78
+ selected = select_sessions_interactive(sessions)
79
+ if not selected:
80
+ print("\n⚠️ No sessions selected. Exiting.")
81
+ return
82
+ print(f"\n✓ Selected {len(selected)} session(s)\n")
83
+
84
+ # Export
85
+ output_dir = Path(args.output) / args.agent
86
+ exported = export_sessions(db_path, selected, output_dir)
87
+ print(f"\n✅ Successfully exported {len(exported)} session(s) to {output_dir}/")
88
+
89
+
90
+ if __name__ == "__main__":
91
+ main()
@@ -0,0 +1,74 @@
1
+ """
2
+ Database operations for agent session export
3
+ """
4
+
5
+ from datetime import datetime, timedelta
6
+ import os
7
+ from pathlib import Path
8
+ import sqlite3
9
+ from typing import Any
10
+
11
+
12
+ def find_db_path() -> Path:
13
+ """Find the OpenCode database path"""
14
+ paths = [
15
+ os.path.expanduser("data/opencode/opencode.db"),
16
+ os.path.expanduser("~/.local/share/opencode/opencode.db"),
17
+ ]
18
+
19
+ for path in paths:
20
+ if os.path.exists(path):
21
+ return Path(path)
22
+
23
+ raise FileNotFoundError("Could not find opencode.db database")
24
+
25
+
26
+ def get_recent_sessions(db_path: Path, days: int = 7) -> list[dict[str, Any]]:
27
+ """Get sessions from the last N days"""
28
+ conn = sqlite3.connect(db_path)
29
+ conn.row_factory = sqlite3.Row
30
+ cursor = conn.cursor()
31
+
32
+ # Calculate timestamp for N days ago (milliseconds)
33
+ cutoff_time = int((datetime.now() - timedelta(days=days)).timestamp() * 1000)
34
+
35
+ # Query sessions with basic info
36
+ cursor.execute(
37
+ """
38
+ SELECT
39
+ s.id,
40
+ s.title,
41
+ s.time_created,
42
+ s.time_updated,
43
+ s.slug,
44
+ s.directory,
45
+ s.version,
46
+ s.summary_files
47
+ FROM session s
48
+ WHERE s.time_created >= ?
49
+ ORDER BY s.time_created DESC
50
+ """,
51
+ (cutoff_time,),
52
+ )
53
+
54
+ sessions = []
55
+ for row in cursor.fetchall():
56
+ # Convert timestamp to readable format
57
+ created_dt = datetime.fromtimestamp(row["time_created"] / 1000)
58
+
59
+ sessions.append(
60
+ {
61
+ "id": row["id"],
62
+ "title": row["title"],
63
+ "time_created": row["time_created"],
64
+ "time_updated": row["time_updated"],
65
+ "created_formatted": created_dt.strftime("%Y-%m-%d %H:%M:%S"),
66
+ "slug": row["slug"],
67
+ "directory": row["directory"],
68
+ "version": row["version"],
69
+ "summary_files": row["summary_files"],
70
+ }
71
+ )
72
+
73
+ conn.close()
74
+ return sessions
@@ -0,0 +1,126 @@
1
+ """
2
+ Session export functionality
3
+ """
4
+
5
+ import json
6
+ from pathlib import Path
7
+ import sqlite3
8
+ from typing import Any
9
+
10
+
11
+ def export_session(db_path: Path, session: dict[str, Any], output_dir: Path) -> Path:
12
+ """Export a single session to JSON"""
13
+ conn = sqlite3.connect(db_path)
14
+ conn.row_factory = sqlite3.Row
15
+ cursor = conn.cursor()
16
+
17
+ # Build session data
18
+ session_data = {
19
+ "id": session["id"],
20
+ "title": session["title"],
21
+ "slug": session["slug"],
22
+ "directory": session["directory"],
23
+ "version": session["version"],
24
+ "time_created": session["time_created"],
25
+ "time_updated": session["time_updated"],
26
+ "summary_files": session["summary_files"],
27
+ "stats": {
28
+ "total_cost": 0,
29
+ "total_input_tokens": 0,
30
+ "total_output_tokens": 0,
31
+ "message_count": 0,
32
+ },
33
+ "messages": [],
34
+ }
35
+
36
+ # Get messages for this session
37
+ cursor.execute(
38
+ """
39
+ SELECT * FROM message
40
+ WHERE session_id = ?
41
+ ORDER BY time_created ASC
42
+ """,
43
+ (session["id"],),
44
+ )
45
+
46
+ for msg_row in cursor.fetchall():
47
+ msg_data = json.loads(msg_row["data"])
48
+
49
+ message = {
50
+ "id": msg_row["id"],
51
+ "role": msg_data.get("role", "unknown"),
52
+ "agent": msg_data.get("agent"),
53
+ "mode": msg_data.get("mode"),
54
+ "model": msg_data.get("modelID"),
55
+ "provider": msg_data.get("providerID"),
56
+ "time_created": msg_row["time_created"],
57
+ "time_completed": msg_data.get("time", {}).get("completed"),
58
+ "tokens": msg_data.get("tokens", {}),
59
+ "cost": msg_data.get("cost", 0),
60
+ "parts": [],
61
+ }
62
+
63
+ # Update session stats
64
+ session_data["stats"]["message_count"] += 1
65
+ if message["cost"]:
66
+ session_data["stats"]["total_cost"] += message["cost"]
67
+ tokens = message["tokens"] or {}
68
+ session_data["stats"]["total_input_tokens"] += tokens.get("input", 0)
69
+ session_data["stats"]["total_output_tokens"] += tokens.get("output", 0)
70
+
71
+ # Get parts for this message
72
+ cursor.execute(
73
+ """
74
+ SELECT * FROM part
75
+ WHERE message_id = ?
76
+ ORDER BY time_created ASC
77
+ """,
78
+ (msg_row["id"],),
79
+ )
80
+
81
+ for part_row in cursor.fetchall():
82
+ part_data = json.loads(part_row["data"])
83
+ part = {
84
+ "type": part_data.get("type"),
85
+ "time_created": part_row["time_created"],
86
+ }
87
+
88
+ if part["type"] == "text" or part["type"] == "reasoning":
89
+ part["text"] = part_data.get("text", "")
90
+ elif part["type"] == "tool":
91
+ part["tool"] = part_data.get("tool")
92
+ part["callID"] = part_data.get("callID")
93
+ part["title"] = part_data.get("title", "")
94
+ part["state"] = part_data.get("state", {})
95
+ elif part["type"] in ["step-start", "step-finish"]:
96
+ part["reason"] = part_data.get("reason")
97
+ part["tokens"] = part_data.get("tokens")
98
+ part["cost"] = part_data.get("cost")
99
+
100
+ message["parts"].append(part)
101
+
102
+ session_data["messages"].append(message)
103
+
104
+ conn.close()
105
+
106
+ # Save to file
107
+ output_path = output_dir / f"{session['id']}.json"
108
+ with open(output_path, "w", encoding="utf-8") as f:
109
+ json.dump(session_data, f, ensure_ascii=False, indent=2)
110
+
111
+ return output_path
112
+
113
+
114
+ def export_sessions(db_path: Path, sessions: list[dict[str, Any]], output_dir: Path) -> list[Path]:
115
+ """Export multiple sessions"""
116
+ output_dir = Path(output_dir)
117
+ output_dir.mkdir(parents=True, exist_ok=True)
118
+
119
+ print("📤 Exporting sessions...")
120
+ exported = []
121
+ for session in sessions:
122
+ output_path = export_session(db_path, session, output_dir)
123
+ exported.append(output_path)
124
+ print(f" ✓ {session['title'][:50]}... → {output_path.name}")
125
+
126
+ return exported
@@ -0,0 +1,75 @@
1
+ """
2
+ Session selection utilities
3
+ """
4
+
5
+ import sys
6
+ from typing import Any
7
+
8
+ import questionary
9
+
10
+
11
+ def is_terminal() -> bool:
12
+ """Check if running in a terminal"""
13
+ return sys.stdin.isatty() and sys.stdout.isatty()
14
+
15
+
16
+ def select_sessions_interactive(sessions: list[dict[str, Any]]) -> list[dict[str, Any]]:
17
+ """Let user select sessions interactively"""
18
+ if not sessions:
19
+ print("No sessions found in the specified time range.")
20
+ return []
21
+
22
+ # If not in terminal, use simple selection
23
+ if not is_terminal():
24
+ return select_sessions_simple(sessions)
25
+
26
+ # Prepare choices for questionary
27
+ choices = []
28
+ for session in sessions:
29
+ # Format: Title (Date) - ID
30
+ label = f"{session['title'][:60]}{'...' if len(session['title']) > 60 else ''}"
31
+ description = f"{session['created_formatted']} | {session['id']}"
32
+
33
+ choices.append(questionary.Choice(title=label, value=session, description=description))
34
+
35
+ # Show interactive checkbox
36
+ selected = questionary.checkbox(
37
+ "选择要导出的会话 (空格选择/取消, 回车确认):",
38
+ choices=choices,
39
+ instruction="\n使用 ↑↓ 移动, 空格 选择/取消, 回车 确认导出",
40
+ ).ask()
41
+
42
+ return selected or []
43
+
44
+
45
+ def select_sessions_simple(sessions: list[dict[str, Any]]) -> list[dict[str, Any]]:
46
+ """Simple selection for non-terminal environments"""
47
+ print("Available sessions:")
48
+ print("-" * 80)
49
+ for i, session in enumerate(sessions, 1):
50
+ print(f"{i}. {session['title'][:60]}")
51
+ print(f" {session['created_formatted']} | {session['id']}")
52
+ print()
53
+
54
+ print("Enter session numbers to export (comma-separated, e.g., '1,3,5' or 'all'):")
55
+ try:
56
+ selection = input("> ").strip()
57
+ except EOFError:
58
+ print("\n⚠️ No input provided. Exiting.")
59
+ return []
60
+
61
+ if selection.lower() == "all":
62
+ return sessions
63
+
64
+ try:
65
+ indices = [int(x.strip()) - 1 for x in selection.split(",")]
66
+ selected = []
67
+ for idx in indices:
68
+ if 0 <= idx < len(sessions):
69
+ selected.append(sessions[idx])
70
+ else:
71
+ print(f"⚠️ Invalid selection: {idx + 1}")
72
+ return selected
73
+ except ValueError:
74
+ print("⚠️ Invalid input. Please enter numbers separated by commas.")
75
+ return []
@@ -0,0 +1,145 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.14"
4
+
5
+ [[package]]
6
+ name = "agent-dump"
7
+ version = "0.1.0"
8
+ source = { editable = "." }
9
+ dependencies = [
10
+ { name = "prompt-toolkit" },
11
+ { name = "questionary" },
12
+ ]
13
+
14
+ [package.dev-dependencies]
15
+ dev = [
16
+ { name = "pyright" },
17
+ { name = "ruff" },
18
+ { name = "ty" },
19
+ ]
20
+
21
+ [package.metadata]
22
+ requires-dist = [
23
+ { name = "prompt-toolkit", specifier = ">=3.0.0" },
24
+ { name = "questionary", specifier = ">=2.1.1" },
25
+ ]
26
+
27
+ [package.metadata.requires-dev]
28
+ dev = [
29
+ { name = "pyright", specifier = ">=1.1.408" },
30
+ { name = "ruff", specifier = ">=0.15.2" },
31
+ { name = "ty", specifier = ">=0.0.18" },
32
+ ]
33
+
34
+ [[package]]
35
+ name = "nodeenv"
36
+ version = "1.10.0"
37
+ source = { registry = "https://pypi.org/simple" }
38
+ sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
39
+ wheels = [
40
+ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
41
+ ]
42
+
43
+ [[package]]
44
+ name = "prompt-toolkit"
45
+ version = "3.0.52"
46
+ source = { registry = "https://pypi.org/simple" }
47
+ dependencies = [
48
+ { name = "wcwidth" },
49
+ ]
50
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
51
+ wheels = [
52
+ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
53
+ ]
54
+
55
+ [[package]]
56
+ name = "pyright"
57
+ version = "1.1.408"
58
+ source = { registry = "https://pypi.org/simple" }
59
+ dependencies = [
60
+ { name = "nodeenv" },
61
+ { name = "typing-extensions" },
62
+ ]
63
+ sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" }
64
+ wheels = [
65
+ { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" },
66
+ ]
67
+
68
+ [[package]]
69
+ name = "questionary"
70
+ version = "2.1.1"
71
+ source = { registry = "https://pypi.org/simple" }
72
+ dependencies = [
73
+ { name = "prompt-toolkit" },
74
+ ]
75
+ sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" }
76
+ wheels = [
77
+ { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" },
78
+ ]
79
+
80
+ [[package]]
81
+ name = "ruff"
82
+ version = "0.15.2"
83
+ source = { registry = "https://pypi.org/simple" }
84
+ sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" }
85
+ wheels = [
86
+ { url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" },
87
+ { url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" },
88
+ { url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" },
89
+ { url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" },
90
+ { url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" },
91
+ { url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" },
92
+ { url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" },
93
+ { url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" },
94
+ { url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" },
95
+ { url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" },
96
+ { url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" },
97
+ { url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" },
98
+ { url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" },
99
+ { url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" },
100
+ { url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" },
101
+ { url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" },
102
+ { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" },
103
+ ]
104
+
105
+ [[package]]
106
+ name = "ty"
107
+ version = "0.0.18"
108
+ source = { registry = "https://pypi.org/simple" }
109
+ sdist = { url = "https://files.pythonhosted.org/packages/74/15/9682700d8d60fdca7afa4febc83a2354b29cdcd56e66e19c92b521db3b39/ty-0.0.18.tar.gz", hash = "sha256:04ab7c3db5dcbcdac6ce62e48940d3a0124f377c05499d3f3e004e264ae94b83", size = 5214774, upload-time = "2026-02-20T21:51:31.173Z" }
110
+ wheels = [
111
+ { url = "https://files.pythonhosted.org/packages/ae/d8/920460d4c22ea68fcdeb0b2fb53ea2aeb9c6d7875bde9278d84f2ac767b6/ty-0.0.18-py3-none-linux_armv6l.whl", hash = "sha256:4e5e91b0a79857316ef893c5068afc4b9872f9d257627d9bc8ac4d2715750d88", size = 10280825, upload-time = "2026-02-20T21:51:25.03Z" },
112
+ { url = "https://files.pythonhosted.org/packages/83/56/62587de582d3d20d78fcdddd0594a73822ac5a399a12ef512085eb7a4de6/ty-0.0.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee0e578b3f8416e2d5416da9553b78fd33857868aa1384cb7fefeceee5ff102d", size = 10118324, upload-time = "2026-02-20T21:51:22.27Z" },
113
+ { url = "https://files.pythonhosted.org/packages/2f/2d/dbdace8d432a0755a7417f659bfd5b8a4261938ecbdfd7b42f4c454f5aa9/ty-0.0.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3f7a0487d36b939546a91d141f7fc3dbea32fab4982f618d5b04dc9d5b6da21e", size = 9605861, upload-time = "2026-02-20T21:51:16.066Z" },
114
+ { url = "https://files.pythonhosted.org/packages/6b/d9/de11c0280f778d5fc571393aada7fe9b8bc1dd6a738f2e2c45702b8b3150/ty-0.0.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5e2fa8d45f57ca487a470e4bf66319c09b561150e98ae2a6b1a97ef04c1a4eb", size = 10092701, upload-time = "2026-02-20T21:51:26.862Z" },
115
+ { url = "https://files.pythonhosted.org/packages/0f/94/068d4d591d791041732171e7b63c37a54494b2e7d28e88d2167eaa9ad875/ty-0.0.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d75652e9e937f7044b1aca16091193e7ef11dac1c7ec952b7fb8292b7ba1f5f2", size = 10109203, upload-time = "2026-02-20T21:51:11.59Z" },
116
+ { url = "https://files.pythonhosted.org/packages/34/e4/526a4aa56dc0ca2569aaa16880a1ab105c3b416dd70e87e25a05688999f3/ty-0.0.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:563c868edceb8f6ddd5e91113c17d3676b028f0ed380bdb3829b06d9beb90e58", size = 10614200, upload-time = "2026-02-20T21:51:20.298Z" },
117
+ { url = "https://files.pythonhosted.org/packages/fd/3d/b68ab20a34122a395880922587fbfc3adf090d22e0fb546d4d20fe8c2621/ty-0.0.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:502e2a1f948bec563a0454fc25b074bf5cf041744adba8794d024277e151d3b0", size = 11153232, upload-time = "2026-02-20T21:51:14.121Z" },
118
+ { url = "https://files.pythonhosted.org/packages/68/ea/678243c042343fcda7e6af36036c18676c355878dcdcd517639586d2cf9e/ty-0.0.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc881dea97021a3aa29134a476937fd8054775c4177d01b94db27fcfb7aab65b", size = 10832934, upload-time = "2026-02-20T21:51:32.92Z" },
119
+ { url = "https://files.pythonhosted.org/packages/d8/bd/7f8d647cef8b7b346c0163230a37e903c7461c7248574840b977045c77df/ty-0.0.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:421fcc3bc64cab56f48edb863c7c1c43649ec4d78ff71a1acb5366ad723b6021", size = 10700888, upload-time = "2026-02-20T21:51:09.673Z" },
120
+ { url = "https://files.pythonhosted.org/packages/6e/06/cb3620dc48c5d335ba7876edfef636b2f4498eff4a262ff90033b9e88408/ty-0.0.18-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0fe5038a7136a0e638a2fb1ad06e3d3c4045314c6ba165c9c303b9aeb4623d6c", size = 10078965, upload-time = "2026-02-20T21:51:07.678Z" },
121
+ { url = "https://files.pythonhosted.org/packages/60/27/c77a5a84533fa3b685d592de7b4b108eb1f38851c40fac4e79cc56ec7350/ty-0.0.18-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d123600a52372677613a719bbb780adeb9b68f47fb5f25acb09171de390e0035", size = 10134659, upload-time = "2026-02-20T21:51:18.311Z" },
122
+ { url = "https://files.pythonhosted.org/packages/43/6e/60af6b88c73469e628ba5253a296da6984e0aa746206f3034c31f1a04ed1/ty-0.0.18-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb4bc11d32a1bf96a829bf6b9696545a30a196ac77bbc07cc8d3dfee35e03723", size = 10297494, upload-time = "2026-02-20T21:51:39.631Z" },
123
+ { url = "https://files.pythonhosted.org/packages/33/90/612dc0b68224c723faed6adac2bd3f930a750685db76dfe17e6b9e534a83/ty-0.0.18-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dda2efbf374ba4cd704053d04e32f2f784e85c2ddc2400006b0f96f5f7e4b667", size = 10791944, upload-time = "2026-02-20T21:51:37.13Z" },
124
+ { url = "https://files.pythonhosted.org/packages/0d/da/f4ada0fd08a9e4138fe3fd2bcd3797753593f423f19b1634a814b9b2a401/ty-0.0.18-py3-none-win32.whl", hash = "sha256:c5768607c94977dacddc2f459ace6a11a408a0f57888dd59abb62d28d4fee4f7", size = 9677964, upload-time = "2026-02-20T21:51:42.039Z" },
125
+ { url = "https://files.pythonhosted.org/packages/5e/fa/090ed9746e5c59fc26d8f5f96dc8441825171f1f47752f1778dad690b08b/ty-0.0.18-py3-none-win_amd64.whl", hash = "sha256:b78d0fa1103d36fc2fce92f2092adace52a74654ab7884d54cdaec8eb5016a4d", size = 10636576, upload-time = "2026-02-20T21:51:29.159Z" },
126
+ { url = "https://files.pythonhosted.org/packages/92/4f/5dd60904c8105cda4d0be34d3a446c180933c76b84ae0742e58f02133713/ty-0.0.18-py3-none-win_arm64.whl", hash = "sha256:01770c3c82137c6b216aa3251478f0b197e181054ee92243772de553d3586398", size = 10095449, upload-time = "2026-02-20T21:51:34.914Z" },
127
+ ]
128
+
129
+ [[package]]
130
+ name = "typing-extensions"
131
+ version = "4.15.0"
132
+ source = { registry = "https://pypi.org/simple" }
133
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
134
+ wheels = [
135
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
136
+ ]
137
+
138
+ [[package]]
139
+ name = "wcwidth"
140
+ version = "0.6.0"
141
+ source = { registry = "https://pypi.org/simple" }
142
+ sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" }
143
+ wheels = [
144
+ { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" },
145
+ ]