opencode-db 0.7.3__tar.gz → 0.8.2__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 (57) hide show
  1. {opencode_db-0.7.3/opencode_db.egg-info → opencode_db-0.8.2}/PKG-INFO +9 -1
  2. {opencode_db-0.7.3 → opencode_db-0.8.2}/README.md +82 -7
  3. {opencode_db-0.7.3 → opencode_db-0.8.2}/docs/README_PYPI.md +8 -0
  4. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/cli.py +6 -2
  5. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/__init__.py +5 -0
  6. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_costs.py +18 -49
  7. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_delete.py +5 -0
  8. opencode_db-0.8.2/opencode_db/commands/cmd_directories.py +135 -0
  9. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_export.py +24 -30
  10. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_help.py +19 -0
  11. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_info.py +13 -22
  12. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_list.py +35 -55
  13. opencode_db-0.8.2/opencode_db/commands/cmd_projects.py +130 -0
  14. opencode_db-0.8.2/opencode_db/commands/cmd_schema.py +56 -0
  15. opencode_db-0.8.2/opencode_db/commands/cmd_search.py +71 -0
  16. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_stats.py +11 -38
  17. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_todos.py +11 -27
  18. opencode_db-0.8.2/opencode_db/commands/cmd_tree.py +101 -0
  19. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_vacuum.py +11 -4
  20. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_view.py +31 -34
  21. opencode_db-0.8.2/opencode_db/config.py +40 -0
  22. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/db.py +32 -45
  23. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/i18n.py +63 -0
  24. opencode_db-0.8.2/opencode_db/schema.py +243 -0
  25. opencode_db-0.8.2/opencode_db/schema_manifest.json +500 -0
  26. opencode_db-0.8.2/opencode_db/store/__init__.py +20 -0
  27. opencode_db-0.8.2/opencode_db/store/base.py +410 -0
  28. opencode_db-0.8.2/opencode_db/store/v1.py +253 -0
  29. opencode_db-0.8.2/opencode_db/store/v2.py +459 -0
  30. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/utils.py +8 -0
  31. {opencode_db-0.7.3 → opencode_db-0.8.2/opencode_db.egg-info}/PKG-INFO +9 -1
  32. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db.egg-info/SOURCES.txt +10 -0
  33. {opencode_db-0.7.3 → opencode_db-0.8.2}/pyproject.toml +4 -1
  34. {opencode_db-0.7.3 → opencode_db-0.8.2}/tests/test_commands.py +86 -1
  35. opencode_db-0.8.2/tests/test_schema.py +133 -0
  36. opencode_db-0.8.2/tests/test_store_v2.py +381 -0
  37. opencode_db-0.7.3/opencode_db/commands/cmd_projects.py +0 -93
  38. opencode_db-0.7.3/opencode_db/commands/cmd_search.py +0 -124
  39. opencode_db-0.7.3/opencode_db/commands/cmd_tree.py +0 -110
  40. opencode_db-0.7.3/opencode_db/config.py +0 -16
  41. {opencode_db-0.7.3 → opencode_db-0.8.2}/LICENSE +0 -0
  42. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/__init__.py +0 -0
  43. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/__main__.py +0 -0
  44. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_prune.py +0 -0
  45. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/formatters.py +0 -0
  46. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/markdown_to_ansi.py +0 -0
  47. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/runtime.py +0 -0
  48. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db.egg-info/dependency_links.txt +0 -0
  49. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db.egg-info/entry_points.txt +0 -0
  50. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db.egg-info/requires.txt +0 -0
  51. {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db.egg-info/top_level.txt +0 -0
  52. {opencode_db-0.7.3 → opencode_db-0.8.2}/setup.cfg +0 -0
  53. {opencode_db-0.7.3 → opencode_db-0.8.2}/tests/test_db.py +0 -0
  54. {opencode_db-0.7.3 → opencode_db-0.8.2}/tests/test_formatters.py +0 -0
  55. {opencode_db-0.7.3 → opencode_db-0.8.2}/tests/test_i18n.py +0 -0
  56. {opencode_db-0.7.3 → opencode_db-0.8.2}/tests/test_runtime.py +0 -0
  57. {opencode_db-0.7.3 → opencode_db-0.8.2}/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.2
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
@@ -32,8 +32,10 @@ uv tool install opencode-db
32
32
 
33
33
  ```bash
34
34
  opencode-db list # recent sessions
35
+ opencode-db directories # launch directories, including missing ones
35
36
  opencode-db view # interactive session viewer (colored, scrollable)
36
37
  opencode-db stats # database summary
38
+ opencode-db schema # detect v1/v2 schema
37
39
  opencode-db costs --total # total token costs
38
40
  opencode-db export # export dialog to .md (interactive)
39
41
  opencode-db help # full reference
@@ -43,9 +45,11 @@ opencode-db help # full reference
43
45
 
44
46
  - **`view`** — просмотр сессии в терминале с ANSI-цветами, Markdown-рендерингом и прокруткой через `less`
45
47
  - **`list`** / **`info`** / **`search`** / **`tree`** — навигация и поиск по сессиям
48
+ - **`directories`** (`dirs`) — все директории запусков, включая отсутствующие; `list --directory` добавляет путь к сессии
46
49
  - **`export`** — экспорт диалога в Markdown (с поддержкой Obsidian)
47
50
  - **`delete`** / **`prune`** — удаление и массовая очистка с фильтрами
48
51
  - **`costs`** / **`stats`** — аналитика токенов и расходов
52
+ - **`schema`** — определяет v1/v2-схему OpenCode, миграции и доступные операции
49
53
  - **`--db-path`** / **`OPENCODE_DB`** — кастомный путь к БД
50
54
 
51
55
  ## Optional extras
@@ -60,3 +64,7 @@ pip install opencode-db # zero external dependencies
60
64
  - Python 3.12+ — the CLI checks the interpreter at startup and exits with a clear message instead of an `ImportError` on older versions
61
65
  - [OpenCode](https://opencode.ai) (must have been run at least once)
62
66
  - macOS / Linux
67
+
68
+ **Directory support:** `global` is a synthetic project for non-Git directories; `directories` shows the real `session.directory` and marks it as `Global (no project root)`. Missing paths are detected with `os.path.isdir()` at command runtime and can be filtered with `--missing`.
69
+
70
+ **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
 
@@ -169,6 +172,7 @@ opencode-db list --project proj_xxx # filter by project
169
172
  opencode-db list --agent build # filter by agent type
170
173
  opencode-db list --model deepseek # filter by model name
171
174
  opencode-db list --json # JSON output
175
+ opencode-db list --all --directory # include directory paths
172
176
  ```
173
177
 
174
178
  | Flag | Description | Default |
@@ -181,6 +185,37 @@ opencode-db list --json # JSON output
181
185
  | `--sort {date,cost,tokens,messages}` | Sort order | date |
182
186
  | `--json` | Output as JSON | — |
183
187
  | `--all` | Show all (overrides --limit) | — |
188
+ | `--directory` | Add a session directory column | — |
189
+
190
+ ---
191
+
192
+ ### `directories` — launch directories
193
+
194
+ Lists every directory where OpenCode has run, including paths that no longer exist. Combines session directories with project roots and project-directory mappings.
195
+
196
+ ```bash
197
+ opencode-db directories # grouped by path
198
+ opencode-db directories --missing # only missing/inaccessible paths
199
+ opencode-db directories --existing # only existing paths
200
+ opencode-db directories --project global # Global sessions by real directory
201
+ opencode-db directories --json # full paths, counts, tokens, cost
202
+ ```
203
+
204
+ `global` is a synthetic project for non-Git directories. The command never shows
205
+ `/` as a real directory; it shows the actual `session.directory` and marks the
206
+ project as `Global (no project root)`.
207
+
208
+ | Flag | Description | Default |
209
+ |---|---|---|
210
+ | `--missing` | Only paths not found on disk | — |
211
+ | `--existing` | Only paths that exist | — |
212
+ | `--project ID` | Filter by project | — |
213
+ | `--sort {date,sessions,cost,path}` | Sort order | date |
214
+ | `--limit N` | Max entries | 50 |
215
+ | `--no-check` | Skip filesystem check (`exists: null`) | — |
216
+ | `--json` | Output as JSON | — |
217
+
218
+ > `os.path.isdir()` is a point-in-time check. An offline volume or permission error can look like a missing directory.
184
219
 
185
220
  ---
186
221
 
@@ -387,13 +422,17 @@ Example output:
387
422
  Shows all projects in the database with session counts, tokens, and costs.
388
423
 
389
424
  ```bash
390
- opencode-db projects # project table
425
+ opencode-db projects # project table with paths
391
426
  opencode-db projects --json # JSON output
427
+ opencode-db projects --missing # projects whose path is gone
428
+ opencode-db projects --no-global # hide the synthetic Global project
392
429
  ```
393
430
 
394
431
  | Flag | Description |
395
432
  |---|---|
396
433
  | `--json` | JSON output |
434
+ | `--missing` | Only projects with a missing path |
435
+ | `--no-global` | Hide the synthetic Global project |
397
436
 
398
437
  ---
399
438
 
@@ -412,6 +451,26 @@ opencode-db stats --json # JSON output
412
451
 
413
452
  ---
414
453
 
454
+ ### `schema` — detect database schema
455
+
456
+ Shows which OpenCode schema family the database uses, the applied migration, and which operations are supported.
457
+
458
+ ```bash
459
+ opencode-db schema # human-readable summary
460
+ opencode-db schema --json # JSON output
461
+ ```
462
+
463
+ Supported families:
464
+
465
+ | OpenCode | Schema | Support |
466
+ |---|---|---|
467
+ | 1.19.x | v1 | read + write |
468
+ | 2.x preview | v2 | read-only |
469
+
470
+ 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.
471
+
472
+ ---
473
+
415
474
  ### `todos` — view tasks from sessions
416
475
 
417
476
  Displays todo items created during OpenCode sessions.
@@ -517,7 +576,7 @@ opencode-db/
517
576
  │ ├── config.py ← constants and paths
518
577
  │ ├── db.py ← SQLite queries
519
578
  │ ├── formatters.py ← tables, JSON, Markdown, snapshots
520
- │ ├── i18n.py ← localization (EN/RU, 280 keys)
579
+ │ ├── i18n.py ← localization (EN/RU, 324 keys)
521
580
  │ ├── markdown_to_ansi.py ← Markdown → ANSI renderer for `view`
522
581
  │ ├── runtime.py ← Python 3.12+ version guard
523
582
  │ ├── utils.py ← date/token formatting, validation
@@ -557,11 +616,14 @@ opencode-db/
557
616
  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
617
  3. `cli.py` builds an `argparse` parser with subparsers from every command module and dispatches the call.
559
618
  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`).
619
+ 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.
620
+ 6. `store/` selects `V1Store` or `V2Store`; commands read through the same interface regardless of the underlying schema.
621
+ 7. Destructive operations (`delete`, `prune`) always require confirmation (or `--force`); on v2 they are refused until write support lands.
562
622
 
563
623
  ### Database schema (key tables)
564
624
 
625
+ Stable OpenCode 1.19.x (v1):
626
+
565
627
  ```
566
628
  session — sessions: id, title, model (JSON), agent, cost, tokens_*, parent_id, summary_*
567
629
  message — messages: id, session_id, time_created, data (JSON: role, agent)
@@ -570,6 +632,19 @@ todo — tasks: session_id, content, status, priority
570
632
  project — projects: id, worktree, name
571
633
  ```
572
634
 
635
+ Preview OpenCode 2.x (v2):
636
+
637
+ ```
638
+ session_v2 — renamed session table with the same usage fields
639
+ session_message — typed JSON messages: user, assistant, synthetic, system, shell, compaction, ...
640
+ project — projects: id, worktree, name, time_active, ...
641
+ ```
642
+
643
+ v2 has no `message`, `part`, or `todo` tables. Assistant content is nested in
644
+ `session_message.data.content`, and todos are derived from the latest completed
645
+ `todowrite` tool call. Use `opencode-db schema` to inspect the detected family
646
+ before running write commands.
647
+
573
648
  ---
574
649
 
575
650
  ## Using inside OpenCode TUI
@@ -650,7 +725,7 @@ opencode-db --lang en help # English help text
650
725
  opencode-db --lang en stats # English statistics
651
726
  ```
652
727
 
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.
728
+ The `--lang` flag applies to any command. All 324 UI strings are translated. Data content (session titles, project names, etc.) remains in its original language.
654
729
 
655
730
  The language can also be set via environment variable `OPENCODE_DB_LANG=en`.
656
731
 
@@ -735,7 +810,7 @@ source .venv/bin/activate
735
810
  uv pip install -e ".[rich]"
736
811
  uv pip install pytest ruff==0.17.1
737
812
 
738
- make check # check-python + ruff + 191 tests
813
+ make check # check-python + ruff + 214 tests
739
814
  ```
740
815
 
741
816
  `make check` fails fast with a clear message if the active interpreter is older than 3.12.
@@ -16,8 +16,10 @@ uv tool install opencode-db
16
16
 
17
17
  ```bash
18
18
  opencode-db list # recent sessions
19
+ opencode-db directories # launch directories, including missing ones
19
20
  opencode-db view # interactive session viewer (colored, scrollable)
20
21
  opencode-db stats # database summary
22
+ opencode-db schema # detect v1/v2 schema
21
23
  opencode-db costs --total # total token costs
22
24
  opencode-db export # export dialog to .md (interactive)
23
25
  opencode-db help # full reference
@@ -27,9 +29,11 @@ opencode-db help # full reference
27
29
 
28
30
  - **`view`** — просмотр сессии в терминале с ANSI-цветами, Markdown-рендерингом и прокруткой через `less`
29
31
  - **`list`** / **`info`** / **`search`** / **`tree`** — навигация и поиск по сессиям
32
+ - **`directories`** (`dirs`) — все директории запусков, включая отсутствующие; `list --directory` добавляет путь к сессии
30
33
  - **`export`** — экспорт диалога в Markdown (с поддержкой Obsidian)
31
34
  - **`delete`** / **`prune`** — удаление и массовая очистка с фильтрами
32
35
  - **`costs`** / **`stats`** — аналитика токенов и расходов
36
+ - **`schema`** — определяет v1/v2-схему OpenCode, миграции и доступные операции
33
37
  - **`--db-path`** / **`OPENCODE_DB`** — кастомный путь к БД
34
38
 
35
39
  ## Optional extras
@@ -44,3 +48,7 @@ pip install opencode-db # zero external dependencies
44
48
  - Python 3.12+ — the CLI checks the interpreter at startup and exits with a clear message instead of an `ImportError` on older versions
45
49
  - [OpenCode](https://opencode.ai) (must have been run at least once)
46
50
  - macOS / Linux
51
+
52
+ **Directory support:** `global` is a synthetic project for non-Git directories; `directories` shows the real `session.directory` and marks it as `Global (no project root)`. Missing paths are detected with `os.path.isdir()` at command runtime and can be filtered with `--missing`.
53
+
54
+ **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.
@@ -43,7 +43,11 @@ def build_parser() -> argparse.ArgumentParser:
43
43
  )
44
44
 
45
45
  subparsers = parser.add_subparsers(dest="command", required=True)
46
- for _name, mod in commands.get_all().items():
46
+ seen: set[str] = set()
47
+ for mod in commands.get_all().values():
48
+ if mod.__name__ in seen:
49
+ continue
50
+ seen.add(mod.__name__)
47
51
  mod.register(subparsers)
48
52
  return parser
49
53
 
@@ -74,7 +78,7 @@ def main(argv=None) -> Any | Literal[0] | Literal[1]:
74
78
  if args.db_path:
75
79
  os.environ["OPENCODE_DB"] = args.db_path
76
80
 
77
- db = get_db()
81
+ db = get_db(allow_unknown=args.command in {"schema", "help"})
78
82
 
79
83
  try:
80
84
  handler = commands.get_handler(args.command)
@@ -13,12 +13,14 @@
13
13
  from . import (
14
14
  cmd_costs,
15
15
  cmd_delete,
16
+ cmd_directories,
16
17
  cmd_export,
17
18
  cmd_help,
18
19
  cmd_info,
19
20
  cmd_list,
20
21
  cmd_projects,
21
22
  cmd_prune,
23
+ cmd_schema,
22
24
  cmd_search,
23
25
  cmd_stats,
24
26
  cmd_todos,
@@ -34,12 +36,15 @@ COMMANDS = {
34
36
  "info": cmd_info,
35
37
  "export": cmd_export,
36
38
  "delete": cmd_delete,
39
+ "directories": cmd_directories,
40
+ "dirs": cmd_directories,
37
41
  "costs": cmd_costs,
38
42
  "prune": cmd_prune,
39
43
  "search": cmd_search,
40
44
  "tree": cmd_tree,
41
45
  "projects": cmd_projects,
42
46
  "stats": cmd_stats,
47
+ "schema": cmd_schema,
43
48
  "todos": cmd_todos,
44
49
  "vacuum": cmd_vacuum,
45
50
  "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
 
@@ -0,0 +1,135 @@
1
+ """Команда directories: все директории запусков OpenCode."""
2
+
3
+ import argparse
4
+ from typing import Literal
5
+
6
+ from ..formatters import print_json, print_table_simple
7
+ from ..i18n import _
8
+ from ..store import open_store
9
+ from ..utils import build_help_epilog, format_cost, format_ts, shorten_path
10
+
11
+ _DIRECTORIES_EXAMPLES = [
12
+ ("", "help.directories.e0"),
13
+ ("--missing", "help.directories.e1"),
14
+ ("--json", "help.directories.e2"),
15
+ ("--no-check", "help.directories.e3"),
16
+ ]
17
+
18
+
19
+ def register(subparsers) -> None:
20
+ p = subparsers.add_parser(
21
+ "directories",
22
+ aliases=["dirs"],
23
+ help=_("help.cmd.directories"),
24
+ formatter_class=argparse.RawDescriptionHelpFormatter,
25
+ epilog=build_help_epilog("directories", _DIRECTORIES_EXAMPLES),
26
+ )
27
+ p.add_argument("--missing", action="store_true", help="Only missing directories")
28
+ p.add_argument("--existing", action="store_true", help="Only existing directories")
29
+ p.add_argument("--project", type=str, help="Filter by project ID")
30
+ p.add_argument(
31
+ "--sort",
32
+ choices=["date", "sessions", "cost", "path"],
33
+ default="date",
34
+ help="Sort order (default: date)",
35
+ )
36
+ p.add_argument("--limit", type=int, default=50, help="Max results (default 50)")
37
+ p.add_argument("--no-check", action="store_true", help="Skip filesystem check")
38
+ p.add_argument("--json", action="store_true", help="JSON output")
39
+
40
+
41
+ def run(args, db) -> Literal[1] | Literal[0]:
42
+ if getattr(args, "missing", False) and getattr(args, "existing", False):
43
+ print(_("directories.conflict"))
44
+ return 1
45
+
46
+ entries = open_store(db).get_directories(check_exists=not getattr(args, "no_check", False))
47
+
48
+ if getattr(args, "project", None):
49
+ entries = [
50
+ entry
51
+ for entry in entries
52
+ if args.project == entry["project_id"] or args.project in entry["project_ids"]
53
+ ]
54
+ if getattr(args, "missing", False):
55
+ entries = [entry for entry in entries if entry["exists"] is False]
56
+ elif getattr(args, "existing", False):
57
+ entries = [entry for entry in entries if entry["exists"] is True]
58
+
59
+ sorters = {
60
+ "date": lambda entry: entry["last_ts"] or 0,
61
+ "sessions": lambda entry: entry["sessions"],
62
+ "cost": lambda entry: entry["cost"],
63
+ "path": lambda entry: entry["path"],
64
+ }
65
+ reverse = getattr(args, "sort", "date") != "path"
66
+ entries.sort(key=sorters[getattr(args, "sort", "date")], reverse=reverse)
67
+ if getattr(args, "limit", 50):
68
+ entries = entries[: getattr(args, "limit", 50)]
69
+
70
+ if getattr(args, "json", False):
71
+ data = [
72
+ {
73
+ "path": entry["path"],
74
+ "exists": entry["exists"],
75
+ "project_id": entry["project_id"],
76
+ "project_ids": entry["project_ids"],
77
+ "project": entry["project_label"],
78
+ "is_global": entry["is_global"],
79
+ "sessions": entry["sessions"],
80
+ "first_activity": format_ts(entry["first_ts"]) if entry["first_ts"] else None,
81
+ "last_activity": format_ts(entry["last_ts"]) if entry["last_ts"] else None,
82
+ "tokens_input": entry["tokens_input"],
83
+ "tokens_output": entry["tokens_output"],
84
+ "tokens_reasoning": entry["tokens_reasoning"],
85
+ "tokens_cache_read": entry["tokens_cache_read"],
86
+ "tokens_cache_write": entry["tokens_cache_write"],
87
+ "cost": entry["cost"],
88
+ "sources": entry["sources"],
89
+ }
90
+ for entry in entries
91
+ ]
92
+ print_json(data)
93
+ return 0
94
+
95
+ if not entries:
96
+ print(_("directories.none"))
97
+ return 0
98
+
99
+ status_icon = {True: "✓", False: "✗", None: "?"}
100
+ table = []
101
+ for entry in entries:
102
+ status = {
103
+ True: _("directories.exists"),
104
+ False: _("directories.missing"),
105
+ None: _("directories.unknown"),
106
+ }[entry["exists"]]
107
+ last = format_ts(entry["last_ts"], "%Y-%m-%d") if entry["last_ts"] else "—"
108
+ table.append(
109
+ [
110
+ shorten_path(entry["path"], 52),
111
+ f"{status_icon[entry['exists']]} {status}",
112
+ entry["project_label"][:22],
113
+ str(entry["sessions"]),
114
+ last,
115
+ format_cost(entry["cost"]),
116
+ ]
117
+ )
118
+
119
+ print(f"\n {_('directories.title')}")
120
+ print_table_simple(
121
+ [
122
+ _("directories.header.path"),
123
+ _("directories.header.status"),
124
+ _("directories.header.project"),
125
+ _("directories.header.sessions"),
126
+ _("directories.header.last"),
127
+ _("directories.header.cost"),
128
+ ],
129
+ table,
130
+ )
131
+
132
+ missing = sum(1 for entry in entries if entry["exists"] is False)
133
+ sessions = sum(entry["sessions"] for entry in entries)
134
+ print(_("directories.summary", total=len(entries), sessions=sessions, missing=missing))
135
+ return 0