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 +9 -0
- agent_dump/__main__.py +8 -0
- agent_dump/cli.py +91 -0
- agent_dump/db.py +74 -0
- agent_dump/exporter.py +126 -0
- agent_dump/selector.py +75 -0
- agent_dump-0.1.0.dist-info/METADATA +165 -0
- agent_dump-0.1.0.dist-info/RECORD +11 -0
- agent_dump-0.1.0.dist-info/WHEEL +4 -0
- agent_dump-0.1.0.dist-info/entry_points.txt +2 -0
- agent_dump-0.1.0.dist-info/licenses/LICENSE +21 -0
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
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,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.
|