agent-dump 0.1.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.
agent_dump/__init__.py ADDED
@@ -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
agent_dump/__main__.py ADDED
@@ -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()
agent_dump/cli.py ADDED
@@ -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()
agent_dump/db.py ADDED
@@ -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
agent_dump/exporter.py ADDED
@@ -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
agent_dump/selector.py ADDED
@@ -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,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,11 @@
1
+ agent_dump/__init__.py,sha256=CLmupjtbZNOU8E8zFMlm1qO7rffivPhP2AEH7_aDtN0,296
2
+ agent_dump/__main__.py,sha256=ytB3s7vhkD3unQ__hRErh1gPHHSGFvElzphEguPyFSg,117
3
+ agent_dump/cli.py,sha256=VHOuCfmXMwu41J74C3arN0Gdjgux7OXJA70lOqC96I4,2814
4
+ agent_dump/db.py,sha256=Cz8C2TYlUbtoBgQWE-RgXQNIrflm2UySZGZ8v83Nfwg,2028
5
+ agent_dump/exporter.py,sha256=Ks_myYHe79OHkkPe-byssoSgsepLd7NZFwGRTjtmDdU,4071
6
+ agent_dump/selector.py,sha256=E93js3DDuCqsTQaYK5dz_BE8HBrMgH8eHADdYGVB7aQ,2368
7
+ agent_dump-0.1.0.dist-info/METADATA,sha256=LxPPr9WRtOEl5nG2rgzqZ3O2-MW5NuBe_vSC4fV2iXs,4496
8
+ agent_dump-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
9
+ agent_dump-0.1.0.dist-info/entry_points.txt,sha256=uiaMVYf0AejYHNGi_nJDsveFMk5rwWd--3ugoKq8f4o,51
10
+ agent_dump-0.1.0.dist-info/licenses/LICENSE,sha256=0sQoYHhVUn3z4TLBQaZI1eBnejacFvxuAadX3YvnoEw,1081
11
+ agent_dump-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agent-dump = agent_dump.cli:main
@@ -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.