44import json
55import re
66import sys
7+ import urllib .error
78import urllib .request
89from datetime import datetime , timezone
910from pathlib import Path
11+ from typing import Any , Mapping , cast
1012
1113from .anonymizer import Anonymizer
1214from .config import CONFIG_FILE , DataClawConfig , load_config , save_config
13- from .parser import CLAUDE_DIR , CODEX_DIR , discover_projects , parse_project_sessions
15+ from .parser import CLAUDE_DIR , CODEX_DIR , OPENCODE_DIR , discover_projects , parse_project_sessions
1416from .secrets import _has_mixed_char_types , _shannon_entropy , redact_session
1517
1618HF_TAG = "dataclaw"
5658 "Step 6/6: After explicit user approval, publish: dataclaw export --publish-attestation \" User explicitly approved publishing to Hugging Face.\" " ,
5759]
5860
59- EXPLICIT_SOURCE_CHOICES = {"claude" , "codex" , "both" }
60- SOURCE_CHOICES = ["auto" , "claude" , "codex" , "both" ]
61+ EXPLICIT_SOURCE_CHOICES = {"claude" , "codex" , "opencode" , " both" }
62+ SOURCE_CHOICES = ["auto" , "claude" , "codex" , "opencode" , " both" ]
6163
6264
6365def _mask_secret (s : str ) -> str :
@@ -67,7 +69,7 @@ def _mask_secret(s: str) -> str:
6769 return f"{ s [:4 ]} ...{ s [- 4 :]} "
6870
6971
70- def _mask_config_for_display (config : dict ) -> dict :
72+ def _mask_config_for_display (config : Mapping [ str , Any ] ) -> dict [ str , Any ] :
7173 """Return a copy of config with redact_strings values masked."""
7274 out = dict (config )
7375 if out .get ("redact_strings" ):
@@ -81,7 +83,9 @@ def _source_label(source_filter: str) -> str:
8183 return "Claude Code"
8284 if source_filter == "codex" :
8385 return "Codex"
84- return "Claude Code or Codex"
86+ if source_filter == "opencode" :
87+ return "OpenCode"
88+ return "Claude Code, Codex, or OpenCode"
8589
8690
8791def _normalize_source_filter (source_filter : str ) -> str :
@@ -119,7 +123,9 @@ def _has_session_sources(source_filter: str = "auto") -> bool:
119123 return CLAUDE_DIR .exists ()
120124 if source_filter == "codex" :
121125 return CODEX_DIR .exists ()
122- return CLAUDE_DIR .exists () or CODEX_DIR .exists ()
126+ if source_filter == "opencode" :
127+ return OPENCODE_DIR .exists ()
128+ return CLAUDE_DIR .exists () or CODEX_DIR .exists () or OPENCODE_DIR .exists ()
123129
124130
125131def _filter_projects_by_source (projects : list [dict ], source_filter : str ) -> list [dict ]:
@@ -130,11 +136,12 @@ def _filter_projects_by_source(projects: list[dict], source_filter: str) -> list
130136
131137
132138def _format_size (size_bytes : int ) -> str :
139+ size = float (size_bytes )
133140 for unit in ("B" , "KB" , "MB" ):
134- if size_bytes < 1024 :
135- return f"{ size_bytes :.1f} { unit } " if unit != "B" else f"{ size_bytes } B"
136- size_bytes /= 1024
137- return f"{ size_bytes :.1f} GB"
141+ if size < 1024 :
142+ return f"{ size :.1f} { unit } " if unit != "B" else f"{ int ( size ) } B"
143+ size /= 1024
144+ return f"{ size :.1f} GB"
138145
139146
140147def _format_token_count (count : int ) -> str :
@@ -1111,7 +1118,7 @@ def prep(source_filter: str = "auto") -> None:
11111118 repo_id = default_repo_name (hf_user )
11121119
11131120 # Build contextual next_steps
1114- stage_config = dict (config )
1121+ stage_config = cast ( DataClawConfig , dict (config ) )
11151122 if source_explicit :
11161123 stage_config ["source" ] = resolved_source_choice
11171124 next_steps , next_command = _build_status_next_steps (stage , stage_config , hf_user , repo_id )
0 commit comments