@hasna/terminal 2.2.0 → 2.3.0

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 (205) hide show
  1. package/dist/cli.js +29 -12
  2. package/package.json +1 -1
  3. package/src/ai.ts +50 -36
  4. package/src/cli.tsx +29 -12
  5. package/src/context-hints.ts +89 -0
  6. package/src/discover.ts +238 -0
  7. package/src/economy.ts +53 -0
  8. package/src/output-store.ts +65 -0
  9. package/src/providers/index.ts +4 -4
  10. package/src/sessions-db.ts +81 -0
  11. package/temp/rtk/.claude/agents/code-reviewer.md +221 -0
  12. package/temp/rtk/.claude/agents/debugger.md +519 -0
  13. package/temp/rtk/.claude/agents/rtk-testing-specialist.md +461 -0
  14. package/temp/rtk/.claude/agents/rust-rtk.md +511 -0
  15. package/temp/rtk/.claude/agents/technical-writer.md +355 -0
  16. package/temp/rtk/.claude/commands/diagnose.md +352 -0
  17. package/temp/rtk/.claude/commands/test-routing.md +362 -0
  18. package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +16 -0
  19. package/temp/rtk/.claude/hooks/rtk-rewrite.sh +70 -0
  20. package/temp/rtk/.claude/hooks/rtk-suggest.sh +152 -0
  21. package/temp/rtk/.claude/rules/cli-testing.md +526 -0
  22. package/temp/rtk/.claude/skills/issue-triage/SKILL.md +348 -0
  23. package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +134 -0
  24. package/temp/rtk/.claude/skills/performance.md +435 -0
  25. package/temp/rtk/.claude/skills/pr-triage/SKILL.md +315 -0
  26. package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +71 -0
  27. package/temp/rtk/.claude/skills/repo-recap.md +206 -0
  28. package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +78 -0
  29. package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +124 -0
  30. package/temp/rtk/.claude/skills/security-guardian.md +503 -0
  31. package/temp/rtk/.claude/skills/ship.md +404 -0
  32. package/temp/rtk/.github/workflows/benchmark.yml +34 -0
  33. package/temp/rtk/.github/workflows/dco-check.yaml +12 -0
  34. package/temp/rtk/.github/workflows/release-please.yml +51 -0
  35. package/temp/rtk/.github/workflows/release.yml +343 -0
  36. package/temp/rtk/.github/workflows/security-check.yml +135 -0
  37. package/temp/rtk/.github/workflows/validate-docs.yml +78 -0
  38. package/temp/rtk/.release-please-manifest.json +3 -0
  39. package/temp/rtk/ARCHITECTURE.md +1491 -0
  40. package/temp/rtk/CHANGELOG.md +640 -0
  41. package/temp/rtk/CLAUDE.md +605 -0
  42. package/temp/rtk/CONTRIBUTING.md +199 -0
  43. package/temp/rtk/Cargo.lock +1668 -0
  44. package/temp/rtk/Cargo.toml +64 -0
  45. package/temp/rtk/Formula/rtk.rb +43 -0
  46. package/temp/rtk/INSTALL.md +390 -0
  47. package/temp/rtk/LICENSE +21 -0
  48. package/temp/rtk/README.md +386 -0
  49. package/temp/rtk/README_es.md +159 -0
  50. package/temp/rtk/README_fr.md +197 -0
  51. package/temp/rtk/README_ja.md +159 -0
  52. package/temp/rtk/README_ko.md +159 -0
  53. package/temp/rtk/README_zh.md +167 -0
  54. package/temp/rtk/ROADMAP.md +15 -0
  55. package/temp/rtk/SECURITY.md +217 -0
  56. package/temp/rtk/TEST_EXEC_TIME.md +102 -0
  57. package/temp/rtk/build.rs +57 -0
  58. package/temp/rtk/docs/AUDIT_GUIDE.md +432 -0
  59. package/temp/rtk/docs/FEATURES.md +1410 -0
  60. package/temp/rtk/docs/TROUBLESHOOTING.md +309 -0
  61. package/temp/rtk/docs/filter-workflow.md +102 -0
  62. package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
  63. package/temp/rtk/docs/tracking.md +583 -0
  64. package/temp/rtk/hooks/opencode-rtk.ts +39 -0
  65. package/temp/rtk/hooks/rtk-awareness.md +29 -0
  66. package/temp/rtk/hooks/rtk-rewrite.sh +61 -0
  67. package/temp/rtk/hooks/test-rtk-rewrite.sh +442 -0
  68. package/temp/rtk/install.sh +124 -0
  69. package/temp/rtk/release-please-config.json +10 -0
  70. package/temp/rtk/scripts/benchmark.sh +592 -0
  71. package/temp/rtk/scripts/check-installation.sh +162 -0
  72. package/temp/rtk/scripts/install-local.sh +37 -0
  73. package/temp/rtk/scripts/rtk-economics.sh +137 -0
  74. package/temp/rtk/scripts/test-all.sh +561 -0
  75. package/temp/rtk/scripts/test-aristote.sh +227 -0
  76. package/temp/rtk/scripts/test-tracking.sh +79 -0
  77. package/temp/rtk/scripts/update-readme-metrics.sh +32 -0
  78. package/temp/rtk/scripts/validate-docs.sh +73 -0
  79. package/temp/rtk/src/aws_cmd.rs +880 -0
  80. package/temp/rtk/src/binlog.rs +1645 -0
  81. package/temp/rtk/src/cargo_cmd.rs +1727 -0
  82. package/temp/rtk/src/cc_economics.rs +1157 -0
  83. package/temp/rtk/src/ccusage.rs +340 -0
  84. package/temp/rtk/src/config.rs +187 -0
  85. package/temp/rtk/src/container.rs +855 -0
  86. package/temp/rtk/src/curl_cmd.rs +134 -0
  87. package/temp/rtk/src/deps.rs +268 -0
  88. package/temp/rtk/src/diff_cmd.rs +367 -0
  89. package/temp/rtk/src/discover/mod.rs +274 -0
  90. package/temp/rtk/src/discover/provider.rs +388 -0
  91. package/temp/rtk/src/discover/registry.rs +2022 -0
  92. package/temp/rtk/src/discover/report.rs +202 -0
  93. package/temp/rtk/src/discover/rules.rs +667 -0
  94. package/temp/rtk/src/display_helpers.rs +402 -0
  95. package/temp/rtk/src/dotnet_cmd.rs +1771 -0
  96. package/temp/rtk/src/dotnet_format_report.rs +133 -0
  97. package/temp/rtk/src/dotnet_trx.rs +593 -0
  98. package/temp/rtk/src/env_cmd.rs +204 -0
  99. package/temp/rtk/src/filter.rs +462 -0
  100. package/temp/rtk/src/filters/README.md +52 -0
  101. package/temp/rtk/src/filters/ansible-playbook.toml +34 -0
  102. package/temp/rtk/src/filters/basedpyright.toml +47 -0
  103. package/temp/rtk/src/filters/biome.toml +45 -0
  104. package/temp/rtk/src/filters/brew-install.toml +37 -0
  105. package/temp/rtk/src/filters/composer-install.toml +40 -0
  106. package/temp/rtk/src/filters/df.toml +16 -0
  107. package/temp/rtk/src/filters/dotnet-build.toml +64 -0
  108. package/temp/rtk/src/filters/du.toml +16 -0
  109. package/temp/rtk/src/filters/fail2ban-client.toml +15 -0
  110. package/temp/rtk/src/filters/gcc.toml +49 -0
  111. package/temp/rtk/src/filters/gcloud.toml +22 -0
  112. package/temp/rtk/src/filters/hadolint.toml +24 -0
  113. package/temp/rtk/src/filters/helm.toml +29 -0
  114. package/temp/rtk/src/filters/iptables.toml +27 -0
  115. package/temp/rtk/src/filters/jj.toml +28 -0
  116. package/temp/rtk/src/filters/jq.toml +24 -0
  117. package/temp/rtk/src/filters/make.toml +41 -0
  118. package/temp/rtk/src/filters/markdownlint.toml +24 -0
  119. package/temp/rtk/src/filters/mix-compile.toml +27 -0
  120. package/temp/rtk/src/filters/mix-format.toml +15 -0
  121. package/temp/rtk/src/filters/mvn-build.toml +44 -0
  122. package/temp/rtk/src/filters/oxlint.toml +43 -0
  123. package/temp/rtk/src/filters/ping.toml +63 -0
  124. package/temp/rtk/src/filters/pio-run.toml +40 -0
  125. package/temp/rtk/src/filters/poetry-install.toml +50 -0
  126. package/temp/rtk/src/filters/pre-commit.toml +35 -0
  127. package/temp/rtk/src/filters/ps.toml +16 -0
  128. package/temp/rtk/src/filters/quarto-render.toml +41 -0
  129. package/temp/rtk/src/filters/rsync.toml +48 -0
  130. package/temp/rtk/src/filters/shellcheck.toml +27 -0
  131. package/temp/rtk/src/filters/shopify-theme.toml +29 -0
  132. package/temp/rtk/src/filters/skopeo.toml +45 -0
  133. package/temp/rtk/src/filters/sops.toml +16 -0
  134. package/temp/rtk/src/filters/ssh.toml +44 -0
  135. package/temp/rtk/src/filters/stat.toml +34 -0
  136. package/temp/rtk/src/filters/swift-build.toml +41 -0
  137. package/temp/rtk/src/filters/systemctl-status.toml +33 -0
  138. package/temp/rtk/src/filters/terraform-plan.toml +35 -0
  139. package/temp/rtk/src/filters/tofu-fmt.toml +16 -0
  140. package/temp/rtk/src/filters/tofu-init.toml +38 -0
  141. package/temp/rtk/src/filters/tofu-plan.toml +35 -0
  142. package/temp/rtk/src/filters/tofu-validate.toml +17 -0
  143. package/temp/rtk/src/filters/trunk-build.toml +39 -0
  144. package/temp/rtk/src/filters/ty.toml +50 -0
  145. package/temp/rtk/src/filters/uv-sync.toml +37 -0
  146. package/temp/rtk/src/filters/xcodebuild.toml +99 -0
  147. package/temp/rtk/src/filters/yamllint.toml +25 -0
  148. package/temp/rtk/src/find_cmd.rs +598 -0
  149. package/temp/rtk/src/format_cmd.rs +386 -0
  150. package/temp/rtk/src/gain.rs +723 -0
  151. package/temp/rtk/src/gh_cmd.rs +1651 -0
  152. package/temp/rtk/src/git.rs +2012 -0
  153. package/temp/rtk/src/go_cmd.rs +592 -0
  154. package/temp/rtk/src/golangci_cmd.rs +254 -0
  155. package/temp/rtk/src/grep_cmd.rs +288 -0
  156. package/temp/rtk/src/gt_cmd.rs +810 -0
  157. package/temp/rtk/src/hook_audit_cmd.rs +283 -0
  158. package/temp/rtk/src/hook_check.rs +171 -0
  159. package/temp/rtk/src/init.rs +1859 -0
  160. package/temp/rtk/src/integrity.rs +537 -0
  161. package/temp/rtk/src/json_cmd.rs +231 -0
  162. package/temp/rtk/src/learn/detector.rs +628 -0
  163. package/temp/rtk/src/learn/mod.rs +119 -0
  164. package/temp/rtk/src/learn/report.rs +184 -0
  165. package/temp/rtk/src/lint_cmd.rs +694 -0
  166. package/temp/rtk/src/local_llm.rs +316 -0
  167. package/temp/rtk/src/log_cmd.rs +248 -0
  168. package/temp/rtk/src/ls.rs +324 -0
  169. package/temp/rtk/src/main.rs +2482 -0
  170. package/temp/rtk/src/mypy_cmd.rs +389 -0
  171. package/temp/rtk/src/next_cmd.rs +241 -0
  172. package/temp/rtk/src/npm_cmd.rs +236 -0
  173. package/temp/rtk/src/parser/README.md +267 -0
  174. package/temp/rtk/src/parser/error.rs +46 -0
  175. package/temp/rtk/src/parser/formatter.rs +336 -0
  176. package/temp/rtk/src/parser/mod.rs +311 -0
  177. package/temp/rtk/src/parser/types.rs +119 -0
  178. package/temp/rtk/src/pip_cmd.rs +302 -0
  179. package/temp/rtk/src/playwright_cmd.rs +479 -0
  180. package/temp/rtk/src/pnpm_cmd.rs +573 -0
  181. package/temp/rtk/src/prettier_cmd.rs +221 -0
  182. package/temp/rtk/src/prisma_cmd.rs +482 -0
  183. package/temp/rtk/src/psql_cmd.rs +382 -0
  184. package/temp/rtk/src/pytest_cmd.rs +384 -0
  185. package/temp/rtk/src/read.rs +217 -0
  186. package/temp/rtk/src/rewrite_cmd.rs +50 -0
  187. package/temp/rtk/src/ruff_cmd.rs +402 -0
  188. package/temp/rtk/src/runner.rs +271 -0
  189. package/temp/rtk/src/summary.rs +297 -0
  190. package/temp/rtk/src/tee.rs +405 -0
  191. package/temp/rtk/src/telemetry.rs +248 -0
  192. package/temp/rtk/src/toml_filter.rs +1655 -0
  193. package/temp/rtk/src/tracking.rs +1416 -0
  194. package/temp/rtk/src/tree.rs +209 -0
  195. package/temp/rtk/src/tsc_cmd.rs +259 -0
  196. package/temp/rtk/src/utils.rs +432 -0
  197. package/temp/rtk/src/verify_cmd.rs +47 -0
  198. package/temp/rtk/src/vitest_cmd.rs +385 -0
  199. package/temp/rtk/src/wc_cmd.rs +401 -0
  200. package/temp/rtk/src/wget_cmd.rs +260 -0
  201. package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +11 -0
  202. package/temp/rtk/tests/fixtures/dotnet/format_changes.json +31 -0
  203. package/temp/rtk/tests/fixtures/dotnet/format_empty.json +1 -0
  204. package/temp/rtk/tests/fixtures/dotnet/format_success.json +12 -0
  205. package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +18 -0
