@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.
Files changed (267) hide show
  1. package/dist/App.js +404 -0
  2. package/dist/Browse.js +79 -0
  3. package/dist/FuzzyPicker.js +47 -0
  4. package/dist/Onboarding.js +51 -0
  5. package/dist/Spinner.js +12 -0
  6. package/dist/StatusBar.js +49 -0
  7. package/dist/ai.js +322 -0
  8. package/dist/cache.js +41 -0
  9. package/dist/cli.js +64 -16
  10. package/dist/command-rewriter.js +64 -0
  11. package/dist/command-validator.js +86 -0
  12. package/dist/compression.js +107 -0
  13. package/dist/context-hints.js +275 -0
  14. package/dist/diff-cache.js +107 -0
  15. package/dist/discover.js +212 -0
  16. package/dist/economy.js +123 -0
  17. package/dist/expand-store.js +38 -0
  18. package/dist/file-cache.js +72 -0
  19. package/dist/file-index.js +62 -0
  20. package/dist/history.js +62 -0
  21. package/dist/lazy-executor.js +54 -0
  22. package/dist/line-dedup.js +59 -0
  23. package/dist/loop-detector.js +75 -0
  24. package/dist/mcp/install.js +98 -0
  25. package/dist/mcp/server.js +569 -0
  26. package/dist/noise-filter.js +86 -0
  27. package/dist/output-processor.js +129 -0
  28. package/dist/output-router.js +41 -0
  29. package/dist/output-store.js +111 -0
  30. package/dist/parsers/base.js +2 -0
  31. package/dist/parsers/build.js +64 -0
  32. package/dist/parsers/errors.js +101 -0
  33. package/dist/parsers/files.js +78 -0
  34. package/dist/parsers/git.js +99 -0
  35. package/dist/parsers/index.js +48 -0
  36. package/dist/parsers/tests.js +89 -0
  37. package/dist/providers/anthropic.js +39 -0
  38. package/dist/providers/base.js +4 -0
  39. package/dist/providers/cerebras.js +95 -0
  40. package/dist/providers/groq.js +95 -0
  41. package/dist/providers/index.js +73 -0
  42. package/dist/providers/xai.js +95 -0
  43. package/dist/recipes/model.js +20 -0
  44. package/dist/recipes/storage.js +136 -0
  45. package/dist/search/content-search.js +68 -0
  46. package/dist/search/file-search.js +61 -0
  47. package/dist/search/filters.js +34 -0
  48. package/dist/search/index.js +5 -0
  49. package/dist/search/semantic.js +320 -0
  50. package/dist/session-boot.js +59 -0
  51. package/dist/session-context.js +55 -0
  52. package/dist/sessions-db.js +173 -0
  53. package/dist/smart-display.js +286 -0
  54. package/dist/snapshots.js +51 -0
  55. package/dist/supervisor.js +112 -0
  56. package/dist/test-watchlist.js +131 -0
  57. package/dist/tool-profiles.js +122 -0
  58. package/dist/tree.js +94 -0
  59. package/dist/usage-cache.js +65 -0
  60. package/package.json +8 -1
  61. package/src/ai.ts +8 -0
  62. package/src/cli.tsx +57 -18
  63. package/src/output-processor.ts +6 -1
  64. package/src/output-store.ts +58 -12
  65. package/src/tool-profiles.ts +139 -0
  66. package/.claude/scheduled_tasks.lock +0 -1
  67. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -20
  68. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -14
  69. package/CONTRIBUTING.md +0 -80
  70. package/benchmarks/benchmark.mjs +0 -115
  71. package/imported_modules.txt +0 -0
  72. package/temp/rtk/.claude/agents/code-reviewer.md +0 -221
  73. package/temp/rtk/.claude/agents/debugger.md +0 -519
  74. package/temp/rtk/.claude/agents/rtk-testing-specialist.md +0 -461
  75. package/temp/rtk/.claude/agents/rust-rtk.md +0 -511
  76. package/temp/rtk/.claude/agents/technical-writer.md +0 -355
  77. package/temp/rtk/.claude/commands/diagnose.md +0 -352
  78. package/temp/rtk/.claude/commands/test-routing.md +0 -362
  79. package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +0 -16
  80. package/temp/rtk/.claude/hooks/rtk-rewrite.sh +0 -70
  81. package/temp/rtk/.claude/hooks/rtk-suggest.sh +0 -152
  82. package/temp/rtk/.claude/rules/cli-testing.md +0 -526
  83. package/temp/rtk/.claude/skills/issue-triage/SKILL.md +0 -348
  84. package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +0 -134
  85. package/temp/rtk/.claude/skills/performance.md +0 -435
  86. package/temp/rtk/.claude/skills/pr-triage/SKILL.md +0 -315
  87. package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +0 -71
  88. package/temp/rtk/.claude/skills/repo-recap.md +0 -206
  89. package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +0 -78
  90. package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +0 -124
  91. package/temp/rtk/.claude/skills/security-guardian.md +0 -503
  92. package/temp/rtk/.claude/skills/ship.md +0 -404
  93. package/temp/rtk/.github/workflows/benchmark.yml +0 -34
  94. package/temp/rtk/.github/workflows/dco-check.yaml +0 -12
  95. package/temp/rtk/.github/workflows/release-please.yml +0 -51
  96. package/temp/rtk/.github/workflows/release.yml +0 -343
  97. package/temp/rtk/.github/workflows/security-check.yml +0 -135
  98. package/temp/rtk/.github/workflows/validate-docs.yml +0 -78
  99. package/temp/rtk/.release-please-manifest.json +0 -3
  100. package/temp/rtk/ARCHITECTURE.md +0 -1491
  101. package/temp/rtk/CHANGELOG.md +0 -640
  102. package/temp/rtk/CLAUDE.md +0 -605
  103. package/temp/rtk/CONTRIBUTING.md +0 -199
  104. package/temp/rtk/Cargo.lock +0 -1668
  105. package/temp/rtk/Cargo.toml +0 -64
  106. package/temp/rtk/Formula/rtk.rb +0 -43
  107. package/temp/rtk/INSTALL.md +0 -390
  108. package/temp/rtk/LICENSE +0 -21
  109. package/temp/rtk/README.md +0 -386
  110. package/temp/rtk/README_es.md +0 -159
  111. package/temp/rtk/README_fr.md +0 -197
  112. package/temp/rtk/README_ja.md +0 -159
  113. package/temp/rtk/README_ko.md +0 -159
  114. package/temp/rtk/README_zh.md +0 -167
  115. package/temp/rtk/ROADMAP.md +0 -15
  116. package/temp/rtk/SECURITY.md +0 -217
  117. package/temp/rtk/TEST_EXEC_TIME.md +0 -102
  118. package/temp/rtk/build.rs +0 -57
  119. package/temp/rtk/docs/AUDIT_GUIDE.md +0 -432
  120. package/temp/rtk/docs/FEATURES.md +0 -1410
  121. package/temp/rtk/docs/TROUBLESHOOTING.md +0 -309
  122. package/temp/rtk/docs/filter-workflow.md +0 -102
  123. package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
  124. package/temp/rtk/docs/tracking.md +0 -583
  125. package/temp/rtk/hooks/opencode-rtk.ts +0 -39
  126. package/temp/rtk/hooks/rtk-awareness.md +0 -29
  127. package/temp/rtk/hooks/rtk-rewrite.sh +0 -61
  128. package/temp/rtk/hooks/test-rtk-rewrite.sh +0 -442
  129. package/temp/rtk/install.sh +0 -124
  130. package/temp/rtk/release-please-config.json +0 -10
  131. package/temp/rtk/scripts/benchmark.sh +0 -592
  132. package/temp/rtk/scripts/check-installation.sh +0 -162
  133. package/temp/rtk/scripts/install-local.sh +0 -37
  134. package/temp/rtk/scripts/rtk-economics.sh +0 -137
  135. package/temp/rtk/scripts/test-all.sh +0 -561
  136. package/temp/rtk/scripts/test-aristote.sh +0 -227
  137. package/temp/rtk/scripts/test-tracking.sh +0 -79
  138. package/temp/rtk/scripts/update-readme-metrics.sh +0 -32
  139. package/temp/rtk/scripts/validate-docs.sh +0 -73
  140. package/temp/rtk/src/aws_cmd.rs +0 -880
  141. package/temp/rtk/src/binlog.rs +0 -1645
  142. package/temp/rtk/src/cargo_cmd.rs +0 -1727
  143. package/temp/rtk/src/cc_economics.rs +0 -1157
  144. package/temp/rtk/src/ccusage.rs +0 -340
  145. package/temp/rtk/src/config.rs +0 -187
  146. package/temp/rtk/src/container.rs +0 -855
  147. package/temp/rtk/src/curl_cmd.rs +0 -134
  148. package/temp/rtk/src/deps.rs +0 -268
  149. package/temp/rtk/src/diff_cmd.rs +0 -367
  150. package/temp/rtk/src/discover/mod.rs +0 -274
  151. package/temp/rtk/src/discover/provider.rs +0 -388
  152. package/temp/rtk/src/discover/registry.rs +0 -2022
  153. package/temp/rtk/src/discover/report.rs +0 -202
  154. package/temp/rtk/src/discover/rules.rs +0 -667
  155. package/temp/rtk/src/display_helpers.rs +0 -402
  156. package/temp/rtk/src/dotnet_cmd.rs +0 -1771
  157. package/temp/rtk/src/dotnet_format_report.rs +0 -133
  158. package/temp/rtk/src/dotnet_trx.rs +0 -593
  159. package/temp/rtk/src/env_cmd.rs +0 -204
  160. package/temp/rtk/src/filter.rs +0 -462
  161. package/temp/rtk/src/filters/README.md +0 -52
  162. package/temp/rtk/src/filters/ansible-playbook.toml +0 -34
  163. package/temp/rtk/src/filters/basedpyright.toml +0 -47
  164. package/temp/rtk/src/filters/biome.toml +0 -45
  165. package/temp/rtk/src/filters/brew-install.toml +0 -37
  166. package/temp/rtk/src/filters/composer-install.toml +0 -40
  167. package/temp/rtk/src/filters/df.toml +0 -16
  168. package/temp/rtk/src/filters/dotnet-build.toml +0 -64
  169. package/temp/rtk/src/filters/du.toml +0 -16
  170. package/temp/rtk/src/filters/fail2ban-client.toml +0 -15
  171. package/temp/rtk/src/filters/gcc.toml +0 -49
  172. package/temp/rtk/src/filters/gcloud.toml +0 -22
  173. package/temp/rtk/src/filters/hadolint.toml +0 -24
  174. package/temp/rtk/src/filters/helm.toml +0 -29
  175. package/temp/rtk/src/filters/iptables.toml +0 -27
  176. package/temp/rtk/src/filters/jj.toml +0 -28
  177. package/temp/rtk/src/filters/jq.toml +0 -24
  178. package/temp/rtk/src/filters/make.toml +0 -41
  179. package/temp/rtk/src/filters/markdownlint.toml +0 -24
  180. package/temp/rtk/src/filters/mix-compile.toml +0 -27
  181. package/temp/rtk/src/filters/mix-format.toml +0 -15
  182. package/temp/rtk/src/filters/mvn-build.toml +0 -44
  183. package/temp/rtk/src/filters/oxlint.toml +0 -43
  184. package/temp/rtk/src/filters/ping.toml +0 -63
  185. package/temp/rtk/src/filters/pio-run.toml +0 -40
  186. package/temp/rtk/src/filters/poetry-install.toml +0 -50
  187. package/temp/rtk/src/filters/pre-commit.toml +0 -35
  188. package/temp/rtk/src/filters/ps.toml +0 -16
  189. package/temp/rtk/src/filters/quarto-render.toml +0 -41
  190. package/temp/rtk/src/filters/rsync.toml +0 -48
  191. package/temp/rtk/src/filters/shellcheck.toml +0 -27
  192. package/temp/rtk/src/filters/shopify-theme.toml +0 -29
  193. package/temp/rtk/src/filters/skopeo.toml +0 -45
  194. package/temp/rtk/src/filters/sops.toml +0 -16
  195. package/temp/rtk/src/filters/ssh.toml +0 -44
  196. package/temp/rtk/src/filters/stat.toml +0 -34
  197. package/temp/rtk/src/filters/swift-build.toml +0 -41
  198. package/temp/rtk/src/filters/systemctl-status.toml +0 -33
  199. package/temp/rtk/src/filters/terraform-plan.toml +0 -35
  200. package/temp/rtk/src/filters/tofu-fmt.toml +0 -16
  201. package/temp/rtk/src/filters/tofu-init.toml +0 -38
  202. package/temp/rtk/src/filters/tofu-plan.toml +0 -35
  203. package/temp/rtk/src/filters/tofu-validate.toml +0 -17
  204. package/temp/rtk/src/filters/trunk-build.toml +0 -39
  205. package/temp/rtk/src/filters/ty.toml +0 -50
  206. package/temp/rtk/src/filters/uv-sync.toml +0 -37
  207. package/temp/rtk/src/filters/xcodebuild.toml +0 -99
  208. package/temp/rtk/src/filters/yamllint.toml +0 -25
  209. package/temp/rtk/src/find_cmd.rs +0 -598
  210. package/temp/rtk/src/format_cmd.rs +0 -386
  211. package/temp/rtk/src/gain.rs +0 -723
  212. package/temp/rtk/src/gh_cmd.rs +0 -1651
  213. package/temp/rtk/src/git.rs +0 -2012
  214. package/temp/rtk/src/go_cmd.rs +0 -592
  215. package/temp/rtk/src/golangci_cmd.rs +0 -254
  216. package/temp/rtk/src/grep_cmd.rs +0 -288
  217. package/temp/rtk/src/gt_cmd.rs +0 -810
  218. package/temp/rtk/src/hook_audit_cmd.rs +0 -283
  219. package/temp/rtk/src/hook_check.rs +0 -171
  220. package/temp/rtk/src/init.rs +0 -1859
  221. package/temp/rtk/src/integrity.rs +0 -537
  222. package/temp/rtk/src/json_cmd.rs +0 -231
  223. package/temp/rtk/src/learn/detector.rs +0 -628
  224. package/temp/rtk/src/learn/mod.rs +0 -119
  225. package/temp/rtk/src/learn/report.rs +0 -184
  226. package/temp/rtk/src/lint_cmd.rs +0 -694
  227. package/temp/rtk/src/local_llm.rs +0 -316
  228. package/temp/rtk/src/log_cmd.rs +0 -248
  229. package/temp/rtk/src/ls.rs +0 -324
  230. package/temp/rtk/src/main.rs +0 -2482
  231. package/temp/rtk/src/mypy_cmd.rs +0 -389
  232. package/temp/rtk/src/next_cmd.rs +0 -241
  233. package/temp/rtk/src/npm_cmd.rs +0 -236
  234. package/temp/rtk/src/parser/README.md +0 -267
  235. package/temp/rtk/src/parser/error.rs +0 -46
  236. package/temp/rtk/src/parser/formatter.rs +0 -336
  237. package/temp/rtk/src/parser/mod.rs +0 -311
  238. package/temp/rtk/src/parser/types.rs +0 -119
  239. package/temp/rtk/src/pip_cmd.rs +0 -302
  240. package/temp/rtk/src/playwright_cmd.rs +0 -479
  241. package/temp/rtk/src/pnpm_cmd.rs +0 -573
  242. package/temp/rtk/src/prettier_cmd.rs +0 -221
  243. package/temp/rtk/src/prisma_cmd.rs +0 -482
  244. package/temp/rtk/src/psql_cmd.rs +0 -382
  245. package/temp/rtk/src/pytest_cmd.rs +0 -384
  246. package/temp/rtk/src/read.rs +0 -217
  247. package/temp/rtk/src/rewrite_cmd.rs +0 -50
  248. package/temp/rtk/src/ruff_cmd.rs +0 -402
  249. package/temp/rtk/src/runner.rs +0 -271
  250. package/temp/rtk/src/summary.rs +0 -297
  251. package/temp/rtk/src/tee.rs +0 -405
  252. package/temp/rtk/src/telemetry.rs +0 -248
  253. package/temp/rtk/src/toml_filter.rs +0 -1655
  254. package/temp/rtk/src/tracking.rs +0 -1416
  255. package/temp/rtk/src/tree.rs +0 -209
  256. package/temp/rtk/src/tsc_cmd.rs +0 -259
  257. package/temp/rtk/src/utils.rs +0 -432
  258. package/temp/rtk/src/verify_cmd.rs +0 -47
  259. package/temp/rtk/src/vitest_cmd.rs +0 -385
  260. package/temp/rtk/src/wc_cmd.rs +0 -401
  261. package/temp/rtk/src/wget_cmd.rs +0 -260
  262. package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +0 -11
  263. package/temp/rtk/tests/fixtures/dotnet/format_changes.json +0 -31
  264. package/temp/rtk/tests/fixtures/dotnet/format_empty.json +0 -1
  265. package/temp/rtk/tests/fixtures/dotnet/format_success.json +0 -12
  266. package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +0 -18
  267. package/tsconfig.json +0 -15
