@hasna/terminal 2.3.0 → 2.3.1

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.
Files changed (202) hide show
  1. package/dist/cli.js +64 -16
  2. package/package.json +1 -1
  3. package/src/ai.ts +8 -0
  4. package/src/cli.tsx +57 -18
  5. package/src/output-processor.ts +6 -1
  6. package/src/output-store.ts +58 -12
  7. package/src/tool-profiles.ts +139 -0
  8. package/temp/rtk/.claude/agents/code-reviewer.md +0 -221
  9. package/temp/rtk/.claude/agents/debugger.md +0 -519
  10. package/temp/rtk/.claude/agents/rtk-testing-specialist.md +0 -461
  11. package/temp/rtk/.claude/agents/rust-rtk.md +0 -511
  12. package/temp/rtk/.claude/agents/technical-writer.md +0 -355
  13. package/temp/rtk/.claude/commands/diagnose.md +0 -352
  14. package/temp/rtk/.claude/commands/test-routing.md +0 -362
  15. package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +0 -16
  16. package/temp/rtk/.claude/hooks/rtk-rewrite.sh +0 -70
  17. package/temp/rtk/.claude/hooks/rtk-suggest.sh +0 -152
  18. package/temp/rtk/.claude/rules/cli-testing.md +0 -526
  19. package/temp/rtk/.claude/skills/issue-triage/SKILL.md +0 -348
  20. package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +0 -134
  21. package/temp/rtk/.claude/skills/performance.md +0 -435
  22. package/temp/rtk/.claude/skills/pr-triage/SKILL.md +0 -315
  23. package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +0 -71
  24. package/temp/rtk/.claude/skills/repo-recap.md +0 -206
  25. package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +0 -78
  26. package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +0 -124
  27. package/temp/rtk/.claude/skills/security-guardian.md +0 -503
  28. package/temp/rtk/.claude/skills/ship.md +0 -404
  29. package/temp/rtk/.github/workflows/benchmark.yml +0 -34
  30. package/temp/rtk/.github/workflows/dco-check.yaml +0 -12
  31. package/temp/rtk/.github/workflows/release-please.yml +0 -51
  32. package/temp/rtk/.github/workflows/release.yml +0 -343
  33. package/temp/rtk/.github/workflows/security-check.yml +0 -135
  34. package/temp/rtk/.github/workflows/validate-docs.yml +0 -78
  35. package/temp/rtk/.release-please-manifest.json +0 -3
  36. package/temp/rtk/ARCHITECTURE.md +0 -1491
  37. package/temp/rtk/CHANGELOG.md +0 -640
  38. package/temp/rtk/CLAUDE.md +0 -605
  39. package/temp/rtk/CONTRIBUTING.md +0 -199
  40. package/temp/rtk/Cargo.lock +0 -1668
  41. package/temp/rtk/Cargo.toml +0 -64
  42. package/temp/rtk/Formula/rtk.rb +0 -43
  43. package/temp/rtk/INSTALL.md +0 -390
  44. package/temp/rtk/LICENSE +0 -21
  45. package/temp/rtk/README.md +0 -386
  46. package/temp/rtk/README_es.md +0 -159
  47. package/temp/rtk/README_fr.md +0 -197
  48. package/temp/rtk/README_ja.md +0 -159
  49. package/temp/rtk/README_ko.md +0 -159
  50. package/temp/rtk/README_zh.md +0 -167
  51. package/temp/rtk/ROADMAP.md +0 -15
  52. package/temp/rtk/SECURITY.md +0 -217
  53. package/temp/rtk/TEST_EXEC_TIME.md +0 -102
  54. package/temp/rtk/build.rs +0 -57
  55. package/temp/rtk/docs/AUDIT_GUIDE.md +0 -432
  56. package/temp/rtk/docs/FEATURES.md +0 -1410
  57. package/temp/rtk/docs/TROUBLESHOOTING.md +0 -309
  58. package/temp/rtk/docs/filter-workflow.md +0 -102
  59. package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
  60. package/temp/rtk/docs/tracking.md +0 -583
  61. package/temp/rtk/hooks/opencode-rtk.ts +0 -39
  62. package/temp/rtk/hooks/rtk-awareness.md +0 -29
  63. package/temp/rtk/hooks/rtk-rewrite.sh +0 -61
  64. package/temp/rtk/hooks/test-rtk-rewrite.sh +0 -442
  65. package/temp/rtk/install.sh +0 -124
  66. package/temp/rtk/release-please-config.json +0 -10
  67. package/temp/rtk/scripts/benchmark.sh +0 -592
  68. package/temp/rtk/scripts/check-installation.sh +0 -162
  69. package/temp/rtk/scripts/install-local.sh +0 -37
  70. package/temp/rtk/scripts/rtk-economics.sh +0 -137
  71. package/temp/rtk/scripts/test-all.sh +0 -561
  72. package/temp/rtk/scripts/test-aristote.sh +0 -227
  73. package/temp/rtk/scripts/test-tracking.sh +0 -79
  74. package/temp/rtk/scripts/update-readme-metrics.sh +0 -32
  75. package/temp/rtk/scripts/validate-docs.sh +0 -73
  76. package/temp/rtk/src/aws_cmd.rs +0 -880
  77. package/temp/rtk/src/binlog.rs +0 -1645
  78. package/temp/rtk/src/cargo_cmd.rs +0 -1727
  79. package/temp/rtk/src/cc_economics.rs +0 -1157
  80. package/temp/rtk/src/ccusage.rs +0 -340
  81. package/temp/rtk/src/config.rs +0 -187
  82. package/temp/rtk/src/container.rs +0 -855
  83. package/temp/rtk/src/curl_cmd.rs +0 -134
  84. package/temp/rtk/src/deps.rs +0 -268
  85. package/temp/rtk/src/diff_cmd.rs +0 -367
  86. package/temp/rtk/src/discover/mod.rs +0 -274
  87. package/temp/rtk/src/discover/provider.rs +0 -388
  88. package/temp/rtk/src/discover/registry.rs +0 -2022
  89. package/temp/rtk/src/discover/report.rs +0 -202
  90. package/temp/rtk/src/discover/rules.rs +0 -667
  91. package/temp/rtk/src/display_helpers.rs +0 -402
  92. package/temp/rtk/src/dotnet_cmd.rs +0 -1771
  93. package/temp/rtk/src/dotnet_format_report.rs +0 -133
  94. package/temp/rtk/src/dotnet_trx.rs +0 -593
  95. package/temp/rtk/src/env_cmd.rs +0 -204
  96. package/temp/rtk/src/filter.rs +0 -462
  97. package/temp/rtk/src/filters/README.md +0 -52
  98. package/temp/rtk/src/filters/ansible-playbook.toml +0 -34
  99. package/temp/rtk/src/filters/basedpyright.toml +0 -47
  100. package/temp/rtk/src/filters/biome.toml +0 -45
  101. package/temp/rtk/src/filters/brew-install.toml +0 -37
  102. package/temp/rtk/src/filters/composer-install.toml +0 -40
  103. package/temp/rtk/src/filters/df.toml +0 -16
  104. package/temp/rtk/src/filters/dotnet-build.toml +0 -64
  105. package/temp/rtk/src/filters/du.toml +0 -16
  106. package/temp/rtk/src/filters/fail2ban-client.toml +0 -15
  107. package/temp/rtk/src/filters/gcc.toml +0 -49
  108. package/temp/rtk/src/filters/gcloud.toml +0 -22
  109. package/temp/rtk/src/filters/hadolint.toml +0 -24
  110. package/temp/rtk/src/filters/helm.toml +0 -29
  111. package/temp/rtk/src/filters/iptables.toml +0 -27
  112. package/temp/rtk/src/filters/jj.toml +0 -28
  113. package/temp/rtk/src/filters/jq.toml +0 -24
  114. package/temp/rtk/src/filters/make.toml +0 -41
  115. package/temp/rtk/src/filters/markdownlint.toml +0 -24
  116. package/temp/rtk/src/filters/mix-compile.toml +0 -27
  117. package/temp/rtk/src/filters/mix-format.toml +0 -15
  118. package/temp/rtk/src/filters/mvn-build.toml +0 -44
  119. package/temp/rtk/src/filters/oxlint.toml +0 -43
  120. package/temp/rtk/src/filters/ping.toml +0 -63
  121. package/temp/rtk/src/filters/pio-run.toml +0 -40
  122. package/temp/rtk/src/filters/poetry-install.toml +0 -50
  123. package/temp/rtk/src/filters/pre-commit.toml +0 -35
  124. package/temp/rtk/src/filters/ps.toml +0 -16
  125. package/temp/rtk/src/filters/quarto-render.toml +0 -41
  126. package/temp/rtk/src/filters/rsync.toml +0 -48
  127. package/temp/rtk/src/filters/shellcheck.toml +0 -27
  128. package/temp/rtk/src/filters/shopify-theme.toml +0 -29
  129. package/temp/rtk/src/filters/skopeo.toml +0 -45
  130. package/temp/rtk/src/filters/sops.toml +0 -16
  131. package/temp/rtk/src/filters/ssh.toml +0 -44
  132. package/temp/rtk/src/filters/stat.toml +0 -34
  133. package/temp/rtk/src/filters/swift-build.toml +0 -41
  134. package/temp/rtk/src/filters/systemctl-status.toml +0 -33
  135. package/temp/rtk/src/filters/terraform-plan.toml +0 -35
  136. package/temp/rtk/src/filters/tofu-fmt.toml +0 -16
  137. package/temp/rtk/src/filters/tofu-init.toml +0 -38
  138. package/temp/rtk/src/filters/tofu-plan.toml +0 -35
  139. package/temp/rtk/src/filters/tofu-validate.toml +0 -17
  140. package/temp/rtk/src/filters/trunk-build.toml +0 -39
  141. package/temp/rtk/src/filters/ty.toml +0 -50
  142. package/temp/rtk/src/filters/uv-sync.toml +0 -37
  143. package/temp/rtk/src/filters/xcodebuild.toml +0 -99
  144. package/temp/rtk/src/filters/yamllint.toml +0 -25
  145. package/temp/rtk/src/find_cmd.rs +0 -598
  146. package/temp/rtk/src/format_cmd.rs +0 -386
  147. package/temp/rtk/src/gain.rs +0 -723
  148. package/temp/rtk/src/gh_cmd.rs +0 -1651
  149. package/temp/rtk/src/git.rs +0 -2012
  150. package/temp/rtk/src/go_cmd.rs +0 -592
  151. package/temp/rtk/src/golangci_cmd.rs +0 -254
  152. package/temp/rtk/src/grep_cmd.rs +0 -288
  153. package/temp/rtk/src/gt_cmd.rs +0 -810
  154. package/temp/rtk/src/hook_audit_cmd.rs +0 -283
  155. package/temp/rtk/src/hook_check.rs +0 -171
  156. package/temp/rtk/src/init.rs +0 -1859
  157. package/temp/rtk/src/integrity.rs +0 -537
  158. package/temp/rtk/src/json_cmd.rs +0 -231
  159. package/temp/rtk/src/learn/detector.rs +0 -628
  160. package/temp/rtk/src/learn/mod.rs +0 -119
  161. package/temp/rtk/src/learn/report.rs +0 -184
  162. package/temp/rtk/src/lint_cmd.rs +0 -694
  163. package/temp/rtk/src/local_llm.rs +0 -316
  164. package/temp/rtk/src/log_cmd.rs +0 -248
  165. package/temp/rtk/src/ls.rs +0 -324
  166. package/temp/rtk/src/main.rs +0 -2482
  167. package/temp/rtk/src/mypy_cmd.rs +0 -389
  168. package/temp/rtk/src/next_cmd.rs +0 -241
  169. package/temp/rtk/src/npm_cmd.rs +0 -236
  170. package/temp/rtk/src/parser/README.md +0 -267
  171. package/temp/rtk/src/parser/error.rs +0 -46
  172. package/temp/rtk/src/parser/formatter.rs +0 -336
  173. package/temp/rtk/src/parser/mod.rs +0 -311
  174. package/temp/rtk/src/parser/types.rs +0 -119
  175. package/temp/rtk/src/pip_cmd.rs +0 -302
  176. package/temp/rtk/src/playwright_cmd.rs +0 -479
  177. package/temp/rtk/src/pnpm_cmd.rs +0 -573
  178. package/temp/rtk/src/prettier_cmd.rs +0 -221
  179. package/temp/rtk/src/prisma_cmd.rs +0 -482
  180. package/temp/rtk/src/psql_cmd.rs +0 -382
  181. package/temp/rtk/src/pytest_cmd.rs +0 -384
  182. package/temp/rtk/src/read.rs +0 -217
  183. package/temp/rtk/src/rewrite_cmd.rs +0 -50
  184. package/temp/rtk/src/ruff_cmd.rs +0 -402
  185. package/temp/rtk/src/runner.rs +0 -271
  186. package/temp/rtk/src/summary.rs +0 -297
  187. package/temp/rtk/src/tee.rs +0 -405
  188. package/temp/rtk/src/telemetry.rs +0 -248
  189. package/temp/rtk/src/toml_filter.rs +0 -1655
  190. package/temp/rtk/src/tracking.rs +0 -1416
  191. package/temp/rtk/src/tree.rs +0 -209
  192. package/temp/rtk/src/tsc_cmd.rs +0 -259
  193. package/temp/rtk/src/utils.rs +0 -432
  194. package/temp/rtk/src/verify_cmd.rs +0 -47
  195. package/temp/rtk/src/vitest_cmd.rs +0 -385
  196. package/temp/rtk/src/wc_cmd.rs +0 -401
  197. package/temp/rtk/src/wget_cmd.rs +0 -260
  198. package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +0 -11
  199. package/temp/rtk/tests/fixtures/dotnet/format_changes.json +0 -31
  200. package/temp/rtk/tests/fixtures/dotnet/format_empty.json +0 -1
  201. package/temp/rtk/tests/fixtures/dotnet/format_success.json +0 -12
  202. package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +0 -18
@@ -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
- }