opencode-db 0.3.0__py3-none-any.whl
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.
- cli.py +70 -0
- cmd_costs.py +197 -0
- cmd_delete.py +58 -0
- cmd_export.py +189 -0
- cmd_help.py +210 -0
- cmd_info.py +152 -0
- cmd_list.py +124 -0
- cmd_projects.py +82 -0
- cmd_prune.py +127 -0
- cmd_search.py +111 -0
- cmd_stats.py +115 -0
- cmd_todos.py +90 -0
- cmd_tree.py +95 -0
- cmd_vacuum.py +67 -0
- commands/__init__.py +51 -0
- config.py +14 -0
- db.py +374 -0
- formatters.py +208 -0
- i18n.py +532 -0
- opencode_db-0.3.0.dist-info/METADATA +7 -0
- opencode_db-0.3.0.dist-info/RECORD +26 -0
- opencode_db-0.3.0.dist-info/WHEEL +5 -0
- opencode_db-0.3.0.dist-info/entry_points.txt +2 -0
- opencode_db-0.3.0.dist-info/licenses/LICENSE +25 -0
- opencode_db-0.3.0.dist-info/top_level.txt +20 -0
- utils.py +115 -0
cli.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Точка входа: парсинг аргументов и диспетчеризация команд.
|
|
2
|
+
|
|
3
|
+
Каждая команда регистрирует свой subparser через register(subparsers).
|
|
4
|
+
Это даёт:
|
|
5
|
+
- Автоматический --help для каждой команды
|
|
6
|
+
- Валидацию аргументов
|
|
7
|
+
- Единообразный интерфейс
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any, Literal
|
|
13
|
+
|
|
14
|
+
import commands
|
|
15
|
+
from db import get_db
|
|
16
|
+
from i18n import _, set_lang
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
20
|
+
"""Создаёт парсер аргументов со всеми подкомандами."""
|
|
21
|
+
parser = argparse.ArgumentParser(
|
|
22
|
+
prog="opencode-db",
|
|
23
|
+
description="OpenCode database manager: browse, export, analyze, clean up sessions.",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--lang",
|
|
27
|
+
choices=["en", "ru"],
|
|
28
|
+
default=None,
|
|
29
|
+
help="Output language / язык вывода (en/ru)",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
33
|
+
for _name, mod in commands.get_all().items():
|
|
34
|
+
mod.register(subparsers)
|
|
35
|
+
return parser
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main(argv=None) -> Any | Literal[0] | Literal[1]:
|
|
39
|
+
"""Главная функция. Парсит аргументы, открывает БД, запускает команду.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
argv: список аргументов (по умолчанию sys.argv[1:])
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
int: код возврата (0 = успех, 1 = ошибка)
|
|
46
|
+
"""
|
|
47
|
+
parser = build_parser()
|
|
48
|
+
|
|
49
|
+
if argv is None:
|
|
50
|
+
argv = sys.argv[1:]
|
|
51
|
+
|
|
52
|
+
if not argv:
|
|
53
|
+
parser.print_help()
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
args = parser.parse_args(argv)
|
|
57
|
+
|
|
58
|
+
if args.lang:
|
|
59
|
+
set_lang(args.lang)
|
|
60
|
+
|
|
61
|
+
db = get_db()
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
handler = commands.get_handler(args.command)
|
|
65
|
+
if handler is None:
|
|
66
|
+
print(_("cmd.unknown", cmd=args.command))
|
|
67
|
+
return 1
|
|
68
|
+
return handler.run(args, db)
|
|
69
|
+
finally:
|
|
70
|
+
db.close()
|
cmd_costs.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Команда costs: анализ расходов на токены.
|
|
2
|
+
|
|
3
|
+
Стоимость берётся напрямую из поля session.cost БД OpenCode.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from db import get_session_info, get_session_title, parse_model
|
|
9
|
+
from i18n import _
|
|
10
|
+
from utils import format_cost, format_tokens
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register(subparsers) -> None:
|
|
14
|
+
p = subparsers.add_parser("costs", help=_("help.cmd.costs"))
|
|
15
|
+
p.add_argument("session_id", nargs="?", help="Session ID")
|
|
16
|
+
p.add_argument("--project", type=str, help="Filter by project")
|
|
17
|
+
p.add_argument("--total", action="store_true", help="Total across all sessions")
|
|
18
|
+
p.add_argument("--json", action="store_true", help="JSON output")
|
|
19
|
+
p.add_argument("--limit", type=int, default=30, help="Max rows")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run(args, db) -> Literal[0]:
|
|
23
|
+
if args.session_id:
|
|
24
|
+
return _show_session(db, args.session_id, args.json)
|
|
25
|
+
|
|
26
|
+
if args.total:
|
|
27
|
+
return _show_total(db, args.project, args.json)
|
|
28
|
+
|
|
29
|
+
return _show_list(db, args.project, args.limit, args.json)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _show_session(db, session_id, as_json) -> Literal[0]:
|
|
33
|
+
info = get_session_info(db, session_id)
|
|
34
|
+
title = get_session_title(info)
|
|
35
|
+
model = parse_model(info)
|
|
36
|
+
|
|
37
|
+
ti = info["tokens_input"] or 0
|
|
38
|
+
to = info["tokens_output"] or 0
|
|
39
|
+
tr = info["tokens_reasoning"] or 0
|
|
40
|
+
cost = info["cost"] or 0
|
|
41
|
+
|
|
42
|
+
if as_json:
|
|
43
|
+
from formatters import print_json
|
|
44
|
+
|
|
45
|
+
print_json(
|
|
46
|
+
{
|
|
47
|
+
"id": info["id"],
|
|
48
|
+
"title": title,
|
|
49
|
+
"model": model,
|
|
50
|
+
"tokens_input": ti,
|
|
51
|
+
"tokens_output": to,
|
|
52
|
+
"tokens_reasoning": tr,
|
|
53
|
+
"tokens_total": ti + to + tr,
|
|
54
|
+
"cost": cost,
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
print(f"\n {_('costs.session')}: {title}")
|
|
60
|
+
print(f" {_('costs.model')}: {model}")
|
|
61
|
+
print(f" {'─' * 50}")
|
|
62
|
+
print(f" {_('costs.input')}: {format_tokens(ti)}")
|
|
63
|
+
print(f" {_('costs.output')}: {format_tokens(to)}")
|
|
64
|
+
print(f" {_('costs.reasoning')}: {format_tokens(tr)}")
|
|
65
|
+
if info["tokens_cache_read"]:
|
|
66
|
+
print(f" {_('costs.cache_read')}: {format_tokens(info['tokens_cache_read'])}")
|
|
67
|
+
if info["tokens_cache_write"]:
|
|
68
|
+
print(f" {_('costs.cache_write')}: {format_tokens(info['tokens_cache_write'])}")
|
|
69
|
+
print(f" {'─' * 50}")
|
|
70
|
+
print(f" {_('costs.total')}: {format_tokens(ti + to + tr)}")
|
|
71
|
+
print(f" {_('costs.cost')}: {format_cost(cost)}")
|
|
72
|
+
print()
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _show_total(db, project_id, as_json) -> Literal[0]:
|
|
77
|
+
where = "1=1"
|
|
78
|
+
params = []
|
|
79
|
+
|
|
80
|
+
if project_id:
|
|
81
|
+
where = "s.project_id = ?"
|
|
82
|
+
params.append(project_id)
|
|
83
|
+
|
|
84
|
+
row = db.execute(
|
|
85
|
+
f"""
|
|
86
|
+
SELECT COUNT(*) as cnt,
|
|
87
|
+
SUM(tokens_input) as ti, SUM(tokens_output) as tok_out,
|
|
88
|
+
SUM(tokens_reasoning) as tr, SUM(cost) as c
|
|
89
|
+
FROM session WHERE {where}
|
|
90
|
+
""",
|
|
91
|
+
params,
|
|
92
|
+
).fetchone()
|
|
93
|
+
|
|
94
|
+
cnt = row["cnt"] or 0
|
|
95
|
+
total_cost = row["c"] or 0
|
|
96
|
+
total_ti = row["ti"] or 0
|
|
97
|
+
total_to = row["tok_out"] or 0
|
|
98
|
+
total_tr = row["tr"] or 0
|
|
99
|
+
|
|
100
|
+
if as_json:
|
|
101
|
+
from formatters import print_json
|
|
102
|
+
|
|
103
|
+
print_json(
|
|
104
|
+
{
|
|
105
|
+
"sessions": cnt,
|
|
106
|
+
"tokens_input": total_ti,
|
|
107
|
+
"tokens_output": total_to,
|
|
108
|
+
"tokens_reasoning": total_tr,
|
|
109
|
+
"tokens_total": total_ti + total_to + total_tr,
|
|
110
|
+
"cost": total_cost,
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
scope = _("costs.scope_project") if project_id else _("costs.scope_all")
|
|
116
|
+
print(f"\n {'=' * 45}")
|
|
117
|
+
print(f" {_('costs.total_header', scope=scope)}")
|
|
118
|
+
print(f" {_('costs.sessions')}: {cnt}")
|
|
119
|
+
print(f" {'=' * 45}")
|
|
120
|
+
print(f" {_('costs.tokens_input')}: {format_tokens(total_ti)}")
|
|
121
|
+
print(f" {_('costs.tokens_output')}: {format_tokens(total_to)}")
|
|
122
|
+
print(f" {_('costs.tokens_reasoning')}: {format_tokens(total_tr)}")
|
|
123
|
+
print(f" {_('costs.tokens_total')}: {format_tokens(total_ti + total_to + total_tr)}")
|
|
124
|
+
print(f" {_('costs.cost')}: {format_cost(total_cost)}")
|
|
125
|
+
print()
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _show_list(db, project_id, limit, as_json) -> Literal[0]:
|
|
130
|
+
where = "1=1"
|
|
131
|
+
params = []
|
|
132
|
+
|
|
133
|
+
if project_id:
|
|
134
|
+
where = "s.project_id = ?"
|
|
135
|
+
params.append(project_id)
|
|
136
|
+
|
|
137
|
+
rows = db.execute(
|
|
138
|
+
f"""
|
|
139
|
+
SELECT s.id, s.title, s.model,
|
|
140
|
+
s.tokens_input, s.tokens_output, s.tokens_reasoning,
|
|
141
|
+
s.cost
|
|
142
|
+
FROM session s
|
|
143
|
+
WHERE {where}
|
|
144
|
+
ORDER BY (s.tokens_input + s.tokens_output) DESC
|
|
145
|
+
LIMIT ?
|
|
146
|
+
""",
|
|
147
|
+
(*params, limit),
|
|
148
|
+
).fetchall()
|
|
149
|
+
|
|
150
|
+
if as_json:
|
|
151
|
+
from formatters import print_json
|
|
152
|
+
|
|
153
|
+
data = []
|
|
154
|
+
for r in rows:
|
|
155
|
+
data.append(
|
|
156
|
+
{
|
|
157
|
+
"id": r["id"],
|
|
158
|
+
"title": get_session_title(r),
|
|
159
|
+
"model": parse_model(r),
|
|
160
|
+
"tokens_total": (r["tokens_input"] or 0)
|
|
161
|
+
+ (r["tokens_output"] or 0)
|
|
162
|
+
+ (r["tokens_reasoning"] or 0),
|
|
163
|
+
"cost": r["cost"] or 0,
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
print_json(data)
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
from formatters import print_table_simple
|
|
170
|
+
|
|
171
|
+
table = []
|
|
172
|
+
for r in rows:
|
|
173
|
+
total_tokens = (
|
|
174
|
+
(r["tokens_input"] or 0) + (r["tokens_output"] or 0) + (r["tokens_reasoning"] or 0)
|
|
175
|
+
)
|
|
176
|
+
table.append(
|
|
177
|
+
[
|
|
178
|
+
r["id"][:24],
|
|
179
|
+
get_session_title(r)[:28],
|
|
180
|
+
parse_model(r)[:20],
|
|
181
|
+
format_tokens(total_tokens),
|
|
182
|
+
format_cost(r["cost"] or 0),
|
|
183
|
+
]
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
print_table_simple(
|
|
187
|
+
[
|
|
188
|
+
_("list.header.id"),
|
|
189
|
+
_("list.header.title"),
|
|
190
|
+
_("list.header.model"),
|
|
191
|
+
_("costs.total"),
|
|
192
|
+
_("costs.cost"),
|
|
193
|
+
],
|
|
194
|
+
table,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return 0
|
cmd_delete.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Команда delete: удаление сессии."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from db import (
|
|
6
|
+
get_message_count,
|
|
7
|
+
get_part_count,
|
|
8
|
+
get_session_info,
|
|
9
|
+
get_session_title,
|
|
10
|
+
parse_model,
|
|
11
|
+
)
|
|
12
|
+
from i18n import _
|
|
13
|
+
from utils import confirm, format_cost, format_ts
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register(subparsers) -> None:
|
|
17
|
+
p = subparsers.add_parser("delete", help=_("help.cmd.delete"))
|
|
18
|
+
p.add_argument("session_id", help="Session ID to delete")
|
|
19
|
+
p.add_argument("--dry-run", action="store_true", help="Preview without deleting")
|
|
20
|
+
p.add_argument("--force", "-f", action="store_true", help="Skip confirmation")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run(args, db) -> Literal[0]:
|
|
24
|
+
info = get_session_info(db, args.session_id)
|
|
25
|
+
session_id = info["id"]
|
|
26
|
+
title = get_session_title(info)
|
|
27
|
+
model = parse_model(info)
|
|
28
|
+
|
|
29
|
+
msg_count = get_message_count(db, session_id)
|
|
30
|
+
part_count = get_part_count(db, session_id)
|
|
31
|
+
created = format_ts(info["time_created"])
|
|
32
|
+
|
|
33
|
+
print(f"\n{'=' * 50}")
|
|
34
|
+
print(f" {_('delete.header')}")
|
|
35
|
+
print(f"{'=' * 50}")
|
|
36
|
+
print(f" {_('delete.id')}: {session_id}")
|
|
37
|
+
print(f" {_('delete.title')}: {title}")
|
|
38
|
+
print(f" {_('delete.model')}: {model}")
|
|
39
|
+
print(f" {_('delete.created')}: {created}")
|
|
40
|
+
print(f" {_('delete.messages')}: {msg_count}")
|
|
41
|
+
print(f" {_('delete.parts')}: {part_count}")
|
|
42
|
+
print(f" {_('delete.cost')}: {format_cost(info['cost'])}")
|
|
43
|
+
|
|
44
|
+
if args.dry_run:
|
|
45
|
+
print(f"\n{_('delete.dry_run')}")
|
|
46
|
+
print(f" {_('delete.dry_run_hint')}")
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
if not args.force and not confirm(_("delete.confirm", id=session_id[:24])):
|
|
50
|
+
print(_("canceled"))
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
db.execute("DELETE FROM session WHERE id = ?", (session_id,))
|
|
54
|
+
db.commit()
|
|
55
|
+
|
|
56
|
+
print(f"\n{_('delete.done', id=session_id[:24])}")
|
|
57
|
+
print(_("delete.done_detail", msg=msg_count, parts=part_count))
|
|
58
|
+
return 0
|
cmd_export.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Команда export: экспорт диалога в Markdown.
|
|
2
|
+
|
|
3
|
+
Режимы выбора сессии:
|
|
4
|
+
- export <session_id> — по ID (полный или префикс)
|
|
5
|
+
- export --latest — самая свежая сессия
|
|
6
|
+
- export (без аргументов) — интерактивный выбор из списка
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
from db import (
|
|
13
|
+
get_latest_session,
|
|
14
|
+
get_messages,
|
|
15
|
+
get_recent_sessions,
|
|
16
|
+
get_session_info,
|
|
17
|
+
get_session_title,
|
|
18
|
+
parse_model,
|
|
19
|
+
)
|
|
20
|
+
from formatters import collect_snapshot, format_part_to_md, update_log
|
|
21
|
+
from i18n import _
|
|
22
|
+
from utils import format_cost, format_ts, format_ts_short
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def register(subparsers) -> None:
|
|
26
|
+
p = subparsers.add_parser("export", help=_("help.cmd.export"))
|
|
27
|
+
p.add_argument(
|
|
28
|
+
"session_id",
|
|
29
|
+
nargs="?",
|
|
30
|
+
help="Session ID (optional — interactive mode if omitted)",
|
|
31
|
+
)
|
|
32
|
+
p.add_argument("--latest", "-l", action="store_true", help="Export the most recent session")
|
|
33
|
+
p.add_argument(
|
|
34
|
+
"--full",
|
|
35
|
+
"-F",
|
|
36
|
+
action="store_true",
|
|
37
|
+
help="Full output (do not truncate tool outputs)",
|
|
38
|
+
)
|
|
39
|
+
p.add_argument("--force", "-f", action="store_true", help="Overwrite existing file")
|
|
40
|
+
p.add_argument("--note", "-n", type=str, help="Description for log.md")
|
|
41
|
+
p.add_argument(
|
|
42
|
+
"--output",
|
|
43
|
+
"-o",
|
|
44
|
+
type=str,
|
|
45
|
+
default=None,
|
|
46
|
+
help="Output directory (default: current)",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _export_by_id(db, session_id, output_dir, force, note, full) -> Literal[1] | Literal[0]:
|
|
51
|
+
session_info = get_session_info(db, session_id)
|
|
52
|
+
return _do_export(db, session_info, output_dir, force, note, full)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _export_latest(db, output_dir, force, note, full) -> Literal[1] | Literal[0]:
|
|
56
|
+
session_id = get_latest_session(db)
|
|
57
|
+
session_info = get_session_info(db, session_id)
|
|
58
|
+
title = get_session_title(session_info)
|
|
59
|
+
print(_("session.latest", title=title))
|
|
60
|
+
return _do_export(db, session_info, output_dir, force, note, full)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _export_interactive(db, output_dir, force, note, full) -> Literal[1] | Literal[0]:
|
|
64
|
+
sessions = get_recent_sessions(db)
|
|
65
|
+
|
|
66
|
+
if not sessions:
|
|
67
|
+
print(_("export.no_sessions"))
|
|
68
|
+
return 1
|
|
69
|
+
|
|
70
|
+
print(f"\n {'─' * 60}")
|
|
71
|
+
print(f" {_('export.interactive_header')}")
|
|
72
|
+
print(f" {'─' * 60}")
|
|
73
|
+
|
|
74
|
+
for i, s in enumerate(sessions, 1):
|
|
75
|
+
title = get_session_title(s)
|
|
76
|
+
created = format_ts(s["time_created"], "%Y-%m-%d %H:%M")
|
|
77
|
+
cost = format_cost(s["cost"])
|
|
78
|
+
print(f" {i:2d}. {s['id'][:24]} {created} {title[:40]:40s} {cost}")
|
|
79
|
+
|
|
80
|
+
print(f" {'─' * 60}")
|
|
81
|
+
|
|
82
|
+
while True:
|
|
83
|
+
try:
|
|
84
|
+
choice = input(" " + _("export.interactive_prompt")).strip()
|
|
85
|
+
except (EOFError, KeyboardInterrupt):
|
|
86
|
+
print()
|
|
87
|
+
return 1
|
|
88
|
+
|
|
89
|
+
if not choice:
|
|
90
|
+
print(_("export.interactive_abort"))
|
|
91
|
+
return 1
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
idx = int(choice) - 1
|
|
95
|
+
if 0 <= idx < len(sessions):
|
|
96
|
+
break
|
|
97
|
+
except ValueError:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
print(f" {_('export.interactive_error', n=len(sessions))}")
|
|
101
|
+
|
|
102
|
+
session_id = sessions[idx]["id"]
|
|
103
|
+
title = get_session_title(sessions[idx])
|
|
104
|
+
print(_("export.interactive_exporting", title=title))
|
|
105
|
+
session_info = get_session_info(db, session_id)
|
|
106
|
+
return _do_export(db, session_info, output_dir, force, note, full)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _do_export(db, session_info, output_dir, force, note, full=False) -> Literal[1] | Literal[0]:
|
|
110
|
+
"""Внутренняя функция: экспорт диалога."""
|
|
111
|
+
title = get_session_title(session_info)
|
|
112
|
+
model = parse_model(session_info)
|
|
113
|
+
|
|
114
|
+
messages = get_messages(db, session_info["id"])
|
|
115
|
+
|
|
116
|
+
date_str = format_ts_short(session_info["time_created"])
|
|
117
|
+
filename = os.path.join(output_dir, f"opencode-{date_str}.md")
|
|
118
|
+
|
|
119
|
+
if os.path.isfile(filename) and not force:
|
|
120
|
+
print(_("export.exists", path=filename))
|
|
121
|
+
print(_("export.exists_hint"))
|
|
122
|
+
return 1
|
|
123
|
+
|
|
124
|
+
date_display = format_ts(session_info["time_created"])
|
|
125
|
+
|
|
126
|
+
md_header = f"""# {_("md.header.title")}: {title}
|
|
127
|
+
|
|
128
|
+
- **{_("md.header.date")}**: {date_display}
|
|
129
|
+
- **{_("md.header.model")}**: {model}
|
|
130
|
+
- **{_("md.header.session_id")}**: `{session_info["id"][:24]}`
|
|
131
|
+
- **{_("md.header.messages")}**: {len(messages)}
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
md_body = []
|
|
137
|
+
for msg in messages:
|
|
138
|
+
if not msg.get("parts"):
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
ts = format_ts(msg["time"], "%H:%M:%S")
|
|
142
|
+
|
|
143
|
+
if msg["role"] == "user":
|
|
144
|
+
md_body.append(f"## {_('export.role_user', ts=ts)}")
|
|
145
|
+
else:
|
|
146
|
+
agent_tag = _("export.agent_tag", agent=msg["agent"]) if msg.get("agent") else ""
|
|
147
|
+
md_body.append(f"## {_('export.role_assistant', ts=ts, agent=agent_tag)}")
|
|
148
|
+
|
|
149
|
+
md_body.append("")
|
|
150
|
+
|
|
151
|
+
for part in msg["parts"]:
|
|
152
|
+
formatted = format_part_to_md(part, full=full)
|
|
153
|
+
if formatted:
|
|
154
|
+
md_body.append(formatted)
|
|
155
|
+
md_body.append("")
|
|
156
|
+
|
|
157
|
+
md_body.append("---")
|
|
158
|
+
md_body.append("")
|
|
159
|
+
|
|
160
|
+
content = md_header + "\n".join(md_body)
|
|
161
|
+
|
|
162
|
+
with open(filename, "w", encoding="utf-8") as f:
|
|
163
|
+
f.write(content)
|
|
164
|
+
|
|
165
|
+
print(_("export.saved", path=filename))
|
|
166
|
+
print(_("export.messages", n=len(messages)))
|
|
167
|
+
|
|
168
|
+
obsidian_dir = os.getcwd()
|
|
169
|
+
if os.path.isdir(os.path.join(obsidian_dir, ".obsidian")):
|
|
170
|
+
snapshot = collect_snapshot(obsidian_dir)
|
|
171
|
+
now_str = format_ts(session_info["time_created"], "%Y-%m-%d %H:%M")
|
|
172
|
+
update_log(obsidian_dir, title, note, filename, snapshot, now_str)
|
|
173
|
+
else:
|
|
174
|
+
print(_("not_obsidian"))
|
|
175
|
+
|
|
176
|
+
return 0
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def run(args, db) -> Literal[1] | Literal[0]:
|
|
180
|
+
output_dir = args.output or os.getcwd()
|
|
181
|
+
full = args.full
|
|
182
|
+
|
|
183
|
+
if args.session_id:
|
|
184
|
+
return _export_by_id(db, args.session_id, output_dir, args.force, args.note, full)
|
|
185
|
+
|
|
186
|
+
if args.latest:
|
|
187
|
+
return _export_latest(db, output_dir, args.force, args.note, full)
|
|
188
|
+
|
|
189
|
+
return _export_interactive(db, output_dir, args.force, args.note, full)
|