Skip to content

feat: add Kilo Code CLI analyzer#118

Merged
mike1858 merged 4 commits intomainfrom
feat/kilo-cli
Apr 5, 2026
Merged

feat: add Kilo Code CLI analyzer#118
mike1858 merged 4 commits intomainfrom
feat/kilo-cli

Conversation

@mike1858
Copy link
Copy Markdown
Member

@mike1858 mike1858 commented Feb 14, 2026

Add support for Kilo Code CLI (terminal-based AI coding agent, fork of OpenCode). Supports both on-disk data formats:

  1. Legacy JSON files — per-file message layout under ~/.local/share/kilo/storage/
  2. SQLite database~/.local/share/kilo/kilo.db (and channel-specific variants like kilo-canary.db), using the same schema as OpenCode's SQLite database

Changes

  • New KiloCliAnalyzer with full Analyzer trait implementation supporting dual JSON+SQLite sources
  • Shared OpenCodeFormatAnalyzer (in opencode_common.rs) for legacy JSON parsing — both OpenCode and Kilo CLI delegate to this
  • Shared SQLite helpers in opencode.rs made pub(crate)parse_sqlite_messages, build_conversation_message, DB loaders, stats computation — parameterized by Application and hash_prefix so both analyzers share the code with their own identities
  • Uses kilo_cli_ prefix for unique global hashes (prevents cross-tool deduplication collision with OpenCode)
  • SQLite records take priority over legacy JSON during deduplication (richer data)
  • Watches both the JSON message directory and the app directory (for DB changes) for real-time updates
  • 24 Kilo CLI tests covering: analyzer basics, SQLite data blob parsing, stats computation, step-finish token fallback, in-memory end-to-end DB test, global hash consistency (JSON ↔ SQLite), is_valid_data_path for .db files, glob patterns

Closes #117.

Summary by CodeRabbit

  • New Features

    • Added Kilo CLI analyzer with automatic source discovery, parallel ingestion, deduplication, watch directories, and daily statistics for coding activity.
  • Integration

    • Introduced a shared OpenCode-format parser to support both Kilo CLI and existing formats for consistent parsing and hashing.
  • Tests

    • Added comprehensive tests covering parsing, DB/JSON paths, stats aggregation, hashing, and end-to-end flows.
  • Documentation

    • README updated to clarify Kilo Code is available as a VS Code extension and CLI.

Add support for Kilo Code CLI (terminal-based AI coding agent, fork of
OpenCode). Reads data from ~/.local/share/kilo/storage/ with per-file
message layout.

- New KiloCliAnalyzer with full Analyzer trait implementation
- Separate from existing KiloCode VS Code extension analyzer
- Uses kilo_cli_ prefix for unique global hashes
- Resolves model names from assistant modelID and nested user model.modelID
- 18 new tests (14 unit + 4 integration)
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 14, 2026

📝 Walkthrough

Walkthrough

Adds Kilo CLI support by introducing a new KiloCliAnalyzer and a shared OpenCodeFormatAnalyzer module. The analyzers discover and parse Kilo/OpenCode JSON and SQLite stores, compute per-day agentic coding tool stats, and register KiloCliAnalyzer in the analyzer registry.

Changes

