Skip to content

Conversation

@gmackie
Copy link

@gmackie gmackie commented Dec 25, 2025

Summary

  • Adds support for monitoring Claude usage within OpenCode as requested in Support for Claude within OpenCode #176
  • Auto-detects OpenCode installation and merges usage data with Claude Code data
  • Adds new data_source setting to configure which sources to use ('claude-code', 'opencode', 'auto')

Implementation Details

OpenCode stores data in ~/.local/share/opencode/storage/ with:

  • message/{sessionID}/{msgID}.json for individual messages with token data
  • Sessions tracked via sessionID in message metadata

New files:

  • opencode_reader.py - Parser for OpenCode's message storage format
  • test_opencode_reader.py - Comprehensive test coverage

Test Plan

  • All 535 tests pass
  • 78% code coverage maintained
  • New OpenCode reader has 92.91% coverage

Closes #176

Summary by CodeRabbit

  • New Features

    • Support for both Claude Code and OpenCode usage sources with automatic detection and configurable selection (default: auto)
    • Unified data loading that can merge and analyze entries from multiple sources
    • Data-source selection surfaced in settings/CLI and honored across monitoring and aggregation, with clearer reporting of the detected source
  • Tests

    • Comprehensive tests for the OpenCode reader and multi-source integration

✏️ Tip: You can customize this high-level summary in your review settings.

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
@coderabbitai
Copy link

coderabbitai bot commented Dec 25, 2025

📝 Walkthrough

Walkthrough

Adds multi-source data loading (Claude + OpenCode) and an OpenCode reader; introduces a new data_source setting propagated from CLI/config through orchestrator, data manager, analysis, and aggregation; replaces single-source loaders with a unified loader that detects/merges sources and returns detected source info.

Changes

Cohort / File(s) Summary
Configuration Layer
src/claude_monitor/core/settings.py
Added data_source: Literal["auto","claude","opencode"], validator, propagate to CLI namespace, and persist in LastUsedParams.
CLI / Orchestration
src/claude_monitor/cli/main.py, src/claude_monitor/monitoring/orchestrator.py
Propagate data_source (default "auto") into MonitoringOrchestrator and onward to DataManager.
Monitoring Data Manager
src/claude_monitor/monitoring/data_manager.py
DataManager.__init__ gains data_source and forwards it to analyze_usage/get_data.
Data Loading Infrastructure
src/claude_monitor/data/reader.py, src/claude_monitor/data/__init__.py
New DataSource enum, detection helpers, get_data_source_info, and load_usage_entries_unified(...) returning (entries, raw, detected_source); package exports updated.
OpenCode Reader
src/claude_monitor/data/opencode_reader.py
New module: discovery, parsing, deduplication, token accounting, cost calc, load_opencode_entries, load_opencode_raw_entries, and detect/path helpers.
Aggregation & Analysis
src/claude_monitor/data/aggregator.py, src/claude_monitor/data/analysis.py
UsageAggregator and analyze_usage accept data_source, map string→DataSource, and use load_usage_entries_unified; metadata/logs include detected source.
Monitoring Orchestration
src/claude_monitor/monitoring/orchestrator.py
Orchestrator ctor documents and passes data_source into DataManager.
Tests
src/tests/test_analysis.py, src/tests/test_monitoring_orchestrator.py, src/tests/test_opencode_reader.py, src/tests/test_settings.py
Tests updated to mock/use load_usage_entries_unified (3-tuple), assert data_source plumbing, and add comprehensive OpenCode reader tests.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • rvaidya
  • kory-
  • PedramNavid
  • adawalli
  • eleloi

Poem

🐰 Hop, hop, the sources align—
Claude and OpenCode share one line.
Auto-detect sniffs which path is true,
Entries merge and metadata too.
A rabbit cheers: "New data, woo-hoo!"

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add OpenCode support for usage monitoring' directly and concisely describes the primary change: adding OpenCode as a new data source for usage monitoring alongside existing Claude Code support.
Linked Issues check ✅ Passed The pull request successfully implements the core requirement from issue #176 by adding OpenCode data collection and auto-detection, enabling users to monitor Claude usage within the OpenCode editor.
Out of Scope Changes check ✅ Passed All code changes are directly related to adding OpenCode support: new data source infrastructure (reader module, enum, detection), configuration settings, and comprehensive test coverage. No unrelated changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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 to load_usage_entries. The updated tests use assert_called_once() without argument verification. While this is more flexible, it loses test coverage for ensuring the correct data_path, hours_back, mode, include_raw, and source parameters 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.AUTO

