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.
cmd_help.py ADDED
@@ -0,0 +1,210 @@
1
+ """Команда help: подробная справка по opencode-db."""
2
+
3
+ from typing import Literal
4
+
5
+ from i18n import _
6
+
7
+
8
+ def register(subparsers) -> None:
9
+ p = subparsers.add_parser("help", help=_("help.cmd.help"))
10
+ p.add_argument("topic", nargs="?", help="Command name")
11
+ p.add_argument("--json", action="store_true", help="JSON output")
12
+
13
+
14
+ _COMMANDS = [
15
+ (
16
+ "list",
17
+ "help.cmd.list",
18
+ [
19
+ ("", "help.list.e0"),
20
+ ("--limit 10", "help.list.e1"),
21
+ ("--sort cost", "help.list.e2"),
22
+ ("--project proj_xxx", "help.list.e3"),
23
+ ("--json", "help.list.e4"),
24
+ ],
25
+ ),
26
+ (
27
+ "info",
28
+ "help.cmd.info",
29
+ [
30
+ ("<session_id>", "help.info.e0"),
31
+ ("<session_id> --json", "help.info.e1"),
32
+ ],
33
+ ),
34
+ (
35
+ "export",
36
+ "help.cmd.export",
37
+ [
38
+ ("<session_id>", "help.export.e0"),
39
+ ("--latest", "help.export.e1"),
40
+ ("", "help.export.e2"),
41
+ ("--full", "help.export.e3"),
42
+ ("--force", "help.export.e4"),
43
+ ('--note "text"', "help.export.e5"),
44
+ ],
45
+ ),
46
+ (
47
+ "delete",
48
+ "help.cmd.delete",
49
+ [
50
+ ("<session_id>", "help.delete.e0"),
51
+ ("<session_id> --dry-run", "help.delete.e1"),
52
+ ("<session_id> --force", "help.delete.e2"),
53
+ ],
54
+ ),
55
+ (
56
+ "costs",
57
+ "help.cmd.costs",
58
+ [
59
+ ("", "help.costs.e0"),
60
+ ("<session_id>", "help.costs.e1"),
61
+ ("--total", "help.costs.e2"),
62
+ ("--project proj_xxx", "help.costs.e3"),
63
+ ("--json", "help.costs.e4"),
64
+ ],
65
+ ),
66
+ (
67
+ "prune",
68
+ "help.cmd.prune",
69
+ [
70
+ ("--older-than 90d", "help.prune.e0"),
71
+ ("--keep-last 20", "help.prune.e1"),
72
+ ("--dry-run", "help.prune.e2"),
73
+ ("--force", "help.prune.e3"),
74
+ ],
75
+ ),
76
+ (
77
+ "search",
78
+ "help.cmd.search",
79
+ [
80
+ ('"text"', "help.search.e0"),
81
+ ('"text" --session ses_1c9d', "help.search.e1"),
82
+ ('"text" --json', "help.search.e2"),
83
+ ],
84
+ ),
85
+ (
86
+ "tree",
87
+ "help.cmd.tree",
88
+ [
89
+ ("", "help.tree.e0"),
90
+ ("<session_id>", "help.tree.e1"),
91
+ ("--depth 3", "help.tree.e2"),
92
+ ],
93
+ ),
94
+ (
95
+ "projects",
96
+ "help.cmd.projects",
97
+ [
98
+ ("", "help.projects.e0"),
99
+ ("--json", "help.projects.e1"),
100
+ ],
101
+ ),
102
+ (
103
+ "stats",
104
+ "help.cmd.stats",
105
+ [
106
+ ("", "help.stats.e0"),
107
+ ("--json", "help.stats.e1"),
108
+ ],
109
+ ),
110
+ (
111
+ "todos",
112
+ "help.cmd.todos",
113
+ [
114
+ ("", "help.todos.e0"),
115
+ ("<session_id>", "help.todos.e1"),
116
+ ("--status pending", "help.todos.e2"),
117
+ ("--json", "help.todos.e3"),
118
+ ],
119
+ ),
120
+ (
121
+ "vacuum",
122
+ "help.cmd.vacuum",
123
+ [
124
+ ("", "help.vacuum.e0"),
125
+ ("--force", "help.vacuum.e1"),
126
+ ],
127
+ ),
128
+ ]
129
+
130
+ _COMMAND_MAP: dict[str, tuple[str, list]] = {}
131
+ for name, desc_key, examples in _COMMANDS:
132
+ _COMMAND_MAP[name] = (desc_key, examples)
133
+
134
+
135
+ def _print_general_help() -> None:
136
+ """Вывод полной справки."""
137
+ print()
138
+ print(f" {_('help.header_box_top')}")
139
+ print(f" {_('help.header_title')}")
140
+ print(f" {_('help.header_box_bottom')}")
141
+ print()
142
+ print(f" {_('help.intro')}")
143
+ print(f" {_('help.intro2')}")
144
+ print()
145
+ print(f" {_('help.usage')}")
146
+ print()
147
+
148
+ for name, desc_key, examples in _COMMANDS:
149
+ desc = _(desc_key)
150
+ print(f" {name:12s} {desc}")
151
+ for flag, example_key in examples:
152
+ example_desc = _(example_key)
153
+ if flag:
154
+ print(f" {'':12s} opencode-db {name} {flag:30s} # {example_desc}")
155
+ else:
156
+ print(f" {'':12s} opencode-db {name:33s} # {example_desc}")
157
+ print()
158
+
159
+ print(f" {'─' * 58}")
160
+ print(f" {_('help.footer')}")
161
+ print(f" {_('help.footer2')}")
162
+ print(f" {_('help.footer3')}")
163
+ print()
164
+
165
+
166
+ def _print_command_help(cmd_name) -> Literal[1] | Literal[0]:
167
+ """Вывод справки по одной команде."""
168
+ entry = _COMMAND_MAP.get(cmd_name)
169
+ if not entry:
170
+ print(f"{_('help.unknown', cmd=cmd_name)}")
171
+ print(f" {_('help.available', cmds=', '.join(_COMMAND_MAP))}")
172
+ return 1
173
+
174
+ desc_key, examples = entry
175
+ print()
176
+ print(_("help.cmd_header", cmd=cmd_name, desc=_(desc_key)))
177
+ print()
178
+ for flag, example_key in examples:
179
+ example_desc = _(example_key)
180
+ if flag:
181
+ print(f" opencode-db {cmd_name} {flag:30s} # {example_desc}")
182
+ else:
183
+ print(f" opencode-db {cmd_name:33s} # {example_desc}")
184
+ print()
185
+ return 0
186
+
187
+
188
+ def run(args, db) -> Literal[1] | Literal[0]:
189
+ import json
190
+
191
+ if args.topic:
192
+ return _print_command_help(args.topic)
193
+
194
+ if args.json:
195
+ data = []
196
+ for name, desc_key, examples in _COMMANDS:
197
+ data.append(
198
+ {
199
+ "name": name,
200
+ "description": _(desc_key),
201
+ "examples": [
202
+ {"args": flag, "comment": _(example_key)} for flag, example_key in examples
203
+ ],
204
+ }
205
+ )
206
+ print(json.dumps(data, ensure_ascii=False, indent=2))
207
+ return 0
208
+
209
+ _print_general_help()
210
+ return 0
cmd_info.py ADDED
@@ -0,0 +1,152 @@
1
+ """Команда info: детальная информация о сессии."""
2
+
3
+ # import db as db_module
4
+ from typing import Literal
5
+
6
+ from db import (
7
+ get_children_sessions,
8
+ get_message_count,
9
+ get_parent_session,
10
+ get_part_count,
11
+ get_project_name,
12
+ get_session_info,
13
+ get_session_title,
14
+ get_session_todos,
15
+ parse_model,
16
+ )
17
+ from i18n import _
18
+ from utils import format_cost, format_tokens, format_ts
19
+
20
+
21
+ def register(subparsers) -> None:
22
+ p = subparsers.add_parser("info", help=_("help.cmd.info"))
23
+ p.add_argument("session_id", help="Session ID")
24
+ p.add_argument("--json", action="store_true", help="JSON output")
25
+
26
+
27
+ def run(args, db) -> Literal[0]:
28
+ info = get_session_info(db, args.session_id)
29
+ session_id = info["id"]
30
+ msg_count = get_message_count(db, session_id)
31
+ part_count = get_part_count(db, session_id)
32
+ children = get_children_sessions(db, session_id)
33
+ parent = get_parent_session(db, session_id)
34
+ todos = get_session_todos(db, session_id)
35
+ project_name = get_project_name(db, info["project_id"]) if info["project_id"] else "—"
36
+
37
+ model = parse_model(info)
38
+ title = get_session_title(info)
39
+ created = format_ts(info["time_created"])
40
+ updated = format_ts(info["time_updated"])
41
+
42
+ if info["time_updated"] and info["time_created"]:
43
+ duration_sec = (info["time_updated"] - info["time_created"]) / 1000
44
+ if duration_sec < 60:
45
+ duration = _("info.dur_sec", n=int(duration_sec))
46
+ elif duration_sec < 3600:
47
+ duration = _("info.dur_min", n=int(duration_sec / 60))
48
+ else:
49
+ hours = int(duration_sec / 3600)
50
+ mins = int((duration_sec % 3600) / 60)
51
+ duration = _("info.dur_hours", h=hours, m=mins)
52
+ else:
53
+ duration = "—"
54
+
55
+ if args.json:
56
+ data = {
57
+ "id": info["id"],
58
+ "title": title,
59
+ "project": {"id": info["project_id"], "name": project_name},
60
+ "model": model,
61
+ "agent": info["agent"],
62
+ "version": info["version"],
63
+ "directory": info["directory"],
64
+ "parent_id": info["parent_id"],
65
+ "time_created": created,
66
+ "time_updated": updated,
67
+ "duration": duration,
68
+ "messages": msg_count,
69
+ "parts": part_count,
70
+ "cost": info["cost"],
71
+ "tokens": {
72
+ "input": info["tokens_input"],
73
+ "output": info["tokens_output"],
74
+ "reasoning": info["tokens_reasoning"],
75
+ "cache_read": info["tokens_cache_read"],
76
+ "cache_write": info["tokens_cache_write"],
77
+ },
78
+ "code_changes": {
79
+ "additions": info["summary_additions"],
80
+ "deletions": info["summary_deletions"],
81
+ "files": info["summary_files"],
82
+ },
83
+ "children": [{"id": c["id"], "title": c["title"]} for c in children]
84
+ if children
85
+ else [],
86
+ "todos": [{"content": t["content"], "status": t["status"]} for t in todos]
87
+ if todos
88
+ else [],
89
+ }
90
+ if parent:
91
+ data["parent"] = {"id": parent["id"], "title": parent["title"]}
92
+
93
+ from formatters import print_json
94
+
95
+ print_json(data)
96
+ return 0
97
+
98
+ print(f"\n{'=' * 60}")
99
+ print(f" {title}")
100
+ print(f"{'=' * 60}")
101
+ print(f" {_('info.id')}: {session_id}")
102
+ print(f" {_('info.project')}: {project_name} ({info['project_id'][:24]})")
103
+ print(f" {_('info.model')}: {model}")
104
+ print(f" {_('info.agent')}: {info['agent'] or '—'}")
105
+ print(f" {_('info.version')}: {info['version'] or '—'}")
106
+ print(f" {_('info.created')}: {created}")
107
+ print(f" {_('info.updated')}: {updated}")
108
+ print(f" {_('info.duration')}: {duration}")
109
+
110
+ if parent:
111
+ parent_title = parent["title"] or _("info.unnamed")
112
+ print(f" {_('info.parent')}: {parent['id'][:24]} — {parent_title}")
113
+
114
+ if children:
115
+ print(f" {_('info.branches')}: {len(children)}")
116
+ for c in children:
117
+ c_title = c["title"] or _("info.unnamed")
118
+ print(_("info.children_prefix", id=c["id"][:24], title=c_title))
119
+
120
+ print(f"\n {'─' * 56}")
121
+ print(f" {_('info.messages')}: {msg_count}")
122
+ print(f" {_('info.parts')}: {part_count}")
123
+ print(f" {'─' * 56}")
124
+
125
+ print(f" {_('info.tokens_input')}: {format_tokens(info['tokens_input'])}")
126
+ print(f" {_('info.tokens_output')}: {format_tokens(info['tokens_output'])}")
127
+ print(f" {_('info.tokens_reasoning')}: {format_tokens(info['tokens_reasoning'])}")
128
+ if info["tokens_cache_read"]:
129
+ print(f" {_('info.cache_read')}: {format_tokens(info['tokens_cache_read'])}")
130
+ if info["tokens_cache_write"]:
131
+ print(f" {_('info.cache_write')}: {format_tokens(info['tokens_cache_write'])}")
132
+ total_tokens = (info["tokens_input"] or 0) + (info["tokens_output"] or 0)
133
+ print(f" {_('info.tokens_total')}: {format_tokens(total_tokens)}")
134
+ print(f" {_('info.cost')}: {format_cost(info['cost'])}")
135
+
136
+ if info["summary_files"]:
137
+ print(f"\n {'─' * 56}")
138
+ print(f" {_('info.code_changes')}:")
139
+ print(f" {_('info.files_changed')}: {info['summary_files']}")
140
+ print(f" {_('info.lines_added')}: {info['summary_additions'] or 0}")
141
+ print(f" {_('info.lines_deleted')}: {info['summary_deletions'] or 0}")
142
+
143
+ if todos:
144
+ print(f"\n {'─' * 56}")
145
+ print(f" {_('info.todos')}:")
146
+ status_icon = {"pending": "⏳", "completed": "✅", "cancelled": "❌"}
147
+ for t in todos:
148
+ icon = status_icon.get(t["status"], "📝")
149
+ print(f" {icon} {t['content'][:60]}")
150
+
151
+ print()
152
+ return 0
cmd_list.py ADDED
@@ -0,0 +1,124 @@
1
+ """Команда list: список сессий с фильтрацией и сортировкой."""
2
+
3
+ from typing import Literal
4
+
5
+ import db as db_module
6
+ from db import get_session_title, parse_model
7
+ from formatters import print_json, print_table_simple
8
+ from i18n import _
9
+ from utils import format_ts
10
+
11
+
12
+ def register(subparsers) -> None:
13
+ p = subparsers.add_parser("list", help=_("help.cmd.list"))
14
+ p.add_argument("--limit", type=int, default=25, help="Max results (default 25)")
15
+ p.add_argument("--offset", type=int, default=0, help="Offset")
16
+ p.add_argument("--project", type=str, help="Filter by project ID")
17
+ p.add_argument("--agent", type=str, help="Filter by agent type")
18
+ p.add_argument("--model", type=str, help="Filter by model name")
19
+ p.add_argument(
20
+ "--sort",
21
+ choices=["date", "cost", "tokens", "messages"],
22
+ default="date",
23
+ help="Sort order (default: date)",
24
+ )
25
+ p.add_argument("--json", action="store_true", help="JSON output")
26
+ p.add_argument("--all", action="store_true", help="Show all (no limit)")
27
+
28
+
29
+ def run(args, db) -> Literal[0]:
30
+ conditions = []
31
+ params = []
32
+
33
+ if args.project:
34
+ conditions.append("s.project_id = ?")
35
+ params.append(args.project)
36
+
37
+ if args.agent:
38
+ conditions.append("s.agent = ?")
39
+ params.append(args.agent)
40
+
41
+ if args.model:
42
+ conditions.append("s.model LIKE ?")
43
+ params.append(f"%{args.model}%")
44
+
45
+ where = " AND ".join(conditions) if conditions else "1=1"
46
+
47
+ sort_map = {
48
+ "date": "COALESCE(MAX(m.time_created), s.time_created) DESC",
49
+ "cost": "s.cost DESC",
50
+ "tokens": "(s.tokens_input + s.tokens_output) DESC",
51
+ "messages": "msg_count DESC",
52
+ }
53
+ order = sort_map.get(args.sort, "COALESCE(MAX(m.time_created), s.time_created) DESC")
54
+
55
+ limit = 999999 if args.all else args.limit
56
+
57
+ rows = db.execute(
58
+ f"""
59
+ SELECT s.id, s.title, s.model, s.project_id, s.agent,
60
+ s.cost, s.tokens_input, s.tokens_output,
61
+ MAX(m.time_created) as last_time,
62
+ COUNT(DISTINCT m.id) as msg_count
63
+ FROM session s
64
+ LEFT JOIN message m ON m.session_id = s.id
65
+ WHERE {where}
66
+ GROUP BY s.id
67
+ ORDER BY {order}
68
+ LIMIT ? OFFSET ?
69
+ """,
70
+ (*params, limit, args.offset),
71
+ ).fetchall()
72
+
73
+ if args.json:
74
+ data = []
75
+ for r in rows:
76
+ data.append(
77
+ {
78
+ "id": r["id"],
79
+ "title": get_session_title(r),
80
+ "project_id": r["project_id"],
81
+ "model": parse_model(r),
82
+ "agent": r["agent"],
83
+ "cost": r["cost"],
84
+ "tokens_input": r["tokens_input"],
85
+ "tokens_output": r["tokens_output"],
86
+ "messages": r["msg_count"],
87
+ "last_activity": format_ts(r["last_time"]) if r["last_time"] else None,
88
+ }
89
+ )
90
+ print_json(data)
91
+ return 0
92
+
93
+ table_rows = []
94
+ for r in rows:
95
+ project_name = db_module.get_project_name(db, r["project_id"]) if r["project_id"] else "—"
96
+ model_name = parse_model(r)
97
+ last_act = format_ts(r["last_time"], "%Y-%m-%d %H:%M") if r["last_time"] else "—"
98
+
99
+ table_rows.append(
100
+ [
101
+ r["id"][:24],
102
+ get_session_title(r)[:32],
103
+ project_name[:16],
104
+ model_name[:20],
105
+ last_act,
106
+ str(r["msg_count"]),
107
+ f"${r['cost']:.4f}" if r["cost"] else "—",
108
+ ]
109
+ )
110
+
111
+ print_table_simple(
112
+ [
113
+ _("list.header.id"),
114
+ _("list.header.title"),
115
+ _("list.header.project"),
116
+ _("list.header.model"),
117
+ _("list.header.activity"),
118
+ _("list.header.messages"),
119
+ _("list.header.cost"),
120
+ ],
121
+ table_rows,
122
+ )
123
+
124
+ return 0
cmd_projects.py ADDED
@@ -0,0 +1,82 @@
1
+ """Команда projects: список проектов со статистикой."""
2
+
3
+ from typing import Literal
4
+
5
+ from i18n import _
6
+ from utils import format_cost, format_tokens, format_ts
7
+
8
+
9
+ def register(subparsers) -> None:
10
+ p = subparsers.add_parser("projects", help=_("help.cmd.projects"))
11
+ p.add_argument("--json", action="store_true", help="JSON output")
12
+
13
+
14
+ def run(args, db) -> Literal[0]:
15
+ rows = db.execute("""
16
+ SELECT p.id, p.name, p.worktree, p.time_created, p.time_updated,
17
+ COUNT(DISTINCT s.id) as session_count,
18
+ COALESCE(SUM(s.cost), 0) as total_cost,
19
+ COALESCE(SUM(s.tokens_input), 0) as total_ti,
20
+ COALESCE(SUM(s.tokens_output), 0) as total_to,
21
+ COALESCE(SUM(s.tokens_reasoning), 0) as total_tr
22
+ FROM project p
23
+ LEFT JOIN session s ON s.project_id = p.id
24
+ GROUP BY p.id
25
+ ORDER BY p.time_updated DESC
26
+ """).fetchall()
27
+
28
+ if args.json:
29
+ from formatters import print_json
30
+
31
+ data = []
32
+ for r in rows:
33
+ data.append(
34
+ {
35
+ "id": r["id"],
36
+ "name": r["name"],
37
+ "worktree": r["worktree"],
38
+ "time_created": format_ts(r["time_created"]) if r["time_created"] else None,
39
+ "time_updated": format_ts(r["time_updated"]) if r["time_updated"] else None,
40
+ "sessions": r["session_count"],
41
+ "cost": r["total_cost"],
42
+ "tokens_input": r["total_ti"],
43
+ "tokens_output": r["total_to"],
44
+ "tokens_reasoning": r["total_tr"],
45
+ "tokens_total": r["total_ti"] + r["total_to"] + r["total_tr"],
46
+ }
47
+ )
48
+ print_json(data)
49
+ return 0
50
+
51
+ from formatters import print_table_simple
52
+
53
+ table = []
54
+ for r in rows:
55
+ name = r["name"] or "—"
56
+ updated = format_ts(r["time_updated"], "%Y-%m-%d") if r["time_updated"] else "—"
57
+ total_tokens = r["total_ti"] + r["total_to"] + r["total_tr"]
58
+
59
+ table.append(
60
+ [
61
+ r["id"][:12],
62
+ name[:20],
63
+ updated,
64
+ str(r["session_count"]),
65
+ format_tokens(total_tokens),
66
+ format_cost(r["total_cost"]),
67
+ ]
68
+ )
69
+
70
+ print_table_simple(
71
+ [
72
+ _("projects.header.id"),
73
+ _("projects.header.name"),
74
+ _("projects.header.activity"),
75
+ _("projects.header.sessions"),
76
+ _("projects.header.tokens"),
77
+ _("projects.header.cost"),
78
+ ],
79
+ table,
80
+ )
81
+
82
+ return 0
cmd_prune.py ADDED
@@ -0,0 +1,127 @@
1
+ """Команда prune: массовая очистка старых сессий."""
2
+
3
+ # from db import get_session_info, get_session_title, parse_model, \
4
+ # get_message_count, get_part_count
5
+ from typing import Literal
6
+
7
+ from i18n import _
8
+ from utils import confirm, format_ts, parse_time_spec
9
+
10
+
11
+ def register(subparsers) -> None:
12
+ p = subparsers.add_parser("prune", help=_("help.cmd.prune"))
13
+ p.add_argument("--older-than", type=str, help="Delete sessions older than (30d, 6m, 1y)")
14
+ p.add_argument("--keep-last", type=int, help="Keep N most recent sessions")
15
+ p.add_argument("--project", type=str, help="Limit to project")
16
+ p.add_argument("--dry-run", action="store_true", help="Preview without deleting")
17
+ p.add_argument("--force", "-f", action="store_true", help="Skip confirmation")
18
+
19
+
20
+ def run(args, db) -> Literal[1] | Literal[0]:
21
+ if not args.older_than and not args.keep_last and not args.project:
22
+ print(_("prune.filter_required"))
23
+ print(_("prune.filter_example"))
24
+ return 1
25
+
26
+ conditions = []
27
+ params = []
28
+
29
+ if args.project:
30
+ conditions.append("s.project_id = ?")
31
+ params.append(args.project)
32
+
33
+ if args.older_than:
34
+ cutoff = parse_time_spec(args.older_than)
35
+ if cutoff is None:
36
+ print(_("prune.bad_spec", spec=args.older_than))
37
+ print(_("prune.bad_spec_hint"))
38
+ return 1
39
+ cutoff_ms = int(cutoff.timestamp() * 1000)
40
+ conditions.append("s.time_created < ?")
41
+ params.append(cutoff_ms)
42
+
43
+ exclude_ids = set()
44
+ if args.keep_last:
45
+ inner_where = " AND ".join(conditions) if conditions else "1=1"
46
+ keep_rows = db.execute(
47
+ f"""
48
+ SELECT s.id FROM session s
49
+ WHERE {inner_where}
50
+ ORDER BY s.time_created DESC
51
+ LIMIT ?
52
+ """,
53
+ (*params, args.keep_last),
54
+ ).fetchall()
55
+ exclude_ids = {r["id"] for r in keep_rows}
56
+
57
+ where = " AND ".join(conditions) if conditions else "1=1"
58
+
59
+ if exclude_ids:
60
+ placeholders = ",".join("?" for _ in exclude_ids)
61
+ target_rows = db.execute(
62
+ f"""
63
+ SELECT s.id, s.title, s.model, s.time_created, s.cost
64
+ FROM session s
65
+ WHERE {where}
66
+ AND s.id NOT IN ({placeholders})
67
+ ORDER BY s.time_created DESC
68
+ """,
69
+ (*params, *exclude_ids),
70
+ ).fetchall()
71
+ else:
72
+ target_rows = db.execute(
73
+ f"""
74
+ SELECT s.id, s.title, s.model, s.time_created, s.cost
75
+ FROM session s
76
+ WHERE {where}
77
+ ORDER BY s.time_created DESC
78
+ """,
79
+ params,
80
+ ).fetchall()
81
+
82
+ if not target_rows:
83
+ print(_("prune.none"))
84
+ return 0
85
+
86
+ total_cost = sum(r["cost"] or 0 for r in target_rows)
87
+ total_tokens = sum(
88
+ db.execute(
89
+ "SELECT COALESCE(SUM(tokens_input + tokens_output), 0) FROM session WHERE id = ?",
90
+ (r["id"],),
91
+ ).fetchone()[0]
92
+ for r in target_rows
93
+ )
94
+
95
+ print(f"\n {'=' * 55}")
96
+ print(f" {_('prune.header', n=len(target_rows))}")
97
+ print(f" {'=' * 55}")
98
+
99
+ for r in target_rows[:20]:
100
+ created = format_ts(r["time_created"])
101
+ print(f" • {r['id'][:24]} {created} {r['title'] or '—'}")
102
+
103
+ if len(target_rows) > 20:
104
+ print(f" … и ещё {len(target_rows) - 20}")
105
+
106
+ print(f"\n {_('prune.cost_total', cost=f'${total_cost:.4f}')}")
107
+ print(f" {_('prune.tokens_total', n=str(total_tokens))}")
108
+
109
+ if args.dry_run:
110
+ print(f"\n{_('prune.dry_run')}")
111
+ return 0
112
+
113
+ if not args.force and not confirm(_("prune.confirm", n=len(target_rows))):
114
+ print(_("canceled"))
115
+ return 0
116
+
117
+ ids_to_delete = [r["id"] for r in target_rows]
118
+ placeholders = ",".join("?" for _ in ids_to_delete)
119
+
120
+ db.execute(f"DELETE FROM part WHERE session_id IN ({placeholders})", ids_to_delete)
121
+ db.execute(f"DELETE FROM message WHERE session_id IN ({placeholders})", ids_to_delete)
122
+ db.execute(f"DELETE FROM todo WHERE session_id IN ({placeholders})", ids_to_delete)
123
+ db.execute(f"DELETE FROM session WHERE id IN ({placeholders})", ids_to_delete)
124
+ db.commit()
125
+
126
+ print(f"\n{_('prune.done', n=len(ids_to_delete))}")
127
+ return 0