package/dist/cli.js CHANGED
@@ -28,6 +28,7 @@ SUBCOMMANDS:
28
28
  collection create|list Recipe collections
29
29
  mcp serve Start MCP server for AI agents
30
30
  mcp install --claude|--codex Install MCP server
31
+ discover [--days=N] [--json] Scan Claude sessions, show token savings potential
31
32
  snapshot Terminal state as JSON
32
33
  --help Show this help
33
34
  --version Show version
@@ -253,17 +254,8 @@ else if (args[0] === "collection") {
253
254
  }
254
255
  // ── Stats command ────────────────────────────────────────────────────────────
255
256
  else if (args[0] === "stats") {
256
- const { getEconomyStats, formatTokens } = await import("./economy.js");
257
- const s = getEconomyStats();
258
- console.log("Token Economy:");
259
- console.log(` Total saved: ${formatTokens(s.totalTokensSaved)}`);
260
- console.log(` Total used: ${formatTokens(s.totalTokensUsed)}`);
261
- console.log(` By feature:`);
262
- console.log(` Structured: ${formatTokens(s.savingsByFeature.structured)}`);
263
- console.log(` Compressed: ${formatTokens(s.savingsByFeature.compressed)}`);
264
- console.log(` Diff cache: ${formatTokens(s.savingsByFeature.diff)}`);
265
- console.log(` NL cache: ${formatTokens(s.savingsByFeature.cache)}`);
266
- console.log(` Search: ${formatTokens(s.savingsByFeature.search)}`);
257
+ const { formatEconomicsSummary } = await import("./economy.js");
258
+ console.log(formatEconomicsSummary());
267
259
  }