Cohort / File(s) Summary
Documentation
README.md
Edited Kilo Code entry to append " (VS Code extension + CLI)".
New Kilo CLI Analyzer
src/analyzers/kilo_cli.rs
New KiloCliAnalyzer implementing Analyzer; discovers JSON/SQLite sources, routes .db to shared SQLite parser and JSON to OpenCode parser, deduplicates messages, computes per-day stats, exposes availability/watch dirs and contribution strategy.
Shared OpenCode Format
src/analyzers/opencode_common.rs, src/analyzers/opencode.rs
New opencode_common with configurable OpenCodeFormatAnalyzer and many pub(crate) helpers; unified JSON/SQLite parsing, model name resolution, timestamp conversion, tool-stats extraction from "parts", and altered build_conversation_message/parse_sqlite_messages to accept Application + hash_prefix. opencode.rs visibility expanded and signatures updated to support shared usage.
Module & Registry Integration
src/analyzers/mod.rs, src/main.rs, src/types.rs
Exports/re-exports KiloCliAnalyzer, exposes opencode_common at crate visibility, adds Application::KiloCli variant, and registers KiloCliAnalyzer in analyzer registry.
Tests
src/analyzers/tests/kilo_cli.rs, src/analyzers/tests/mod.rs
Extensive new tests validating JSON/SQLite parsing, hashing, stats aggregation, in-memory SQLite flows, path filtering, glob patterns, and analyzer lifecycle; test module registration added.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant Kilo as KiloCliAnalyzer
    participant OpenCode as OpenCodeFormatAnalyzer
    participant FS as FileSystem
    participant Parser as MessageParser
    participant Stats as StatsAggregator

    Client->>Kilo: initialize()
    Kilo->>OpenCode: new(config: Kilo)
    Client->>Kilo: discover_data_sources()
    Kilo->>OpenCode: discover_data_sources()
    OpenCode->>FS: walk storage dirs (JSON, *.db)
    FS-->>OpenCode: list of DataSource
    Client->>Kilo: get_stats_with_sources(sources)
    Kilo->>OpenCode: get_stats_with_sources(sources)
    OpenCode->>Parser: parse_sources_parallel()
    par Parallel
        Parser->>FS: read message / db rows
        FS-->>Parser: message content / rows
        Parser->>FS: read "parts" files
        FS-->>Parser: tool stats parts
    end
    Parser->>Stats: emit ConversationMessage + Stats
    Stats->>Stats: aggregate per-day metrics & dedupe
    Stats-->>OpenCode: AgenticCodingToolStats
    OpenCode-->>Kilo: AgenticCodingToolStats
    Kilo-->>Client: stats result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • bl-ue

Poem

🐰 I hopped through code with ears held high,
Found kilo traces beneath the sky.
JSON, SQLite — parsed in a blink,
Stats assembled before I could think.
Cheers — a carrot-compiled delight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: add Kilo Code CLI analyzer' accurately describes the main change—adding a new analyzer for Kilo Code CLI support.
Linked Issues check ✅ Passed The PR fully implements the requirements from issue #117: KiloCliAnalyzer detects and parses Kilo CLI data from ~/.local/share/kilo/, extracts token/cost metrics, and integrates the analyzer into the registry for unified tracking.
Out of Scope Changes check ✅ Passed All changes directly support Kilo CLI analyzer implementation. Refactoring of OpenCode shared code into opencode_common.rs and updating OpenCode's visibility are necessary to support code reuse and are in scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/kilo-cli

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
Copy Markdown

@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: 2

🤖 Fix all issues with AI agents
In `@src/analyzers/kilo_cli.rs`:
- Around line 30-36: The data_dir and storage_root functions currently hardcode
"~/.local/share/kilo/storage" which is incorrect; update both to compute the
Kilo CLI data directory using platform-appropriate APIs (e.g., use
dirs::config_dir() or dirs::home_dir() to build a path to ".kilocode") so
data_dir returns the message storage path under "~/.kilocode/" and storage_root
returns the root "~/.kilocode/" directory; modify the implementations of
data_dir() and storage_root() to derive the base via dirs::config_dir() (falling
back to dirs::home_dir()) and then join ".kilocode" (and "message" for data_dir)
to satisfy cross-platform requirements.
- Around line 512-529: parse_source currently reloads projects and sessions
every call by calling Self::storage_root(), load_projects and load_sessions;
change this to reuse a preloaded context instead—either add cached fields to the
analyzer struct (e.g., projects: HashMap<...>, sessions: HashMap<...>) and
initialize them once from storage_root(), or add a new helper
parse_source_with_context(&self, source: &DataSource, projects: &ProjectsType,
sessions: &SessionsType, part_root: &Path) and update the single-file watcher
path to call that helper (matching how parse_sources_parallel_with_paths already
reuses context). Ensure you stop calling load_projects/load_sessions inside
parse_source and use the provided/cached projects and sessions and the existing
storage_root/part_root values.
🧹 Nitpick comments (3)
src/analyzers/tests/kilo_cli.rs (1)

26-31: #[tokio::test] is unnecessary here — get_stats_with_sources is synchronous.

The function being tested isn't async, so a plain #[test] suffices. The async runtime adds overhead for no benefit.

