@@ -8,11 +8,12 @@ Usage: opencode-dcp-stats [--sessions N] [--min-messages M] [--json] [--verbose]
88
99import json
1010import argparse
11- from pathlib import Path
1211from datetime import datetime
1312from collections import defaultdict
1413from 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)
1718DCP_TOOLS = {
1819 "compress" , "prune" , "distill" ,
@@ -24,42 +25,9 @@ CACHE_READ_COST_PER_1K = 0.00030 # $0.30 per 1M tokens
2425INPUT_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
6533def 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
488451if __name__ == "__main__" :
489- main ()
452+ raise SystemExit ( main () )
0 commit comments