Skip to content

feat(scanner): add scanner.opencodeDbPaths to settings.json#409

Merged
junhoyeo merged 3 commits intomainfrom
feat/scanner-settings-config
Apr 8, 2026
Merged

feat(scanner): add scanner.opencodeDbPaths to settings.json#409
junhoyeo merged 3 commits intomainfrom
feat/scanner-settings-config

Conversation

@junhoyeo
Copy link
Copy Markdown
Owner

@junhoyeo junhoyeo commented Apr 8, 2026

Note: This PR replaces #408 (auto-closed when its base branch fix/opencode-channel-dbs was deleted as part of merging #406). All review threads from #408 were resolved before the rebase. The branch is now rebased directly onto main (squash commit 7156cac) with no functional changes.


Summary

Stacked on #406.

Follow-up to the discussion under #387: adds a persistent, declarative counterpart to the TOKSCALE_EXTRA_DIRS env var so users who launch opencode with OPENCODE_DB pointing at a path outside ~/.local/share/opencode can pin those dbs in ~/.config/tokscale/settings.json.

{
  "scanner": {
    "opencodeDbPaths": [
      "/custom/location/opencode.db",
      "/another/location/opencode-stable.db"
    ]
  }
}

Why this shape

Tokscale already has ~/.config/tokscale/settings.json for TUI preferences, so scanner is just another top-level section — no second config file to manage. The struct lives in tokscale-core (not just the CLI) so headless consumers of the scanner get the same config surface.

Changes

