opencode-db 0.7.3__tar.gz → 0.8.0__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.7.3/opencode_db.egg-info → opencode_db-0.8.0}/PKG-INFO +5 -1
- {opencode_db-0.7.3 → opencode_db-0.8.0}/README.md +45 -6
- {opencode_db-0.7.3 → opencode_db-0.8.0}/docs/README_PYPI.md +4 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/cli.py +1 -1
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/__init__.py +2 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_costs.py +18 -49
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_delete.py +5 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_export.py +24 -30
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_help.py +8 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_info.py +13 -22
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_list.py +11 -43
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_projects.py +2 -12
- opencode_db-0.8.0/opencode_db/commands/cmd_schema.py +56 -0
- opencode_db-0.8.0/opencode_db/commands/cmd_search.py +71 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_stats.py +11 -38
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_todos.py +11 -27
- opencode_db-0.8.0/opencode_db/commands/cmd_tree.py +101 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_vacuum.py +11 -4
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_view.py +31 -34
- opencode_db-0.8.0/opencode_db/config.py +40 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/db.py +32 -45
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/i18n.py +34 -0
- opencode_db-0.8.0/opencode_db/schema.py +243 -0
- opencode_db-0.8.0/opencode_db/schema_manifest.json +500 -0
- opencode_db-0.8.0/opencode_db/store/__init__.py +20 -0
- opencode_db-0.8.0/opencode_db/store/base.py +286 -0
- opencode_db-0.8.0/opencode_db/store/v1.py +253 -0
- opencode_db-0.8.0/opencode_db/store/v2.py +459 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0/opencode_db.egg-info}/PKG-INFO +5 -1
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db.egg-info/SOURCES.txt +9 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/pyproject.toml +4 -1
- {opencode_db-0.7.3 → opencode_db-0.8.0}/tests/test_commands.py +1 -1
- opencode_db-0.8.0/tests/test_schema.py +133 -0
- opencode_db-0.8.0/tests/test_store_v2.py +282 -0
- opencode_db-0.7.3/opencode_db/commands/cmd_search.py +0 -124
- opencode_db-0.7.3/opencode_db/commands/cmd_tree.py +0 -110
- opencode_db-0.7.3/opencode_db/config.py +0 -16
- {opencode_db-0.7.3 → opencode_db-0.8.0}/LICENSE +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/__init__.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/__main__.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_prune.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/formatters.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/markdown_to_ansi.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/runtime.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/utils.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db.egg-info/dependency_links.txt +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db.egg-info/entry_points.txt +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db.egg-info/requires.txt +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db.egg-info/top_level.txt +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/setup.cfg +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/tests/test_db.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/tests/test_formatters.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/tests/test_i18n.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/tests/test_runtime.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.0}/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.8.0
|
|
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
|
|
@@ -34,6 +34,7 @@ uv tool install opencode-db
|
|
|
34
34
|
opencode-db list # recent sessions
|
|
35
35
|
opencode-db view # interactive session viewer (colored, scrollable)
|
|
36
36
|
opencode-db stats # database summary
|
|
37
|
+
opencode-db schema # detect v1/v2 schema
|
|
37
38
|
opencode-db costs --total # total token costs
|
|
38
39
|
opencode-db export # export dialog to .md (interactive)
|
|
39
40
|
opencode-db help # full reference
|
|
@@ -46,6 +47,7 @@ opencode-db help # full reference
|
|
|
46
47
|
- **`export`** — экспорт диалога в Markdown (с поддержкой Obsidian)
|
|
47
48
|
- **`delete`** / **`prune`** — удаление и массовая очистка с фильтрами
|
|
48
49
|
- **`costs`** / **`stats`** — аналитика токенов и расходов
|
|
50
|
+
- **`schema`** — определяет v1/v2-схему OpenCode, миграции и доступные операции
|
|
49
51
|
- **`--db-path`** / **`OPENCODE_DB`** — кастомный путь к БД
|
|
50
52
|
|
|
51
53
|
## Optional extras
|
|
@@ -60,3 +62,5 @@ pip install opencode-db # zero external dependencies
|
|
|
60
62
|
- Python 3.12+ — the CLI checks the interpreter at startup and exits with a clear message instead of an `ImportError` on older versions
|
|
61
63
|
- [OpenCode](https://opencode.ai) (must have been run at least once)
|
|
62
64
|
- macOS / Linux
|
|
65
|
+
|
|
66
|
+
**Schema support:** OpenCode 1.19.x (v1) — read and write; OpenCode 2.x preview (v2) — read-only. Run `opencode-db schema` to check the detected schema. On v2, `delete`, `prune`, and `vacuum` are refused until write support is implemented.
|
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
[Русская версия](docs/README.ru.md)
|
|
12
12
|
|
|
13
|
+
> **Published on PyPI:** [opencode-db](https://pypi.org/project/opencode-db/) — install the ready-made package with `uv tool install opencode-db` or `pip install opencode-db`.
|
|
14
|
+
|
|
13
15
|
```
|
|
14
16
|
opencode-db help # full reference with examples
|
|
15
17
|
opencode-db list # list recent sessions
|
|
16
18
|
opencode-db stats # database summary
|
|
19
|
+
opencode-db schema # detect v1/v2 schema
|
|
17
20
|
opencode-db export # save a dialog to .md (interactive)
|
|
18
21
|
opencode-db costs --total # total token costs
|
|
19
22
|
```
|
|
@@ -83,7 +86,7 @@ pipx install opencode-db
|
|
|
83
86
|
pip install opencode-db
|
|
84
87
|
```
|
|
85
88
|
|
|
86
|
-
|
|
89
|
+
**Zero external dependencies** — runs on the Python standard library alone. Only Python 3.12+ is required; `rich` is optional for enhanced Markdown rendering.
|
|
87
90
|
|
|
88
91
|
### Manual install
|
|
89
92
|
|
|
@@ -412,6 +415,26 @@ opencode-db stats --json # JSON output
|
|
|
412
415
|
|
|
413
416
|
---
|
|
414
417
|
|
|
418
|
+
### `schema` — detect database schema
|
|
419
|
+
|
|
420
|
+
Shows which OpenCode schema family the database uses, the applied migration, and which operations are supported.
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
opencode-db schema # human-readable summary
|
|
424
|
+
opencode-db schema --json # JSON output
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Supported families:
|
|
428
|
+
|
|
429
|
+
| OpenCode | Schema | Support |
|
|
430
|
+
|---|---|---|
|
|
431
|
+
| 1.19.x | v1 | read + write |
|
|
432
|
+
| 2.x preview | v2 | read-only |
|
|
433
|
+
|
|
434
|
+
On a v2 database, `delete`, `prune`, and `vacuum` are refused until write support is implemented. Detection is based on tables and the migration journal, not on the OpenCode version string.
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
415
438
|
### `todos` — view tasks from sessions
|
|
416
439
|
|
|
417
440
|
Displays todo items created during OpenCode sessions.
|
|
@@ -517,7 +540,7 @@ opencode-db/
|
|
|
517
540
|
│ ├── config.py ← constants and paths
|
|
518
541
|
│ ├── db.py ← SQLite queries
|
|
519
542
|
│ ├── formatters.py ← tables, JSON, Markdown, snapshots
|
|
520
|
-
│ ├── i18n.py ← localization (EN/RU,
|
|
543
|
+
│ ├── i18n.py ← localization (EN/RU, 296 keys)
|
|
521
544
|
│ ├── markdown_to_ansi.py ← Markdown → ANSI renderer for `view`
|
|
522
545
|
│ ├── runtime.py ← Python 3.12+ version guard
|
|
523
546
|
│ ├── utils.py ← date/token formatting, validation
|
|
@@ -557,11 +580,14 @@ opencode-db/
|
|
|
557
580
|
2. `runtime.require_python()` checks the interpreter before importing `cli`/`db`; on Python < 3.12 it prints a clear message with remediation steps instead of failing with `ImportError`.
|
|
558
581
|
3. `cli.py` builds an `argparse` parser with subparsers from every command module and dispatches the call.
|
|
559
582
|
4. Each command is a separate `opencode_db/commands/cmd_*.py` module exporting two functions: `register(subparsers)` and `run(args, db)`.
|
|
560
|
-
5. `db.py` opens a connection to OpenCode's SQLite database (`~/.local/share/opencode/opencode.db`).
|
|
561
|
-
6.
|
|
583
|
+
5. `db.py` opens a connection to OpenCode's SQLite database (`~/.local/share/opencode/opencode.db`, with `opencode-next.db` fallback) and detects the schema family.
|
|
584
|
+
6. `store/` selects `V1Store` or `V2Store`; commands read through the same interface regardless of the underlying schema.
|
|
585
|
+
7. Destructive operations (`delete`, `prune`) always require confirmation (or `--force`); on v2 they are refused until write support lands.
|
|
562
586
|
|
|
563
587
|
### Database schema (key tables)
|
|
564
588
|
|
|
589
|
+
Stable OpenCode 1.19.x (v1):
|
|
590
|
+
|
|
565
591
|
```
|
|
566
592
|
session — sessions: id, title, model (JSON), agent, cost, tokens_*, parent_id, summary_*
|
|
567
593
|
message — messages: id, session_id, time_created, data (JSON: role, agent)
|
|
@@ -570,6 +596,19 @@ todo — tasks: session_id, content, status, priority
|
|
|
570
596
|
project — projects: id, worktree, name
|
|
571
597
|
```
|
|
572
598
|
|
|
599
|
+
Preview OpenCode 2.x (v2):
|
|
600
|
+
|
|
601
|
+
```
|
|
602
|
+
session_v2 — renamed session table with the same usage fields
|
|
603
|
+
session_message — typed JSON messages: user, assistant, synthetic, system, shell, compaction, ...
|
|
604
|
+
project — projects: id, worktree, name, time_active, ...
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
v2 has no `message`, `part`, or `todo` tables. Assistant content is nested in
|
|
608
|
+
`session_message.data.content`, and todos are derived from the latest completed
|
|
609
|
+
`todowrite` tool call. Use `opencode-db schema` to inspect the detected family
|
|
610
|
+
before running write commands.
|
|
611
|
+
|
|
573
612
|
---
|
|
574
613
|
|
|
575
614
|
## Using inside OpenCode TUI
|
|
@@ -650,7 +689,7 @@ opencode-db --lang en help # English help text
|
|
|
650
689
|
opencode-db --lang en stats # English statistics
|
|
651
690
|
```
|
|
652
691
|
|
|
653
|
-
The `--lang` flag applies to any command. All
|
|
692
|
+
The `--lang` flag applies to any command. All 296 UI strings are translated. Data content (session titles, project names, etc.) remains in its original language.
|
|
654
693
|
|
|
655
694
|
The language can also be set via environment variable `OPENCODE_DB_LANG=en`.
|
|
656
695
|
|
|
@@ -735,7 +774,7 @@ source .venv/bin/activate
|
|
|
735
774
|
uv pip install -e ".[rich]"
|
|
736
775
|
uv pip install pytest ruff==0.17.1
|
|
737
776
|
|
|
738
|
-
make check # check-python + ruff +
|
|
777
|
+
make check # check-python + ruff + 205 tests
|
|
739
778
|
```
|
|
740
779
|
|
|
741
780
|
`make check` fails fast with a clear message if the active interpreter is older than 3.12.
|
|
@@ -18,6 +18,7 @@ uv tool install opencode-db
|
|
|
18
18
|
opencode-db list # recent sessions
|
|
19
19
|
opencode-db view # interactive session viewer (colored, scrollable)
|
|
20
20
|
opencode-db stats # database summary
|
|
21
|
+
opencode-db schema # detect v1/v2 schema
|
|
21
22
|
opencode-db costs --total # total token costs
|
|
22
23
|
opencode-db export # export dialog to .md (interactive)
|
|
23
24
|
opencode-db help # full reference
|
|
@@ -30,6 +31,7 @@ opencode-db help # full reference
|
|
|
30
31
|
- **`export`** — экспорт диалога в Markdown (с поддержкой Obsidian)
|
|
31
32
|
- **`delete`** / **`prune`** — удаление и массовая очистка с фильтрами
|
|
32
33
|
- **`costs`** / **`stats`** — аналитика токенов и расходов
|
|
34
|
+
- **`schema`** — определяет v1/v2-схему OpenCode, миграции и доступные операции
|
|
33
35
|
- **`--db-path`** / **`OPENCODE_DB`** — кастомный путь к БД
|
|
34
36
|
|
|
35
37
|
## Optional extras
|
|
@@ -44,3 +46,5 @@ pip install opencode-db # zero external dependencies
|
|
|
44
46
|
- Python 3.12+ — the CLI checks the interpreter at startup and exits with a clear message instead of an `ImportError` on older versions
|
|
45
47
|
- [OpenCode](https://opencode.ai) (must have been run at least once)
|
|
46
48
|
- macOS / Linux
|
|
49
|
+
|
|
50
|
+
**Schema support:** OpenCode 1.19.x (v1) — read and write; OpenCode 2.x preview (v2) — read-only. Run `opencode-db schema` to check the detected schema. On v2, `delete`, `prune`, and `vacuum` are refused until write support is implemented.
|
|
@@ -74,7 +74,7 @@ def main(argv=None) -> Any | Literal[0] | Literal[1]:
|
|
|
74
74
|
if args.db_path:
|
|
75
75
|
os.environ["OPENCODE_DB"] = args.db_path
|
|
76
76
|
|
|
77
|
-
db = get_db()
|
|
77
|
+
db = get_db(allow_unknown=args.command in {"schema", "help"})
|
|
78
78
|
|
|
79
79
|
try:
|
|
80
80
|
handler = commands.get_handler(args.command)
|
|
@@ -19,6 +19,7 @@ from . import (
|
|
|
19
19
|
cmd_list,
|
|
20
20
|
cmd_projects,
|
|
21
21
|
cmd_prune,
|
|
22
|
+
cmd_schema,
|
|
22
23
|
cmd_search,
|
|
23
24
|
cmd_stats,
|
|
24
25
|
cmd_todos,
|
|
@@ -40,6 +41,7 @@ COMMANDS = {
|
|
|
40
41
|
"tree": cmd_tree,
|
|
41
42
|
"projects": cmd_projects,
|
|
42
43
|
"stats": cmd_stats,
|
|
44
|
+
"schema": cmd_schema,
|
|
43
45
|
"todos": cmd_todos,
|
|
44
46
|
"vacuum": cmd_vacuum,
|
|
45
47
|
"view": cmd_view,
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import argparse
|
|
7
7
|
|
|
8
|
-
from ..db import SessionError
|
|
8
|
+
from ..db import SessionError
|
|
9
9
|
from ..i18n import _
|
|
10
|
+
from ..store import open_store
|
|
10
11
|
from ..utils import build_help_epilog, format_cost, format_tokens
|
|
11
12
|
|
|
12
13
|
_COSTS_EXAMPLES = [
|
|
@@ -33,23 +34,24 @@ def register(subparsers) -> None:
|
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
def run(args, db) -> int:
|
|
37
|
+
store = open_store(db)
|
|
36
38
|
if args.session_id:
|
|
37
|
-
return _show_session(
|
|
39
|
+
return _show_session(store, args.session_id, args.json)
|
|
38
40
|
|
|
39
41
|
if args.total:
|
|
40
|
-
return _show_total(
|
|
42
|
+
return _show_total(store, args.project, args.json)
|
|
41
43
|
|
|
42
|
-
return _show_list(
|
|
44
|
+
return _show_list(store, args.project, args.limit, args.json)
|
|
43
45
|
|
|
44
46
|
|
|
45
|
-
def _show_session(
|
|
47
|
+
def _show_session(store, session_id, as_json) -> int:
|
|
46
48
|
try:
|
|
47
|
-
info = get_session_info(
|
|
49
|
+
info = store.get_session_info(session_id)
|
|
48
50
|
except SessionError as e:
|
|
49
51
|
print(e.message)
|
|
50
52
|
return 1
|
|
51
|
-
title = get_session_title(info)
|
|
52
|
-
model = parse_model(info)
|
|
53
|
+
title = store.get_session_title(info)
|
|
54
|
+
model = store.parse_model(info)
|
|
53
55
|
|
|
54
56
|
ti = info["tokens_input"] or 0
|
|
55
57
|
to = info["tokens_output"] or 0
|
|
@@ -90,23 +92,8 @@ def _show_session(db, session_id, as_json) -> int:
|
|
|
90
92
|
return 0
|
|
91
93
|
|
|
92
94
|
|
|
93
|
-
def _show_total(
|
|
94
|
-
|
|
95
|
-
params = []
|
|
96
|
-
|
|
97
|
-
if project_id:
|
|
98
|
-
where = "s.project_id = ?"
|
|
99
|
-
params.append(project_id)
|
|
100
|
-
|
|
101
|
-
row = db.execute(
|
|
102
|
-
f"""
|
|
103
|
-
SELECT COUNT(*) as cnt,
|
|
104
|
-
SUM(tokens_input) as ti, SUM(tokens_output) as tok_out,
|
|
105
|
-
SUM(tokens_reasoning) as tr, SUM(cost) as c
|
|
106
|
-
FROM session WHERE {where}
|
|
107
|
-
""",
|
|
108
|
-
params,
|
|
109
|
-
).fetchone()
|
|
95
|
+
def _show_total(store, project_id, as_json) -> int:
|
|
96
|
+
row = store.get_cost_total(project_id)
|
|
110
97
|
|
|
111
98
|
cnt = row["cnt"] or 0
|
|
112
99
|
total_cost = row["c"] or 0
|
|
@@ -143,26 +130,8 @@ def _show_total(db, project_id, as_json) -> int:
|
|
|
143
130
|
return 0
|
|
144
131
|
|
|
145
132
|
|
|
146
|
-
def _show_list(
|
|
147
|
-
|
|
148
|
-
params = []
|
|
149
|
-
|
|
150
|
-
if project_id:
|
|
151
|
-
where = "s.project_id = ?"
|
|
152
|
-
params.append(project_id)
|
|
153
|
-
|
|
154
|
-
rows = db.execute(
|
|
155
|
-
f"""
|
|
156
|
-
SELECT s.id, s.title, s.model,
|
|
157
|
-
s.tokens_input, s.tokens_output, s.tokens_reasoning,
|
|
158
|
-
s.cost
|
|
159
|
-
FROM session s
|
|
160
|
-
WHERE {where}
|
|
161
|
-
ORDER BY (s.tokens_input + s.tokens_output) DESC
|
|
162
|
-
LIMIT ?
|
|
163
|
-
""",
|
|
164
|
-
(*params, limit),
|
|
165
|
-
).fetchall()
|
|
133
|
+
def _show_list(store, project_id, limit, as_json) -> int:
|
|
134
|
+
rows = store.get_cost_list(project_id, limit)
|
|
166
135
|
|
|
167
136
|
if as_json:
|
|
168
137
|
from ..formatters import print_json
|
|
@@ -172,8 +141,8 @@ def _show_list(db, project_id, limit, as_json) -> int:
|
|
|
172
141
|
data.append(
|
|
173
142
|
{
|
|
174
143
|
"id": r["id"],
|
|
175
|
-
"title": get_session_title(r),
|
|
176
|
-
"model": parse_model(r),
|
|
144
|
+
"title": store.get_session_title(r),
|
|
145
|
+
"model": store.parse_model(r),
|
|
177
146
|
"tokens_total": (r["tokens_input"] or 0)
|
|
178
147
|
+ (r["tokens_output"] or 0)
|
|
179
148
|
+ (r["tokens_reasoning"] or 0),
|
|
@@ -193,8 +162,8 @@ def _show_list(db, project_id, limit, as_json) -> int:
|
|
|
193
162
|
table.append(
|
|
194
163
|
[
|
|
195
164
|
r["id"][:24],
|
|
196
|
-
get_session_title(r)[:28],
|
|
197
|
-
parse_model(r)[:20],
|
|
165
|
+
store.get_session_title(r)[:28],
|
|
166
|
+
store.parse_model(r)[:20],
|
|
198
167
|
format_tokens(total_tokens),
|
|
199
168
|
format_cost(r["cost"] or 0),
|
|
200
169
|
]
|
|
@@ -12,6 +12,7 @@ from ..db import (
|
|
|
12
12
|
parse_model,
|
|
13
13
|
)
|
|
14
14
|
from ..i18n import _
|
|
15
|
+
from ..store import open_store
|
|
15
16
|
from ..utils import build_help_epilog, confirm, format_cost, format_ts, parse_date, parse_time_spec
|
|
16
17
|
|
|
17
18
|
_DELETE_EXAMPLES = [
|
|
@@ -322,6 +323,10 @@ def _a(args, name: str, default=None):
|
|
|
322
323
|
|
|
323
324
|
|
|
324
325
|
def run(args, db) -> int:
|
|
326
|
+
store = open_store(db)
|
|
327
|
+
if store.schema.read_only:
|
|
328
|
+
print(_("schema.unsupported_write"))
|
|
329
|
+
return 1
|
|
325
330
|
if _a(args, "interactive", False):
|
|
326
331
|
return _delete_interactive(db, args)
|
|
327
332
|
|
|
@@ -9,17 +9,10 @@
|
|
|
9
9
|
import argparse
|
|
10
10
|
import os
|
|
11
11
|
|
|
12
|
-
from ..db import
|
|
13
|
-
SessionError,
|
|
14
|
-
get_latest_session,
|
|
15
|
-
get_messages,
|
|
16
|
-
get_recent_sessions,
|
|
17
|
-
get_session_info,
|
|
18
|
-
get_session_title,
|
|
19
|
-
parse_model,
|
|
20
|
-
)
|
|
12
|
+
from ..db import SessionError
|
|
21
13
|
from ..formatters import collect_snapshot, format_part_to_md, update_log
|
|
22
14
|
from ..i18n import _
|
|
15
|
+
from ..store import open_store
|
|
23
16
|
from ..utils import build_help_epilog, format_cost, format_ts, format_ts_short
|
|
24
17
|
|
|
25
18
|
_EXPORT_EXAMPLES = [
|
|
@@ -62,29 +55,29 @@ def register(subparsers) -> None:
|
|
|
62
55
|
)
|
|
63
56
|
|
|
64
57
|
|
|
65
|
-
def _export_by_id(
|
|
58
|
+
def _export_by_id(store, session_id, output_dir, force, note, full) -> int:
|
|
66
59
|
try:
|
|
67
|
-
session_info = get_session_info(
|
|
60
|
+
session_info = store.get_session_info(session_id)
|
|
68
61
|
except SessionError as e:
|
|
69
62
|
print(e.message)
|
|
70
63
|
return 1
|
|
71
|
-
return _do_export(
|
|
64
|
+
return _do_export(store, session_info, output_dir, force, note, full)
|
|
72
65
|
|
|
73
66
|
|
|
74
|
-
def _export_latest(
|
|
67
|
+
def _export_latest(store, output_dir, force, note, full) -> int:
|
|
75
68
|
try:
|
|
76
|
-
session_id = get_latest_session(
|
|
77
|
-
session_info = get_session_info(
|
|
69
|
+
session_id = store.get_latest_session()
|
|
70
|
+
session_info = store.get_session_info(session_id)
|
|
78
71
|
except SessionError as e:
|
|
79
72
|
print(e.message)
|
|
80
73
|
return 1
|
|
81
|
-
title = get_session_title(session_info)
|
|
74
|
+
title = store.get_session_title(session_info)
|
|
82
75
|
print(_("session.latest", title=title))
|
|
83
|
-
return _do_export(
|
|
76
|
+
return _do_export(store, session_info, output_dir, force, note, full)
|
|
84
77
|
|
|
85
78
|
|
|
86
|
-
def _export_interactive(
|
|
87
|
-
sessions = get_recent_sessions(
|
|
79
|
+
def _export_interactive(store, output_dir, force, note, full) -> int:
|
|
80
|
+
sessions = store.get_recent_sessions()
|
|
88
81
|
|
|
89
82
|
if not sessions:
|
|
90
83
|
print(_("export.no_sessions"))
|
|
@@ -95,7 +88,7 @@ def _export_interactive(db, output_dir, force, note, full) -> int:
|
|
|
95
88
|
print(f" {'─' * 60}")
|
|
96
89
|
|
|
97
90
|
for i, s in enumerate(sessions, 1):
|
|
98
|
-
title = get_session_title(s)
|
|
91
|
+
title = store.get_session_title(s)
|
|
99
92
|
created = format_ts(s["time_created"], "%Y-%m-%d %H:%M")
|
|
100
93
|
cost = format_cost(s["cost"])
|
|
101
94
|
print(f" {i:2d}. {s['id'][:24]} {created} {title[:40]:40s} {cost}")
|
|
@@ -123,22 +116,22 @@ def _export_interactive(db, output_dir, force, note, full) -> int:
|
|
|
123
116
|
print(f" {_('export.interactive_error', n=len(sessions))}")
|
|
124
117
|
|
|
125
118
|
session_id = sessions[idx]["id"]
|
|
126
|
-
title = get_session_title(sessions[idx])
|
|
119
|
+
title = store.get_session_title(sessions[idx])
|
|
127
120
|
print(_("export.interactive_exporting", title=title))
|
|
128
121
|
try:
|
|
129
|
-
session_info = get_session_info(
|
|
122
|
+
session_info = store.get_session_info(session_id)
|
|
130
123
|
except SessionError as e:
|
|
131
124
|
print(e.message)
|
|
132
125
|
return 1
|
|
133
|
-
return _do_export(
|
|
126
|
+
return _do_export(store, session_info, output_dir, force, note, full)
|
|
134
127
|
|
|
135
128
|
|
|
136
|
-
def _do_export(
|
|
129
|
+
def _do_export(store, session_info, output_dir, force, note, full=False) -> int:
|
|
137
130
|
"""Внутренняя функция: экспорт диалога."""
|
|
138
|
-
title = get_session_title(session_info)
|
|
139
|
-
model = parse_model(session_info)
|
|
131
|
+
title = store.get_session_title(session_info)
|
|
132
|
+
model = store.parse_model(session_info)
|
|
140
133
|
|
|
141
|
-
messages = get_messages(
|
|
134
|
+
messages = store.get_messages(session_info["id"])
|
|
142
135
|
|
|
143
136
|
date_str = format_ts_short(session_info["time_created"])
|
|
144
137
|
filename = os.path.join(output_dir, f"opencode-{date_str}.md")
|
|
@@ -207,10 +200,11 @@ def run(args, db) -> int:
|
|
|
207
200
|
output_dir = args.output or os.getcwd()
|
|
208
201
|
full = args.full
|
|
209
202
|
|
|
203
|
+
store = open_store(db)
|
|
210
204
|
if args.session_id:
|
|
211
|
-
return _export_by_id(
|
|
205
|
+
return _export_by_id(store, args.session_id, output_dir, args.force, args.note, full)
|
|
212
206
|
|
|
213
207
|
if args.latest:
|
|
214
|
-
return _export_latest(
|
|
208
|
+
return _export_latest(store, output_dir, args.force, args.note, full)
|
|
215
209
|
|
|
216
|
-
return _export_interactive(
|
|
210
|
+
return _export_interactive(store, output_dir, args.force, args.note, full)
|
|
@@ -2,19 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
|
|
5
|
-
from ..db import
|
|
6
|
-
SessionError,
|
|
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
|
-
)
|
|
5
|
+
from ..db import SessionError
|
|
17
6
|
from ..i18n import _
|
|
7
|
+
from ..store import open_store
|
|
18
8
|
from ..utils import build_help_epilog, format_cost, format_tokens, format_ts
|
|
19
9
|
|
|
20
10
|
_INFO_EXAMPLES = [
|
|
@@ -35,21 +25,22 @@ def register(subparsers) -> None:
|
|
|
35
25
|
|
|
36
26
|
|
|
37
27
|
def run(args, db) -> int:
|
|
28
|
+
store = open_store(db)
|
|
38
29
|
try:
|
|
39
|
-
info = get_session_info(
|
|
30
|
+
info = store.get_session_info(args.session_id)
|
|
40
31
|
except SessionError as e:
|
|
41
32
|
print(e.message)
|
|
42
33
|
return 1
|
|
43
34
|
session_id = info["id"]
|
|
44
|
-
msg_count = get_message_count(
|
|
45
|
-
part_count = get_part_count(
|
|
46
|
-
children = get_children_sessions(
|
|
47
|
-
parent = get_parent_session(
|
|
48
|
-
todos = get_session_todos(
|
|
49
|
-
project_name = get_project_name(
|
|
50
|
-
|
|
51
|
-
model = parse_model(info)
|
|
52
|
-
title = get_session_title(info)
|
|
35
|
+
msg_count = store.get_message_count(session_id)
|
|
36
|
+
part_count = store.get_part_count(session_id)
|
|
37
|
+
children = store.get_children_sessions(session_id)
|
|
38
|
+
parent = store.get_parent_session(session_id)
|
|
39
|
+
todos = store.get_session_todos(session_id)
|
|
40
|
+
project_name = store.get_project_name(info["project_id"]) if info["project_id"] else "—"
|
|
41
|
+
|
|
42
|
+
model = store.parse_model(info)
|
|
43
|
+
title = store.get_session_title(info)
|
|
53
44
|
created = format_ts(info["time_created"])
|
|
54
45
|
updated = format_ts(info["time_updated"])
|
|
55
46
|
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
import argparse
|
|
4
4
|
from typing import Literal
|
|
5
5
|
|
|
6
|
-
from .. import db as db_module
|
|
7
6
|
from ..db import get_session_title, parse_model
|
|
8
7
|
from ..formatters import print_json, print_table_simple
|
|
9
8
|
from ..i18n import _
|
|
9
|
+
from ..store import open_store
|
|
10
10
|
from ..utils import build_help_epilog, format_ts
|
|
11
11
|
|
|
12
12
|
_LIST_EXAMPLES = [
|
|
@@ -41,48 +41,16 @@ def register(subparsers) -> None:
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def run(args, db) -> Literal[0]:
|
|
44
|
-
|
|
45
|
-
params = []
|
|
46
|
-
|
|
47
|
-
if args.project:
|
|
48
|
-
conditions.append("s.project_id = ?")
|
|
49
|
-
params.append(args.project)
|
|
50
|
-
|
|
51
|
-
if args.agent:
|
|
52
|
-
conditions.append("s.agent = ?")
|
|
53
|
-
params.append(args.agent)
|
|
54
|
-
|
|
55
|
-
if args.model:
|
|
56
|
-
conditions.append("s.model LIKE ?")
|
|
57
|
-
params.append(f"%{args.model}%")
|
|
58
|
-
|
|
59
|
-
where = " AND ".join(conditions) if conditions else "1=1"
|
|
60
|
-
|
|
61
|
-
sort_map = {
|
|
62
|
-
"date": "COALESCE(MAX(m.time_created), s.time_created) DESC",
|
|
63
|
-
"cost": "s.cost DESC",
|
|
64
|
-
"tokens": "(s.tokens_input + s.tokens_output) DESC",
|
|
65
|
-
"messages": "msg_count DESC",
|
|
66
|
-
}
|
|
67
|
-
order = sort_map.get(args.sort, "COALESCE(MAX(m.time_created), s.time_created) DESC")
|
|
68
|
-
|
|
44
|
+
store = open_store(db)
|
|
69
45
|
limit = 999999 if args.all else args.limit
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
LEFT JOIN message m ON m.session_id = s.id
|
|
79
|
-
WHERE {where}
|
|
80
|
-
GROUP BY s.id
|
|
81
|
-
ORDER BY {order}
|
|
82
|
-
LIMIT ? OFFSET ?
|
|
83
|
-
""",
|
|
84
|
-
(*params, limit, args.offset),
|
|
85
|
-
).fetchall()
|
|
46
|
+
rows = store.list_sessions(
|
|
47
|
+
project_id=args.project,
|
|
48
|
+
agent=args.agent,
|
|
49
|
+
model=args.model,
|
|
50
|
+
sort=args.sort,
|
|
51
|
+
limit=limit,
|
|
52
|
+
offset=args.offset,
|
|
53
|
+
)
|
|
86
54
|
|
|
87
55
|
if args.json:
|
|
88
56
|
data = []
|
|
@@ -106,7 +74,7 @@ def run(args, db) -> Literal[0]:
|
|
|
106
74
|
|
|
107
75
|
table_rows = []
|
|
108
76
|
for r in rows:
|
|
109
|
-
project_name =
|
|
77
|
+
project_name = store.get_project_name(r["project_id"]) if r["project_id"] else "—"
|
|
110
78
|
model_name = parse_model(r)
|
|
111
79
|
last_act = format_ts(r["last_time"], "%Y-%m-%d %H:%M") if r["last_time"] else "—"
|
|
112
80
|
|
|
@@ -4,6 +4,7 @@ import argparse
|
|
|
4
4
|
from typing import Literal
|
|
5
5
|
|
|
6
6
|
from ..i18n import _
|
|
7
|
+
from ..store import open_store
|
|
7
8
|
from ..utils import build_help_epilog, format_cost, format_tokens, format_ts
|
|
8
9
|
|
|
9
10
|
_PROJECTS_EXAMPLES = [
|
|
@@ -23,18 +24,7 @@ def register(subparsers) -> None:
|
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def run(args, db) -> Literal[0]:
|
|
26
|
-
rows = db.
|
|
27
|
-
SELECT p.id, p.name, p.worktree, p.time_created, p.time_updated,
|
|
28
|
-
COUNT(DISTINCT s.id) as session_count,
|
|
29
|
-
COALESCE(SUM(s.cost), 0) as total_cost,
|
|
30
|
-
COALESCE(SUM(s.tokens_input), 0) as total_ti,
|
|
31
|
-
COALESCE(SUM(s.tokens_output), 0) as total_to,
|
|
32
|
-
COALESCE(SUM(s.tokens_reasoning), 0) as total_tr
|
|
33
|
-
FROM project p
|
|
34
|
-
LEFT JOIN session s ON s.project_id = p.id
|
|
35
|
-
GROUP BY p.id
|
|
36
|
-
ORDER BY p.time_updated DESC
|
|
37
|
-
""").fetchall()
|
|
27
|
+
rows = open_store(db).get_projects()
|
|
38
28
|
|
|
39
29
|
if args.json:
|
|
40
30
|
from ..formatters import print_json
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Команда schema: семейство схемы, миграции и возможности."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from ..config import resolve_db_path
|
|
7
|
+
from ..formatters import print_json
|
|
8
|
+
from ..i18n import _
|
|
9
|
+
from ..schema import describe_schema, detect_schema
|
|
10
|
+
from ..utils import build_help_epilog
|
|
11
|
+
|
|
12
|
+
_SCHEMA_EXAMPLES = [
|
|
13
|
+
("", "help.schema.e0"),
|
|
14
|
+
("--json", "help.schema.e1"),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def register(subparsers) -> None:
|
|
19
|
+
p = subparsers.add_parser(
|
|
20
|
+
"schema",
|
|
21
|
+
help=_("help.cmd.schema"),
|
|
22
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
23
|
+
epilog=build_help_epilog("schema", _SCHEMA_EXAMPLES),
|
|
24
|
+
)
|
|
25
|
+
p.add_argument("--json", action="store_true", help="JSON output")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run(args, db) -> Literal[0]:
|
|
29
|
+
info = detect_schema(db)
|
|
30
|
+
data = describe_schema(info)
|
|
31
|
+
data["db_path"] = resolve_db_path()[0]
|
|
32
|
+
|
|
33
|
+
if args.json:
|
|
34
|
+
print_json(data)
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
print()
|
|
38
|
+
print(f" {_('schema.title')}")
|
|
39
|
+
print(f" {'─' * 50}")
|
|
40
|
+
print(f" {_('schema.db_path'):16s}: {data['db_path']}")
|
|
41
|
+
print(f" {_('schema.family'):16s}: {info.label}")
|
|
42
|
+
print(f" {_('schema.tables'):16s}: {data['table_count']}")
|
|
43
|
+
print(f" {_('schema.migrations'):16s}: {data['migration_count']}")
|
|
44
|
+
print(f" {_('schema.max_migration'):16s}: {info.max_migration or '—'}")
|
|
45
|
+
capabilities = ", ".join(data["capabilities"]) or "—"
|
|
46
|
+
print(f" {_('schema.capabilities'):16s}: {capabilities}")
|
|
47
|
+
|
|
48
|
+
manifest = data["manifest"]
|
|
49
|
+
if manifest.get("ref"):
|
|
50
|
+
commit = (manifest.get("commit") or "")[:10]
|
|
51
|
+
print(f" {_('schema.manifest'):16s}: {manifest['ref']} @ {commit}")
|
|
52
|
+
|
|
53
|
+
for warning in data["warnings"]:
|
|
54
|
+
print(_("schema.warning", message=warning))
|
|
55
|
+
print()
|
|
56
|
+
return 0
|