opencode-db 0.6.0__tar.gz → 0.6.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {opencode_db-0.6.0 → opencode_db-0.6.2}/PKG-INFO +1 -1
  2. {opencode_db-0.6.0 → opencode_db-0.6.2}/README.md +25 -0
  3. {opencode_db-0.6.0 → opencode_db-0.6.2}/cli.py +10 -0
  4. {opencode_db-0.6.0 → opencode_db-0.6.2}/config.py +3 -1
  5. {opencode_db-0.6.0 → opencode_db-0.6.2}/formatters.py +16 -6
  6. {opencode_db-0.6.0 → opencode_db-0.6.2}/opencode_db.egg-info/PKG-INFO +1 -1
  7. {opencode_db-0.6.0 → opencode_db-0.6.2}/pyproject.toml +1 -1
  8. {opencode_db-0.6.0 → opencode_db-0.6.2}/tests/test_db.py +72 -0
  9. {opencode_db-0.6.0 → opencode_db-0.6.2}/tests/test_formatters.py +45 -0
  10. {opencode_db-0.6.0 → opencode_db-0.6.2}/LICENSE +0 -0
  11. {opencode_db-0.6.0 → opencode_db-0.6.2}/README_PYPI.md +0 -0
  12. {opencode_db-0.6.0 → opencode_db-0.6.2}/cmd_costs.py +0 -0
  13. {opencode_db-0.6.0 → opencode_db-0.6.2}/cmd_delete.py +0 -0
  14. {opencode_db-0.6.0 → opencode_db-0.6.2}/cmd_export.py +0 -0
  15. {opencode_db-0.6.0 → opencode_db-0.6.2}/cmd_help.py +0 -0
  16. {opencode_db-0.6.0 → opencode_db-0.6.2}/cmd_info.py +0 -0
  17. {opencode_db-0.6.0 → opencode_db-0.6.2}/cmd_list.py +0 -0
  18. {opencode_db-0.6.0 → opencode_db-0.6.2}/cmd_projects.py +0 -0
  19. {opencode_db-0.6.0 → opencode_db-0.6.2}/cmd_prune.py +0 -0
  20. {opencode_db-0.6.0 → opencode_db-0.6.2}/cmd_search.py +0 -0
  21. {opencode_db-0.6.0 → opencode_db-0.6.2}/cmd_stats.py +0 -0
  22. {opencode_db-0.6.0 → opencode_db-0.6.2}/cmd_todos.py +0 -0
  23. {opencode_db-0.6.0 → opencode_db-0.6.2}/cmd_tree.py +0 -0
  24. {opencode_db-0.6.0 → opencode_db-0.6.2}/cmd_vacuum.py +0 -0
  25. {opencode_db-0.6.0 → opencode_db-0.6.2}/commands/__init__.py +0 -0
  26. {opencode_db-0.6.0 → opencode_db-0.6.2}/db.py +0 -0
  27. {opencode_db-0.6.0 → opencode_db-0.6.2}/i18n.py +0 -0
  28. {opencode_db-0.6.0 → opencode_db-0.6.2}/opencode_db.egg-info/SOURCES.txt +0 -0
  29. {opencode_db-0.6.0 → opencode_db-0.6.2}/opencode_db.egg-info/dependency_links.txt +0 -0
  30. {opencode_db-0.6.0 → opencode_db-0.6.2}/opencode_db.egg-info/entry_points.txt +0 -0
  31. {opencode_db-0.6.0 → opencode_db-0.6.2}/opencode_db.egg-info/top_level.txt +0 -0
  32. {opencode_db-0.6.0 → opencode_db-0.6.2}/setup.cfg +0 -0
  33. {opencode_db-0.6.0 → opencode_db-0.6.2}/tests/test_commands.py +0 -0
  34. {opencode_db-0.6.0 → opencode_db-0.6.2}/tests/test_i18n.py +0 -0
  35. {opencode_db-0.6.0 → opencode_db-0.6.2}/tests/test_utils.py +0 -0
  36. {opencode_db-0.6.0 → opencode_db-0.6.2}/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencode-db
3
- Version: 0.6.0
3
+ Version: 0.6.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
@@ -26,6 +26,31 @@ opencode-db costs --total # total token costs
26
26
  - OpenCode (must have been run at least once — the database is created automatically)
27
27
  - macOS / Linux
28
28
 
29
+ ### Database location
30
+
31
+ By default, `opencode-db` looks for OpenCode's database at:
32
+
33
+ ```
34
+ ~/.local/share/opencode/opencode.db
35
+ ```
36
+
37
+ If your OpenCode stores the database at a custom path, you have two options:
38
+
39
+ **1. Environment variable** — set it once in your shell profile (`.zshrc`, `.bashrc`):
40
+
41
+ ```bash
42
+ export OPENCODE_DB=/path/to/opencode.db
43
+ ```
44
+
45
+ **2. Global `--db-path` flag** — for ad-hoc use or inspecting another database:
46
+
47
+ ```bash
48
+ opencode-db --db-path /path/to/opencode.db list
49
+ opencode-db --db-path /path/to/opencode.db stats
50
+ ```
51
+
52
+ The `--db-path` flag takes precedence over the environment variable.
53
+
29
54
  ### One-command install (recommended)