tokscale-core::scanner

  • New public ScannerSettings struct with opencode_db_paths: Vec<PathBuf>, serde camelCase, #[serde(default)] at both struct and field level so older/partial settings.json files deserialize cleanly.
  • New scan_all_clients_with_scanner_settings(home_dir, clients, use_env_roots, settings) entry point. Runs auto-discovery then merges user-configured paths via merge_user_opencode_db_paths, which:
    • Canonicalizes for dedup (a user who explicitly lists an auto-discovered db won't get double-parsed).
    • Silently skips non-existent paths (stale config must never break a scan — the config outlives any single opencode install).
    • Rejects WAL/SHM/journal sidecars via the same is_opencode_db_filename check introduced in fix(opencode): track all channel-suffixed dbs #406.
  • scan_all_clients_with_env_strategy and scan_all_clients become thin wrappers that pass ScannerSettings::default(), preserving backward compatibility.

tokscale-core::lib

  • ReportOptions and LocalParseOptions both grow a scanner_settings: scanner::ScannerSettings field and derive Default.
  • parse_all_messages_with_pricing_with_env_strategy takes &ScannerSettings and passes it to the new scanner entry point. All four internal call sites (model report, monthly report, graph, local-graph) thread it through from their enclosing options.

tokscale-cli::tui::settings

  • Settings struct gets a scanner: ScannerSettings field with #[serde(default)].
  • New load_scanner_settings() helper — the single function every CLI entry point calls to populate LocalParseOptions/ReportOptions.

CLI call sites wired (one-line field addition each):

  • commands/wrapped.rs (× 2)
  • main.rs run_models_report / run_monthly_report / run_clients_command / run_graph_command / run_submit_command
  • tui/data/mod.rs (× 3)

Test plan

  • cargo test --workspace886 tests pass (322 + 81 + 480 + 3 + 0 doctest), 2 ignored (unrelated, pre-existing)
  • cargo clippy -p tokscale-core --lib --tests -- -D warnings — clean
  • New tokscale-core tests (in scanner::tests):
    • test_merge_user_opencode_db_paths_picks_up_path_outside_xdg — the flagship OPENCODE_DB=/abs/path use case
    • test_merge_user_opencode_db_paths_skips_nonexistent_and_sidecars — graceful degradation for stale config + sidecar rejection
    • test_merge_user_opencode_db_paths_dedups_against_auto_discovered — canonicalized dedup
    • test_scanner_settings_deserialize_from_json_camel_caseopencodeDbPaths field contract + empty-object form
    • test_scan_all_clients_with_scanner_settings_merges_user_path — end-to-end through scan_all_clients_with_scanner_settings with both an auto-discovered db and a user-configured outside-XDG db
  • New tokscale-cli tests (in tui::settings::tests):
    • settings_load_backfills_scanner_when_missing_from_json — older settings.json without the key still loads
    • settings_load_reads_scanner_opencode_db_paths — paths deserialize from the expected shape
    • settings_accepts_empty_scanner_object"scanner": {} is a valid no-op
    • settings_round_trips_scanner_section_through_json — save → load preserves paths verbatim

Follow-ups (out of scope)

  • A TUI pane to edit scanner.opencodeDbPaths interactively. Right now users have to hand-edit the JSON file.
  • :memory: in-process opencode dbs — still no file to scan.
  • A broader ScannerSettings schema for other clients' override paths (currently only opencode has a use case).

Open with Devin

Summary by cubic

Adds persistent scanner config via scanner.opencodeDbPaths in ~/.config/tokscale/settings.json so users can pin OpenCode SQLite DBs outside the default data dir; paths merge with auto-discovery, dedupe by canonical path, skip missing, ignore SQLite sidecars, are honored across all report paths (including parse_local_clients and the wrapped UI), and are only merged when the OpenCode client is enabled so --clients claude doesn’t import OpenCode DBs.

  • New Features

    • tokscale-core::scanner::ScannerSettings (camelCase, Default) with opencodeDbPaths.
    • scan_all_clients_with_scanner_settings merges user paths with discovery; dedupes, skips missing, rejects WAL/SHM/journal via is_opencode_db_filename.
    • ReportOptions and LocalParseOptions carry scanner_settings (Default); wrappers pass defaults for backward compatibility.
    • CLI Settings adds scanner; load_scanner_settings() threads config into all command entry points; README documents the setting.
  • Bug Fixes

    • parse_local_clients now passes scanner_settings, so tokscale clients and wrapped views honor opencodeDbPaths.
    • Only merge opencodeDbPaths when OpenCode is enabled, preventing unrelated scans (e.g. --clients claude) from importing OpenCode DBs or inflating counts.
    • tui::DataLoader uses a cfg(test) helper to ignore real settings in tests, keeping unit tests hermetic.

Written for commit 0e1a79d. Summary will update on new commits.

junhoyeo added 3 commits April 8, 2026 09:40
Adds a persistent, declarative counterpart to TOKSCALE_EXTRA_DIRS for the
OpenCode scanner. Users who launch opencode with `OPENCODE_DB` pointing at
a path outside `~/.local/share/opencode` (the upstream env var supports
absolute paths) can now point tokscale at those dbs in
`~/.config/tokscale/settings.json`:

    {
      "scanner": {
        "opencodeDbPaths": [
          "/custom/location/opencode.db",
          "/another/location/opencode-stable.db"
        ]
      }
    }

Implementation shape:

- `tokscale_core::scanner::ScannerSettings` — new public, Default,
  serde-camelCase struct. Lives in core so any non-CLI consumer of the
  scanner sees the same config surface.
- `scan_all_clients_with_scanner_settings` — new scanner entry point that
  runs auto-discovery then merges user-configured paths into
  `ScanResult.opencode_dbs` via `merge_user_opencode_db_paths`, which
  canonicalizes for dedup, silently skips non-existent paths (stale
  config must never break a scan), and rejects WAL/SHM sidecars through
  the same `is_opencode_db_filename` check used in #406.
- `ScannerSettings` threaded through `ReportOptions` and
  `LocalParseOptions` (both now derive `Default`), and through the
  private `parse_all_messages_with_pricing_with_env_strategy` helper.
- CLI `Settings` struct gets a `scanner: ScannerSettings` field with
  `#[serde(default)]` so older settings.json files without the key still
  load cleanly. `load_scanner_settings()` is the single helper every
  entry point calls to populate the options structs.

Why add it here instead of in a brand-new config file: tokscale already
has `~/.config/tokscale/settings.json` for TUI prefs, so `scanner` is
just another top-level section. Avoids introducing a second config file
to manage and keeps the CLI-side loader to a one-line helper.

Constraint: must reuse the same filename validation as auto-discovery so
  sidecars (.db-wal, .db-shm, .db-journal) can never be passed in by
  accident — they would immediately fail on rusqlite::Connection::open.
Constraint: unknown/malformed/missing user paths must degrade gracefully,
  never erroring — the config outlives any single opencode install.
Rejected: separate `scanner.toml` | two config files is worse for
  discoverability than one `settings.json` with sections.
Rejected: env var `TOKSCALE_OPENCODE_DB_PATHS` | env vars are hard to
  discover and don't survive shell sessions; users asked explicitly for
  a persistent config knob.
Rejected: TUI-only setting | the scanner runs from CLI-headless paths
  too (headless, server, wrapped), and threading a TUI struct into
  tokscale-core would invert the dependency direction.
Confidence: high
Scope-risk: moderate (touches every entry point that builds
  ReportOptions/LocalParseOptions, but the change at each site is a
  one-line field addition)
Directive: when adding new top-level scanner knobs, add them to
  `ScannerSettings` in core — do NOT put scanner config in the CLI
  `Settings` struct directly, or headless callers will lose access.
Not-tested: concurrent scanner_settings updates during a running scan
  (not a supported scenario — settings are snapshotted at call time)
… hermeticize data loader tests

parse_local_clients was the one reporting path that still called
scan_all_clients_with_env_strategy, which silently discarded
options.scanner_settings. Every other path
(parse_all_messages_with_pricing_with_env_strategy, get_model_report,
get_monthly_report, generate_graph_with_loaded_pricing,
parse_local_unified_messages_resolved) already passes scanner_settings,
so 'tokscale clients' and the wrapped agents view were the only code
paths that never honored scanner.opencodeDbPaths from settings.json.
Route parse_local_clients through scan_all_clients_with_scanner_settings
to match, then pin the behavior with a regression test that builds an
external opencode.db and asserts it only shows up when the caller
passes it via ScannerSettings.opencode_db_paths.

tui/data/mod.rs::DataLoader::load also reads
~/.config/tokscale/settings.json via load_scanner_settings(), and the
same file's unit tests go through DataLoader::load(), so any developer
with scanner.opencodeDbPaths configured sees machine-dependent test
results. Replace the three call sites with a small cfg(test)-gated
helper that returns ScannerSettings::default() under cfg(test) and
delegates to load_scanner_settings() in production, plus a test that
asserts the helper stays hermetic.
…is enabled

scan_all_clients_with_scanner_settings used to call
merge_user_opencode_db_paths unconditionally after the inner scan, which
bypassed the existing enabled.contains(&ClientId::OpenCode) guard.
A request like 'tokscale --claude' would still pull in user-pinned
OpenCode dbs from scanner.opencodeDbPaths, parse the SQLite rows, and
inflate parse_local_clients counts even though message-level filters
later dropped the rows from the returned message vector. Beyond the
counts inconsistency, this also wasted SQLite parsing work on every
non-opencode scan for users with the new config.

Move the merge into scan_all_clients_with_env_strategy_inner, right
inside the existing 'if enabled.contains(&ClientId::OpenCode)' block,
so the merge is naturally scoped to opencode-enabled scans without
duplicating the (non-obvious) 'synthetic enables all' enable logic.
scan_all_clients_with_scanner_settings becomes a thin pass-through
wrapper that forwards scanner_settings to the inner function.

Add two regression tests:

- scanner: test_scan_all_clients_with_scanner_settings_respects_opencode_client_filter
  exercises the four canonical filter shapes (claude, opencode,
  synthetic, empty) against a temp home with both an auto-discoverable
  opencode.db and a user-configured external db, asserting opencode_dbs
  is empty under --claude and populated for the other three.

- lib (end-to-end): test_parse_local_clients_claude_filter_ignores_scanner_settings_opencode_db_paths
  builds a real Claude JSONL session plus an external opencode SQLite
  pinned via ScannerSettings.opencode_db_paths and asserts a
  --clients claude run yields counts.OpenCode == 0, counts.Claude == 1,
  and zero opencode rows in the returned messages.
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 8, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
tokscale Ignored Ignored Apr 8, 2026 0:41am

Request Review

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 7 files

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

@junhoyeo junhoyeo merged commit 46ed70f into main Apr 8, 2026
16 checks passed
@junhoyeo junhoyeo deleted the feat/scanner-settings-config branch April 8, 2026 00:48
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.

1 participant