opencode-db 0.5.1__tar.gz → 0.6.1__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.
- {opencode_db-0.5.1/opencode_db.egg-info → opencode_db-0.6.1}/PKG-INFO +1 -1
- {opencode_db-0.5.1 → opencode_db-0.6.1}/README.md +49 -23
- {opencode_db-0.5.1 → opencode_db-0.6.1}/cli.py +10 -0
- opencode_db-0.6.1/cmd_delete.py +343 -0
- opencode_db-0.6.1/cmd_prune.py +32 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/config.py +3 -1
- {opencode_db-0.5.1 → opencode_db-0.6.1}/i18n.py +51 -1
- {opencode_db-0.5.1 → opencode_db-0.6.1/opencode_db.egg-info}/PKG-INFO +1 -1
- {opencode_db-0.5.1 → opencode_db-0.6.1}/pyproject.toml +1 -1
- {opencode_db-0.5.1 → opencode_db-0.6.1}/tests/test_commands.py +1 -1
- {opencode_db-0.5.1 → opencode_db-0.6.1}/utils.py +13 -0
- opencode_db-0.5.1/cmd_delete.py +0 -74
- opencode_db-0.5.1/cmd_prune.py +0 -138
- {opencode_db-0.5.1 → opencode_db-0.6.1}/LICENSE +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/README_PYPI.md +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/cmd_costs.py +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/cmd_export.py +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/cmd_help.py +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/cmd_info.py +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/cmd_list.py +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/cmd_projects.py +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/cmd_search.py +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/cmd_stats.py +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/cmd_todos.py +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/cmd_tree.py +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/cmd_vacuum.py +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/commands/__init__.py +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/db.py +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/formatters.py +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/opencode_db.egg-info/SOURCES.txt +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/opencode_db.egg-info/dependency_links.txt +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/opencode_db.egg-info/entry_points.txt +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/opencode_db.egg-info/top_level.txt +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/setup.cfg +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/tests/test_db.py +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/tests/test_formatters.py +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/tests/test_i18n.py +0 -0
- {opencode_db-0.5.1 → opencode_db-0.6.1}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opencode-db
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: OpenCode database CLI manager — browse, export, analyze, clean up sessions
|
|
5
5
|
Project-URL: Homepage, https://github.com/VasilevNStas/opencode-db
|
|
6
6
|
Project-URL: Source, https://github.com/VasilevNStas/opencode-db
|
|
@@ -26,6 +26,31 @@ opencode-db costs --total # total token costs
|
|
|
26
26
|
- OpenCode (must have been run at least once — the database is created automatically)
|
|
27
27
|
- macOS / Linux
|
|
28
28
|
|
|
29
|
+
### Database location
|
|
30
|
+
|
|
31
|
+
By default, `opencode-db` looks for OpenCode's database at:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
~/.local/share/opencode/opencode.db
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
If your OpenCode stores the database at a custom path, you have two options:
|
|
38
|
+
|
|
39
|
+
**1. Environment variable** — set it once in your shell profile (`.zshrc`, `.bashrc`):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export OPENCODE_DB=/path/to/opencode.db
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**2. Global `--db-path` flag** — for ad-hoc use or inspecting another database:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
opencode-db --db-path /path/to/opencode.db list
|
|
49
|
+
opencode-db --db-path /path/to/opencode.db stats
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The `--db-path` flag takes precedence over the environment variable.
|
|
53
|
+
|
|
29
54
|
### One-command install (recommended)
|
|
30
55
|
|
|
31
56
|
```bash
|
|
@@ -210,20 +235,35 @@ If the project directory contains `.obsidian`, a `log.md` file is automatically
|
|
|
210
235
|
|
|
211
236
|
---
|
|
212
237
|
|
|
213
|
-
### `delete` — delete
|
|
238
|
+
### `delete` — delete sessions
|
|
214
239
|
|
|
215
|
-
Removes
|
|
240
|
+
Removes sessions and all related data (messages, parts, todos). Supports single deletion by ID or bulk deletion with filters.
|
|
216
241
|
|
|
217
242
|
```bash
|
|
218
|
-
opencode-db delete ses_1c9d
|
|
219
|
-
opencode-db delete ses_1c9d
|
|
220
|
-
opencode-db delete ses_1c9d --
|
|
243
|
+
opencode-db delete ses_1c9d # single session with confirmation
|
|
244
|
+
opencode-db delete ses_1c9d --force # single session, no confirmation
|
|
245
|
+
opencode-db delete ses_1c9d --dry-run # preview what would be deleted
|
|
246
|
+
opencode-db delete --older-than 90d # sessions older than 90 days
|
|
247
|
+
opencode-db delete --older-than 6m --dry-run # preview sessions older than 6 months
|
|
248
|
+
opencode-db delete --keep-last 30 # keep 30 most recent, delete the rest
|
|
249
|
+
opencode-db delete --before 2026-05-20 # sessions before a specific date
|
|
250
|
+
opencode-db delete --after 2026-01-01 # sessions after a specific date
|
|
251
|
+
opencode-db delete --project proj_xxx # sessions in a specific project
|
|
252
|
+
opencode-db delete --older-than 90d --keep-last 20 # combined filters
|
|
253
|
+
opencode-db delete --interactive # choose from a list interactively
|
|
221
254
|
```
|
|
222
255
|
|
|
223
256
|
| Flag | Description |
|
|
224
257
|
|---|---|
|
|
258
|
+
| `session_id` | Session ID (optional with `--interactive`) |
|
|
259
|
+
| `--older-than` | Delete sessions older than (e.g. `30d`, `6m`, `1y`) |
|
|
260
|
+
| `--before` | Delete sessions before date (`YYYY-MM-DD`) |
|
|
261
|
+
| `--after` | Delete sessions after date (`YYYY-MM-DD`) |
|
|
262
|
+
| `--keep-last` | Keep N most recent, delete the rest |
|
|
263
|
+
| `--project` | Limit to a specific project |
|
|
225
264
|
| `--dry-run` | Only show what would be deleted |
|
|
226
265
|
| `--force`, `-f` | Skip confirmation prompt |
|
|
266
|
+
| `--interactive` | Pick sessions from a list |
|
|
227
267
|
|
|
228
268
|
---
|
|
229
269
|
|
|
@@ -252,31 +292,17 @@ Costs are taken directly from the database (`session.cost`), which stores the ac
|
|
|
252
292
|
|
|
253
293
|
---
|
|
254
294
|
|
|
255
|
-
### `prune` —
|
|
295
|
+
### `prune` — alias for `delete` (bulk cleanup)
|
|
256
296
|
|
|
257
|
-
|
|
297
|
+
Alias for `delete` with the same flags: `--older-than`, `--keep-last`, `--project`, `--dry-run`, `--force`.
|
|
258
298
|
|
|
259
299
|
```bash
|
|
260
|
-
opencode-db prune --older-than
|
|
261
|
-
opencode-db prune --
|
|
262
|
-
opencode-db prune --older-than 6m # older than 6 months
|
|
263
|
-
opencode-db prune --older-than 1y # older than 1 year
|
|
264
|
-
opencode-db prune --keep-last 20 # keep the 20 most recent
|
|
265
|
-
opencode-db prune --older-than 30d --keep-last 10 # combined filters
|
|
266
|
-
opencode-db prune --project proj_xxx # per project
|
|
267
|
-
opencode-db prune --older-than 30d --force # skip confirmation
|
|
300
|
+
opencode-db prune --older-than 90d # same as opencode-db delete --older-than 90d
|
|
301
|
+
opencode-db prune --keep-last 20 # same as opencode-db delete --keep-last 20
|
|
268
302
|
```
|
|
269
303
|
|
|
270
304
|
Time spec formats: `30d` (days), `6m` (months), `1y` (years).
|
|
271
305
|
|
|
272
|
-
| Flag | Description |
|
|
273
|
-
|---|---|
|
|
274
|
-
| `--older-than SPEC` | Delete sessions older than the given duration |
|
|
275
|
-
| `--keep-last N` | Keep N most recent sessions |
|
|
276
|
-
| `--project ID` | Limit to a specific project |
|
|
277
|
-
| `--dry-run` | Only show what would be deleted |
|
|
278
|
-
| `--force`, `-f` | Skip confirmation prompt |
|
|
279
|
-
|
|
280
306
|
---
|
|
281
307
|
|
|
282
308
|
### `search` — search across messages
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import argparse
|
|
11
|
+
import os
|
|
11
12
|
import sys
|
|
12
13
|
from typing import Any, Literal
|
|
13
14
|
|
|
@@ -28,6 +29,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
28
29
|
default=None,
|
|
29
30
|
help="Output language / язык вывода (en/ru)",
|
|
30
31
|
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--db-path",
|
|
34
|
+
type=str,
|
|
35
|
+
default=None,
|
|
36
|
+
help="Path to opencode.db (default: ~/.local/share/opencode/opencode.db)",
|
|
37
|
+
)
|
|
31
38
|
|
|
32
39
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
33
40
|
for _name, mod in commands.get_all().items():
|
|
@@ -58,6 +65,9 @@ def main(argv=None) -> Any | Literal[0] | Literal[1]:
|
|
|
58
65
|
if args.lang:
|
|
59
66
|
set_lang(args.lang)
|
|
60
67
|
|
|
68
|
+
if args.db_path:
|
|
69
|
+
os.environ["OPENCODE_DB"] = args.db_path
|
|
70
|
+
|
|
61
71
|
db = get_db()
|
|
62
72
|
|
|
63
73
|
try:
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""Команда delete: удаление сессии или массовая очистка."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from db import (
|
|
6
|
+
SessionError,
|
|
7
|
+
get_message_count,
|
|
8
|
+
get_part_count,
|
|
9
|
+
get_recent_sessions,
|
|
10
|
+
get_session_info,
|
|
11
|
+
get_session_title,
|
|
12
|
+
parse_model,
|
|
13
|
+
)
|
|
14
|
+
from i18n import _
|
|
15
|
+
from utils import build_help_epilog, confirm, format_cost, format_ts, parse_date, parse_time_spec
|
|
16
|
+
|
|
17
|
+
_DELETE_EXAMPLES = [
|
|
18
|
+
("<session_id>", "help.delete.e0"),
|
|
19
|
+
("<session_id> --dry-run", "help.delete.e1"),
|
|
20
|
+
("<session_id> --force", "help.delete.e2"),
|
|
21
|
+
("--older-than 90d", "help.delete.e3"),
|
|
22
|
+
("--keep-last 30", "help.delete.e4"),
|
|
23
|
+
("--before 2026-05-20", "help.delete.e5"),
|
|
24
|
+
("--interactive", "help.delete.e6"),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def register(subparsers) -> None:
|
|
29
|
+
p = subparsers.add_parser(
|
|
30
|
+
"delete",
|
|
31
|
+
help=_("help.cmd.delete"),
|
|
32
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
33
|
+
epilog=build_help_epilog("delete", _DELETE_EXAMPLES),
|
|
34
|
+
)
|
|
35
|
+
p.add_argument("session_id", nargs="?", help="Session ID (optional with --interactive)")
|
|
36
|
+
p.add_argument("--older-than", type=str, help="Delete sessions older than (30d, 6m, 1y)")
|
|
37
|
+
p.add_argument("--before", type=str, help="Delete sessions before date (YYYY-MM-DD)")
|
|
38
|
+
p.add_argument("--after", type=str, help="Delete sessions after date (YYYY-MM-DD)")
|
|
39
|
+
p.add_argument("--keep-last", type=int, help="Keep N most recent sessions")
|
|
40
|
+
p.add_argument("--project", type=str, help="Limit to project")
|
|
41
|
+
p.add_argument("--dry-run", action="store_true", help="Preview without deleting")
|
|
42
|
+
p.add_argument("--force", "-f", action="store_true", help="Skip confirmation")
|
|
43
|
+
p.add_argument("--interactive", action="store_true", help="Choose sessions from a list")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _delete_single(db, session_id, dry_run, force) -> int:
|
|
47
|
+
"""Удаление одной сессии по ID."""
|
|
48
|
+
try:
|
|
49
|
+
info = get_session_info(db, session_id)
|
|
50
|
+
except SessionError as e:
|
|
51
|
+
print(e.message)
|
|
52
|
+
return 1
|
|
53
|
+
sid = info["id"]
|
|
54
|
+
title = get_session_title(info)
|
|
55
|
+
model = parse_model(info)
|
|
56
|
+
msg_count = get_message_count(db, sid)
|
|
57
|
+
part_count = get_part_count(db, sid)
|
|
58
|
+
created = format_ts(info["time_created"])
|
|
59
|
+
|
|
60
|
+
print(f"\n{'=' * 50}")
|
|
61
|
+
print(f" {_('delete.header')}")
|
|
62
|
+
print(f"{'=' * 50}")
|
|
63
|
+
print(f" {_('delete.id')}: {sid}")
|
|
64
|
+
print(f" {_('delete.title')}: {title}")
|
|
65
|
+
print(f" {_('delete.model')}: {model}")
|
|
66
|
+
print(f" {_('delete.created')}: {created}")
|
|
67
|
+
print(f" {_('delete.messages')}: {msg_count}")
|
|
68
|
+
print(f" {_('delete.parts')}: {part_count}")
|
|
69
|
+
print(f" {_('delete.cost')}: {format_cost(info['cost'])}")
|
|
70
|
+
|
|
71
|
+
if dry_run:
|
|
72
|
+
print(f"\n{_('delete.dry_run')}")
|
|
73
|
+
print(f" {_('delete.dry_run_hint')}")
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
if not force and not confirm(_("delete.confirm", id=sid[:24])):
|
|
77
|
+
print(_("canceled"))
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
db.execute("DELETE FROM session WHERE id = ?", (sid,))
|
|
81
|
+
db.commit()
|
|
82
|
+
|
|
83
|
+
print(f"\n{_('delete.done', id=sid[:24])}")
|
|
84
|
+
print(_("delete.done_detail", msg=msg_count, parts=part_count))
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _build_mass_conditions(args):
|
|
89
|
+
"""Строит условия WHERE для массового удаления."""
|
|
90
|
+
conditions = []
|
|
91
|
+
params = []
|
|
92
|
+
project = _a(args, "project")
|
|
93
|
+
older_than = _a(args, "older_than")
|
|
94
|
+
before = _a(args, "before")
|
|
95
|
+
after = _a(args, "after")
|
|
96
|
+
|
|
97
|
+
if project:
|
|
98
|
+
conditions.append("s.project_id = ?")
|
|
99
|
+
params.append(project)
|
|
100
|
+
|
|
101
|
+
if older_than:
|
|
102
|
+
cutoff = parse_time_spec(older_than)
|
|
103
|
+
if cutoff is None:
|
|
104
|
+
return None, None, _("delete.bad_spec", spec=older_than)
|
|
105
|
+
conditions.append("s.time_created < ?")
|
|
106
|
+
params.append(int(cutoff.timestamp() * 1000))
|
|
107
|
+
|
|
108
|
+
if before:
|
|
109
|
+
dt = parse_date(before)
|
|
110
|
+
if dt is None:
|
|
111
|
+
return None, None, _("delete.bad_date", date=before)
|
|
112
|
+
conditions.append("s.time_created < ?")
|
|
113
|
+
params.append(int(dt.timestamp() * 1000))
|
|
114
|
+
|
|
115
|
+
if after:
|
|
116
|
+
dt = parse_date(after)
|
|
117
|
+
if dt is None:
|
|
118
|
+
return None, None, _("delete.bad_date", date=after)
|
|
119
|
+
conditions.append("s.time_created > ?")
|
|
120
|
+
params.append(int(dt.timestamp() * 1000))
|
|
121
|
+
|
|
122
|
+
return conditions, params, None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _collect_mass_targets(db, conditions, params, keep_last):
|
|
126
|
+
"""Собирает список сессий для массового удаления."""
|
|
127
|
+
exclude_ids = set()
|
|
128
|
+
if keep_last:
|
|
129
|
+
inner_where = " AND ".join(conditions) if conditions else "1=1"
|
|
130
|
+
keep_rows = db.execute(
|
|
131
|
+
f"""
|
|
132
|
+
SELECT s.id FROM session s
|
|
133
|
+
WHERE {inner_where}
|
|
134
|
+
ORDER BY s.time_created DESC
|
|
135
|
+
LIMIT ?
|
|
136
|
+
""",
|
|
137
|
+
(*params, keep_last),
|
|
138
|
+
).fetchall()
|
|
139
|
+
exclude_ids = {r["id"] for r in keep_rows}
|
|
140
|
+
|
|
141
|
+
where = " AND ".join(conditions) if conditions else "1=1"
|
|
142
|
+
|
|
143
|
+
if exclude_ids:
|
|
144
|
+
placeholders = ",".join("?" for _ in exclude_ids)
|
|
145
|
+
rows = db.execute(
|
|
146
|
+
f"""
|
|
147
|
+
SELECT s.id, s.title, s.model, s.time_created, s.cost
|
|
148
|
+
FROM session s
|
|
149
|
+
WHERE {where}
|
|
150
|
+
AND s.id NOT IN ({placeholders})
|
|
151
|
+
ORDER BY s.time_created DESC
|
|
152
|
+
""",
|
|
153
|
+
(*params, *exclude_ids),
|
|
154
|
+
).fetchall()
|
|
155
|
+
else:
|
|
156
|
+
rows = db.execute(
|
|
157
|
+
f"""
|
|
158
|
+
SELECT s.id, s.title, s.model, s.time_created, s.cost
|
|
159
|
+
FROM session s
|
|
160
|
+
WHERE {where}
|
|
161
|
+
ORDER BY s.time_created DESC
|
|
162
|
+
""",
|
|
163
|
+
params,
|
|
164
|
+
).fetchall()
|
|
165
|
+
|
|
166
|
+
return rows
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _delete_mass(db, args) -> int:
|
|
170
|
+
"""Массовое удаление сессий."""
|
|
171
|
+
keep_last = _a(args, "keep_last")
|
|
172
|
+
dry_run = _a(args, "dry_run", False)
|
|
173
|
+
force = _a(args, "force", False)
|
|
174
|
+
|
|
175
|
+
conditions, params, err = _build_mass_conditions(args)
|
|
176
|
+
if err:
|
|
177
|
+
print(err)
|
|
178
|
+
return 1
|
|
179
|
+
|
|
180
|
+
if not conditions and not keep_last:
|
|
181
|
+
print(_("delete.filter_required"))
|
|
182
|
+
return 1
|
|
183
|
+
|
|
184
|
+
target_rows = _collect_mass_targets(db, conditions, params, keep_last)
|
|
185
|
+
|
|
186
|
+
if not target_rows:
|
|
187
|
+
print(_("delete.none"))
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
total_cost = sum(r["cost"] or 0 for r in target_rows)
|
|
191
|
+
total_tokens = sum(
|
|
192
|
+
db.execute(
|
|
193
|
+
"SELECT COALESCE(SUM(tokens_input + tokens_output), 0) FROM session WHERE id = ?",
|
|
194
|
+
(r["id"],),
|
|
195
|
+
).fetchone()[0]
|
|
196
|
+
for r in target_rows
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
print(f"\n {'=' * 55}")
|
|
200
|
+
print(f" {_('delete.mass_header', n=len(target_rows))}")
|
|
201
|
+
print(f" {'=' * 55}")
|
|
202
|
+
|
|
203
|
+
for r in target_rows[:20]:
|
|
204
|
+
created = format_ts(r["time_created"])
|
|
205
|
+
print(f" • {r['id'][:24]} {created} {r['title'] or '—'}")
|
|
206
|
+
|
|
207
|
+
if len(target_rows) > 20:
|
|
208
|
+
print(f" … и ещё {len(target_rows) - 20}")
|
|
209
|
+
|
|
210
|
+
print(f"\n {_('delete.cost_total', cost=f'${total_cost:.4f}')}")
|
|
211
|
+
print(f" {_('delete.tokens_total', n=str(total_tokens))}")
|
|
212
|
+
|
|
213
|
+
if dry_run:
|
|
214
|
+
print(f"\n{_('delete.dry_run')}")
|
|
215
|
+
return 0
|
|
216
|
+
|
|
217
|
+
if not force and not confirm(_("delete.mass_confirm", n=len(target_rows))):
|
|
218
|
+
print(_("canceled"))
|
|
219
|
+
return 0
|
|
220
|
+
|
|
221
|
+
ids = [r["id"] for r in target_rows]
|
|
222
|
+
placeholders = ",".join("?" for _ in ids)
|
|
223
|
+
|
|
224
|
+
db.execute(f"DELETE FROM part WHERE session_id IN ({placeholders})", ids)
|
|
225
|
+
db.execute(f"DELETE FROM message WHERE session_id IN ({placeholders})", ids)
|
|
226
|
+
db.execute(f"DELETE FROM todo WHERE session_id IN ({placeholders})", ids)
|
|
227
|
+
db.execute(f"DELETE FROM session WHERE id IN ({placeholders})", ids)
|
|
228
|
+
db.commit()
|
|
229
|
+
|
|
230
|
+
print(f"\n{_('delete.done_mass', n=len(ids))}")
|
|
231
|
+
return 0
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _delete_interactive(db, args) -> int:
|
|
235
|
+
"""Интерактивное удаление — выбор из списка последних сессий."""
|
|
236
|
+
sessions = get_recent_sessions(db, limit=20)
|
|
237
|
+
|
|
238
|
+
if not sessions:
|
|
239
|
+
print(_("delete.no_sessions"))
|
|
240
|
+
return 1
|
|
241
|
+
|
|
242
|
+
print(f"\n {_('delete.interactive_header')}")
|
|
243
|
+
print(f" {'─' * 55}")
|
|
244
|
+
for i, s in enumerate(sessions, 1):
|
|
245
|
+
title = get_session_title(s)
|
|
246
|
+
project = s.get("project_id", "") or ""
|
|
247
|
+
print(f" {i:2d}) {s['id'][:24]} {title[:40]:40s} {project[:20]}")
|
|
248
|
+
|
|
249
|
+
print()
|
|
250
|
+
choices = input(_("delete.interactive_prompt")).strip()
|
|
251
|
+
|
|
252
|
+
if not choices:
|
|
253
|
+
print(_("canceled"))
|
|
254
|
+
return 0
|
|
255
|
+
|
|
256
|
+
selected = _parse_choices(choices, len(sessions))
|
|
257
|
+
if not selected:
|
|
258
|
+
return 1
|
|
259
|
+
|
|
260
|
+
target_rows = [sessions[i - 1] for i in selected]
|
|
261
|
+
total_cost = sum(r["cost"] or 0 for r in target_rows)
|
|
262
|
+
|
|
263
|
+
print(f"\n {'=' * 55}")
|
|
264
|
+
print(f" {_('delete.mass_header', n=len(target_rows))}")
|
|
265
|
+
print(f" {'=' * 55}")
|
|
266
|
+
for r in target_rows:
|
|
267
|
+
created = format_ts(r["time_created"])
|
|
268
|
+
print(f" • {r['id'][:24]} {created} {r['title'] or '—'}")
|
|
269
|
+
|
|
270
|
+
dry_run = _a(args, "dry_run", False)
|
|
271
|
+
force = _a(args, "force", False)
|
|
272
|
+
print(f"\n {_('delete.cost_total', cost=f'${total_cost:.4f}')}")
|
|
273
|
+
|
|
274
|
+
if dry_run:
|
|
275
|
+
print(f"\n{_('delete.dry_run')}")
|
|
276
|
+
return 0
|
|
277
|
+
|
|
278
|
+
if not force and not confirm(_("delete.mass_confirm", n=len(target_rows))):
|
|
279
|
+
print(_("canceled"))
|
|
280
|
+
return 0
|
|
281
|
+
|
|
282
|
+
ids = [r["id"] for r in target_rows]
|
|
283
|
+
placeholders = ",".join("?" for _ in ids)
|
|
284
|
+
|
|
285
|
+
db.execute(f"DELETE FROM part WHERE session_id IN ({placeholders})", ids)
|
|
286
|
+
db.execute(f"DELETE FROM message WHERE session_id IN ({placeholders})", ids)
|
|
287
|
+
db.execute(f"DELETE FROM todo WHERE session_id IN ({placeholders})", ids)
|
|
288
|
+
db.execute(f"DELETE FROM session WHERE id IN ({placeholders})", ids)
|
|
289
|
+
db.commit()
|
|
290
|
+
|
|
291
|
+
print(f"\n{_('delete.done_mass', n=len(ids))}")
|
|
292
|
+
return 0
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _parse_choices(text: str, max_n: int) -> list[int]:
|
|
296
|
+
"""Парсит ввод пользователя вида '1,3-5,7' в список индексов."""
|
|
297
|
+
result = set()
|
|
298
|
+
for part in text.replace(" ", "").split(","):
|
|
299
|
+
if not part:
|
|
300
|
+
continue
|
|
301
|
+
if "-" in part:
|
|
302
|
+
try:
|
|
303
|
+
a, b = part.split("-", 1)
|
|
304
|
+
for i in range(int(a), int(b) + 1):
|
|
305
|
+
if 1 <= i <= max_n:
|
|
306
|
+
result.add(i)
|
|
307
|
+
except ValueError:
|
|
308
|
+
continue
|
|
309
|
+
else:
|
|
310
|
+
try:
|
|
311
|
+
i = int(part)
|
|
312
|
+
if 1 <= i <= max_n:
|
|
313
|
+
result.add(i)
|
|
314
|
+
except ValueError:
|
|
315
|
+
continue
|
|
316
|
+
return sorted(result)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _a(args, name: str, default=None):
|
|
320
|
+
"""getattr с дефолтом — для совместимости Namespace из разных парсеров."""
|
|
321
|
+
return getattr(args, name, default)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def run(args, db) -> int:
|
|
325
|
+
if _a(args, "interactive", False):
|
|
326
|
+
return _delete_interactive(db, args)
|
|
327
|
+
|
|
328
|
+
if _a(args, "session_id"):
|
|
329
|
+
session_id = _a(args, "session_id")
|
|
330
|
+
dry_run = _a(args, "dry_run", False)
|
|
331
|
+
force = _a(args, "force", False)
|
|
332
|
+
return _delete_single(db, session_id, dry_run, force)
|
|
333
|
+
|
|
334
|
+
older_than = _a(args, "older_than")
|
|
335
|
+
before = _a(args, "before")
|
|
336
|
+
after = _a(args, "after")
|
|
337
|
+
keep_last = _a(args, "keep_last")
|
|
338
|
+
project = _a(args, "project")
|
|
339
|
+
if older_than or before or after or keep_last or project:
|
|
340
|
+
return _delete_mass(db, args)
|
|
341
|
+
|
|
342
|
+
print(_("delete.usage"))
|
|
343
|
+
return 1
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Команда prune: массовая очистка (alias для delete)."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from cmd_delete import run as delete_run
|
|
6
|
+
from i18n import _
|
|
7
|
+
from utils import build_help_epilog
|
|
8
|
+
|
|
9
|
+
_PRUNE_EXAMPLES = [
|
|
10
|
+
("--older-than 90d", "help.prune.e0"),
|
|
11
|
+
("--keep-last 20", "help.prune.e1"),
|
|
12
|
+
("--dry-run", "help.prune.e2"),
|
|
13
|
+
("--force", "help.prune.e3"),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register(subparsers) -> None:
|
|
18
|
+
p = subparsers.add_parser(
|
|
19
|
+
"prune",
|
|
20
|
+
help=_("help.cmd.prune"),
|
|
21
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
22
|
+
epilog=build_help_epilog("prune", _PRUNE_EXAMPLES),
|
|
23
|
+
)
|
|
24
|
+
p.add_argument("--older-than", type=str, help="Delete sessions older than (30d, 6m, 1y)")
|
|
25
|
+
p.add_argument("--keep-last", type=int, help="Keep N most recent sessions")
|
|
26
|
+
p.add_argument("--project", type=str, help="Limit to project")
|
|
27
|
+
p.add_argument("--dry-run", action="store_true", help="Preview without deleting")
|
|
28
|
+
p.add_argument("--force", "-f", action="store_true", help="Skip confirmation")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run(args, db) -> int:
|
|
32
|
+
return delete_run(args, db)
|
|
@@ -8,7 +8,9 @@ import os
|
|
|
8
8
|
|
|
9
9
|
# --- Пути ---
|
|
10
10
|
# База данных opencode. Стандартное расположение для Linux/macOS.
|
|
11
|
-
OPENCODE_DB = os.
|
|
11
|
+
OPENCODE_DB = os.environ.get("OPENCODE_DB") or os.path.expanduser(
|
|
12
|
+
"~/.local/share/opencode/opencode.db"
|
|
13
|
+
)
|
|
12
14
|
|
|
13
15
|
# Директории, игнорируемые при сборе snapshot'а проекта
|
|
14
16
|
IGNORE_DIRS = {".git", "node_modules", ".venv", "__pycache__", ".opencode"}
|
|
@@ -185,6 +185,40 @@ L: dict[str, dict[str, str]] = {
|
|
|
185
185
|
"ru": " Сообщений: {msg}, частей: {parts}",
|
|
186
186
|
"en": " Messages: {msg}, parts: {parts}",
|
|
187
187
|
},
|
|
188
|
+
"delete.none": {"ru": " ✅ Нет сессий для удаления.", "en": " ✅ No sessions to delete."},
|
|
189
|
+
"delete.filter_required": {
|
|
190
|
+
"ru": "❌ Укажи ID сессии, фильтр, или --interactive",
|
|
191
|
+
"en": "❌ Specify session ID, a filter, or --interactive",
|
|
192
|
+
},
|
|
193
|
+
"delete.usage": {
|
|
194
|
+
"ru": " Использование: opencode-db delete <session_id>",
|
|
195
|
+
"en": " Usage: opencode-db delete <session_id>",
|
|
196
|
+
},
|
|
197
|
+
"delete.bad_spec": {
|
|
198
|
+
"ru": "❌ Не могу разобрать: --older-than {spec}",
|
|
199
|
+
"en": "❌ Cannot parse: --older-than {spec}",
|
|
200
|
+
},
|
|
201
|
+
"delete.bad_date": {
|
|
202
|
+
"ru": "❌ Неверный формат даты: {date}. Используй ГГГГ-ММ-ДД",
|
|
203
|
+
"en": "❌ Invalid date format: {date}. Use YYYY-MM-DD",
|
|
204
|
+
},
|
|
205
|
+
"delete.mass_header": {
|
|
206
|
+
"ru": "Будет удалено сессий: {n}",
|
|
207
|
+
"en": "Sessions to delete: {n}",
|
|
208
|
+
},
|
|
209
|
+
"delete.cost_total": {
|
|
210
|
+
"ru": "Стоимость удаляемых: {cost}",
|
|
211
|
+
"en": "Cost of deleted sessions: {cost}",
|
|
212
|
+
},
|
|
213
|
+
"delete.tokens_total": {"ru": "Освободится токенов: ~{n}", "en": "Tokens freed: ~{n}"},
|
|
214
|
+
"delete.mass_confirm": {"ru": "Удалить {n} сессий?", "en": "Delete {n} sessions?"},
|
|
215
|
+
"delete.done_mass": {"ru": " ✅ Удалено {n} сессий.", "en": " ✅ Deleted {n} sessions."},
|
|
216
|
+
"delete.interactive_header": {"ru": "Последние сессии:", "en": "Recent sessions:"},
|
|
217
|
+
"delete.interactive_prompt": {
|
|
218
|
+
"ru": " Введи номера (1,3-5) или Enter для отмены: ",
|
|
219
|
+
"en": " Enter numbers (1,3-5) or Enter to cancel: ",
|
|
220
|
+
},
|
|
221
|
+
"delete.no_sessions": {"ru": "❌ Нет сессий для удаления.", "en": "❌ No sessions to delete."},
|
|
188
222
|
# ==================================================================
|
|
189
223
|
# COSTS
|
|
190
224
|
# ==================================================================
|
|
@@ -369,7 +403,7 @@ L: dict[str, dict[str, str]] = {
|
|
|
369
403
|
"help.cmd.list": {"ru": "Список сессий", "en": "List sessions"},
|
|
370
404
|
"help.cmd.info": {"ru": "Детальная информация о сессии", "en": "Session details"},
|
|
371
405
|
"help.cmd.export": {"ru": "Экспорт диалога в Markdown", "en": "Export dialog to Markdown"},
|
|
372
|
-
"help.cmd.delete": {"ru": "Удаление
|
|
406
|
+
"help.cmd.delete": {"ru": "Удаление сессий", "en": "Delete sessions"},
|
|
373
407
|
"help.cmd.costs": {"ru": "Анализ расходов на токены", "en": "Token cost analysis"},
|
|
374
408
|
"help.cmd.prune": {"ru": "Массовая очистка сессий", "en": "Bulk session cleanup"},
|
|
375
409
|
"help.cmd.search": {"ru": "Поиск по сообщениям", "en": "Search messages"},
|
|
@@ -408,6 +442,22 @@ L: dict[str, dict[str, str]] = {
|
|
|
408
442
|
"help.delete.e0": {"ru": "Удалить с подтверждением", "en": "Delete with confirmation"},
|
|
409
443
|
"help.delete.e1": {"ru": "Показать что будет удалено", "en": "Preview what will be deleted"},
|
|
410
444
|
"help.delete.e2": {"ru": "Без подтверждения", "en": "Skip confirmation"},
|
|
445
|
+
"help.delete.e3": {
|
|
446
|
+
"ru": "Удалить сессии старше 90 дней",
|
|
447
|
+
"en": "Delete sessions older than 90 days",
|
|
448
|
+
},
|
|
449
|
+
"help.delete.e4": {
|
|
450
|
+
"ru": "Оставить 30 последних, остальное удалить",
|
|
451
|
+
"en": "Keep 30 most recent, delete the rest",
|
|
452
|
+
},
|
|
453
|
+
"help.delete.e5": {
|
|
454
|
+
"ru": "Удалить сессии до указанной даты",
|
|
455
|
+
"en": "Delete sessions before a date",
|
|
456
|
+
},
|
|
457
|
+
"help.delete.e6": {
|
|
458
|
+
"ru": "Интерактивный выбор из списка",
|
|
459
|
+
"en": "Choose sessions from a list",
|
|
460
|
+
},
|
|
411
461
|
"help.costs.e0": {"ru": "Топ сессий по расходам", "en": "Top sessions by cost"},
|
|
412
462
|
"help.costs.e1": {"ru": "Детально по сессии", "en": "Single session breakdown"},
|
|
413
463
|
"help.costs.e2": {"ru": "Сумма по всей БД", "en": "Total across all sessions"},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opencode-db
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: OpenCode database CLI manager — browse, export, analyze, clean up sessions
|
|
5
5
|
Project-URL: Homepage, https://github.com/VasilevNStas/opencode-db
|
|
6
6
|
Project-URL: Source, https://github.com/VasilevNStas/opencode-db
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "opencode-db"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.1"
|
|
8
8
|
description = "OpenCode database CLI manager — browse, export, analyze, clean up sessions"
|
|
9
9
|
readme = "README_PYPI.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -477,7 +477,7 @@ class TestPruneCommand:
|
|
|
477
477
|
from cmd_prune import run
|
|
478
478
|
|
|
479
479
|
args = _ns(older_than="30d", keep_last=None, project=None, dry_run=False, force=False)
|
|
480
|
-
with patch("
|
|
480
|
+
with patch("cmd_delete.confirm", return_value=False):
|
|
481
481
|
assert run(args, db) == 0
|
|
482
482
|
assert _count_sessions(db) == 4
|
|
483
483
|
|
|
@@ -37,6 +37,19 @@ def summarize_val(v, max_len=120) -> str:
|
|
|
37
37
|
return s
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
def parse_date(date_str: str) -> datetime | None:
|
|
41
|
+
"""Парсит дату в формате ГГГГ-ММ-ДД."""
|
|
42
|
+
if not date_str or not isinstance(date_str, str):
|
|
43
|
+
return None
|
|
44
|
+
try:
|
|
45
|
+
parts = date_str.strip().split("-")
|
|
46
|
+
if len(parts) != 3:
|
|
47
|
+
return None
|
|
48
|
+
return datetime(int(parts[0]), int(parts[1]), int(parts[2]), tzinfo=UTC)
|
|
49
|
+
except (ValueError, IndexError):
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
40
53
|
def parse_time_spec(spec) -> None | datetime:
|
|
41
54
|
"""Парсит спецификацию времени ('30d', '6m', '1y') в timedelta."""
|
|
42
55
|
if not spec or not isinstance(spec, str):
|
opencode_db-0.5.1/cmd_delete.py
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
"""Команда delete: удаление сессии."""
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
|
|
5
|
-
from db import (
|
|
6
|
-
SessionError,
|
|
7
|
-
get_message_count,
|
|
8
|
-
get_part_count,
|
|
9
|
-
get_session_info,
|
|
10
|
-
get_session_title,
|
|
11
|
-
parse_model,
|
|
12
|
-
)
|
|
13
|
-
from i18n import _
|
|
14
|
-
from utils import build_help_epilog, confirm, format_cost, format_ts
|
|
15
|
-
|
|
16
|
-
_DELETE_EXAMPLES = [
|
|
17
|
-
("<session_id>", "help.delete.e0"),
|
|
18
|
-
("<session_id> --dry-run", "help.delete.e1"),
|
|
19
|
-
("<session_id> --force", "help.delete.e2"),
|
|
20
|
-
]
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def register(subparsers) -> None:
|
|
24
|
-
p = subparsers.add_parser(
|
|
25
|
-
"delete",
|
|
26
|
-
help=_("help.cmd.delete"),
|
|
27
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
28
|
-
epilog=build_help_epilog("delete", _DELETE_EXAMPLES),
|
|
29
|
-
)
|
|
30
|
-
p.add_argument("session_id", help="Session ID to delete")
|
|
31
|
-
p.add_argument("--dry-run", action="store_true", help="Preview without deleting")
|
|
32
|
-
p.add_argument("--force", "-f", action="store_true", help="Skip confirmation")
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def run(args, db) -> int:
|
|
36
|
-
try:
|
|
37
|
-
info = get_session_info(db, args.session_id)
|
|
38
|
-
except SessionError as e:
|
|
39
|
-
print(e.message)
|
|
40
|
-
return 1
|
|
41
|
-
session_id = info["id"]
|
|
42
|
-
title = get_session_title(info)
|
|
43
|
-
model = parse_model(info)
|
|
44
|
-
|
|
45
|
-
msg_count = get_message_count(db, session_id)
|
|
46
|
-
part_count = get_part_count(db, session_id)
|
|
47
|
-
created = format_ts(info["time_created"])
|
|
48
|
-
|
|
49
|
-
print(f"\n{'=' * 50}")
|
|
50
|
-
print(f" {_('delete.header')}")
|
|
51
|
-
print(f"{'=' * 50}")
|
|
52
|
-
print(f" {_('delete.id')}: {session_id}")
|
|
53
|
-
print(f" {_('delete.title')}: {title}")
|
|
54
|
-
print(f" {_('delete.model')}: {model}")
|
|
55
|
-
print(f" {_('delete.created')}: {created}")
|
|
56
|
-
print(f" {_('delete.messages')}: {msg_count}")
|
|
57
|
-
print(f" {_('delete.parts')}: {part_count}")
|
|
58
|
-
print(f" {_('delete.cost')}: {format_cost(info['cost'])}")
|
|
59
|
-
|
|
60
|
-
if args.dry_run:
|
|
61
|
-
print(f"\n{_('delete.dry_run')}")
|
|
62
|
-
print(f" {_('delete.dry_run_hint')}")
|
|
63
|
-
return 0
|
|
64
|
-
|
|
65
|
-
if not args.force and not confirm(_("delete.confirm", id=session_id[:24])):
|
|
66
|
-
print(_("canceled"))
|
|
67
|
-
return 0
|
|
68
|
-
|
|
69
|
-
db.execute("DELETE FROM session WHERE id = ?", (session_id,))
|
|
70
|
-
db.commit()
|
|
71
|
-
|
|
72
|
-
print(f"\n{_('delete.done', id=session_id[:24])}")
|
|
73
|
-
print(_("delete.done_detail", msg=msg_count, parts=part_count))
|
|
74
|
-
return 0
|
opencode_db-0.5.1/cmd_prune.py
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
"""Команда prune: массовая очистка старых сессий."""
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
from typing import Literal
|
|
5
|
-
|
|
6
|
-
from i18n import _
|
|
7
|
-
from utils import build_help_epilog, confirm, format_ts, parse_time_spec
|
|
8
|
-
|
|
9
|
-
_PRUNE_EXAMPLES = [
|
|
10
|
-
("--older-than 90d", "help.prune.e0"),
|
|
11
|
-
("--keep-last 20", "help.prune.e1"),
|
|
12
|
-
("--dry-run", "help.prune.e2"),
|
|
13
|
-
("--force", "help.prune.e3"),
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def register(subparsers) -> None:
|
|
18
|
-
p = subparsers.add_parser(
|
|
19
|
-
"prune",
|
|
20
|
-
help=_("help.cmd.prune"),
|
|
21
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
22
|
-
epilog=build_help_epilog("prune", _PRUNE_EXAMPLES),
|
|
23
|
-
)
|
|
24
|
-
p.add_argument("--older-than", type=str, help="Delete sessions older than (30d, 6m, 1y)")
|
|
25
|
-
p.add_argument("--keep-last", type=int, help="Keep N most recent sessions")
|
|
26
|
-
p.add_argument("--project", type=str, help="Limit to project")
|
|
27
|
-
p.add_argument("--dry-run", action="store_true", help="Preview without deleting")
|
|
28
|
-
p.add_argument("--force", "-f", action="store_true", help="Skip confirmation")
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def run(args, db) -> Literal[1] | Literal[0]:
|
|
32
|
-
if not args.older_than and not args.keep_last and not args.project:
|
|
33
|
-
print(_("prune.filter_required"))
|
|
34
|
-
print(_("prune.filter_example"))
|
|
35
|
-
return 1
|
|
36
|
-
|
|
37
|
-
conditions = []
|
|
38
|
-
params = []
|
|
39
|
-
|
|
40
|
-
if args.project:
|
|
41
|
-
conditions.append("s.project_id = ?")
|
|
42
|
-
params.append(args.project)
|
|
43
|
-
|
|
44
|
-
if args.older_than:
|
|
45
|
-
cutoff = parse_time_spec(args.older_than)
|
|
46
|
-
if cutoff is None:
|
|
47
|
-
print(_("prune.bad_spec", spec=args.older_than))
|
|
48
|
-
print(_("prune.bad_spec_hint"))
|
|
49
|
-
return 1
|
|
50
|
-
cutoff_ms = int(cutoff.timestamp() * 1000)
|
|
51
|
-
conditions.append("s.time_created < ?")
|
|
52
|
-
params.append(cutoff_ms)
|
|
53
|
-
|
|
54
|
-
exclude_ids = set()
|
|
55
|
-
if args.keep_last:
|
|
56
|
-
inner_where = " AND ".join(conditions) if conditions else "1=1"
|
|
57
|
-
keep_rows = db.execute(
|
|
58
|
-
f"""
|
|
59
|
-
SELECT s.id FROM session s
|
|
60
|
-
WHERE {inner_where}
|
|
61
|
-
ORDER BY s.time_created DESC
|
|
62
|
-
LIMIT ?
|
|
63
|
-
""",
|
|
64
|
-
(*params, args.keep_last),
|
|
65
|
-
).fetchall()
|
|
66
|
-
exclude_ids = {r["id"] for r in keep_rows}
|
|
67
|
-
|
|
68
|
-
where = " AND ".join(conditions) if conditions else "1=1"
|
|
69
|
-
|
|
70
|
-
if exclude_ids:
|
|
71
|
-
placeholders = ",".join("?" for _ in exclude_ids)
|
|
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
|
-
AND s.id NOT IN ({placeholders})
|
|
78
|
-
ORDER BY s.time_created DESC
|
|
79
|
-
""",
|
|
80
|
-
(*params, *exclude_ids),
|
|
81
|
-
).fetchall()
|
|
82
|
-
else:
|
|
83
|
-
target_rows = db.execute(
|
|
84
|
-
f"""
|
|
85
|
-
SELECT s.id, s.title, s.model, s.time_created, s.cost
|
|
86
|
-
FROM session s
|
|
87
|
-
WHERE {where}
|
|
88
|
-
ORDER BY s.time_created DESC
|
|
89
|
-
""",
|
|
90
|
-
params,
|
|
91
|
-
).fetchall()
|
|
92
|
-
|
|
93
|
-
if not target_rows:
|
|
94
|
-
print(_("prune.none"))
|
|
95
|
-
return 0
|
|
96
|
-
|
|
97
|
-
total_cost = sum(r["cost"] or 0 for r in target_rows)
|
|
98
|
-
total_tokens = sum(
|
|
99
|
-
db.execute(
|
|
100
|
-
"SELECT COALESCE(SUM(tokens_input + tokens_output), 0) FROM session WHERE id = ?",
|
|
101
|
-
(r["id"],),
|
|
102
|
-
).fetchone()[0]
|
|
103
|
-
for r in target_rows
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
print(f"\n {'=' * 55}")
|
|
107
|
-
print(f" {_('prune.header', n=len(target_rows))}")
|
|
108
|
-
print(f" {'=' * 55}")
|
|
109
|
-
|
|
110
|
-
for r in target_rows[:20]:
|
|
111
|
-
created = format_ts(r["time_created"])
|
|
112
|
-
print(f" • {r['id'][:24]} {created} {r['title'] or '—'}")
|
|
113
|
-
|
|
114
|
-
if len(target_rows) > 20:
|
|
115
|
-
print(f" … и ещё {len(target_rows) - 20}")
|
|
116
|
-
|
|
117
|
-
print(f"\n {_('prune.cost_total', cost=f'${total_cost:.4f}')}")
|
|
118
|
-
print(f" {_('prune.tokens_total', n=str(total_tokens))}")
|
|
119
|
-
|
|
120
|
-
if args.dry_run:
|
|
121
|
-
print(f"\n{_('prune.dry_run')}")
|
|
122
|
-
return 0
|
|
123
|
-
|
|
124
|
-
if not args.force and not confirm(_("prune.confirm", n=len(target_rows))):
|
|
125
|
-
print(_("canceled"))
|
|
126
|
-
return 0
|
|
127
|
-
|
|
128
|
-
ids_to_delete = [r["id"] for r in target_rows]
|
|
129
|
-
placeholders = ",".join("?" for _ in ids_to_delete)
|
|
130
|
-
|
|
131
|
-
db.execute(f"DELETE FROM part WHERE session_id IN ({placeholders})", ids_to_delete)
|
|
132
|
-
db.execute(f"DELETE FROM message WHERE session_id IN ({placeholders})", ids_to_delete)
|
|
133
|
-
db.execute(f"DELETE FROM todo WHERE session_id IN ({placeholders})", ids_to_delete)
|
|
134
|
-
db.execute(f"DELETE FROM session WHERE id IN ({placeholders})", ids_to_delete)
|
|
135
|
-
db.commit()
|
|
136
|
-
|
|
137
|
-
print(f"\n{_('prune.done', n=len(ids_to_delete))}")
|
|
138
|
-
return 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|