opencode-db 0.6.1__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.
- {opencode_db-0.6.1 → opencode_db-0.6.2}/PKG-INFO +1 -1
- {opencode_db-0.6.1 → opencode_db-0.6.2}/formatters.py +16 -6
- {opencode_db-0.6.1 → opencode_db-0.6.2}/opencode_db.egg-info/PKG-INFO +1 -1
- {opencode_db-0.6.1 → opencode_db-0.6.2}/pyproject.toml +1 -1
- {opencode_db-0.6.1 → opencode_db-0.6.2}/tests/test_db.py +72 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/tests/test_formatters.py +45 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/LICENSE +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/README.md +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/README_PYPI.md +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/cli.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/cmd_costs.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/cmd_delete.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/cmd_export.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/cmd_help.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/cmd_info.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/cmd_list.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/cmd_projects.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/cmd_prune.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/cmd_search.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/cmd_stats.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/cmd_todos.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/cmd_tree.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/cmd_vacuum.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/commands/__init__.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/config.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/db.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/i18n.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/opencode_db.egg-info/SOURCES.txt +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/opencode_db.egg-info/dependency_links.txt +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/opencode_db.egg-info/entry_points.txt +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/opencode_db.egg-info/top_level.txt +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/setup.cfg +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/tests/test_commands.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/tests/test_i18n.py +0 -0
- {opencode_db-0.6.1 → opencode_db-0.6.2}/tests/test_utils.py +0 -0
- {opencode_db-0.6.1 → 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.
|
|
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
|
|
@@ -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(
|
|
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.
|
|
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.
|
|
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
|
|
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
|