opencode-db 0.8.0__tar.gz → 0.8.3__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.8.0/opencode_db.egg-info → opencode_db-0.8.3}/PKG-INFO +6 -1
- {opencode_db-0.8.0 → opencode_db-0.8.3}/README.md +41 -4
- {opencode_db-0.8.0 → opencode_db-0.8.3}/docs/README_PYPI.md +5 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/cli.py +11 -1
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/__init__.py +3 -0
- opencode_db-0.8.3/opencode_db/commands/cmd_directories.py +135 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_help.py +11 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_list.py +23 -11
- opencode_db-0.8.3/opencode_db/commands/cmd_projects.py +130 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/i18n.py +29 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/store/base.py +124 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/store/v1.py +1 -1
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/store/v2.py +1 -1
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/utils.py +8 -0
- opencode_db-0.8.3/opencode_db/version.py +25 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3/opencode_db.egg-info}/PKG-INFO +6 -1
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db.egg-info/SOURCES.txt +4 -1
- {opencode_db-0.8.0 → opencode_db-0.8.3}/pyproject.toml +1 -1
- {opencode_db-0.8.0 → opencode_db-0.8.3}/tests/test_commands.py +85 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/tests/test_store_v2.py +99 -0
- opencode_db-0.8.3/tests/test_version.py +22 -0
- opencode_db-0.8.0/opencode_db/commands/cmd_projects.py +0 -83
- {opencode_db-0.8.0 → opencode_db-0.8.3}/LICENSE +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/__init__.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/__main__.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_costs.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_delete.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_export.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_info.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_prune.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_schema.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_search.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_stats.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_todos.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_tree.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_vacuum.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_view.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/config.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/db.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/formatters.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/markdown_to_ansi.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/runtime.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/schema.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/schema_manifest.json +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/store/__init__.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db.egg-info/dependency_links.txt +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db.egg-info/entry_points.txt +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db.egg-info/requires.txt +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db.egg-info/top_level.txt +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/setup.cfg +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/tests/test_db.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/tests/test_formatters.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/tests/test_i18n.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/tests/test_runtime.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/tests/test_schema.py +0 -0
- {opencode_db-0.8.0 → opencode_db-0.8.3}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opencode-db
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.3
|
|
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
|
|
@@ -31,7 +31,9 @@ uv tool install opencode-db
|
|
|
31
31
|
## Quick start
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
+
opencode-db --version # installed version
|
|
34
35
|
opencode-db list # recent sessions
|
|
36
|
+
opencode-db directories # launch directories, including missing ones
|
|
35
37
|
opencode-db view # interactive session viewer (colored, scrollable)
|
|
36
38
|
opencode-db stats # database summary
|
|
37
39
|
opencode-db schema # detect v1/v2 schema
|
|
@@ -44,6 +46,7 @@ opencode-db help # full reference
|
|
|
44
46
|
|
|
45
47
|
- **`view`** — просмотр сессии в терминале с ANSI-цветами, Markdown-рендерингом и прокруткой через `less`
|
|
46
48
|
- **`list`** / **`info`** / **`search`** / **`tree`** — навигация и поиск по сессиям
|
|
49
|
+
- **`directories`** (`dirs`) — все директории запусков, включая отсутствующие; `list --directory` добавляет путь к сессии
|
|
47
50
|
- **`export`** — экспорт диалога в Markdown (с поддержкой Obsidian)
|
|
48
51
|
- **`delete`** / **`prune`** — удаление и массовая очистка с фильтрами
|
|
49
52
|
- **`costs`** / **`stats`** — аналитика токенов и расходов
|
|
@@ -63,4 +66,6 @@ pip install opencode-db # zero external dependencies
|
|
|
63
66
|
- [OpenCode](https://opencode.ai) (must have been run at least once)
|
|
64
67
|
- macOS / Linux
|
|
65
68
|
|
|
69
|
+
**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`.
|
|
70
|
+
|
|
66
71
|
**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.
|
|
@@ -13,6 +13,7 @@
|
|
|
13
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
14
|
|
|
15
15
|
```
|
|
16
|
+
opencode-db --version # installed version
|
|
16
17
|
opencode-db help # full reference with examples
|
|
17
18
|
opencode-db list # list recent sessions
|
|
18
19
|
opencode-db stats # database summary
|
|
@@ -172,6 +173,7 @@ opencode-db list --project proj_xxx # filter by project
|
|
|
172
173
|
opencode-db list --agent build # filter by agent type
|
|
173
174
|
opencode-db list --model deepseek # filter by model name
|
|
174
175
|
opencode-db list --json # JSON output
|
|
176
|
+
opencode-db list --all --directory # include directory paths
|
|
175
177
|
```
|
|
176
178
|
|
|
177
179
|
| Flag | Description | Default |
|
|
@@ -184,6 +186,37 @@ opencode-db list --json # JSON output
|
|
|
184
186
|
| `--sort {date,cost,tokens,messages}` | Sort order | date |
|
|
185
187
|
| `--json` | Output as JSON | — |
|
|
186
188
|
| `--all` | Show all (overrides --limit) | — |
|
|
189
|
+
| `--directory` | Add a session directory column | — |
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
### `directories` — launch directories
|
|
194
|
+
|
|
195
|
+
Lists every directory where OpenCode has run, including paths that no longer exist. Combines session directories with project roots and project-directory mappings.
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
opencode-db directories # grouped by path
|
|
199
|
+
opencode-db directories --missing # only missing/inaccessible paths
|
|
200
|
+
opencode-db directories --existing # only existing paths
|
|
201
|
+
opencode-db directories --project global # Global sessions by real directory
|
|
202
|
+
opencode-db directories --json # full paths, counts, tokens, cost
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`global` is a synthetic project for non-Git directories. The command never shows
|
|
206
|
+
`/` as a real directory; it shows the actual `session.directory` and marks the
|
|
207
|
+
project as `Global (no project root)`.
|
|
208
|
+
|
|
209
|
+
| Flag | Description | Default |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `--missing` | Only paths not found on disk | — |
|
|
212
|
+
| `--existing` | Only paths that exist | — |
|
|
213
|
+
| `--project ID` | Filter by project | — |
|
|
214
|
+
| `--sort {date,sessions,cost,path}` | Sort order | date |
|
|
215
|
+
| `--limit N` | Max entries | 50 |
|
|
216
|
+
| `--no-check` | Skip filesystem check (`exists: null`) | — |
|
|
217
|
+
| `--json` | Output as JSON | — |
|
|
218
|
+
|
|
219
|
+
> `os.path.isdir()` is a point-in-time check. An offline volume or permission error can look like a missing directory.
|
|
187
220
|
|
|
188
221
|
---
|
|
189
222
|
|
|
@@ -390,13 +423,17 @@ Example output:
|
|
|
390
423
|
Shows all projects in the database with session counts, tokens, and costs.
|
|
391
424
|
|
|
392
425
|
```bash
|
|
393
|
-
opencode-db projects # project table
|
|
426
|
+
opencode-db projects # project table with paths
|
|
394
427
|
opencode-db projects --json # JSON output
|
|
428
|
+
opencode-db projects --missing # projects whose path is gone
|
|
429
|
+
opencode-db projects --no-global # hide the synthetic Global project
|
|
395
430
|
```
|
|
396
431
|
|
|
397
432
|
| Flag | Description |
|
|
398
433
|
|---|---|
|
|
399
434
|
| `--json` | JSON output |
|
|
435
|
+
| `--missing` | Only projects with a missing path |
|
|
436
|
+
| `--no-global` | Hide the synthetic Global project |
|
|
400
437
|
|
|
401
438
|
---
|
|
402
439
|
|
|
@@ -540,7 +577,7 @@ opencode-db/
|
|
|
540
577
|
│ ├── config.py ← constants and paths
|
|
541
578
|
│ ├── db.py ← SQLite queries
|
|
542
579
|
│ ├── formatters.py ← tables, JSON, Markdown, snapshots
|
|
543
|
-
│ ├── i18n.py ← localization (EN/RU,
|
|
580
|
+
│ ├── i18n.py ← localization (EN/RU, 324 keys)
|
|
544
581
|
│ ├── markdown_to_ansi.py ← Markdown → ANSI renderer for `view`
|
|
545
582
|
│ ├── runtime.py ← Python 3.12+ version guard
|
|
546
583
|
│ ├── utils.py ← date/token formatting, validation
|
|
@@ -689,7 +726,7 @@ opencode-db --lang en help # English help text
|
|
|
689
726
|
opencode-db --lang en stats # English statistics
|
|
690
727
|
```
|
|
691
728
|
|
|
692
|
-
The `--lang` flag applies to any command. All
|
|
729
|
+
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.
|
|
693
730
|
|
|
694
731
|
The language can also be set via environment variable `OPENCODE_DB_LANG=en`.
|
|
695
732
|
|
|
@@ -774,7 +811,7 @@ source .venv/bin/activate
|
|
|
774
811
|
uv pip install -e ".[rich]"
|
|
775
812
|
uv pip install pytest ruff==0.17.1
|
|
776
813
|
|
|
777
|
-
make check # check-python + ruff +
|
|
814
|
+
make check # check-python + ruff + 216 tests
|
|
778
815
|
```
|
|
779
816
|
|
|
780
817
|
`make check` fails fast with a clear message if the active interpreter is older than 3.12.
|
|
@@ -15,7 +15,9 @@ uv tool install opencode-db
|
|
|
15
15
|
## Quick start
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
+
opencode-db --version # installed version
|
|
18
19
|
opencode-db list # recent sessions
|
|
20
|
+
opencode-db directories # launch directories, including missing ones
|
|
19
21
|
opencode-db view # interactive session viewer (colored, scrollable)
|
|
20
22
|
opencode-db stats # database summary
|
|
21
23
|
opencode-db schema # detect v1/v2 schema
|
|
@@ -28,6 +30,7 @@ opencode-db help # full reference
|
|
|
28
30
|
|
|
29
31
|
- **`view`** — просмотр сессии в терминале с ANSI-цветами, Markdown-рендерингом и прокруткой через `less`
|
|
30
32
|
- **`list`** / **`info`** / **`search`** / **`tree`** — навигация и поиск по сессиям
|
|
33
|
+
- **`directories`** (`dirs`) — все директории запусков, включая отсутствующие; `list --directory` добавляет путь к сессии
|
|
31
34
|
- **`export`** — экспорт диалога в Markdown (с поддержкой Obsidian)
|
|
32
35
|
- **`delete`** / **`prune`** — удаление и массовая очистка с фильтрами
|
|
33
36
|
- **`costs`** / **`stats`** — аналитика токенов и расходов
|
|
@@ -47,4 +50,6 @@ pip install opencode-db # zero external dependencies
|
|
|
47
50
|
- [OpenCode](https://opencode.ai) (must have been run at least once)
|
|
48
51
|
- macOS / Linux
|
|
49
52
|
|
|
53
|
+
**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`.
|
|
54
|
+
|
|
50
55
|
**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.
|
|
@@ -21,6 +21,7 @@ require_python()
|
|
|
21
21
|
from . import commands # noqa: E402
|
|
22
22
|
from .db import get_db # noqa: E402
|
|
23
23
|
from .i18n import _, set_lang # noqa: E402
|
|
24
|
+
from .version import get_version # noqa: E402
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -41,9 +42,18 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
41
42
|
default=None,
|
|
42
43
|
help="Path to opencode.db (default: ~/.local/share/opencode/opencode.db)",
|
|
43
44
|
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--version",
|
|
47
|
+
action="version",
|
|
48
|
+
version=f"%(prog)s {get_version()}",
|
|
49
|
+
)
|
|
44
50
|
|
|
45
51
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
46
|
-
|
|
52
|
+
seen: set[str] = set()
|
|
53
|
+
for mod in commands.get_all().values():
|
|
54
|
+
if mod.__name__ in seen:
|
|
55
|
+
continue
|
|
56
|
+
seen.add(mod.__name__)
|
|
47
57
|
mod.register(subparsers)
|
|
48
58
|
return parser
|
|
49
59
|
|
|
@@ -13,6 +13,7 @@
|
|
|
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,
|
|
@@ -35,6 +36,8 @@ COMMANDS = {
|
|
|
35
36
|
"info": cmd_info,
|
|
36
37
|
"export": cmd_export,
|
|
37
38
|
"delete": cmd_delete,
|
|
39
|
+
"directories": cmd_directories,
|
|
40
|
+
"dirs": cmd_directories,
|
|
38
41
|
"costs": cmd_costs,
|
|
39
42
|
"prune": cmd_prune,
|
|
40
43
|
"search": cmd_search,
|
|
@@ -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
|
|
@@ -41,6 +41,7 @@ _COMMANDS = [
|
|
|
41
41
|
("--sort cost", "help.list.e2"),
|
|
42
42
|
("--project proj_xxx", "help.list.e3"),
|
|
43
43
|
("--json", "help.list.e4"),
|
|
44
|
+
("--directory", "help.list.e5"),
|
|
44
45
|
],
|
|
45
46
|
),
|
|
46
47
|
(
|
|
@@ -119,6 +120,16 @@ _COMMANDS = [
|
|
|
119
120
|
("--json", "help.projects.e1"),
|
|
120
121
|
],
|
|
121
122
|
),
|
|
123
|
+
(
|
|
124
|
+
"directories",
|
|
125
|
+
"help.cmd.directories",
|
|
126
|
+
[
|
|
127
|
+
("", "help.directories.e0"),
|
|
128
|
+
("--missing", "help.directories.e1"),
|
|
129
|
+
("--json", "help.directories.e2"),
|
|
130
|
+
("--no-check", "help.directories.e3"),
|
|
131
|
+
],
|
|
132
|
+
),
|
|
122
133
|
(
|
|
123
134
|
"schema",
|
|
124
135
|
"help.cmd.schema",
|
|
@@ -7,7 +7,7 @@ from ..db import get_session_title, parse_model
|
|
|
7
7
|
from ..formatters import print_json, print_table_simple
|
|
8
8
|
from ..i18n import _
|
|
9
9
|
from ..store import open_store
|
|
10
|
-
from ..utils import build_help_epilog, format_ts
|
|
10
|
+
from ..utils import build_help_epilog, format_ts, shorten_path
|
|
11
11
|
|
|
12
12
|
_LIST_EXAMPLES = [
|
|
13
13
|
("", "help.list.e0"),
|
|
@@ -15,6 +15,7 @@ _LIST_EXAMPLES = [
|
|
|
15
15
|
("--sort cost", "help.list.e2"),
|
|
16
16
|
("--project proj_xxx", "help.list.e3"),
|
|
17
17
|
("--json", "help.list.e4"),
|
|
18
|
+
("--directory", "help.list.e5"),
|
|
18
19
|
]
|
|
19
20
|
|
|
20
21
|
|
|
@@ -38,6 +39,7 @@ def register(subparsers) -> None:
|
|
|
38
39
|
)
|
|
39
40
|
p.add_argument("--json", action="store_true", help="JSON output")
|
|
40
41
|
p.add_argument("--all", action="store_true", help="Show all (no limit)")
|
|
42
|
+
p.add_argument("--directory", action="store_true", help="Show session directory")
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
def run(args, db) -> Literal[0]:
|
|
@@ -60,6 +62,7 @@ def run(args, db) -> Literal[0]:
|
|
|
60
62
|
"id": r["id"],
|
|
61
63
|
"title": get_session_title(r),
|
|
62
64
|
"project_id": r["project_id"],
|
|
65
|
+
"directory": r["directory"],
|
|
63
66
|
"model": parse_model(r),
|
|
64
67
|
"agent": r["agent"],
|
|
65
68
|
"cost": r["cost"],
|
|
@@ -78,29 +81,38 @@ def run(args, db) -> Literal[0]:
|
|
|
78
81
|
model_name = parse_model(r)
|
|
79
82
|
last_act = format_ts(r["last_time"], "%Y-%m-%d %H:%M") if r["last_time"] else "—"
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
row = [
|
|
85
|
+
r["id"][:24],
|
|
86
|
+
get_session_title(r)[:32],
|
|
87
|
+
project_name[:16],
|
|
88
|
+
]
|
|
89
|
+
if getattr(args, "directory", False):
|
|
90
|
+
row.append(shorten_path(r["directory"] or "—", 44))
|
|
91
|
+
row.extend(
|
|
82
92
|
[
|
|
83
|
-
r["id"][:24],
|
|
84
|
-
get_session_title(r)[:32],
|
|
85
|
-
project_name[:16],
|
|
86
93
|
model_name[:20],
|
|
87
94
|
last_act,
|
|
88
95
|
str(r["msg_count"]),
|
|
89
96
|
f"${r['cost']:.4f}" if r["cost"] else "—",
|
|
90
97
|
]
|
|
91
98
|
)
|
|
99
|
+
table_rows.append(row)
|
|
92
100
|
|
|
93
|
-
|
|
101
|
+
headers = [
|
|
102
|
+
_("list.header.id"),
|
|
103
|
+
_("list.header.title"),
|
|
104
|
+
_("list.header.project"),
|
|
105
|
+
]
|
|
106
|
+
if getattr(args, "directory", False):
|
|
107
|
+
headers.append(_("list.header.directory"))
|
|
108
|
+
headers.extend(
|
|
94
109
|
[
|
|
95
|
-
_("list.header.id"),
|
|
96
|
-
_("list.header.title"),
|
|
97
|
-
_("list.header.project"),
|
|
98
110
|
_("list.header.model"),
|
|
99
111
|
_("list.header.activity"),
|
|
100
112
|
_("list.header.messages"),
|
|
101
113
|
_("list.header.cost"),
|
|
102
|
-
]
|
|
103
|
-
table_rows,
|
|
114
|
+
]
|
|
104
115
|
)
|
|
116
|
+
print_table_simple(headers, table_rows)
|
|
105
117
|
|
|
106
118
|
return 0
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Команда projects: список проектов со статистикой."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from ..i18n import _
|
|
8
|
+
from ..store import open_store
|
|
9
|
+
from ..utils import build_help_epilog, format_cost, format_tokens, format_ts, shorten_path
|
|
10
|
+
|
|
11
|
+
_PROJECTS_EXAMPLES = [
|
|
12
|
+
("", "help.projects.e0"),
|
|
13
|
+
("--json", "help.projects.e1"),
|
|
14
|
+
("--no-global", "help.projects.e2"),
|
|
15
|
+
("--missing", "help.projects.e3"),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def register(subparsers) -> None:
|
|
20
|
+
p = subparsers.add_parser(
|
|
21
|
+
"projects",
|
|
22
|
+
help=_("help.cmd.projects"),
|
|
23
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
24
|
+
epilog=build_help_epilog("projects", _PROJECTS_EXAMPLES),
|
|
25
|
+
)
|
|
26
|
+
p.add_argument("--json", action="store_true", help="JSON output")
|
|
27
|
+
p.add_argument("--no-global", action="store_true", help="Hide the Global project")
|
|
28
|
+
p.add_argument("--missing", action="store_true", help="Only projects with a missing path")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _project_view(r) -> dict:
|
|
32
|
+
worktree = r["worktree"]
|
|
33
|
+
is_global = r["id"] == "global"
|
|
34
|
+
path = None if is_global else worktree
|
|
35
|
+
exists = None if is_global else (os.path.isdir(path) if path else False)
|
|
36
|
+
name = r["name"]
|
|
37
|
+
if not name and path and not is_global:
|
|
38
|
+
name = os.path.basename(str(path).rstrip("/")) or None
|
|
39
|
+
return {
|
|
40
|
+
"id": r["id"],
|
|
41
|
+
"name": name,
|
|
42
|
+
"worktree": worktree,
|
|
43
|
+
"path": path,
|
|
44
|
+
"exists": exists,
|
|
45
|
+
"is_global": is_global,
|
|
46
|
+
"time_created": format_ts(r["time_created"]) if r["time_created"] else None,
|
|
47
|
+
"time_updated": format_ts(r["time_updated"]) if r["time_updated"] else None,
|
|
48
|
+
"sessions": r["session_count"],
|
|
49
|
+
"cost": r["total_cost"],
|
|
50
|
+
"tokens_input": r["total_ti"],
|
|
51
|
+
"tokens_output": r["total_to"],
|
|
52
|
+
"tokens_reasoning": r["total_tr"],
|
|
53
|
+
"tokens_total": r["total_ti"] + r["total_to"] + r["total_tr"],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def run(args, db) -> Literal[0]:
|
|
58
|
+
rows = open_store(db).get_projects()
|
|
59
|
+
views = [_project_view(r) for r in rows]
|
|
60
|
+
if getattr(args, "no_global", False):
|
|
61
|
+
views = [v for v in views if not v["is_global"]]
|
|
62
|
+
if getattr(args, "missing", False):
|
|
63
|
+
views = [v for v in views if v["exists"] is False]
|
|
64
|
+
|
|
65
|
+
if args.json:
|
|
66
|
+
from ..formatters import print_json
|
|
67
|
+
|
|
68
|
+
data = []
|
|
69
|
+
for v in views:
|
|
70
|
+
data.append(
|
|
71
|
+
{
|
|
72
|
+
"id": v["id"],
|
|
73
|
+
"name": v["name"],
|
|
74
|
+
"worktree": v["worktree"],
|
|
75
|
+
"path": v["path"],
|
|
76
|
+
"exists": v["exists"],
|
|
77
|
+
"is_global": v["is_global"],
|
|
78
|
+
"time_created": v["time_created"],
|
|
79
|
+
"time_updated": v["time_updated"],
|
|
80
|
+
"sessions": v["sessions"],
|
|
81
|
+
"cost": v["cost"],
|
|
82
|
+
"tokens_input": v["tokens_input"],
|
|
83
|
+
"tokens_output": v["tokens_output"],
|
|
84
|
+
"tokens_reasoning": v["tokens_reasoning"],
|
|
85
|
+
"tokens_total": v["tokens_total"],
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
print_json(data)
|
|
89
|
+
return 0
|
|
90
|
+
|
|
91
|
+
from ..formatters import print_table_simple
|
|
92
|
+
|
|
93
|
+
if not views:
|
|
94
|
+
print(_("projects.none"))
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
table = []
|
|
98
|
+
for v in views:
|
|
99
|
+
name = _("projects.global") if v["is_global"] else (v["name"] or "—")
|
|
100
|
+
path = "—" if v["is_global"] else shorten_path(v["path"], 40)
|
|
101
|
+
if v["exists"] is False:
|
|
102
|
+
path = f"{path} ({_('projects.missing')})"
|
|
103
|
+
updated = v["time_updated"][:10] if v["time_updated"] else "—"
|
|
104
|
+
|
|
105
|
+
table.append(
|
|
106
|
+
[
|
|
107
|
+
v["id"][:12],
|
|
108
|
+
name[:20],
|
|
109
|
+
path,
|
|
110
|
+
updated,
|
|
111
|
+
str(v["sessions"]),
|
|
112
|
+
format_tokens(v["tokens_total"]),
|
|
113
|
+
format_cost(v["cost"]),
|
|
114
|
+
]
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
print_table_simple(
|
|
118
|
+
[
|
|
119
|
+
_("projects.header.id"),
|
|
120
|
+
_("projects.header.name"),
|
|
121
|
+
_("projects.header.path"),
|
|
122
|
+
_("projects.header.activity"),
|
|
123
|
+
_("projects.header.sessions"),
|
|
124
|
+
_("projects.header.tokens"),
|
|
125
|
+
_("projects.header.cost"),
|
|
126
|
+
],
|
|
127
|
+
table,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return 0
|
|
@@ -93,6 +93,7 @@ L: dict[str, dict[str, str]] = {
|
|
|
93
93
|
"list.header.id": {"ru": "ID", "en": "ID"},
|
|
94
94
|
"list.header.title": {"ru": "Название", "en": "Title"},
|
|
95
95
|
"list.header.project": {"ru": "Проект", "en": "Project"},
|
|
96
|
+
"list.header.directory": {"ru": "Директория", "en": "Directory"},
|
|
96
97
|
"list.header.model": {"ru": "Модель", "en": "Model"},
|
|
97
98
|
"list.header.activity": {"ru": "Активность", "en": "Activity"},
|
|
98
99
|
"list.header.messages": {"ru": "Сообщ.", "en": "Msgs"},
|
|
@@ -303,10 +304,14 @@ L: dict[str, dict[str, str]] = {
|
|
|
303
304
|
# ==================================================================
|
|
304
305
|
"projects.header.id": {"ru": "ID", "en": "ID"},
|
|
305
306
|
"projects.header.name": {"ru": "Название", "en": "Name"},
|
|
307
|
+
"projects.header.path": {"ru": "Путь", "en": "Path"},
|
|
306
308
|
"projects.header.activity": {"ru": "Активность", "en": "Activity"},
|
|
307
309
|
"projects.header.sessions": {"ru": "Сессии", "en": "Sessions"},
|
|
308
310
|
"projects.header.tokens": {"ru": "Токены", "en": "Tokens"},
|
|
309
311
|
"projects.header.cost": {"ru": "Стоимость", "en": "Cost"},
|
|
312
|
+
"projects.global": {"ru": "Global (без корня)", "en": "Global (no project root)"},
|
|
313
|
+
"projects.missing": {"ru": "не найдена", "en": "not found"},
|
|
314
|
+
"projects.none": {"ru": "Проекты не найдены.", "en": "No projects found."},
|
|
310
315
|
# ==================================================================
|
|
311
316
|
# STATS
|
|
312
317
|
# ==================================================================
|
|
@@ -468,6 +473,27 @@ L: dict[str, dict[str, str]] = {
|
|
|
468
473
|
"help.cmd.search": {"ru": "Поиск по сообщениям", "en": "Search messages"},
|
|
469
474
|
"help.cmd.tree": {"ru": "Дерево ветвления сессий", "en": "Session branching tree"},
|
|
470
475
|
"help.cmd.projects": {"ru": "Список проектов", "en": "Project list"},
|
|
476
|
+
"help.cmd.directories": {"ru": "Директории запусков", "en": "Launch directories"},
|
|
477
|
+
"help.directories.e0": {"ru": "Все директории с сессиями", "en": "All directories with sessions"},
|
|
478
|
+
"help.directories.e1": {"ru": "Только отсутствующие директории", "en": "Only missing directories"},
|
|
479
|
+
"help.directories.e2": {"ru": "Вывод в JSON", "en": "JSON output"},
|
|
480
|
+
"help.directories.e3": {"ru": "Не проверять файловую систему", "en": "Skip filesystem check"},
|
|
481
|
+
"directories.title": {"ru": "Директории запусков OpenCode", "en": "OpenCode launch directories"},
|
|
482
|
+
"directories.header.path": {"ru": "Путь", "en": "Path"},
|
|
483
|
+
"directories.header.status": {"ru": "Статус", "en": "Status"},
|
|
484
|
+
"directories.header.project": {"ru": "Проект", "en": "Project"},
|
|
485
|
+
"directories.header.sessions": {"ru": "Сессии", "en": "Sessions"},
|
|
486
|
+
"directories.header.last": {"ru": "Последняя", "en": "Last"},
|
|
487
|
+
"directories.header.cost": {"ru": "Стоимость", "en": "Cost"},
|
|
488
|
+
"directories.exists": {"ru": "да", "en": "yes"},
|
|
489
|
+
"directories.missing": {"ru": "не найдена", "en": "not found"},
|
|
490
|
+
"directories.unknown": {"ru": "?", "en": "?"},
|
|
491
|
+
"directories.global": {"ru": "Global (без корня)", "en": "Global (no project root)"},
|
|
492
|
+
"directories.none": {"ru": "Директории не найдены.", "en": "No directories found."},
|
|
493
|
+
"directories.total": {"ru": "Всего директорий: {n}", "en": "Directories: {n}"},
|
|
494
|
+
"directories.summary": {"ru": "Директорий: {total}, сессий: {sessions}, отсутствует: {missing}", "en": "Directories: {total}, sessions: {sessions}, missing: {missing}"},
|
|
495
|
+
"directories.conflict": {"ru": "Нельзя одновременно использовать --missing и --existing.", "en": "Cannot use --missing and --existing together."},
|
|
496
|
+
|
|
471
497
|
"help.cmd.stats": {"ru": "Статистика БД", "en": "Database statistics"},
|
|
472
498
|
"help.cmd.todos": {"ru": "Задачи из сессий", "en": "Tasks from sessions"},
|
|
473
499
|
"help.cmd.vacuum": {"ru": "Оптимизация БД", "en": "Database optimization"},
|
|
@@ -494,6 +520,7 @@ L: dict[str, dict[str, str]] = {
|
|
|
494
520
|
"help.list.e2": {"ru": "Сортировка по стоимости", "en": "Sort by cost"},
|
|
495
521
|
"help.list.e3": {"ru": "Фильтр по проекту", "en": "Filter by project"},
|
|
496
522
|
"help.list.e4": {"ru": "Вывод в JSON", "en": "JSON output"},
|
|
523
|
+
"help.list.e5": {"ru": "Показать директории сессий", "en": "Show session directories"},
|
|
497
524
|
"help.info.e0": {
|
|
498
525
|
"ru": "Все метаданные, токены, todo, ветвление",
|
|
499
526
|
"en": "Metadata, tokens, todos, branching",
|
|
@@ -544,6 +571,8 @@ L: dict[str, dict[str, str]] = {
|
|
|
544
571
|
"help.tree.e2": {"ru": "Глубина 3 уровня", "en": "Max depth of 3"},
|
|
545
572
|
"help.projects.e0": {"ru": "Проекты со статистикой", "en": "Projects with statistics"},
|
|
546
573
|
"help.projects.e1": {"ru": "Вывод в JSON", "en": "JSON output"},
|
|
574
|
+
"help.projects.e2": {"ru": "Скрыть Global-проект", "en": "Hide the Global project"},
|
|
575
|
+
"help.projects.e3": {"ru": "Только проекты с отсутствующим путём", "en": "Only projects with a missing path"},
|
|
547
576
|
"help.stats.e0": {
|
|
548
577
|
"ru": "Сессии, сообщения, токены, стоимость, топ моделей",
|
|
549
578
|
"en": "Sessions, messages, tokens, costs, top models",
|