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.
Files changed (55) hide show
  1. {opencode_db-0.7.3/opencode_db.egg-info → opencode_db-0.8.0}/PKG-INFO +5 -1
  2. {opencode_db-0.7.3 → opencode_db-0.8.0}/README.md +45 -6
  3. {opencode_db-0.7.3 → opencode_db-0.8.0}/docs/README_PYPI.md +4 -0
  4. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/cli.py +1 -1
  5. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/__init__.py +2 -0
  6. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_costs.py +18 -49
  7. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_delete.py +5 -0
  8. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_export.py +24 -30
  9. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_help.py +8 -0
  10. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_info.py +13 -22
  11. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_list.py +11 -43
  12. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_projects.py +2 -12
  13. opencode_db-0.8.0/opencode_db/commands/cmd_schema.py +56 -0
  14. opencode_db-0.8.0/opencode_db/commands/cmd_search.py +71 -0
  15. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_stats.py +11 -38
  16. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_todos.py +11 -27
  17. opencode_db-0.8.0/opencode_db/commands/cmd_tree.py +101 -0
  18. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_vacuum.py +11 -4
  19. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_view.py +31 -34
  20. opencode_db-0.8.0/opencode_db/config.py +40 -0
  21. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/db.py +32 -45
  22. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/i18n.py +34 -0
  23. opencode_db-0.8.0/opencode_db/schema.py +243 -0
  24. opencode_db-0.8.0/opencode_db/schema_manifest.json +500 -0
  25. opencode_db-0.8.0/opencode_db/store/__init__.py +20 -0
  26. opencode_db-0.8.0/opencode_db/store/base.py +286 -0
  27. opencode_db-0.8.0/opencode_db/store/v1.py +253 -0
  28. opencode_db-0.8.0/opencode_db/store/v2.py +459 -0
  29. {opencode_db-0.7.3 → opencode_db-0.8.0/opencode_db.egg-info}/PKG-INFO +5 -1
  30. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db.egg-info/SOURCES.txt +9 -0
  31. {opencode_db-0.7.3 → opencode_db-0.8.0}/pyproject.toml +4 -1
  32. {opencode_db-0.7.3 → opencode_db-0.8.0}/tests/test_commands.py +1 -1
  33. opencode_db-0.8.0/tests/test_schema.py +133 -0
  34. opencode_db-0.8.0/tests/test_store_v2.py +282 -0
  35. opencode_db-0.7.3/opencode_db/commands/cmd_search.py +0 -124
  36. opencode_db-0.7.3/opencode_db/commands/cmd_tree.py +0 -110
  37. opencode_db-0.7.3/opencode_db/config.py +0 -16
  38. {opencode_db-0.7.3 → opencode_db-0.8.0}/LICENSE +0 -0
  39. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/__init__.py +0 -0
  40. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/__main__.py +0 -0
  41. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/commands/cmd_prune.py +0 -0
  42. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/formatters.py +0 -0
  43. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/markdown_to_ansi.py +0 -0
  44. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/runtime.py +0 -0
  45. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db/utils.py +0 -0
  46. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db.egg-info/dependency_links.txt +0 -0
  47. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db.egg-info/entry_points.txt +0 -0
  48. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db.egg-info/requires.txt +0 -0
  49. {opencode_db-0.7.3 → opencode_db-0.8.0}/opencode_db.egg-info/top_level.txt +0 -0
  50. {opencode_db-0.7.3 → opencode_db-0.8.0}/setup.cfg +0 -0
  51. {opencode_db-0.7.3 → opencode_db-0.8.0}/tests/test_db.py +0 -0
  52. {opencode_db-0.7.3 → opencode_db-0.8.0}/tests/test_formatters.py +0 -0
  53. {opencode_db-0.7.3 → opencode_db-0.8.0}/tests/test_i18n.py +0 -0
  54. {opencode_db-0.7.3 → opencode_db-0.8.0}/tests/test_runtime.py +0 -0
  55. {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.7.3
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
- [Zero external dependencies](https://treyhunner.com/2015/12/python-imports-and-path-manipulation/) — pure Python standard library only (`rich` is optional).
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, 280 keys)
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. Destructive operations (`delete`, `prune`) always require confirmation (or `--force`).
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 280 UI strings are translated. Data content (session titles, project names, etc.) remains in its original language.
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 + 191 tests
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, get_session_info, get_session_title, parse_model
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(db, args.session_id, args.json)
39
+ return _show_session(store, args.session_id, args.json)
38
40
 
39
41
  if args.total:
40
- return _show_total(db, args.project, args.json)
42
+ return _show_total(store, args.project, args.json)
41
43
 
42
- return _show_list(db, args.project, args.limit, args.json)
44
+ return _show_list(store, args.project, args.limit, args.json)
43
45
 
44
46
 
45
- def _show_session(db, session_id, as_json) -> int:
47
+ def _show_session(store, session_id, as_json) -> int:
46
48
  try:
47
- info = get_session_info(db, session_id)
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(db, project_id, as_json) -> int:
94
- where = "1=1"
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(db, project_id, limit, as_json) -> int:
147
- where = "1=1"
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(db, session_id, output_dir, force, note, full) -> int:
58
+ def _export_by_id(store, session_id, output_dir, force, note, full) -> int:
66
59
  try:
67
- session_info = get_session_info(db, session_id)
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(db, session_info, output_dir, force, note, full)
64
+ return _do_export(store, session_info, output_dir, force, note, full)
72
65
 
73
66
 
74
- def _export_latest(db, output_dir, force, note, full) -> int:
67
+ def _export_latest(store, output_dir, force, note, full) -> int:
75
68
  try:
76
- session_id = get_latest_session(db)
77
- session_info = get_session_info(db, session_id)
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(db, session_info, output_dir, force, note, full)
76
+ return _do_export(store, session_info, output_dir, force, note, full)
84
77
 
85
78
 
86
- def _export_interactive(db, output_dir, force, note, full) -> int:
87
- sessions = get_recent_sessions(db)
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(db, session_id)
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(db, session_info, output_dir, force, note, full)
126
+ return _do_export(store, session_info, output_dir, force, note, full)
134
127
 
135
128
 
136
- def _do_export(db, session_info, output_dir, force, note, full=False) -> int:
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(db, session_info["id"])
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(db, args.session_id, output_dir, args.force, args.note, full)
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(db, output_dir, args.force, args.note, full)
208
+ return _export_latest(store, output_dir, args.force, args.note, full)
215
209
 
216
- return _export_interactive(db, output_dir, args.force, args.note, full)
210
+ return _export_interactive(store, output_dir, args.force, args.note, full)
@@ -119,6 +119,14 @@ _COMMANDS = [
119
119
  ("--json", "help.projects.e1"),
120
120
  ],
121
121
  ),
122
+ (
123
+ "schema",
124
+ "help.cmd.schema",
125
+ [
126
+ ("", "help.schema.e0"),
127
+ ("--json", "help.schema.e1"),
128
+ ],
129
+ ),
122
130
  (
123
131
  "stats",
124
132
  "help.cmd.stats",
@@ -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(db, args.session_id)
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(db, session_id)
45
- part_count = get_part_count(db, session_id)
46
- children = get_children_sessions(db, session_id)
47
- parent = get_parent_session(db, session_id)
48
- todos = get_session_todos(db, session_id)
49
- project_name = get_project_name(db, info["project_id"]) if info["project_id"] else "—"
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
- conditions = []
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
- rows = db.execute(
72
- f"""
73
- SELECT s.id, s.title, s.model, s.project_id, s.agent,
74
- s.cost, s.tokens_input, s.tokens_output,
75
- MAX(m.time_created) as last_time,
76
- COUNT(DISTINCT m.id) as msg_count
77
- FROM session s
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 = db_module.get_project_name(db, r["project_id"]) if r["project_id"] else "—"
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.execute("""
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