@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,129 @@
1
+ // AI-powered output processor — uses cheap AI to intelligently summarize any output
2
+ // NOTHING is hardcoded. The AI decides what's important, what's noise, what to keep.
3
+ import { getProvider } from "./providers/index.js";
4
+ import { estimateTokens } from "./parsers/index.js";
5
+ import { recordSaving } from "./economy.js";
6
+ import { discoverOutputHints } from "./context-hints.js";
7
+ import { formatProfileHints } from "./tool-profiles.js";
8
+ const MIN_LINES_TO_PROCESS = 15;
9
+ const MAX_OUTPUT_FOR_AI = 8000; // chars to send to AI (truncate if longer)
10
+ const SUMMARIZE_PROMPT = `You are an intelligent terminal assistant. Given a user's original question and the command output, ANSWER THE QUESTION directly.
11
+
12
+ RULES:
13
+ - If the user asked a YES/NO question, start with Yes or No, then explain briefly
14
+ - If the user asked "how many", give the number first, then context
15
+ - If the user asked "show me X", show only X, not everything
16
+ - ANSWER the question using the data — don't just summarize the raw output
17
+ - Use symbols: ✓ for success/yes, ✗ for failure/no, ⚠ for warnings
18
+ - Maximum 8 lines
19
+ - Keep errors/failures verbatim
20
+ - Be direct and concise — the user wants an ANSWER, not a data dump
21
+ - For TEST OUTPUT: look for "X pass" and "X fail" lines. These are DEFINITIVE. If you see "42 pass, 0 fail" in the output, the answer is "42 tests pass, 0 fail." NEVER say "no tests found" or "incomplete" when pass/fail counts are visible.
22
+ - For BUILD OUTPUT: if tsc/build exits 0 with no output, it SUCCEEDED. Empty output = success.`;
23
+ /**
24
+ * Process command output through AI summarization.
25
+ * Cheap AI call (~100 tokens) saves 1000+ tokens downstream.
26
+ */
27
+ export async function processOutput(command, output, originalPrompt) {
28
+ const lines = output.split("\n");
29
+ // Short output — skip AI UNLESS we have an original prompt (NL mode needs answer framing)
30
+ if (lines.length <= MIN_LINES_TO_PROCESS && !originalPrompt) {
31
+ return {
32
+ summary: output,
33
+ full: output,
34
+ tokensSaved: 0,
35
+ aiTokensUsed: 0,
36
+ aiProcessed: false,
37
+ aiCostUsd: 0,
38
+ savingsValueUsd: 0,
39
+ netSavingsUsd: 0,
40
+ };
41
+ }
42
+ // Truncate very long output before sending to AI
43
+ let toSummarize = output;
44
+ if (toSummarize.length > MAX_OUTPUT_FOR_AI) {
45
+ const headChars = Math.floor(MAX_OUTPUT_FOR_AI * 0.6);
46
+ const tailChars = Math.floor(MAX_OUTPUT_FOR_AI * 0.3);
47
+ toSummarize = output.slice(0, headChars) +
48
+ `\n\n... (${lines.length} total lines, middle truncated) ...\n\n` +
49
+ output.slice(-tailChars);
50
+ }
51
+ try {
52
+ // Discover output hints — regex discovers patterns, AI decides what matters
53
+ const outputHints = discoverOutputHints(output, command);
54
+ const hintsBlock = outputHints.length > 0
55
+ ? `\n\nOUTPUT OBSERVATIONS:\n${outputHints.join("\n")}`
56
+ : "";
57
+ // Inject tool-specific profile hints
58
+ const profileBlock = formatProfileHints(command);
59
+ const profileHints = profileBlock ? `\n\n${profileBlock}` : "";
60
+ const provider = getProvider();
61
+ const summary = await provider.complete(`${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}${hintsBlock}${profileHints}`, {
62
+ system: SUMMARIZE_PROMPT,
63
+ maxTokens: 300,
64
+ });
65
+ const originalTokens = estimateTokens(output);
66
+ const summaryTokens = estimateTokens(summary);
67
+ const saved = Math.max(0, originalTokens - summaryTokens);
68
+ if (saved > 0) {
69
+ recordSaving("compressed", saved);
70
+ }
71
+ // Try to extract structured JSON if the AI returned it
72
+ let structured;
73
+ try {
74
+ const jsonMatch = summary.match(/\{[\s\S]*\}/);
75
+ if (jsonMatch) {
76
+ structured = JSON.parse(jsonMatch[0]);
77
+ }
78
+ }
79
+ catch { /* not JSON, that's fine */ }
80
+ // Cost calculation
81
+ // AI input: system prompt (~200 tokens) + command + output sent to AI
82
+ const aiInputTokens = estimateTokens(SUMMARIZE_PROMPT) + estimateTokens(toSummarize) + 20;
83
+ const aiOutputTokens = summaryTokens;
84
+ const aiTokensUsed = aiInputTokens + aiOutputTokens;
85
+ // Cerebras qwen-3-235b pricing: $0.60/M input, $1.20/M output
86
+ const aiCostUsd = (aiInputTokens * 0.60 + aiOutputTokens * 1.20) / 1_000_000;
87
+ // Value of tokens saved (at Claude Sonnet $3/M input — what the agent would pay)
88
+ const savingsValueUsd = (saved * 3.0) / 1_000_000;
89
+ const netSavingsUsd = savingsValueUsd - aiCostUsd;
90
+ // Only record savings if net positive (AI cost < token savings value)
91
+ if (netSavingsUsd > 0 && saved > 0) {
92
+ recordSaving("compressed", saved);
93
+ }
94
+ return {
95
+ summary,
96
+ full: output,
97
+ structured,
98
+ tokensSaved: saved,
99
+ aiTokensUsed,
100
+ aiProcessed: true,
101
+ aiCostUsd,
102
+ savingsValueUsd,
103
+ netSavingsUsd,
104
+ };
105
+ }
106
+ catch {
107
+ // AI unavailable — fall back to simple truncation
108
+ const head = lines.slice(0, 5).join("\n");
109
+ const tail = lines.slice(-5).join("\n");
110
+ const fallback = `${head}\n ... (${lines.length - 10} lines hidden) ...\n${tail}`;
111
+ return {
112
+ summary: fallback,
113
+ full: output,
114
+ tokensSaved: Math.max(0, estimateTokens(output) - estimateTokens(fallback)),
115
+ aiTokensUsed: 0,
116
+ aiProcessed: false,
117
+ aiCostUsd: 0,
118
+ savingsValueUsd: 0,
119
+ netSavingsUsd: 0,
120
+ };
121
+ }
122
+ }
123
+ /**
124
+ * Lightweight version — just decides IF output should be processed.
125
+ * Returns true if the output would benefit from AI summarization.
126
+ */
127
+ export function shouldProcess(output) {
128
+ return output.split("\n").length > MIN_LINES_TO_PROCESS;
129
+ }
@@ -0,0 +1,41 @@
1
+ // Output intelligence router — auto-detect command type and optimize output
2
+ import { parseOutput, estimateTokens } from "./parsers/index.js";
3
+ import { compress, stripAnsi } from "./compression.js";
4
+ import { recordSaving } from "./economy.js";
5
+ /** Route command output through the best optimization path */
6
+ export function routeOutput(command, output, maxTokens) {
7
+ const clean = stripAnsi(output);
8
+ const rawTokens = estimateTokens(clean);
9
+ // Try structured parsing first
10
+ const parsed = parseOutput(command, clean);
11
+ if (parsed) {
12
+ const json = JSON.stringify(parsed.data);
13
+ const jsonTokens = estimateTokens(json);
14
+ const saved = rawTokens - jsonTokens;
15
+ if (saved > 0) {
16
+ recordSaving("structured", saved);
17
+ return {
18
+ raw: clean,
19
+ structured: parsed.data,
20
+ parser: parsed.parser,
21
+ tokensSaved: saved,
22
+ format: "json",
23
+ };
24
+ }
25
+ }
26
+ // Try compression if structured didn't save enough
27
+ if (maxTokens || rawTokens > 200) {
28
+ const compressed = compress(command, clean, { maxTokens, format: "text" });
29
+ if (compressed.tokensSaved > 0) {
30
+ recordSaving("compressed", compressed.tokensSaved);
31
+ return {
32
+ raw: clean,
33
+ compressed: compressed.content,
34
+ tokensSaved: compressed.tokensSaved,
35
+ format: "compressed",
36
+ };
37
+ }
38
+ }
39
+ // Return raw if no optimization helps
40
+ return { raw: clean, tokensSaved: 0, format: "raw" };
41
+ }
@@ -0,0 +1,111 @@
1
+ // Output store — saves full raw output to disk when AI compresses it
2
+ // Agents can read the file for full detail. Tiered retention strategy.
3
+ import { existsSync, mkdirSync, writeFileSync, readdirSync, statSync, unlinkSync } from "fs";
4
+ import { join } from "path";
5
+ import { createHash } from "crypto";
6
+ const OUTPUTS_DIR = join(process.env.HOME ?? "~", ".terminal", "outputs");
7
+ /** Ensure outputs directory exists */
8
+ function ensureDir() {
9
+ if (!existsSync(OUTPUTS_DIR))
10
+ mkdirSync(OUTPUTS_DIR, { recursive: true });
11
+ }
12
+ /** Generate a short hash for an output */
13
+ function hashOutput(command, output) {
14
+ return createHash("md5").update(command + output.slice(0, 1000)).digest("hex").slice(0, 12);
15
+ }
16
+ /** Tiered retention: recent = keep all, older = keep only high-value */
17
+ function rotate() {
18
+ try {
19
+ const now = Date.now();
20
+ const ONE_HOUR = 60 * 60 * 1000;
21
+ const ONE_DAY = 24 * ONE_HOUR;
22
+ const files = readdirSync(OUTPUTS_DIR)
23
+ .filter(f => f.endsWith(".txt"))
24
+ .map(f => {
25
+ const path = join(OUTPUTS_DIR, f);
26
+ const stat = statSync(path);
27
+ return { name: f, path, mtime: stat.mtimeMs, size: stat.size };
28
+ })
29
+ .sort((a, b) => b.mtime - a.mtime); // newest first
30
+ for (const file of files) {
31
+ const age = now - file.mtime;
32
+ // Last 1 hour: keep everything
33
+ if (age < ONE_HOUR)
34
+ continue;
35
+ // Last 24 hours: keep outputs >2KB (meaningful compression)
36
+ if (age < ONE_DAY) {
37
+ if (file.size < 2000) {
38
+ try {
39
+ unlinkSync(file.path);
40
+ }
41
+ catch { }
42
+ }
43
+ continue;
44
+ }
45
+ // Older than 24h: keep only >10KB (high-value saves)
46
+ if (file.size < 10000) {
47
+ try {
48
+ unlinkSync(file.path);
49
+ }
50
+ catch { }
51
+ continue;
52
+ }
53
+ // Older than 7 days: remove everything
54
+ if (age > 7 * ONE_DAY) {
55
+ try {
56
+ unlinkSync(file.path);
57
+ }
58
+ catch { }
59
+ }
60
+ }
61
+ // Hard cap: never exceed 100 files or 10MB total
62
+ const remaining = readdirSync(OUTPUTS_DIR)
63
+ .filter(f => f.endsWith(".txt"))
64
+ .map(f => ({ path: join(OUTPUTS_DIR, f), mtime: statSync(join(OUTPUTS_DIR, f)).mtimeMs, size: statSync(join(OUTPUTS_DIR, f)).size }))
65
+ .sort((a, b) => b.mtime - a.mtime);
66
+ let totalSize = 0;
67
+ for (let i = 0; i < remaining.length; i++) {
68
+ totalSize += remaining[i].size;
69
+ if (i >= 100 || totalSize > 10 * 1024 * 1024) {
70
+ try {
71
+ unlinkSync(remaining[i].path);
72
+ }
73
+ catch { }
74
+ }
75
+ }
76
+ }
77
+ catch { }
78
+ }
79
+ /** Save full output to disk, return the file path */
80
+ export function saveOutput(command, rawOutput) {
81
+ ensureDir();
82
+ const hash = hashOutput(command, rawOutput);
83
+ const filename = `${hash}.txt`;
84
+ const filepath = join(OUTPUTS_DIR, filename);
85
+ const content = `$ ${command}\n${"─".repeat(60)}\n${rawOutput}`;
86
+ writeFileSync(filepath, content, "utf8");
87
+ rotate();
88
+ return filepath;
89
+ }
90
+ /** Format the hint line that tells agents where to find full output */
91
+ export function formatOutputHint(filepath) {
92
+ return `[full output: ${filepath}]`;
93
+ }
94
+ /** Get the outputs directory path */
95
+ export function getOutputsDir() {
96
+ return OUTPUTS_DIR;
97
+ }
98
+ /** Manually purge all outputs */
99
+ export function purgeOutputs() {
100
+ if (!existsSync(OUTPUTS_DIR))
101
+ return 0;
102
+ let count = 0;
103
+ for (const f of readdirSync(OUTPUTS_DIR)) {
104
+ try {
105
+ unlinkSync(join(OUTPUTS_DIR, f));
106
+ count++;
107
+ }
108
+ catch { }
109
+ }
110
+ return count;
111
+ }
@@ -0,0 +1,2 @@
1
+ // Base types for output parsers
2
+ export {};
@@ -0,0 +1,64 @@
1
+ // Parser for build output (npm/bun/pnpm build, tsc, webpack, vite, etc.)
2
+ export const buildParser = {
3
+ name: "build",
4
+ detect(command, output) {
5
+ if (/\b(npm|bun|pnpm|yarn)\s+(run\s+)?build\b/.test(command))
6
+ return true;
7
+ if (/\btsc\b/.test(command))
8
+ return true;
9
+ if (/\b(webpack|vite|esbuild|rollup|turbo)\b/.test(command))
10
+ return true;
11
+ return /\b(compiled|bundled|built)\b/i.test(output) && /\b(success|error|warning)\b/i.test(output);
12
+ },
13
+ parse(_command, output) {
14
+ const lines = output.split("\n");
15
+ let warnings = 0, errors = 0, duration;
16
+ // Count warnings and errors
17
+ for (const line of lines) {
18
+ if (/\bwarning\b/i.test(line))
19
+ warnings++;
20
+ if (/\berror\b/i.test(line) && !/0 errors/.test(line))
21
+ errors++;
22
+ }
23
+ // Specific patterns
24
+ const tscErrors = output.match(/Found (\d+) error/);
25
+ if (tscErrors)
26
+ errors = parseInt(tscErrors[1]);
27
+ const warningCount = output.match(/(\d+)\s+warning/);
28
+ if (warningCount)
29
+ warnings = parseInt(warningCount[1]);
30
+ // Duration
31
+ const timeMatch = output.match(/(?:in|took)\s+([\d.]+\s*(?:s|ms|m))/i) ||
32
+ output.match(/Done in ([\d.]+s)/);
33
+ if (timeMatch)
34
+ duration = timeMatch[1];
35
+ const status = errors > 0 ? "failure" : "success";
36
+ return { status, warnings, errors, duration };
37
+ },
38
+ };
39
+ export const npmInstallParser = {
40
+ name: "npm-install",
41
+ detect(command, _output) {
42
+ return /\b(npm|bun|pnpm|yarn)\s+(install|add|i)\b/.test(command);
43
+ },
44
+ parse(_command, output) {
45
+ let installed = 0, vulnerabilities = 0, duration;
46
+ // npm: added 47 packages in 3.2s
47
+ const npmMatch = output.match(/added\s+(\d+)\s+packages?\s+in\s+([\d.]+s)/);
48
+ if (npmMatch) {
49
+ installed = parseInt(npmMatch[1]);
50
+ duration = npmMatch[2];
51
+ }
52
+ // bun: 47 packages installed [1.2s]
53
+ const bunMatch = output.match(/(\d+)\s+packages?\s+installed.*?\[([\d.]+[ms]*s)\]/);
54
+ if (!npmMatch && bunMatch) {
55
+ installed = parseInt(bunMatch[1]);
56
+ duration = bunMatch[2];
57
+ }
58
+ // Vulnerabilities
59
+ const vulnMatch = output.match(/(\d+)\s+vulnerabilit/);
60
+ if (vulnMatch)
61
+ vulnerabilities = parseInt(vulnMatch[1]);
62
+ return { installed, vulnerabilities, duration };
63
+ },
64
+ };
@@ -0,0 +1,101 @@
1
+ // Parser for common error patterns
2
+ const ERROR_PATTERNS = [
3
+ {
4
+ type: "port_in_use",
5
+ pattern: /EADDRINUSE.*?(?::(\d+))|port\s+(\d+)\s+(?:is\s+)?(?:already\s+)?in\s+use/i,
6
+ extract: (m) => ({
7
+ type: "port_in_use",
8
+ message: m[0],
9
+ suggestion: `Kill the process: lsof -i :${m[1] ?? m[2]} -t | xargs kill`,
10
+ }),
11
+ },
12
+ {
13
+ type: "file_not_found",
14
+ pattern: /ENOENT.*?'([^']+)'|No such file or directory:\s*(.+)/,
15
+ extract: (m) => ({
16
+ type: "file_not_found",
17
+ message: m[0],
18
+ file: m[1] ?? m[2]?.trim(),
19
+ suggestion: "Check the file path exists",
20
+ }),
21
+ },
22
+ {
23
+ type: "permission_denied",
24
+ pattern: /EACCES.*?'([^']+)'|Permission denied:\s*(.+)/,
25
+ extract: (m) => ({
26
+ type: "permission_denied",
27
+ message: m[0],
28
+ file: m[1] ?? m[2]?.trim(),
29
+ suggestion: "Check file permissions or run with sudo",
30
+ }),
31
+ },
32
+ {
33
+ type: "command_not_found",
34
+ pattern: /command not found:\s*(\S+)|(\S+):\s*not found/,
35
+ extract: (m) => ({
36
+ type: "command_not_found",
37
+ message: m[0],
38
+ suggestion: `Install ${m[1] ?? m[2]} or check your PATH`,
39
+ }),
40
+ },
41
+ {
42
+ type: "dependency_missing",
43
+ pattern: /Cannot find module\s+'([^']+)'|Module not found.*?'([^']+)'/,
44
+ extract: (m) => ({
45
+ type: "dependency_missing",
46
+ message: m[0],
47
+ suggestion: `Install: npm install ${m[1] ?? m[2]}`,
48
+ }),
49
+ },
50
+ {
51
+ type: "syntax_error",
52
+ pattern: /SyntaxError:\s*(.+)|error TS\d+:\s*(.+)/,
53
+ extract: (m, output) => {
54
+ const fileMatch = output.match(/(\S+\.\w+):(\d+)/);
55
+ return {
56
+ type: "syntax_error",
57
+ message: m[1] ?? m[2] ?? m[0],
58
+ file: fileMatch?.[1],
59
+ line: fileMatch ? parseInt(fileMatch[2]) : undefined,
60
+ suggestion: "Fix the syntax error in the referenced file",
61
+ };
62
+ },
63
+ },
64
+ {
65
+ type: "out_of_memory",
66
+ pattern: /ENOMEM|JavaScript heap out of memory|Killed/,
67
+ extract: (m) => ({
68
+ type: "out_of_memory",
69
+ message: m[0],
70
+ suggestion: "Increase memory: NODE_OPTIONS=--max-old-space-size=4096",
71
+ }),
72
+ },
73
+ {
74
+ type: "network_error",
75
+ pattern: /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|fetch failed/,
76
+ extract: (m) => ({
77
+ type: "network_error",
78
+ message: m[0],
79
+ suggestion: "Check network connection and target URL/host",
80
+ }),
81
+ },
82
+ ];
83
+ export const errorParser = {
84
+ name: "error",
85
+ detect(_command, output) {
86
+ return ERROR_PATTERNS.some(({ pattern }) => pattern.test(output));
87
+ },
88
+ parse(_command, output) {
89
+ for (const { pattern, extract } of ERROR_PATTERNS) {
90
+ const match = output.match(pattern);
91
+ if (match)
92
+ return extract(match, output);
93
+ }
94
+ // Generic error fallback
95
+ const errorLine = output.split("\n").find(l => /error/i.test(l));
96
+ return {
97
+ type: "unknown",
98
+ message: errorLine?.trim() ?? "Unknown error",
99
+ };
100
+ },
101
+ };
@@ -0,0 +1,78 @@
1
+ // Parser for file listing output (ls -la, find, etc.)
2
+ const NODE_MODULES_RE = /node_modules/;
3
+ const DIST_RE = /\b(dist|build|\.next|__pycache__|coverage|\.git)\b/;
4
+ const SOURCE_EXTS = /\.(ts|tsx|js|jsx|py|go|rs|java|rb|sh|c|cpp|h|css|scss|html|vue|svelte|md|json|yaml|yml|toml)$/;
5
+ export const lsParser = {
6
+ name: "ls",
7
+ detect(command, output) {
8
+ return /^\s*(ls|ll|la)\b/.test(command) && output.includes(" ");
9
+ },
10
+ parse(_command, output) {
11
+ const lines = output.split("\n").filter(l => l.trim());
12
+ const entries = [];
13
+ for (const line of lines) {
14
+ // ls -la format: drwxr-xr-x 5 user group 160 Mar 10 09:00 dirname
15
+ const match = line.match(/^([dlcbps-])([rwxsStT-]{9})\s+\d+\s+\S+\s+\S+\s+(\d+)\s+(\w+\s+\d+\s+[\d:]+)\s+(.+)$/);
16
+ if (match) {
17
+ const typeChar = match[1];
18
+ entries.push({
19
+ name: match[5],
20
+ type: typeChar === "d" ? "dir" : typeChar === "l" ? "symlink" : "file",
21
+ size: parseInt(match[3]),
22
+ modified: match[4],
23
+ permissions: match[1] + match[2],
24
+ });
25
+ }
26
+ else if (line.trim() && !line.startsWith("total ")) {
27
+ // Simple ls output — just filenames
28
+ entries.push({ name: line.trim(), type: "file" });
29
+ }
30
+ }
31
+ return entries;
32
+ },
33
+ };
34
+ export const findParser = {
35
+ name: "find",
36
+ detect(command, _output) {
37
+ return /^\s*(find|fd)\b/.test(command);
38
+ },
39
+ parse(_command, output) {
40
+ const lines = output.split("\n").filter(l => l.trim());
41
+ const source = [];
42
+ const other = [];
43
+ let nodeModulesCount = 0;
44
+ let distCount = 0;
45
+ for (const line of lines) {
46
+ const path = line.trim();
47
+ if (!path)
48
+ continue;
49
+ if (NODE_MODULES_RE.test(path)) {
50
+ nodeModulesCount++;
51
+ continue;
52
+ }
53
+ if (DIST_RE.test(path)) {
54
+ distCount++;
55
+ continue;
56
+ }
57
+ const name = path.split("/").pop() ?? path;
58
+ const entry = { name: path, type: SOURCE_EXTS.test(name) ? "file" : "other" };
59
+ if (SOURCE_EXTS.test(name)) {
60
+ source.push(entry);
61
+ }
62
+ else {
63
+ other.push(entry);
64
+ }
65
+ }
66
+ const filtered = [];
67
+ if (nodeModulesCount > 0)
68
+ filtered.push({ count: nodeModulesCount, reason: "node_modules" });
69
+ if (distCount > 0)
70
+ filtered.push({ count: distCount, reason: "dist/build" });
71
+ return {
72
+ total: lines.length,
73
+ source,
74
+ other,
75
+ filtered,
76
+ };
77
+ },
78
+ };
@@ -0,0 +1,99 @@
1
+ // Parsers for git output (log, status, diff)
2
+ export const gitLogParser = {
3
+ name: "git-log",
4
+ detect(command, _output) {
5
+ return /\bgit\s+log\b/.test(command);
6
+ },
7
+ parse(_command, output) {
8
+ const entries = [];
9
+ const lines = output.split("\n");
10
+ // Detect oneline format: "abc1234 commit message"
11
+ const firstLine = lines[0]?.trim() ?? "";
12
+ const isOneline = /^[a-f0-9]{7,12}\s+/.test(firstLine) && !firstLine.startsWith("commit ");
13
+ if (isOneline) {
14
+ for (const line of lines) {
15
+ const match = line.trim().match(/^([a-f0-9]{7,12})\s+(.+)$/);
16
+ if (match) {
17
+ entries.push({ hash: match[1], author: "", date: "", message: match[2] });
18
+ }
19
+ }
20
+ return entries;
21
+ }
22
+ // Verbose format
23
+ let hash = "", author = "", date = "", message = [];
24
+ for (const line of lines) {
25
+ const commitMatch = line.match(/^commit\s+([a-f0-9]+)/);
26
+ if (commitMatch) {
27
+ if (hash) {
28
+ entries.push({ hash: hash.slice(0, 8), author, date, message: message.join(" ").trim() });
29
+ }
30
+ hash = commitMatch[1];
31
+ author = "";
32
+ date = "";
33
+ message = [];
34
+ continue;
35
+ }
36
+ const authorMatch = line.match(/^Author:\s+(.+)/);
37
+ if (authorMatch) {
38
+ author = authorMatch[1];
39
+ continue;
40
+ }
41
+ const dateMatch = line.match(/^Date:\s+(.+)/);
42
+ if (dateMatch) {
43
+ date = dateMatch[1].trim();
44
+ continue;
45
+ }
46
+ if (line.startsWith(" ")) {
47
+ message.push(line.trim());
48
+ }
49
+ }
50
+ if (hash) {
51
+ entries.push({ hash: hash.slice(0, 8), author, date, message: message.join(" ").trim() });
52
+ }
53
+ return entries;
54
+ },
55
+ };
56
+ export const gitStatusParser = {
57
+ name: "git-status",
58
+ detect(command, _output) {
59
+ return /\bgit\s+status\b/.test(command);
60
+ },
61
+ parse(_command, output) {
62
+ const lines = output.split("\n");
63
+ let branch = "";
64
+ const staged = [];
65
+ const unstaged = [];
66
+ const untracked = [];
67
+ const branchMatch = output.match(/On branch\s+(\S+)/);
68
+ if (branchMatch)
69
+ branch = branchMatch[1];
70
+ let section = "";
71
+ for (const line of lines) {
72
+ if (line.includes("Changes to be committed")) {
73
+ section = "staged";
74
+ continue;
75
+ }
76
+ if (line.includes("Changes not staged")) {
77
+ section = "unstaged";
78
+ continue;
79
+ }
80
+ if (line.includes("Untracked files")) {
81
+ section = "untracked";
82
+ continue;
83
+ }
84
+ const fileMatch = line.match(/^\s+(?:new file|modified|deleted|renamed):\s+(.+)/);
85
+ if (fileMatch) {
86
+ if (section === "staged")
87
+ staged.push(fileMatch[1].trim());
88
+ else if (section === "unstaged")
89
+ unstaged.push(fileMatch[1].trim());
90
+ continue;
91
+ }
92
+ // Untracked files are just indented filenames
93
+ if (section === "untracked" && line.match(/^\s+\S/) && !line.includes("(use ")) {
94
+ untracked.push(line.trim());
95
+ }
96
+ }
97
+ return { branch, staged, unstaged, untracked };
98
+ },
99
+ };
@@ -0,0 +1,48 @@
1
+ // Output parser registry — auto-detect command output type and parse to structured JSON
2
+ import { lsParser, findParser } from "./files.js";
3
+ import { testParser } from "./tests.js";
4
+ import { gitLogParser, gitStatusParser } from "./git.js";
5
+ import { buildParser, npmInstallParser } from "./build.js";
6
+ import { errorParser } from "./errors.js";
7
+ // Ordered by specificity — more specific parsers first
8
+ const parsers = [
9
+ npmInstallParser,
10
+ testParser,
11
+ gitLogParser,
12
+ gitStatusParser,
13
+ buildParser,
14
+ findParser,
15
+ lsParser,
16
+ errorParser, // fallback for error detection
17
+ ];
18
+ /** Try to parse command output with the best matching parser */
19
+ export function parseOutput(command, output) {
20
+ for (const parser of parsers) {
21
+ if (parser.detect(command, output)) {
22
+ try {
23
+ const data = parser.parse(command, output);
24
+ return { parser: parser.name, data, raw: output };
25
+ }
26
+ catch {
27
+ continue;
28
+ }
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+ /** Get all parsers that match (for debugging/info) */
34
+ export function detectParsers(command, output) {
35
+ return parsers.filter(p => p.detect(command, output)).map(p => p.name);
36
+ }
37
+ /** Estimate token count for a string (rough: ~4 chars per token) */
38
+ export function estimateTokens(text) {
39
+ return Math.ceil(text.length / 4);
40
+ }
41
+ /** Calculate token savings between raw output and parsed JSON */
42
+ export function tokenSavings(raw, parsed) {
43
+ const rawTokens = estimateTokens(raw);
44
+ const parsedTokens = estimateTokens(JSON.stringify(parsed));
45
+ const saved = Math.max(0, rawTokens - parsedTokens);
46
+ const percent = rawTokens > 0 ? Math.round((saved / rawTokens) * 100) : 0;
47
+ return { rawTokens, parsedTokens, saved, percent };
48
+ }