@@ -0,0 +1,212 @@
1
+ // Discover — scan Claude Code session history to find token savings opportunities
2
+ // Reads ~/.claude/projects/*/sessions/*.jsonl, extracts Bash commands + output sizes,
3
+ // estimates how much terminal would have saved.
4
+ import { readdirSync, readFileSync, statSync, existsSync } from "fs";
5
+ import { join } from "path";
6
+ import { estimateTokens } from "./parsers/index.js";
7
+ /** Find all Claude session JSONL files */
8
+ function findSessionFiles(claudeDir, maxAge) {
9
+ const files = [];
10
+ const projectsDir = join(claudeDir, "projects");
11
+ if (!existsSync(projectsDir))
12
+ return files;
13
+ const now = Date.now();
14
+ const cutoff = maxAge ? now - maxAge : 0;
15
+ try {
16
+ for (const project of readdirSync(projectsDir)) {
17
+ const projectPath = join(projectsDir, project);
18
+ // Look for session JSONL files (not subagents)
19
+ try {
20
+ for (const entry of readdirSync(projectPath)) {
21
+ if (entry.endsWith(".jsonl")) {
22
+ const filePath = join(projectPath, entry);
23
+ try {
24
+ const stat = statSync(filePath);
25
+ if (stat.mtimeMs > cutoff)
26
+ files.push(filePath);
27
+ }
28
+ catch { }
29
+ }
30
+ }
31
+ }
32
+ catch { }
33
+ }
34
+ }
35
+ catch { }
36
+ return files;
37
+ }
38
+ /** Extract Bash commands and their output sizes from a session file */
39
+ function extractCommands(sessionFile) {
40
+ const commands = [];
41
+ try {
42
+ const content = readFileSync(sessionFile, "utf8");
43
+ const lines = content.split("\n").filter(l => l.trim());
44
+ // Track tool_use IDs to match with tool_results
45
+ const pendingToolUses = new Map(); // id -> command
46
+ for (const line of lines) {
47
+ try {
48
+ const obj = JSON.parse(line);
49
+ const msg = obj.message;
50
+ if (!msg?.content || !Array.isArray(msg.content))
51
+ continue;
52
+ for (const block of msg.content) {
53
+ // Capture Bash tool_use commands
54
+ if (block.type === "tool_use" && block.name === "Bash" && block.input?.command) {
55
+ pendingToolUses.set(block.id, block.input.command);
56
+ }
57
+ // Capture tool_result outputs and match to commands
58
+ if (block.type === "tool_result" && block.tool_use_id) {
59
+ const command = pendingToolUses.get(block.tool_use_id);
60
+ if (command) {
61
+ let outputText = "";
62
+ if (typeof block.content === "string") {
63
+ outputText = block.content;
64
+ }
65
+ else if (Array.isArray(block.content)) {
66
+ outputText = block.content
67
+ .filter((c) => c.type === "text")
68
+ .map((c) => c.text)
69
+ .join("\n");
70
+ }
71
+ if (outputText.length > 0) {
72
+ commands.push({
73
+ command,
74
+ outputTokens: estimateTokens(outputText),
75
+ outputChars: outputText.length,
76
+ sessionFile,
77
+ });
78
+ }
79
+ pendingToolUses.delete(block.tool_use_id);
80
+ }
81
+ }
82
+ }
83
+ }
84
+ catch { } // skip malformed lines
85
+ }
86
+ }
87
+ catch { } // skip unreadable files
88
+ return commands;
89
+ }
90
+ /** Categorize a command into a bucket */
91
+ function categorizeCommand(cmd) {
92
+ const trimmed = cmd.trim();
93
+ if (/^git\b/.test(trimmed))
94
+ return "git";
95
+ if (/\b(bun|npm|yarn|pnpm)\s+(test|run\s+test)/.test(trimmed))
96
+ return "test";
97
+ if (/\b(bun|npm|yarn|pnpm)\s+run\s+(build|typecheck|lint)/.test(trimmed))
98
+ return "build";
99
+ if (/^(grep|rg)\b/.test(trimmed))
100
+ return "grep";
101
+ if (/^find\b/.test(trimmed))
102
+ return "find";
103
+ if (/^(cat|head|tail|less)\b/.test(trimmed))
104
+ return "read";
105
+ if (/^(ls|tree|du|wc)\b/.test(trimmed))
106
+ return "list";
107
+ if (/^(curl|wget|fetch)\b/.test(trimmed))
108
+ return "network";
109
+ if (/^(docker|kubectl|helm)\b/.test(trimmed))
110
+ return "infra";
111
+ if (/^(python|pip|pytest)\b/.test(trimmed))
112
+ return "python";
113
+ if (/^(cargo|rustc)\b/.test(trimmed))
114
+ return "rust";
115
+ if (/^(go\s|golangci)\b/.test(trimmed))
116
+ return "go";
117
+ return "other";
118
+ }
119
+ /** Normalize command for grouping (strip variable parts like paths, hashes) */
120
+ function normalizeCommand(cmd) {
121
+ return cmd
122
+ .replace(/[0-9a-f]{7,40}/g, "{hash}") // git hashes
123
+ .replace(/\/[\w./-]+\.(ts|tsx|js|json|py|rs|go)\b/g, "{file}") // file paths
124
+ .replace(/\d{4}-\d{2}-\d{2}/g, "{date}") // dates
125
+ .replace(/:\d+/g, ":{line}") // line numbers
126
+ .trim();
127
+ }
128
+ /** Run discover across all Claude sessions */
129
+ export function discover(options = {}) {
130
+ const claudeDir = join(process.env.HOME ?? "~", ".claude");
131
+ const maxAge = (options.maxAgeDays ?? 30) * 24 * 60 * 60 * 1000;
132
+ const minTokens = options.minTokens ?? 50;
133
+ const sessionFiles = findSessionFiles(claudeDir, maxAge);
134
+ const allCommands = [];
135
+ for (const file of sessionFiles) {
136
+ allCommands.push(...extractCommands(file));
137
+ }
138
+ // Filter to commands with meaningful output
139
+ const significant = allCommands.filter(c => c.outputTokens >= minTokens);
140
+ // Group by normalized command
141
+ const groups = new Map();
142
+ for (const cmd of significant) {
143
+ const key = normalizeCommand(cmd.command);
144
+ const existing = groups.get(key) ?? { count: 0, totalTokens: 0, example: cmd.command };
145
+ existing.count++;
146
+ existing.totalTokens += cmd.outputTokens;
147
+ groups.set(key, existing);
148
+ }
149
+ // Top commands by total tokens
150
+ const topCommands = [...groups.entries()]
151
+ .map(([cmd, data]) => ({
152
+ command: data.example,
153
+ count: data.count,
154
+ totalTokens: data.totalTokens,
155
+ avgTokens: Math.round(data.totalTokens / data.count),
156
+ }))
157
+ .sort((a, b) => b.totalTokens - a.totalTokens)
158
+ .slice(0, 20);
159
+ // Category breakdown
160
+ const commandsByCategory = {};
161
+ for (const cmd of significant) {
162
+ const cat = categorizeCommand(cmd.command);
163
+ if (!commandsByCategory[cat])
164
+ commandsByCategory[cat] = { count: 0, tokens: 0 };
165
+ commandsByCategory[cat].count++;
166
+ commandsByCategory[cat].tokens += cmd.outputTokens;
167
+ }
168
+ const totalOutputTokens = significant.reduce((sum, c) => sum + c.outputTokens, 0);
169
+ // Conservative 70% compression estimate (RTK claims 60-90%)
170
+ const estimatedSavings = Math.round(totalOutputTokens * 0.7);
171
+ // Each saved input token is repeated across ~5 turns on average before compaction
172
+ const multipliedSavings = estimatedSavings * 5;
173
+ // At Opus rates ($5/M input tokens)
174
+ const estimatedSavingsUsd = (multipliedSavings * 5) / 1_000_000;
175
+ return {
176
+ totalSessions: sessionFiles.length,
177
+ totalCommands: significant.length,
178
+ totalOutputTokens,
179
+ estimatedSavings,
180
+ estimatedSavingsUsd,
181
+ topCommands,
182
+ commandsByCategory,
183
+ };
184
+ }
185
+ /** Format discover report for CLI display */
186
+ export function formatDiscoverReport(report) {
187
+ const lines = [];
188
+ lines.push(`📊 Terminal Discover — Token Savings Analysis`);
189
+ lines.push(` Scanned ${report.totalSessions} sessions, ${report.totalCommands} commands with >50 token output\n`);
190
+ lines.push(`💰 Estimated savings with open-terminal:`);
191
+ lines.push(` Output tokens: ${report.totalOutputTokens.toLocaleString()}`);
192
+ lines.push(` Compressible: ${report.estimatedSavings.toLocaleString()} tokens (70% avg)`);
193
+ lines.push(` Repeated ~5x before compaction = ${(report.estimatedSavings * 5).toLocaleString()} billable tokens`);
194
+ lines.push(` At Opus rates: $${report.estimatedSavingsUsd.toFixed(2)} saved\n`);
195
+ if (report.topCommands.length > 0) {
196
+ lines.push(`🔝 Top commands by token cost:`);
197
+ for (const cmd of report.topCommands.slice(0, 15)) {
198
+ const avg = cmd.avgTokens.toLocaleString().padStart(6);
199
+ const total = cmd.totalTokens.toLocaleString().padStart(8);
200
+ lines.push(` ${String(cmd.count).padStart(4)}× ${avg} avg → ${total} total ${cmd.command.slice(0, 60)}`);
201
+ }
202
+ lines.push("");
203
+ }
204
+ if (Object.keys(report.commandsByCategory).length > 0) {
205
+ lines.push(`📁 By category:`);
206
+ const sorted = Object.entries(report.commandsByCategory).sort((a, b) => b[1].tokens - a[1].tokens);
207
+ for (const [cat, data] of sorted) {
208
+ lines.push(` ${cat.padEnd(10)} ${String(data.count).padStart(5)} cmds ${data.tokens.toLocaleString().padStart(10)} tokens`);
209
+ }
210
+ }
211
+ return lines.join("\n");
212
+ }
@@ -0,0 +1,123 @@
1
+ // Token economy tracker — tracks token savings across all interactions
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ const DIR = join(homedir(), ".terminal");
6
+ const ECONOMY_FILE = join(DIR, "economy.json");
7
+ let stats = null;
8
+ function ensureDir() {
9
+ if (!existsSync(DIR))
10
+ mkdirSync(DIR, { recursive: true });
11
+ }
12
+ function loadStats() {
13
+ if (stats)
14
+ return stats;
15
+ ensureDir();
16
+ if (existsSync(ECONOMY_FILE)) {
17
+ try {
18
+ const saved = JSON.parse(readFileSync(ECONOMY_FILE, "utf8"));
19
+ stats = {
20
+ totalTokensSaved: saved.totalTokensSaved ?? 0,
21
+ totalTokensUsed: saved.totalTokensUsed ?? 0,
22
+ savingsByFeature: {
23
+ structured: saved.savingsByFeature?.structured ?? 0,
24
+ compressed: saved.savingsByFeature?.compressed ?? 0,
25
+ diff: saved.savingsByFeature?.diff ?? 0,
26
+ cache: saved.savingsByFeature?.cache ?? 0,
27
+ search: saved.savingsByFeature?.search ?? 0,
28
+ },
29
+ sessionStart: Date.now(),
30
+ sessionSaved: 0,
31
+ sessionUsed: 0,
32
+ };
33
+ return stats;
34
+ }
35
+ catch { }
36
+ }
37
+ stats = {
38
+ totalTokensSaved: 0,
39
+ totalTokensUsed: 0,
40
+ savingsByFeature: { structured: 0, compressed: 0, diff: 0, cache: 0, search: 0 },
41
+ sessionStart: Date.now(),
42
+ sessionSaved: 0,
43
+ sessionUsed: 0,
44
+ };
45
+ return stats;
46
+ }
47
+ function saveStats() {
48
+ ensureDir();
49
+ if (stats) {
50
+ writeFileSync(ECONOMY_FILE, JSON.stringify(stats, null, 2));
51
+ }
52
+ }
53
+ /** Record token savings from a feature */
54
+ export function recordSaving(feature, tokensSaved) {
55
+ const s = loadStats();
56
+ s.totalTokensSaved += tokensSaved;
57
+ s.sessionSaved += tokensSaved;
58
+ s.savingsByFeature[feature] += tokensSaved;
59
+ saveStats();
60
+ }
61
+ /** Record tokens used (for AI calls) */
62
+ export function recordUsage(tokens) {
63
+ const s = loadStats();
64
+ s.totalTokensUsed += tokens;
65
+ s.sessionUsed += tokens;
66
+ saveStats();
67
+ }
68
+ /** Get current economy stats */
69
+ export function getEconomyStats() {
70
+ return { ...loadStats() };
71
+ }
72
+ /** Format token count for display */
73
+ export function formatTokens(n) {
74
+ if (n >= 1_000_000)
75
+ return `${(n / 1_000_000).toFixed(1)}M`;
76
+ if (n >= 1_000)
77
+ return `${(n / 1_000).toFixed(1)}K`;
78
+ return `${n}`;
79
+ }
80
+ // ── Weighted economics ──────────────────────────────────────────────────────
81
+ // Saved input tokens are repeated across multiple turns before compaction.
82
+ // Weighted pricing accounts for the actual billing impact.
83
+ /** Provider pricing per million tokens */
84
+ const PROVIDER_PRICING = {
85
+ cerebras: { input: 0.60, output: 1.20 },
86
+ groq: { input: 0.15, output: 0.60 },
87
+ xai: { input: 0.20, output: 1.50 },
88
+ anthropic: { input: 0.80, output: 4.00 }, // Haiku
89
+ "anthropic-sonnet": { input: 3.00, output: 15.00 },
90
+ "anthropic-opus": { input: 5.00, output: 25.00 },
91
+ };
92
+ /** Estimate USD savings from compressed tokens */
93
+ export function estimateSavingsUsd(tokensSaved, consumerModel = "anthropic-opus", avgTurnsBeforeCompaction = 5) {
94
+ const pricing = PROVIDER_PRICING[consumerModel] ?? PROVIDER_PRICING["anthropic-opus"];
95
+ const multipliedTokens = tokensSaved * avgTurnsBeforeCompaction;
96
+ const savingsUsd = (multipliedTokens * pricing.input) / 1_000_000;
97
+ return { savingsUsd, multipliedTokens, ratePerMillion: pricing.input };
98
+ }
99
+ /** Format a full economics summary */
100
+ export function formatEconomicsSummary() {
101
+ const s = loadStats();
102
+ const opus = estimateSavingsUsd(s.totalTokensSaved, "anthropic-opus");
103
+ const sonnet = estimateSavingsUsd(s.totalTokensSaved, "anthropic-sonnet");
104
+ const haiku = estimateSavingsUsd(s.totalTokensSaved, "anthropic");
105
+ return [
106
+ `Token Economy:`,
107
+ ` Tokens saved: ${formatTokens(s.totalTokensSaved)}`,
108
+ ` Tokens used: ${formatTokens(s.totalTokensUsed)}`,
109
+ ` Ratio: ${s.totalTokensUsed > 0 ? (s.totalTokensSaved / s.totalTokensUsed).toFixed(1) : "∞"}x return`,
110
+ ``,
111
+ ` Estimated USD savings (×5 turns before compaction):`,
112
+ ` Opus ($5/M): $${opus.savingsUsd.toFixed(2)} (${formatTokens(opus.multipliedTokens)} billable tokens)`,
113
+ ` Sonnet ($3/M): $${sonnet.savingsUsd.toFixed(2)}`,
114
+ ` Haiku ($0.8/M): $${haiku.savingsUsd.toFixed(2)}`,
115
+ ``,
116
+ ` By feature:`,
117
+ ` Compressed: ${formatTokens(s.savingsByFeature.compressed)}`,
118
+ ` Structured: ${formatTokens(s.savingsByFeature.structured)}`,
119
+ ` Diff cache: ${formatTokens(s.savingsByFeature.diff)}`,
120
+ ` NL cache: ${formatTokens(s.savingsByFeature.cache)}`,
121
+ ` Search: ${formatTokens(s.savingsByFeature.search)}`,
122
+ ].join("\n");
123
+ }
@@ -0,0 +1,38 @@
1
+ // Expand store — keeps full output for progressive disclosure
2
+ // Agents get summary first, call expand(key) only if they need details
3
+ const MAX_ENTRIES = 50;
4
+ const store = new Map();
5
+ let counter = 0;
6
+ /** Store full output and return a retrieval key */
7
+ export function storeOutput(command, output) {
8
+ const key = `out_${++counter}`;
9
+ // Evict oldest if over limit
10
+ if (store.size >= MAX_ENTRIES) {
11
+ const oldest = store.keys().next().value;
12
+ if (oldest)
13
+ store.delete(oldest);
14
+ }
15
+ store.set(key, { command, output, timestamp: Date.now() });
16
+ return key;
17
+ }
18
+ /** Retrieve full output by key, optionally filtered */
19
+ export function expandOutput(key, grep) {
20
+ const entry = store.get(key);
21
+ if (!entry)
22
+ return { found: false };
23
+ let output = entry.output;
24
+ if (grep) {
25
+ const pattern = new RegExp(grep, "i");
26
+ output = output.split("\n").filter(l => pattern.test(l)).join("\n");
27
+ }
28
+ return { found: true, output, lines: output.split("\n").length };
29
+ }
30
+ /** List available stored outputs */
31
+ export function listStored() {
32
+ return [...store.entries()].map(([key, entry]) => ({
33
+ key,
34
+ command: entry.command.slice(0, 60),
35
+ lines: entry.output.split("\n").length,
36
+ age: Date.now() - entry.timestamp,
37
+ }));
38
+ }
@@ -0,0 +1,72 @@
1
+ // Universal session file cache — cache any file read, serve from memory on repeat
2
+ import { statSync, readFileSync } from "fs";
3
+ const cache = new Map();
4
+ /** Read a file with session caching. Returns content + cache metadata. */
5
+ export function cachedRead(filePath, options = {}) {
6
+ const { offset, limit } = options;
7
+ try {
8
+ const stat = statSync(filePath);
9
+ const mtime = stat.mtimeMs;
10
+ const existing = cache.get(filePath);
11
+ // Cache hit — file unchanged
12
+ if (existing && existing.mtime === mtime) {
13
+ existing.readCount++;
14
+ existing.lastReadAt = Date.now();
15
+ const lines = existing.content.split("\n");
16
+ if (offset !== undefined || limit !== undefined) {
17
+ const start = offset ?? 0;
18
+ const end = limit !== undefined ? start + limit : lines.length;
19
+ return {
20
+ content: lines.slice(start, end).join("\n"),
21
+ cached: true,
22
+ readCount: existing.readCount,
23
+ };
24
+ }
25
+ return { content: existing.content, cached: true, readCount: existing.readCount };
26
+ }
27
+ // Cache miss or stale — read from disk
28
+ const content = readFileSync(filePath, "utf8");
29
+ cache.set(filePath, {
30
+ content,
31
+ mtime,
32
+ readCount: 1,
33
+ firstReadAt: Date.now(),
34
+ lastReadAt: Date.now(),
35
+ });
36
+ const lines = content.split("\n");
37
+ if (offset !== undefined || limit !== undefined) {
38
+ const start = offset ?? 0;
39
+ const end = limit !== undefined ? start + limit : lines.length;
40
+ return { content: lines.slice(start, end).join("\n"), cached: false, readCount: 1 };
41
+ }
42
+ return { content, cached: false, readCount: 1 };
43
+ }
44
+ catch (e) {
45
+ return { content: `Error: ${e.message}`, cached: false, readCount: 0 };
46
+ }
47
+ }
48
+ /** Invalidate cache for a file (call after writes) */
49
+ export function invalidateFile(filePath) {
50
+ cache.delete(filePath);
51
+ }
52
+ /** Invalidate all files matching a pattern */
53
+ export function invalidatePattern(pattern) {
54
+ for (const key of cache.keys()) {
55
+ if (pattern.test(key))
56
+ cache.delete(key);
57
+ }
58
+ }
59
+ /** Get cache stats */
60
+ export function cacheStats() {
61
+ let totalReads = 0;
62
+ let cacheHits = 0;
63
+ for (const entry of cache.values()) {
64
+ totalReads += entry.readCount;
65
+ cacheHits += Math.max(0, entry.readCount - 1); // first read is never cached
66
+ }
67
+ return { files: cache.size, totalReads, cacheHits };
68
+ }
69
+ /** Clear the entire cache */
70
+ export function clearFileCache() {
71
+ cache.clear();
72
+ }
@@ -0,0 +1,62 @@
1
+ // Pre-computed file index — build once, serve search from memory
2
+ // Eliminates subprocess spawning for repeat file queries
3
+ import { spawn } from "child_process";
4
+ let index = null;
5
+ let indexCwd = "";
6
+ let indexTime = 0;
7
+ let watcher = null;
8
+ const INDEX_TTL = 30_000; // 30 seconds
9
+ function exec(command, cwd) {
10
+ return new Promise((resolve) => {
11
+ const proc = spawn("/bin/zsh", ["-c", command], { cwd, stdio: ["ignore", "pipe", "pipe"] });
12
+ let out = "";
13
+ proc.stdout?.on("data", (d) => { out += d.toString(); });
14
+ proc.on("close", () => resolve(out));
15
+ });
16
+ }
17
+ /** Build or return cached file index */
18
+ export async function getFileIndex(cwd) {
19
+ // Return cached if fresh
20
+ if (index && indexCwd === cwd && Date.now() - indexTime < INDEX_TTL) {
21
+ return index;
22
+ }
23
+ const raw = await exec("find . -type f -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' -not -path '*/.next/*' -not -path '*/build/*' 2>/dev/null", cwd);
24
+ index = raw.split("\n").filter(l => l.trim()).map(p => {
25
+ const path = p.trim();
26
+ const parts = path.split("/");
27
+ const name = parts[parts.length - 1] ?? path;
28
+ const dir = parts.slice(0, -1).join("/") || ".";
29
+ const ext = name.includes(".") ? "." + name.split(".").pop() : "";
30
+ return { path, dir, name, ext };
31
+ });
32
+ indexCwd = cwd;
33
+ indexTime = Date.now();
34
+ return index;
35
+ }
36
+ /** Search file index by glob pattern (in-memory, no subprocess) */
37
+ export async function searchIndex(cwd, pattern) {
38
+ const idx = await getFileIndex(cwd);
39
+ // Convert glob to regex
40
+ const regex = new RegExp("^" + pattern
41
+ .replace(/\./g, "\\.")
42
+ .replace(/\*/g, ".*")
43
+ .replace(/\?/g, ".")
44
+ + "$", "i");
45
+ return idx.filter(e => regex.test(e.name) || regex.test(e.path)).map(e => e.path);
46
+ }
47
+ /** Get file index stats */
48
+ export async function indexStats(cwd) {
49
+ const idx = await getFileIndex(cwd);
50
+ const byExt = {};
51
+ const byDir = {};
52
+ for (const e of idx) {
53
+ byExt[e.ext || "(none)"] = (byExt[e.ext || "(none)"] ?? 0) + 1;
54
+ const topDir = e.dir.split("/").slice(0, 2).join("/");
55
+ byDir[topDir] = (byDir[topDir] ?? 0) + 1;
56
+ }
57
+ return { totalFiles: idx.length, byExtension: byExt, byDir };
58
+ }
59
+ /** Invalidate index */
60
+ export function invalidateIndex() {
61
+ index = null;
62
+ }
@@ -0,0 +1,62 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ const DIR = join(homedir(), ".terminal");
5
+ const HISTORY_FILE = join(DIR, "history.json");
6
+ const CONFIG_FILE = join(DIR, "config.json");
7
+ export const DEFAULT_PERMISSIONS = {
8
+ destructive: true,
9
+ network: true,
10
+ sudo: true,
11
+ write_outside_cwd: true,
12
+ install: true,
13
+ };
14
+ export const DEFAULT_CONFIG = {
15
+ onboarded: false,
16
+ confirm: false,
17
+ permissions: DEFAULT_PERMISSIONS,
18
+ };
19
+ function ensureDir() {
20
+ if (!existsSync(DIR))
21
+ mkdirSync(DIR, { recursive: true });
22
+ }
23
+ export function loadHistory() {
24
+ ensureDir();
25
+ if (!existsSync(HISTORY_FILE))
26
+ return [];
27
+ try {
28
+ return JSON.parse(readFileSync(HISTORY_FILE, "utf8"));
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ }
34
+ export function saveHistory(entries) {
35
+ ensureDir();
36
+ writeFileSync(HISTORY_FILE, JSON.stringify(entries.slice(-500), null, 2));
37
+ }
38
+ export function appendHistory(entry) {
39
+ const existing = loadHistory();
40
+ saveHistory([...existing, entry]);
41
+ }
42
+ export function loadConfig() {
43
+ ensureDir();
44
+ if (!existsSync(CONFIG_FILE))
45
+ return { ...DEFAULT_CONFIG };
46
+ try {
47
+ const saved = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
48
+ return {
49
+ ...DEFAULT_CONFIG,
50
+ ...saved,
51
+ confirm: saved.confirm ?? false,
52
+ permissions: { ...DEFAULT_PERMISSIONS, ...(saved.permissions ?? {}) },
53
+ };
54
+ }
55
+ catch {
56
+ return { ...DEFAULT_CONFIG };
57
+ }
58
+ }
59
+ export function saveConfig(config) {
60
+ ensureDir();
61
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
62
+ }
@@ -0,0 +1,54 @@
1
+ // Lazy execution — for large result sets, return count + sample + categories
2
+ // instead of full output. Agent requests slices on demand.
3
+ import { dirname } from "path";
4
+ const LAZY_THRESHOLD = 200; // lines before switching to lazy mode (was 100, too aggressive)
5
+ // Commands where the user explicitly wants full output — never lazify
6
+ const PASSTHROUGH_COMMANDS = [
7
+ // File reading — user explicitly wants content
8
+ /\bcat\b/, /\bhead\b/, /\btail\b/, /\bbat\b/, /\bless\b/, /\bmore\b/,
9
+ // Git review commands — truncating diffs/patches loses semantic meaning
10
+ /\bgit\s+diff\b/, /\bgit\s+show\b/, /\bgit\s+log\s+-p\b/, /\bgit\s+log\s+--patch\b/,
11
+ // Summary/report commands — summarizing a summary is pointless
12
+ /\bsummary\b/i, /\bstatus\b/i, /\breport\b/i, /\bstats\b/i,
13
+ /\bweek\b/i, /\btoday\b/i, /\bdashboard\b/i,
14
+ ];
15
+ /** Check if output should use lazy mode */
16
+ export function shouldBeLazy(output, command) {
17
+ // Never lazify explicit read commands or summary commands
18
+ if (command && PASSTHROUGH_COMMANDS.some(p => p.test(command)))
19
+ return false;
20
+ return output.split("\n").filter(l => l.trim()).length > LAZY_THRESHOLD;
21
+ }
22
+ /** Convert large output to lazy format: count + sample + categories */
23
+ export function toLazy(output, command) {
24
+ const lines = output.split("\n").filter(l => l.trim());
25
+ const sample = lines.slice(0, 20);
26
+ // Try to categorize by directory (for file-like output)
27
+ const categories = {};
28
+ const isFilePaths = lines.filter(l => l.includes("/")).length > lines.length * 0.5;
29
+ if (isFilePaths) {
30
+ for (const line of lines) {
31
+ const dir = dirname(line.trim()) || ".";
32
+ // Group by top-level dir
33
+ const topDir = dir.split("/").slice(0, 2).join("/");
34
+ categories[topDir] = (categories[topDir] ?? 0) + 1;
35
+ }
36
+ }
37
+ return {
38
+ lazy: true,
39
+ count: lines.length,
40
+ sample,
41
+ categories: Object.keys(categories).length > 1 ? categories : undefined,
42
+ hint: `${lines.length} results. Showing first 20. Use a more specific query to narrow results.`,
43
+ };
44
+ }
45
+ /** Get a slice of output */
46
+ export function getSlice(output, offset, limit) {
47
+ const allLines = output.split("\n").filter(l => l.trim());
48
+ const slice = allLines.slice(offset, offset + limit);
49
+ return {
50
+ lines: slice,
51
+ total: allLines.length,
52
+ hasMore: offset + limit < allLines.length,
53
+ };
54
+ }
@@ -0,0 +1,59 @@
1
+ // Cross-command line deduplication — track lines already shown to agent
2
+ // When new output contains >50% already-seen lines, suppress them
3
+ const seenLines = new Set();
4
+ const MAX_SEEN = 5000;
5
+ function normalize(line) {
6
+ return line.trim().toLowerCase();
7
+ }
8
+ /** Deduplicate output lines against session history */
9
+ export function dedup(output) {
10
+ const lines = output.split("\n");
11
+ if (lines.length < 5) {
12
+ // Short output — add to seen, don't dedup
13
+ for (const l of lines) {
14
+ if (l.trim())
15
+ seenLines.add(normalize(l));
16
+ }
17
+ return { output, novelCount: lines.length, seenCount: 0, deduplicated: false };
18
+ }
19
+ let novelCount = 0;
20
+ let seenCount = 0;
21
+ const novel = [];
22
+ for (const line of lines) {
23
+ const norm = normalize(line);
24
+ if (!norm) {
25
+ novel.push(line);
26
+ continue;
27
+ }
28
+ if (seenLines.has(norm)) {
29
+ seenCount++;
30
+ }
31
+ else {
32
+ novelCount++;
33
+ novel.push(line);
34
+ seenLines.add(norm);
35
+ }
36
+ }
37
+ // Evict oldest if too large
38
+ if (seenLines.size > MAX_SEEN) {
39
+ const entries = [...seenLines];
40
+ for (let i = 0; i < entries.length - MAX_SEEN; i++) {
41
+ seenLines.delete(entries[i]);
42
+ }
43
+ }
44
+ // Only dedup if >50% were already seen
45
+ if (seenCount > lines.length * 0.5) {
46
+ const result = novel.join("\n");
47
+ return { output: result + `\n(${seenCount} lines already shown, omitted)`, novelCount, seenCount, deduplicated: true };
48
+ }
49
+ // Add all to seen but return full output
50
+ for (const l of lines) {
51
+ if (l.trim())
52
+ seenLines.add(normalize(l));
53
+ }
54
+ return { output, novelCount: lines.length, seenCount: 0, deduplicated: false };
55
+ }
56
+ /** Clear dedup history */
57
+ export function clearDedup() {
58
+ seenLines.clear();
59
+ }