Skip to content

Commit e30759f

Browse files
committed
Update CLI source options for OpenCode
1 parent 7eedfbd commit e30759f

3 files changed

Lines changed: 27 additions & 16 deletions

File tree

dataclaw/cli.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
import json
55
import re
66
import sys
7+
import urllib.error
78
import urllib.request
89
from datetime import datetime, timezone
910
from pathlib import Path
11+
from typing import Any, Mapping, cast
1012

1113
from .anonymizer import Anonymizer
1214
from .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
1416
from .secrets import _has_mixed_char_types, _shannon_entropy, redact_session
1517

1618
HF_TAG = "dataclaw"
@@ -56,8 +58,8 @@
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

6365
def _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

8791
def _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

125131
def _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

132138
def _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

140147
def _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)

dataclaw/config.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import json
44
import sys
55
from pathlib import Path
6-
from typing import TypedDict
6+
from typing import TypedDict, cast
77

88
CONFIG_DIR = Path.home() / ".dataclaw"
99
CONFIG_FILE = CONFIG_DIR / "config.json"
@@ -20,6 +20,10 @@ class DataClawConfig(TypedDict, total=False):
2020
last_export: dict
2121
stage: str | None # "auth" | "configure" | "review" | "confirmed" | "done"
2222
projects_confirmed: bool # True once user has addressed folder exclusions
23+
review_attestations: dict
24+
review_verification: dict
25+
last_confirm: dict
26+
publish_attestation: str
2327

2428

2529
DEFAULT_CONFIG: DataClawConfig = {
@@ -35,10 +39,10 @@ def load_config() -> DataClawConfig:
3539
try:
3640
with open(CONFIG_FILE) as f:
3741
stored = json.load(f)
38-
return {**DEFAULT_CONFIG, **stored}
42+
return cast(DataClawConfig, {**DEFAULT_CONFIG, **stored})
3943
except (json.JSONDecodeError, OSError) as e:
4044
print(f"Warning: could not read {CONFIG_FILE}: {e}", file=sys.stderr)
41-
return dict(DEFAULT_CONFIG)
45+
return cast(DataClawConfig, dict(DEFAULT_CONFIG))
4246

4347

4448
def save_config(config: DataClawConfig) -> None:

tests/test_cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ def test_no_projects(self, monkeypatch, capsys):
400400
monkeypatch.setattr("dataclaw.cli.discover_projects", lambda: [])
401401
list_projects()
402402
captured = capsys.readouterr()
403-
assert "No Claude Code or Codex sessions" in captured.out
403+
assert "No Claude Code, Codex, or OpenCode sessions" in captured.out
404404

405405
def test_source_filter_codex(self, monkeypatch, capsys):
406406
monkeypatch.setattr(
@@ -626,7 +626,7 @@ def test_export_requires_explicit_source_selection(self, monkeypatch, capsys):
626626
assert payload["error"] == "Source scope is not confirmed yet."
627627
assert payload["blocked_on_step"] == "Step 2/6"
628628
assert len(payload["process_steps"]) == 6
629-
assert payload["allowed_sources"] == ["both", "claude", "codex"]
629+
assert payload["allowed_sources"] == ["both", "claude", "codex", "opencode"]
630630
assert payload["next_command"] == "dataclaw config --source both"
631631

632632
def test_configure_next_steps_require_full_folder_presentation(self):

0 commit comments

Comments
 (0)