Suggested fix
-#[tokio::test]
-async fn test_kilo_cli_get_stats_empty_sources() {
+#[test]
+fn test_kilo_cli_get_stats_empty_sources() {
     let analyzer = KiloCliAnalyzer::new();
     let result = analyzer.get_stats_with_sources(vec![]);
     assert!(result.is_ok());
     assert!(result.unwrap().messages.is_empty());
 }
src/analyzers/kilo_cli.rs (2)

340-356: Only read and glob tool types update specific file stats.

Other common tool types (e.g., write, edit, bash/shell) are counted as generic tool_calls but don't populate files_edited, files_added, terminal_commands, etc. This is fine for an initial implementation but worth tracking as a follow-up to get richer file operation metrics.

Would you like me to open an issue to track expanding tool-type coverage (write, edit, bash, etc.) for richer file operation stats?


386-427: The tool_calls = 1 assignment is intentional and already documented.

This pattern appears identically in both opencode.rs and kilo_cli.rs, with the same condition and comment: "Ensure tool_calls is at least 1 when we had a model call." It represents a deliberate design decision to count bare model inferences (messages with tokens but no actual tool invocations) as a single "tool call" for metric purposes. The current comment adequately explains the intent, so no changes are required.

Comment thread src/analyzers/kilo_cli.rs Outdated
Comment thread src/analyzers/kilo_cli.rs
@mike1858
Copy link
Copy Markdown
Member Author

@coderabbitai Review.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 14, 2026

@mike1858: I'll review the changes in this PR.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Sewer56
Copy link
Copy Markdown
Contributor

Sewer56 commented Feb 17, 2026

Should this code not be refactored to just lift the common code out into a dedicated struct? Or support both from same file. Since it's a fork of same client.

…odule

Kilo CLI is a fork of OpenCode and both use the identical on-disk format
(one JSON file per message, per-session directories). The two analyzer
implementations were ~93% duplicated (~1,464 lines of near-identical code).

Extract all shared types, helpers, and the Analyzer trait implementation
into a new opencode_common module parameterised by an OpenCodeFormatConfig
(display name, Application variant, hash prefix, storage sub-directory).
Both OpenCodeAnalyzer and KiloCliAnalyzer are now thin newtype wrappers
that delegate to OpenCodeFormatAnalyzer with their respective configs.

Before: opencode.rs (681 lines) + kilo_cli.rs (867 lines) = 1548 lines
After:  opencode_common.rs (949) + opencode.rs (66) + kilo_cli.rs (66) = 1081 lines

All 215 tests pass, clippy clean, fmt clean, docs build.

Addresses review feedback from Sewer56 on PR #118.
@Sewer56
Copy link
Copy Markdown
Contributor

Sewer56 commented Feb 17, 2026

(Extra Note: opencode just moved to sqlite, I imagine kilo and other forks will follow very soon. I just never got around to opening issue for this; normally I'd add it myself, but it's been a busy last few weekends.)

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (2)
src/analyzers/opencode_common.rs (1)

486-491: Roles other than "user" silently map to MessageRole::Assistant.

If the on-disk format ever introduces a third role (e.g. "system" or "tool"), it would be silently counted as an assistant message. This is fine for the current schema but worth a brief comment or a log/trace for unexpected values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/analyzers/opencode_common.rs` around lines 486 - 491, The current
conversion of msg.role to MessageRole (the role: if msg.role == "user" {
MessageRole::User } else { MessageRole::Assistant }) silently treats any
non-"user" value as Assistant; change this to an explicit match on msg.role
(e.g. "user" => MessageRole::User, "assistant" => MessageRole::Assistant, _ => {
trace or warn the unexpected msg.role value and then map to a sensible default
}) so unexpected roles are logged/traced rather than silently accepted; update
the code around the role field creation where msg.role and MessageRole are used
and add a brief comment noting why unknown roles are logged and the chosen
fallback.
src/analyzers/kilo_cli.rs (1)

12-66: Consider a macro to eliminate the duplicated delegation boilerplate.

Both KiloCliAnalyzer and OpenCodeAnalyzer have identical 40-line impl Analyzer blocks that purely delegate to self.0. A small declarative macro could generate these wrappers from just the struct name and config, reducing the chance of the two drifting apart if the Analyzer trait gains new methods.

💡 Example macro sketch
macro_rules! opencode_format_analyzer {
    ($name:ident, $config:expr) => {
        pub struct $name(OpenCodeFormatAnalyzer);

        impl $name {
            pub fn new() -> Self {
                Self(OpenCodeFormatAnalyzer::new($config))
            }
        }

        #[async_trait]
        impl Analyzer for $name {
            fn display_name(&self) -> &'static str { self.0.display_name() }
            fn get_data_glob_patterns(&self) -> Vec<String> { self.0.get_data_glob_patterns() }
            // ... remaining methods ...
            fn contribution_strategy(&self) -> ContributionStrategy { self.0.contribution_strategy() }
        }
    };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/analyzers/kilo_cli.rs` around lines 12 - 66, The impl block for
KiloCliAnalyzer duplicates a long delegation boilerplate to
OpenCodeFormatAnalyzer; create a declarative macro (e.g.,
opencode_format_analyzer!) that takes the wrapper name and OpenCodeFormatConfig
and generates the pub struct $name(OpenCodeFormatAnalyzer), the new() ctor, and
the entire #[async_trait] impl Analyzer delegating each method to self.0
(covering display_name, get_data_glob_patterns, discover_data_sources,
is_available, parse_source, parse_sources_parallel_with_paths,
parse_sources_parallel, get_stats_with_sources, get_watch_directories,
is_valid_data_path, and contribution_strategy), then replace the current
KiloCliAnalyzer (and the similar OpenCodeAnalyzer) definitions with invocations
of that macro to eliminate the duplicated code while preserving async_trait and
signatures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/analyzers/kilo_cli.rs`:
- Around line 12-66: The impl block for KiloCliAnalyzer duplicates a long
delegation boilerplate to OpenCodeFormatAnalyzer; create a declarative macro
(e.g., opencode_format_analyzer!) that takes the wrapper name and
OpenCodeFormatConfig and generates the pub struct $name(OpenCodeFormatAnalyzer),
the new() ctor, and the entire #[async_trait] impl Analyzer delegating each
method to self.0 (covering display_name, get_data_glob_patterns,
discover_data_sources, is_available, parse_source,
parse_sources_parallel_with_paths, parse_sources_parallel,
get_stats_with_sources, get_watch_directories, is_valid_data_path, and
contribution_strategy), then replace the current KiloCliAnalyzer (and the
similar OpenCodeAnalyzer) definitions with invocations of that macro to
eliminate the duplicated code while preserving async_trait and signatures.

In `@src/analyzers/opencode_common.rs`:
- Around line 486-491: The current conversion of msg.role to MessageRole (the
role: if msg.role == "user" { MessageRole::User } else { MessageRole::Assistant
}) silently treats any non-"user" value as Assistant; change this to an explicit
match on msg.role (e.g. "user" => MessageRole::User, "assistant" =>
MessageRole::Assistant, _ => { trace or warn the unexpected msg.role value and
then map to a sensible default }) so unexpected roles are logged/traced rather
than silently accepted; update the code around the role field creation where
msg.role and MessageRole are used and add a brief comment noting why unknown
roles are logged and the chosen fallback.

mike1858 added 2 commits April 5, 2026 08:43
# Conflicts:
#	src/analyzers/opencode.rs
Kilo Code CLI has switched from per-file JSON to a SQLite database
(~/.local/share/kilo/kilo.db), using the same schema as OpenCode.
This adds full SQLite support to KiloCliAnalyzer, mirroring the
OpenCode analyzer's dual JSON+SQLite support.

Changes:
- Expand KiloCliAnalyzer from thin opencode_common wrapper to full
  analyzer with SQLite support (kilo.db + channel-specific DBs)
- Make shared helpers in opencode.rs pub(crate) so both analyzers
  can reuse DB parsing, stats computation, and message building
- Parameterize build_conversation_message and parse_sqlite_messages
  to accept Application variant and hash_prefix
- SQLite records take priority over legacy JSON during deduplication
- Watch both JSON dir and app dir for real-time updates
- 24 tests (was 4): parsing, stats, step-finish fallback, in-memory
  end-to-end, global hash consistency, is_valid_data_path, glob patterns

All 282 tests pass, clippy clean, fmt clean, docs build.
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (2)
src/analyzers/kilo_cli.rs (1)

54-81: Consider extracting shared DB discovery logic.

The discover_db_files() implementation is nearly identical to OpenCodeAnalyzer::discover_db_files() in opencode.rs, differing only in the glob pattern (kilo*.db vs opencode*.db) and filename checks. If additional OpenCode forks are added in the future, consider extracting this into a parameterized helper in opencode_common.rs.

This is a low-priority refactor since the current duplication is manageable and the pattern is straightforward.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/analyzers/kilo_cli.rs` around lines 54 - 81, The discover_db_files
implementation in kilo_cli.rs duplicates OpenCodeAnalyzer::discover_db_files;
extract the shared logic into a parameterized helper (e.g.,
opencode_common::discover_db_files_for_prefix) that takes the filename
prefix/pattern and a predicate for allowed filenames, then have Kilo CLI's
discover_db_files() call that helper with "kilo" (pattern "kilo*.db" and accept
"kilo.db" or names starting with "kilo-" and ending with ".db"); update
OpenCodeAnalyzer::discover_db_files() to call the same helper with "opencode" to
remove duplication while preserving the existing filename checks and behavior.
src/analyzers/tests/kilo_cli.rs (1)

38-44: Unnecessary #[tokio::test] for synchronous function.

get_stats_with_sources is synchronous (no .await call), so #[test] would suffice here. Using #[tokio::test] works but adds unnecessary runtime overhead.

🛠️ Optional fix
-#[tokio::test]
-async fn test_kilo_cli_get_stats_empty_sources() {
+#[test]
+fn test_kilo_cli_get_stats_empty_sources() {
     let analyzer = KiloCliAnalyzer::new();
     let result = analyzer.get_stats_with_sources(vec![]);
     assert!(result.is_ok());
     assert!(result.unwrap().messages.is_empty());
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/analyzers/tests/kilo_cli.rs` around lines 38 - 44, The test
`test_kilo_cli_get_stats_empty_sources` is marked async with `#[tokio::test]`
but calls the synchronous method `KiloCliAnalyzer::get_stats_with_sources`;
change the test to a regular synchronous test by replacing `#[tokio::test]` with
`#[test]` and removing `async` from the `fn` signature so the function is `fn
test_kilo_cli_get_stats_empty_sources() { ... }` while keeping the calls to
`KiloCliAnalyzer::new()` and `get_stats_with_sources(vec![])` and the same
assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/analyzers/kilo_cli.rs`:
- Around line 54-81: The discover_db_files implementation in kilo_cli.rs
duplicates OpenCodeAnalyzer::discover_db_files; extract the shared logic into a
parameterized helper (e.g., opencode_common::discover_db_files_for_prefix) that
takes the filename prefix/pattern and a predicate for allowed filenames, then
have Kilo CLI's discover_db_files() call that helper with "kilo" (pattern
"kilo*.db" and accept "kilo.db" or names starting with "kilo-" and ending with
".db"); update OpenCodeAnalyzer::discover_db_files() to call the same helper
with "opencode" to remove duplication while preserving the existing filename
checks and behavior.

In `@src/analyzers/tests/kilo_cli.rs`:
- Around line 38-44: The test `test_kilo_cli_get_stats_empty_sources` is marked
async with `#[tokio::test]` but calls the synchronous method
`KiloCliAnalyzer::get_stats_with_sources`; change the test to a regular
synchronous test by replacing `#[tokio::test]` with `#[test]` and removing
`async` from the `fn` signature so the function is `fn
test_kilo_cli_get_stats_empty_sources() { ... }` while keeping the calls to
`KiloCliAnalyzer::new()` and `get_stats_with_sources(vec![])` and the same
assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ced89da5-5116-4494-9478-2867553a9a8e

📥 Commits

Reviewing files that changed from the base of the PR and between 1e8d7c7 and a3e627d.

📒 Files selected for processing (3)
  • src/analyzers/kilo_cli.rs
  • src/analyzers/opencode.rs
  • src/analyzers/tests/kilo_cli.rs

@mike1858 mike1858 merged commit 3a706b5 into main Apr 5, 2026
6 checks passed
@mike1858 mike1858 deleted the feat/kilo-cli branch April 5, 2026 15:54
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.

Add support for Kilo CLI (kilo-code) tracking

2 participants