From bb5a22af43ee3ed623e6025cc3aa8e2b145f4544 Mon Sep 17 00:00:00 2001 From: Daniel Espendiller Date: Thu, 18 Jun 2026 19:58:57 +0200 Subject: [PATCH 1/2] Add JetBrains Junie client support --- README.md | 6 +- crates/tokscale-cli/src/commands/wrapped.rs | 15 ++ crates/tokscale-cli/src/main.rs | 5 + crates/tokscale-cli/src/tui/client_ui.rs | 4 + crates/tokscale-cli/src/tui/data/mod.rs | 10 + crates/tokscale-cli/src/tui/ui/widgets.rs | 1 + crates/tokscale-core/src/clients.rs | 24 +- crates/tokscale-core/src/lib.rs | 44 ++++ crates/tokscale-core/src/scanner.rs | 1 + crates/tokscale-core/src/sessions/junie.rs | 237 ++++++++++++++++++++ crates/tokscale-core/src/sessions/mod.rs | 1 + crates/tokscale-core/tests/junie.rs | 169 ++++++++++++++ packages/frontend/src/lib/constants.ts | 3 + packages/frontend/src/lib/types.ts | 1 + 14 files changed, 518 insertions(+), 3 deletions(-) create mode 100644 crates/tokscale-core/src/sessions/junie.rs create mode 100644 crates/tokscale-core/tests/junie.rs diff --git a/README.md b/README.md index 87f03fae4..62890fd6c 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ | Gajae-Code | [gajae-code (gjc)](https://github.com/Yeachan-Heo/gajae-code) | `~/.gjc/agent/sessions/` (override via `GJC_CODING_AGENT_DIR`, `GJC_CONFIG_DIR`, `PI_CONFIG_DIR`; `$XDG_DATA_HOME/gjc/sessions/` on Linux/macOS) | ✅ Yes | | Jcode | [Jcode](https://github.com/1jehuang/jcode) | `~/.jcode/sessions/session_*.json` + `session_*.journal.jsonl` sidecars (override via `JCODE_HOME`) | ✅ Yes | | MiMo Code | [MiMo Code](https://github.com/XiaomiMiMo/MiMo) | `~/.local/share/micode/mimocode.db` (XDG data dir; SQLite) | ✅ Yes | +| Junie | [Junie](https://www.jetbrains.com/junie/) | `~/.junie/sessions/*/events.jsonl` | ✅ Yes | | Command Code | [Command Code](https://github.com/CommandCodeAI/command-code) | `~/.commandcode/projects/**/*.jsonl` | ✅ Yes | | Synthetic | [Synthetic](https://synthetic.new/) | Re-attributed from other sources via `hf:` model prefix or `synthetic` provider (+ [Octofriend](https://github.com/synthetic-lab/octofriend): `~/.local/share/octofriend/sqlite.db`) | ✅ Yes | @@ -157,7 +158,7 @@ In the age of AI-assisted development, **tokens are the new energy**. They power - GitHub-style contribution graph with 9 color themes - Real-time filtering and sorting - Zero flicker rendering -- **Multi-platform support** - Track usage across OpenCode, Claude Code, Codex CLI, Copilot CLI, Cursor IDE, Gemini CLI, Amp, Codebuff, Droid, OpenClaw, Hermes Agent, Pi, Kimi CLI, Qwen CLI, Roo Code, Kilo, Mux, Kilo CLI, Crush, Goose, Antigravity, Antigravity CLI, Zed, Kiro, Trae, Warp/Oz, Cline, Gajae-Code, Grok Build, Jcode, MiMo Code, Command Code, and Synthetic +- **Multi-platform support** - Track usage across OpenCode, Claude Code, Codex CLI, Copilot CLI, Cursor IDE, Gemini CLI, Amp, Codebuff, Droid, OpenClaw, Hermes Agent, Pi, Kimi CLI, Qwen CLI, Roo Code, Kilo, Mux, Kilo CLI, Crush, Goose, Antigravity, Antigravity CLI, Zed, Kiro, Trae, Warp/Oz, Cline, Gajae-Code, Grok Build, Jcode, MiMo Code, Command Code, Junie, and Synthetic - **Real-time pricing** - Fetches current pricing from LiteLLM with 1-hour disk cache; automatic OpenRouter fallback and Cursor model pricing for newly released models - **Detailed breakdowns** - Input, output, cache read/write, and reasoning token tracking - **Native Rust core** - All parsing and aggregation done in Rust for 10x faster processing @@ -368,7 +369,7 @@ tokscale --client synthetic tokscale --client opencode,claude --week --json ``` -Possible values: `opencode`, `claude`, `codex`, `copilot`, `gemini`, `cursor`, `amp`, `codebuff`, `droid`, `openclaw`, `hermes`, `pi`, `kimi`, `qwen`, `roocode`, `kilocode`, `kilo`, `mux`, `crush`, `goose`, `antigravity`, `antigravity-cli`, `zed`, `kiro`, `trae`, `warp`, `cline`, `gjc`, `grok`, `jcode`, `micode`, `commandcode`, `synthetic`. +Possible values: `opencode`, `claude`, `codex`, `copilot`, `gemini`, `cursor`, `amp`, `codebuff`, `droid`, `openclaw`, `hermes`, `pi`, `kimi`, `qwen`, `roocode`, `kilocode`, `kilo`, `mux`, `crush`, `goose`, `antigravity`, `antigravity-cli`, `zed`, `kiro`, `trae`, `warp`, `cline`, `gjc`, `grok`, `jcode`, `micode`, `commandcode`, `junie`, `synthetic`. > **Deprecation notice**: The legacy single-client flags (`--opencode`, `--claude`, `--codex`, etc.) still work for backward compatibility but are hidden from `--help` and will be removed in the next major release. Migrate to `--client` whenever possible. Running tokscale in an interactive terminal will print a one-line warning when a legacy flag is used. @@ -1296,6 +1297,7 @@ AI coding tools store their session data in cross-platform locations. Most tools | Jcode | `~/.jcode/sessions/` | `%USERPROFILE%\.jcode\sessions\` | Configurable via `JCODE_HOME` env var; parses `session_*.json` snapshots plus `session_*.journal.jsonl` sidecars | | MiMo Code | `~/.local/share/micode/` | `%USERPROFILE%\.local\share\micode\` | Uses XDG data directory; SQLite database `mimocode.db` | | Gajae-Code | `~/.gjc/agent/sessions/` | `%USERPROFILE%\.gjc\agent\sessions\` | Configurable via `GJC_CODING_AGENT_DIR` (also `GJC_CONFIG_DIR`/`PI_CONFIG_DIR`; `$XDG_DATA_HOME/gjc/sessions/` flattens on Linux/macOS) | +| Junie | `~/.junie/sessions/` | `%USERPROFILE%\.junie\sessions\` | Same home-relative path on all platforms; parses `events.jsonl` usage events | | Synthetic | Re-attributed from other sources | Re-attributed from other sources | Detects `hf:` model prefix + `synthetic` provider | > **Note**: On Windows, `~` expands to `%USERPROFILE%` (e.g., `C:\Users\YourName`). These tools intentionally use Unix-style paths (like `.local/share`) even on Windows for cross-platform consistency, rather than Windows-native paths like `%APPDATA%`. diff --git a/crates/tokscale-cli/src/commands/wrapped.rs b/crates/tokscale-cli/src/commands/wrapped.rs index aec3ebcbf..302987ac9 100644 --- a/crates/tokscale-cli/src/commands/wrapped.rs +++ b/crates/tokscale-cli/src/commands/wrapped.rs @@ -1457,6 +1457,7 @@ fn client_display_name(client: &str) -> Option<&'static str> { "cline" => Some("Cline"), "gjc" => Some("Gajae-Code"), "jcode" => Some("Jcode"), + "junie" => Some("Junie"), "synthetic" => Some("Synthetic"), _ => None, } @@ -1499,6 +1500,7 @@ fn client_logo_url(client_name: &str) -> Option<&'static str> { "https://raw.githubusercontent.com/junhoyeo/tokscale/main/.github/assets/client-zed.webp", ), "Jcode" => Some("https://raw.githubusercontent.com/junhoyeo/tokscale/main/.github/assets/client-jcode.png"), + "Junie" => Some("https://github.com/JetBrains.png"), "Synthetic" => Some("https://tokscale.ai/assets/logos/synthetic.png"), _ => None, } @@ -2455,6 +2457,11 @@ mod tests { assert_eq!(client_display_name("jcode"), Some("Jcode")); } + #[test] + fn test_client_display_name_junie() { + assert_eq!(client_display_name("junie"), Some("Junie")); + } + #[test] fn test_client_display_name_unknown() { assert_eq!(client_display_name("unknown"), None); @@ -2638,6 +2645,14 @@ mod tests { ); } + #[test] + fn test_client_logo_url_junie() { + assert_eq!( + client_logo_url("Junie"), + Some("https://github.com/JetBrains.png") + ); + } + #[test] fn test_client_logo_url_unknown() { assert_eq!(client_logo_url("Unknown"), None); diff --git a/crates/tokscale-cli/src/main.rs b/crates/tokscale-cli/src/main.rs index e245f4edf..593bccd90 100644 --- a/crates/tokscale-cli/src/main.rs +++ b/crates/tokscale-cli/src/main.rs @@ -893,6 +893,7 @@ pub enum ClientFilter { Micode, #[value(name = "antigravity-cli")] AntigravityCli, + Junie, Synthetic, } @@ -934,6 +935,7 @@ impl ClientFilter { Self::Commandcode => "commandcode", Self::Micode => "micode", Self::AntigravityCli => "antigravity-cli", + Self::Junie => "junie", Self::Synthetic => "synthetic", } } @@ -978,6 +980,7 @@ impl ClientFilter { Self::Commandcode => Some(ClientId::CommandCode), Self::Micode => Some(ClientId::MiMoCode), Self::AntigravityCli => Some(ClientId::AntigravityCli), + Self::Junie => Some(ClientId::Junie), Self::Synthetic => None, } } @@ -1019,6 +1022,7 @@ impl ClientFilter { ClientId::CommandCode => Self::Commandcode, ClientId::MiMoCode => Self::Micode, ClientId::AntigravityCli => Self::AntigravityCli, + ClientId::Junie => Self::Junie, } } @@ -3614,6 +3618,7 @@ fn capitalize_client(client: &str) -> String { "gjc" => "Gajae-Code".to_string(), "jcode" => "Jcode".to_string(), "commandcode" => "Command Code".to_string(), + "junie" => "Junie".to_string(), other => other.to_string(), } } diff --git a/crates/tokscale-cli/src/tui/client_ui.rs b/crates/tokscale-cli/src/tui/client_ui.rs index 77a815bbb..8117f0cc8 100644 --- a/crates/tokscale-cli/src/tui/client_ui.rs +++ b/crates/tokscale-cli/src/tui/client_ui.rs @@ -134,6 +134,10 @@ pub const CLIENT_UI: [ClientUi; ClientId::COUNT] = [ display_name: "Antigravity CLI", hotkey: 'f', }, + ClientUi { + display_name: "Junie", + hotkey: 'p', + }, ]; pub fn display_name(client: ClientId) -> &'static str { diff --git a/crates/tokscale-cli/src/tui/data/mod.rs b/crates/tokscale-cli/src/tui/data/mod.rs index b55c1b94d..e96e1f6fd 100644 --- a/crates/tokscale-cli/src/tui/data/mod.rs +++ b/crates/tokscale-cli/src/tui/data/mod.rs @@ -1370,6 +1370,7 @@ mod tests { assert_eq!(clients[29], ClientId::CommandCode); assert_eq!(clients[30], ClientId::MiMoCode); assert_eq!(clients[31], ClientId::AntigravityCli); + assert_eq!(clients[32], ClientId::Junie); } #[test] @@ -1461,6 +1462,10 @@ mod tests { crate::tui::client_ui::display_name(ClientId::AntigravityCli), "Antigravity CLI" ); + assert_eq!( + crate::tui::client_ui::display_name(ClientId::Junie), + "Junie" + ); } #[test] @@ -1493,6 +1498,7 @@ mod tests { assert_eq!(crate::tui::client_ui::hotkey(ClientId::Grok), 'u'); assert_eq!(crate::tui::client_ui::hotkey(ClientId::Jcode), 'j'); assert_eq!(crate::tui::client_ui::hotkey(ClientId::AntigravityCli), 'f'); + assert_eq!(crate::tui::client_ui::hotkey(ClientId::Junie), 'p'); } #[test] @@ -1589,6 +1595,10 @@ mod tests { crate::tui::client_ui::from_hotkey('f'), Some(ClientId::AntigravityCli) ); + assert_eq!( + crate::tui::client_ui::from_hotkey('p'), + Some(ClientId::Junie) + ); } #[test] diff --git a/crates/tokscale-cli/src/tui/ui/widgets.rs b/crates/tokscale-cli/src/tui/ui/widgets.rs index 6b27a9973..c4886992f 100644 --- a/crates/tokscale-cli/src/tui/ui/widgets.rs +++ b/crates/tokscale-cli/src/tui/ui/widgets.rs @@ -290,6 +290,7 @@ pub fn get_client_color(client: &str) -> Color { "warp" => Color::Rgb(1, 155, 150), // #019B96 Warp teal "gjc" => Color::Rgb(220, 38, 38), // #DC2626 gajae-code red-claw "jcode" => Color::Rgb(245, 158, 11), // #F59E0B Jcode amber + "junie" => Color::Rgb(123, 97, 255), // #7B61FF Junie violet _ => Color::Rgb(136, 136, 136), // #888888 } } diff --git a/crates/tokscale-core/src/clients.rs b/crates/tokscale-core/src/clients.rs index 38d387048..fdadbe65a 100644 --- a/crates/tokscale-core/src/clients.rs +++ b/crates/tokscale-core/src/clients.rs @@ -486,6 +486,15 @@ define_clients!( headless: false, parse_local: true, submit_default: true + }, + Junie = 32 => { + id: "junie", + root: PathRoot::Home, + relative: ".junie/sessions", + pattern: "events.jsonl", + headless: false, + parse_local: true, + submit_default: true } ); @@ -538,7 +547,7 @@ mod tests { #[test] fn test_client_id_count() { - assert_eq!(ClientId::COUNT, 32); + assert_eq!(ClientId::COUNT, 33); } #[test] @@ -555,6 +564,19 @@ mod tests { assert!(!client.data().headless); } + #[test] + fn test_junie_client_registered_as_local_session_source() { + let client = ClientId::from_str("junie").expect("junie client should be registered"); + assert_eq!( + client.data().resolve_path("/tmp/home"), + "/tmp/home/.junie/sessions" + ); + assert_eq!(client.data().pattern, "events.jsonl"); + assert!(client.data().parse_local); + assert!(client.data().submit_default); + assert!(!client.data().headless); + } + #[test] fn test_client_id_all_len_matches_count() { assert_eq!(ClientId::ALL.len(), ClientId::COUNT); diff --git a/crates/tokscale-core/src/lib.rs b/crates/tokscale-core/src/lib.rs index 61a5a1496..210b9da59 100644 --- a/crates/tokscale-core/src/lib.rs +++ b/crates/tokscale-core/src/lib.rs @@ -1229,6 +1229,32 @@ fn parse_all_messages_with_pricing_with_env_strategy( .filter(|message| should_keep_deduped_message(&mut gjc_seen, message)), ); + // Junie events carry authoritative per-call `modelUsage.cost` values. + // Keep this off the generic source cache because cached_messages() + // reprices every message unconditionally; only fill cost from pricing + // when Junie emitted no usable cost. + let mut junie_seen: HashSet = HashSet::new(); + let junie_messages: Vec = scan_result + .get(ClientId::Junie) + .par_iter() + .flat_map(|path| { + sessions::junie::parse_junie_file(path) + .into_iter() + .map(|mut msg| { + if msg.cost <= 0.0 { + apply_pricing_if_available(&mut msg, pricing); + } + msg + }) + .collect::>() + }) + .collect(); + all_messages.extend( + junie_messages + .into_iter() + .filter(|message| should_keep_deduped_message(&mut junie_seen, message)), + ); + let kimi_outcomes: Vec = scan_result .get(ClientId::Kimi) .par_iter() @@ -2431,6 +2457,24 @@ pub fn parse_local_clients(options: LocalParseOptions) -> Result = scan_result + .get(ClientId::Junie) + .par_iter() + .flat_map(|path| sessions::junie::parse_junie_file(path)) + .collect(); + let mut junie_seen: HashSet = HashSet::new(); + let junie_msgs: Vec = junie_msgs_raw + .into_iter() + .filter(|message| should_keep_deduped_message(&mut junie_seen, message)) + .map(|message| unified_to_parsed(&message)) + .collect(); + let junie_count = summed_parsed_message_count(&junie_msgs); + counts.set(ClientId::Junie, junie_count); + messages.extend(junie_msgs); + // Parse Kimi wire.jsonl files in parallel let kimi_msgs: Vec = scan_result .get(ClientId::Kimi) diff --git a/crates/tokscale-core/src/scanner.rs b/crates/tokscale-core/src/scanner.rs index 64c6add4d..3aa72f6ed 100644 --- a/crates/tokscale-core/src/scanner.rs +++ b/crates/tokscale-core/src/scanner.rs @@ -328,6 +328,7 @@ pub fn scan_directory(root: &str, pattern: &str) -> Vec { "sessions.json" => file_name == "sessions.json", "wire.jsonl" => file_name == "wire.jsonl", "updates.jsonl" => file_name == "updates.jsonl", + "events.jsonl" => file_name == "events.jsonl", "ui_messages.json" => file_name == "ui_messages.json", "session-usage.json" => file_name == "session-usage.json", "chat-messages.json" => file_name == "chat-messages.json", diff --git a/crates/tokscale-core/src/sessions/junie.rs b/crates/tokscale-core/src/sessions/junie.rs new file mode 100644 index 000000000..b830baf67 --- /dev/null +++ b/crates/tokscale-core/src/sessions/junie.rs @@ -0,0 +1,237 @@ +//! Junie session parser +//! +//! Junie stores local sessions under `~/.junie/sessions//events.jsonl`. + +use super::utils::file_modified_timestamp_ms; +use super::UnifiedMessage; +use crate::{pricing, provider_identity, TokenBreakdown}; +use chrono::{Local, LocalResult, NaiveDateTime, TimeZone}; +use serde_json::Value; +use std::collections::HashSet; +use std::io::{BufRead, BufReader}; +use std::path::Path; + +const USAGE_EVENT_KIND: &str = "LlmResponseMetadataEvent"; +const USER_PROMPT_KIND: &str = "UserPromptEvent"; +const SKIP_EVENT_KINDS: &[&str] = &[ + "AgentStateUpdatedEvent", + "AgentCurrentStatusUpdatedEvent", + "AgentPatchCreatedEvent", +]; + +pub fn parse_junie_file(path: &Path) -> Vec { + let file = match std::fs::File::open(path) { + Ok(file) => file, + Err(_) => return Vec::new(), + }; + + let session_id = session_id_from_path(path); + let default_timestamp = + session_timestamp_from_id(&session_id).unwrap_or_else(|| file_modified_timestamp_ms(path)); + let mut pending_turn_start = false; + let mut messages = Vec::new(); + let mut seen = HashSet::new(); + + for line in BufReader::new(file).lines() { + let Ok(line) = line else { + continue; + }; + // Junie state snapshots can be very large and do not carry the usage + // rows Tokscale needs; skip them before JSON parsing. + if SKIP_EVENT_KINDS.iter().any(|kind| line.contains(kind)) { + continue; + } + if !line.contains(USAGE_EVENT_KIND) && !line.contains(USER_PROMPT_KIND) { + continue; + } + + let Ok(value) = serde_json::from_str::(&line) else { + continue; + }; + if event_kind(&value) == Some(USER_PROMPT_KIND) { + pending_turn_start = true; + continue; + } + + let Some(agent_event) = value + .pointer("/event/agentEvent") + .filter(|event| string_field(event, "kind") == Some(USAGE_EVENT_KIND)) + else { + continue; + }; + + let timestamp = number_field(&value, "timestampMs") + .filter(|timestamp| *timestamp > 0) + .unwrap_or(default_timestamp); + let agent = agent_name(agent_event); + let Some(usages) = agent_event.get("modelUsage").and_then(Value::as_array) else { + continue; + }; + + for (usage_index, usage) in usages.iter().enumerate() { + let Some(model_raw) = string_field(usage, "model") else { + continue; + }; + let model_id = pricing::aliases::resolve_alias(model_raw) + .unwrap_or(model_raw) + .to_string(); + let provider_id = provider_from_usage(usage, &model_id); + let tokens = tokens_from_usage(usage); + let cost = float_field(usage, "cost") + .filter(|cost| cost.is_finite() && *cost >= 0.0) + .unwrap_or(0.0); + if tokens.total() == 0 && cost == 0.0 { + continue; + } + + let dedup_key = format!( + "junie:{session_id}:{timestamp}:{model_id}:{}:{}:{}:{}:{}:{:.12}:{usage_index}", + tokens.input, + tokens.output, + tokens.cache_read, + tokens.cache_write, + tokens.reasoning, + cost + ); + if !seen.insert(dedup_key.clone()) { + continue; + } + + let mut message = UnifiedMessage::new_with_agent( + "junie", + model_id, + provider_id, + &session_id, + timestamp, + tokens, + cost, + agent.clone(), + ); + message.dedup_key = Some(dedup_key); + message.duration_ms = number_field(usage, "time").filter(|duration| *duration > 0); + if pending_turn_start { + message.is_turn_start = true; + pending_turn_start = false; + } + messages.push(message); + } + } + + messages +} + +fn session_id_from_path(path: &Path) -> String { + path.parent() + .and_then(Path::file_name) + .and_then(|name| name.to_str()) + .filter(|name| !name.trim().is_empty()) + .unwrap_or("unknown") + .to_string() +} + +fn session_timestamp_from_id(session_id: &str) -> Option { + let mut parts = session_id.split('-'); + if parts.next()? != "session" { + return None; + } + let date = parts.next()?; + let time = parts.next()?; + if date.len() != 6 + || time.len() != 6 + || !date.bytes().all(|byte| byte.is_ascii_digit()) + || !time.bytes().all(|byte| byte.is_ascii_digit()) + { + return None; + } + + let naive = NaiveDateTime::parse_from_str(&format!("{date}{time}"), "%y%m%d%H%M%S").ok()?; + match Local.from_local_datetime(&naive) { + LocalResult::Single(datetime) => Some(datetime.timestamp_millis()), + LocalResult::Ambiguous(earliest, _) => Some(earliest.timestamp_millis()), + LocalResult::None => None, + } +} + +fn event_kind(value: &Value) -> Option<&str> { + string_field(value, "kind") +} + +fn agent_name(agent_event: &Value) -> Option { + let agent = agent_event.get("agent")?; + string_field(agent, "name") + .or_else(|| string_field(agent, "id")) + .map(str::to_string) +} + +fn provider_from_usage(usage: &Value, model_id: &str) -> String { + string_field(usage, "provider") + .and_then(provider_identity::canonical_provider) + .or_else(|| provider_identity::inferred_provider_from_model(model_id).map(str::to_string)) + .unwrap_or_else(|| "junie".to_string()) +} + +fn tokens_from_usage(usage: &Value) -> TokenBreakdown { + TokenBreakdown { + input: first_number_field(usage, &["inputTokens", "input"]), + output: first_number_field(usage, &["outputTokens", "output"]), + cache_read: first_number_field( + usage, + &["cacheInputTokens", "cacheReadInputTokens", "cacheRead"], + ), + cache_write: first_number_field( + usage, + &[ + "cacheCreateTokens", + "cacheCreationInputTokens", + "cacheWrite", + ], + ), + reasoning: first_number_field( + usage, + &["reasoningTokens", "reasoningOutputTokens", "thinkingTokens"], + ), + } +} + +fn string_field<'a>(value: &'a Value, field: &str) -> Option<&'a str> { + value + .get(field) + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) +} + +fn first_number_field(value: &Value, fields: &[&str]) -> i64 { + fields + .iter() + .find_map(|field| number_field(value, field)) + .unwrap_or(0) +} + +fn number_field(value: &Value, field: &str) -> Option { + number_value(value.get(field)?) +} + +fn number_value(value: &Value) -> Option { + if let Some(value) = value.as_i64() { + return Some(value.max(0)); + } + if let Some(value) = value.as_u64() { + return Some(value.min(i64::MAX as u64) as i64); + } + if let Some(value) = value.as_f64() { + return value.is_finite().then_some(value.max(0.0) as i64); + } + value + .as_str() + .and_then(|value| value.trim().parse::().ok()) + .and_then(|value| value.is_finite().then_some(value.max(0.0) as i64)) +} + +fn float_field(value: &Value, field: &str) -> Option { + let value = value.get(field)?; + if let Some(number) = value.as_f64() { + return Some(number); + } + value.as_str()?.trim().parse().ok() +} diff --git a/crates/tokscale-core/src/sessions/mod.rs b/crates/tokscale-core/src/sessions/mod.rs index caf898f9f..18c508b45 100644 --- a/crates/tokscale-core/src/sessions/mod.rs +++ b/crates/tokscale-core/src/sessions/mod.rs @@ -20,6 +20,7 @@ pub mod goose; pub mod grok; pub mod hermes; pub mod jcode; +pub mod junie; pub mod kilo; pub mod kilocode; pub mod kimi; diff --git a/crates/tokscale-core/tests/junie.rs b/crates/tokscale-core/tests/junie.rs new file mode 100644 index 000000000..92bc9a410 --- /dev/null +++ b/crates/tokscale-core/tests/junie.rs @@ -0,0 +1,169 @@ +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; + +use tokscale_core::pricing::{litellm::ModelPricing, PricingService}; +use tokscale_core::scanner::ScannerSettings; +use tokscale_core::sessions::junie::parse_junie_file; +use tokscale_core::{ + parse_local_clients, parse_local_unified_messages_with_pricing, ClientId, LocalParseOptions, +}; + +fn write_junie_session(home: &Path, session_id: &str, events: &str) -> PathBuf { + let session_dir = home.join(".junie/sessions").join(session_id); + fs::create_dir_all(&session_dir).unwrap(); + let events_path = session_dir.join("events.jsonl"); + fs::write(&events_path, events).unwrap(); + events_path +} + +fn junie_options(home: &Path) -> LocalParseOptions { + LocalParseOptions { + home_dir: Some(home.to_str().unwrap().to_string()), + use_env_roots: false, + clients: Some(vec!["junie".to_string()]), + since: None, + until: None, + year: None, + scanner_settings: ScannerSettings::default(), + } +} + +fn make_pricing_service() -> PricingService { + let mut litellm_data = HashMap::new(); + litellm_data.insert( + "junie-test-model".to_string(), + ModelPricing { + input_cost_per_token: Some(0.001), + output_cost_per_token: Some(0.002), + cache_read_input_token_cost: Some(0.0001), + cache_creation_input_token_cost: Some(0.0005), + ..Default::default() + }, + ); + PricingService::new(litellm_data, HashMap::new()) +} + +#[test] +fn test_junie_parser_reads_model_usage_cost_tokens_and_turn_start() { + let home_dir = tempfile::TempDir::new().unwrap(); + let home = home_dir.path(); + let session_id = "session-260618-191750-jnus"; + + let events_path = write_junie_session( + home, + session_id, + concat!( + r#"{"kind":"UserPromptEvent","timestampMs":1781803079339}"#, + "\n", + r#"{"kind":"SessionA2uxEvent","event":{"state":"IN_PROGRESS","agentEvent":{"kind":"LlmResponseMetadataEvent","agent":{"kind":"MainAgent","id":"main","name":"main"},"modelUsage":[{"model":"gpt-4.1-2025-04-14","cost":0.42,"inputTokens":100,"cacheInputTokens":20,"cacheCreateTokens":5,"outputTokens":10,"reasoningTokens":3,"time":2500}]}},"timestampMs":1781803080555}"#, + "\n", + ), + ); + + let messages = parse_junie_file(&events_path); + + assert_eq!(messages.len(), 1); + let message = &messages[0]; + assert_eq!(message.client, "junie"); + assert_eq!(message.session_id, session_id); + assert_eq!(message.model_id, "gpt-4.1-2025-04-14"); + assert_eq!(message.provider_id, "openai"); + assert_eq!(message.tokens.input, 100); + assert_eq!(message.tokens.cache_read, 20); + assert_eq!(message.tokens.cache_write, 5); + assert_eq!(message.tokens.output, 10); + assert_eq!(message.tokens.reasoning, 3); + assert_eq!(message.cost, 0.42); + assert_eq!(message.duration_ms, Some(2500)); + assert_eq!(message.agent.as_deref(), Some("main")); + assert!(message.is_turn_start); +} + +#[test] +fn test_junie_parser_infers_provider_from_model() { + let home_dir = tempfile::TempDir::new().unwrap(); + let home = home_dir.path(); + + let events_path = write_junie_session( + home, + "session-provider-inference", + concat!( + r#"{"kind":"SessionA2uxEvent","event":{"agentEvent":{"kind":"LlmResponseMetadataEvent","agent":{"id":"main"},"modelUsage":[{"model":"claude-opus-4-8","cost":0.5,"inputTokens":10,"outputTokens":2}]}},"timestampMs":1781803080555}"#, + "\n", + ), + ); + + let messages = parse_junie_file(&events_path); + + assert_eq!(messages.len(), 1); + assert_eq!(messages[0].provider_id, "anthropic"); +} + +#[test] +fn test_junie_parser_uses_session_id_timestamp_when_event_has_no_timestamp() { + let home_dir = tempfile::TempDir::new().unwrap(); + let home = home_dir.path(); + + let events_path = write_junie_session( + home, + "session-260618-191750-jnus", + concat!( + r#"{"kind":"SessionA2uxEvent","event":{"state":"IN_PROGRESS","agentEvent":{"kind":"LlmResponseMetadataEvent","modelUsage":[{"model":"gpt-4.1-2025-04-14","cost":0.5,"inputTokens":10,"outputTokens":2}]}}}"#, + "\n", + ), + ); + + let messages = parse_junie_file(&events_path); + + assert_eq!(messages.len(), 1); + assert_eq!(messages[0].date, "2026-06-18"); + assert!(messages[0].timestamp > 0); +} + +#[tokio::test] +async fn test_junie_end_to_end_preserves_embedded_cost_and_prices_missing_cost() { + let home_dir = tempfile::TempDir::new().unwrap(); + let home = home_dir.path(); + + write_junie_session( + home, + "session-priced", + concat!( + r#"{"kind":"UserPromptEvent","timestampMs":1781803079339}"#, + "\n", + r#"{"kind":"SessionA2uxEvent","event":{"agentEvent":{"kind":"LlmResponseMetadataEvent","agent":{"id":"main"},"modelUsage":[{"model":"junie-test-model","cost":0.123,"inputTokens":1000,"outputTokens":250},{"model":"junie-test-model","inputTokens":10,"cacheInputTokens":2,"cacheCreateTokens":3,"outputTokens":5,"reasoningTokens":1}]}},"timestampMs":1781803080555}"#, + "\n", + ), + ); + + let pricing = make_pricing_service(); + let messages = parse_local_unified_messages_with_pricing(junie_options(home), Some(&pricing)) + .await + .unwrap(); + + assert_eq!(messages.len(), 2); + let embedded = messages + .iter() + .find(|message| (message.cost - 0.123).abs() < 1e-10) + .expect("embedded-cost Junie message was not preserved"); + assert_eq!(embedded.tokens.input, 1000); + + let priced = messages + .iter() + .find(|message| message.tokens.input == 10) + .expect("missing-cost Junie message was not repriced"); + let expected = 10.0 * 0.001 + (5.0 + 1.0) * 0.002 + 2.0 * 0.0001 + 3.0 * 0.0005; + assert!((priced.cost - expected).abs() < 1e-10); + + let parsed = parse_local_clients(junie_options(home)).unwrap(); + assert_eq!(parsed.counts.get(ClientId::Junie), 2); + assert_eq!( + parsed + .messages + .iter() + .filter(|message| message.client == "junie") + .count(), + 2 + ); +} diff --git a/packages/frontend/src/lib/constants.ts b/packages/frontend/src/lib/constants.ts index 87f16b26c..22d565120 100644 --- a/packages/frontend/src/lib/constants.ts +++ b/packages/frontend/src/lib/constants.ts @@ -60,6 +60,7 @@ export const SOURCE_DISPLAY_NAMES: Record = { jcode: "Jcode", commandcode: "Command Code", micode: "MiMo Code", + junie: "Junie", }; // Client logos from GitHub CDN (public repo) @@ -99,6 +100,7 @@ export const SOURCE_LOGOS: Record = { commandcode: "https://raw.githubusercontent.com/CommandCodeAI/command-code/main/.github/commandcode/logo/command-code-logo-black-bg.png", micode: "https://github.com/XiaomiMiMo.png", + junie: "https://github.com/JetBrains.png", }; export const SOURCE_COLORS: Record = { @@ -135,6 +137,7 @@ export const SOURCE_COLORS: Record = { jcode: "#F59E0B", commandcode: "#A855F7", micode: "#FF6900", + junie: "#7B61FF", }; export const SOURCE_TEXT_COLORS: Partial> = { diff --git a/packages/frontend/src/lib/types.ts b/packages/frontend/src/lib/types.ts index 6df43e551..fbcf13696 100644 --- a/packages/frontend/src/lib/types.ts +++ b/packages/frontend/src/lib/types.ts @@ -32,6 +32,7 @@ export const SUPPORTED_CLIENT_TYPES = [ "commandcode", "micode", "antigravity-cli", + "junie", ] as const; export type CcMirrorClientType = `cc-mirror/${string}`; From e60a16f5bc8c436b972afef58b87ebf6bd002765 Mon Sep 17 00:00:00 2001 From: Junho Yeo Date: Sun, 21 Jun 2026 08:32:11 +0900 Subject: [PATCH 2/2] docs(readme): sync ja/ko/zh with the new Junie client entries --- README.ja.md | 6 ++++-- README.ko.md | 6 ++++-- README.zh-cn.md | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.ja.md b/README.ja.md index 85f68d297..93ec0be4e 100644 --- a/README.ja.md +++ b/README.ja.md @@ -85,6 +85,7 @@ | Gajae-Code | [gajae-code (gjc)](https://github.com/Yeachan-Heo/gajae-code) | `~/.gjc/agent/sessions/`(`GJC_CODING_AGENT_DIR`、`GJC_CONFIG_DIR`、`PI_CONFIG_DIR` でオーバーライド可能;Linux/macOS では `$XDG_DATA_HOME/gjc/sessions/` も解決) | ✅ 対応 | | Jcode | [Jcode](https://github.com/1jehuang/jcode) | `~/.jcode/sessions/session_*.json` + `session_*.journal.jsonl` サイドカー(`JCODE_HOME` で上書き可) | ✅ 対応 | | MiMo Code | [MiMo Code](https://github.com/XiaomiMiMo/MiMo) | `~/.local/share/micode/mimocode.db`(XDG データディレクトリ;SQLite) | ✅ 対応 | +| Junie | [Junie](https://www.jetbrains.com/junie/) | `~/.junie/sessions/*/events.jsonl` | ✅ 対応 | | Command Code | [Command Code](https://github.com/CommandCodeAI/command-code) | `~/.commandcode/projects/**/*.jsonl` | ✅ 対応 | | Synthetic | [Synthetic](https://synthetic.new/) | `hf:`モデルや`synthetic`プロバイダを検出して他ソースから再帰属(+ [Octofriend](https://github.com/synthetic-lab/octofriend): `~/.local/share/octofriend/sqlite.db`) | ✅ 対応 | @@ -155,7 +156,7 @@ AI支援開発の時代において、**トークンは新しいエネルギー* - 9色テーマのGitHubスタイル貢献グラフ - リアルタイムフィルタリングとソート - ゼロフリッカーレンダリング -- **マルチプラットフォームサポート** - OpenCode、Claude Code、Codex CLI、Copilot CLI、Cursor IDE、Gemini CLI、Amp、Codebuff、Droid、OpenClaw、Hermes Agent、Pi、Kimi CLI、Qwen CLI、Roo Code、Kilo、Mux、Kilo CLI、Crush、Goose、Antigravity、Antigravity CLI、Zed、Kiro、Trae、Warp/Oz、Cline、Gajae-Code、Grok Build、Jcode、MiMo Code、Command Code、Synthetic全体の使用量追跡 +- **マルチプラットフォームサポート** - OpenCode、Claude Code、Codex CLI、Copilot CLI、Cursor IDE、Gemini CLI、Amp、Codebuff、Droid、OpenClaw、Hermes Agent、Pi、Kimi CLI、Qwen CLI、Roo Code、Kilo、Mux、Kilo CLI、Crush、Goose、Antigravity、Antigravity CLI、Zed、Kiro、Trae、Warp/Oz、Cline、Gajae-Code、Grok Build、Jcode、MiMo Code、Command Code、Junie、Synthetic全体の使用量追跡 - **リアルタイム価格** - 1時間ディスクキャッシュ付きでLiteLLMから現在の価格を取得;OpenRouter自動フォールバックと新規モデル向けCursor価格サポート - **詳細な内訳** - 入力、出力、キャッシュ読み書き、推論トークン追跡 - **ネイティブRustコア** - 10倍高速な処理のため、すべての解析と集計をRustで実行 @@ -334,7 +335,7 @@ tokscale --client synthetic tokscale --client opencode,claude --week --json ``` -利用可能な値: `opencode`, `claude`, `codex`, `copilot`, `gemini`, `cursor`, `amp`, `codebuff`, `droid`, `openclaw`, `hermes`, `pi`, `kimi`, `qwen`, `roocode`, `kilocode`, `kilo`, `mux`, `crush`, `goose`, `antigravity`, `antigravity-cli`, `zed`, `kiro`, `trae`, `warp`, `cline`, `gjc`, `grok`, `jcode`, `micode`, `commandcode`, `synthetic`。 +利用可能な値: `opencode`, `claude`, `codex`, `copilot`, `gemini`, `cursor`, `amp`, `codebuff`, `droid`, `openclaw`, `hermes`, `pi`, `kimi`, `qwen`, `roocode`, `kilocode`, `kilo`, `mux`, `crush`, `goose`, `antigravity`, `antigravity-cli`, `zed`, `kiro`, `trae`, `warp`, `cline`, `gjc`, `grok`, `jcode`, `micode`, `commandcode`, `junie`, `synthetic`。 > **非推奨のお知らせ**: 既存の単一クライアントフラグ(`--opencode`、`--claude`、`--codex` など)は後方互換性のため引き続き動作しますが、`--help` から非表示となり、次のメジャーリリースで削除予定です。可能な限り `--client` への移行を推奨します。インタラクティブな端末で旧フラグを使用すると 1 行の警告が表示されます。 @@ -1071,6 +1072,7 @@ AIコーディングツールはクロスプラットフォームの場所にセ | Grok Build | `~/.grok/sessions/` | `%USERPROFILE%\.grok\sessions\` | `GROK_HOME` 環境変数で設定可能。`updates.jsonl` セッション更新を解析 | | MiMo Code | `~/.local/share/micode/` | `%USERPROFILE%\.local\share\micode\` | XDG データディレクトリを使用;SQLite データベース `mimocode.db` | | Gajae-Code | `~/.gjc/agent/sessions/` | `%USERPROFILE%\.gjc\agent\sessions\` | `GJC_CODING_AGENT_DIR` で設定可能(`GJC_CONFIG_DIR`/`PI_CONFIG_DIR` も解決;Linux/macOS では `$XDG_DATA_HOME/gjc/sessions/` も対応) | +| Junie | `~/.junie/sessions/` | `%USERPROFILE%\.junie\sessions\` | すべてのプラットフォームで同じホーム相対パス;`events.jsonl` 使用イベントを解析 | | Synthetic | 他ソースから再帰属 | 他ソースから再帰属 | `hf:`モデル + `synthetic`プロバイダを検出 | > **注**: Windowsでは`~`は`%USERPROFILE%`に展開されます(例:`C:\Users\ユーザー名`)。これらのツールは`%APPDATA%`のようなWindowsネイティブパスではなく、クロスプラットフォームの一貫性のためにUnixスタイルのパス(`.local/share`など)を意図的に使用しています。 diff --git a/README.ko.md b/README.ko.md index 70741b58a..ae9894422 100644 --- a/README.ko.md +++ b/README.ko.md @@ -85,6 +85,7 @@ | Gajae-Code | [gajae-code (gjc)](https://github.com/Yeachan-Heo/gajae-code) | `~/.gjc/agent/sessions/` (`GJC_CODING_AGENT_DIR`, `GJC_CONFIG_DIR`, `PI_CONFIG_DIR`로 오버라이드 가능; Linux/macOS에서는 `$XDG_DATA_HOME/gjc/sessions/`도 확인) | ✅ 지원 | | Jcode | [Jcode](https://github.com/1jehuang/jcode) | `~/.jcode/sessions/session_*.json` + `session_*.journal.jsonl` 사이드카 (`JCODE_HOME`으로 재정의 가능) | ✅ 지원 | | MiMo Code | [MiMo Code](https://github.com/XiaomiMiMo/MiMo) | `~/.local/share/micode/mimocode.db` (XDG 데이터 디렉토리; SQLite) | ✅ 지원 | +| Junie | [Junie](https://www.jetbrains.com/junie/) | `~/.junie/sessions/*/events.jsonl` | ✅ 지원 | | Command Code | [Command Code](https://github.com/CommandCodeAI/command-code) | `~/.commandcode/projects/**/*.jsonl` | ✅ 지원 | | Synthetic | [Synthetic](https://synthetic.new/) | `hf:` 모델/`synthetic` provider 감지로 다른 소스에서 재귀속 (+ [Octofriend](https://github.com/synthetic-lab/octofriend): `~/.local/share/octofriend/sqlite.db`) | ✅ 지원 | @@ -155,7 +156,7 @@ AI 지원 개발 시대에 **토큰은 새로운 에너지**입니다. 토큰은 - 9가지 테마의 GitHub 스타일 기여 그래프 - 실시간 필터링 및 정렬 - 깜빡임 없는 렌더링 -- **멀티 플랫폼 지원** - OpenCode, Claude Code, Codex CLI, Copilot CLI, Cursor IDE, Gemini CLI, Amp, Codebuff, Droid, OpenClaw, Hermes Agent, Pi, Kimi CLI, Qwen CLI, Roo Code, Kilo, Mux, Kilo CLI, Crush, Goose, Antigravity, Antigravity CLI, Zed, Kiro, Trae, Warp/Oz, Cline, Gajae-Code, Grok Build, Jcode, MiMo Code, Command Code, Synthetic 사용량 통합 추적 +- **멀티 플랫폼 지원** - OpenCode, Claude Code, Codex CLI, Copilot CLI, Cursor IDE, Gemini CLI, Amp, Codebuff, Droid, OpenClaw, Hermes Agent, Pi, Kimi CLI, Qwen CLI, Roo Code, Kilo, Mux, Kilo CLI, Crush, Goose, Antigravity, Antigravity CLI, Zed, Kiro, Trae, Warp/Oz, Cline, Gajae-Code, Grok Build, Jcode, MiMo Code, Command Code, Junie, Synthetic 사용량 통합 추적 - **실시간 가격 반영** - LiteLLM에서 최신 가격을 가져와(디스크 캐시 1시간) 비용 계산; OpenRouter 자동 폴백 및 신규 모델용 Cursor 가격 지원 - **상세 분석** - 입력, 출력, 캐시 읽기/쓰기, 추론 토큰까지 추적 - **네이티브 Rust 코어** - 모든 파싱과 집계를 Rust로 처리해 최대 10배 빠른 성능 @@ -334,7 +335,7 @@ tokscale --client synthetic tokscale --client opencode,claude --week --json ``` -가능한 값: `opencode`, `claude`, `codex`, `copilot`, `gemini`, `cursor`, `amp`, `codebuff`, `droid`, `openclaw`, `hermes`, `pi`, `kimi`, `qwen`, `roocode`, `kilocode`, `kilo`, `mux`, `crush`, `goose`, `antigravity`, `antigravity-cli`, `zed`, `kiro`, `trae`, `warp`, `cline`, `gjc`, `grok`, `jcode`, `micode`, `commandcode`, `synthetic`. +가능한 값: `opencode`, `claude`, `codex`, `copilot`, `gemini`, `cursor`, `amp`, `codebuff`, `droid`, `openclaw`, `hermes`, `pi`, `kimi`, `qwen`, `roocode`, `kilocode`, `kilo`, `mux`, `crush`, `goose`, `antigravity`, `antigravity-cli`, `zed`, `kiro`, `trae`, `warp`, `cline`, `gjc`, `grok`, `jcode`, `micode`, `commandcode`, `junie`, `synthetic`. > **Deprecation 안내**: 기존 단일 클라이언트 플래그 (`--opencode`, `--claude`, `--codex` 등)는 하위 호환성을 위해 여전히 동작하지만 `--help`에서 숨겨졌으며 다음 메이저 릴리스에서 제거됩니다. 가능한 한 `--client`로 마이그레이션하세요. 인터랙티브 터미널에서 레거시 플래그를 사용하면 한 줄 경고가 출력됩니다. @@ -1071,6 +1072,7 @@ AI 코딩 도구들은 크로스 플랫폼 위치에 세션 데이터를 저장 | Grok Build | `~/.grok/sessions/` | `%USERPROFILE%\.grok\sessions\` | `GROK_HOME` 환경변수로 설정 가능; `updates.jsonl` 세션 업데이트 파싱 | | MiMo Code | `~/.local/share/micode/` | `%USERPROFILE%\.local\share\micode\` | XDG 데이터 디렉토리 사용; SQLite 데이터베이스 `mimocode.db` | | Gajae-Code | `~/.gjc/agent/sessions/` | `%USERPROFILE%\.gjc\agent\sessions\` | `GJC_CODING_AGENT_DIR`로 설정 가능 (`GJC_CONFIG_DIR`/`PI_CONFIG_DIR`도 지원; Linux/macOS에서는 `$XDG_DATA_HOME/gjc/sessions/`도 확인) | +| Junie | `~/.junie/sessions/` | `%USERPROFILE%\.junie\sessions\` | 모든 플랫폼에서 동일한 home 상대 경로 사용; `events.jsonl` 사용 이벤트 파싱 | | Synthetic | 다른 소스에서 재귀속 | 다른 소스에서 재귀속 | `hf:` 모델 접두사 + `synthetic` provider 감지 | > **참고**: Windows에서 `~`는 `%USERPROFILE%`로 확장됩니다 (예: `C:\Users\사용자이름`). 이러한 도구들은 `%APPDATA%`와 같은 Windows 기본 경로 대신 크로스 플랫폼 일관성을 위해 의도적으로 Unix 스타일 경로(`.local/share` 등)를 사용합니다. diff --git a/README.zh-cn.md b/README.zh-cn.md index ffb8c7a57..5a46ce2a4 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -85,6 +85,7 @@ | Gajae-Code | [gajae-code (gjc)](https://github.com/Yeachan-Heo/gajae-code) | `~/.gjc/agent/sessions/`(可通过 `GJC_CODING_AGENT_DIR`、`GJC_CONFIG_DIR`、`PI_CONFIG_DIR` 覆盖;Linux/macOS 上 `$XDG_DATA_HOME/gjc/sessions/` 亦支持) | ✅ 支持 | | Jcode | [Jcode](https://github.com/1jehuang/jcode) | `~/.jcode/sessions/session_*.json` + `session_*.journal.jsonl` sidecar(可通过 `JCODE_HOME` 覆盖) | ✅ 支持 | | MiMo Code | [MiMo Code](https://github.com/XiaomiMiMo/MiMo) | `~/.local/share/micode/mimocode.db`(XDG 数据目录;SQLite) | ✅ 支持 | +| Junie | [Junie](https://www.jetbrains.com/junie/) | `~/.junie/sessions/*/events.jsonl` | ✅ 支持 | | Command Code | [Command Code](https://github.com/CommandCodeAI/command-code) | `~/.commandcode/projects/**/*.jsonl` | ✅ 支持 | | Synthetic | [Synthetic](https://synthetic.new/) | 通过 `hf:` 模型前缀或 `synthetic` provider 从其他来源重归属(+ [Octofriend](https://github.com/synthetic-lab/octofriend): `~/.local/share/octofriend/sqlite.db`) | ✅ 支持 | @@ -155,7 +156,7 @@ - 9 种颜色主题的 GitHub 风格贡献图 - 实时筛选和排序 - 零闪烁渲染 -- **多平台支持** - 跟踪 OpenCode、Claude Code、Codex CLI、Copilot CLI、Cursor IDE、Gemini CLI、Amp、Codebuff、Droid、OpenClaw、Hermes Agent、Pi、Kimi CLI、Qwen CLI、Roo Code、Kilo、Mux、Kilo CLI、Crush、Goose、Antigravity、Antigravity CLI、Zed、Kiro、Trae、Warp/Oz、Cline、Gajae-Code、Grok Build、Jcode、MiMo Code、Command Code 和 Synthetic 的使用情况 +- **多平台支持** - 跟踪 OpenCode、Claude Code、Codex CLI、Copilot CLI、Cursor IDE、Gemini CLI、Amp、Codebuff、Droid、OpenClaw、Hermes Agent、Pi、Kimi CLI、Qwen CLI、Roo Code、Kilo、Mux、Kilo CLI、Crush、Goose、Antigravity、Antigravity CLI、Zed、Kiro、Trae、Warp/Oz、Cline、Gajae-Code、Grok Build、Jcode、MiMo Code、Command Code、Junie 和 Synthetic 的使用情况 - **实时定价** - 从 LiteLLM 获取当前价格,带 1 小时磁盘缓存;OpenRouter 自动回退和新模型的 Cursor 定价支持 - **详细分解** - 输入、输出、缓存读写和推理 Token 跟踪 - **原生 Rust 核心** - 所有解析和聚合在 Rust 中完成,处理速度提升 10 倍 @@ -335,7 +336,7 @@ tokscale --client synthetic tokscale --client opencode,claude --week --json ``` -可用值:`opencode`、`claude`、`codex`、`copilot`、`gemini`、`cursor`、`amp`、`codebuff`、`droid`、`openclaw`、`hermes`、`pi`、`kimi`、`qwen`、`roocode`、`kilocode`、`kilo`、`mux`、`crush`、`goose`、`antigravity`、`antigravity-cli`、`zed`、`kiro`、`trae`、`warp`、`cline`、`gjc`、`grok`、`jcode`、`micode`、`commandcode`、`synthetic`。 +可用值:`opencode`、`claude`、`codex`、`copilot`、`gemini`、`cursor`、`amp`、`codebuff`、`droid`、`openclaw`、`hermes`、`pi`、`kimi`、`qwen`、`roocode`、`kilocode`、`kilo`、`mux`、`crush`、`goose`、`antigravity`、`antigravity-cli`、`zed`、`kiro`、`trae`、`warp`、`cline`、`gjc`、`grok`、`jcode`、`micode`、`commandcode`、`junie`、`synthetic`。 > **弃用通知**:旧的单客户端选项(`--opencode`、`--claude`、`--codex` 等)出于向后兼容仍然可用,但已从 `--help` 中隐藏,将在下一个主要版本中移除。请尽量迁移到 `--client`。在交互式终端中使用旧选项时会输出一行警告。 @@ -1075,6 +1076,7 @@ AI 编程工具将会话数据存储在跨平台位置。大多数工具在所 | Grok Build | `~/.grok/sessions/` | `%USERPROFILE%\.grok\sessions\` | 可通过 `GROK_HOME` 环境变量配置;解析 `updates.jsonl` 会话更新 | | MiMo Code | `~/.local/share/micode/` | `%USERPROFILE%\.local\share\micode\` | 使用 XDG 数据目录;SQLite 数据库 `mimocode.db` | | Gajae-Code | `~/.gjc/agent/sessions/` | `%USERPROFILE%\.gjc\agent\sessions\` | 可通过 `GJC_CODING_AGENT_DIR`(也可用 `GJC_CONFIG_DIR`/`PI_CONFIG_DIR`;Linux/macOS 上 `$XDG_DATA_HOME/gjc/sessions/` 亦支持)配置 | +| Junie | `~/.junie/sessions/` | `%USERPROFILE%\.junie\sessions\` | 所有平台使用相同的 home 相对路径;解析 `events.jsonl` 使用事件 | | Synthetic | 从其他来源重归属 | 从其他来源重归属 | 检测 `hf:` 模型前缀 + `synthetic` provider | > **注意**:在 Windows 上,`~` 扩展为 `%USERPROFILE%`(例如 `C:\Users\用户名`)。这些工具故意使用 Unix 风格的路径(如 `.local/share`)而不是 Windows 原生路径(如 `%APPDATA%`),以实现跨平台一致性。