opencode-db 0.3.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.
- cli.py +70 -0
- cmd_costs.py +197 -0
- cmd_delete.py +58 -0
- cmd_export.py +189 -0
- cmd_help.py +210 -0
- cmd_info.py +152 -0
- cmd_list.py +124 -0
- cmd_projects.py +82 -0
- cmd_prune.py +127 -0
- cmd_search.py +111 -0
- cmd_stats.py +115 -0
- cmd_todos.py +90 -0
- cmd_tree.py +95 -0
- cmd_vacuum.py +67 -0
- commands/__init__.py +51 -0
- config.py +14 -0
- db.py +374 -0
- formatters.py +208 -0
- i18n.py +532 -0
- opencode_db-0.3.0.dist-info/METADATA +7 -0
- opencode_db-0.3.0.dist-info/RECORD +26 -0
- opencode_db-0.3.0.dist-info/WHEEL +5 -0
- opencode_db-0.3.0.dist-info/entry_points.txt +2 -0
- opencode_db-0.3.0.dist-info/licenses/LICENSE +25 -0
- opencode_db-0.3.0.dist-info/top_level.txt +20 -0
- utils.py +115 -0
cmd_search.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Команда search: поиск по тексту сообщений и частей."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from db import resolve_session_id
|
|
7
|
+
from i18n import _
|
|
8
|
+
from utils import format_ts
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register(subparsers) -> None:
|
|
12
|
+
p = subparsers.add_parser("search", help=_("help.cmd.search"))
|
|
13
|
+
p.add_argument("query", help="Search text")
|
|
14
|
+
p.add_argument("--session", type=str, help="Limit to session")
|
|
15
|
+
p.add_argument("--limit", type=int, default=30, help="Max results")
|
|
16
|
+
p.add_argument("--json", action="store_true", help="JSON output")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _search_in_part_data(
|
|
20
|
+
data, query_lower
|
|
21
|
+
) -> tuple[Literal[False], Literal[""]] | tuple[Literal[True], str]:
|
|
22
|
+
"""Ищет query в part.data (JSON), проверяя текстовые поля."""
|
|
23
|
+
if not data:
|
|
24
|
+
return False, ""
|
|
25
|
+
try:
|
|
26
|
+
parsed = json.loads(data) if isinstance(data, str) else data
|
|
27
|
+
except (json.JSONDecodeError, TypeError):
|
|
28
|
+
return False, ""
|
|
29
|
+
|
|
30
|
+
text = parsed.get("text", "")
|
|
31
|
+
if isinstance(text, str) and query_lower in text.lower():
|
|
32
|
+
return True, text[:200]
|
|
33
|
+
|
|
34
|
+
state = parsed.get("state", {})
|
|
35
|
+
if isinstance(state, dict):
|
|
36
|
+
t_input = state.get("input", {})
|
|
37
|
+
t_output = state.get("output", "")
|
|
38
|
+
if isinstance(t_input, dict):
|
|
39
|
+
for v in t_input.values():
|
|
40
|
+
if isinstance(v, str) and query_lower in v.lower():
|
|
41
|
+
return True, f"[tool] {str(v)[:200]}"
|
|
42
|
+
if isinstance(t_output, str) and query_lower in t_output.lower():
|
|
43
|
+
return True, f"[output] {t_output[:200]}"
|
|
44
|
+
|
|
45
|
+
return False, ""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def run(args, db) -> Literal[0]:
|
|
49
|
+
query_lower = args.query.lower()
|
|
50
|
+
|
|
51
|
+
conditions = []
|
|
52
|
+
params = []
|
|
53
|
+
|
|
54
|
+
if args.session:
|
|
55
|
+
full_id = resolve_session_id(db, args.session)
|
|
56
|
+
conditions.append("p.session_id = ?")
|
|
57
|
+
params.append(full_id)
|
|
58
|
+
|
|
59
|
+
where = " AND ".join(conditions) if conditions else "1=1"
|
|
60
|
+
|
|
61
|
+
rows = db.execute(
|
|
62
|
+
f"""
|
|
63
|
+
SELECT p.id, p.session_id, p.message_id, p.data,
|
|
64
|
+
m.time_created,
|
|
65
|
+
json_extract(m.data, '$.role') as role
|
|
66
|
+
FROM part p
|
|
67
|
+
JOIN message m ON m.id = p.message_id
|
|
68
|
+
WHERE {where}
|
|
69
|
+
AND p.data IS NOT NULL
|
|
70
|
+
ORDER BY p.time_created DESC
|
|
71
|
+
LIMIT ?
|
|
72
|
+
""",
|
|
73
|
+
(*params, args.limit * 5),
|
|
74
|
+
).fetchall()
|
|
75
|
+
|
|
76
|
+
results = []
|
|
77
|
+
for r in rows:
|
|
78
|
+
found, snippet = _search_in_part_data(r["data"], query_lower)
|
|
79
|
+
if found:
|
|
80
|
+
results.append(
|
|
81
|
+
{
|
|
82
|
+
"session_id": r["session_id"][:24],
|
|
83
|
+
"message_id": r["message_id"][:12],
|
|
84
|
+
"role": r["role"],
|
|
85
|
+
"time": format_ts(r["time_created"]) if r["time_created"] else "—",
|
|
86
|
+
"snippet": snippet,
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
if len(results) >= args.limit:
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
if args.json:
|
|
93
|
+
from formatters import print_json
|
|
94
|
+
|
|
95
|
+
print_json(results)
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
if not results:
|
|
99
|
+
print(_("search.none", query=args.query))
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
print(_("search.header", query=args.query))
|
|
103
|
+
print(f" {'─' * 60}")
|
|
104
|
+
|
|
105
|
+
for r in results:
|
|
106
|
+
print(f" [{r['time']}] {r['role']} {r['session_id']} msg:{r['message_id']}")
|
|
107
|
+
print(f" {r['snippet']}")
|
|
108
|
+
print()
|
|
109
|
+
|
|
110
|
+
print(_("search.found", n=len(results)))
|
|
111
|
+
return 0
|
cmd_stats.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Команда stats: общая статистика по базе данных."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from db import parse_model
|
|
6
|
+
from i18n import _
|
|
7
|
+
from utils import format_cost, format_tokens, format_ts
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register(subparsers) -> None:
|
|
11
|
+
p = subparsers.add_parser("stats", help=_("help.cmd.stats"))
|
|
12
|
+
p.add_argument("--json", action="store_true", help="JSON output")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run(args, db) -> Literal[0]:
|
|
16
|
+
session_count = db.execute("SELECT COUNT(*) FROM session").fetchone()[0]
|
|
17
|
+
message_count = db.execute("SELECT COUNT(*) FROM message").fetchone()[0]
|
|
18
|
+
part_count = db.execute("SELECT COUNT(*) FROM part").fetchone()[0]
|
|
19
|
+
project_count = db.execute("SELECT COUNT(*) FROM project").fetchone()[0]
|
|
20
|
+
todo_count = db.execute("SELECT COUNT(*) FROM todo").fetchone()[0]
|
|
21
|
+
|
|
22
|
+
totals = db.execute("""
|
|
23
|
+
SELECT
|
|
24
|
+
COALESCE(SUM(cost), 0) as total_cost,
|
|
25
|
+
COALESCE(SUM(tokens_input), 0) as total_ti,
|
|
26
|
+
COALESCE(SUM(tokens_output), 0) as total_to,
|
|
27
|
+
COALESCE(SUM(tokens_reasoning), 0) as total_tr,
|
|
28
|
+
COALESCE(SUM(tokens_cache_read), 0) as total_cr,
|
|
29
|
+
COALESCE(SUM(tokens_cache_write), 0) as total_cw
|
|
30
|
+
FROM session
|
|
31
|
+
""").fetchone()
|
|
32
|
+
|
|
33
|
+
first_last = db.execute("""
|
|
34
|
+
SELECT MIN(time_created) as first_ts, MAX(time_created) as last_ts
|
|
35
|
+
FROM session
|
|
36
|
+
""").fetchone()
|
|
37
|
+
|
|
38
|
+
top_models = db.execute("""
|
|
39
|
+
SELECT model, COUNT(*) as cnt, SUM(cost) as cost
|
|
40
|
+
FROM session
|
|
41
|
+
WHERE model IS NOT NULL
|
|
42
|
+
GROUP BY model
|
|
43
|
+
ORDER BY cnt DESC
|
|
44
|
+
LIMIT 5
|
|
45
|
+
""").fetchall()
|
|
46
|
+
|
|
47
|
+
agents = db.execute("""
|
|
48
|
+
SELECT agent, COUNT(*) as cnt
|
|
49
|
+
FROM session
|
|
50
|
+
WHERE agent IS NOT NULL
|
|
51
|
+
GROUP BY agent
|
|
52
|
+
ORDER BY cnt DESC
|
|
53
|
+
""").fetchall()
|
|
54
|
+
|
|
55
|
+
if args.json:
|
|
56
|
+
from formatters import print_json
|
|
57
|
+
|
|
58
|
+
data = {
|
|
59
|
+
"sessions": session_count,
|
|
60
|
+
"messages": message_count,
|
|
61
|
+
"parts": part_count,
|
|
62
|
+
"projects": project_count,
|
|
63
|
+
"todos": todo_count,
|
|
64
|
+
"cost": totals["total_cost"],
|
|
65
|
+
"tokens_input": totals["total_ti"],
|
|
66
|
+
"tokens_output": totals["total_to"],
|
|
67
|
+
"tokens_reasoning": totals["total_tr"],
|
|
68
|
+
"tokens_cache_read": totals["total_cr"],
|
|
69
|
+
"tokens_cache_write": totals["total_cw"],
|
|
70
|
+
"tokens_total": totals["total_ti"] + totals["total_to"] + totals["total_tr"],
|
|
71
|
+
"first_session": format_ts(first_last["first_ts"]) if first_last["first_ts"] else None,
|
|
72
|
+
"last_session": format_ts(first_last["last_ts"]) if first_last["last_ts"] else None,
|
|
73
|
+
"top_models": [
|
|
74
|
+
{"model": parse_model(m), "count": m["cnt"], "cost": m["cost"]} for m in top_models
|
|
75
|
+
],
|
|
76
|
+
"agents": [{"agent": a["agent"], "count": a["cnt"]} for a in agents],
|
|
77
|
+
}
|
|
78
|
+
print_json(data)
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
total_tokens = totals["total_ti"] + totals["total_to"] + totals["total_tr"]
|
|
82
|
+
first_date = format_ts(first_last["first_ts"], "%Y-%m-%d") if first_last["first_ts"] else "—"
|
|
83
|
+
last_date = format_ts(first_last["last_ts"], "%Y-%m-%d") if first_last["last_ts"] else "—"
|
|
84
|
+
|
|
85
|
+
print(f"\n {'=' * 50}")
|
|
86
|
+
print(f" {_('stats.header')}")
|
|
87
|
+
print(f" {'=' * 50}")
|
|
88
|
+
print(f" {_('stats.projects')}: {project_count}")
|
|
89
|
+
print(f" {_('stats.sessions')}: {session_count}")
|
|
90
|
+
print(f" {_('stats.messages')}: {message_count}")
|
|
91
|
+
print(f" {_('stats.parts')}: {part_count}")
|
|
92
|
+
print(f" {_('stats.todos')}: {todo_count}")
|
|
93
|
+
print(f" {'─' * 50}")
|
|
94
|
+
print(f" {_('stats.first_session')}: {first_date}")
|
|
95
|
+
print(f" {_('stats.last_session')}: {last_date}")
|
|
96
|
+
print(f" {'─' * 50}")
|
|
97
|
+
print(f" {_('stats.tokens_input')}: {format_tokens(totals['total_ti'])}")
|
|
98
|
+
print(f" {_('stats.tokens_output')}: {format_tokens(totals['total_to'])}")
|
|
99
|
+
print(f" {_('stats.tokens_reasoning')}: {format_tokens(totals['total_tr'])}")
|
|
100
|
+
print(f" {_('stats.tokens_total')}: {format_tokens(total_tokens)}")
|
|
101
|
+
print(f" {_('stats.cost')}: {format_cost(totals['total_cost'])}")
|
|
102
|
+
|
|
103
|
+
if top_models:
|
|
104
|
+
print(f"\n {_('stats.top_models')}:")
|
|
105
|
+
for m in top_models:
|
|
106
|
+
model_str = parse_model(m)
|
|
107
|
+
print(f" {m['cnt']:4d}× {model_str:30s} ({format_cost(m['cost'] or 0)})")
|
|
108
|
+
|
|
109
|
+
if agents:
|
|
110
|
+
print(f"\n {_('stats.agents')}:")
|
|
111
|
+
for a in agents:
|
|
112
|
+
print(f" {a['cnt']:4d}× {a['agent']}")
|
|
113
|
+
|
|
114
|
+
print()
|
|
115
|
+
return 0
|
cmd_todos.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Команда todos: просмотр задач (todo) из сессий."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from db import resolve_session_id
|
|
6
|
+
from i18n import _
|
|
7
|
+
from utils import format_ts
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register(subparsers) -> None:
|
|
11
|
+
p = subparsers.add_parser("todos", help=_("help.cmd.todos"))
|
|
12
|
+
p.add_argument("session_id", nargs="?", help="Session ID")
|
|
13
|
+
p.add_argument(
|
|
14
|
+
"--status",
|
|
15
|
+
choices=["pending", "completed", "cancelled"],
|
|
16
|
+
help="Filter by status",
|
|
17
|
+
)
|
|
18
|
+
p.add_argument("--limit", type=int, default=30, help="Max entries")
|
|
19
|
+
p.add_argument("--json", action="store_true", help="JSON output")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run(args, db) -> Literal[0]:
|
|
23
|
+
conditions = []
|
|
24
|
+
params = []
|
|
25
|
+
|
|
26
|
+
if args.session_id:
|
|
27
|
+
full_id = resolve_session_id(db, args.session_id)
|
|
28
|
+
conditions.append("t.session_id = ?")
|
|
29
|
+
params.append(full_id)
|
|
30
|
+
|
|
31
|
+
if args.status:
|
|
32
|
+
conditions.append("t.status = ?")
|
|
33
|
+
params.append(args.status)
|
|
34
|
+
|
|
35
|
+
where = " AND ".join(conditions) if conditions else "1=1"
|
|
36
|
+
|
|
37
|
+
rows = db.execute(
|
|
38
|
+
f"""
|
|
39
|
+
SELECT t.session_id, t.content, t.status, t.priority,
|
|
40
|
+
t.position, t.time_created, t.time_updated,
|
|
41
|
+
s.title as session_title
|
|
42
|
+
FROM todo t
|
|
43
|
+
JOIN session s ON s.id = t.session_id
|
|
44
|
+
WHERE {where}
|
|
45
|
+
ORDER BY t.time_created DESC
|
|
46
|
+
LIMIT ?
|
|
47
|
+
""",
|
|
48
|
+
(*params, args.limit),
|
|
49
|
+
).fetchall()
|
|
50
|
+
|
|
51
|
+
if args.json:
|
|
52
|
+
from formatters import print_json
|
|
53
|
+
|
|
54
|
+
data = []
|
|
55
|
+
for r in rows:
|
|
56
|
+
data.append(
|
|
57
|
+
{
|
|
58
|
+
"session_id": r["session_id"][:24],
|
|
59
|
+
"session_title": r["session_title"],
|
|
60
|
+
"content": r["content"],
|
|
61
|
+
"status": r["status"],
|
|
62
|
+
"priority": r["priority"],
|
|
63
|
+
"created": format_ts(r["time_created"]) if r["time_created"] else None,
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
print_json(data)
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
if not rows:
|
|
70
|
+
print(_("todos.none"))
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
status_icon = {"pending": "⏳", "completed": "✅", "cancelled": "❌"}
|
|
74
|
+
priority_mark = {"high": " 🔴", "medium": "", "low": " 🟢"}
|
|
75
|
+
|
|
76
|
+
print(_("todos.header", n=len(rows)))
|
|
77
|
+
print(f" {'─' * 55}")
|
|
78
|
+
|
|
79
|
+
for r in rows:
|
|
80
|
+
icon = status_icon.get(r["status"], "📝")
|
|
81
|
+
prio = priority_mark.get(r["priority"], "")
|
|
82
|
+
created = format_ts(r["time_created"], "%Y-%m-%d") if r["time_created"] else "—"
|
|
83
|
+
title = r["session_title"] or "—"
|
|
84
|
+
|
|
85
|
+
print(f" {icon} {r['content'][:60]}{prio}")
|
|
86
|
+
print(f" {_('todos.session')}: {title[:40]}")
|
|
87
|
+
print(f" {_('todos.status')}: {r['status']}, {_('todos.created')}: {created}")
|
|
88
|
+
print()
|
|
89
|
+
|
|
90
|
+
return 0
|
cmd_tree.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Команда tree: дерево ветвления сессий."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from db import get_session_title, resolve_session_id
|
|
6
|
+
from i18n import _
|
|
7
|
+
from utils import format_ts
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register(subparsers) -> None:
|
|
11
|
+
p = subparsers.add_parser("tree", help=_("help.cmd.tree"))
|
|
12
|
+
p.add_argument("session_id", nargs="?", help="Start from this session")
|
|
13
|
+
p.add_argument("--project", type=str, help="Filter by project")
|
|
14
|
+
p.add_argument("--depth", type=int, default=5, help="Max depth")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _build_tree(db, parent_id, depth, max_depth, indent=0, visited=None):
|
|
18
|
+
if visited is None:
|
|
19
|
+
visited = set()
|
|
20
|
+
if depth > max_depth:
|
|
21
|
+
return []
|
|
22
|
+
|
|
23
|
+
lines = []
|
|
24
|
+
|
|
25
|
+
if parent_id is None:
|
|
26
|
+
rows = db.execute("""
|
|
27
|
+
SELECT id, title, time_created
|
|
28
|
+
FROM session
|
|
29
|
+
WHERE parent_id IS NULL
|
|
30
|
+
ORDER BY time_created
|
|
31
|
+
""").fetchall()
|
|
32
|
+
else:
|
|
33
|
+
rows = db.execute(
|
|
34
|
+
"""
|
|
35
|
+
SELECT id, title, time_created
|
|
36
|
+
FROM session
|
|
37
|
+
WHERE parent_id = ?
|
|
38
|
+
ORDER BY time_created
|
|
39
|
+
""",
|
|
40
|
+
(parent_id,),
|
|
41
|
+
).fetchall()
|
|
42
|
+
|
|
43
|
+
for r in rows:
|
|
44
|
+
if r["id"] in visited:
|
|
45
|
+
continue
|
|
46
|
+
visited.add(r["id"])
|
|
47
|
+
|
|
48
|
+
prefix = " " * indent + ("└─ " if indent > 0 else "")
|
|
49
|
+
title = get_session_title(r)
|
|
50
|
+
created = format_ts(r["time_created"], "%Y-%m-%d")
|
|
51
|
+
lines.append(f"{prefix}{r['id'][:24]} {created} {title}")
|
|
52
|
+
|
|
53
|
+
lines.extend(_build_tree(db, r["id"], depth + 1, max_depth, indent + 1, visited))
|
|
54
|
+
|
|
55
|
+
return lines
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run(args, db) -> Literal[0]:
|
|
59
|
+
if args.session_id:
|
|
60
|
+
full_id = resolve_session_id(db, args.session_id)
|
|
61
|
+
info = db.execute(
|
|
62
|
+
"SELECT id, title, time_created FROM session WHERE id = ?", (full_id,)
|
|
63
|
+
).fetchone()
|
|
64
|
+
|
|
65
|
+
title = get_session_title(info)
|
|
66
|
+
print(f"\n {_('tree.header', title=title)}")
|
|
67
|
+
print(f" {'─' * 55}")
|
|
68
|
+
|
|
69
|
+
lines = _build_tree(db, full_id, 1, args.depth, 0)
|
|
70
|
+
for line in lines:
|
|
71
|
+
print(f" {line}")
|
|
72
|
+
|
|
73
|
+
elif args.project:
|
|
74
|
+
print(f"\n {_('tree.header_project', project=args.project)}")
|
|
75
|
+
print(f" {'─' * 55}")
|
|
76
|
+
|
|
77
|
+
lines = _build_tree(db, None, 1, args.depth, 0)
|
|
78
|
+
project_lines = []
|
|
79
|
+
for line in lines:
|
|
80
|
+
sid = line.split(" ")[0].strip().lstrip("└─ ")
|
|
81
|
+
row = db.execute("SELECT project_id FROM session WHERE id = ?", (sid,)).fetchone()
|
|
82
|
+
if row and row["project_id"] == args.project:
|
|
83
|
+
project_lines.append(line)
|
|
84
|
+
for line in project_lines:
|
|
85
|
+
print(f" {line}")
|
|
86
|
+
|
|
87
|
+
else:
|
|
88
|
+
print(f"\n {_('tree.header_all')}")
|
|
89
|
+
print(f" {'─' * 55}")
|
|
90
|
+
lines = _build_tree(db, None, 1, args.depth, 0)
|
|
91
|
+
for line in lines:
|
|
92
|
+
print(f" {line}")
|
|
93
|
+
|
|
94
|
+
print()
|
|
95
|
+
return 0
|
cmd_vacuum.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Команда vacuum: оптимизация базы данных."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from config import OPENCODE_DB
|
|
7
|
+
from i18n import _
|
|
8
|
+
from utils import confirm
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register(subparsers) -> None:
|
|
12
|
+
p = subparsers.add_parser("vacuum", help=_("help.cmd.vacuum"))
|
|
13
|
+
p.add_argument("--force", "-f", action="store_true", help="Skip confirmation")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run(args, db) -> Literal[0] | Literal[1]:
|
|
17
|
+
try:
|
|
18
|
+
size_before = os.path.getsize(OPENCODE_DB)
|
|
19
|
+
except OSError:
|
|
20
|
+
size_before = 0
|
|
21
|
+
|
|
22
|
+
size_before_mb = size_before / (1024 * 1024)
|
|
23
|
+
|
|
24
|
+
print(f"\n {_('vacuum.header')}")
|
|
25
|
+
print(f" {'─' * 40}")
|
|
26
|
+
print(f" {_('vacuum.size_before')}: {size_before_mb:.1f} MB")
|
|
27
|
+
print(f" {_('vacuum.path')}: {OPENCODE_DB}")
|
|
28
|
+
|
|
29
|
+
if not args.force and not confirm(_("vacuum.confirm")):
|
|
30
|
+
print(_("vacuum.canceled"))
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
print(f" {_('vacuum.vacuum')}", end=" ", flush=True)
|
|
34
|
+
try:
|
|
35
|
+
db.execute("VACUUM")
|
|
36
|
+
print(_("vacuum.done"))
|
|
37
|
+
except Exception as e:
|
|
38
|
+
print(_("vacuum.error", error=e))
|
|
39
|
+
return 1
|
|
40
|
+
|
|
41
|
+
print(f" {_('vacuum.reindex')}", end=" ", flush=True)
|
|
42
|
+
try:
|
|
43
|
+
db.execute("REINDEX")
|
|
44
|
+
print(_("vacuum.done"))
|
|
45
|
+
except Exception as e:
|
|
46
|
+
print(_("vacuum.error", error=e))
|
|
47
|
+
|
|
48
|
+
print(f" {_('vacuum.analyze')}", end=" ", flush=True)
|
|
49
|
+
try:
|
|
50
|
+
db.execute("ANALYZE")
|
|
51
|
+
print(_("vacuum.done"))
|
|
52
|
+
except Exception as e:
|
|
53
|
+
print(_("vacuum.error", error=e))
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
size_after = os.path.getsize(OPENCODE_DB)
|
|
57
|
+
size_after_mb = size_after / (1024 * 1024)
|
|
58
|
+
saved = size_before - size_after
|
|
59
|
+
saved_mb = saved / (1024 * 1024)
|
|
60
|
+
print(f"\n {_('vacuum.size_after')}: {size_after_mb:.1f} MB")
|
|
61
|
+
if saved > 0:
|
|
62
|
+
print(f" {_('vacuum.freed')}: {saved_mb:.1f} MB")
|
|
63
|
+
except OSError:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
print()
|
|
67
|
+
return 0
|
commands/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Реестр команд. Импортирует модули команд и регистрирует их.
|
|
2
|
+
|
|
3
|
+
Каждый модуль (cmd_xxx.py) экспортирует:
|
|
4
|
+
- register(subparsers) — регистрирует subparser
|
|
5
|
+
- run(args, db) — выполняет команду
|
|
6
|
+
|
|
7
|
+
Добавление новой команды:
|
|
8
|
+
1. Создать cmd_xxx.py
|
|
9
|
+
2. Добавить import cmd_xxx
|
|
10
|
+
3. Добавить "xxx": cmd_xxx в COMMANDS
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import cmd_costs
|
|
14
|
+
import cmd_delete
|
|
15
|
+
import cmd_export
|
|
16
|
+
import cmd_help
|
|
17
|
+
import cmd_info
|
|
18
|
+
import cmd_list
|
|
19
|
+
import cmd_projects
|
|
20
|
+
import cmd_prune
|
|
21
|
+
import cmd_search
|
|
22
|
+
import cmd_stats
|
|
23
|
+
import cmd_todos
|
|
24
|
+
import cmd_tree
|
|
25
|
+
import cmd_vacuum
|
|
26
|
+
|
|
27
|
+
# Словарь {имя_команды: модуль}
|
|
28
|
+
# Модуль должен иметь register(subparsers) и run(args, db)
|
|
29
|
+
COMMANDS = {
|
|
30
|
+
"list": cmd_list,
|
|
31
|
+
"info": cmd_info,
|
|
32
|
+
"export": cmd_export,
|
|
33
|
+
"delete": cmd_delete,
|
|
34
|
+
"costs": cmd_costs,
|
|
35
|
+
"prune": cmd_prune,
|
|
36
|
+
"search": cmd_search,
|
|
37
|
+
"tree": cmd_tree,
|
|
38
|
+
"projects": cmd_projects,
|
|
39
|
+
"stats": cmd_stats,
|
|
40
|
+
"todos": cmd_todos,
|
|
41
|
+
"vacuum": cmd_vacuum,
|
|
42
|
+
"help": cmd_help,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_handler(name):
|
|
47
|
+
return COMMANDS.get(name)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_all():
|
|
51
|
+
return COMMANDS
|
config.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Конфигурация и константы opencode-db.
|
|
2
|
+
|
|
3
|
+
Здесь централизованы пути и общие настройки.
|
|
4
|
+
Стоимость сессий берётся напрямую из БД (session.cost), а не рассчитывается.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
# --- Пути ---
|
|
10
|
+
# База данных opencode. Стандартное расположение для Linux/macOS.
|
|
11
|
+
OPENCODE_DB = os.path.expanduser("~/.local/share/opencode/opencode.db")
|
|
12
|
+
|
|
13
|
+
# Директории, игнорируемые при сборе snapshot'а проекта
|
|
14
|
+
IGNORE_DIRS = {".git", "node_modules", ".venv", "__pycache__", ".opencode"}
|