@hasna/terminal 2.3.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/dist/cli.js +64 -16
  2. package/package.json +1 -1
  3. package/src/ai.ts +8 -0
  4. package/src/cli.tsx +57 -18
  5. package/src/output-processor.ts +6 -1
  6. package/src/output-store.ts +58 -12
  7. package/src/tool-profiles.ts +139 -0
  8. package/temp/rtk/.claude/agents/code-reviewer.md +0 -221
  9. package/temp/rtk/.claude/agents/debugger.md +0 -519
  10. package/temp/rtk/.claude/agents/rtk-testing-specialist.md +0 -461
  11. package/temp/rtk/.claude/agents/rust-rtk.md +0 -511
  12. package/temp/rtk/.claude/agents/technical-writer.md +0 -355
  13. package/temp/rtk/.claude/commands/diagnose.md +0 -352
  14. package/temp/rtk/.claude/commands/test-routing.md +0 -362
  15. package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +0 -16
  16. package/temp/rtk/.claude/hooks/rtk-rewrite.sh +0 -70
  17. package/temp/rtk/.claude/hooks/rtk-suggest.sh +0 -152
  18. package/temp/rtk/.claude/rules/cli-testing.md +0 -526
  19. package/temp/rtk/.claude/skills/issue-triage/SKILL.md +0 -348
  20. package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +0 -134
  21. package/temp/rtk/.claude/skills/performance.md +0 -435
  22. package/temp/rtk/.claude/skills/pr-triage/SKILL.md +0 -315
  23. package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +0 -71
  24. package/temp/rtk/.claude/skills/repo-recap.md +0 -206
  25. package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +0 -78
  26. package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +0 -124
  27. package/temp/rtk/.claude/skills/security-guardian.md +0 -503
  28. package/temp/rtk/.claude/skills/ship.md +0 -404
  29. package/temp/rtk/.github/workflows/benchmark.yml +0 -34
  30. package/temp/rtk/.github/workflows/dco-check.yaml +0 -12
  31. package/temp/rtk/.github/workflows/release-please.yml +0 -51
  32. package/temp/rtk/.github/workflows/release.yml +0 -343
  33. package/temp/rtk/.github/workflows/security-check.yml +0 -135
  34. package/temp/rtk/.github/workflows/validate-docs.yml +0 -78
  35. package/temp/rtk/.release-please-manifest.json +0 -3
  36. package/temp/rtk/ARCHITECTURE.md +0 -1491
  37. package/temp/rtk/CHANGELOG.md +0 -640
  38. package/temp/rtk/CLAUDE.md +0 -605
  39. package/temp/rtk/CONTRIBUTING.md +0 -199
  40. package/temp/rtk/Cargo.lock +0 -1668
  41. package/temp/rtk/Cargo.toml +0 -64
  42. package/temp/rtk/Formula/rtk.rb +0 -43
  43. package/temp/rtk/INSTALL.md +0 -390
  44. package/temp/rtk/LICENSE +0 -21
  45. package/temp/rtk/README.md +0 -386
  46. package/temp/rtk/README_es.md +0 -159
  47. package/temp/rtk/README_fr.md +0 -197
  48. package/temp/rtk/README_ja.md +0 -159
  49. package/temp/rtk/README_ko.md +0 -159
  50. package/temp/rtk/README_zh.md +0 -167
  51. package/temp/rtk/ROADMAP.md +0 -15
  52. package/temp/rtk/SECURITY.md +0 -217
  53. package/temp/rtk/TEST_EXEC_TIME.md +0 -102
  54. package/temp/rtk/build.rs +0 -57
  55. package/temp/rtk/docs/AUDIT_GUIDE.md +0 -432
  56. package/temp/rtk/docs/FEATURES.md +0 -1410
  57. package/temp/rtk/docs/TROUBLESHOOTING.md +0 -309
  58. package/temp/rtk/docs/filter-workflow.md +0 -102
  59. package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
  60. package/temp/rtk/docs/tracking.md +0 -583
  61. package/temp/rtk/hooks/opencode-rtk.ts +0 -39
  62. package/temp/rtk/hooks/rtk-awareness.md +0 -29
  63. package/temp/rtk/hooks/rtk-rewrite.sh +0 -61
  64. package/temp/rtk/hooks/test-rtk-rewrite.sh +0 -442
  65. package/temp/rtk/install.sh +0 -124
  66. package/temp/rtk/release-please-config.json +0 -10
  67. package/temp/rtk/scripts/benchmark.sh +0 -592
  68. package/temp/rtk/scripts/check-installation.sh +0 -162
  69. package/temp/rtk/scripts/install-local.sh +0 -37
  70. package/temp/rtk/scripts/rtk-economics.sh +0 -137
  71. package/temp/rtk/scripts/test-all.sh +0 -561
  72. package/temp/rtk/scripts/test-aristote.sh +0 -227
  73. package/temp/rtk/scripts/test-tracking.sh +0 -79
  74. package/temp/rtk/scripts/update-readme-metrics.sh +0 -32
  75. package/temp/rtk/scripts/validate-docs.sh +0 -73
  76. package/temp/rtk/src/aws_cmd.rs +0 -880
  77. package/temp/rtk/src/binlog.rs +0 -1645
  78. package/temp/rtk/src/cargo_cmd.rs +0 -1727
  79. package/temp/rtk/src/cc_economics.rs +0 -1157
  80. package/temp/rtk/src/ccusage.rs +0 -340
  81. package/temp/rtk/src/config.rs +0 -187
  82. package/temp/rtk/src/container.rs +0 -855
  83. package/temp/rtk/src/curl_cmd.rs +0 -134
  84. package/temp/rtk/src/deps.rs +0 -268
  85. package/temp/rtk/src/diff_cmd.rs +0 -367
  86. package/temp/rtk/src/discover/mod.rs +0 -274
  87. package/temp/rtk/src/discover/provider.rs +0 -388
  88. package/temp/rtk/src/discover/registry.rs +0 -2022
  89. package/temp/rtk/src/discover/report.rs +0 -202
  90. package/temp/rtk/src/discover/rules.rs +0 -667
  91. package/temp/rtk/src/display_helpers.rs +0 -402
  92. package/temp/rtk/src/dotnet_cmd.rs +0 -1771
  93. package/temp/rtk/src/dotnet_format_report.rs +0 -133
  94. package/temp/rtk/src/dotnet_trx.rs +0 -593
  95. package/temp/rtk/src/env_cmd.rs +0 -204
  96. package/temp/rtk/src/filter.rs +0 -462
  97. package/temp/rtk/src/filters/README.md +0 -52
  98. package/temp/rtk/src/filters/ansible-playbook.toml +0 -34
  99. package/temp/rtk/src/filters/basedpyright.toml +0 -47
  100. package/temp/rtk/src/filters/biome.toml +0 -45
  101. package/temp/rtk/src/filters/brew-install.toml +0 -37
  102. package/temp/rtk/src/filters/composer-install.toml +0 -40
  103. package/temp/rtk/src/filters/df.toml +0 -16
  104. package/temp/rtk/src/filters/dotnet-build.toml +0 -64
  105. package/temp/rtk/src/filters/du.toml +0 -16
  106. package/temp/rtk/src/filters/fail2ban-client.toml +0 -15
  107. package/temp/rtk/src/filters/gcc.toml +0 -49
  108. package/temp/rtk/src/filters/gcloud.toml +0 -22
  109. package/temp/rtk/src/filters/hadolint.toml +0 -24
  110. package/temp/rtk/src/filters/helm.toml +0 -29
  111. package/temp/rtk/src/filters/iptables.toml +0 -27
  112. package/temp/rtk/src/filters/jj.toml +0 -28
  113. package/temp/rtk/src/filters/jq.toml +0 -24
  114. package/temp/rtk/src/filters/make.toml +0 -41
  115. package/temp/rtk/src/filters/markdownlint.toml +0 -24
  116. package/temp/rtk/src/filters/mix-compile.toml +0 -27
  117. package/temp/rtk/src/filters/mix-format.toml +0 -15
  118. package/temp/rtk/src/filters/mvn-build.toml +0 -44
  119. package/temp/rtk/src/filters/oxlint.toml +0 -43
  120. package/temp/rtk/src/filters/ping.toml +0 -63
  121. package/temp/rtk/src/filters/pio-run.toml +0 -40
  122. package/temp/rtk/src/filters/poetry-install.toml +0 -50
  123. package/temp/rtk/src/filters/pre-commit.toml +0 -35
  124. package/temp/rtk/src/filters/ps.toml +0 -16
  125. package/temp/rtk/src/filters/quarto-render.toml +0 -41
  126. package/temp/rtk/src/filters/rsync.toml +0 -48
  127. package/temp/rtk/src/filters/shellcheck.toml +0 -27
  128. package/temp/rtk/src/filters/shopify-theme.toml +0 -29
  129. package/temp/rtk/src/filters/skopeo.toml +0 -45
  130. package/temp/rtk/src/filters/sops.toml +0 -16
  131. package/temp/rtk/src/filters/ssh.toml +0 -44
  132. package/temp/rtk/src/filters/stat.toml +0 -34
  133. package/temp/rtk/src/filters/swift-build.toml +0 -41
  134. package/temp/rtk/src/filters/systemctl-status.toml +0 -33
  135. package/temp/rtk/src/filters/terraform-plan.toml +0 -35
  136. package/temp/rtk/src/filters/tofu-fmt.toml +0 -16
  137. package/temp/rtk/src/filters/tofu-init.toml +0 -38
  138. package/temp/rtk/src/filters/tofu-plan.toml +0 -35
  139. package/temp/rtk/src/filters/tofu-validate.toml +0 -17
  140. package/temp/rtk/src/filters/trunk-build.toml +0 -39
  141. package/temp/rtk/src/filters/ty.toml +0 -50
  142. package/temp/rtk/src/filters/uv-sync.toml +0 -37
  143. package/temp/rtk/src/filters/xcodebuild.toml +0 -99
  144. package/temp/rtk/src/filters/yamllint.toml +0 -25
  145. package/temp/rtk/src/find_cmd.rs +0 -598
  146. package/temp/rtk/src/format_cmd.rs +0 -386
  147. package/temp/rtk/src/gain.rs +0 -723
  148. package/temp/rtk/src/gh_cmd.rs +0 -1651
  149. package/temp/rtk/src/git.rs +0 -2012
  150. package/temp/rtk/src/go_cmd.rs +0 -592
  151. package/temp/rtk/src/golangci_cmd.rs +0 -254
  152. package/temp/rtk/src/grep_cmd.rs +0 -288
  153. package/temp/rtk/src/gt_cmd.rs +0 -810
  154. package/temp/rtk/src/hook_audit_cmd.rs +0 -283
  155. package/temp/rtk/src/hook_check.rs +0 -171
  156. package/temp/rtk/src/init.rs +0 -1859
  157. package/temp/rtk/src/integrity.rs +0 -537
  158. package/temp/rtk/src/json_cmd.rs +0 -231
  159. package/temp/rtk/src/learn/detector.rs +0 -628
  160. package/temp/rtk/src/learn/mod.rs +0 -119
  161. package/temp/rtk/src/learn/report.rs +0 -184
  162. package/temp/rtk/src/lint_cmd.rs +0 -694
  163. package/temp/rtk/src/local_llm.rs +0 -316
  164. package/temp/rtk/src/log_cmd.rs +0 -248
  165. package/temp/rtk/src/ls.rs +0 -324
  166. package/temp/rtk/src/main.rs +0 -2482
  167. package/temp/rtk/src/mypy_cmd.rs +0 -389
  168. package/temp/rtk/src/next_cmd.rs +0 -241
  169. package/temp/rtk/src/npm_cmd.rs +0 -236
  170. package/temp/rtk/src/parser/README.md +0 -267
  171. package/temp/rtk/src/parser/error.rs +0 -46
  172. package/temp/rtk/src/parser/formatter.rs +0 -336
  173. package/temp/rtk/src/parser/mod.rs +0 -311
  174. package/temp/rtk/src/parser/types.rs +0 -119
  175. package/temp/rtk/src/pip_cmd.rs +0 -302
  176. package/temp/rtk/src/playwright_cmd.rs +0 -479
  177. package/temp/rtk/src/pnpm_cmd.rs +0 -573
  178. package/temp/rtk/src/prettier_cmd.rs +0 -221
  179. package/temp/rtk/src/prisma_cmd.rs +0 -482
  180. package/temp/rtk/src/psql_cmd.rs +0 -382
  181. package/temp/rtk/src/pytest_cmd.rs +0 -384
  182. package/temp/rtk/src/read.rs +0 -217
  183. package/temp/rtk/src/rewrite_cmd.rs +0 -50
  184. package/temp/rtk/src/ruff_cmd.rs +0 -402
  185. package/temp/rtk/src/runner.rs +0 -271
  186. package/temp/rtk/src/summary.rs +0 -297
  187. package/temp/rtk/src/tee.rs +0 -405
  188. package/temp/rtk/src/telemetry.rs +0 -248
  189. package/temp/rtk/src/toml_filter.rs +0 -1655
  190. package/temp/rtk/src/tracking.rs +0 -1416
  191. package/temp/rtk/src/tree.rs +0 -209
  192. package/temp/rtk/src/tsc_cmd.rs +0 -259
  193. package/temp/rtk/src/utils.rs +0 -432
  194. package/temp/rtk/src/verify_cmd.rs +0 -47
  195. package/temp/rtk/src/vitest_cmd.rs +0 -385
  196. package/temp/rtk/src/wc_cmd.rs +0 -401
  197. package/temp/rtk/src/wget_cmd.rs +0 -260
  198. package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +0 -11
  199. package/temp/rtk/tests/fixtures/dotnet/format_changes.json +0 -31
  200. package/temp/rtk/tests/fixtures/dotnet/format_empty.json +0 -1
  201. package/temp/rtk/tests/fixtures/dotnet/format_success.json +0 -12
  202. package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +0 -18
