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.
Files changed (56) hide show
  1. {opencode_db-0.8.0/opencode_db.egg-info → opencode_db-0.8.3}/PKG-INFO +6 -1
  2. {opencode_db-0.8.0 → opencode_db-0.8.3}/README.md +41 -4
  3. {opencode_db-0.8.0 → opencode_db-0.8.3}/docs/README_PYPI.md +5 -0
  4. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/cli.py +11 -1
  5. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/__init__.py +3 -0
  6. opencode_db-0.8.3/opencode_db/commands/cmd_directories.py +135 -0
  7. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_help.py +11 -0
  8. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_list.py +23 -11
  9. opencode_db-0.8.3/opencode_db/commands/cmd_projects.py +130 -0
  10. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/i18n.py +29 -0
  11. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/store/base.py +124 -0
  12. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/store/v1.py +1 -1
  13. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/store/v2.py +1 -1
  14. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/utils.py +8 -0
  15. opencode_db-0.8.3/opencode_db/version.py +25 -0
  16. {opencode_db-0.8.0 → opencode_db-0.8.3/opencode_db.egg-info}/PKG-INFO +6 -1
  17. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db.egg-info/SOURCES.txt +4 -1
  18. {opencode_db-0.8.0 → opencode_db-0.8.3}/pyproject.toml +1 -1
  19. {opencode_db-0.8.0 → opencode_db-0.8.3}/tests/test_commands.py +85 -0
  20. {opencode_db-0.8.0 → opencode_db-0.8.3}/tests/test_store_v2.py +99 -0
  21. opencode_db-0.8.3/tests/test_version.py +22 -0
  22. opencode_db-0.8.0/opencode_db/commands/cmd_projects.py +0 -83
  23. {opencode_db-0.8.0 → opencode_db-0.8.3}/LICENSE +0 -0
  24. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/__init__.py +0 -0
  25. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/__main__.py +0 -0
  26. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_costs.py +0 -0
  27. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_delete.py +0 -0
  28. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_export.py +0 -0
  29. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_info.py +0 -0
  30. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_prune.py +0 -0
  31. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_schema.py +0 -0
  32. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_search.py +0 -0
  33. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_stats.py +0 -0
  34. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_todos.py +0 -0
  35. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_tree.py +0 -0
  36. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_vacuum.py +0 -0
  37. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/commands/cmd_view.py +0 -0
  38. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/config.py +0 -0
  39. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/db.py +0 -0
  40. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/formatters.py +0 -0
  41. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/markdown_to_ansi.py +0 -0
  42. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/runtime.py +0 -0
  43. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/schema.py +0 -0
  44. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/schema_manifest.json +0 -0
  45. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db/store/__init__.py +0 -0
  46. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db.egg-info/dependency_links.txt +0 -0
  47. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db.egg-info/entry_points.txt +0 -0
  48. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db.egg-info/requires.txt +0 -0
  49. {opencode_db-0.8.0 → opencode_db-0.8.3}/opencode_db.egg-info/top_level.txt +0 -0
  50. {opencode_db-0.8.0 → opencode_db-0.8.3}/setup.cfg +0 -0
  51. {opencode_db-0.8.0 → opencode_db-0.8.3}/tests/test_db.py +0 -0
  52. {opencode_db-0.8.0 → opencode_db-0.8.3}/tests/test_formatters.py +0 -0
  53. {opencode_db-0.8.0 → opencode_db-0.8.3}/tests/test_i18n.py +0 -0
  54. {opencode_db-0.8.0 → opencode_db-0.8.3}/tests/test_runtime.py +0 -0
  55. {opencode_db-0.8.0 → opencode_db-0.8.3}/tests/test_schema.py +0 -0
  56. {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.0
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, 296 keys)
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 296 UI strings are translated. Data content (session titles, project names, etc.) remains in its original language.
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 + 205 tests
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
- for _name, mod in commands.get_all().items():
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
- table_rows.append(
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
- print_table_simple(
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",