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.
- {opencode_db-0.7.3/opencode_db.egg-info → opencode_db-0.8.2}/PKG-INFO +9 -1
- {opencode_db-0.7.3 → opencode_db-0.8.2}/README.md +82 -7
- {opencode_db-0.7.3 → opencode_db-0.8.2}/docs/README_PYPI.md +8 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/cli.py +6 -2
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/__init__.py +5 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_costs.py +18 -49
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_delete.py +5 -0
- opencode_db-0.8.2/opencode_db/commands/cmd_directories.py +135 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_export.py +24 -30
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_help.py +19 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_info.py +13 -22
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_list.py +35 -55
- opencode_db-0.8.2/opencode_db/commands/cmd_projects.py +130 -0
- opencode_db-0.8.2/opencode_db/commands/cmd_schema.py +56 -0
- opencode_db-0.8.2/opencode_db/commands/cmd_search.py +71 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_stats.py +11 -38
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_todos.py +11 -27
- opencode_db-0.8.2/opencode_db/commands/cmd_tree.py +101 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_vacuum.py +11 -4
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_view.py +31 -34
- opencode_db-0.8.2/opencode_db/config.py +40 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/db.py +32 -45
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/i18n.py +63 -0
- opencode_db-0.8.2/opencode_db/schema.py +243 -0
- opencode_db-0.8.2/opencode_db/schema_manifest.json +500 -0
- opencode_db-0.8.2/opencode_db/store/__init__.py +20 -0
- opencode_db-0.8.2/opencode_db/store/base.py +410 -0
- opencode_db-0.8.2/opencode_db/store/v1.py +253 -0
- opencode_db-0.8.2/opencode_db/store/v2.py +459 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/utils.py +8 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2/opencode_db.egg-info}/PKG-INFO +9 -1
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db.egg-info/SOURCES.txt +10 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/pyproject.toml +4 -1
- {opencode_db-0.7.3 → opencode_db-0.8.2}/tests/test_commands.py +86 -1
- opencode_db-0.8.2/tests/test_schema.py +133 -0
- opencode_db-0.8.2/tests/test_store_v2.py +381 -0
- opencode_db-0.7.3/opencode_db/commands/cmd_projects.py +0 -93
- 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.2}/LICENSE +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/__init__.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/__main__.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/commands/cmd_prune.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/formatters.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/markdown_to_ansi.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db/runtime.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db.egg-info/dependency_links.txt +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db.egg-info/entry_points.txt +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db.egg-info/requires.txt +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/opencode_db.egg-info/top_level.txt +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/setup.cfg +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/tests/test_db.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/tests/test_formatters.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/tests/test_i18n.py +0 -0
- {opencode_db-0.7.3 → opencode_db-0.8.2}/tests/test_runtime.py +0 -0
- {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.
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
|
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 +
|
|
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
|
-
|
|
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
|
|
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
|
|
|
@@ -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
|