package/dist/cli.js CHANGED
@@ -361,17 +361,52 @@ else if (args[0] === "repo") {
361
361
  else if (args[0] === "symbols" && args[1]) {
362
362
  const { extractSymbolsFromFile } = await import("./search/semantic.js");
363
363
  const { resolve } = await import("path");
364
- const filePath = resolve(args[1]);
365
- const symbols = extractSymbolsFromFile(filePath);
366
- if (symbols.length === 0) {
367
- console.log("No symbols found.");
364
+ const { statSync, readdirSync } = await import("fs");
365
+ const target = resolve(args[1]);
366
+ const filter = args[2]; // optional: grep-like filter on symbol name
367
+ // Support directories — recurse and extract symbols from all source files
368
+ const files = [];
369
+ try {
370
+ if (statSync(target).isDirectory()) {
371
+ const walk = (dir) => {
372
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
373
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist")
374
+ continue;
375
+ const full = resolve(dir, entry.name);
376
+ if (entry.isDirectory())
377
+ walk(full);
378
+ else if (/\.(ts|tsx|py|go|rs)$/.test(entry.name) && !/\.(test|spec)\.\w+$/.test(entry.name))
379
+ files.push(full);
380
+ }
381
+ };
382
+ walk(target);
383
+ }
384
+ else {
385
+ files.push(target);
386
+ }
368
387
  }
369
- else {
370
- for (const s of symbols) {
388
+ catch {
389
+ files.push(target);
390
+ }
391
+ let totalSymbols = 0;
392
+ for (const file of files) {
393
+ const symbols = extractSymbolsFromFile(file);
394
+ const filtered = filter ? symbols.filter(s => s.name.toLowerCase().includes(filter.toLowerCase()) || s.kind.toLowerCase().includes(filter.toLowerCase())) : symbols;
395
+ if (filtered.length === 0)
396
+ continue;
397
+ const relPath = file.replace(process.cwd() + "/", "");
398
+ if (files.length > 1)
399
+ console.log(`\n${relPath}:`);
400
+ for (const s of filtered) {
371
401
  const exp = s.exported ? "⬡" : "·";
372
402
  console.log(` ${exp} ${s.kind.padEnd(10)} L${String(s.line).padStart(4)} ${s.name}`);
373
403
  }
404
+ totalSymbols += filtered.length;
374
405
  }
406
+ if (totalSymbols === 0)
407
+ console.log("No symbols found.");
408
+ else if (files.length > 1)
409
+ console.log(`\n${totalSymbols} symbols across ${files.length} files`);
375
410
  }
376
411
  // ── History command ──────────────────────────────────────────────────────────
377
412
  else if (args[0] === "history") {
@@ -621,7 +656,7 @@ else if (args.length > 0) {
621
656
  catch (e) {
622
657
  // Empty result (grep exit 1 = no matches) — not a real error
623
658
  const errStdout = e.stdout?.toString() ?? "";
624
- const errStderr = e.stderr?.toString() ?? "";
659
+ let errStderr = e.stderr?.toString() ?? "";
625
660
  if (e.status === 1 && !errStdout.trim() && !errStderr.trim()) {
626
661
  // Empty result — retry with broader scope before giving up
627
662
  if (!actualCmd.includes("#(broadened)")) {
@@ -643,24 +678,37 @@ else if (args.length > 0) {
643
678
  console.log(`No results found for: ${prompt}`);
644
679
  process.exit(0);
645
680
  }
646
- // Auto-retry: if command failed (exit 2+), ask AI for a simpler alternative
647
- if (e.status >= 2 && !actualCmd.includes("(retry)")) {
648
- try {
649
- const retryCmd = await translateToCommand(`${prompt} (The previous command failed with: ${errStderr.slice(0, 200)}. Try a SIMPLER approach. Use basic commands only.)`, perms, []);
650
- if (retryCmd && !isIrreversible(retryCmd) && !checkPermissions(retryCmd, perms)) {
651
- console.error(`[open-terminal] retrying: $ ${retryCmd}`);
652
- const retryResult = execSync(retryCmd + " #(retry)", { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, cwd: process.cwd() });
681
+ // 3-retry learning loop: each attempt learns from the previous failure
682
+ if (e.status >= 2) {
683
+ const retryStrategies = [
684
+ // Attempt 2: inject error context
685
+ `${prompt} (Command "${actualCmd}" failed with: ${errStderr.slice(0, 300)}. Fix this specific error. Keep the approach but correct the issue.)`,
686
+ // Attempt 3: inject corrections + force simplicity
687
+ `${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.)`,
688
+ ];
689
+ for (let attempt = 0; attempt < retryStrategies.length; attempt++) {
690
+ try {
691
+ const retryCmd = await translateToCommand(retryStrategies[attempt], perms, []);
692
+ if (!retryCmd || retryCmd === actualCmd || isIrreversible(retryCmd) || checkPermissions(retryCmd, perms))
693
+ continue;
694
+ console.error(`[open-terminal] retry ${attempt + 2}/3: $ ${retryCmd}`);
695
+ const retryResult = execSync(retryCmd + ` #(retry${attempt + 2})`, { encoding: "utf8", maxBuffer: 10 * 1024 * 1024, cwd: process.cwd() });
653
696
  const retryClean = stripNoise(stripAnsi(retryResult)).cleaned;
654
697
  if (retryClean.length > 5) {
655
- // Record the correction so we learn from it
698
+ // Record correction AI learns for next time
656
699
  recordCorrection(prompt, actualCmd, errStderr.slice(0, 500), retryCmd, true);
657
700
  const processed = await processOutput(retryCmd, retryClean, prompt);
658
701
  console.log(processed.aiProcessed ? processed.summary : retryClean);
659
702
  process.exit(0);
660
703
  }
661
704
  }
705
+ catch (retryErr) {
706
+ // This attempt also failed — record it and try next strategy
707
+ const retryStderr = retryErr.stderr?.toString() ?? "";
708
+ errStderr = retryStderr; // update for next attempt's context
709
+ continue;
710
+ }
662
711
  }
663
- catch { /* retry also failed, fall through */ }
664
712
  }
665
713
  // Combine stdout+stderr and try AI answer framing (for audit/lint/test commands)
666
714
  const combined = errStderr && errStdout.includes(errStderr.trim()) ? errStdout : errStdout + errStderr;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/terminal",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
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
@@ -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,
@@ -1,13 +1,11 @@
1
1
  // Output store — saves full raw output to disk when AI compresses it
2
- // Agents can read the file for full detail. Rotates to cap disk usage.
2
+ // Agents can read the file for full detail. Tiered retention strategy.
3
3
 
4
4
  import { existsSync, mkdirSync, writeFileSync, readdirSync, statSync, unlinkSync } from "fs";
5
5
  import { join } from "path";
6
6
  import { createHash } from "crypto";
7
7
 
8
8
  const OUTPUTS_DIR = join(process.env.HOME ?? "~", ".terminal", "outputs");
9
- const MAX_FILES = 50;
10
- const MAX_TOTAL_SIZE = 5 * 1024 * 1024; // 5MB
11
9
 
12
10
  /** Ensure outputs directory exists */
13
11
  function ensureDir() {
@@ -19,19 +17,59 @@ function hashOutput(command: string, output: string): string {
19
17
  return createHash("md5").update(command + output.slice(0, 1000)).digest("hex").slice(0, 12);
20
18
  }
21
19
 
22
- /** Rotate old files to stay within limits */
20
+ /** Tiered retention: recent = keep all, older = keep only high-value */
23
21
  function rotate() {
24
22
  try {
23
+ const now = Date.now();
24
+ const ONE_HOUR = 60 * 60 * 1000;
25
+ const ONE_DAY = 24 * ONE_HOUR;
26
+
25
27
  const files = readdirSync(OUTPUTS_DIR)
26
- .map(f => ({ name: f, path: join(OUTPUTS_DIR, f), mtime: statSync(join(OUTPUTS_DIR, f)).mtimeMs, size: statSync(join(OUTPUTS_DIR, f)).size }))
28
+ .filter(f => f.endsWith(".txt"))
29
+ .map(f => {
30
+ const path = join(OUTPUTS_DIR, f);
31
+ const stat = statSync(path);
32
+ return { name: f, path, mtime: stat.mtimeMs, size: stat.size };
33
+ })
27
34
  .sort((a, b) => b.mtime - a.mtime); // newest first
28
35
 
29
- // Remove excess files
36
+ for (const file of files) {
37
+ const age = now - file.mtime;
38
+
39
+ // Last 1 hour: keep everything
40
+ if (age < ONE_HOUR) continue;
41
+
42
+ // Last 24 hours: keep outputs >2KB (meaningful compression)
43
+ if (age < ONE_DAY) {
44
+ if (file.size < 2000) {
45
+ try { unlinkSync(file.path); } catch {}
46
+ }
47
+ continue;
48
+ }
49
+
50
+ // Older than 24h: keep only >10KB (high-value saves)
51
+ if (file.size < 10000) {
52
+ try { unlinkSync(file.path); } catch {}
53
+ continue;
54
+ }
55
+
56
+ // Older than 7 days: remove everything
57
+ if (age > 7 * ONE_DAY) {
58
+ try { unlinkSync(file.path); } catch {}
59
+ }
60
+ }
61
+
62
+ // Hard cap: never exceed 100 files or 10MB total
63
+ const remaining = readdirSync(OUTPUTS_DIR)
64
+ .filter(f => f.endsWith(".txt"))
65
+ .map(f => ({ path: join(OUTPUTS_DIR, f), mtime: statSync(join(OUTPUTS_DIR, f)).mtimeMs, size: statSync(join(OUTPUTS_DIR, f)).size }))
66
+ .sort((a, b) => b.mtime - a.mtime);
67
+
30
68
  let totalSize = 0;
31
- for (let i = 0; i < files.length; i++) {
32
- totalSize += files[i].size;
33
- if (i >= MAX_FILES || totalSize > MAX_TOTAL_SIZE) {
34
- try { unlinkSync(files[i].path); } catch {}
69
+ for (let i = 0; i < remaining.length; i++) {
70
+ totalSize += remaining[i].size;
71
+ if (i >= 100 || totalSize > 10 * 1024 * 1024) {
72
+ try { unlinkSync(remaining[i].path); } catch {}
35
73
  }
36
74
  }
37
75
  } catch {}
@@ -45,12 +83,10 @@ export function saveOutput(command: string, rawOutput: string): string {
45
83
  const filename = `${hash}.txt`;
46
84
  const filepath = join(OUTPUTS_DIR, filename);
47
85
 
48
- // Write with command header
49
86
  const content = `$ ${command}\n${"─".repeat(60)}\n${rawOutput}`;
50
87
  writeFileSync(filepath, content, "utf8");
51
88
 
52
89
  rotate();
53
-
54
90
  return filepath;
55
91
  }
56
92
 
@@ -63,3 +99,13 @@ export function formatOutputHint(filepath: string): string {
63
99
  export function getOutputsDir(): string {
64
100
  return OUTPUTS_DIR;
65
101
  }
102
+
103
+ /** Manually purge all outputs */
104
+ export function purgeOutputs(): number {
105
+ if (!existsSync(OUTPUTS_DIR)) return 0;
106
+ let count = 0;
107
+ for (const f of readdirSync(OUTPUTS_DIR)) {
108
+ try { unlinkSync(join(OUTPUTS_DIR, f)); count++; } catch {}
109
+ }
110
+ return count;
111
+ }
@@ -0,0 +1,139 @@
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
+
5
+ import { existsSync, readFileSync, readdirSync } from "fs";
6
+ import { join } from "path";
7
+
8
+ export interface ToolProfile {
9
+ name: string;
10
+ /** Regex pattern to detect this tool in a command */
11
+ detect: string;
12
+ /** Hints injected into the AI output processor prompt */
13
+ hints: {
14
+ compress?: string; // How to compress this tool's output
15
+ errors?: string; // How to extract errors from this tool
16
+ success?: string; // What success looks like
17
+ };
18
+ /** Output handling */
19
+ output?: {
20
+ maxLines?: number; // Cap output before AI processing
21
+ preservePatterns?: string[]; // Regex patterns to always keep
22
+ stripPatterns?: string[]; // Regex patterns to always remove
23
+ };
24
+ }
25
+
26
+ const PROFILES_DIR = join(process.env.HOME ?? "~", ".terminal", "profiles");
27
+
28
+ /** Built-in profiles — sensible defaults, user can override */
29
+ const BUILTIN_PROFILES: ToolProfile[] = [
30
+ {
31
+ name: "git",
32
+ detect: "^git\\b",
33
+ hints: {
34
+ compress: "For git output: show branch, file counts, insertions/deletions summary. Collapse individual diffs to file-level stats.",
35
+ errors: "Git errors often include a suggested fix (e.g., 'did you mean X?'). Extract the suggestion.",
36
+ success: "Clean working tree, successful push/pull, merge complete.",
37
+ },
38
+ output: { preservePatterns: ["conflict", "CONFLICT", "fatal", "error", "diverged"] },
39
+ },
40
+ {
41
+ name: "test",
42
+ detect: "\\b(bun|npm|yarn|pnpm)\\s+(test|run\\s+test)|\\bpytest\\b|\\bcargo\\s+test\\b|\\bgo\\s+test\\b",
43
+ hints: {
44
+ compress: "For test output: show pass/fail counts FIRST, then list ONLY failing test names with error snippets. Skip passing tests entirely.",
45
+ errors: "Test failures have: test name, expected vs actual, stack trace. Extract all three.",
46
+ success: "All tests passing = one line: '✓ N tests pass, 0 fail'",
47
+ },
48
+ output: { preservePatterns: ["FAIL", "fail", "Error", "✗", "expected", "received"] },
49
+ },
50
+ {
51
+ name: "build",
52
+ detect: "\\b(tsc|bun\\s+run\\s+build|npm\\s+run\\s+build|cargo\\s+build|go\\s+build|make)\\b",
53
+ hints: {
54
+ compress: "For build output: if success with no errors, say '✓ Build succeeded'. If errors, list each error with file:line and message.",
55
+ errors: "Build errors have file:line:column format. Group by file.",
56
+ success: "Empty output or exit 0 = build succeeded.",
57
+ },
58
+ },
59
+ {
60
+ name: "lint",
61
+ detect: "\\b(eslint|biome|ruff|clippy|golangci-lint|prettier|tsc\\s+--noEmit)\\b",
62
+ hints: {
63
+ compress: "For lint output: group violations by rule name, show count per rule, one example per rule. Skip clean files.",
64
+ errors: "Lint violations: file:line rule-name message. Group by rule.",
65
+ },
66
+ output: { maxLines: 100 },
67
+ },
68
+ {
69
+ name: "install",
70
+ detect: "\\b(npm\\s+install|bun\\s+install|yarn|pip\\s+install|cargo\\s+build|go\\s+mod)\\b",
71
+ hints: {
72
+ compress: "For install output: show only errors and final summary (packages added/removed/updated). Strip progress bars, funding notices, deprecation warnings.",
73
+ },
74
+ output: { stripPatterns: ["npm warn", "packages are looking for funding", "run `npm fund`"] },
75
+ },
76
+ {
77
+ name: "find",
78
+ detect: "^find\\b",
79
+ hints: {
80
+ compress: "For find output: if >50 results, group by top-level directory with counts. Show first 10 results as examples.",
81
+ },
82
+ },
83
+ {
84
+ name: "docker",
85
+ detect: "\\b(docker|kubectl|helm)\\b",
86
+ hints: {
87
+ compress: "For container output: show container status, image, ports. Strip pull progress and layer hashes.",
88
+ errors: "Docker errors: extract the error message after 'Error response from daemon:'",
89
+ },
90
+ },
91
+ ];
92
+
93
+ /** Load user profiles from ~/.terminal/profiles/ */
94
+ function loadUserProfiles(): ToolProfile[] {
95
+ if (!existsSync(PROFILES_DIR)) return [];
96
+
97
+ const profiles: ToolProfile[] = [];
98
+ try {
99
+ for (const file of readdirSync(PROFILES_DIR)) {
100
+ if (!file.endsWith(".json")) continue;
101
+ try {
102
+ const content = JSON.parse(readFileSync(join(PROFILES_DIR, file), "utf8"));
103
+ if (content.name && content.detect) profiles.push(content as ToolProfile);
104
+ } catch {}
105
+ }
106
+ } catch {}
107
+ return profiles;
108
+ }
109
+
110
+ /** Get all profiles — user profiles override builtins by name */
111
+ export function getProfiles(): ToolProfile[] {
112
+ const user = loadUserProfiles();
113
+ const userNames = new Set(user.map(p => p.name));
114
+ const builtins = BUILTIN_PROFILES.filter(p => !userNames.has(p.name));
115
+ return [...user, ...builtins];
116
+ }
117
+
118
+ /** Find the matching profile for a command */
119
+ export function matchProfile(command: string): ToolProfile | null {
120
+ for (const profile of getProfiles()) {
121
+ try {
122
+ if (new RegExp(profile.detect).test(command)) return profile;
123
+ } catch {}
124
+ }
125
+ return null;
126
+ }
127
+
128
+ /** Format profile hints for injection into AI prompt */
129
+ export function formatProfileHints(command: string): string {
130
+ const profile = matchProfile(command);
131
+ if (!profile) return "";
132
+
133
+ const lines: string[] = [`TOOL PROFILE (${profile.name}):`];
134
+ if (profile.hints.compress) lines.push(` Compression: ${profile.hints.compress}`);
135
+ if (profile.hints.errors) lines.push(` Errors: ${profile.hints.errors}`);
136
+ if (profile.hints.success) lines.push(` Success: ${profile.hints.success}`);
137
+
138
+ return lines.join("\n");
139
+ }