Skip to content

Commit fd233eb

Browse files
committed
refactor: migrate scripts to OpenCode session API
1 parent 8f57ea3 commit fd233eb

7 files changed

Lines changed: 593 additions & 292 deletions

File tree

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ Thumbs.db
2828
# OpenCode
2929
.opencode/
3030

31+
# Python cache
32+
__pycache__/
33+
*.py[cod]
34+
*$py.class
35+
3136
# Generated prompt files (from scripts/generate-prompts.ts)
3237
lib/prompts/**/*.generated.ts
3338

@@ -40,4 +45,4 @@ test-update.ts
4045
docs/
4146
SCHEMA_NOTES.md
4247

43-
repomix-output.xml
48+
repomix-output.xml

scripts/opencode-dcp-stats

Lines changed: 66 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ Usage: opencode-dcp-stats [--sessions N] [--min-messages M] [--json] [--verbose]
88

99
import json
1010
import argparse
11-
from pathlib import Path
1211
from datetime import datetime
1312
from collections import defaultdict
1413
from typing import Optional
1514

15+
from opencode_api import APIError, add_api_arguments, create_client_from_args, list_sessions_across_projects
16+
1617
# DCP tool names across versions (compress is canonical; others are legacy aliases)
1718
DCP_TOOLS = {
1819
"compress", "prune", "distill",
@@ -24,42 +25,9 @@ CACHE_READ_COST_PER_1K = 0.00030 # $0.30 per 1M tokens
2425
INPUT_COST_PER_1K = 0.003 # $3.00 per 1M tokens
2526

2627

27-
def get_session_messages(storage: Path, session_id: str) -> list[dict]:
28-
"""Get all messages for a session, sorted by creation order."""
29-
message_dir = storage / "message" / session_id
30-
if not message_dir.exists():
31-
return []
32-
33-
messages = []
34-
for msg_file in message_dir.glob("*.json"):
35-
try:
36-
msg = json.loads(msg_file.read_text())
37-
msg["_file"] = msg_file
38-
msg["_id"] = msg_file.stem
39-
messages.append(msg)
40-
except (json.JSONDecodeError, IOError):
41-
pass
42-
43-
return sorted(messages, key=lambda m: m.get("_id", ""))
44-
45-
46-
def get_message_parts(storage: Path, message_id: str) -> list[dict]:
47-
"""Get all parts for a message, sorted by creation order."""
48-
parts_dir = storage / "part" / message_id
49-
if not parts_dir.exists():
50-
return []
51-
52-
parts = []
53-
for part_file in parts_dir.glob("*.json"):
54-
try:
55-
part = json.loads(part_file.read_text())
56-
part["_file"] = part_file
57-
part["_id"] = part_file.stem
58-
parts.append(part)
59-
except (json.JSONDecodeError, IOError):
60-
pass
61-
62-
return sorted(parts, key=lambda p: p.get("_id", ""))
28+
def get_session_messages(client, session: dict) -> list[dict]:
29+
"""Get all messages for a session."""
30+
return client.get_session_messages(session["id"], directory=session.get("directory"))
6331

6432

6533
def is_ignored_message(message: dict, parts: list[dict]) -> bool:
@@ -83,22 +51,20 @@ def is_ignored_message(message: dict, parts: list[dict]) -> bool:
8351
return True
8452

8553

86-
def count_real_user_messages(storage: Path, session_id: str) -> int:
54+
def count_real_user_messages(messages: list[dict]) -> int:
8755
"""Count user messages that are not ignored (real user interactions)."""
88-
messages = get_session_messages(storage, session_id)
8956
count = 0
90-
57+
9158
for msg in messages:
59+
info = msg.get("info", {})
60+
parts = msg.get("parts", [])
9261
# Only count user role messages
93-
if msg.get("role") != "user":
62+
if info.get("role") != "user":
9463
continue
95-
96-
msg_id = msg.get("_id", "")
97-
parts = get_message_parts(storage, msg_id)
98-
64+
9965
if not is_ignored_message(msg, parts):
10066
count += 1
101-
67+
10268
return count
10369

10470

@@ -136,10 +102,9 @@ def calc_cache_hit_rate(tokens: dict) -> float:
136102
return (cache_read / total_context) * 100
137103

138104

139-
def analyze_session(storage: Path, session_id: str) -> dict:
105+
def analyze_session(messages: list[dict], session_id: str) -> dict:
140106
"""Analyze DCP impact for a single session."""
141-
messages = get_session_messages(storage, session_id)
142-
107+
143108
result = {
144109
"session_id": session_id,
145110
"dcp_events": [],
@@ -164,14 +129,15 @@ def analyze_session(storage: Path, session_id: str) -> dict:
164129
prev_step = None
165130
prev_dcp_tools = []
166131
steps_since_dcp = None # None = no DCP yet, 0 = just had DCP, 1+ = steps after
167-
132+
168133
for i, msg in enumerate(messages):
169-
msg_id = msg.get("_id", "")
170-
parts = get_message_parts(storage, msg_id)
171-
134+
msg_info = msg.get("info", {})
135+
msg_id = msg_info.get("id", "")
136+
parts = msg.get("parts", [])
137+
172138
step_finish = extract_step_finish(parts)
173139
dcp_tools = extract_dcp_tools(parts)
174-
140+
175141
if step_finish:
176142
result["total_steps"] += 1
177143
tokens = step_finish.get("tokens", {})
@@ -239,27 +205,24 @@ def analyze_session(storage: Path, session_id: str) -> dict:
239205
return result
240206

241207

242-
def analyze_sessions(num_sessions: int = 20, min_messages: int = 5, output_json: bool = False, verbose: bool = False, session_id: str = None):
208+
def analyze_sessions(
209+
client,
210+
num_sessions: int = 20,
211+
min_messages: int = 5,
212+
output_json: bool = False,
213+
verbose: bool = False,
214+
session_id: str = None,
215+
session_list_limit: int = 5000,
216+
):
243217
"""Analyze DCP impact across recent sessions."""
244-
storage = Path.home() / ".local/share/opencode/storage"
245-
message_dir = storage / "message"
246-
session_dir = storage / "session"
247-
248-
if not message_dir.exists():
249-
print("Error: OpenCode storage not found at", storage)
250-
return
251-
218+
252219
# Get sessions to analyze
253220
if session_id:
254221
# Analyze specific session
255-
session_path = message_dir / session_id
256-
if not session_path.exists():
257-
print(f"Error: Session {session_id} not found")
258-
return
259-
sessions = [session_path]
222+
sessions = [client.get_session(session_id)]
260223
else:
261-
sessions = sorted(message_dir.iterdir(), key=lambda x: x.stat().st_mtime, reverse=True)[:num_sessions]
262-
224+
sessions = list_sessions_across_projects(client, per_project_limit=session_list_limit)[:num_sessions]
225+
263226
all_results = []
264227
grand_totals = {
265228
"sessions_analyzed": 0,
@@ -283,44 +246,34 @@ def analyze_sessions(num_sessions: int = 20, min_messages: int = 5, output_json:
283246
"hit_rates_by_distance": defaultdict(list)
284247
}
285248

286-
for session_path in sessions:
287-
session_id = session_path.name
288-
249+
for session in sessions:
250+
session_id = session.get("id", "")
251+
messages = get_session_messages(client, session)
252+
289253
# Check minimum message count (excluding ignored messages)
290-
real_user_messages = count_real_user_messages(storage, session_id)
254+
real_user_messages = count_real_user_messages(messages)
291255
if real_user_messages < min_messages:
292256
grand_totals["sessions_skipped_short"] += 1
293257
continue
294-
295-
result = analyze_session(storage, session_id)
258+
259+
result = analyze_session(messages, session_id)
296260
result["user_messages"] = real_user_messages
297-
298-
# Get session metadata
299-
title = "Unknown"
300-
for s_dir in session_dir.iterdir():
301-
s_file = s_dir / f"{session_id}.json"
302-
if s_file.exists():
303-
try:
304-
sess = json.loads(s_file.read_text())
305-
title = sess.get("title", "Untitled")[:50]
306-
except (json.JSONDecodeError, IOError):
307-
pass
308-
break
309-
310-
result["title"] = title
311-
261+
262+
# Session metadata from API
263+
result["title"] = session.get("title", "Untitled")[:50]
264+
312265
if result["total_dcp_calls"] > 0:
313266
all_results.append(result)
314267
grand_totals["sessions_with_dcp"] += 1
315-
268+
316269
grand_totals["sessions_analyzed"] += 1
317270
grand_totals["total_dcp_calls"] += result["total_dcp_calls"]
318271
grand_totals["total_steps"] += result["total_steps"]
319-
272+
320273
for tool, stats in result["by_tool"].items():
321274
for key in stats:
322275
grand_totals["by_tool"][tool][key] += stats[key]
323-
276+
324277
# Aggregate hit rates by distance
325278
for dist, rates in result["hit_rates_by_distance"].items():
326279
grand_totals["hit_rates_by_distance"][dist].extend(rates)
@@ -474,16 +427,26 @@ def main():
474427
help="Output as JSON")
475428
parser.add_argument("--verbose", "-v", action="store_true",
476429
help="Show detailed per-event breakdown")
430+
add_api_arguments(parser)
477431
args = parser.parse_args()
478-
479-
analyze_sessions(
480-
num_sessions=args.sessions,
481-
min_messages=args.min_messages,
482-
output_json=args.json,
483-
verbose=args.verbose,
484-
session_id=args.session
485-
)
432+
433+
try:
434+
with create_client_from_args(args) as client:
435+
analyze_sessions(
436+
client,
437+
num_sessions=args.sessions,
438+
min_messages=args.min_messages,
439+
output_json=args.json,
440+
verbose=args.verbose,
441+
session_id=args.session,
442+
session_list_limit=args.session_list_limit,
443+
)
444+
except APIError as err:
445+
print(f"Error: {err}")
446+
return 1
447+
448+
return 0
486449

487450

488451
if __name__ == "__main__":
489-
main()
452+
raise SystemExit(main())

scripts/opencode-find-session

Lines changed: 31 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,35 @@
11
#!/usr/bin/env python3
22
"""
3-
Find OpenCode session IDs by title search.
3+
Find OpenCode session IDs by title search using the OpenCode API.
44
Returns matching session IDs ordered by last usage time.
55
66
Usage: opencode-find-session <search-term> [--exact] [--json]
77
"""
88

99
import json
1010
import argparse
11-
from pathlib import Path
1211
from datetime import datetime
1312

13+
from opencode_api import APIError, add_api_arguments, create_client_from_args, list_sessions_across_projects
1414

15-
def get_all_sessions(storage: Path) -> list[dict]:
16-
"""Get all sessions with their metadata."""
17-
session_dir = storage / "session"
18-
message_dir = storage / "message"
19-
20-
if not session_dir.exists():
21-
return []
22-
15+
16+
def get_all_sessions(client, session_list_limit: int) -> list[dict]:
17+
"""Get all sessions with normalized metadata from API."""
18+
api_sessions = list_sessions_across_projects(client, per_project_limit=session_list_limit)
2319
sessions = []
24-
25-
for app_dir in session_dir.iterdir():
26-
if not app_dir.is_dir():
27-
continue
28-
29-
for session_file in app_dir.glob("*.json"):
30-
try:
31-
session = json.loads(session_file.read_text())
32-
session_id = session_file.stem
33-
34-
# Get last modified time from message directory
35-
msg_path = message_dir / session_id
36-
if msg_path.exists():
37-
mtime = msg_path.stat().st_mtime
38-
else:
39-
mtime = session_file.stat().st_mtime
40-
41-
sessions.append({
42-
"id": session_id,
43-
"title": session.get("title", "Untitled"),
44-
"created_at": session.get("createdAt"),
45-
"last_used": mtime,
46-
"last_used_iso": datetime.fromtimestamp(mtime).isoformat()
47-
})
48-
except (json.JSONDecodeError, IOError):
49-
pass
50-
20+
for session in api_sessions:
21+
time_data = session.get("time", {})
22+
updated_ms = time_data.get("updated") or time_data.get("created") or 0
23+
last_used = updated_ms / 1000 if updated_ms else 0
24+
sessions.append(
25+
{
26+
"id": session.get("id", ""),
27+
"title": session.get("title", "Untitled"),
28+
"created_at": time_data.get("created"),
29+
"last_used": last_used,
30+
"last_used_iso": datetime.fromtimestamp(last_used).isoformat() if last_used else None,
31+
}
32+
)
5133
return sessions
5234

5335

@@ -101,6 +83,7 @@ def main():
10183
)
10284
parser.add_argument(
10385
"search_term",
86+
nargs="?",
10487
type=str,
10588
help="Text to search for in session titles"
10689
)
@@ -119,16 +102,19 @@ def main():
119102
action="store_true",
120103
help="Show all sessions (ignore search term)"
121104
)
105+
add_api_arguments(parser)
122106
args = parser.parse_args()
123-
124-
storage = Path.home() / ".local/share/opencode/storage"
125-
126-
if not storage.exists():
127-
print("Error: OpenCode storage not found at", storage)
107+
108+
if not args.all and not args.search_term:
109+
parser.error("search_term is required unless --all is used")
110+
111+
try:
112+
with create_client_from_args(args) as client:
113+
sessions = get_all_sessions(client, args.session_list_limit)
114+
except APIError as err:
115+
print(f"Error: {err}")
128116
return 1
129-
130-
sessions = get_all_sessions(storage)
131-
117+
132118
if args.all:
133119
results = sorted(sessions, key=lambda s: s["last_used"], reverse=True)
134120
else:

0 commit comments

Comments
 (0)