30
55
 
31
56
  ```bash
@@ -8,6 +8,7 @@
8
8
  """
9
9
 
10
10
  import argparse
11
+ import os
11
12
  import sys
12
13
  from typing import Any, Literal
13
14
 
@@ -28,6 +29,12 @@ def build_parser() -> argparse.ArgumentParser:
28
29
  default=None,
29
30
  help="Output language / язык вывода (en/ru)",
30
31
  )
32
+ parser.add_argument(
33
+ "--db-path",
34
+ type=str,
35
+ default=None,
36
+ help="Path to opencode.db (default: ~/.local/share/opencode/opencode.db)",
37
+ )
31
38
 
32
39
  subparsers = parser.add_subparsers(dest="command", required=True)
33
40
  for _name, mod in commands.get_all().items():
@@ -58,6 +65,9 @@ def main(argv=None) -> Any | Literal[0] | Literal[1]:
58
65
  if args.lang:
59
66
  set_lang(args.lang)
60
67
 
68
+ if args.db_path:
69
+ os.environ["OPENCODE_DB"] = args.db_path
70
+
61
71
  db = get_db()
62
72
 
63
73
  try:
@@ -8,7 +8,9 @@ import os
8
8
 
9
9
  # --- Пути ---
10
10
  # База данных opencode. Стандартное расположение для Linux/macOS.
11
- OPENCODE_DB = os.path.expanduser("~/.local/share/opencode/opencode.db")
11
+ OPENCODE_DB = os.environ.get("OPENCODE_DB") or os.path.expanduser(
12
+ "~/.local/share/opencode/opencode.db"
13
+ )
12
14
 
13
15
  # Директории, игнорируемые при сборе snapshot'а проекта
14
16
  IGNORE_DIRS = {".git", "node_modules", ".venv", "__pycache__", ".opencode"}
@@ -1,18 +1,21 @@
1
1
  """Форматирование вывода: таблицы, JSON, Markdown."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import json
4
6
  import os
7
+ from typing import Any
5
8
 
6
9
  from config import IGNORE_DIRS
7
10
  from i18n import _
8
11
 
9
12
 
10
- def print_json(data) -> None:
13
+ def print_json(data: Any) -> None:
11
14
  """Выводит данные в JSON-формате."""
12
15
  print(json.dumps(data, ensure_ascii=False, indent=2, default=str))
13
16
 
14
17
 
15
- def print_table_simple(headers, rows, sep=" ") -> None:
18
+ def print_table_simple(headers: list[str], rows: list[list], sep: str = " ") -> None:
16
19
  """Выводит таблицу с авто-шириной колонок."""
17
20
  if not rows:
18
21
  print(_("empty"))
@@ -37,7 +40,7 @@ def print_table_simple(headers, rows, sep=" ") -> None:
37
40
  print(_("fmt.rows_count", n=len(rows)))
38
41
 
39
42
 
40
- def summarize_val(v, max_len=120) -> str:
43
+ def summarize_val(v: Any, max_len: int = 120) -> str:
41
44
  """Обрезает длинное значение."""
42
45
  s = str(v)
43
46
  if len(s) > max_len:
@@ -45,7 +48,7 @@ def summarize_val(v, max_len=120) -> str:
45
48
  return s
46
49
 
47
50
 
48
- def format_part_to_md(part, full=False):
51
+ def format_part_to_md(part: dict, full: bool = False) -> str:
49
52
  """Преобразует часть сообщения в строку Markdown.
50
53
 
51
54
  Args:
@@ -149,7 +152,7 @@ def format_part_to_md(part, full=False):
149
152
  return ""
150
153
 
151
154
 
152
- def collect_snapshot(project_dir):
155
+ def collect_snapshot(project_dir: str) -> dict[str, Any]:
153
156
  """Собирает статистику по проекту для log.md.
154
157
 
155
158
  Returns:
@@ -182,7 +185,14 @@ def collect_snapshot(project_dir):
182
185
  return snapshot
183
186
 
184
187
 
185
- def update_log(output_dir, title, note, dialog_filename, snapshot, now_str) -> None:
188
+ def update_log(
189
+ output_dir: str,
190
+ title: str,
191
+ note: str | None,
192
+ dialog_filename: str | None,
193
+ snapshot: dict[str, Any],
194
+ now_str: str,
195
+ ) -> None:
186
196
  """Создаёт или дополняет log.md в директории проекта."""
187
197
  log_path = os.path.join(output_dir, "log.md")
188
198
  heading = f"{title}: {note}" if note else title
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencode-db
3
- Version: 0.6.0
3
+ Version: 0.6.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "opencode-db"
7
- version = "0.6.0"
7
+ version = "0.6.2"
8
8
  description = "OpenCode database CLI manager — browse, export, analyze, clean up sessions"
9
9
  readme = "README_PYPI.md"
10
10
  requires-python = ">=3.12"
@@ -275,3 +275,75 @@ class TestGetMessages:
275
275
  )
276
276
  messages = get_messages(db, "ses_001")
277
277
  assert len(messages) == 2
278
+
279
+
280
+ class TestEdgeCases:
281
+ def test_session_null_title_and_model(self, db) -> None:
282
+ db.execute(
283
+ "INSERT INTO session (id, time_created, title, model, cost) VALUES (?, ?, ?, ?, ?)",
284
+ ("ses_null_meta", 50000, None, None, 0.0),
285
+ )
286
+ db.commit()
287
+ info = get_session_info(db, "ses_null_meta")
288
+ assert info["title"] is None
289
+ assert info["model"] is None
290
+
291
+ def test_session_zero_cost(self, db) -> None:
292
+ db.execute(
293
+ "INSERT INTO session (id, time_created, cost) VALUES (?, ?, ?)",
294
+ ("ses_zero_cost", 55000, 0.0),
295
+ )
296
+ db.commit()
297
+ info = get_session_info(db, "ses_zero_cost")
298
+ assert info["cost"] == 0.0
299
+
300
+ def test_session_null_parent(self, db) -> None:
301
+ db.execute(
302
+ "INSERT INTO session (id, time_created, parent_id) VALUES (?, ?, ?)",
303
+ ("ses_orphan", 60000, None),
304
+ )
305
+ db.commit()
306
+ children = get_children_sessions(db, "ses_orphan")
307
+ assert children == []
308
+
309
+ def test_part_without_type_has_empty_parts(self, db) -> None:
310
+ db.execute(
311
+ "INSERT INTO session (id, time_created) VALUES (?, ?)",
312
+ ("ses_skip", 1000),
313
+ )
314
+ db.execute(
315
+ "INSERT INTO message (id, session_id, time_created, data) VALUES (?, ?, ?, ?)",
316
+ ("msg_skip", "ses_skip", 1000, json.dumps({"role": "user"})),
317
+ )
318
+ db.execute(
319
+ "INSERT INTO part (id, message_id, session_id, time_created, data) VALUES (?, ?, ?, ?, ?)",
320
+ ("part_skip", "msg_skip", "ses_skip", 1000, json.dumps({"text": "no type"})),
321
+ )
322
+ db.commit()
323
+ messages = get_messages(db, "ses_skip")
324
+ assert len(messages) == 1
325
+ assert len(messages[0]["parts"]) == 0
326
+
327
+ def test_message_null_role(self, db) -> None:
328
+ db.execute(
329
+ "INSERT INTO session (id, time_created) VALUES (?, ?)",
330
+ ("ses_edge_b", 2000),
331
+ )
332
+ db.execute(
333
+ "INSERT INTO message (id, session_id, time_created, data) VALUES (?, ?, ?, ?)",
334
+ ("msg_edge_b", "ses_edge_b", 2000, json.dumps({"role": None})),
335
+ )
336
+ db.execute(
337
+ "INSERT INTO part (id, message_id, session_id, time_created, data) VALUES (?, ?, ?, ?, ?)",
338
+ (
339
+ "part_edge_b",
340
+ "msg_edge_b",
341
+ "ses_edge_b",
342
+ 2000,
343
+ json.dumps({"type": "text", "text": "hi"}),
344
+ ),
345
+ )
346
+ db.commit()
347
+ messages = get_messages(db, "ses_edge_b")
348
+ assert len(messages) == 1
349
+ assert messages[0]["role"] is None or messages[0]["role"] == "?"
@@ -171,3 +171,48 @@ class TestFormatPartToMd:
171
171
  full=True,
172
172
  )
173
173
  assert "line" in result
174
+
175
+ def test_unknown_type_returns_empty(self):
176
+ result = format_part_to_md({"type": "unknown_type"})
177
+ assert result == ""
178
+
179
+ def test_empty_part_returns_empty(self):
180
+ result = format_part_to_md({"type": "text", "text": ""})
181
+ assert result == ""
182
+
183
+ def test_tool_without_input_key(self):
184
+ result = format_part_to_md(
185
+ {
186
+ "type": "tool",
187
+ "tool": "bash",
188
+ }
189
+ )
190
+ assert "bash" in result
191
+
192
+ def test_tool_with_null_output(self):
193
+ result = format_part_to_md(
194
+ {
195
+ "type": "tool",
196
+ "tool": "bash",
197
+ "input": {"command": "ls"},
198
+ "output": None,
199
+ "status": "completed",
200
+ }
201
+ )
202
+ assert "bash" in result
203
+
204
+ def test_tool_with_unknown_status(self):
205
+ result = format_part_to_md(
206
+ {
207
+ "type": "tool",
208
+ "tool": "bash",
209
+ "input": {"command": "ls"},
210
+ "output": "",
211
+ "status": "unknown",
212
+ }
213
+ )
214
+ assert "🔧" in result
215
+
216
+ def test_reasoning_empty_text(self):
217
+ result = format_part_to_md({"type": "reasoning", "text": ""})
218
+ assert result == ""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes