@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,131 @@
1
+ // Test focus tracker — tracks test status across runs, only reports changes
2
+ // Instead of showing "248 passed, 2 failed" every time, shows:
3
+ // "auth.login: FIXED, auth.logout: STILL FAILING, 246 unchanged"
4
+ // Per-cwd watchlist
5
+ const watchlists = new Map();
6
+ /** Extract test names and status from test runner output (any runner) */
7
+ function extractTests(output) {
8
+ const tests = [];
9
+ const lines = output.split("\n");
10
+ for (let i = 0; i < lines.length; i++) {
11
+ const line = lines[i];
12
+ // PASS/FAIL with test name: "PASS src/auth.test.ts" or "✓ login works" or "✗ logout fails"
13
+ const passMatch = line.match(/(?:PASS|✓|✔|✅)\s+(.+)/);
14
+ if (passMatch) {
15
+ tests.push({ name: passMatch[1].trim(), status: "pass" });
16
+ continue;
17
+ }
18
+ const failMatch = line.match(/(?:FAIL|✗|✕|❌|×)\s+(.+)/);
19
+ if (failMatch) {
20
+ // Capture error from next few lines
21
+ const errorLines = [];
22
+ for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
23
+ if (lines[j].match(/(?:PASS|FAIL|✓|✗|✔|✕|Tests:|^\s*$)/))
24
+ break;
25
+ errorLines.push(lines[j].trim());
26
+ }
27
+ tests.push({ name: failMatch[1].trim(), status: "fail", error: errorLines.join(" ").slice(0, 200) });
28
+ continue;
29
+ }
30
+ // Jest/vitest style: " ● test name" for failures
31
+ const jestFail = line.match(/^\s*●\s+(.+)/);
32
+ if (jestFail) {
33
+ tests.push({ name: jestFail[1].trim(), status: "fail" });
34
+ continue;
35
+ }
36
+ }
37
+ return tests;
38
+ }
39
+ /** Detect if output looks like test runner output */
40
+ export function isTestOutput(output, command) {
41
+ // If the command is explicitly a test command, trust it
42
+ if (command && /\b(bun\s+test|npm\s+test|jest|vitest|pytest|cargo\s+test|go\s+test)\b/.test(command))
43
+ return true;
44
+ // Otherwise require BOTH a summary line AND a test runner marker in the output
45
+ const summaryLine = /(?:\d+\s+pass|\d+\s+fail|Tests?:\s+\d+|Ran\s+\d+\s+tests?)\s*$/im;
46
+ const testMarkers = /(?:✓|✗|✔|✕|PASS\s+\S+\.test|FAIL\s+\S+\.test|bun test v|jest|vitest|pytest)/;
47
+ return summaryLine.test(output) && testMarkers.test(output);
48
+ }
49
+ /** Track test results and return only changes */
50
+ export function trackTests(cwd, output) {
51
+ const current = extractTests(output);
52
+ const prev = watchlists.get(cwd);
53
+ // Count totals from raw output (more reliable than extracted tests)
54
+ let totalPassed = 0, totalFailed = 0;
55
+ const summaryMatch = output.match(/(\d+)\s+pass/i);
56
+ const failMatch = output.match(/(\d+)\s+fail/i);
57
+ if (summaryMatch)
58
+ totalPassed = parseInt(summaryMatch[1]);
59
+ if (failMatch)
60
+ totalFailed = parseInt(failMatch[1]);
61
+ // Fallback to extracted counts
62
+ if (totalPassed === 0)
63
+ totalPassed = current.filter(t => t.status === "pass").length;
64
+ if (totalFailed === 0)
65
+ totalFailed = current.filter(t => t.status === "fail").length;
66
+ // Store current for next comparison
67
+ const currentMap = new Map();
68
+ for (const t of current)
69
+ currentMap.set(t.name, t);
70
+ watchlists.set(cwd, currentMap);
71
+ // First run — no comparison possible
72
+ if (!prev) {
73
+ return {
74
+ changed: [],
75
+ newTests: current.filter(t => t.status === "fail"), // only show failures on first run
76
+ totalPassed,
77
+ totalFailed,
78
+ unchangedCount: 0,
79
+ firstRun: true,
80
+ };
81
+ }
82
+ // Compare with previous
83
+ const changed = [];
84
+ const newTests = [];
85
+ let unchangedCount = 0;
86
+ for (const [name, test] of currentMap) {
87
+ const prevTest = prev.get(name);
88
+ if (!prevTest) {
89
+ newTests.push(test);
90
+ }
91
+ else if (prevTest.status !== test.status) {
92
+ changed.push({ name, from: prevTest.status, to: test.status, error: test.error });
93
+ }
94
+ else {
95
+ unchangedCount++;
96
+ }
97
+ }
98
+ return { changed, newTests, totalPassed, totalFailed, unchangedCount, firstRun: false };
99
+ }
100
+ /** Format watchlist result for display */
101
+ export function formatWatchResult(result) {
102
+ const lines = [];
103
+ if (result.firstRun) {
104
+ lines.push(`${result.totalPassed} passed, ${result.totalFailed} failed`);
105
+ if (result.newTests.length > 0) {
106
+ for (const t of result.newTests) {
107
+ lines.push(` ✗ ${t.name}${t.error ? `: ${t.error}` : ""}`);
108
+ }
109
+ }
110
+ return lines.join("\n");
111
+ }
112
+ // Status changes
113
+ for (const c of result.changed) {
114
+ if (c.to === "pass")
115
+ lines.push(` ✓ FIXED: ${c.name}`);
116
+ else
117
+ lines.push(` ✗ BROKE: ${c.name}${c.error ? ` — ${c.error}` : ""}`);
118
+ }
119
+ // New failures
120
+ for (const t of result.newTests.filter(t => t.status === "fail")) {
121
+ lines.push(` ✗ NEW FAIL: ${t.name}${t.error ? ` — ${t.error}` : ""}`);
122
+ }
123
+ // Summary
124
+ if (result.changed.length === 0 && result.newTests.filter(t => t.status === "fail").length === 0) {
125
+ lines.push(`✓ ${result.totalPassed} passed, ${result.totalFailed} failed (no changes)`);
126
+ }
127
+ else {
128
+ lines.push(`${result.totalPassed} passed, ${result.totalFailed} failed, ${result.unchangedCount} unchanged`);
129
+ }
130
+ return lines.join("\n");
131
+ }
@@ -0,0 +1,122 @@
1
+ // Tool profiles — config-driven AI enhancement for specific command categories
2
+ // Profiles are loaded from ~/.terminal/profiles/ (user-customizable)
3
+ // Each profile tells the AI how to handle a specific tool's output
4
+ import { existsSync, readFileSync, readdirSync } from "fs";
5
+ import { join } from "path";
6
+ const PROFILES_DIR = join(process.env.HOME ?? "~", ".terminal", "profiles");
7
+ /** Built-in profiles — sensible defaults, user can override */
8
+ const BUILTIN_PROFILES = [
9
+ {
10
+ name: "git",
11
+ detect: "^git\\b",
12
+ hints: {
13
+ compress: "For git output: show branch, file counts, insertions/deletions summary. Collapse individual diffs to file-level stats.",
14
+ errors: "Git errors often include a suggested fix (e.g., 'did you mean X?'). Extract the suggestion.",
15
+ success: "Clean working tree, successful push/pull, merge complete.",
16
+ },
17
+ output: { preservePatterns: ["conflict", "CONFLICT", "fatal", "error", "diverged"] },
18
+ },
19
+ {
20
+ name: "test",
21
+ detect: "\\b(bun|npm|yarn|pnpm)\\s+(test|run\\s+test)|\\bpytest\\b|\\bcargo\\s+test\\b|\\bgo\\s+test\\b",
22
+ hints: {
23
+ compress: "For test output: show pass/fail counts FIRST, then list ONLY failing test names with error snippets. Skip passing tests entirely.",
24
+ errors: "Test failures have: test name, expected vs actual, stack trace. Extract all three.",
25
+ success: "All tests passing = one line: '✓ N tests pass, 0 fail'",
26
+ },
27
+ output: { preservePatterns: ["FAIL", "fail", "Error", "✗", "expected", "received"] },
28
+ },
29
+ {
30
+ name: "build",
31
+ detect: "\\b(tsc|bun\\s+run\\s+build|npm\\s+run\\s+build|cargo\\s+build|go\\s+build|make)\\b",
32
+ hints: {
33
+ compress: "For build output: if success with no errors, say '✓ Build succeeded'. If errors, list each error with file:line and message.",
34
+ errors: "Build errors have file:line:column format. Group by file.",
35
+ success: "Empty output or exit 0 = build succeeded.",
36
+ },
37
+ },
38
+ {
39
+ name: "lint",
40
+ detect: "\\b(eslint|biome|ruff|clippy|golangci-lint|prettier|tsc\\s+--noEmit)\\b",
41
+ hints: {
42
+ compress: "For lint output: group violations by rule name, show count per rule, one example per rule. Skip clean files.",
43
+ errors: "Lint violations: file:line rule-name message. Group by rule.",
44
+ },
45
+ output: { maxLines: 100 },
46
+ },
47
+ {
48
+ name: "install",
49
+ detect: "\\b(npm\\s+install|bun\\s+install|yarn|pip\\s+install|cargo\\s+build|go\\s+mod)\\b",
50
+ hints: {
51
+ compress: "For install output: show only errors and final summary (packages added/removed/updated). Strip progress bars, funding notices, deprecation warnings.",
52
+ },
53
+ output: { stripPatterns: ["npm warn", "packages are looking for funding", "run `npm fund`"] },
54
+ },
55
+ {
56
+ name: "find",
57
+ detect: "^find\\b",
58
+ hints: {
59
+ compress: "For find output: if >50 results, group by top-level directory with counts. Show first 10 results as examples.",
60
+ },
61
+ },
62
+ {
63
+ name: "docker",
64
+ detect: "\\b(docker|kubectl|helm)\\b",
65
+ hints: {
66
+ compress: "For container output: show container status, image, ports. Strip pull progress and layer hashes.",
67
+ errors: "Docker errors: extract the error message after 'Error response from daemon:'",
68
+ },
69
+ },
70
+ ];
71
+ /** Load user profiles from ~/.terminal/profiles/ */
72
+ function loadUserProfiles() {
73
+ if (!existsSync(PROFILES_DIR))
74
+ return [];
75
+ const profiles = [];
76
+ try {
77
+ for (const file of readdirSync(PROFILES_DIR)) {
78
+ if (!file.endsWith(".json"))
79
+ continue;
80
+ try {
81
+ const content = JSON.parse(readFileSync(join(PROFILES_DIR, file), "utf8"));
82
+ if (content.name && content.detect)
83
+ profiles.push(content);
84
+ }
85
+ catch { }
86
+ }
87
+ }
88
+ catch { }
89
+ return profiles;
90
+ }
91
+ /** Get all profiles — user profiles override builtins by name */
92
+ export function getProfiles() {
93
+ const user = loadUserProfiles();
94
+ const userNames = new Set(user.map(p => p.name));
95
+ const builtins = BUILTIN_PROFILES.filter(p => !userNames.has(p.name));
96
+ return [...user, ...builtins];
97
+ }
98
+ /** Find the matching profile for a command */
99
+ export function matchProfile(command) {
100
+ for (const profile of getProfiles()) {
101
+ try {
102
+ if (new RegExp(profile.detect).test(command))
103
+ return profile;
104
+ }
105
+ catch { }
106
+ }
107
+ return null;
108
+ }
109
+ /** Format profile hints for injection into AI prompt */
110
+ export function formatProfileHints(command) {
111
+ const profile = matchProfile(command);
112
+ if (!profile)
113
+ return "";
114
+ const lines = [`TOOL PROFILE (${profile.name}):`];
115
+ if (profile.hints.compress)
116
+ lines.push(` Compression: ${profile.hints.compress}`);
117
+ if (profile.hints.errors)
118
+ lines.push(` Errors: ${profile.hints.errors}`);
119
+ if (profile.hints.success)
120
+ lines.push(` Success: ${profile.hints.success}`);
121
+ return lines.join("\n");
122
+ }
package/dist/tree.js ADDED
@@ -0,0 +1,94 @@
1
+ // Tree compression — convert flat file paths to compact tree representation
2
+ import { readdirSync, statSync } from "fs";
3
+ import { join, basename } from "path";
4
+ import { DEFAULT_EXCLUDE_DIRS } from "./search/filters.js";
5
+ /** Build a tree from a directory */
6
+ export function buildTree(dirPath, options = {}) {
7
+ const { maxDepth = 2, includeHidden = false, depth = 0 } = options;
8
+ const name = basename(dirPath) || dirPath;
9
+ const node = { name, type: "dir", children: [], fileCount: 0 };
10
+ if (depth >= maxDepth) {
11
+ // Count files without listing them
12
+ try {
13
+ const entries = readdirSync(dirPath);
14
+ node.fileCount = entries.length;
15
+ node.children = undefined; // don't expand
16
+ }
17
+ catch {
18
+ node.fileCount = 0;
19
+ }
20
+ return node;
21
+ }
22
+ try {
23
+ const entries = readdirSync(dirPath);
24
+ for (const entry of entries) {
25
+ if (!includeHidden && entry.startsWith("."))
26
+ continue;
27
+ if (DEFAULT_EXCLUDE_DIRS.includes(entry)) {
28
+ // Show as collapsed with count
29
+ try {
30
+ const subPath = join(dirPath, entry);
31
+ const subStat = statSync(subPath);
32
+ if (subStat.isDirectory()) {
33
+ node.children.push({ name: entry, type: "dir", fileCount: -1 }); // -1 = hidden
34
+ continue;
35
+ }
36
+ }
37
+ catch {
38
+ continue;
39
+ }
40
+ }
41
+ const fullPath = join(dirPath, entry);
42
+ try {
43
+ const stat = statSync(fullPath);
44
+ if (stat.isDirectory()) {
45
+ node.children.push(buildTree(fullPath, { maxDepth, includeHidden, depth: depth + 1 }));
46
+ }
47
+ else {
48
+ node.children.push({ name: entry, type: "file", size: stat.size });
49
+ node.fileCount++;
50
+ }
51
+ }
52
+ catch {
53
+ continue;
54
+ }
55
+ }
56
+ }
57
+ catch { }
58
+ return node;
59
+ }
60
+ /** Render tree as compact string (for agents — minimum tokens) */
61
+ export function compactTree(node, indent = 0) {
62
+ const pad = " ".repeat(indent);
63
+ if (node.type === "file")
64
+ return `${pad}${node.name}`;
65
+ if (node.fileCount === -1)
66
+ return `${pad}${node.name}/ (hidden)`;
67
+ if (!node.children || node.children.length === 0)
68
+ return `${pad}${node.name}/ (empty)`;
69
+ if (!node.children.some(c => c.children)) {
70
+ // Leaf directory — compact single line
71
+ const files = node.children.filter(c => c.type === "file").map(c => c.name);
72
+ const dirs = node.children.filter(c => c.type === "dir");
73
+ const parts = [];
74
+ if (files.length <= 5) {
75
+ parts.push(...files);
76
+ }
77
+ else {
78
+ parts.push(`${files.length} files`);
79
+ }
80
+ for (const d of dirs) {
81
+ parts.push(`${d.name}/${d.fileCount != null ? ` (${d.fileCount === -1 ? "hidden" : d.fileCount + " files"})` : ""}`);
82
+ }
83
+ return `${pad}${node.name}/ [${parts.join(", ")}]`;
84
+ }
85
+ const lines = [`${pad}${node.name}/`];
86
+ for (const child of node.children) {
87
+ lines.push(compactTree(child, indent + 1));
88
+ }
89
+ return lines.join("\n");
90
+ }
91
+ /** Render tree as JSON (for MCP) */
92
+ export function treeToJson(node) {
93
+ return node;
94
+ }
@@ -0,0 +1,65 @@
1
+ // Usage learning cache — zero-cost repeated queries
2
+ // After 3 identical prompt→command mappings, cache locally
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
4
+ import { homedir } from "os";
5
+ import { join } from "path";
6
+ import { createHash } from "crypto";
7
+ const DIR = join(homedir(), ".terminal");
8
+ const CACHE_FILE = join(DIR, "learned.json");
9
+ function ensureDir() {
10
+ if (!existsSync(DIR))
11
+ mkdirSync(DIR, { recursive: true });
12
+ }
13
+ function hash(s) {
14
+ return createHash("md5").update(s).digest("hex").slice(0, 12);
15
+ }
16
+ function cacheKey(prompt) {
17
+ const projectHash = hash(process.cwd());
18
+ const promptHash = hash(prompt.toLowerCase().trim());
19
+ return `${projectHash}:${promptHash}`;
20
+ }
21
+ function loadCache() {
22
+ ensureDir();
23
+ if (!existsSync(CACHE_FILE))
24
+ return {};
25
+ try {
26
+ return JSON.parse(readFileSync(CACHE_FILE, "utf8"));
27
+ }
28
+ catch {
29
+ return {};
30
+ }
31
+ }
32
+ function saveCache(cache) {
33
+ ensureDir();
34
+ writeFileSync(CACHE_FILE, JSON.stringify(cache));
35
+ }
36
+ /** Check if we have a learned command for this prompt (3+ identical mappings) */
37
+ export function getLearned(prompt) {
38
+ const key = cacheKey(prompt);
39
+ const cache = loadCache();
40
+ const entry = cache[key];
41
+ if (entry && entry.count >= 3)
42
+ return entry.command;
43
+ return null;
44
+ }
45
+ /** Record a prompt→command mapping */
46
+ export function recordMapping(prompt, command) {
47
+ const key = cacheKey(prompt);
48
+ const cache = loadCache();
49
+ const existing = cache[key];
50
+ if (existing && existing.command === command) {
51
+ existing.count++;
52
+ existing.lastUsed = Date.now();
53
+ }
54
+ else {
55
+ cache[key] = { command, count: 1, lastUsed: Date.now() };
56
+ }
57
+ saveCache(cache);
58
+ }
59
+ /** Get cache stats */
60
+ export function learnedStats() {
61
+ const cache = loadCache();
62
+ const entries = Object.keys(cache).length;
63
+ const cached = Object.values(cache).filter(e => e.count >= 3).length;
64
+ return { entries, cached };
65
+ }
package/package.json CHANGED
@@ -1,8 +1,15 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "description": "Smart terminal wrapper for AI agents and humans — structured output, token compression, MCP server, natural language",
5
5
  "type": "module",
6
+ "files": [
7
+ "dist/**",
8
+ "src/**",
9
+ "README.md",
10
+ "LICENSE",
11
+ "CHANGELOG.md"
12
+ ],
6
13
  "bin": {
7
14
  "t": "dist/cli.js",
8
15
  "terminal": "dist/cli.js"
package/src/ai.ts CHANGED
@@ -216,6 +216,14 @@ RULES:
216
216
  - For conceptual questions about what code does: use cat on the relevant file, the AI summary will explain it.
217
217
  - For DESTRUCTIVE requests (delete, remove, install, push): output BLOCKED: <reason>. NEVER try to execute destructive commands.
218
218
 
219
+ AST-POWERED QUERIES: For code STRUCTURE questions, use the built-in AST tool instead of grep:
220
+ - "find all exported functions" → terminal symbols src/ (lists all functions, classes, interfaces with line numbers)
221
+ - "show all interfaces" → terminal symbols src/ | grep interface
222
+ - "what does file X export" → terminal symbols src/file.ts
223
+ - "show me the class hierarchy" → terminal symbols src/
224
+ The "terminal symbols" command uses AST parsing (not regex) — it understands TypeScript, Python, Go, Rust code structure.
225
+ For TEXT search (TODO, string matches, imports) → use grep as normal.
226
+
219
227
  COMPOUND QUESTIONS: For questions asking multiple things, prefer ONE command that captures all info. Extract multiple answers from a single output.
220
228
  - "how many tests and do they pass" → bun test (extract count AND pass/fail from output)
221
229
  - "what files changed and how many lines" → git log --stat -3 (shows files AND line counts)
package/src/cli.tsx CHANGED
@@ -335,15 +335,43 @@ else if (args[0] === "repo") {
335
335
  else if (args[0] === "symbols" && args[1]) {
336
336
  const { extractSymbolsFromFile } = await import("./search/semantic.js");
337
337
  const { resolve } = await import("path");
338
- const filePath = resolve(args[1]);
339
- const symbols = extractSymbolsFromFile(filePath);
340
- if (symbols.length === 0) { console.log("No symbols found."); }
341
- else {
342
- for (const s of symbols) {
338
+ const { statSync, readdirSync } = await import("fs");
339
+ const target = resolve(args[1]);
340
+ const filter = args[2]; // optional: grep-like filter on symbol name
341
+
342
+ // Support directories recurse and extract symbols from all source files
343
+ const files: string[] = [];
344
+ try {
345
+ if (statSync(target).isDirectory()) {
346
+ const walk = (dir: string) => {
347
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
348
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") continue;
349
+ const full = resolve(dir, entry.name);
350
+ if (entry.isDirectory()) walk(full);
351
+ else if (/\.(ts|tsx|py|go|rs)$/.test(entry.name) && !/\.(test|spec)\.\w+$/.test(entry.name)) files.push(full);
352
+ }
353
+ };
354
+ walk(target);
355
+ } else {
356
+ files.push(target);
357
+ }
358
+ } catch { files.push(target); }
359
+
360
+ let totalSymbols = 0;
361
+ for (const file of files) {
362
+ const symbols = extractSymbolsFromFile(file);
363
+ const filtered = filter ? symbols.filter(s => s.name.toLowerCase().includes(filter.toLowerCase()) || s.kind.toLowerCase().includes(filter.toLowerCase())) : symbols;
364
+ if (filtered.length === 0) continue;
365
+ const relPath = file.replace(process.cwd() + "/", "");
366
+ if (files.length > 1) console.log(`\n${relPath}:`);
367
+ for (const s of filtered) {
343
368
  const exp = s.exported ? "⬡" : "·";
344
369
  console.log(` ${exp} ${s.kind.padEnd(10)} L${String(s.line).padStart(4)} ${s.name}`);
345
370
  }
371
+ totalSymbols += filtered.length;
346
372
  }
373
+ if (totalSymbols === 0) console.log("No symbols found.");
374
+ else if (files.length > 1) console.log(`\n${totalSymbols} symbols across ${files.length} files`);
347
375
  }
348
376
 
349
377
  // ── History command ──────────────────────────────────────────────────────────
@@ -603,7 +631,7 @@ else if (args.length > 0) {
603
631
  } catch (e: any) {
604
632
  // Empty result (grep exit 1 = no matches) — not a real error
605
633
  const errStdout = e.stdout?.toString() ?? "";
606
- const errStderr = e.stderr?.toString() ?? "";
634
+ let errStderr = e.stderr?.toString() ?? "";
607
635
  if (e.status === 1 && !errStdout.trim() && !errStderr.trim()) {
608
636
  // Empty result — retry with broader scope before giving up
609
637
  if (!actualCmd.includes("#(broadened)")) {
@@ -628,26 +656,37 @@ else if (args.length > 0) {
628
656
  process.exit(0);
629
657
  }
630
658
 
631
- // Auto-retry: if command failed (exit 2+), ask AI for a simpler alternative
632
- if (e.status >= 2 && !actualCmd.includes("(retry)")) {
633
- try {
634
- const retryCmd = await translateToCommand(
635
- `${prompt} (The previous command failed with: ${errStderr.slice(0, 200)}. Try a SIMPLER approach. Use basic commands only.)`,
636
- perms, []
637
- );
638
- if (retryCmd && !isIrreversible(retryCmd) && !checkPermissions(retryCmd, perms)) {
639
- console.error(`[open-terminal] retrying: $ ${retryCmd}`);
640
- const retryResult = execSync(retryCmd + " #(retry)", { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, cwd: process.cwd() });
659
+ // 3-retry learning loop: each attempt learns from the previous failure
660
+ if (e.status >= 2) {
661
+ const retryStrategies = [
662
+ // Attempt 2: inject error context
663
+ `${prompt} (Command "${actualCmd}" failed with: ${errStderr.slice(0, 300)}. Fix this specific error. Keep the approach but correct the issue.)`,
664
+ // Attempt 3: inject corrections + force simplicity
665
+ `${prompt} (TWO commands already failed for this query. Use the ABSOLUTE SIMPLEST approach: basic grep -rn, find, wc -l, cat. No awk, no xargs, no subshells. Must work on macOS BSD.)`,
666
+ ];
667
+
668
+ for (let attempt = 0; attempt < retryStrategies.length; attempt++) {
669
+ try {
670
+ const retryCmd = await translateToCommand(retryStrategies[attempt], perms, []);
671
+ if (!retryCmd || retryCmd === actualCmd || isIrreversible(retryCmd) || checkPermissions(retryCmd, perms)) continue;
672
+
673
+ console.error(`[open-terminal] retry ${attempt + 2}/3: $ ${retryCmd}`);
674
+ const retryResult = execSync(retryCmd + ` #(retry${attempt + 2})`, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, cwd: process.cwd() });
641
675
  const retryClean = stripNoise(stripAnsi(retryResult)).cleaned;
642
676
  if (retryClean.length > 5) {
643
- // Record the correction so we learn from it
677
+ // Record correction AI learns for next time
644
678
  recordCorrection(prompt, actualCmd, errStderr.slice(0, 500), retryCmd, true);
645
679
  const processed = await processOutput(retryCmd, retryClean, prompt);
646
680
  console.log(processed.aiProcessed ? processed.summary : retryClean);
647
681
  process.exit(0);
648
682
  }
683
+ } catch (retryErr: any) {
684
+ // This attempt also failed — record it and try next strategy
685
+ const retryStderr = retryErr.stderr?.toString() ?? "";
686
+ errStderr = retryStderr; // update for next attempt's context
687
+ continue;
649
688
  }
650
- } catch { /* retry also failed, fall through */ }
689
+ }
651
690
  }
652
691
 
653
692
  // Combine stdout+stderr and try AI answer framing (for audit/lint/test commands)
@@ -5,6 +5,7 @@ import { getProvider } from "./providers/index.js";
5
5
  import { estimateTokens } from "./parsers/index.js";
6
6
  import { recordSaving } from "./economy.js";
7
7
  import { discoverOutputHints } from "./context-hints.js";
8
+ import { formatProfileHints } from "./tool-profiles.js";
8
9
 
9
10
  export interface ProcessedOutput {
10
11
  /** AI-generated summary (concise, structured) */
@@ -86,9 +87,13 @@ export async function processOutput(
86
87
  ? `\n\nOUTPUT OBSERVATIONS:\n${outputHints.join("\n")}`
87
88
  : "";
88
89
 
90
+ // Inject tool-specific profile hints
91
+ const profileBlock = formatProfileHints(command);
92
+ const profileHints = profileBlock ? `\n\n${profileBlock}` : "";
93
+
89
94
  const provider = getProvider();
90
95
  const summary = await provider.complete(
91
- `${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}${hintsBlock}`,
96
+ `${originalPrompt ? `User asked: ${originalPrompt}\n` : ""}Command: ${command}\nOutput (${lines.length} lines):\n${toSummarize}${hintsBlock}${profileHints}`,
92
97
  {
93
98
  system: SUMMARIZE_PROMPT,
94
99
  maxTokens: 300,