Or 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 save method at lines 26-42 persists several settings to last_used.json (theme, timezone, time_format, etc.) but does not include the newly added data_source field. 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 efficient any(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 unrecognized data_source values.

The source map silently falls back to AUTO for 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.AUTO
src/claude_monitor/data/opencode_reader.py (2)

21-23: Duplicated constant with reader.py.

OPENCODE_STORAGE_PATH is defined both here and in reader.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.py already imports from this module, define it here and import it in reader.py:

In reader.py:

+from claude_monitor.data.opencode_reader import OPENCODE_STORAGE_PATH
-OPENCODE_STORAGE_PATH = "~/.local/share/opencode/storage"

53-59: Unused cutoff_time variable.

cutoff_time is assigned on line 55 but never used; only cutoff_ms is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 06f0fe1 and 1c80941.

📒 Files selected for processing (12)
  • src/claude_monitor/cli/main.py
  • src/claude_monitor/core/settings.py
  • src/claude_monitor/data/__init__.py
  • src/claude_monitor/data/aggregator.py
  • src/claude_monitor/data/analysis.py
  • src/claude_monitor/data/opencode_reader.py
  • src/claude_monitor/data/reader.py
  • src/claude_monitor/monitoring/data_manager.py
  • src/claude_monitor/monitoring/orchestrator.py
  • src/tests/test_analysis.py
  • src/tests/test_monitoring_orchestrator.py
  • src/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_source parameter is properly:

  • Added to the constructor with a sensible default of "auto"
  • Documented in the docstring
  • Stored as an instance attribute
  • Propagated to analyze_usage at line 66

The 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 API
src/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_source parameter 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:

  • Literal type to restrict to valid values at type-check time
  • Field with 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, and theme, 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_source field 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_source parameter is properly added to the signature, documented in the docstring, and correctly passed through to DataManager. 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 DataSource enum provides a clean abstraction for source selection. The documentation clarifies that AUTO and ALL have 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_source to 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_*.json matches the documented OpenCode storage format.


212-224: Verify OpenCode model ID format compatibility with pricing calculator.

The modelID from OpenCode is passed directly to PricingCalculator.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_model has 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.

Comment on lines 445 to 456
# 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")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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]>
Copy link

@coderabbitai coderabbitai bot left a 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 efficient any() 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.CLAUDE as 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.AUTO

Option 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 available
src/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.name

Option 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1c80941 and 4335cb4.

📒 Files selected for processing (2)
  • src/claude_monitor/data/opencode_reader.py
  • src/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.AUTO and DataSource.ALL by 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]>
Copy link

@coderabbitai coderabbitai bot left a 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_path handling.


492-524: LGTM! get_data_source_info properly handles all source types.

The function now correctly handles AUTO and ALL sources 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 TestSettings class has validation tests for plan, theme, timezone, time_format, and log_level, but lacks explicit tests for the new data_source validator. Consider adding tests like test_data_source_validator_valid_values and test_data_source_validator_invalid_value for consistency.

src/claude_monitor/data/opencode_reader.py (1)

138-145: Unused mode parameter in _process_message_file.

The mode: CostMode parameter is passed to _process_message_file but never used. The PricingCalculator.calculate_cost is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4335cb4 and 2038ccd.

📒 Files selected for processing (7)
  • src/claude_monitor/core/settings.py
  • src/claude_monitor/data/analysis.py
  • src/claude_monitor/data/opencode_reader.py
  • src/claude_monitor/data/reader.py
  • src/tests/test_analysis.py
  • src/tests/test_opencode_reader.py
  • src/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_source field 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_source validator 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_source is correctly saved in LastUsedParams.save() and propagated via to_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_source to 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_data fixture 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_info function 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 AUTO gracefully.


69-80: LGTM! Proper integration with unified loader.

The function correctly unpacks the three-value return from load_usage_entries_unified and logs useful information including the detected source and entry count.


105-116: LGTM! Metadata properly records detected data source.

The detected_source.value is 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_sources function properly checks for Claude Code using any() (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_entries function 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.read and the internal cache_creation_tokens/cache_read_tokens terminology.


246-263: LGTM! Utility functions are clean and focused.

Both detect_opencode_installation and get_opencode_storage_path are simple, well-documented utilities that properly handle path expansion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for Claude within OpenCode

1 participant