copilot-session-usage 0.1.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.
- copilot_session_usage/__init__.py +10 -0
- copilot_session_usage/_internal/__init__.py +1 -0
- copilot_session_usage/_internal/copilot_cli.py +18 -0
- copilot_session_usage/_internal/core.py +1016 -0
- copilot_session_usage/_internal/vscode.py +242 -0
- copilot_session_usage/api.py +211 -0
- copilot_session_usage/cli.py +282 -0
- copilot_session_usage/data/__init__.py +0 -0
- copilot_session_usage/data/custom-models-pricing.yml +22 -0
- copilot_session_usage/data/models-and-pricing.lock +7 -0
- copilot_session_usage/data/models-and-pricing.yml +269 -0
- copilot_session_usage-0.1.0.dist-info/METADATA +187 -0
- copilot_session_usage-0.1.0.dist-info/RECORD +16 -0
- copilot_session_usage-0.1.0.dist-info/WHEEL +4 -0
- copilot_session_usage-0.1.0.dist-info/entry_points.txt +2 -0
- copilot_session_usage-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""VS Code Copilot session discovery logic.
|
|
2
|
+
|
|
3
|
+
Implements workspace storage layout parsing, state.vscdb queries, and
|
|
4
|
+
session directory resolution for the VS Code Copilot extension.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sqlite3
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
# ─── Workspace DB cache ─────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
_WS_DB_CACHE: dict[str, list[dict]] = {}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ─── Workspace storage path resolution (cross-platform) ─────────────────────
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def default_workspace_storage_roots() -> list[Path]:
|
|
25
|
+
r"""Return all existing workspaceStorage directories for the current platform.
|
|
26
|
+
|
|
27
|
+
Checked locations:
|
|
28
|
+
|
|
29
|
+
- macOS: ~/Library/Application Support/Code{,-Insiders}/User/workspaceStorage
|
|
30
|
+
- Windows: %APPDATA%\\Code{,-Insiders}\\User\\workspaceStorage
|
|
31
|
+
- Linux: $XDG_CONFIG_HOME/Code{,-Insiders}/User/workspaceStorage
|
|
32
|
+
~/.vscode-server{,-insiders}/data/User/workspaceStorage (WSL2 / remote)
|
|
33
|
+
|
|
34
|
+
Only directories that actually exist are returned.
|
|
35
|
+
"""
|
|
36
|
+
home = Path.home()
|
|
37
|
+
candidates: list[Path] = []
|
|
38
|
+
|
|
39
|
+
if sys.platform == "darwin":
|
|
40
|
+
base = home / "Library" / "Application Support"
|
|
41
|
+
for variant in ("Code", "Code - Insiders"):
|
|
42
|
+
candidates.append(base / variant / "User" / "workspaceStorage")
|
|
43
|
+
|
|
44
|
+
elif sys.platform == "win32":
|
|
45
|
+
appdata = Path(os.environ.get("APPDATA") or (home / "AppData" / "Roaming"))
|
|
46
|
+
for variant in ("Code", "Code - Insiders"):
|
|
47
|
+
candidates.append(appdata / variant / "User" / "workspaceStorage")
|
|
48
|
+
|
|
49
|
+
else:
|
|
50
|
+
xdg = Path(os.environ.get("XDG_CONFIG_HOME") or (home / ".config"))
|
|
51
|
+
for variant in ("Code", "Code - Insiders"):
|
|
52
|
+
candidates.append(xdg / variant / "User" / "workspaceStorage")
|
|
53
|
+
for variant in (".vscode-server", ".vscode-server-insiders"):
|
|
54
|
+
candidates.append(home / variant / "data" / "User" / "workspaceStorage")
|
|
55
|
+
|
|
56
|
+
return [p for p in candidates if p.is_dir()]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_workspace_folder(ws_dir: Path) -> str:
|
|
60
|
+
"""Return the actual folder path for a workspace storage directory."""
|
|
61
|
+
ws_json = ws_dir / "workspace.json"
|
|
62
|
+
if ws_json.exists():
|
|
63
|
+
try:
|
|
64
|
+
data: dict[str, Any] = json.loads(ws_json.read_text(encoding="utf-8"))
|
|
65
|
+
folder = data.get("folder") or data.get("workspace", "")
|
|
66
|
+
if isinstance(folder, str):
|
|
67
|
+
return folder.removeprefix("file://") if folder.startswith("file://") else folder
|
|
68
|
+
except (json.JSONDecodeError, OSError):
|
|
69
|
+
pass
|
|
70
|
+
return ""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_sessions_from_workspace(ws_dir: Path, use_cache: bool = True) -> list[dict]:
|
|
74
|
+
"""Return session metadata from a workspace's state.vscdb.
|
|
75
|
+
|
|
76
|
+
When ``use_cache`` is True (the default), results are cached per
|
|
77
|
+
workspace directory so batch operations only read each DB once.
|
|
78
|
+
"""
|
|
79
|
+
cache_key = str(ws_dir.resolve())
|
|
80
|
+
if use_cache and cache_key in _WS_DB_CACHE:
|
|
81
|
+
return _WS_DB_CACHE[cache_key]
|
|
82
|
+
|
|
83
|
+
db_path = ws_dir / "state.vscdb"
|
|
84
|
+
if not db_path.exists():
|
|
85
|
+
return []
|
|
86
|
+
try:
|
|
87
|
+
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
|
88
|
+
cur = conn.execute("SELECT value FROM ItemTable WHERE key = 'chat.ChatSessionStore.index'")
|
|
89
|
+
row = cur.fetchone()
|
|
90
|
+
conn.close()
|
|
91
|
+
except (sqlite3.OperationalError, sqlite3.DatabaseError):
|
|
92
|
+
return []
|
|
93
|
+
if not row:
|
|
94
|
+
return []
|
|
95
|
+
try:
|
|
96
|
+
data = json.loads(row[0])
|
|
97
|
+
except json.JSONDecodeError:
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
workspace_folder = _get_workspace_folder(ws_dir)
|
|
101
|
+
sessions = []
|
|
102
|
+
for v in data.get("entries", {}).values():
|
|
103
|
+
session_id = v.get("sessionId", "")
|
|
104
|
+
if not session_id:
|
|
105
|
+
continue
|
|
106
|
+
debug_log_dir = ws_dir / "GitHub.copilot-chat/debug-logs" / session_id
|
|
107
|
+
sessions.append(
|
|
108
|
+
{
|
|
109
|
+
"session_id": session_id,
|
|
110
|
+
"title": v.get("title", ""),
|
|
111
|
+
"workspace_folder": workspace_folder,
|
|
112
|
+
"workspace_hash": ws_dir.name,
|
|
113
|
+
"created_ms": v.get("timing", {}).get("created"),
|
|
114
|
+
"last_message_ms": v.get("lastMessageDate"),
|
|
115
|
+
"has_debug_logs": debug_log_dir.exists(),
|
|
116
|
+
"debug_log_dir": str(debug_log_dir),
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if use_cache:
|
|
121
|
+
_WS_DB_CACHE[cache_key] = sessions
|
|
122
|
+
return sessions
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def find_session_dir_by_id(session_id: str, ws_roots: list[Path]) -> Path | None:
|
|
126
|
+
"""Search ws_roots for a debug log directory matching session_id."""
|
|
127
|
+
for ws_dir in ws_roots:
|
|
128
|
+
if not ws_dir.is_dir():
|
|
129
|
+
continue
|
|
130
|
+
for candidate in ws_dir.iterdir():
|
|
131
|
+
if not candidate.is_dir():
|
|
132
|
+
continue
|
|
133
|
+
log_dir = candidate / "GitHub.copilot-chat" / "debug-logs" / session_id
|
|
134
|
+
if log_dir.exists():
|
|
135
|
+
return log_dir
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def find_sessions_by_title(title: str, ws_roots: list[Path]) -> list[dict]:
|
|
140
|
+
"""Search ws_roots for sessions whose title contains the given string."""
|
|
141
|
+
lower = title.lower()
|
|
142
|
+
matches: list[dict] = []
|
|
143
|
+
for ws_dir in ws_roots:
|
|
144
|
+
if not ws_dir.is_dir():
|
|
145
|
+
continue
|
|
146
|
+
for ws_sub in ws_dir.iterdir():
|
|
147
|
+
if not ws_sub.is_dir():
|
|
148
|
+
continue
|
|
149
|
+
for session in get_sessions_from_workspace(ws_sub, use_cache=True):
|
|
150
|
+
if lower in session.get("title", "").lower():
|
|
151
|
+
matches.append(session)
|
|
152
|
+
return sorted(matches, key=lambda s: s.get("created_ms") or 0, reverse=True)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def find_latest_session_dir(
|
|
156
|
+
ws_roots: list[Path], workspace_filter: str | None = None
|
|
157
|
+
) -> Path | None:
|
|
158
|
+
"""Return the most recently modified debug log directory across ws_roots."""
|
|
159
|
+
latest: Path | None = None
|
|
160
|
+
latest_mtime = 0.0
|
|
161
|
+
for ws_dir in ws_roots:
|
|
162
|
+
if not ws_dir.is_dir():
|
|
163
|
+
continue
|
|
164
|
+
for ws_sub in ws_dir.iterdir():
|
|
165
|
+
if not ws_sub.is_dir():
|
|
166
|
+
continue
|
|
167
|
+
if workspace_filter:
|
|
168
|
+
folder = _get_workspace_folder(ws_sub)
|
|
169
|
+
if workspace_filter not in folder:
|
|
170
|
+
continue
|
|
171
|
+
debug_logs_base = ws_sub / "GitHub.copilot-chat" / "debug-logs"
|
|
172
|
+
if not debug_logs_base.exists():
|
|
173
|
+
continue
|
|
174
|
+
for session_dir in debug_logs_base.iterdir():
|
|
175
|
+
if not session_dir.is_dir():
|
|
176
|
+
continue
|
|
177
|
+
mtime = session_dir.stat().st_mtime
|
|
178
|
+
if mtime > latest_mtime:
|
|
179
|
+
latest_mtime = mtime
|
|
180
|
+
latest = session_dir
|
|
181
|
+
return latest
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def list_recent_sessions(
|
|
185
|
+
ws_roots: list[Path],
|
|
186
|
+
limit: int = 20,
|
|
187
|
+
since_ms: int | None = None,
|
|
188
|
+
workspace_filter: str | None = None,
|
|
189
|
+
require_logs: bool = False,
|
|
190
|
+
) -> list[dict]:
|
|
191
|
+
"""List sessions from ws_roots, sorted most-recent first.
|
|
192
|
+
|
|
193
|
+
When ``require_logs`` is True, only sessions with existing debug logs
|
|
194
|
+
are returned (useful for batch analysis).
|
|
195
|
+
"""
|
|
196
|
+
from copilot_session_usage._internal import core
|
|
197
|
+
|
|
198
|
+
all_sessions: list[dict] = []
|
|
199
|
+
for ws_dir in ws_roots:
|
|
200
|
+
if not ws_dir.is_dir():
|
|
201
|
+
continue
|
|
202
|
+
for ws_sub in ws_dir.iterdir():
|
|
203
|
+
if not ws_sub.is_dir():
|
|
204
|
+
continue
|
|
205
|
+
for session in get_sessions_from_workspace(ws_sub, use_cache=True):
|
|
206
|
+
if since_ms and (session.get("created_ms") or 0) < since_ms:
|
|
207
|
+
continue
|
|
208
|
+
if workspace_filter and workspace_filter not in session.get("workspace_folder", ""):
|
|
209
|
+
continue
|
|
210
|
+
if require_logs and not session.get("has_debug_logs"):
|
|
211
|
+
continue
|
|
212
|
+
session["created_at"] = core.ts_to_iso(session.get("created_ms"))
|
|
213
|
+
session["last_activity_at"] = core.ts_to_iso(session.get("last_message_ms"))
|
|
214
|
+
all_sessions.append(session)
|
|
215
|
+
all_sessions.sort(key=lambda s: s.get("created_ms") or 0, reverse=True)
|
|
216
|
+
return all_sessions[:limit]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def resolve_ws_roots(workspace_storage: str | None) -> list[Path]:
|
|
220
|
+
"""Resolve workspaceStorage roots from an explicit override or auto-detection."""
|
|
221
|
+
if workspace_storage:
|
|
222
|
+
root = Path(workspace_storage)
|
|
223
|
+
if not root.is_dir():
|
|
224
|
+
msg = f"--workspace-storage path not found: {root}"
|
|
225
|
+
raise click.ClickException(msg)
|
|
226
|
+
return [root]
|
|
227
|
+
roots = default_workspace_storage_roots()
|
|
228
|
+
if not roots:
|
|
229
|
+
msg = (
|
|
230
|
+
"No workspaceStorage directory found for this platform.\n"
|
|
231
|
+
"Pass --workspace-storage PATH to specify the location manually.\n"
|
|
232
|
+
"Common paths:\n"
|
|
233
|
+
" macOS: ~/Library/Application Support/Code/User/workspaceStorage\n"
|
|
234
|
+
" Linux: ~/.config/Code/User/workspaceStorage\n"
|
|
235
|
+
" Windows: %APPDATA%\\Code\\User\\workspaceStorage\n"
|
|
236
|
+
" WSL2: /mnt/c/Users/<you>/AppData/Roaming/Code/User/workspaceStorage"
|
|
237
|
+
)
|
|
238
|
+
raise click.ClickException(msg)
|
|
239
|
+
return roots
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
import click # noqa: E402
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Public Python API for copilot-session-usage.
|
|
2
|
+
|
|
3
|
+
All functions accept an optional ``agent`` parameter for future routing
|
|
4
|
+
between VS Code and Copilot-CLI providers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from copilot_session_usage._internal import core, vscode
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def analyze_session(path: Path, detail: str = "compact", agent: str = "vscode") -> dict:
|
|
15
|
+
"""Analyze one session by its debug-log directory path.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
path: Path to the session's debug-log directory.
|
|
19
|
+
detail: ``minimal``, ``compact`` (default), or ``full``.
|
|
20
|
+
agent: Provider to use (``vscode`` or ``cli``).
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Session analysis dict shaped to the requested detail level.
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
NotImplementedError: If ``agent`` is ``"cli"``.
|
|
27
|
+
"""
|
|
28
|
+
if agent == "cli":
|
|
29
|
+
raise NotImplementedError("Copilot-CLI support is not yet implemented.")
|
|
30
|
+
pricing = core.load_pricing()
|
|
31
|
+
result = core.analyze_session(Path(path), pricing)
|
|
32
|
+
return core.shape_session(result, detail)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def list_sessions(
|
|
36
|
+
workspace_roots: list[Path] | None = None,
|
|
37
|
+
limit: int = 20,
|
|
38
|
+
since: str | None = None,
|
|
39
|
+
workspace_filter: str | None = None,
|
|
40
|
+
agent: str = "vscode",
|
|
41
|
+
) -> list[dict]:
|
|
42
|
+
"""List recent sessions (metadata only, no JSONL reads).
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
workspace_roots: Override workspaceStorage directories.
|
|
46
|
+
Auto-detected if None.
|
|
47
|
+
limit: Maximum sessions to return.
|
|
48
|
+
since: Only sessions created after this date (YYYY-MM-DD or ISO 8601).
|
|
49
|
+
workspace_filter: Only sessions from this workspace folder.
|
|
50
|
+
agent: Provider to use (``vscode`` or ``cli``).
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of session metadata dicts, most-recent first.
|
|
54
|
+
"""
|
|
55
|
+
if agent == "cli":
|
|
56
|
+
raise NotImplementedError("Copilot-CLI support is not yet implemented.")
|
|
57
|
+
|
|
58
|
+
if workspace_roots is None:
|
|
59
|
+
workspace_roots = vscode.default_workspace_storage_roots()
|
|
60
|
+
|
|
61
|
+
since_ms = core.parse_since_to_ms(since) if since else None
|
|
62
|
+
return vscode.list_recent_sessions(
|
|
63
|
+
workspace_roots,
|
|
64
|
+
limit=limit,
|
|
65
|
+
since_ms=since_ms,
|
|
66
|
+
workspace_filter=workspace_filter,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def find_sessions_by_title(
|
|
71
|
+
title: str,
|
|
72
|
+
workspace_roots: list[Path] | None = None,
|
|
73
|
+
agent: str = "vscode",
|
|
74
|
+
) -> list[dict]:
|
|
75
|
+
"""Fuzzy-match sessions by title substring.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
title: Substring to search for (case-insensitive).
|
|
79
|
+
workspace_roots: Override workspaceStorage directories.
|
|
80
|
+
agent: Provider to use (``vscode`` or ``cli``).
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Matching session metadata dicts, most-recent first.
|
|
84
|
+
"""
|
|
85
|
+
if agent == "cli":
|
|
86
|
+
raise NotImplementedError("Copilot-CLI support is not yet implemented.")
|
|
87
|
+
|
|
88
|
+
if workspace_roots is None:
|
|
89
|
+
workspace_roots = vscode.default_workspace_storage_roots()
|
|
90
|
+
|
|
91
|
+
return vscode.find_sessions_by_title(title, workspace_roots)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def find_session_by_id(
|
|
95
|
+
session_id: str, workspace_roots: list[Path] | None = None, agent: str = "vscode"
|
|
96
|
+
) -> dict | None:
|
|
97
|
+
"""Analyze a session by its exact UUID.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
session_id: The session UUID.
|
|
101
|
+
workspace_roots: Override workspaceStorage directories.
|
|
102
|
+
agent: Provider to use (``vscode`` or ``cli``).
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Session analysis dict, or None if not found.
|
|
106
|
+
"""
|
|
107
|
+
if agent == "cli":
|
|
108
|
+
raise NotImplementedError("Copilot-CLI support is not yet implemented.")
|
|
109
|
+
|
|
110
|
+
if workspace_roots is None:
|
|
111
|
+
workspace_roots = vscode.default_workspace_storage_roots()
|
|
112
|
+
|
|
113
|
+
session_dir = vscode.find_session_dir_by_id(session_id, workspace_roots)
|
|
114
|
+
if session_dir is None:
|
|
115
|
+
return None
|
|
116
|
+
pricing = core.load_pricing()
|
|
117
|
+
return core.analyze_session(session_dir, pricing)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def analyze_latest(
|
|
121
|
+
workspace_roots: list[Path] | None = None,
|
|
122
|
+
detail: str = "compact",
|
|
123
|
+
workspace_filter: str | None = None,
|
|
124
|
+
agent: str = "vscode",
|
|
125
|
+
) -> dict:
|
|
126
|
+
"""Analyze the most recently modified session.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
workspace_roots: Override workspaceStorage directories.
|
|
130
|
+
detail: ``minimal``, ``compact`` (default), or ``full``.
|
|
131
|
+
workspace_filter: Only sessions from this workspace folder.
|
|
132
|
+
agent: Provider to use (``vscode`` or ``cli``).
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Session analysis dict shaped to the requested detail level.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
ValueError: If no sessions are found.
|
|
139
|
+
"""
|
|
140
|
+
if agent == "cli":
|
|
141
|
+
raise NotImplementedError("Copilot-CLI support is not yet implemented.")
|
|
142
|
+
|
|
143
|
+
if workspace_roots is None:
|
|
144
|
+
workspace_roots = vscode.default_workspace_storage_roots()
|
|
145
|
+
|
|
146
|
+
session_dir = vscode.find_latest_session_dir(workspace_roots, workspace_filter=workspace_filter)
|
|
147
|
+
if session_dir is None:
|
|
148
|
+
raise ValueError("No session debug logs found in workspace storage.")
|
|
149
|
+
pricing = core.load_pricing()
|
|
150
|
+
result = core.analyze_session(session_dir, pricing)
|
|
151
|
+
return core.shape_session(result, detail)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def batch_analyze(
|
|
155
|
+
n: int,
|
|
156
|
+
workspace_roots: list[Path] | None = None,
|
|
157
|
+
detail: str = "compact",
|
|
158
|
+
since: str | None = None,
|
|
159
|
+
workspace_filter: str | None = None,
|
|
160
|
+
agent: str = "vscode",
|
|
161
|
+
) -> dict:
|
|
162
|
+
"""Analyze the N most recent sessions.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
n: Number of sessions to analyze.
|
|
166
|
+
workspace_roots: Override workspaceStorage directories.
|
|
167
|
+
detail: ``minimal``, ``compact`` (default), or ``full``.
|
|
168
|
+
since: Only sessions created after this date.
|
|
169
|
+
workspace_filter: Only sessions from this workspace folder.
|
|
170
|
+
agent: Provider to use (``vscode`` or ``cli``).
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Dict with ``summary`` (aggregate) and ``sessions`` (per-session list).
|
|
174
|
+
"""
|
|
175
|
+
if agent == "cli":
|
|
176
|
+
raise NotImplementedError("Copilot-CLI support is not yet implemented.")
|
|
177
|
+
|
|
178
|
+
if workspace_roots is None:
|
|
179
|
+
workspace_roots = vscode.default_workspace_storage_roots()
|
|
180
|
+
|
|
181
|
+
since_ms = core.parse_since_to_ms(since) if since else None
|
|
182
|
+
sessions = vscode.list_recent_sessions(
|
|
183
|
+
workspace_roots,
|
|
184
|
+
limit=n,
|
|
185
|
+
since_ms=since_ms,
|
|
186
|
+
workspace_filter=workspace_filter,
|
|
187
|
+
require_logs=True,
|
|
188
|
+
)
|
|
189
|
+
pricing = core.load_pricing()
|
|
190
|
+
results: list[dict] = []
|
|
191
|
+
for session in sessions:
|
|
192
|
+
session_dir = Path(session["debug_log_dir"])
|
|
193
|
+
if not session_dir.exists():
|
|
194
|
+
continue
|
|
195
|
+
result = core.analyze_session(session_dir, pricing)
|
|
196
|
+
result["title"] = session.get("title") or result.get("title")
|
|
197
|
+
results.append(result)
|
|
198
|
+
return core.shape_batch(results, detail)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def load_pricing(ref_dir: Path | None = None) -> dict:
|
|
202
|
+
"""Load pricing data.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
ref_dir: Directory containing pricing YAML files. If None, uses
|
|
206
|
+
the bundled data directory shipped with the package.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Pricing dict with model rates.
|
|
210
|
+
"""
|
|
211
|
+
return core.load_pricing(ref_dir)
|