-
-
Notifications
You must be signed in to change notification settings - Fork 296
Add OpenCode support for usage monitoring #178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Implements support for monitoring Claude usage within OpenCode as requested in issue Maciek-roboblog#176. Changes: - Add opencode_reader.py to parse OpenCode's message storage format - Support data_source setting ('claude-code', 'opencode', 'auto') - Auto-detect OpenCode installation and merge usage data - Add tests for OpenCode reader functionality - Update analysis and aggregator to handle OpenCode entries OpenCode stores data in ~/.local/share/opencode/storage/ with: - message/{sessionID}/{msgID}.json for individual messages - Sessions tracked via sessionID in message metadata Closes Maciek-roboblog#176
📝 WalkthroughWalkthroughAdds multi-source data loading (Claude + OpenCode) and an OpenCode reader; introduces a new Changes
Sequence Diagram(s)sequenceDiagram
participant CLI as CLI (main.py)
participant Settings as Settings
participant Orch as MonitoringOrchestrator
participant Manager as DataManager
participant Analysis as analyze_usage()
participant Reader as load_usage_entries_unified()
participant Sources as Data Sources (Claude / OpenCode)
CLI->>Settings: load config (includes data_source)
Settings->>Settings: validate_data_source()
Settings-->>CLI: args.data_source
CLI->>Orch: init(data_source)
Orch->>Manager: init(data_source)
rect rgb(235,245,255)
Note over Manager,Analysis: Data retrieval & analysis
Manager->>Analysis: get_data() → analyze_usage(data_source)
Analysis->>Reader: load_usage_entries_unified(source=DataSource...)
Reader->>Sources: detect_available_sources() / load per-source
Sources-->>Reader: per-source entries
Reader-->>Analysis: (entries, raw_entries, detected_source)
Analysis-->>Manager: usage + metadata(data_source=detected_source)
end
Manager-->>Orch: aggregated results
Orch-->>CLI: monitoring output
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (5 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (7)
src/tests/test_analysis.py (1)
75-75: Consider restoring argument verification in test assertions.The original tests used
assert_called_once_with(...)to verify exact arguments passed toload_usage_entries. The updated tests useassert_called_once()without argument verification. While this is more flexible, it loses test coverage for ensuring the correctdata_path,hours_back,mode,include_raw, andsourceparameters are passed through.🔎 Suggested fix to restore argument verification
For example, in
test_analyze_usage_basic:- mock_load.assert_called_once() + mock_load.assert_called_once() + call_kwargs = mock_load.call_args[1] + assert call_kwargs['hours_back'] == 24 + assert call_kwargs['source'] == DataSource.AUTOOr use a more explicit assertion pattern to verify the critical parameters while allowing flexibility on others.
Also applies to: 96-96, 118-118
src/tests/test_opencode_reader.py (1)
27-45: Test comment suggests incomplete verification.The comment at lines 43-44 notes "This might still return False due to expanduser behavior" and "The actual test is more about the function structure." This suggests the test may not be effectively verifying the intended behavior.
🔎 Consider improving the test to actually verify detection
def test_detect_opencode_installation_exists(self, tmp_path: Path) -> None: """Test detection when OpenCode storage exists.""" message_dir = tmp_path / "message" message_dir.mkdir(parents=True) (message_dir / "ses_test").mkdir() with patch( "claude_monitor.data.opencode_reader.OPENCODE_STORAGE_PATH", str(tmp_path), ): - # Create a mock that returns the tmp_path with patch( - "claude_monitor.data.opencode_reader.Path.expanduser", - return_value=tmp_path, + "claude_monitor.data.opencode_reader.Path", ): + # Mock Path to handle expanduser correctly + mock_path = patch.object(Path, 'expanduser', return_value=tmp_path) + with mock_path: - result = detect_opencode_installation() - # Note: This might still return False due to expanduser behavior - # The actual test is more about the function structure + result = detect_opencode_installation() + assert result is True, "Should detect OpenCode when message dir exists"This ensures the test actually verifies the detection logic rather than just exercising the code path.
src/claude_monitor/core/settings.py (1)
26-42: Consider persisting data_source in last_used parameters.The
savemethod at lines 26-42 persists several settings tolast_used.json(theme, timezone, time_format, etc.) but does not include the newly addeddata_sourcefield. This means the data source preference won't be remembered across sessions like other settings.🔎 Suggested addition to persist data_source
params = { "theme": settings.theme, "timezone": settings.timezone, "time_format": settings.time_format, "refresh_rate": settings.refresh_rate, "reset_hour": settings.reset_hour, "view": settings.view, + "data_source": settings.data_source, "timestamp": datetime.now().isoformat(), }This would make the data source preference persistent across sessions, consistent with other configuration options.
src/claude_monitor/data/reader.py (1)
344-368: Solid detection logic with good error handling.The detection correctly handles permission errors and validates the presence of actual data files.
One minor observation: the Claude Code check uses
list(claude_path.rglob("*.jsonl"))which materializes the entire list just to check existence, while OpenCode uses the more efficientany(opencode_path.iterdir()). For consistency and performance on large directories, consider:🔎 Optional: Use generator for Claude Code detection
# Check for Claude Code installation claude_path = Path(CLAUDE_CODE_PATH).expanduser() - if claude_path.exists() and list(claude_path.rglob("*.jsonl")): + if claude_path.exists() and any(claude_path.rglob("*.jsonl")): logger.info("Detected Claude Code data source") available.append(DataSource.CLAUDE)src/claude_monitor/data/analysis.py (1)
55-62: Consider warning on unrecognizeddata_sourcevalues.The source map silently falls back to
AUTOfor unknown values. A typo like"claudee"would be accepted without feedback.🔎 Optional: Add warning for unknown values
source_map = { "auto": DataSource.AUTO, "all": DataSource.ALL, "claude": DataSource.CLAUDE, "opencode": DataSource.OPENCODE, } - source_enum = source_map.get(data_source.lower(), DataSource.AUTO) + data_source_lower = data_source.lower() + source_enum = source_map.get(data_source_lower) + if source_enum is None: + logger.warning(f"Unknown data_source '{data_source}', defaulting to 'auto'") + source_enum = DataSource.AUTOsrc/claude_monitor/data/opencode_reader.py (2)
21-23: Duplicated constant withreader.py.
OPENCODE_STORAGE_PATHis defined both here and inreader.py(line 32). If either needs to change, they could become inconsistent.🔎 Proposed fix: Single source of truth
Consider defining the constant in one place and importing it. Since
reader.pyalready imports from this module, define it here and import it inreader.py:In
reader.py:+from claude_monitor.data.opencode_reader import OPENCODE_STORAGE_PATH -OPENCODE_STORAGE_PATH = "~/.local/share/opencode/storage"
53-59: Unusedcutoff_timevariable.
cutoff_timeis assigned on line 55 but never used; onlycutoff_msis passed to_process_message_file. This is dead code.🔎 Proposed fix
- cutoff_time = None if hours_back: - cutoff_time = datetime.now(tz.utc) - timedelta(hours=hours_back) - # Convert to milliseconds timestamp for comparison - cutoff_ms = int(cutoff_time.timestamp() * 1000) + cutoff_dt = datetime.now(tz.utc) - timedelta(hours=hours_back) + cutoff_ms = int(cutoff_dt.timestamp() * 1000) else: cutoff_ms = None
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
src/claude_monitor/cli/main.pysrc/claude_monitor/core/settings.pysrc/claude_monitor/data/__init__.pysrc/claude_monitor/data/aggregator.pysrc/claude_monitor/data/analysis.pysrc/claude_monitor/data/opencode_reader.pysrc/claude_monitor/data/reader.pysrc/claude_monitor/monitoring/data_manager.pysrc/claude_monitor/monitoring/orchestrator.pysrc/tests/test_analysis.pysrc/tests/test_monitoring_orchestrator.pysrc/tests/test_opencode_reader.py
🧰 Additional context used
🧬 Code graph analysis (7)
src/claude_monitor/data/reader.py (2)
src/claude_monitor/core/models.py (2)
CostMode(11-16)UsageEntry(20-31)src/claude_monitor/data/opencode_reader.py (1)
load_opencode_entries(26-91)
src/claude_monitor/monitoring/orchestrator.py (1)
src/claude_monitor/monitoring/data_manager.py (1)
DataManager(13-150)
src/claude_monitor/data/__init__.py (1)
src/claude_monitor/data/reader.py (6)
DataSource(37-43)detect_available_sources(344-368)detect_data_source(371-389)get_data_source_info(490-514)load_usage_entries(46-98)load_usage_entries_unified(392-487)
src/tests/test_analysis.py (2)
src/claude_monitor/data/reader.py (1)
DataSource(37-43)src/claude_monitor/data/analysis.py (1)
analyze_usage(18-116)
src/claude_monitor/data/aggregator.py (1)
src/claude_monitor/data/reader.py (2)
DataSource(37-43)load_usage_entries_unified(392-487)
src/claude_monitor/data/opencode_reader.py (2)
src/claude_monitor/core/models.py (2)
CostMode(11-16)UsageEntry(20-31)src/claude_monitor/core/pricing.py (2)
PricingCalculator(14-230)calculate_cost(71-133)
src/claude_monitor/data/analysis.py (2)
src/claude_monitor/data/reader.py (2)
DataSource(37-43)load_usage_entries_unified(392-487)src/claude_monitor/core/models.py (1)
CostMode(11-16)
🔇 Additional comments (20)
src/claude_monitor/cli/main.py (1)
171-171: LGTM! Clean data_source propagation from CLI to components.The use of
getattr(args, "data_source", "auto")with a fallback default ensures backward compatibility if the attribute is missing, while passing the string value through to components that handle conversion to the DataSource enum.Also applies to: 394-394
src/claude_monitor/monitoring/data_manager.py (1)
16-38: LGTM! Clean integration of data_source parameter.The
data_sourceparameter is properly:
- Added to the constructor with a sensible default of
"auto"- Documented in the docstring
- Stored as an instance attribute
- Propagated to
analyze_usageat line 66The implementation follows the established pattern and delegates validation to downstream components.
src/tests/test_monitoring_orchestrator.py (1)
78-81: LGTM! Test expectations correctly updated for new data_source parameter.The tests properly verify that:
- Default initialization passes
data_source="auto"to DataManager- Custom parameters still respect the default data_source value
- The new parameter doesn't break existing initialization patterns
Also applies to: 94-96
src/claude_monitor/data/__init__.py (1)
1-24: LGTM! Clear public API with backward compatibility.The updated module provides:
- Clear documentation of supported data sources (Claude Code and OpenCode)
- Proper exports of both legacy (
load_usage_entries) and unified (load_usage_entries_unified) loaders for backward compatibility- Detection and introspection functions for data sources
- Well-defined
__all__for explicit public APIsrc/tests/test_opencode_reader.py (2)
114-143: LGTM! Comprehensive message processing test.The test properly verifies:
- Valid assistant messages are processed with correct token accounting
- Output and reasoning tokens are combined (550 = 500 + 50)
- Cache tokens are correctly extracted (read: 1000, write: 200)
- Model ID and message ID are preserved
- Raw data is returned when requested
263-314: LGTM! Realistic integration test with proper structure.The test creates a realistic directory structure mimicking OpenCode's storage layout and verifies:
- Multiple messages are loaded correctly
- Both entries and raw data are returned when
include_raw=True- Entries are sorted by timestamp
- Token accounting is correct across multiple messages
src/claude_monitor/data/aggregator.py (2)
95-114: LGTM! Clean data_source parameter addition.The
data_sourceparameter is properly added to the constructor with:
- Sensible default value
"auto"- Clear documentation
- Storage as instance attribute for later use
281-299: LGTM! Clean migration to unified data loading.The changes properly:
- Import the unified loader and DataSource enum
- Map string source values to DataSource enum with all valid options
- Use the unified loader with proper unpacking of the 3-tuple return
- Log the detected source for observability
The source_map correctly covers all valid values:
"auto","all","claude","opencode", matching the Settings validation.src/claude_monitor/core/settings.py (3)
112-115: LGTM! Well-defined data_source field with clear constraints.The field properly uses:
Literaltype to restrict to valid values at type-check timeFieldwith informative description- Sensible default
"auto"for automatic detection
206-218: LGTM! Consistent validator with helpful error message.The validator follows the established pattern used for
plan,view, andtheme, providing:
- Case-insensitive normalization
- Validation against allowed values
- Clear error message listing valid options
356-372: LGTM! data_source properly added to namespace conversion.The
data_sourcefield is correctly included in the namespace at line 372, ensuring compatibility with code that expects argparse-style arguments.src/claude_monitor/monitoring/orchestrator.py (1)
19-36: LGTM! Parameter plumbing is correct.The
data_sourceparameter is properly added to the signature, documented in the docstring, and correctly passed through toDataManager. The default value"auto"aligns with the unified loader's behavior.src/claude_monitor/data/reader.py (3)
37-44: Well-designed enum with clear semantics.The
DataSourceenum provides a clean abstraction for source selection. The documentation clarifies thatAUTOandALLhave the same behavior, which is helpful for API users.
371-389: Verify the source preference order.The function prefers OpenCode over Claude Code when both are available. This seems intentional per the docstring, but please confirm this is the desired behavior. Users with both installations might expect Claude Code to take precedence since it's the original/primary integration.
417-430: Consider cross-source deduplication.When combining data from both Claude Code and OpenCode, there's no deduplication between sources. If the same Claude interaction is logged in both places (e.g., user has both tools tracking the same API key), entries could be counted twice.
Since the message ID and request ID formats likely differ between sources, existing deduplication logic within each source won't catch cross-source duplicates.
Is this a realistic scenario, or are Claude Code and OpenCode inherently tracking different interactions (e.g., CLI vs editor usage)?
src/claude_monitor/data/analysis.py (1)
65-76: LGTM! Good observability with detected source tracking.The unified loader integration correctly unpacks the 3-tuple return value and propagates the
detected_sourceto both logs and metadata. This provides good visibility into which data source was actually used.Also applies to: 101-112
src/claude_monitor/data/opencode_reader.py (4)
94-125: LGTM!Clean implementation for loading raw entries. Good filtering for messages with token data and appropriate error handling.
128-137: LGTM!The file pattern
msg_*.jsonmatches the documented OpenCode storage format.
212-224: Verify OpenCode model ID format compatibility with pricing calculator.The
modelIDfrom OpenCode is passed directly toPricingCalculator.calculate_cost. Ensure OpenCode's model naming convention (e.g.,claude-3-5-sonnet-20241022) matches what the pricing calculator expects, or that the calculator's fallback logic handles unknown formats gracefully.Based on the relevant code snippet,
PricingCalculator._get_pricing_for_modelhas fallback logic for "opus", "haiku", and defaults to "sonnet" pricing, which should handle most cases. Verify with sample OpenCode data that model IDs are recognized correctly.
245-262: LGTM!Simple and correct utility functions. The existence check is appropriate for installation detection.
| # Load from OpenCode if available | ||
| if DataSource.OPENCODE in available: | ||
| entries, raw_data = load_opencode_entries( | ||
| data_path=None, # Use default OpenCode path | ||
| hours_back=hours_back, | ||
| mode=mode, | ||
| include_raw=include_raw, | ||
| ) | ||
| all_entries.extend(entries) | ||
| if include_raw and raw_data and all_raw is not None: | ||
| all_raw.extend(raw_data) | ||
| logger.info(f"Loaded {len(entries)} entries from OpenCode") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent data_path handling for OpenCode in AUTO mode.
When source is AUTO or ALL, the OpenCode loader ignores the provided data_path (line 448 uses data_path=None), while Claude Code uses it. However, when source is explicitly OPENCODE (lines 460-468), data_path is passed through.
This inconsistency could confuse users who provide a custom path expecting it to apply to all sources in AUTO mode.
🔎 Proposed fix for consistent data_path handling
If the intent is to use the default OpenCode path in AUTO mode, this should be documented. Otherwise, consider passing data_path to OpenCode as well, or adding a separate opencode_path parameter:
# Load from OpenCode if available
if DataSource.OPENCODE in available:
entries, raw_data = load_opencode_entries(
- data_path=None, # Use default OpenCode path
+ data_path=None, # OpenCode uses its own default path in AUTO mode
hours_back=hours_back,
mode=mode,
include_raw=include_raw,
)Alternatively, if custom path should apply to OpenCode too when both paths are the same base:
# Consider if data_path should be honored for OpenCode in AUTO mode🤖 Prompt for AI Agents
In src/claude_monitor/data/reader.py around lines 445 to 456, the OpenCode
loader call in the AUTO/ALL branch hardcodes data_path=None causing inconsistent
behavior versus the explicit OPENCODE branch; change the call to pass the
existing data_path variable (so custom paths provided to the overall loader are
honored), while keeping the None behavior when data_path is actually None (i.e.,
replace data_path=None with data_path=data_path), and ensure any downstream
assumptions about None/default path remain valid.
- Fix deduplication issue: use file path as fallback ID when message ID is missing - Document data_path behavior: clarify OpenCode uses its own default path in AUTO mode - Handle AUTO/ALL in get_data_source_info: return proper info for auto-detected sources 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (3)
src/claude_monitor/data/reader.py (2)
344-368: Consider a more efficient existence check for Claude Code files.Line 354 materializes the entire list from
rglob("*.jsonl")just to check if any files exist. For large directories, this could be inefficient. The OpenCode check (line 362) already uses the more efficientany()pattern.🔎 Proposed refactor for efficiency
# Check for Claude Code installation claude_path = Path(CLAUDE_CODE_PATH).expanduser() - if claude_path.exists() and list(claude_path.rglob("*.jsonl")): + if claude_path.exists() and any(claude_path.rglob("*.jsonl")): logger.info("Detected Claude Code data source") available.append(DataSource.CLAUDE)
422-427: Clarify primary_source when no sources are available.When no sources are detected in AUTO/ALL mode, the function returns
DataSource.CLAUDEas the primary_source, even though no actual Claude Code data exists. This could mislead callers about what data was actually loaded.🔎 Consider returning a more accurate indicator
Option 1: Return AUTO to indicate no specific source was used:
if not available: logger.warning("No data sources available") - return [], None, DataSource.CLAUDE + return [], None, DataSource.AUTOOption 2: Add a NONE variant to the enum (more explicit but requires more changes):
class DataSource(Enum): AUTO = "auto" ALL = "all" CLAUDE = "claude" OPENCODE = "opencode" NONE = "none" # No sources availablesrc/claude_monitor/data/opencode_reader.py (1)
176-182: Consider using a shorter fallback ID.The fallback to
str(file_path)addresses the previous deduplication concern, but stores the full absolute path as an ID. For cleaner logging and slightly better memory efficiency, consider using just the filename or a relative path.🔎 Proposed alternatives
Option 1: Use just the filename (assuming filenames are unique):
msg_id = data.get("id", "") if not msg_id: # Generate a unique ID from file path if missing - msg_id = str(file_path) + msg_id = file_path.nameOption 2: Use path relative to message directory:
msg_id = data.get("id", "") if not msg_id: # Generate a unique ID from file path if missing - msg_id = str(file_path) + try: + msg_id = str(file_path.relative_to(message_path)) + except ValueError: + msg_id = str(file_path)
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/claude_monitor/data/opencode_reader.pysrc/claude_monitor/data/reader.py
🧰 Additional context used
🧬 Code graph analysis (2)
src/claude_monitor/data/opencode_reader.py (2)
src/claude_monitor/core/models.py (2)
CostMode(11-16)UsageEntry(20-31)src/claude_monitor/core/pricing.py (2)
PricingCalculator(14-230)calculate_cost(71-133)
src/claude_monitor/data/reader.py (3)
src/claude_monitor/core/settings.py (1)
exists(82-84)src/claude_monitor/core/models.py (2)
CostMode(11-16)UsageEntry(20-31)src/claude_monitor/data/opencode_reader.py (1)
load_opencode_entries(26-91)
🔇 Additional comments (5)
src/claude_monitor/data/reader.py (2)
371-389: LGTM - OpenCode preference is intentional.The function prefers OpenCode over Claude Code when both are available. This is clearly documented and appears intentional for backward compatibility with the legacy single-source API.
492-524: LGTM - AUTO/ALL handling implemented correctly.The function now properly handles
DataSource.AUTOandDataSource.ALLby detecting available sources and returning appropriate metadata. This addresses the previous review feedback.src/claude_monitor/data/opencode_reader.py (3)
26-91: LGTM - Well-structured entry point.The function properly handles path validation, time filtering, file discovery, deduplication, and result sorting. Error cases are logged appropriately.
218-227: LGTM - Reasoning tokens correctly included in output costs.The implementation correctly includes reasoning tokens in output token costs (line 224), which aligns with how these tokens are billed. This is consistent with the UsageEntry creation (line 235) and explicitly documented.
94-265: LGTM - Utility functions are well-implemented.The raw entry loader, file discovery, and installation detection utilities are all straightforward and correct. The use of
rglob("msg_*.json")appropriately filters to OpenCode message files.
- Remove unused cutoff_time variable in opencode_reader.py - Import OPENCODE_STORAGE_PATH from opencode_reader.py to avoid duplication - Use any() instead of list() for more efficient Claude Code detection - Add warning for unrecognized data_source values in analysis.py - Persist data_source preference in last_used settings - Improve test verification in test_opencode_reader.py - Restore argument verification in test_analysis.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (3)
src/claude_monitor/data/reader.py (2)
445-458: Documented: OpenCode uses default path in AUTO mode.The comment at lines 446-447 now documents that OpenCode uses its own default path in AUTO mode, addressing the previous review concern about inconsistent
data_pathhandling.
492-524: LGTM! get_data_source_info properly handles all source types.The function now correctly handles
AUTOandALLsources by detecting available sources, addressing the previous review concern.src/claude_monitor/data/opencode_reader.py (1)
174-180: LGTM! Deduplication handles missing IDs properly.The fix from the previous review is in place - using the file path as a fallback ID when the message lacks an
"id"field, preventing false duplicate detection.
🧹 Nitpick comments (2)
src/tests/test_settings.py (1)
246-266: Consider adding validation tests for data_source.The
TestSettingsclass has validation tests forplan,theme,timezone,time_format, andlog_level, but lacks explicit tests for the newdata_sourcevalidator. Consider adding tests liketest_data_source_validator_valid_valuesandtest_data_source_validator_invalid_valuefor consistency.src/claude_monitor/data/opencode_reader.py (1)
138-145: Unusedmodeparameter in _process_message_file.The
mode: CostModeparameter is passed to_process_message_filebut never used. ThePricingCalculator.calculate_costis called without the mode. Consider either removing the parameter or utilizing it if different cost calculation modes should be supported.🔎 Proposed fix
If the mode is not needed:
def _process_message_file( file_path: Path, - mode: CostMode, cutoff_ms: Optional[int], processed_ids: Set[str], include_raw: bool, pricing_calculator: PricingCalculator, ) -> Tuple[Optional[UsageEntry], Optional[Dict[str, Any]]]:And update the caller:
entry, raw_data = _process_message_file( file_path, - mode, cutoff_ms, processed_ids, include_raw, pricing_calculator, )
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
src/claude_monitor/core/settings.pysrc/claude_monitor/data/analysis.pysrc/claude_monitor/data/opencode_reader.pysrc/claude_monitor/data/reader.pysrc/tests/test_analysis.pysrc/tests/test_opencode_reader.pysrc/tests/test_settings.py
🚧 Files skipped from review as they are similar to previous changes (1)
- src/tests/test_analysis.py
🧰 Additional context used
🧬 Code graph analysis (4)
src/tests/test_opencode_reader.py (3)
src/claude_monitor/data/opencode_reader.py (6)
_find_message_files(126-135)_process_message_file(138-243)detect_opencode_installation(246-254)get_opencode_storage_path(257-263)load_opencode_entries(26-89)load_opencode_raw_entries(92-123)src/claude_monitor/core/pricing.py (1)
PricingCalculator(14-230)src/claude_monitor/data/reader.py (2)
DataSource(37-43)get_data_source_info(492-524)
src/claude_monitor/data/reader.py (3)
src/claude_monitor/core/settings.py (1)
exists(83-85)src/claude_monitor/core/models.py (2)
CostMode(11-16)UsageEntry(20-31)src/claude_monitor/data/opencode_reader.py (1)
load_opencode_entries(26-89)
src/claude_monitor/data/opencode_reader.py (2)
src/claude_monitor/core/models.py (2)
CostMode(11-16)UsageEntry(20-31)src/claude_monitor/core/pricing.py (2)
PricingCalculator(14-230)calculate_cost(71-133)
src/claude_monitor/data/analysis.py (2)
src/claude_monitor/data/reader.py (2)
DataSource(37-43)load_usage_entries_unified(392-489)src/claude_monitor/core/models.py (1)
CostMode(11-16)
🔇 Additional comments (18)
src/claude_monitor/core/settings.py (3)
113-116: LGTM! Data source field properly defined.The new
data_sourcefield follows the established pattern with a sensible default of"auto"for backward compatibility, proper typing, and clear description.
207-219: LGTM! Validator follows established patterns.The
validate_data_sourcevalidator is consistent with other validators in this file (plan, view, theme), properly normalizing to lowercase and providing clear error messages.
37-37: LGTM! Proper persistence and propagation of data_source.The
data_sourceis correctly saved inLastUsedParams.save()and propagated viato_namespace(), ensuring the setting persists across sessions and is available to downstream components.Also applies to: 373-373
src/tests/test_settings.py (1)
58-80: LGTM! Test properly verifies data_source persistence.The test correctly adds
data_sourceto the mock settings and verifies it's saved to the JSON file.src/tests/test_opencode_reader.py (4)
1-22: LGTM! Well-structured test file with comprehensive imports.The test file properly imports all necessary components from the opencode_reader module and includes appropriate testing utilities.
87-107: Good test fixture with realistic sample data.The
sample_message_datafixture provides a comprehensive example of OpenCode message format including all relevant fields (tokens, cache data, timing, model info).
246-309: LGTM! Load function tests cover key scenarios.Tests properly cover nonexistent paths, realistic directory structures, timestamp ordering, and raw data inclusion. The test at lines 258-309 creates a realistic directory structure and verifies proper loading behavior.
344-369: LGTM! DataSource integration tests validate enum and info functions.These tests verify the DataSource enum values and the
get_data_source_infofunction for both OpenCode and Claude sources.src/claude_monitor/data/analysis.py (3)
55-66: LGTM! Clean data source mapping with proper fallback.The source mapping handles all valid values and includes a helpful warning for unknown values, defaulting to
AUTOgracefully.
69-80: LGTM! Proper integration with unified loader.The function correctly unpacks the three-value return from
load_usage_entries_unifiedand logs useful information including the detected source and entry count.
105-116: LGTM! Metadata properly records detected data source.The
detected_source.valueis correctly added to the metadata, providing transparency about which data source was used.src/claude_monitor/data/reader.py (3)
37-44: LGTM! Well-documented DataSource enum.The enum clearly documents each source type with inline comments explaining the behavior of AUTO vs ALL and the default paths for each source.
344-368: LGTM! Robust source detection with proper error handling.The
detect_available_sourcesfunction properly checks for Claude Code usingany()(efficient) and handles potential permission errors when checking OpenCode directories.
371-389: The function precedence is intentional and explicitly documented. The docstring clearly states "Prefers OpenCode if available, otherwise Claude Code," which reflects the deliberate design choice to prioritize the new OpenCode feature over the legacy Claude Code source for backward compatibility.src/claude_monitor/data/opencode_reader.py (4)
1-24: LGTM! Well-documented module with clear purpose.The module docstring clearly explains the OpenCode storage structure and the constants are appropriately defined.
26-89: LGTM! Main entry point is well-structured.The
load_opencode_entriesfunction properly handles path resolution, time filtering, deduplication, and sorting. Good use of early returns for missing paths.
216-225: Good documentation for token mapping.The comment clearly explains the mapping between OpenCode's
cache.write/cache.readand the internalcache_creation_tokens/cache_read_tokensterminology.
246-263: LGTM! Utility functions are clean and focused.Both
detect_opencode_installationandget_opencode_storage_pathare simple, well-documented utilities that properly handle path expansion.
Summary
data_sourcesetting to configure which sources to use ('claude-code', 'opencode', 'auto')Implementation Details
OpenCode stores data in
~/.local/share/opencode/storage/with:message/{sessionID}/{msgID}.jsonfor individual messages with token datasessionIDin message metadataNew files:
opencode_reader.py- Parser for OpenCode's message storage formattest_opencode_reader.py- Comprehensive test coverageTest Plan
Closes #176
Summary by CodeRabbit
New Features
Tests
✏️ Tip: You can customize this high-level summary in your review settings.