@hasna/terminal 2.3.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/App.js +404 -0
- package/dist/Browse.js +79 -0
- package/dist/FuzzyPicker.js +47 -0
- package/dist/Onboarding.js +51 -0
- package/dist/Spinner.js +12 -0
- package/dist/StatusBar.js +49 -0
- package/dist/ai.js +322 -0
- package/dist/cache.js +41 -0
- package/dist/cli.js +64 -16
- package/dist/command-rewriter.js +64 -0
- package/dist/command-validator.js +86 -0
- package/dist/compression.js +107 -0
- package/dist/context-hints.js +275 -0
- package/dist/diff-cache.js +107 -0
- package/dist/discover.js +212 -0
- package/dist/economy.js +123 -0
- package/dist/expand-store.js +38 -0
- package/dist/file-cache.js +72 -0
- package/dist/file-index.js +62 -0
- package/dist/history.js +62 -0
- package/dist/lazy-executor.js +54 -0
- package/dist/line-dedup.js +59 -0
- package/dist/loop-detector.js +75 -0
- package/dist/mcp/install.js +98 -0
- package/dist/mcp/server.js +569 -0
- package/dist/noise-filter.js +86 -0
- package/dist/output-processor.js +129 -0
- package/dist/output-router.js +41 -0
- package/dist/output-store.js +111 -0
- package/dist/parsers/base.js +2 -0
- package/dist/parsers/build.js +64 -0
- package/dist/parsers/errors.js +101 -0
- package/dist/parsers/files.js +78 -0
- package/dist/parsers/git.js +99 -0
- package/dist/parsers/index.js +48 -0
- package/dist/parsers/tests.js +89 -0
- package/dist/providers/anthropic.js +39 -0
- package/dist/providers/base.js +4 -0
- package/dist/providers/cerebras.js +95 -0
- package/dist/providers/groq.js +95 -0
- package/dist/providers/index.js +73 -0
- package/dist/providers/xai.js +95 -0
- package/dist/recipes/model.js +20 -0
- package/dist/recipes/storage.js +136 -0
- package/dist/search/content-search.js +68 -0
- package/dist/search/file-search.js +61 -0
- package/dist/search/filters.js +34 -0
- package/dist/search/index.js +5 -0
- package/dist/search/semantic.js +320 -0
- package/dist/session-boot.js +59 -0
- package/dist/session-context.js +55 -0
- package/dist/sessions-db.js +173 -0
- package/dist/smart-display.js +286 -0
- package/dist/snapshots.js +51 -0
- package/dist/supervisor.js +112 -0
- package/dist/test-watchlist.js +131 -0
- package/dist/tool-profiles.js +122 -0
- package/dist/tree.js +94 -0
- package/dist/usage-cache.js +65 -0
- package/package.json +8 -1
- package/src/ai.ts +8 -0
- package/src/cli.tsx +57 -18
- package/src/output-processor.ts +6 -1
- package/src/output-store.ts +58 -12
- package/src/tool-profiles.ts +139 -0
- package/.claude/scheduled_tasks.lock +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -20
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -14
- package/CONTRIBUTING.md +0 -80
- package/benchmarks/benchmark.mjs +0 -115
- package/imported_modules.txt +0 -0
- package/temp/rtk/.claude/agents/code-reviewer.md +0 -221
- package/temp/rtk/.claude/agents/debugger.md +0 -519
- package/temp/rtk/.claude/agents/rtk-testing-specialist.md +0 -461
- package/temp/rtk/.claude/agents/rust-rtk.md +0 -511
- package/temp/rtk/.claude/agents/technical-writer.md +0 -355
- package/temp/rtk/.claude/commands/diagnose.md +0 -352
- package/temp/rtk/.claude/commands/test-routing.md +0 -362
- package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +0 -16
- package/temp/rtk/.claude/hooks/rtk-rewrite.sh +0 -70
- package/temp/rtk/.claude/hooks/rtk-suggest.sh +0 -152
- package/temp/rtk/.claude/rules/cli-testing.md +0 -526
- package/temp/rtk/.claude/skills/issue-triage/SKILL.md +0 -348
- package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +0 -134
- package/temp/rtk/.claude/skills/performance.md +0 -435
- package/temp/rtk/.claude/skills/pr-triage/SKILL.md +0 -315
- package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +0 -71
- package/temp/rtk/.claude/skills/repo-recap.md +0 -206
- package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +0 -78
- package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +0 -124
- package/temp/rtk/.claude/skills/security-guardian.md +0 -503
- package/temp/rtk/.claude/skills/ship.md +0 -404
- package/temp/rtk/.github/workflows/benchmark.yml +0 -34
- package/temp/rtk/.github/workflows/dco-check.yaml +0 -12
- package/temp/rtk/.github/workflows/release-please.yml +0 -51
- package/temp/rtk/.github/workflows/release.yml +0 -343
- package/temp/rtk/.github/workflows/security-check.yml +0 -135
- package/temp/rtk/.github/workflows/validate-docs.yml +0 -78
- package/temp/rtk/.release-please-manifest.json +0 -3
- package/temp/rtk/ARCHITECTURE.md +0 -1491
- package/temp/rtk/CHANGELOG.md +0 -640
- package/temp/rtk/CLAUDE.md +0 -605
- package/temp/rtk/CONTRIBUTING.md +0 -199
- package/temp/rtk/Cargo.lock +0 -1668
- package/temp/rtk/Cargo.toml +0 -64
- package/temp/rtk/Formula/rtk.rb +0 -43
- package/temp/rtk/INSTALL.md +0 -390
- package/temp/rtk/LICENSE +0 -21
- package/temp/rtk/README.md +0 -386
- package/temp/rtk/README_es.md +0 -159
- package/temp/rtk/README_fr.md +0 -197
- package/temp/rtk/README_ja.md +0 -159
- package/temp/rtk/README_ko.md +0 -159
- package/temp/rtk/README_zh.md +0 -167
- package/temp/rtk/ROADMAP.md +0 -15
- package/temp/rtk/SECURITY.md +0 -217
- package/temp/rtk/TEST_EXEC_TIME.md +0 -102
- package/temp/rtk/build.rs +0 -57
- package/temp/rtk/docs/AUDIT_GUIDE.md +0 -432
- package/temp/rtk/docs/FEATURES.md +0 -1410
- package/temp/rtk/docs/TROUBLESHOOTING.md +0 -309
- package/temp/rtk/docs/filter-workflow.md +0 -102
- package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
- package/temp/rtk/docs/tracking.md +0 -583
- package/temp/rtk/hooks/opencode-rtk.ts +0 -39
- package/temp/rtk/hooks/rtk-awareness.md +0 -29
- package/temp/rtk/hooks/rtk-rewrite.sh +0 -61
- package/temp/rtk/hooks/test-rtk-rewrite.sh +0 -442
- package/temp/rtk/install.sh +0 -124
- package/temp/rtk/release-please-config.json +0 -10
- package/temp/rtk/scripts/benchmark.sh +0 -592
- package/temp/rtk/scripts/check-installation.sh +0 -162
- package/temp/rtk/scripts/install-local.sh +0 -37
- package/temp/rtk/scripts/rtk-economics.sh +0 -137
- package/temp/rtk/scripts/test-all.sh +0 -561
- package/temp/rtk/scripts/test-aristote.sh +0 -227
- package/temp/rtk/scripts/test-tracking.sh +0 -79
- package/temp/rtk/scripts/update-readme-metrics.sh +0 -32
- package/temp/rtk/scripts/validate-docs.sh +0 -73
- package/temp/rtk/src/aws_cmd.rs +0 -880
- package/temp/rtk/src/binlog.rs +0 -1645
- package/temp/rtk/src/cargo_cmd.rs +0 -1727
- package/temp/rtk/src/cc_economics.rs +0 -1157
- package/temp/rtk/src/ccusage.rs +0 -340
- package/temp/rtk/src/config.rs +0 -187
- package/temp/rtk/src/container.rs +0 -855
- package/temp/rtk/src/curl_cmd.rs +0 -134
- package/temp/rtk/src/deps.rs +0 -268
- package/temp/rtk/src/diff_cmd.rs +0 -367
- package/temp/rtk/src/discover/mod.rs +0 -274
- package/temp/rtk/src/discover/provider.rs +0 -388
- package/temp/rtk/src/discover/registry.rs +0 -2022
- package/temp/rtk/src/discover/report.rs +0 -202
- package/temp/rtk/src/discover/rules.rs +0 -667
- package/temp/rtk/src/display_helpers.rs +0 -402
- package/temp/rtk/src/dotnet_cmd.rs +0 -1771
- package/temp/rtk/src/dotnet_format_report.rs +0 -133
- package/temp/rtk/src/dotnet_trx.rs +0 -593
- package/temp/rtk/src/env_cmd.rs +0 -204
- package/temp/rtk/src/filter.rs +0 -462
- package/temp/rtk/src/filters/README.md +0 -52
- package/temp/rtk/src/filters/ansible-playbook.toml +0 -34
- package/temp/rtk/src/filters/basedpyright.toml +0 -47
- package/temp/rtk/src/filters/biome.toml +0 -45
- package/temp/rtk/src/filters/brew-install.toml +0 -37
- package/temp/rtk/src/filters/composer-install.toml +0 -40
- package/temp/rtk/src/filters/df.toml +0 -16
- package/temp/rtk/src/filters/dotnet-build.toml +0 -64
- package/temp/rtk/src/filters/du.toml +0 -16
- package/temp/rtk/src/filters/fail2ban-client.toml +0 -15
- package/temp/rtk/src/filters/gcc.toml +0 -49
- package/temp/rtk/src/filters/gcloud.toml +0 -22
- package/temp/rtk/src/filters/hadolint.toml +0 -24
- package/temp/rtk/src/filters/helm.toml +0 -29
- package/temp/rtk/src/filters/iptables.toml +0 -27
- package/temp/rtk/src/filters/jj.toml +0 -28
- package/temp/rtk/src/filters/jq.toml +0 -24
- package/temp/rtk/src/filters/make.toml +0 -41
- package/temp/rtk/src/filters/markdownlint.toml +0 -24
- package/temp/rtk/src/filters/mix-compile.toml +0 -27
- package/temp/rtk/src/filters/mix-format.toml +0 -15
- package/temp/rtk/src/filters/mvn-build.toml +0 -44
- package/temp/rtk/src/filters/oxlint.toml +0 -43
- package/temp/rtk/src/filters/ping.toml +0 -63
- package/temp/rtk/src/filters/pio-run.toml +0 -40
- package/temp/rtk/src/filters/poetry-install.toml +0 -50
- package/temp/rtk/src/filters/pre-commit.toml +0 -35
- package/temp/rtk/src/filters/ps.toml +0 -16
- package/temp/rtk/src/filters/quarto-render.toml +0 -41
- package/temp/rtk/src/filters/rsync.toml +0 -48
- package/temp/rtk/src/filters/shellcheck.toml +0 -27
- package/temp/rtk/src/filters/shopify-theme.toml +0 -29
- package/temp/rtk/src/filters/skopeo.toml +0 -45
- package/temp/rtk/src/filters/sops.toml +0 -16
- package/temp/rtk/src/filters/ssh.toml +0 -44
- package/temp/rtk/src/filters/stat.toml +0 -34
- package/temp/rtk/src/filters/swift-build.toml +0 -41
- package/temp/rtk/src/filters/systemctl-status.toml +0 -33
- package/temp/rtk/src/filters/terraform-plan.toml +0 -35
- package/temp/rtk/src/filters/tofu-fmt.toml +0 -16
- package/temp/rtk/src/filters/tofu-init.toml +0 -38
- package/temp/rtk/src/filters/tofu-plan.toml +0 -35
- package/temp/rtk/src/filters/tofu-validate.toml +0 -17
- package/temp/rtk/src/filters/trunk-build.toml +0 -39
- package/temp/rtk/src/filters/ty.toml +0 -50
- package/temp/rtk/src/filters/uv-sync.toml +0 -37
- package/temp/rtk/src/filters/xcodebuild.toml +0 -99
- package/temp/rtk/src/filters/yamllint.toml +0 -25
- package/temp/rtk/src/find_cmd.rs +0 -598
- package/temp/rtk/src/format_cmd.rs +0 -386
- package/temp/rtk/src/gain.rs +0 -723
- package/temp/rtk/src/gh_cmd.rs +0 -1651
- package/temp/rtk/src/git.rs +0 -2012
- package/temp/rtk/src/go_cmd.rs +0 -592
- package/temp/rtk/src/golangci_cmd.rs +0 -254
- package/temp/rtk/src/grep_cmd.rs +0 -288
- package/temp/rtk/src/gt_cmd.rs +0 -810
- package/temp/rtk/src/hook_audit_cmd.rs +0 -283
- package/temp/rtk/src/hook_check.rs +0 -171
- package/temp/rtk/src/init.rs +0 -1859
- package/temp/rtk/src/integrity.rs +0 -537
- package/temp/rtk/src/json_cmd.rs +0 -231
- package/temp/rtk/src/learn/detector.rs +0 -628
- package/temp/rtk/src/learn/mod.rs +0 -119
- package/temp/rtk/src/learn/report.rs +0 -184
- package/temp/rtk/src/lint_cmd.rs +0 -694
- package/temp/rtk/src/local_llm.rs +0 -316
- package/temp/rtk/src/log_cmd.rs +0 -248
- package/temp/rtk/src/ls.rs +0 -324
- package/temp/rtk/src/main.rs +0 -2482
- package/temp/rtk/src/mypy_cmd.rs +0 -389
- package/temp/rtk/src/next_cmd.rs +0 -241
- package/temp/rtk/src/npm_cmd.rs +0 -236
- package/temp/rtk/src/parser/README.md +0 -267
- package/temp/rtk/src/parser/error.rs +0 -46
- package/temp/rtk/src/parser/formatter.rs +0 -336
- package/temp/rtk/src/parser/mod.rs +0 -311
- package/temp/rtk/src/parser/types.rs +0 -119
- package/temp/rtk/src/pip_cmd.rs +0 -302
- package/temp/rtk/src/playwright_cmd.rs +0 -479
- package/temp/rtk/src/pnpm_cmd.rs +0 -573
- package/temp/rtk/src/prettier_cmd.rs +0 -221
- package/temp/rtk/src/prisma_cmd.rs +0 -482
- package/temp/rtk/src/psql_cmd.rs +0 -382
- package/temp/rtk/src/pytest_cmd.rs +0 -384
- package/temp/rtk/src/read.rs +0 -217
- package/temp/rtk/src/rewrite_cmd.rs +0 -50
- package/temp/rtk/src/ruff_cmd.rs +0 -402
- package/temp/rtk/src/runner.rs +0 -271
- package/temp/rtk/src/summary.rs +0 -297
- package/temp/rtk/src/tee.rs +0 -405
- package/temp/rtk/src/telemetry.rs +0 -248
- package/temp/rtk/src/toml_filter.rs +0 -1655
- package/temp/rtk/src/tracking.rs +0 -1416
- package/temp/rtk/src/tree.rs +0 -209
- package/temp/rtk/src/tsc_cmd.rs +0 -259
- package/temp/rtk/src/utils.rs +0 -432
- package/temp/rtk/src/verify_cmd.rs +0 -47
- package/temp/rtk/src/vitest_cmd.rs +0 -385
- package/temp/rtk/src/wc_cmd.rs +0 -401
- package/temp/rtk/src/wget_cmd.rs +0 -260
- package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +0 -11
- package/temp/rtk/tests/fixtures/dotnet/format_changes.json +0 -31
- package/temp/rtk/tests/fixtures/dotnet/format_empty.json +0 -1
- package/temp/rtk/tests/fixtures/dotnet/format_success.json +0 -12
- package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +0 -18
- package/tsconfig.json +0 -15
package/temp/rtk/src/tracking.rs
DELETED
|
@@ -1,1416 +0,0 @@
|
|
|
1
|
-
//! Token savings tracking and analytics system.
|
|
2
|
-
//!
|
|
3
|
-
//! This module provides comprehensive tracking of RTK command executions,
|
|
4
|
-
//! recording token savings, execution times, and providing aggregation APIs
|
|
5
|
-
//! for daily/weekly/monthly statistics.
|
|
6
|
-
//!
|
|
7
|
-
//! # Architecture
|
|
8
|
-
//!
|
|
9
|
-
//! - Storage: SQLite database (~/.local/share/rtk/tracking.db)
|
|
10
|
-
//! - Retention: 90-day automatic cleanup
|
|
11
|
-
//! - Metrics: Input/output tokens, savings %, execution time
|
|
12
|
-
//!
|
|
13
|
-
//! # Quick Start
|
|
14
|
-
//!
|
|
15
|
-
//! ```no_run
|
|
16
|
-
//! use rtk::tracking::{TimedExecution, Tracker};
|
|
17
|
-
//!
|
|
18
|
-
//! // Track a command execution
|
|
19
|
-
//! let timer = TimedExecution::start();
|
|
20
|
-
//! let input = "raw output";
|
|
21
|
-
//! let output = "filtered output";
|
|
22
|
-
//! timer.track("ls -la", "rtk ls", input, output);
|
|
23
|
-
//!
|
|
24
|
-
//! // Query statistics
|
|
25
|
-
//! let tracker = Tracker::new().unwrap();
|
|
26
|
-
//! let summary = tracker.get_summary().unwrap();
|
|
27
|
-
//! println!("Saved {} tokens", summary.total_saved);
|
|
28
|
-
//! ```
|
|
29
|
-
//!
|
|
30
|
-
//! See [docs/tracking.md](../docs/tracking.md) for full documentation.
|
|
31
|
-
|
|
32
|
-
use anyhow::Result;
|
|
33
|
-
use chrono::{DateTime, Utc};
|
|
34
|
-
use rusqlite::{params, Connection};
|
|
35
|
-
use serde::Serialize;
|
|
36
|
-
use std::ffi::OsString;
|
|
37
|
-
use std::path::PathBuf;
|
|
38
|
-
use std::time::Instant;
|
|
39
|
-
|
|
40
|
-
// ── Project path helpers ── // added: project-scoped tracking support
|
|
41
|
-
|
|
42
|
-
/// Get the canonical project path string for the current working directory.
|
|
43
|
-
fn current_project_path_string() -> String {
|
|
44
|
-
std::env::current_dir()
|
|
45
|
-
.ok()
|
|
46
|
-
.and_then(|p| p.canonicalize().ok())
|
|
47
|
-
.map(|p| p.to_string_lossy().to_string())
|
|
48
|
-
.unwrap_or_default()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/// Build SQL filter params for project-scoped queries.
|
|
52
|
-
/// Returns (exact_match, glob_prefix) for WHERE clause.
|
|
53
|
-
/// Uses GLOB instead of LIKE to avoid `_` and `%` in paths acting as wildcards. // changed: GLOB
|
|
54
|
-
fn project_filter_params(project_path: Option<&str>) -> (Option<String>, Option<String>) {
|
|
55
|
-
match project_path {
|
|
56
|
-
Some(p) => (
|
|
57
|
-
Some(p.to_string()),
|
|
58
|
-
Some(format!("{}{}*", p, std::path::MAIN_SEPARATOR)), // changed: GLOB pattern with * wildcard
|
|
59
|
-
),
|
|
60
|
-
None => (None, None),
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/// Number of days to retain tracking history before automatic cleanup.
|
|
65
|
-
const HISTORY_DAYS: i64 = 90;
|
|
66
|
-
|
|
67
|
-
/// Main tracking interface for recording and querying command history.
|
|
68
|
-
///
|
|
69
|
-
/// Manages SQLite database connection and provides methods for:
|
|
70
|
-
/// - Recording command executions with token counts and timing
|
|
71
|
-
/// - Querying aggregated statistics (summary, daily, weekly, monthly)
|
|
72
|
-
/// - Retrieving recent command history
|
|
73
|
-
///
|
|
74
|
-
/// # Database Location
|
|
75
|
-
///
|
|
76
|
-
/// - Linux: `~/.local/share/rtk/tracking.db`
|
|
77
|
-
/// - macOS: `~/Library/Application Support/rtk/tracking.db`
|
|
78
|
-
/// - Windows: `%APPDATA%\rtk\tracking.db`
|
|
79
|
-
///
|
|
80
|
-
/// # Examples
|
|
81
|
-
///
|
|
82
|
-
/// ```no_run
|
|
83
|
-
/// use rtk::tracking::Tracker;
|
|
84
|
-
///
|
|
85
|
-
/// let tracker = Tracker::new()?;
|
|
86
|
-
/// tracker.record("ls -la", "rtk ls", 1000, 200, 50)?;
|
|
87
|
-
///
|
|
88
|
-
/// let summary = tracker.get_summary()?;
|
|
89
|
-
/// println!("Total saved: {} tokens", summary.total_saved);
|
|
90
|
-
/// # Ok::<(), anyhow::Error>(())
|
|
91
|
-
/// ```
|
|
92
|
-
pub struct Tracker {
|
|
93
|
-
conn: Connection,
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/// Individual command record from tracking history.
|
|
97
|
-
///
|
|
98
|
-
/// Contains timestamp, command name, and savings metrics for a single execution.
|
|
99
|
-
#[derive(Debug)]
|
|
100
|
-
pub struct CommandRecord {
|
|
101
|
-
/// UTC timestamp when command was executed
|
|
102
|
-
pub timestamp: DateTime<Utc>,
|
|
103
|
-
/// RTK command that was executed (e.g., "rtk ls")
|
|
104
|
-
pub rtk_cmd: String,
|
|
105
|
-
/// Number of tokens saved (input - output)
|
|
106
|
-
pub saved_tokens: usize,
|
|
107
|
-
/// Savings percentage ((saved / input) * 100)
|
|
108
|
-
pub savings_pct: f64,
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/// Aggregated statistics across all recorded commands.
|
|
112
|
-
///
|
|
113
|
-
/// Provides overall metrics and breakdowns by command and by day.
|
|
114
|
-
/// Returned by [`Tracker::get_summary`].
|
|
115
|
-
#[derive(Debug)]
|
|
116
|
-
pub struct GainSummary {
|
|
117
|
-
/// Total number of commands recorded
|
|
118
|
-
pub total_commands: usize,
|
|
119
|
-
/// Total input tokens across all commands
|
|
120
|
-
pub total_input: usize,
|
|
121
|
-
/// Total output tokens across all commands
|
|
122
|
-
pub total_output: usize,
|
|
123
|
-
/// Total tokens saved (input - output)
|
|
124
|
-
pub total_saved: usize,
|
|
125
|
-
/// Average savings percentage across all commands
|
|
126
|
-
pub avg_savings_pct: f64,
|
|
127
|
-
/// Total execution time across all commands (milliseconds)
|
|
128
|
-
pub total_time_ms: u64,
|
|
129
|
-
/// Average execution time per command (milliseconds)
|
|
130
|
-
pub avg_time_ms: u64,
|
|
131
|
-
/// Top 10 commands by tokens saved: (cmd, count, saved, avg_pct, avg_time_ms)
|
|
132
|
-
pub by_command: Vec<(String, usize, usize, f64, u64)>,
|
|
133
|
-
/// Last 30 days of activity: (date, saved_tokens)
|
|
134
|
-
pub by_day: Vec<(String, usize)>,
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/// Daily statistics for token savings and execution metrics.
|
|
138
|
-
///
|
|
139
|
-
/// Serializable to JSON for export via `rtk gain --daily --format json`.
|
|
140
|
-
///
|
|
141
|
-
/// # JSON Schema
|
|
142
|
-
///
|
|
143
|
-
/// ```json
|
|
144
|
-
/// {
|
|
145
|
-
/// "date": "2026-02-03",
|
|
146
|
-
/// "commands": 42,
|
|
147
|
-
/// "input_tokens": 15420,
|
|
148
|
-
/// "output_tokens": 3842,
|
|
149
|
-
/// "saved_tokens": 11578,
|
|
150
|
-
/// "savings_pct": 75.08,
|
|
151
|
-
/// "total_time_ms": 8450,
|
|
152
|
-
/// "avg_time_ms": 201
|
|
153
|
-
/// }
|
|
154
|
-
/// ```
|
|
155
|
-
#[derive(Debug, Serialize)]
|
|
156
|
-
pub struct DayStats {
|
|
157
|
-
/// ISO date (YYYY-MM-DD)
|
|
158
|
-
pub date: String,
|
|
159
|
-
/// Number of commands executed this day
|
|
160
|
-
pub commands: usize,
|
|
161
|
-
/// Total input tokens for this day
|
|
162
|
-
pub input_tokens: usize,
|
|
163
|
-
/// Total output tokens for this day
|
|
164
|
-
pub output_tokens: usize,
|
|
165
|
-
/// Total tokens saved this day
|
|
166
|
-
pub saved_tokens: usize,
|
|
167
|
-
/// Savings percentage for this day
|
|
168
|
-
pub savings_pct: f64,
|
|
169
|
-
/// Total execution time for this day (milliseconds)
|
|
170
|
-
pub total_time_ms: u64,
|
|
171
|
-
/// Average execution time per command (milliseconds)
|
|
172
|
-
pub avg_time_ms: u64,
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/// Weekly statistics for token savings and execution metrics.
|
|
176
|
-
///
|
|
177
|
-
/// Serializable to JSON for export via `rtk gain --weekly --format json`.
|
|
178
|
-
/// Weeks start on Sunday (SQLite default).
|
|
179
|
-
#[derive(Debug, Serialize)]
|
|
180
|
-
pub struct WeekStats {
|
|
181
|
-
/// Week start date (YYYY-MM-DD)
|
|
182
|
-
pub week_start: String,
|
|
183
|
-
/// Week end date (YYYY-MM-DD)
|
|
184
|
-
pub week_end: String,
|
|
185
|
-
/// Number of commands executed this week
|
|
186
|
-
pub commands: usize,
|
|
187
|
-
/// Total input tokens for this week
|
|
188
|
-
pub input_tokens: usize,
|
|
189
|
-
/// Total output tokens for this week
|
|
190
|
-
pub output_tokens: usize,
|
|
191
|
-
/// Total tokens saved this week
|
|
192
|
-
pub saved_tokens: usize,
|
|
193
|
-
/// Savings percentage for this week
|
|
194
|
-
pub savings_pct: f64,
|
|
195
|
-
/// Total execution time for this week (milliseconds)
|
|
196
|
-
pub total_time_ms: u64,
|
|
197
|
-
/// Average execution time per command (milliseconds)
|
|
198
|
-
pub avg_time_ms: u64,
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/// Monthly statistics for token savings and execution metrics.
|
|
202
|
-
///
|
|
203
|
-
/// Serializable to JSON for export via `rtk gain --monthly --format json`.
|
|
204
|
-
#[derive(Debug, Serialize)]
|
|
205
|
-
pub struct MonthStats {
|
|
206
|
-
/// Month identifier (YYYY-MM)
|
|
207
|
-
pub month: String,
|
|
208
|
-
/// Number of commands executed this month
|
|
209
|
-
pub commands: usize,
|
|
210
|
-
/// Total input tokens for this month
|
|
211
|
-
pub input_tokens: usize,
|
|
212
|
-
/// Total output tokens for this month
|
|
213
|
-
pub output_tokens: usize,
|
|
214
|
-
/// Total tokens saved this month
|
|
215
|
-
pub saved_tokens: usize,
|
|
216
|
-
/// Savings percentage for this month
|
|
217
|
-
pub savings_pct: f64,
|
|
218
|
-
/// Total execution time for this month (milliseconds)
|
|
219
|
-
pub total_time_ms: u64,
|
|
220
|
-
/// Average execution time per command (milliseconds)
|
|
221
|
-
pub avg_time_ms: u64,
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
impl Tracker {
|
|
225
|
-
/// Create a new tracker instance.
|
|
226
|
-
///
|
|
227
|
-
/// Opens or creates the SQLite database at the platform-specific location.
|
|
228
|
-
/// Automatically creates the `commands` table if it doesn't exist and runs
|
|
229
|
-
/// any necessary schema migrations.
|
|
230
|
-
///
|
|
231
|
-
/// # Errors
|
|
232
|
-
///
|
|
233
|
-
/// Returns error if:
|
|
234
|
-
/// - Cannot determine database path
|
|
235
|
-
/// - Cannot create parent directories
|
|
236
|
-
/// - Cannot open/create SQLite database
|
|
237
|
-
/// - Schema creation/migration fails
|
|
238
|
-
///
|
|
239
|
-
/// # Examples
|
|
240
|
-
///
|
|
241
|
-
/// ```no_run
|
|
242
|
-
/// use rtk::tracking::Tracker;
|
|
243
|
-
///
|
|
244
|
-
/// let tracker = Tracker::new()?;
|
|
245
|
-
/// # Ok::<(), anyhow::Error>(())
|
|
246
|
-
/// ```
|
|
247
|
-
pub fn new() -> Result<Self> {
|
|
248
|
-
let db_path = get_db_path()?;
|
|
249
|
-
if let Some(parent) = db_path.parent() {
|
|
250
|
-
std::fs::create_dir_all(parent)?;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
let conn = Connection::open(&db_path)?;
|
|
254
|
-
conn.execute(
|
|
255
|
-
"CREATE TABLE IF NOT EXISTS commands (
|
|
256
|
-
id INTEGER PRIMARY KEY,
|
|
257
|
-
timestamp TEXT NOT NULL,
|
|
258
|
-
original_cmd TEXT NOT NULL,
|
|
259
|
-
rtk_cmd TEXT NOT NULL,
|
|
260
|
-
input_tokens INTEGER NOT NULL,
|
|
261
|
-
output_tokens INTEGER NOT NULL,
|
|
262
|
-
saved_tokens INTEGER NOT NULL,
|
|
263
|
-
savings_pct REAL NOT NULL
|
|
264
|
-
)",
|
|
265
|
-
[],
|
|
266
|
-
)?;
|
|
267
|
-
|
|
268
|
-
conn.execute(
|
|
269
|
-
"CREATE INDEX IF NOT EXISTS idx_timestamp ON commands(timestamp)",
|
|
270
|
-
[],
|
|
271
|
-
)?;
|
|
272
|
-
|
|
273
|
-
// Migration: add exec_time_ms column if it doesn't exist
|
|
274
|
-
let _ = conn.execute(
|
|
275
|
-
"ALTER TABLE commands ADD COLUMN exec_time_ms INTEGER DEFAULT 0",
|
|
276
|
-
[],
|
|
277
|
-
);
|
|
278
|
-
// Migration: add project_path column with DEFAULT '' for new rows // changed: added DEFAULT
|
|
279
|
-
let _ = conn.execute(
|
|
280
|
-
"ALTER TABLE commands ADD COLUMN project_path TEXT DEFAULT ''",
|
|
281
|
-
[],
|
|
282
|
-
);
|
|
283
|
-
// One-time migration: normalize NULLs from pre-default schema // changed: guarded with EXISTS
|
|
284
|
-
let has_nulls: bool = conn
|
|
285
|
-
.query_row(
|
|
286
|
-
"SELECT EXISTS(SELECT 1 FROM commands WHERE project_path IS NULL)",
|
|
287
|
-
[],
|
|
288
|
-
|row| row.get(0),
|
|
289
|
-
)
|
|
290
|
-
.unwrap_or(false);
|
|
291
|
-
if has_nulls {
|
|
292
|
-
let _ = conn.execute(
|
|
293
|
-
"UPDATE commands SET project_path = '' WHERE project_path IS NULL",
|
|
294
|
-
[],
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
// Index for fast project-scoped gain queries // added
|
|
298
|
-
let _ = conn.execute(
|
|
299
|
-
"CREATE INDEX IF NOT EXISTS idx_project_path_timestamp ON commands(project_path, timestamp)",
|
|
300
|
-
[],
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
conn.execute(
|
|
304
|
-
"CREATE TABLE IF NOT EXISTS parse_failures (
|
|
305
|
-
id INTEGER PRIMARY KEY,
|
|
306
|
-
timestamp TEXT NOT NULL,
|
|
307
|
-
raw_command TEXT NOT NULL,
|
|
308
|
-
error_message TEXT NOT NULL,
|
|
309
|
-
fallback_succeeded INTEGER NOT NULL DEFAULT 0
|
|
310
|
-
)",
|
|
311
|
-
[],
|
|
312
|
-
)?;
|
|
313
|
-
conn.execute(
|
|
314
|
-
"CREATE INDEX IF NOT EXISTS idx_pf_timestamp ON parse_failures(timestamp)",
|
|
315
|
-
[],
|
|
316
|
-
)?;
|
|
317
|
-
|
|
318
|
-
Ok(Self { conn })
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/// Record a command execution with token counts and timing.
|
|
322
|
-
///
|
|
323
|
-
/// Calculates savings metrics and stores the record in the database.
|
|
324
|
-
/// Automatically cleans up records older than 90 days after insertion.
|
|
325
|
-
///
|
|
326
|
-
/// # Arguments
|
|
327
|
-
///
|
|
328
|
-
/// - `original_cmd`: The standard command (e.g., "ls -la")
|
|
329
|
-
/// - `rtk_cmd`: The RTK command used (e.g., "rtk ls")
|
|
330
|
-
/// - `input_tokens`: Estimated tokens from standard command output
|
|
331
|
-
/// - `output_tokens`: Actual tokens from RTK output
|
|
332
|
-
/// - `exec_time_ms`: Execution time in milliseconds
|
|
333
|
-
///
|
|
334
|
-
/// # Examples
|
|
335
|
-
///
|
|
336
|
-
/// ```no_run
|
|
337
|
-
/// use rtk::tracking::Tracker;
|
|
338
|
-
///
|
|
339
|
-
/// let tracker = Tracker::new()?;
|
|
340
|
-
/// tracker.record("ls -la", "rtk ls", 1000, 200, 50)?;
|
|
341
|
-
/// # Ok::<(), anyhow::Error>(())
|
|
342
|
-
/// ```
|
|
343
|
-
pub fn record(
|
|
344
|
-
&self,
|
|
345
|
-
original_cmd: &str,
|
|
346
|
-
rtk_cmd: &str,
|
|
347
|
-
input_tokens: usize,
|
|
348
|
-
output_tokens: usize,
|
|
349
|
-
exec_time_ms: u64,
|
|
350
|
-
) -> Result<()> {
|
|
351
|
-
let saved = input_tokens.saturating_sub(output_tokens);
|
|
352
|
-
let pct = if input_tokens > 0 {
|
|
353
|
-
(saved as f64 / input_tokens as f64) * 100.0
|
|
354
|
-
} else {
|
|
355
|
-
0.0
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
let project_path = current_project_path_string(); // added: record cwd
|
|
359
|
-
|
|
360
|
-
self.conn.execute(
|
|
361
|
-
"INSERT INTO commands (timestamp, original_cmd, rtk_cmd, project_path, input_tokens, output_tokens, saved_tokens, savings_pct, exec_time_ms)
|
|
362
|
-
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", // added: project_path
|
|
363
|
-
params![
|
|
364
|
-
Utc::now().to_rfc3339(),
|
|
365
|
-
original_cmd,
|
|
366
|
-
rtk_cmd,
|
|
367
|
-
project_path, // added
|
|
368
|
-
input_tokens as i64,
|
|
369
|
-
output_tokens as i64,
|
|
370
|
-
saved as i64,
|
|
371
|
-
pct,
|
|
372
|
-
exec_time_ms as i64
|
|
373
|
-
],
|
|
374
|
-
)?;
|
|
375
|
-
|
|
376
|
-
self.cleanup_old()?;
|
|
377
|
-
Ok(())
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
fn cleanup_old(&self) -> Result<()> {
|
|
381
|
-
let cutoff = Utc::now() - chrono::Duration::days(HISTORY_DAYS);
|
|
382
|
-
self.conn.execute(
|
|
383
|
-
"DELETE FROM commands WHERE timestamp < ?1",
|
|
384
|
-
params![cutoff.to_rfc3339()],
|
|
385
|
-
)?;
|
|
386
|
-
self.conn.execute(
|
|
387
|
-
"DELETE FROM parse_failures WHERE timestamp < ?1",
|
|
388
|
-
params![cutoff.to_rfc3339()],
|
|
389
|
-
)?;
|
|
390
|
-
Ok(())
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/// Record a parse failure for analytics.
|
|
394
|
-
pub fn record_parse_failure(
|
|
395
|
-
&self,
|
|
396
|
-
raw_command: &str,
|
|
397
|
-
error_message: &str,
|
|
398
|
-
fallback_succeeded: bool,
|
|
399
|
-
) -> Result<()> {
|
|
400
|
-
self.conn.execute(
|
|
401
|
-
"INSERT INTO parse_failures (timestamp, raw_command, error_message, fallback_succeeded)
|
|
402
|
-
VALUES (?1, ?2, ?3, ?4)",
|
|
403
|
-
params![
|
|
404
|
-
Utc::now().to_rfc3339(),
|
|
405
|
-
raw_command,
|
|
406
|
-
error_message,
|
|
407
|
-
fallback_succeeded as i32,
|
|
408
|
-
],
|
|
409
|
-
)?;
|
|
410
|
-
self.cleanup_old()?;
|
|
411
|
-
Ok(())
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/// Get parse failure summary for `rtk gain --failures`.
|
|
415
|
-
pub fn get_parse_failure_summary(&self) -> Result<ParseFailureSummary> {
|
|
416
|
-
let total: i64 = self
|
|
417
|
-
.conn
|
|
418
|
-
.query_row("SELECT COUNT(*) FROM parse_failures", [], |row| row.get(0))?;
|
|
419
|
-
|
|
420
|
-
let succeeded: i64 = self.conn.query_row(
|
|
421
|
-
"SELECT COUNT(*) FROM parse_failures WHERE fallback_succeeded = 1",
|
|
422
|
-
[],
|
|
423
|
-
|row| row.get(0),
|
|
424
|
-
)?;
|
|
425
|
-
|
|
426
|
-
let recovery_rate = if total > 0 {
|
|
427
|
-
(succeeded as f64 / total as f64) * 100.0
|
|
428
|
-
} else {
|
|
429
|
-
0.0
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
// Top commands by frequency
|
|
433
|
-
let mut stmt = self.conn.prepare(
|
|
434
|
-
"SELECT raw_command, COUNT(*) as cnt
|
|
435
|
-
FROM parse_failures
|
|
436
|
-
GROUP BY raw_command
|
|
437
|
-
ORDER BY cnt DESC
|
|
438
|
-
LIMIT 10",
|
|
439
|
-
)?;
|
|
440
|
-
let top_commands = stmt
|
|
441
|
-
.query_map([], |row| {
|
|
442
|
-
Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize))
|
|
443
|
-
})?
|
|
444
|
-
.collect::<Result<Vec<_>, _>>()?;
|
|
445
|
-
|
|
446
|
-
// Recent 10
|
|
447
|
-
let mut stmt = self.conn.prepare(
|
|
448
|
-
"SELECT timestamp, raw_command, error_message, fallback_succeeded
|
|
449
|
-
FROM parse_failures
|
|
450
|
-
ORDER BY timestamp DESC
|
|
451
|
-
LIMIT 10",
|
|
452
|
-
)?;
|
|
453
|
-
let recent = stmt
|
|
454
|
-
.query_map([], |row| {
|
|
455
|
-
Ok(ParseFailureRecord {
|
|
456
|
-
timestamp: row.get(0)?,
|
|
457
|
-
raw_command: row.get(1)?,
|
|
458
|
-
error_message: row.get(2)?,
|
|
459
|
-
fallback_succeeded: row.get::<_, i32>(3)? != 0,
|
|
460
|
-
})
|
|
461
|
-
})?
|
|
462
|
-
.collect::<Result<Vec<_>, _>>()?;
|
|
463
|
-
|
|
464
|
-
Ok(ParseFailureSummary {
|
|
465
|
-
total: total as usize,
|
|
466
|
-
recovery_rate,
|
|
467
|
-
top_commands,
|
|
468
|
-
recent,
|
|
469
|
-
})
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/// Get overall summary statistics across all recorded commands.
|
|
473
|
-
///
|
|
474
|
-
/// Returns aggregated metrics including:
|
|
475
|
-
/// - Total commands, tokens (input/output/saved)
|
|
476
|
-
/// - Average savings percentage and execution time
|
|
477
|
-
/// - Top 10 commands by tokens saved
|
|
478
|
-
/// - Last 30 days of activity
|
|
479
|
-
///
|
|
480
|
-
/// # Examples
|
|
481
|
-
///
|
|
482
|
-
/// ```no_run
|
|
483
|
-
/// use rtk::tracking::Tracker;
|
|
484
|
-
///
|
|
485
|
-
/// let tracker = Tracker::new()?;
|
|
486
|
-
/// let summary = tracker.get_summary()?;
|
|
487
|
-
/// println!("Saved {} tokens ({:.1}%)",
|
|
488
|
-
/// summary.total_saved, summary.avg_savings_pct);
|
|
489
|
-
/// # Ok::<(), anyhow::Error>(())
|
|
490
|
-
/// ```
|
|
491
|
-
pub fn get_summary(&self) -> Result<GainSummary> {
|
|
492
|
-
self.get_summary_filtered(None) // delegate to filtered variant
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/// Get summary statistics filtered by project path. // added
|
|
496
|
-
///
|
|
497
|
-
/// When `project_path` is `Some`, matches the exact working directory
|
|
498
|
-
/// or any subdirectory (prefix match with path separator).
|
|
499
|
-
pub fn get_summary_filtered(&self, project_path: Option<&str>) -> Result<GainSummary> {
|
|
500
|
-
let (project_exact, project_glob) = project_filter_params(project_path); // added
|
|
501
|
-
let mut total_commands = 0usize;
|
|
502
|
-
let mut total_input = 0usize;
|
|
503
|
-
let mut total_output = 0usize;
|
|
504
|
-
let mut total_saved = 0usize;
|
|
505
|
-
let mut total_time_ms = 0u64;
|
|
506
|
-
|
|
507
|
-
let mut stmt = self.conn.prepare(
|
|
508
|
-
"SELECT input_tokens, output_tokens, saved_tokens, exec_time_ms
|
|
509
|
-
FROM commands
|
|
510
|
-
WHERE (?1 IS NULL OR project_path = ?1 OR project_path GLOB ?2)", // added: project filter
|
|
511
|
-
)?;
|
|
512
|
-
|
|
513
|
-
let rows = stmt.query_map(params![project_exact, project_glob], |row| {
|
|
514
|
-
// added: params
|
|
515
|
-
Ok((
|
|
516
|
-
row.get::<_, i64>(0)? as usize,
|
|
517
|
-
row.get::<_, i64>(1)? as usize,
|
|
518
|
-
row.get::<_, i64>(2)? as usize,
|
|
519
|
-
row.get::<_, i64>(3)? as u64,
|
|
520
|
-
))
|
|
521
|
-
})?;
|
|
522
|
-
|
|
523
|
-
for row in rows {
|
|
524
|
-
let (input, output, saved, time_ms) = row?;
|
|
525
|
-
total_commands += 1;
|
|
526
|
-
total_input += input;
|
|
527
|
-
total_output += output;
|
|
528
|
-
total_saved += saved;
|
|
529
|
-
total_time_ms += time_ms;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
let avg_savings_pct = if total_input > 0 {
|
|
533
|
-
(total_saved as f64 / total_input as f64) * 100.0
|
|
534
|
-
} else {
|
|
535
|
-
0.0
|
|
536
|
-
};
|
|
537
|
-
|
|
538
|
-
let avg_time_ms = if total_commands > 0 {
|
|
539
|
-
total_time_ms / total_commands as u64
|
|
540
|
-
} else {
|
|
541
|
-
0
|
|
542
|
-
};
|
|
543
|
-
|
|
544
|
-
let by_command = self.get_by_command(project_path)?; // added: pass project filter
|
|
545
|
-
let by_day = self.get_by_day(project_path)?; // added: pass project filter
|
|
546
|
-
|
|
547
|
-
Ok(GainSummary {
|
|
548
|
-
total_commands,
|
|
549
|
-
total_input,
|
|
550
|
-
total_output,
|
|
551
|
-
total_saved,
|
|
552
|
-
avg_savings_pct,
|
|
553
|
-
total_time_ms,
|
|
554
|
-
avg_time_ms,
|
|
555
|
-
by_command,
|
|
556
|
-
by_day,
|
|
557
|
-
})
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
fn get_by_command(
|
|
561
|
-
&self,
|
|
562
|
-
project_path: Option<&str>, // added
|
|
563
|
-
) -> Result<Vec<(String, usize, usize, f64, u64)>> {
|
|
564
|
-
let (project_exact, project_glob) = project_filter_params(project_path); // added
|
|
565
|
-
let mut stmt = self.conn.prepare(
|
|
566
|
-
"SELECT rtk_cmd, COUNT(*), SUM(saved_tokens), AVG(savings_pct), AVG(exec_time_ms)
|
|
567
|
-
FROM commands
|
|
568
|
-
WHERE (?1 IS NULL OR project_path = ?1 OR project_path GLOB ?2)
|
|
569
|
-
GROUP BY rtk_cmd
|
|
570
|
-
ORDER BY SUM(saved_tokens) DESC
|
|
571
|
-
LIMIT 10", // added: project filter in WHERE
|
|
572
|
-
)?;
|
|
573
|
-
|
|
574
|
-
let rows = stmt.query_map(params![project_exact, project_glob], |row| {
|
|
575
|
-
// added: params
|
|
576
|
-
Ok((
|
|
577
|
-
row.get::<_, String>(0)?,
|
|
578
|
-
row.get::<_, i64>(1)? as usize,
|
|
579
|
-
row.get::<_, i64>(2)? as usize,
|
|
580
|
-
row.get::<_, f64>(3)?,
|
|
581
|
-
row.get::<_, f64>(4)? as u64,
|
|
582
|
-
))
|
|
583
|
-
})?;
|
|
584
|
-
|
|
585
|
-
Ok(rows.collect::<Result<Vec<_>, _>>()?)
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
fn get_by_day(
|
|
589
|
-
&self,
|
|
590
|
-
project_path: Option<&str>, // added
|
|
591
|
-
) -> Result<Vec<(String, usize)>> {
|
|
592
|
-
let (project_exact, project_glob) = project_filter_params(project_path); // added
|
|
593
|
-
let mut stmt = self.conn.prepare(
|
|
594
|
-
"SELECT DATE(timestamp), SUM(saved_tokens)
|
|
595
|
-
FROM commands
|
|
596
|
-
WHERE (?1 IS NULL OR project_path = ?1 OR project_path GLOB ?2)
|
|
597
|
-
GROUP BY DATE(timestamp)
|
|
598
|
-
ORDER BY DATE(timestamp) DESC
|
|
599
|
-
LIMIT 30", // added: project filter in WHERE
|
|
600
|
-
)?;
|
|
601
|
-
|
|
602
|
-
let rows = stmt.query_map(params![project_exact, project_glob], |row| {
|
|
603
|
-
// added: params
|
|
604
|
-
Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize))
|
|
605
|
-
})?;
|
|
606
|
-
|
|
607
|
-
let mut result: Vec<_> = rows.collect::<Result<Vec<_>, _>>()?;
|
|
608
|
-
result.reverse();
|
|
609
|
-
Ok(result)
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/// Get daily statistics for all recorded days.
|
|
613
|
-
///
|
|
614
|
-
/// Returns one [`DayStats`] per day with commands executed, tokens saved,
|
|
615
|
-
/// and execution time metrics. Results are ordered chronologically (oldest first).
|
|
616
|
-
///
|
|
617
|
-
/// # Examples
|
|
618
|
-
///
|
|
619
|
-
/// ```no_run
|
|
620
|
-
/// use rtk::tracking::Tracker;
|
|
621
|
-
///
|
|
622
|
-
/// let tracker = Tracker::new()?;
|
|
623
|
-
/// let days = tracker.get_all_days()?;
|
|
624
|
-
/// for day in days.iter().take(7) {
|
|
625
|
-
/// println!("{}: {} commands, {} tokens saved",
|
|
626
|
-
/// day.date, day.commands, day.saved_tokens);
|
|
627
|
-
/// }
|
|
628
|
-
/// # Ok::<(), anyhow::Error>(())
|
|
629
|
-
/// ```
|
|
630
|
-
pub fn get_all_days(&self) -> Result<Vec<DayStats>> {
|
|
631
|
-
self.get_all_days_filtered(None) // delegate to filtered variant
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/// Get daily statistics filtered by project path. // added
|
|
635
|
-
pub fn get_all_days_filtered(&self, project_path: Option<&str>) -> Result<Vec<DayStats>> {
|
|
636
|
-
let (project_exact, project_glob) = project_filter_params(project_path); // added
|
|
637
|
-
let mut stmt = self.conn.prepare(
|
|
638
|
-
"SELECT
|
|
639
|
-
DATE(timestamp) as date,
|
|
640
|
-
COUNT(*) as commands,
|
|
641
|
-
SUM(input_tokens) as input,
|
|
642
|
-
SUM(output_tokens) as output,
|
|
643
|
-
SUM(saved_tokens) as saved,
|
|
644
|
-
SUM(exec_time_ms) as total_time
|
|
645
|
-
FROM commands
|
|
646
|
-
WHERE (?1 IS NULL OR project_path = ?1 OR project_path GLOB ?2)
|
|
647
|
-
GROUP BY DATE(timestamp)
|
|
648
|
-
ORDER BY DATE(timestamp) DESC", // added: project filter
|
|
649
|
-
)?;
|
|
650
|
-
|
|
651
|
-
let rows = stmt.query_map(params![project_exact, project_glob], |row| {
|
|
652
|
-
// added: params
|
|
653
|
-
let input = row.get::<_, i64>(2)? as usize;
|
|
654
|
-
let saved = row.get::<_, i64>(4)? as usize;
|
|
655
|
-
let commands = row.get::<_, i64>(1)? as usize;
|
|
656
|
-
let total_time = row.get::<_, i64>(5)? as u64;
|
|
657
|
-
let savings_pct = if input > 0 {
|
|
658
|
-
(saved as f64 / input as f64) * 100.0
|
|
659
|
-
} else {
|
|
660
|
-
0.0
|
|
661
|
-
};
|
|
662
|
-
let avg_time_ms = if commands > 0 {
|
|
663
|
-
total_time / commands as u64
|
|
664
|
-
} else {
|
|
665
|
-
0
|
|
666
|
-
};
|
|
667
|
-
|
|
668
|
-
Ok(DayStats {
|
|
669
|
-
date: row.get(0)?,
|
|
670
|
-
commands,
|
|
671
|
-
input_tokens: input,
|
|
672
|
-
output_tokens: row.get::<_, i64>(3)? as usize,
|
|
673
|
-
saved_tokens: saved,
|
|
674
|
-
savings_pct,
|
|
675
|
-
total_time_ms: total_time,
|
|
676
|
-
avg_time_ms,
|
|
677
|
-
})
|
|
678
|
-
})?;
|
|
679
|
-
|
|
680
|
-
let mut result: Vec<_> = rows.collect::<Result<Vec<_>, _>>()?;
|
|
681
|
-
result.reverse();
|
|
682
|
-
Ok(result)
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/// Get weekly statistics grouped by week.
|
|
686
|
-
///
|
|
687
|
-
/// Returns one [`WeekStats`] per week with aggregated metrics.
|
|
688
|
-
/// Weeks start on Sunday (SQLite default). Results ordered chronologically.
|
|
689
|
-
///
|
|
690
|
-
/// # Examples
|
|
691
|
-
///
|
|
692
|
-
/// ```no_run
|
|
693
|
-
/// use rtk::tracking::Tracker;
|
|
694
|
-
///
|
|
695
|
-
/// let tracker = Tracker::new()?;
|
|
696
|
-
/// let weeks = tracker.get_by_week()?;
|
|
697
|
-
/// for week in weeks {
|
|
698
|
-
/// println!("{} to {}: {} tokens saved",
|
|
699
|
-
/// week.week_start, week.week_end, week.saved_tokens);
|
|
700
|
-
/// }
|
|
701
|
-
/// # Ok::<(), anyhow::Error>(())
|
|
702
|
-
/// ```
|
|
703
|
-
pub fn get_by_week(&self) -> Result<Vec<WeekStats>> {
|
|
704
|
-
self.get_by_week_filtered(None) // delegate to filtered variant
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
/// Get weekly statistics filtered by project path. // added
|
|
708
|
-
pub fn get_by_week_filtered(&self, project_path: Option<&str>) -> Result<Vec<WeekStats>> {
|
|
709
|
-
let (project_exact, project_glob) = project_filter_params(project_path); // added
|
|
710
|
-
let mut stmt = self.conn.prepare(
|
|
711
|
-
"SELECT
|
|
712
|
-
DATE(timestamp, 'weekday 0', '-6 days') as week_start,
|
|
713
|
-
DATE(timestamp, 'weekday 0') as week_end,
|
|
714
|
-
COUNT(*) as commands,
|
|
715
|
-
SUM(input_tokens) as input,
|
|
716
|
-
SUM(output_tokens) as output,
|
|
717
|
-
SUM(saved_tokens) as saved,
|
|
718
|
-
SUM(exec_time_ms) as total_time
|
|
719
|
-
FROM commands
|
|
720
|
-
WHERE (?1 IS NULL OR project_path = ?1 OR project_path GLOB ?2)
|
|
721
|
-
GROUP BY week_start
|
|
722
|
-
ORDER BY week_start DESC", // added: project filter
|
|
723
|
-
)?;
|
|
724
|
-
|
|
725
|
-
let rows = stmt.query_map(params![project_exact, project_glob], |row| {
|
|
726
|
-
// added: params
|
|
727
|
-
let input = row.get::<_, i64>(3)? as usize;
|
|
728
|
-
let saved = row.get::<_, i64>(5)? as usize;
|
|
729
|
-
let commands = row.get::<_, i64>(2)? as usize;
|
|
730
|
-
let total_time = row.get::<_, i64>(6)? as u64;
|
|
731
|
-
let savings_pct = if input > 0 {
|
|
732
|
-
(saved as f64 / input as f64) * 100.0
|
|
733
|
-
} else {
|
|
734
|
-
0.0
|
|
735
|
-
};
|
|
736
|
-
let avg_time_ms = if commands > 0 {
|
|
737
|
-
total_time / commands as u64
|
|
738
|
-
} else {
|
|
739
|
-
0
|
|
740
|
-
};
|
|
741
|
-
|
|
742
|
-
Ok(WeekStats {
|
|
743
|
-
week_start: row.get(0)?,
|
|
744
|
-
week_end: row.get(1)?,
|
|
745
|
-
commands,
|
|
746
|
-
input_tokens: input,
|
|
747
|
-
output_tokens: row.get::<_, i64>(4)? as usize,
|
|
748
|
-
saved_tokens: saved,
|
|
749
|
-
savings_pct,
|
|
750
|
-
total_time_ms: total_time,
|
|
751
|
-
avg_time_ms,
|
|
752
|
-
})
|
|
753
|
-
})?;
|
|
754
|
-
|
|
755
|
-
let mut result: Vec<_> = rows.collect::<Result<Vec<_>, _>>()?;
|
|
756
|
-
result.reverse();
|
|
757
|
-
Ok(result)
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
/// Get monthly statistics grouped by month.
|
|
761
|
-
///
|
|
762
|
-
/// Returns one [`MonthStats`] per month (YYYY-MM format) with aggregated metrics.
|
|
763
|
-
/// Results ordered chronologically.
|
|
764
|
-
///
|
|
765
|
-
/// # Examples
|
|
766
|
-
///
|
|
767
|
-
/// ```no_run
|
|
768
|
-
/// use rtk::tracking::Tracker;
|
|
769
|
-
///
|
|
770
|
-
/// let tracker = Tracker::new()?;
|
|
771
|
-
/// let months = tracker.get_by_month()?;
|
|
772
|
-
/// for month in months {
|
|
773
|
-
/// println!("{}: {} tokens saved ({:.1}%)",
|
|
774
|
-
/// month.month, month.saved_tokens, month.savings_pct);
|
|
775
|
-
/// }
|
|
776
|
-
/// # Ok::<(), anyhow::Error>(())
|
|
777
|
-
/// ```
|
|
778
|
-
pub fn get_by_month(&self) -> Result<Vec<MonthStats>> {
|
|
779
|
-
self.get_by_month_filtered(None) // delegate to filtered variant
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
/// Get monthly statistics filtered by project path. // added
|
|
783
|
-
pub fn get_by_month_filtered(&self, project_path: Option<&str>) -> Result<Vec<MonthStats>> {
|
|
784
|
-
let (project_exact, project_glob) = project_filter_params(project_path); // added
|
|
785
|
-
let mut stmt = self.conn.prepare(
|
|
786
|
-
"SELECT
|
|
787
|
-
strftime('%Y-%m', timestamp) as month,
|
|
788
|
-
COUNT(*) as commands,
|
|
789
|
-
SUM(input_tokens) as input,
|
|
790
|
-
SUM(output_tokens) as output,
|
|
791
|
-
SUM(saved_tokens) as saved,
|
|
792
|
-
SUM(exec_time_ms) as total_time
|
|
793
|
-
FROM commands
|
|
794
|
-
WHERE (?1 IS NULL OR project_path = ?1 OR project_path GLOB ?2)
|
|
795
|
-
GROUP BY month
|
|
796
|
-
ORDER BY month DESC", // added: project filter
|
|
797
|
-
)?;
|
|
798
|
-
|
|
799
|
-
let rows = stmt.query_map(params![project_exact, project_glob], |row| {
|
|
800
|
-
// added: params
|
|
801
|
-
let input = row.get::<_, i64>(2)? as usize;
|
|
802
|
-
let saved = row.get::<_, i64>(4)? as usize;
|
|
803
|
-
let commands = row.get::<_, i64>(1)? as usize;
|
|
804
|
-
let total_time = row.get::<_, i64>(5)? as u64;
|
|
805
|
-
let savings_pct = if input > 0 {
|
|
806
|
-
(saved as f64 / input as f64) * 100.0
|
|
807
|
-
} else {
|
|
808
|
-
0.0
|
|
809
|
-
};
|
|
810
|
-
let avg_time_ms = if commands > 0 {
|
|
811
|
-
total_time / commands as u64
|
|
812
|
-
} else {
|
|
813
|
-
0
|
|
814
|
-
};
|
|
815
|
-
|
|
816
|
-
Ok(MonthStats {
|
|
817
|
-
month: row.get(0)?,
|
|
818
|
-
commands,
|
|
819
|
-
input_tokens: input,
|
|
820
|
-
output_tokens: row.get::<_, i64>(3)? as usize,
|
|
821
|
-
saved_tokens: saved,
|
|
822
|
-
savings_pct,
|
|
823
|
-
total_time_ms: total_time,
|
|
824
|
-
avg_time_ms,
|
|
825
|
-
})
|
|
826
|
-
})?;
|
|
827
|
-
|
|
828
|
-
let mut result: Vec<_> = rows.collect::<Result<Vec<_>, _>>()?;
|
|
829
|
-
result.reverse();
|
|
830
|
-
Ok(result)
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
/// Get recent command history.
|
|
834
|
-
///
|
|
835
|
-
/// Returns up to `limit` most recent command records, ordered by timestamp (newest first).
|
|
836
|
-
///
|
|
837
|
-
/// # Arguments
|
|
838
|
-
///
|
|
839
|
-
/// - `limit`: Maximum number of records to return
|
|
840
|
-
///
|
|
841
|
-
/// # Examples
|
|
842
|
-
///
|
|
843
|
-
/// ```no_run
|
|
844
|
-
/// use rtk::tracking::Tracker;
|
|
845
|
-
///
|
|
846
|
-
/// let tracker = Tracker::new()?;
|
|
847
|
-
/// let recent = tracker.get_recent(10)?;
|
|
848
|
-
/// for cmd in recent {
|
|
849
|
-
/// println!("{}: {} saved {:.1}%",
|
|
850
|
-
/// cmd.timestamp, cmd.rtk_cmd, cmd.savings_pct);
|
|
851
|
-
/// }
|
|
852
|
-
/// # Ok::<(), anyhow::Error>(())
|
|
853
|
-
/// ```
|
|
854
|
-
pub fn get_recent(&self, limit: usize) -> Result<Vec<CommandRecord>> {
|
|
855
|
-
self.get_recent_filtered(limit, None) // delegate to filtered variant
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
/// Get recent command history filtered by project path. // added
|
|
859
|
-
pub fn get_recent_filtered(
|
|
860
|
-
&self,
|
|
861
|
-
limit: usize,
|
|
862
|
-
project_path: Option<&str>,
|
|
863
|
-
) -> Result<Vec<CommandRecord>> {
|
|
864
|
-
let (project_exact, project_glob) = project_filter_params(project_path); // added
|
|
865
|
-
let mut stmt = self.conn.prepare(
|
|
866
|
-
"SELECT timestamp, rtk_cmd, saved_tokens, savings_pct
|
|
867
|
-
FROM commands
|
|
868
|
-
WHERE (?1 IS NULL OR project_path = ?1 OR project_path GLOB ?2)
|
|
869
|
-
ORDER BY timestamp DESC
|
|
870
|
-
LIMIT ?3", // added: project filter
|
|
871
|
-
)?;
|
|
872
|
-
|
|
873
|
-
let rows = stmt.query_map(
|
|
874
|
-
params![project_exact, project_glob, limit as i64], // added: project params
|
|
875
|
-
|row| {
|
|
876
|
-
Ok(CommandRecord {
|
|
877
|
-
timestamp: DateTime::parse_from_rfc3339(&row.get::<_, String>(0)?)
|
|
878
|
-
.map(|dt| dt.with_timezone(&Utc))
|
|
879
|
-
.unwrap_or_else(|_| Utc::now()),
|
|
880
|
-
rtk_cmd: row.get(1)?,
|
|
881
|
-
saved_tokens: row.get::<_, i64>(2)? as usize,
|
|
882
|
-
savings_pct: row.get(3)?,
|
|
883
|
-
})
|
|
884
|
-
},
|
|
885
|
-
)?;
|
|
886
|
-
|
|
887
|
-
Ok(rows.collect::<Result<Vec<_>, _>>()?)
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
/// Count commands since a given timestamp (for telemetry).
|
|
891
|
-
pub fn count_commands_since(&self, since: chrono::DateTime<chrono::Utc>) -> Result<i64> {
|
|
892
|
-
let ts = since.format("%Y-%m-%dT%H:%M:%S").to_string();
|
|
893
|
-
let count: i64 = self.conn.query_row(
|
|
894
|
-
"SELECT COUNT(*) FROM commands WHERE timestamp >= ?1",
|
|
895
|
-
params![ts],
|
|
896
|
-
|row| row.get(0),
|
|
897
|
-
)?;
|
|
898
|
-
Ok(count)
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
/// Get top N commands by frequency (for telemetry).
|
|
902
|
-
pub fn top_commands(&self, limit: usize) -> Result<Vec<String>> {
|
|
903
|
-
let mut stmt = self.conn.prepare(
|
|
904
|
-
"SELECT rtk_cmd, COUNT(*) as cnt FROM commands
|
|
905
|
-
GROUP BY rtk_cmd ORDER BY cnt DESC LIMIT ?1",
|
|
906
|
-
)?;
|
|
907
|
-
let rows = stmt.query_map(params![limit as i64], |row| {
|
|
908
|
-
let cmd: String = row.get(0)?;
|
|
909
|
-
// Extract just the command name (e.g. "rtk git status" → "git")
|
|
910
|
-
Ok(cmd.split_whitespace().nth(1).unwrap_or(&cmd).to_string())
|
|
911
|
-
})?;
|
|
912
|
-
Ok(rows.filter_map(|r| r.ok()).collect())
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
/// Get overall savings percentage (for telemetry).
|
|
916
|
-
pub fn overall_savings_pct(&self) -> Result<f64> {
|
|
917
|
-
let (total_input, total_saved): (i64, i64) = self.conn.query_row(
|
|
918
|
-
"SELECT COALESCE(SUM(input_tokens), 0), COALESCE(SUM(saved_tokens), 0) FROM commands",
|
|
919
|
-
[],
|
|
920
|
-
|row| Ok((row.get(0)?, row.get(1)?)),
|
|
921
|
-
)?;
|
|
922
|
-
if total_input > 0 {
|
|
923
|
-
Ok((total_saved as f64 / total_input as f64) * 100.0)
|
|
924
|
-
} else {
|
|
925
|
-
Ok(0.0)
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
/// Get total tokens saved across all tracked commands (for telemetry).
|
|
930
|
-
pub fn total_tokens_saved(&self) -> Result<i64> {
|
|
931
|
-
let saved: i64 = self.conn.query_row(
|
|
932
|
-
"SELECT COALESCE(SUM(saved_tokens), 0) FROM commands",
|
|
933
|
-
[],
|
|
934
|
-
|row| row.get(0),
|
|
935
|
-
)?;
|
|
936
|
-
Ok(saved)
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
/// Get tokens saved in the last 24 hours (for telemetry).
|
|
940
|
-
pub fn tokens_saved_24h(&self, since: chrono::DateTime<chrono::Utc>) -> Result<i64> {
|
|
941
|
-
let ts = since.format("%Y-%m-%dT%H:%M:%S").to_string();
|
|
942
|
-
let saved: i64 = self.conn.query_row(
|
|
943
|
-
"SELECT COALESCE(SUM(saved_tokens), 0) FROM commands WHERE timestamp >= ?1",
|
|
944
|
-
params![ts],
|
|
945
|
-
|row| row.get(0),
|
|
946
|
-
)?;
|
|
947
|
-
Ok(saved)
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
fn get_db_path() -> Result<PathBuf> {
|
|
952
|
-
// Priority 1: Environment variable RTK_DB_PATH
|
|
953
|
-
if let Ok(custom_path) = std::env::var("RTK_DB_PATH") {
|
|
954
|
-
return Ok(PathBuf::from(custom_path));
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// Priority 2: Configuration file
|
|
958
|
-
if let Ok(config) = crate::config::Config::load() {
|
|
959
|
-
if let Some(db_path) = config.tracking.database_path {
|
|
960
|
-
return Ok(db_path);
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
// Priority 3: Default platform-specific location
|
|
965
|
-
let data_dir = dirs::data_local_dir().unwrap_or_else(|| PathBuf::from("."));
|
|
966
|
-
Ok(data_dir.join("rtk").join("history.db"))
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
/// Individual parse failure record.
|
|
970
|
-
#[derive(Debug)]
|
|
971
|
-
pub struct ParseFailureRecord {
|
|
972
|
-
pub timestamp: String,
|
|
973
|
-
pub raw_command: String,
|
|
974
|
-
pub error_message: String,
|
|
975
|
-
pub fallback_succeeded: bool,
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
/// Aggregated parse failure summary.
|
|
979
|
-
#[derive(Debug)]
|
|
980
|
-
pub struct ParseFailureSummary {
|
|
981
|
-
pub total: usize,
|
|
982
|
-
pub recovery_rate: f64,
|
|
983
|
-
pub top_commands: Vec<(String, usize)>,
|
|
984
|
-
pub recent: Vec<ParseFailureRecord>,
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
/// Record a parse failure without ever crashing.
|
|
988
|
-
/// Silently ignores all errors — used in the fallback path.
|
|
989
|
-
pub fn record_parse_failure_silent(raw_command: &str, error_message: &str, succeeded: bool) {
|
|
990
|
-
if let Ok(tracker) = Tracker::new() {
|
|
991
|
-
let _ = tracker.record_parse_failure(raw_command, error_message, succeeded);
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
/// Estimate token count from text using ~4 chars = 1 token heuristic.
|
|
996
|
-
///
|
|
997
|
-
/// This is a fast approximation suitable for tracking purposes.
|
|
998
|
-
/// For precise counts, integrate with your LLM's tokenizer API.
|
|
999
|
-
///
|
|
1000
|
-
/// # Formula
|
|
1001
|
-
///
|
|
1002
|
-
/// `tokens = ceil(chars / 4)`
|
|
1003
|
-
///
|
|
1004
|
-
/// # Examples
|
|
1005
|
-
///
|
|
1006
|
-
/// ```
|
|
1007
|
-
/// use rtk::tracking::estimate_tokens;
|
|
1008
|
-
///
|
|
1009
|
-
/// assert_eq!(estimate_tokens(""), 0);
|
|
1010
|
-
/// assert_eq!(estimate_tokens("abcd"), 1); // 4 chars = 1 token
|
|
1011
|
-
/// assert_eq!(estimate_tokens("abcde"), 2); // 5 chars = ceil(1.25) = 2
|
|
1012
|
-
/// assert_eq!(estimate_tokens("hello world"), 3); // 11 chars = ceil(2.75) = 3
|
|
1013
|
-
/// ```
|
|
1014
|
-
pub fn estimate_tokens(text: &str) -> usize {
|
|
1015
|
-
// ~4 chars per token on average
|
|
1016
|
-
(text.len() as f64 / 4.0).ceil() as usize
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
/// Helper struct for timing command execution
|
|
1020
|
-
/// Helper for timing command execution and tracking results.
|
|
1021
|
-
///
|
|
1022
|
-
/// Preferred API for tracking commands. Automatically measures execution time
|
|
1023
|
-
/// and records token savings. Use instead of the deprecated [`track`] function.
|
|
1024
|
-
///
|
|
1025
|
-
/// # Examples
|
|
1026
|
-
///
|
|
1027
|
-
/// ```no_run
|
|
1028
|
-
/// use rtk::tracking::TimedExecution;
|
|
1029
|
-
///
|
|
1030
|
-
/// let timer = TimedExecution::start();
|
|
1031
|
-
/// let input = execute_standard_command()?;
|
|
1032
|
-
/// let output = execute_rtk_command()?;
|
|
1033
|
-
/// timer.track("ls -la", "rtk ls", &input, &output);
|
|
1034
|
-
/// # Ok::<(), anyhow::Error>(())
|
|
1035
|
-
/// ```
|
|
1036
|
-
pub struct TimedExecution {
|
|
1037
|
-
start: Instant,
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
impl TimedExecution {
|
|
1041
|
-
/// Start timing a command execution.
|
|
1042
|
-
///
|
|
1043
|
-
/// Creates a new timer that starts measuring elapsed time immediately.
|
|
1044
|
-
/// Call [`track`](Self::track) or [`track_passthrough`](Self::track_passthrough)
|
|
1045
|
-
/// when the command completes.
|
|
1046
|
-
///
|
|
1047
|
-
/// # Examples
|
|
1048
|
-
///
|
|
1049
|
-
/// ```no_run
|
|
1050
|
-
/// use rtk::tracking::TimedExecution;
|
|
1051
|
-
///
|
|
1052
|
-
/// let timer = TimedExecution::start();
|
|
1053
|
-
/// // ... execute command ...
|
|
1054
|
-
/// timer.track("cmd", "rtk cmd", "input", "output");
|
|
1055
|
-
/// ```
|
|
1056
|
-
pub fn start() -> Self {
|
|
1057
|
-
Self {
|
|
1058
|
-
start: Instant::now(),
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
/// Track the command with elapsed time and token counts.
|
|
1063
|
-
///
|
|
1064
|
-
/// Records the command execution with:
|
|
1065
|
-
/// - Elapsed time since [`start`](Self::start)
|
|
1066
|
-
/// - Token counts estimated from input/output strings
|
|
1067
|
-
/// - Calculated savings metrics
|
|
1068
|
-
///
|
|
1069
|
-
/// # Arguments
|
|
1070
|
-
///
|
|
1071
|
-
/// - `original_cmd`: Standard command (e.g., "ls -la")
|
|
1072
|
-
/// - `rtk_cmd`: RTK command used (e.g., "rtk ls")
|
|
1073
|
-
/// - `input`: Standard command output (for token estimation)
|
|
1074
|
-
/// - `output`: RTK command output (for token estimation)
|
|
1075
|
-
///
|
|
1076
|
-
/// # Examples
|
|
1077
|
-
///
|
|
1078
|
-
/// ```no_run
|
|
1079
|
-
/// use rtk::tracking::TimedExecution;
|
|
1080
|
-
///
|
|
1081
|
-
/// let timer = TimedExecution::start();
|
|
1082
|
-
/// let input = "long output...";
|
|
1083
|
-
/// let output = "short output";
|
|
1084
|
-
/// timer.track("ls -la", "rtk ls", input, output);
|
|
1085
|
-
/// ```
|
|
1086
|
-
pub fn track(&self, original_cmd: &str, rtk_cmd: &str, input: &str, output: &str) {
|
|
1087
|
-
let elapsed_ms = self.start.elapsed().as_millis() as u64;
|
|
1088
|
-
let input_tokens = estimate_tokens(input);
|
|
1089
|
-
let output_tokens = estimate_tokens(output);
|
|
1090
|
-
|
|
1091
|
-
if let Ok(tracker) = Tracker::new() {
|
|
1092
|
-
let _ = tracker.record(
|
|
1093
|
-
original_cmd,
|
|
1094
|
-
rtk_cmd,
|
|
1095
|
-
input_tokens,
|
|
1096
|
-
output_tokens,
|
|
1097
|
-
elapsed_ms,
|
|
1098
|
-
);
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
/// Track passthrough commands (timing-only, no token counting).
|
|
1103
|
-
///
|
|
1104
|
-
/// For commands that stream output or run interactively where output
|
|
1105
|
-
/// cannot be captured. Records execution time but sets tokens to 0
|
|
1106
|
-
/// (does not dilute savings statistics).
|
|
1107
|
-
///
|
|
1108
|
-
/// # Arguments
|
|
1109
|
-
///
|
|
1110
|
-
/// - `original_cmd`: Standard command (e.g., "git tag --list")
|
|
1111
|
-
/// - `rtk_cmd`: RTK command used (e.g., "rtk git tag --list")
|
|
1112
|
-
///
|
|
1113
|
-
/// # Examples
|
|
1114
|
-
///
|
|
1115
|
-
/// ```no_run
|
|
1116
|
-
/// use rtk::tracking::TimedExecution;
|
|
1117
|
-
///
|
|
1118
|
-
/// let timer = TimedExecution::start();
|
|
1119
|
-
/// // ... execute streaming command ...
|
|
1120
|
-
/// timer.track_passthrough("git tag", "rtk git tag");
|
|
1121
|
-
/// ```
|
|
1122
|
-
pub fn track_passthrough(&self, original_cmd: &str, rtk_cmd: &str) {
|
|
1123
|
-
let elapsed_ms = self.start.elapsed().as_millis() as u64;
|
|
1124
|
-
// input_tokens=0, output_tokens=0 won't dilute savings statistics
|
|
1125
|
-
if let Ok(tracker) = Tracker::new() {
|
|
1126
|
-
let _ = tracker.record(original_cmd, rtk_cmd, 0, 0, elapsed_ms);
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
/// Format OsString args for tracking display.
|
|
1132
|
-
///
|
|
1133
|
-
/// Joins arguments with spaces, converting each to UTF-8 (lossy).
|
|
1134
|
-
/// Useful for displaying command arguments in tracking records.
|
|
1135
|
-
///
|
|
1136
|
-
/// # Examples
|
|
1137
|
-
///
|
|
1138
|
-
/// ```
|
|
1139
|
-
/// use std::ffi::OsString;
|
|
1140
|
-
/// use rtk::tracking::args_display;
|
|
1141
|
-
///
|
|
1142
|
-
/// let args = vec![OsString::from("status"), OsString::from("--short")];
|
|
1143
|
-
/// assert_eq!(args_display(&args), "status --short");
|
|
1144
|
-
/// ```
|
|
1145
|
-
pub fn args_display(args: &[OsString]) -> String {
|
|
1146
|
-
args.iter()
|
|
1147
|
-
.map(|a| a.to_string_lossy())
|
|
1148
|
-
.collect::<Vec<_>>()
|
|
1149
|
-
.join(" ")
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
/// Track a command execution (legacy function, use [`TimedExecution`] for new code).
|
|
1153
|
-
///
|
|
1154
|
-
/// # Deprecation Notice
|
|
1155
|
-
///
|
|
1156
|
-
/// This function is deprecated. Use [`TimedExecution`] instead for automatic
|
|
1157
|
-
/// timing and cleaner API.
|
|
1158
|
-
///
|
|
1159
|
-
/// # Arguments
|
|
1160
|
-
///
|
|
1161
|
-
/// - `original_cmd`: Standard command (e.g., "ls -la")
|
|
1162
|
-
/// - `rtk_cmd`: RTK command used (e.g., "rtk ls")
|
|
1163
|
-
/// - `input`: Standard command output (for token estimation)
|
|
1164
|
-
/// - `output`: RTK command output (for token estimation)
|
|
1165
|
-
///
|
|
1166
|
-
/// # Migration
|
|
1167
|
-
///
|
|
1168
|
-
/// ```no_run
|
|
1169
|
-
/// # use rtk::tracking::{track, TimedExecution};
|
|
1170
|
-
/// // Old (deprecated)
|
|
1171
|
-
/// track("ls -la", "rtk ls", "input", "output");
|
|
1172
|
-
///
|
|
1173
|
-
/// // New (preferred)
|
|
1174
|
-
/// let timer = TimedExecution::start();
|
|
1175
|
-
/// timer.track("ls -la", "rtk ls", "input", "output");
|
|
1176
|
-
/// ```
|
|
1177
|
-
#[deprecated(note = "Use TimedExecution instead")]
|
|
1178
|
-
pub fn track(original_cmd: &str, rtk_cmd: &str, input: &str, output: &str) {
|
|
1179
|
-
let input_tokens = estimate_tokens(input);
|
|
1180
|
-
let output_tokens = estimate_tokens(output);
|
|
1181
|
-
|
|
1182
|
-
if let Ok(tracker) = Tracker::new() {
|
|
1183
|
-
let _ = tracker.record(original_cmd, rtk_cmd, input_tokens, output_tokens, 0);
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
#[cfg(test)]
|
|
1188
|
-
mod tests {
|
|
1189
|
-
use super::*;
|
|
1190
|
-
|
|
1191
|
-
// 1. estimate_tokens — verify ~4 chars/token ratio
|
|
1192
|
-
#[test]
|
|
1193
|
-
fn test_estimate_tokens() {
|
|
1194
|
-
assert_eq!(estimate_tokens(""), 0);
|
|
1195
|
-
assert_eq!(estimate_tokens("abcd"), 1); // 4 chars = 1 token
|
|
1196
|
-
assert_eq!(estimate_tokens("abcde"), 2); // 5 chars = ceil(1.25) = 2
|
|
1197
|
-
assert_eq!(estimate_tokens("a"), 1); // 1 char = ceil(0.25) = 1
|
|
1198
|
-
assert_eq!(estimate_tokens("12345678"), 2); // 8 chars = 2 tokens
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
// 2. args_display — format OsString vec
|
|
1202
|
-
#[test]
|
|
1203
|
-
fn test_args_display() {
|
|
1204
|
-
let args = vec![OsString::from("status"), OsString::from("--short")];
|
|
1205
|
-
assert_eq!(args_display(&args), "status --short");
|
|
1206
|
-
assert_eq!(args_display(&[]), "");
|
|
1207
|
-
|
|
1208
|
-
let single = vec![OsString::from("log")];
|
|
1209
|
-
assert_eq!(args_display(&single), "log");
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
// 3. Tracker::record + get_recent — round-trip DB
|
|
1213
|
-
#[test]
|
|
1214
|
-
fn test_tracker_record_and_recent() {
|
|
1215
|
-
let tracker = Tracker::new().expect("Failed to create tracker");
|
|
1216
|
-
|
|
1217
|
-
// Use unique test identifier to avoid conflicts with other tests
|
|
1218
|
-
let test_cmd = format!("rtk git status test_{}", std::process::id());
|
|
1219
|
-
|
|
1220
|
-
tracker
|
|
1221
|
-
.record("git status", &test_cmd, 100, 20, 50)
|
|
1222
|
-
.expect("Failed to record");
|
|
1223
|
-
|
|
1224
|
-
let recent = tracker.get_recent(10).expect("Failed to get recent");
|
|
1225
|
-
|
|
1226
|
-
// Find our specific test record
|
|
1227
|
-
let test_record = recent
|
|
1228
|
-
.iter()
|
|
1229
|
-
.find(|r| r.rtk_cmd == test_cmd)
|
|
1230
|
-
.expect("Test record not found in recent commands");
|
|
1231
|
-
|
|
1232
|
-
assert_eq!(test_record.saved_tokens, 80);
|
|
1233
|
-
assert_eq!(test_record.savings_pct, 80.0);
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// 4. track_passthrough doesn't dilute stats (input=0, output=0)
|
|
1237
|
-
#[test]
|
|
1238
|
-
fn test_track_passthrough_no_dilution() {
|
|
1239
|
-
let tracker = Tracker::new().expect("Failed to create tracker");
|
|
1240
|
-
|
|
1241
|
-
// Use unique test identifiers
|
|
1242
|
-
let pid = std::process::id();
|
|
1243
|
-
let cmd1 = format!("rtk cmd1_test_{}", pid);
|
|
1244
|
-
let cmd2 = format!("rtk cmd2_passthrough_test_{}", pid);
|
|
1245
|
-
|
|
1246
|
-
// Record one real command with 80% savings
|
|
1247
|
-
tracker
|
|
1248
|
-
.record("cmd1", &cmd1, 1000, 200, 10)
|
|
1249
|
-
.expect("Failed to record cmd1");
|
|
1250
|
-
|
|
1251
|
-
// Record passthrough (0, 0)
|
|
1252
|
-
tracker
|
|
1253
|
-
.record("cmd2", &cmd2, 0, 0, 5)
|
|
1254
|
-
.expect("Failed to record passthrough");
|
|
1255
|
-
|
|
1256
|
-
// Verify both records exist in recent history
|
|
1257
|
-
let recent = tracker.get_recent(20).expect("Failed to get recent");
|
|
1258
|
-
|
|
1259
|
-
let record1 = recent
|
|
1260
|
-
.iter()
|
|
1261
|
-
.find(|r| r.rtk_cmd == cmd1)
|
|
1262
|
-
.expect("cmd1 record not found");
|
|
1263
|
-
let record2 = recent
|
|
1264
|
-
.iter()
|
|
1265
|
-
.find(|r| r.rtk_cmd == cmd2)
|
|
1266
|
-
.expect("passthrough record not found");
|
|
1267
|
-
|
|
1268
|
-
// Verify cmd1 has 80% savings
|
|
1269
|
-
assert_eq!(record1.saved_tokens, 800);
|
|
1270
|
-
assert_eq!(record1.savings_pct, 80.0);
|
|
1271
|
-
|
|
1272
|
-
// Verify passthrough has 0% savings
|
|
1273
|
-
assert_eq!(record2.saved_tokens, 0);
|
|
1274
|
-
assert_eq!(record2.savings_pct, 0.0);
|
|
1275
|
-
|
|
1276
|
-
// This validates that passthrough (0 input, 0 output) doesn't dilute stats
|
|
1277
|
-
// because the savings calculation is correct for both cases
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
// 5. TimedExecution::track records with exec_time > 0
|
|
1281
|
-
#[test]
|
|
1282
|
-
fn test_timed_execution_records_time() {
|
|
1283
|
-
let timer = TimedExecution::start();
|
|
1284
|
-
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
1285
|
-
timer.track("test cmd", "rtk test", "raw input data", "filtered");
|
|
1286
|
-
|
|
1287
|
-
// Verify via DB that record exists
|
|
1288
|
-
let tracker = Tracker::new().expect("Failed to create tracker");
|
|
1289
|
-
let recent = tracker.get_recent(5).expect("Failed to get recent");
|
|
1290
|
-
assert!(recent.iter().any(|r| r.rtk_cmd == "rtk test"));
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
// 6. TimedExecution::track_passthrough records with 0 tokens
|
|
1294
|
-
#[test]
|
|
1295
|
-
fn test_timed_execution_passthrough() {
|
|
1296
|
-
let timer = TimedExecution::start();
|
|
1297
|
-
timer.track_passthrough("git tag", "rtk git tag (passthrough)");
|
|
1298
|
-
|
|
1299
|
-
let tracker = Tracker::new().expect("Failed to create tracker");
|
|
1300
|
-
let recent = tracker.get_recent(5).expect("Failed to get recent");
|
|
1301
|
-
|
|
1302
|
-
let pt = recent
|
|
1303
|
-
.iter()
|
|
1304
|
-
.find(|r| r.rtk_cmd.contains("passthrough"))
|
|
1305
|
-
.expect("Passthrough record not found");
|
|
1306
|
-
|
|
1307
|
-
// savings_pct should be 0 for passthrough
|
|
1308
|
-
assert_eq!(pt.savings_pct, 0.0);
|
|
1309
|
-
assert_eq!(pt.saved_tokens, 0);
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
// 7. get_db_path respects environment variable RTK_DB_PATH
|
|
1313
|
-
#[test]
|
|
1314
|
-
fn test_custom_db_path_env() {
|
|
1315
|
-
use std::env;
|
|
1316
|
-
|
|
1317
|
-
let custom_path = "/tmp/rtk_test_custom.db";
|
|
1318
|
-
env::set_var("RTK_DB_PATH", custom_path);
|
|
1319
|
-
|
|
1320
|
-
let db_path = get_db_path().expect("Failed to get db path");
|
|
1321
|
-
assert_eq!(db_path, PathBuf::from(custom_path));
|
|
1322
|
-
|
|
1323
|
-
env::remove_var("RTK_DB_PATH");
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
// 8. get_db_path falls back to default when no custom config
|
|
1327
|
-
#[test]
|
|
1328
|
-
fn test_default_db_path() {
|
|
1329
|
-
use std::env;
|
|
1330
|
-
|
|
1331
|
-
// Ensure no env var is set
|
|
1332
|
-
env::remove_var("RTK_DB_PATH");
|
|
1333
|
-
|
|
1334
|
-
let db_path = get_db_path().expect("Failed to get db path");
|
|
1335
|
-
assert!(db_path.ends_with("rtk/history.db"));
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
// 9. project_filter_params uses GLOB pattern with * wildcard // added
|
|
1339
|
-
#[test]
|
|
1340
|
-
fn test_project_filter_params_glob_pattern() {
|
|
1341
|
-
let (exact, glob) = project_filter_params(Some("/home/user/project"));
|
|
1342
|
-
assert_eq!(exact.unwrap(), "/home/user/project");
|
|
1343
|
-
// Must use * (GLOB) not % (LIKE) for subdirectory prefix matching
|
|
1344
|
-
let glob_val = glob.unwrap();
|
|
1345
|
-
assert!(glob_val.ends_with('*'), "GLOB pattern must end with *");
|
|
1346
|
-
assert!(!glob_val.contains('%'), "Must not contain LIKE wildcard %");
|
|
1347
|
-
assert_eq!(
|
|
1348
|
-
glob_val,
|
|
1349
|
-
format!("/home/user/project{}*", std::path::MAIN_SEPARATOR)
|
|
1350
|
-
);
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
// 10. project_filter_params returns None for None input // added
|
|
1354
|
-
#[test]
|
|
1355
|
-
fn test_project_filter_params_none() {
|
|
1356
|
-
let (exact, glob) = project_filter_params(None);
|
|
1357
|
-
assert!(exact.is_none());
|
|
1358
|
-
assert!(glob.is_none());
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
// 11. GLOB pattern safe with underscores in path names // added
|
|
1362
|
-
#[test]
|
|
1363
|
-
fn test_project_filter_params_underscore_safe() {
|
|
1364
|
-
// In LIKE, _ matches any single char; in GLOB, _ is literal
|
|
1365
|
-
let (exact, glob) = project_filter_params(Some("/home/user/my_project"));
|
|
1366
|
-
assert_eq!(exact.unwrap(), "/home/user/my_project");
|
|
1367
|
-
let glob_val = glob.unwrap();
|
|
1368
|
-
// _ must be preserved literally (GLOB treats _ as literal, LIKE does not)
|
|
1369
|
-
assert!(glob_val.contains("my_project"));
|
|
1370
|
-
assert_eq!(
|
|
1371
|
-
glob_val,
|
|
1372
|
-
format!("/home/user/my_project{}*", std::path::MAIN_SEPARATOR)
|
|
1373
|
-
);
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
// 12. record_parse_failure + get_parse_failure_summary roundtrip
|
|
1377
|
-
#[test]
|
|
1378
|
-
fn test_parse_failure_roundtrip() {
|
|
1379
|
-
let tracker = Tracker::new().expect("Failed to create tracker");
|
|
1380
|
-
let test_cmd = format!("git -C /path status test_{}", std::process::id());
|
|
1381
|
-
|
|
1382
|
-
tracker
|
|
1383
|
-
.record_parse_failure(&test_cmd, "unrecognized subcommand", true)
|
|
1384
|
-
.expect("Failed to record parse failure");
|
|
1385
|
-
|
|
1386
|
-
let summary = tracker
|
|
1387
|
-
.get_parse_failure_summary()
|
|
1388
|
-
.expect("Failed to get summary");
|
|
1389
|
-
|
|
1390
|
-
assert!(summary.total >= 1);
|
|
1391
|
-
assert!(summary.recent.iter().any(|r| r.raw_command == test_cmd));
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
// 13. recovery_rate calculation
|
|
1395
|
-
#[test]
|
|
1396
|
-
fn test_parse_failure_recovery_rate() {
|
|
1397
|
-
let tracker = Tracker::new().expect("Failed to create tracker");
|
|
1398
|
-
let pid = std::process::id();
|
|
1399
|
-
|
|
1400
|
-
// 2 successes, 1 failure
|
|
1401
|
-
tracker
|
|
1402
|
-
.record_parse_failure(&format!("cmd_ok1_{}", pid), "err", true)
|
|
1403
|
-
.unwrap();
|
|
1404
|
-
tracker
|
|
1405
|
-
.record_parse_failure(&format!("cmd_ok2_{}", pid), "err", true)
|
|
1406
|
-
.unwrap();
|
|
1407
|
-
tracker
|
|
1408
|
-
.record_parse_failure(&format!("cmd_fail_{}", pid), "err", false)
|
|
1409
|
-
.unwrap();
|
|
1410
|
-
|
|
1411
|
-
let summary = tracker.get_parse_failure_summary().unwrap();
|
|
1412
|
-
// We can't assert exact rate because other tests may have added records,
|
|
1413
|
-
// but we can verify recovery_rate is between 0 and 100
|
|
1414
|
-
assert!(summary.recovery_rate >= 0.0 && summary.recovery_rate <= 100.0);
|
|
1415
|
-
}
|
|
1416
|
-
}
|