268
260
  // ── Sessions command ─────────────────────────────────────────────────────────
269
261
  else if (args[0] === "sessions") {
@@ -407,6 +399,19 @@ else if (args[0] === "explain" && args[1]) {
407
399
  const explanation = await explainCommand(command);
408
400
  console.log(explanation);
409
401
  }
402
+ // ── Discover command ─────────────────────────────────────────────────────────
403
+ else if (args[0] === "discover") {
404
+ const { discover, formatDiscoverReport } = await import("./discover.js");
405
+ const days = parseInt(args.find(a => a.startsWith("--days="))?.split("=")[1] ?? "30");
406
+ const json = args.includes("--json");
407
+ const report = discover({ maxAgeDays: days });
408
+ if (json) {
409
+ console.log(JSON.stringify(report, null, 2));
410
+ }
411
+ else {
412
+ console.log(formatDiscoverReport(report));
413
+ }
414
+ }
410
415
  // ── Snapshot command ─────────────────────────────────────────────────────────
411
416
  else if (args[0] === "snapshot") {
412
417
  const { captureSnapshot } = await import("./snapshots.js");
@@ -430,6 +435,7 @@ else if (args.length > 0) {
430
435
  const { processOutput, shouldProcess } = await import("./output-processor.js");
431
436
  const { rewriteCommand } = await import("./command-rewriter.js");
432
437
  const { shouldBeLazy, toLazy } = await import("./lazy-executor.js");
438
+ const { saveOutput, formatOutputHint } = await import("./output-store.js");
433
439
  const { parseOutput, estimateTokens } = await import("./parsers/index.js");
434
440
  const { recordSaving, recordUsage } = await import("./economy.js");
435
441
  const { isTestOutput, trackTests, formatWatchResult } = await import("./test-watchlist.js");
@@ -437,6 +443,7 @@ else if (args.length > 0) {
437
443
  const { loadConfig } = await import("./history.js");
438
444
  const { loadContext, saveContext, formatContext } = await import("./session-context.js");
439
445
  const { getLearned, recordMapping } = await import("./usage-cache.js");
446
+ const { recordCorrection, findSimilarCorrections, recordOutput } = await import("./sessions-db.js");
440
447
  const config = loadConfig();
441
448
  const perms = config.permissions;
442
449
  const sessionCtx = formatContext();
@@ -580,7 +587,15 @@ else if (args.length > 0) {
580
587
  if (processed.aiProcessed) {
581
588
  if (processed.tokensSaved > 0)
582
589
  recordSaving("compressed", processed.tokensSaved);
583
- console.log(processed.summary);
590
+ // Save full output for lazy recovery — agents can read the file
591
+ if (processed.tokensSaved > 50) {
592
+ const outputPath = saveOutput(actualCmd, clean);
593
+ console.log(processed.summary);
594
+ console.log(formatOutputHint(outputPath));
595
+ }
596
+ else {
597
+ console.log(processed.summary);
598
+ }
584
599
  if (processed.tokensSaved > 10)
585
600
  console.error(`[open-terminal] ${rawTokens} → ${rawTokens - processed.tokensSaved} tokens (saved ${processed.tokensSaved})`);
586
601
  process.exit(0);
@@ -637,6 +652,8 @@ else if (args.length > 0) {
637
652
  const retryResult = execSync(retryCmd + " #(retry)", { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, cwd: process.cwd() });
638
653
  const retryClean = stripNoise(stripAnsi(retryResult)).cleaned;
639
654
  if (retryClean.length > 5) {
655
+ // Record the correction so we learn from it
656
+ recordCorrection(prompt, actualCmd, errStderr.slice(0, 500), retryCmd, true);
640
657
  const processed = await processOutput(retryCmd, retryClean, prompt);
641
658
  console.log(processed.aiProcessed ? processed.summary : retryClean);
642
659
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Smart terminal wrapper for AI agents and humans — structured output, token compression, MCP server, natural language",
5
5
  "type": "module",
6
6
  "bin": {
package/src/ai.ts CHANGED
@@ -6,51 +6,48 @@ import { join } from "path";
6
6
  import { discoverProjectHints, discoverSafetyHints, formatHints } from "./context-hints.js";
7
7
 
8
8
  // ── model routing ─────────────────────────────────────────────────────────────
9
- // Simple queries fast model. Complex/ambiguous smart model.
9
+ // Config-driven model selection. Defaults per provider, user can override in ~/.terminal/config.json
10
10
 
11
11
  const COMPLEX_SIGNALS = [
12
12
  /\b(undo|revert|rollback|previous|last)\b/i,
13
13
  /\b(all files?|recursively|bulk|batch)\b/i,
14
14
  /\b(pipeline|chain|then|and then|after)\b/i,
15
15
  /\b(if|when|unless|only if)\b/i,
16
- /\b(go into|go to|navigate|cd into|enter)\b.*\b(and|then)\b/i, // multi-step navigation
17
- /\b(inside|within|under)\b/i, // relative references need context awareness
18
- /[|&;]{2}/, // pipes / && in NL (unusual = complex intent)
16
+ /\b(go into|go to|navigate|cd into|enter)\b.*\b(and|then)\b/i,
17
+ /\b(inside|within|under)\b/i,
18
+ /[|&;]{2}/,
19
19
  ];
20
20
 
21
- /** Model routing per provider */
21
+ /** Default models per provider — user can override in ~/.terminal/config.json under "models" */
22
+ const MODEL_DEFAULTS: Record<string, { fast: string; smart: string }> = {
23
+ cerebras: { fast: "qwen-3-235b-a22b-instruct-2507", smart: "qwen-3-235b-a22b-instruct-2507" },
24
+ groq: { fast: "openai/gpt-oss-120b", smart: "moonshotai/kimi-k2-instruct" },
25
+ xai: { fast: "grok-code-fast-1", smart: "grok-4-fast-non-reasoning" },
26
+ anthropic: { fast: "claude-haiku-4-5-20251001", smart: "claude-sonnet-4-6" },
27
+ };
28
+
29
+ /** Load user model overrides from ~/.terminal/config.json */
30
+ function loadModelOverrides(): Record<string, { fast?: string; smart?: string }> {
31
+ try {
32
+ const configPath = join(process.env.HOME ?? "~", ".terminal", "config.json");
33
+ if (existsSync(configPath)) {
34
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
35
+ return config.models ?? {};
36
+ }
37
+ } catch {}
38
+ return {};
39
+ }
40
+
41
+ /** Model routing per provider — config-driven with defaults */
22
42
  function pickModel(nl: string): { fast: string; smart: string; pick: "fast" | "smart" } {
23
43
  const isComplex = COMPLEX_SIGNALS.some((r) => r.test(nl)) || nl.split(" ").length > 10;
24
44
  const provider = getProvider();
45
+ const defaults = MODEL_DEFAULTS[provider.name] ?? MODEL_DEFAULTS.cerebras;
46
+ const overrides = loadModelOverrides()[provider.name] ?? {};
25
47
 
26
- if (provider.name === "anthropic") {
27
- return {
28
- fast: "claude-haiku-4-5-20251001",
29
- smart: "claude-sonnet-4-6",
30
- pick: isComplex ? "smart" : "fast",
31
- };
32
- }
33
-
34
- if (provider.name === "groq") {
35
- return {
36
- fast: "openai/gpt-oss-120b",
37
- smart: "moonshotai/kimi-k2-instruct",
38
- pick: isComplex ? "smart" : "fast",
39
- };
40
- }
41
-
42
- if (provider.name === "xai") {
43
- return {
44
- fast: "grok-code-fast-1",
45
- smart: "grok-4-fast-non-reasoning",
46
- pick: isComplex ? "smart" : "fast",
47
- };
48
- }
49
-
50
- // Cerebras — qwen for everything (llama3.1-8b too unreliable)
51
48
  return {
52
- fast: "qwen-3-235b-a22b-instruct-2507",
53
- smart: "qwen-3-235b-a22b-instruct-2507",
49
+ fast: overrides.fast ?? defaults.fast,
50
+ smart: overrides.smart ?? defaults.smart,
54
51
  pick: isComplex ? "smart" : "fast",
55
52
  };
56
53
  }
@@ -124,6 +121,23 @@ export interface SessionEntry {
124
121
  error?: boolean;
125
122
  }
126
123
 
124
+ // ── correction memory ───────────────────────────────────────────────────────
125
+
126
+ /** Load past corrections relevant to a prompt — injected as negative examples */
127
+ function loadCorrectionHints(prompt: string): string {
128
+ try {
129
+ // Dynamic import to avoid circular deps
130
+ const { findSimilarCorrections } = require("./sessions-db.js");
131
+ const corrections = findSimilarCorrections(prompt, 3);
132
+ if (corrections.length === 0) return "";
133
+
134
+ const lines = corrections.map((c: any) =>
135
+ `AVOID: "${c.failed_command}" (failed: ${c.error_type}). USE: "${c.corrected_command}" instead.`
136
+ );
137
+ return `\n\nLEARNED CORRECTIONS (from past failures):\n${lines.join("\n")}`;
138
+ } catch { return ""; }
139
+ }
140
+
127
141
  // ── project context (powered by context-hints) ──────────────────────────────
128
142
 
129
143
  function detectProjectContext(): string {
@@ -133,7 +147,7 @@ function detectProjectContext(): string {
133
147
 
134
148
  // ── system prompt ─────────────────────────────────────────────────────────────
135
149
 
136
- function buildSystemPrompt(perms: Permissions, sessionEntries: SessionEntry[]): string {
150
+ function buildSystemPrompt(perms: Permissions, sessionEntries: SessionEntry[], currentPrompt?: string): string {
137
151
  const restrictions: string[] = [];
138
152
  if (!perms.destructive)
139
153
  restrictions.push("- NEVER generate commands that delete, remove, or overwrite files/data");
@@ -233,7 +247,7 @@ EXISTENCE CHECKS: If the prompt starts with "is there", "does this have", "do we
233
247
 
234
248
  MONOREPO: If the project context says "MONOREPO", search packages/ or apps/ NOT src/. Use: grep -rn "pattern" packages/ --include="*.ts". For specific packages, use packages/PKGNAME/src/.
235
249
  cwd: ${process.cwd()}
236
- shell: zsh / macOS${projectContext}${safetyBlock}${restrictionBlock}${contextBlock}`;
250
+ shell: zsh / macOS${projectContext}${safetyBlock}${restrictionBlock}${contextBlock}${currentPrompt ? loadCorrectionHints(currentPrompt) : ""}`;
237
251
  }
238
252
 
239
253
  // ── streaming translate ───────────────────────────────────────────────────────
@@ -253,7 +267,7 @@ export async function translateToCommand(
253
267
  const provider = getProvider();
254
268
  const routing = pickModel(nl);
255
269
  const model = routing.pick === "smart" ? routing.smart : routing.fast;
256
- const system = buildSystemPrompt(perms, sessionEntries);
270
+ const system = buildSystemPrompt(perms, sessionEntries, nl);
257
271
 
258
272
  let text: string;
259
273
 
@@ -332,7 +346,7 @@ export async function fixCommand(
332
346
  {
333
347
  model: routing.smart, // always use smart model for fixes
334
348
  maxTokens: 256,
335
- system: buildSystemPrompt(perms, sessionEntries),
349
+ system: buildSystemPrompt(perms, sessionEntries, originalNl),
336
350
  }
337
351
  );
338
352
  if (text.startsWith("BLOCKED:")) throw new Error(text);
package/src/cli.tsx CHANGED
@@ -31,6 +31,7 @@ SUBCOMMANDS:
31
31
  collection create|list Recipe collections
32
32
  mcp serve Start MCP server for AI agents
33
33
  mcp install --claude|--codex Install MCP server
34
+ discover [--days=N] [--json] Scan Claude sessions, show token savings potential
34
35
  snapshot Terminal state as JSON
35
36
  --help Show this help
36
37
  --version Show version
@@ -243,17 +244,8 @@ else if (args[0] === "collection") {
243
244
  // ── Stats command ────────────────────────────────────────────────────────────
244
245
 
245
246
  else if (args[0] === "stats") {
246
- const { getEconomyStats, formatTokens } = await import("./economy.js");
247
- const s = getEconomyStats();
248
- console.log("Token Economy:");
249
- console.log(` Total saved: ${formatTokens(s.totalTokensSaved)}`);
250
- console.log(` Total used: ${formatTokens(s.totalTokensUsed)}`);
251
- console.log(` By feature:`);
252
- console.log(` Structured: ${formatTokens(s.savingsByFeature.structured)}`);
253
- console.log(` Compressed: ${formatTokens(s.savingsByFeature.compressed)}`);
254
- console.log(` Diff cache: ${formatTokens(s.savingsByFeature.diff)}`);
255
- console.log(` NL cache: ${formatTokens(s.savingsByFeature.cache)}`);
256
- console.log(` Search: ${formatTokens(s.savingsByFeature.search)}`);
247
+ const { formatEconomicsSummary } = await import("./economy.js");
248
+ console.log(formatEconomicsSummary());
257
249
  }
258
250
 
259
251
  // ── Sessions command ─────────────────────────────────────────────────────────
@@ -381,6 +373,20 @@ else if (args[0] === "explain" && args[1]) {
381
373
  console.log(explanation);
382
374
  }
383
375
 
376
+ // ── Discover command ─────────────────────────────────────────────────────────
377
+
378
+ else if (args[0] === "discover") {
379
+ const { discover, formatDiscoverReport } = await import("./discover.js");
380
+ const days = parseInt(args.find(a => a.startsWith("--days="))?.split("=")[1] ?? "30");
381
+ const json = args.includes("--json");
382
+ const report = discover({ maxAgeDays: days });
383
+ if (json) {
384
+ console.log(JSON.stringify(report, null, 2));
385
+ } else {
386
+ console.log(formatDiscoverReport(report));
387
+ }
388
+ }
389
+
384
390
  // ── Snapshot command ─────────────────────────────────────────────────────────
385
391
 
386
392
  else if (args[0] === "snapshot") {
@@ -411,6 +417,7 @@ else if (args.length > 0) {
411
417
  const { processOutput, shouldProcess } = await import("./output-processor.js");
412
418
  const { rewriteCommand } = await import("./command-rewriter.js");
413
419
  const { shouldBeLazy, toLazy } = await import("./lazy-executor.js");
420
+ const { saveOutput, formatOutputHint } = await import("./output-store.js");
414
421
  const { parseOutput, estimateTokens } = await import("./parsers/index.js");
415
422
  const { recordSaving, recordUsage } = await import("./economy.js");
416
423
  const { isTestOutput, trackTests, formatWatchResult } = await import("./test-watchlist.js");
@@ -418,6 +425,7 @@ else if (args.length > 0) {
418
425
  const { loadConfig } = await import("./history.js");
419
426
  const { loadContext, saveContext, formatContext } = await import("./session-context.js");
420
427
  const { getLearned, recordMapping } = await import("./usage-cache.js");
428
+ const { recordCorrection, findSimilarCorrections, recordOutput } = await import("./sessions-db.js");
421
429
 
422
430
  const config = loadConfig();
423
431
  const perms = config.permissions;
@@ -566,7 +574,14 @@ else if (args.length > 0) {
566
574
  const processed = await processOutput(actualCmd, clean, prompt);
567
575
  if (processed.aiProcessed) {
568
576
  if (processed.tokensSaved > 0) recordSaving("compressed", processed.tokensSaved);
569
- console.log(processed.summary);
577
+ // Save full output for lazy recovery — agents can read the file
578
+ if (processed.tokensSaved > 50) {
579
+ const outputPath = saveOutput(actualCmd, clean);
580
+ console.log(processed.summary);
581
+ console.log(formatOutputHint(outputPath));
582
+ } else {
583
+ console.log(processed.summary);
584
+ }
570
585
  if (processed.tokensSaved > 10) console.error(`[open-terminal] ${rawTokens} → ${rawTokens - processed.tokensSaved} tokens (saved ${processed.tokensSaved})`);
571
586
  process.exit(0);
572
587
  }
@@ -625,6 +640,8 @@ else if (args.length > 0) {
625
640
  const retryResult = execSync(retryCmd + " #(retry)", { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, cwd: process.cwd() });
626
641
  const retryClean = stripNoise(stripAnsi(retryResult)).cleaned;
627
642
  if (retryClean.length > 5) {
643
+ // Record the correction so we learn from it
644
+ recordCorrection(prompt, actualCmd, errStderr.slice(0, 500), retryCmd, true);
628
645
  const processed = await processOutput(retryCmd, retryClean, prompt);
629
646
  console.log(processed.aiProcessed ? processed.summary : retryClean);
630
647
  process.exit(0);
@@ -160,9 +160,98 @@ export function discoverOutputHints(output: string, command: string): string[] {
160
160
  // Sensitive data (only env var assignments, not code containing the word KEY/TOKEN)
161
161
  if (output.match(/^[A-Z_]+(KEY|TOKEN|SECRET|PASSWORD)\s*=\s*\S+/m)) hints.push("Output may contain sensitive data — redact credentials");
162
162
 
163
+ // Error block extraction — state machine that captures multi-line errors
164
+ if (!isGrepOutput) {
165
+ const errorBlocks = extractErrorBlocks(output);
166
+ if (errorBlocks.length > 0) {
167
+ const summary = errorBlocks.slice(0, 3).map(b => b.trim().split("\n").slice(0, 5).join("\n")).join("\n---\n");
168
+ hints.push(`ERROR BLOCKS FOUND (${errorBlocks.length}):\n${summary}`);
169
+ }
170
+ }
171
+
163
172
  return hints;
164
173
  }
165
174
 
175
+ /** Extract multi-line error blocks using a state machine */
176
+ function extractErrorBlocks(output: string): string[] {
177
+ const lines = output.split("\n");
178
+ const blocks: string[] = [];
179
+ let currentBlock: string[] = [];
180
+ let inErrorBlock = false;
181
+ let blankCount = 0;
182
+
183
+ // Patterns that START an error block
184
+ const errorStarters = [
185
+ /^error/i, /^Error:/i, /^ERROR/,
186
+ /^Traceback/i, /^panic:/i, /^fatal:/i,
187
+ /^FAIL/i, /^✗/, /^✘/,
188
+ /error\s*TS\d+/i, /error\[E\d+\]/,
189
+ /^SyntaxError/i, /^TypeError/i, /^ReferenceError/i,
190
+ /^Unhandled/i, /^Exception/i,
191
+ /ENOENT|EACCES|EADDRINUSE|ECONNREFUSED/,
192
+ ];
193
+
194
+ for (const line of lines) {
195
+ const trimmed = line.trim();
196
+
197
+ if (!trimmed) {
198
+ blankCount++;
199
+ if (inErrorBlock) {
200
+ currentBlock.push(line);
201
+ // 2+ blank lines = end of error block
202
+ if (blankCount >= 2) {
203
+ blocks.push(currentBlock.join("\n").trim());
204
+ currentBlock = [];
205
+ inErrorBlock = false;
206
+ }
207
+ }
208
+ continue;
209
+ }
210
+ blankCount = 0;
211
+
212
+ // Check if this line starts a new error block
213
+ if (!inErrorBlock && errorStarters.some(p => p.test(trimmed))) {
214
+ inErrorBlock = true;
215
+ currentBlock = [line];
216
+ continue;
217
+ }
218
+
219
+ if (inErrorBlock) {
220
+ // Continuation: indented lines, "at ..." stack frames, "--->" pointers, "File ..." python traces
221
+ const isContinuation =
222
+ /^\s+/.test(line) ||
223
+ /^\s*at\s/.test(trimmed) ||
224
+ /^\s*-+>/.test(trimmed) ||
225
+ /^\s*\|/.test(trimmed) ||
226
+ /^\s*File "/.test(trimmed) ||
227
+ /^\s*\d+\s*\|/.test(trimmed) || // rust/compiler line numbers
228
+ /^Caused by:/i.test(trimmed);
229
+
230
+ if (isContinuation) {
231
+ currentBlock.push(line);
232
+ } else {
233
+ // Non-continuation, non-blank = end of error block
234
+ blocks.push(currentBlock.join("\n").trim());
235
+ currentBlock = [];
236
+ inErrorBlock = false;
237
+
238
+ // Check if THIS line starts a new error block
239
+ if (errorStarters.some(p => p.test(trimmed))) {
240
+ inErrorBlock = true;
241
+ currentBlock = [line];
242
+ }
243
+ }
244
+ }
245
+ }
246
+
247
+ // Flush remaining block
248
+ if (currentBlock.length > 0) {
249
+ blocks.push(currentBlock.join("\n").trim());
250
+ }
251
+
252
+ return blocks;
253
+ }
254
+
166
255
  /** Discover safety hints about a command */
167
256
  export function discoverSafetyHints(command: string): string[] {
168
257
  const hints: string[] = [];
@@ -0,0 +1,238 @@
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
+
5
+ import { readdirSync, readFileSync, statSync, existsSync } from "fs";
6
+ import { join } from "path";
7
+ import { estimateTokens } from "./parsers/index.js";
8
+
9
+ export interface DiscoveredCommand {
10
+ command: string;
11
+ outputTokens: number;
12
+ outputChars: number;
13
+ sessionFile: string;
14
+ timestamp?: string;
15
+ }
16
+
17
+ export interface DiscoverReport {
18
+ totalSessions: number;
19
+ totalCommands: number;
20
+ totalOutputTokens: number;
21
+ estimatedSavings: number; // tokens saved at 70% compression
22
+ estimatedSavingsUsd: number; // at Opus rates ($5/M input)
23
+ topCommands: { command: string; count: number; totalTokens: number; avgTokens: number }[];
24
+ commandsByCategory: Record<string, { count: number; tokens: number }>;
25
+ }
26
+
27
+ /** Find all Claude session JSONL files */
28
+ function findSessionFiles(claudeDir: string, maxAge?: number): string[] {
29
+ const files: string[] = [];
30
+ const projectsDir = join(claudeDir, "projects");
31
+ if (!existsSync(projectsDir)) return files;
32
+
33
+ const now = Date.now();
34
+ const cutoff = maxAge ? now - maxAge : 0;
35
+
36
+ try {
37
+ for (const project of readdirSync(projectsDir)) {
38
+ const projectPath = join(projectsDir, project);
39
+ // Look for session JSONL files (not subagents)
40
+ try {
41
+ for (const entry of readdirSync(projectPath)) {
42
+ if (entry.endsWith(".jsonl")) {
43
+ const filePath = join(projectPath, entry);
44
+ try {
45
+ const stat = statSync(filePath);
46
+ if (stat.mtimeMs > cutoff) files.push(filePath);
47
+ } catch {}
48
+ }
49
+ }
50
+ } catch {}
51
+ }
52
+ } catch {}
53
+
54
+ return files;
55
+ }
56
+
57
+ /** Extract Bash commands and their output sizes from a session file */
58
+ function extractCommands(sessionFile: string): DiscoveredCommand[] {
59
+ const commands: DiscoveredCommand[] = [];
60
+
61
+ try {
62
+ const content = readFileSync(sessionFile, "utf8");
63
+ const lines = content.split("\n").filter(l => l.trim());
64
+
65
+ // Track tool_use IDs to match with tool_results
66
+ const pendingToolUses: Map<string, string> = new Map(); // id -> command
67
+
68
+ for (const line of lines) {
69
+ try {
70
+ const obj = JSON.parse(line);
71
+ const msg = obj.message;
72
+ if (!msg?.content || !Array.isArray(msg.content)) continue;
73
+
74
+ for (const block of msg.content) {
75
+ // Capture Bash tool_use commands
76
+ if (block.type === "tool_use" && block.name === "Bash" && block.input?.command) {
77
+ pendingToolUses.set(block.id, block.input.command);
78
+ }
79
+
80
+ // Capture tool_result outputs and match to commands
81
+ if (block.type === "tool_result" && block.tool_use_id) {
82
+ const command = pendingToolUses.get(block.tool_use_id);
83
+ if (command) {
84
+ let outputText = "";
85
+ if (typeof block.content === "string") {
86
+ outputText = block.content;
87
+ } else if (Array.isArray(block.content)) {
88
+ outputText = block.content
89
+ .filter((c: any) => c.type === "text")
90
+ .map((c: any) => c.text)
91
+ .join("\n");
92
+ }
93
+
94
+ if (outputText.length > 0) {
95
+ commands.push({
96
+ command,
97
+ outputTokens: estimateTokens(outputText),
98
+ outputChars: outputText.length,
99
+ sessionFile,
100
+ });
101
+ }
102
+ pendingToolUses.delete(block.tool_use_id);
103
+ }
104
+ }
105
+ }
106
+ } catch {} // skip malformed lines
107
+ }
108
+ } catch {} // skip unreadable files
109
+
110
+ return commands;
111
+ }
112
+
113
+ /** Categorize a command into a bucket */
114
+ function categorizeCommand(cmd: string): string {
115
+ const trimmed = cmd.trim();
116
+ if (/^git\b/.test(trimmed)) return "git";
117
+ if (/\b(bun|npm|yarn|pnpm)\s+(test|run\s+test)/.test(trimmed)) return "test";
118
+ if (/\b(bun|npm|yarn|pnpm)\s+run\s+(build|typecheck|lint)/.test(trimmed)) return "build";
119
+ if (/^(grep|rg)\b/.test(trimmed)) return "grep";
120
+ if (/^find\b/.test(trimmed)) return "find";
121
+ if (/^(cat|head|tail|less)\b/.test(trimmed)) return "read";
122
+ if (/^(ls|tree|du|wc)\b/.test(trimmed)) return "list";
123
+ if (/^(curl|wget|fetch)\b/.test(trimmed)) return "network";
124
+ if (/^(docker|kubectl|helm)\b/.test(trimmed)) return "infra";
125
+ if (/^(python|pip|pytest)\b/.test(trimmed)) return "python";
126
+ if (/^(cargo|rustc)\b/.test(trimmed)) return "rust";
127
+ if (/^(go\s|golangci)\b/.test(trimmed)) return "go";
128
+ return "other";
129
+ }
130
+
131
+ /** Normalize command for grouping (strip variable parts like paths, hashes) */
132
+ function normalizeCommand(cmd: string): string {
133
+ return cmd
134
+ .replace(/[0-9a-f]{7,40}/g, "{hash}") // git hashes
135
+ .replace(/\/[\w./-]+\.(ts|tsx|js|json|py|rs|go)\b/g, "{file}") // file paths
136
+ .replace(/\d{4}-\d{2}-\d{2}/g, "{date}") // dates
137
+ .replace(/:\d+/g, ":{line}") // line numbers
138
+ .trim();
139
+ }
140
+
141
+ /** Run discover across all Claude sessions */
142
+ export function discover(options: { maxAgeDays?: number; minTokens?: number } = {}): DiscoverReport {
143
+ const claudeDir = join(process.env.HOME ?? "~", ".claude");
144
+ const maxAge = (options.maxAgeDays ?? 30) * 24 * 60 * 60 * 1000;
145
+ const minTokens = options.minTokens ?? 50;
146
+
147
+ const sessionFiles = findSessionFiles(claudeDir, maxAge);
148
+ const allCommands: DiscoveredCommand[] = [];
149
+
150
+ for (const file of sessionFiles) {
151
+ allCommands.push(...extractCommands(file));
152
+ }
153
+
154
+ // Filter to commands with meaningful output
155
+ const significant = allCommands.filter(c => c.outputTokens >= minTokens);
156
+
157
+ // Group by normalized command
158
+ const groups = new Map<string, { count: number; totalTokens: number; example: string }>();
159
+ for (const cmd of significant) {
160
+ const key = normalizeCommand(cmd.command);
161
+ const existing = groups.get(key) ?? { count: 0, totalTokens: 0, example: cmd.command };
162
+ existing.count++;
163
+ existing.totalTokens += cmd.outputTokens;
164
+ groups.set(key, existing);
165
+ }
166
+
167
+ // Top commands by total tokens
168
+ const topCommands = [...groups.entries()]
169
+ .map(([cmd, data]) => ({
170
+ command: data.example,
171
+ count: data.count,
172
+ totalTokens: data.totalTokens,
173
+ avgTokens: Math.round(data.totalTokens / data.count),
174
+ }))
175
+ .sort((a, b) => b.totalTokens - a.totalTokens)
176
+ .slice(0, 20);
177
+
178
+ // Category breakdown
179
+ const commandsByCategory: Record<string, { count: number; tokens: number }> = {};
180
+ for (const cmd of significant) {
181
+ const cat = categorizeCommand(cmd.command);
182
+ if (!commandsByCategory[cat]) commandsByCategory[cat] = { count: 0, tokens: 0 };
183
+ commandsByCategory[cat].count++;
184
+ commandsByCategory[cat].tokens += cmd.outputTokens;
185
+ }
186
+
187
+ const totalOutputTokens = significant.reduce((sum, c) => sum + c.outputTokens, 0);
188
+ // Conservative 70% compression estimate (RTK claims 60-90%)
189
+ const estimatedSavings = Math.round(totalOutputTokens * 0.7);
190
+ // Each saved input token is repeated across ~5 turns on average before compaction
191
+ const multipliedSavings = estimatedSavings * 5;
192
+ // At Opus rates ($5/M input tokens)
193
+ const estimatedSavingsUsd = (multipliedSavings * 5) / 1_000_000;
194
+
195
+ return {
196
+ totalSessions: sessionFiles.length,
197
+ totalCommands: significant.length,
198
+ totalOutputTokens,
199
+ estimatedSavings,
200
+ estimatedSavingsUsd,
201
+ topCommands,
202
+ commandsByCategory,
203
+ };
204
+ }
205
+
206
+ /** Format discover report for CLI display */
207
+ export function formatDiscoverReport(report: DiscoverReport): string {
208
+ const lines: string[] = [];
209
+
210
+ lines.push(`📊 Terminal Discover — Token Savings Analysis`);
211
+ lines.push(` Scanned ${report.totalSessions} sessions, ${report.totalCommands} commands with >50 token output\n`);
212
+
213
+ lines.push(`💰 Estimated savings with open-terminal:`);
214
+ lines.push(` Output tokens: ${report.totalOutputTokens.toLocaleString()}`);
215
+ lines.push(` Compressible: ${report.estimatedSavings.toLocaleString()} tokens (70% avg)`);
216
+ lines.push(` Repeated ~5x before compaction = ${(report.estimatedSavings * 5).toLocaleString()} billable tokens`);
217
+ lines.push(` At Opus rates: $${report.estimatedSavingsUsd.toFixed(2)} saved\n`);
218
+
219
+ if (report.topCommands.length > 0) {
220
+ lines.push(`🔝 Top commands by token cost:`);
221
+ for (const cmd of report.topCommands.slice(0, 15)) {
222
+ const avg = cmd.avgTokens.toLocaleString().padStart(6);
223
+ const total = cmd.totalTokens.toLocaleString().padStart(8);
224
+ lines.push(` ${String(cmd.count).padStart(4)}× ${avg} avg → ${total} total ${cmd.command.slice(0, 60)}`);
225
+ }
226
+ lines.push("");
227
+ }
228
+
229
+ if (Object.keys(report.commandsByCategory).length > 0) {
230
+ lines.push(`📁 By category:`);
231
+ const sorted = Object.entries(report.commandsByCategory).sort((a, b) => b[1].tokens - a[1].tokens);
232
+ for (const [cat, data] of sorted) {
233
+ lines.push(` ${cat.padEnd(10)} ${String(data.count).padStart(5)} cmds ${data.tokens.toLocaleString().padStart(10)} tokens`);
234
+ }
235
+ }
236
+
237
+ return lines.join("\n");
238
+ }