@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
package/dist/ai.js ADDED
@@ -0,0 +1,322 @@
1
+ import { cacheGet, cacheSet } from "./cache.js";
2
+ import { getProvider } from "./providers/index.js";
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { join } from "path";
5
+ import { discoverProjectHints, discoverSafetyHints, formatHints } from "./context-hints.js";
6
+ // ── model routing ─────────────────────────────────────────────────────────────
7
+ // Config-driven model selection. Defaults per provider, user can override in ~/.terminal/config.json
8
+ const COMPLEX_SIGNALS = [
9
+ /\b(undo|revert|rollback|previous|last)\b/i,
10
+ /\b(all files?|recursively|bulk|batch)\b/i,
11
+ /\b(pipeline|chain|then|and then|after)\b/i,
12
+ /\b(if|when|unless|only if)\b/i,
13
+ /\b(go into|go to|navigate|cd into|enter)\b.*\b(and|then)\b/i,
14
+ /\b(inside|within|under)\b/i,
15
+ /[|&;]{2}/,
16
+ ];
17
+ /** Default models per provider — user can override in ~/.terminal/config.json under "models" */
18
+ const MODEL_DEFAULTS = {
19
+ cerebras: { fast: "qwen-3-235b-a22b-instruct-2507", smart: "qwen-3-235b-a22b-instruct-2507" },
20
+ groq: { fast: "openai/gpt-oss-120b", smart: "moonshotai/kimi-k2-instruct" },
21
+ xai: { fast: "grok-code-fast-1", smart: "grok-4-fast-non-reasoning" },
22
+ anthropic: { fast: "claude-haiku-4-5-20251001", smart: "claude-sonnet-4-6" },
23
+ };
24
+ /** Load user model overrides from ~/.terminal/config.json */
25
+ function loadModelOverrides() {
26
+ try {
27
+ const configPath = join(process.env.HOME ?? "~", ".terminal", "config.json");
28
+ if (existsSync(configPath)) {
29
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
30
+ return config.models ?? {};
31
+ }
32
+ }
33
+ catch { }
34
+ return {};
35
+ }
36
+ /** Model routing per provider — config-driven with defaults */
37
+ function pickModel(nl) {
38
+ const isComplex = COMPLEX_SIGNALS.some((r) => r.test(nl)) || nl.split(" ").length > 10;
39
+ const provider = getProvider();
40
+ const defaults = MODEL_DEFAULTS[provider.name] ?? MODEL_DEFAULTS.cerebras;
41
+ const overrides = loadModelOverrides()[provider.name] ?? {};
42
+ return {
43
+ fast: overrides.fast ?? defaults.fast,
44
+ smart: overrides.smart ?? defaults.smart,
45
+ pick: isComplex ? "smart" : "fast",
46
+ };
47
+ }
48
+ // ── irreversibility ───────────────────────────────────────────────────────────
49
+ const IRREVERSIBLE_PATTERNS = [
50
+ /\brm\s/, /\brmdir\b/, /\btruncate\b/, /\bdrop\s+table\b/i,
51
+ /\bdelete\s+from\b/i, /\bmv\b.*\/dev\/null/, /\becho\b.*>\s*[^>]/, /\bcat\b.*>\s*[^>]/,
52
+ /\bdd\b/, /\bmkfs\b/, /\bformat\b/, /\bshred\b/,
53
+ // Process/service killing
54
+ /\bkill\b/, /\bkillall\b/, /\bpkill\b/,
55
+ // Git push/force operations
56
+ /\bgit\s+push\b/, /\bgit\s+reset\s+--hard\b/, /\bgit\s+force\b/,
57
+ // Code modification / package installation (security risk)
58
+ /\bnpx\s+\S+/, /\bnpm\s+install\b/, /\bbun\s+add\b/, /\bpip\s+install\b/,
59
+ /\bcodemod\b/, /\bsed\s+-i\b/, /\bawk\s.*>\s*\S+\.\w+/, /\bperl\s+-[pi]\b/,
60
+ // File creation/modification (READ-ONLY terminal)
61
+ /\btouch\b/, /\bmkdir\b/, /\becho\s.*>/, /\btee\b/, /\bcp\b/, /\bmv\b/,
62
+ // Starting servers/processes (dangerous from NL)
63
+ /\b(bun|npm|pnpm|yarn)\s+run\s+dev\b/, /\b(bun|npm)\s+start\b/,
64
+ ];
65
+ // Commands that are ALWAYS safe (read-only git, etc.)
66
+ const SAFE_OVERRIDES = [
67
+ /^\s*git\s+(log|show|diff|branch|status|blame|tag|remote|stash\s+list)\b/,
68
+ /^\s*git\s+log\b/,
69
+ // find -exec with read-only tools is safe
70
+ /\bfind\b.*-exec\s+(wc|cat|head|tail|grep|stat|file|du|ls)\b/,
71
+ // find without -exec is always safe
72
+ /^\s*find\b(?!.*-exec\s+(rm|mv|chmod|chown|sed))/,
73
+ // xargs with read-only tools is safe
74
+ /\bxargs\s+(wc|cat|head|tail|grep|stat|file|du|ls|git\s+log|git\s+show|git\s+blame)\b/,
75
+ /\bxargs\s+-I\s*\S+\s+(wc|cat|head|tail|grep|stat|git)\b/,
76
+ ];
77
+ export function isIrreversible(command) {
78
+ // Safe overrides take priority
79
+ if (SAFE_OVERRIDES.some((r) => r.test(command)))
80
+ return false;
81
+ return IRREVERSIBLE_PATTERNS.some((r) => r.test(command));
82
+ }
83
+ // ── permissions ───────────────────────────────────────────────────────────────
84
+ const DESTRUCTIVE_PATTERNS = [/\brm\b/, /\brmdir\b/, /\btruncate\b/, /\bdrop\s+table\b/i, /\bdelete\s+from\b/i];
85
+ const NETWORK_PATTERNS = [/\bcurl\b/, /\bwget\b/, /\bssh\b/, /\bscp\b/, /\bping\b/, /\bnc\b/, /\bnetcat\b/];
86
+ const SUDO_PATTERNS = [/\bsudo\b/];
87
+ const INSTALL_PATTERNS = [/\bbrew\s+install\b/, /\bnpm\s+install\s+-g\b/, /\bpip\s+install\b/, /\bapt\s+install\b/, /\byum\s+install\b/];
88
+ const WRITE_OUTSIDE_PATTERNS = [/\s(\/etc|\/usr|\/var|\/opt|\/root|~\/[^.])/, />\s*\//];
89
+ export function checkPermissions(command, perms) {
90
+ if (!perms.destructive && DESTRUCTIVE_PATTERNS.some((r) => r.test(command)))
91
+ return "destructive commands are disabled";
92
+ if (!perms.network && NETWORK_PATTERNS.some((r) => r.test(command)))
93
+ return "network commands are disabled";
94
+ if (!perms.sudo && SUDO_PATTERNS.some((r) => r.test(command)))
95
+ return "sudo is disabled";
96
+ if (!perms.install && INSTALL_PATTERNS.some((r) => r.test(command)))
97
+ return "package installation is disabled";
98
+ if (!perms.write_outside_cwd && WRITE_OUTSIDE_PATTERNS.some((r) => r.test(command)))
99
+ return "writing outside cwd is disabled";
100
+ return null;
101
+ }
102
+ // ── correction memory ───────────────────────────────────────────────────────
103
+ /** Load past corrections relevant to a prompt — injected as negative examples */
104
+ function loadCorrectionHints(prompt) {
105
+ try {
106
+ // Dynamic import to avoid circular deps
107
+ const { findSimilarCorrections } = require("./sessions-db.js");
108
+ const corrections = findSimilarCorrections(prompt, 3);
109
+ if (corrections.length === 0)
110
+ return "";
111
+ const lines = corrections.map((c) => `AVOID: "${c.failed_command}" (failed: ${c.error_type}). USE: "${c.corrected_command}" instead.`);
112
+ return `\n\nLEARNED CORRECTIONS (from past failures):\n${lines.join("\n")}`;
113
+ }
114
+ catch {
115
+ return "";
116
+ }
117
+ }
118
+ // ── project context (powered by context-hints) ──────────────────────────────
119
+ function detectProjectContext() {
120
+ const hints = discoverProjectHints(process.cwd());
121
+ return hints.length > 0 ? `\n\n${formatHints(hints)}` : "";
122
+ }
123
+ // ── system prompt ─────────────────────────────────────────────────────────────
124
+ function buildSystemPrompt(perms, sessionEntries, currentPrompt) {
125
+ const restrictions = [];
126
+ if (!perms.destructive)
127
+ restrictions.push("- NEVER generate commands that delete, remove, or overwrite files/data");
128
+ if (!perms.network)
129
+ restrictions.push("- NEVER generate commands that make network requests (curl, wget, ssh, etc.)");
130
+ if (!perms.sudo)
131
+ restrictions.push("- NEVER generate commands requiring sudo");
132
+ if (!perms.write_outside_cwd)
133
+ restrictions.push("- NEVER write to paths outside the current working directory");
134
+ if (!perms.install)
135
+ restrictions.push("- NEVER install packages (brew, npm -g, pip, apt, etc.)");
136
+ const restrictionBlock = restrictions.length > 0
137
+ ? `\n\nRESTRICTIONS:\n${restrictions.join("\n")}\nIf restricted, output: BLOCKED: <reason>`
138
+ : "";
139
+ let contextBlock = "";
140
+ if (sessionEntries.length > 0) {
141
+ const lines = [];
142
+ for (const e of sessionEntries.slice(-5)) {
143
+ lines.push(`> ${e.nl}`);
144
+ lines.push(`$ ${e.cmd}`);
145
+ if (e.output)
146
+ lines.push(e.output);
147
+ if (e.error)
148
+ lines.push("(command failed)");
149
+ }
150
+ contextBlock = `\n\nSESSION HISTORY (user intent > command $ output):\n${lines.join("\n")}`;
151
+ }
152
+ const projectContext = detectProjectContext();
153
+ // Inject safety hints for the command being generated (AI sees what's risky)
154
+ const safetyBlock = sessionEntries.length > 0
155
+ ? (() => {
156
+ const lastCmd = sessionEntries[sessionEntries.length - 1]?.cmd;
157
+ if (lastCmd) {
158
+ const safetyHints = discoverSafetyHints(lastCmd);
159
+ return safetyHints.length > 0 ? `\n\nLAST COMMAND SAFETY:\n${safetyHints.join("\n")}` : "";
160
+ }
161
+ return "";
162
+ })()
163
+ : "";
164
+ return `You are a terminal assistant. Output ONLY the exact shell command — no explanation, no markdown, no backticks.
165
+ The user describes what they want in plain English. You translate to the exact shell command.
166
+
167
+ RULES:
168
+ - SIMPLICITY FIRST: Use the simplest command that works. Prefer grep | sort | head over 10-pipe chains. Complex pipelines are OK when needed, but NEVER pass file:line output to wc or xargs without cleaning it first.
169
+ - ALWAYS use grep -rn (with -r) when searching directories. NEVER use grep without -r on src/ or any directory.
170
+ - When user refers to items from previous output, use the EXACT names shown (e.g., "feature/auth" not "auth", "open-skills" not "open_skills")
171
+ - When user says "the largest/smallest/first/second", look at the previous output to identify the correct item
172
+ - When user says "them all" or "combine them", refer to items from the most recent command output
173
+ - For "show who changed each line" use git blame, for "show remote urls" use git remote -v
174
+ - For text search in code, use grep -rn, NOT nm or objdump (those are for compiled binaries)
175
+ - On macOS: for memory use vm_stat or top -l 1, for disk use df -h, for processes use ps aux
176
+ - macOS uses BSD tools, NOT GNU. Use: du -d 1 (not --max-depth), ls (not ls --color), sort -r (not sort --reverse), ps aux (not ps --sort)
177
+ - NEVER use grep -P (PCRE). macOS grep has NO -P flag. Use grep -E for extended regex, or sed/awk for complex extraction.
178
+ - NEVER invent commands that don't exist. Stick to standard Unix/macOS commands.
179
+ - NEVER install packages (npx, npm install, pip install, brew install). This is a READ-ONLY terminal.
180
+ - NEVER modify source code (sed -i, codemod, awk with redirect). Only observe, never change.
181
+ - Search src/ directory, NOT dist/ or node_modules/ for code queries.
182
+ - Use exact file paths from the project context below. Do NOT guess paths.
183
+ - For "what would break if I deleted X": use grep -rn "from.*X\\|import.*X\\|require.*X" src/ to find all importers.
184
+ - For "find where X is defined": use grep -rn "export.*function X\\|export.*class X\\|export.*const X" src/
185
+ - For "show me the code of function X": if you know the file, use grep -A 30 "function X" src/file.ts. If not, use grep -rn -A 30 "function X" src/ --include="*.ts"
186
+ - ALWAYS use grep -rn (recursive) when searching directories. NEVER use grep without -r on a directory — it will fail.
187
+ - For conceptual questions about what code does: use cat on the relevant file, the AI summary will explain it.
188
+ - For DESTRUCTIVE requests (delete, remove, install, push): output BLOCKED: <reason>. NEVER try to execute destructive commands.
189
+
190
+ AST-POWERED QUERIES: For code STRUCTURE questions, use the built-in AST tool instead of grep:
191
+ - "find all exported functions" → terminal symbols src/ (lists all functions, classes, interfaces with line numbers)
192
+ - "show all interfaces" → terminal symbols src/ | grep interface
193
+ - "what does file X export" → terminal symbols src/file.ts
194
+ - "show me the class hierarchy" → terminal symbols src/
195
+ The "terminal symbols" command uses AST parsing (not regex) — it understands TypeScript, Python, Go, Rust code structure.
196
+ For TEXT search (TODO, string matches, imports) → use grep as normal.
197
+
198
+ COMPOUND QUESTIONS: For questions asking multiple things, prefer ONE command that captures all info. Extract multiple answers from a single output.
199
+ - "how many tests and do they pass" → bun test (extract count AND pass/fail from output)
200
+ - "what files changed and how many lines" → git log --stat -3 (shows files AND line counts)
201
+ - "what version of node and bun" → node -v && bun -v (only use && for trivial non-failing commands)
202
+ NEVER split into separate test runs or expensive commands chained with &&.
203
+
204
+ BLOCKED ALTERNATIVES: If your preferred command would require installing packages (npx, npm install), ALWAYS try a READ-ONLY alternative:
205
+ - Code quality analysis → grep -rn "TODO\\|FIXME\\|HACK\\|XXX" src/
206
+ - Linting → check if "lint" or "typecheck" exists in package.json scripts, run that
207
+ - Security scan → grep -rn "eval\\|exec\\|spawn\\|password\\|secret" src/
208
+ - Dependency audit → cat package.json | grep -A 50 dependencies
209
+ - Test coverage → bun test --coverage (or npm run test:coverage if available)
210
+ NEVER give up. NEVER output BLOCKED for analysis questions. Always try a grep/find/cat/wc/awk read-only alternative.
211
+ - Cyclomatic complexity → grep -rn "if\\|else\\|for\\|while\\|switch\\|case\\|catch\\|&&\\|||" src/ --include="*.ts" | wc -l
212
+ - Unused exports → grep -rn "export function\|export const\|export class" src/ --include="*.ts" | sed 's/.*export [a-z]* //' | sed 's/[(<:].*//' | sort -u
213
+ - Dead code → for each exported name, grep -rn "name" src/ --include="*.ts" | wc -l (if only 1 match = unused)
214
+ - Dependency graph → grep -rn "from " src/ --include="*.ts" | sed 's/:.*from "/→/' | sed 's/".*//' | sort -u
215
+ - Most parameters → grep -rn "function " src/ --include="*.ts" | awk -F'[()]' '{print gsub(/,/,",",$2)+1, $0}' | sort -nr | head -10
216
+ ALWAYS try a heuristic shell approach before giving up. NEVER say BLOCKED for analysis questions.
217
+
218
+ SEMANTIC MAPPING: When the user references a concept, search the file tree for RELATED terms:
219
+ - Look at directory names: src/agent/ likely contains "agentic" code
220
+ - Look at file names: lazy-executor.ts likely handles "lazy mode"
221
+ - When uncertain: grep -rn "keyword" src/ --include="*.ts" -l (list matching files)
222
+
223
+ ACTION vs CONCEPTUAL: If the prompt starts with "run", "execute", "check", "test", "build", "show output of" — ALWAYS generate an executable command. NEVER read README for action requests. Only read docs for "explain why", "what does X mean", "how was X designed".
224
+
225
+ EXISTENCE CHECKS: If the prompt starts with "is there", "does this have", "do we have", "does X exist" — NEVER run/start/launch anything. Use ls, find, or test -d to CHECK existence. These are READ-ONLY questions.
226
+
227
+ MONOREPO: If the project context says "MONOREPO", search packages/ or apps/ NOT src/. Use: grep -rn "pattern" packages/ --include="*.ts". For specific packages, use packages/PKGNAME/src/.
228
+ cwd: ${process.cwd()}
229
+ shell: zsh / macOS${projectContext}${safetyBlock}${restrictionBlock}${contextBlock}${currentPrompt ? loadCorrectionHints(currentPrompt) : ""}`;
230
+ }
231
+ // ── streaming translate ───────────────────────────────────────────────────────
232
+ export async function translateToCommand(nl, perms, sessionEntries, onToken) {
233
+ // Only use cache when there's no session context (context makes same NL produce different commands)
234
+ if (sessionEntries.length === 0) {
235
+ const cached = cacheGet(nl);
236
+ if (cached) {
237
+ onToken?.(cached);
238
+ return cached;
239
+ }
240
+ }
241
+ const provider = getProvider();
242
+ const routing = pickModel(nl);
243
+ const model = routing.pick === "smart" ? routing.smart : routing.fast;
244
+ const system = buildSystemPrompt(perms, sessionEntries, nl);
245
+ let text;
246
+ if (onToken) {
247
+ text = await provider.stream(nl, { model, maxTokens: 256, system }, {
248
+ onToken: (partial) => onToken(partial),
249
+ });
250
+ }
251
+ else {
252
+ text = await provider.complete(nl, { model, maxTokens: 256, system });
253
+ }
254
+ if (text.startsWith("BLOCKED:"))
255
+ throw new Error(text);
256
+ // Strip AI reasoning — extract ONLY the shell command (first line)
257
+ let cleaned = text.trim();
258
+ // Remove ALL markdown code blocks and their content markers
259
+ cleaned = cleaned.replace(/```(?:bash|sh|shell)?\n?/g, "").replace(/```/g, "");
260
+ // Split into lines and find the FIRST one that looks like a SHELL COMMAND
261
+ const lines = cleaned.split("\n");
262
+ let command = "";
263
+ for (const line of lines) {
264
+ const t = line.trim();
265
+ if (!t)
266
+ continue;
267
+ // Skip lines that are clearly English prose, not commands
268
+ if (/^(Based on|I |This |The |Let me|Here|Note:|Since|Looking|To |However|BLOCKED:|If |You |We |For |It |A |An |That )/.test(t))
269
+ continue;
270
+ if (/^[A-Z][a-z].*[.;:!?,]/.test(t))
271
+ continue; // English sentence with punctuation anywhere
272
+ if (t.split(" ").length > 15 && !/[|&;><$]/.test(t))
273
+ continue; // Long line without shell operators = prose
274
+ // Must start with a plausible command character (lowercase, /, ., $, or common tool)
275
+ if (/^[a-z./$~(]/.test(t) || /^[A-Z]+[_=]/.test(t)) {
276
+ command = t;
277
+ break;
278
+ }
279
+ }
280
+ cleaned = command || lines[0]?.trim() || cleaned;
281
+ cacheSet(nl, cleaned);
282
+ return cleaned;
283
+ }
284
+ // ── prefetch ──────────────────────────────────────────────────────────────────
285
+ export function prefetchNext(lastNl, perms, sessionEntries) {
286
+ if (sessionEntries.length === 0 && cacheGet(lastNl))
287
+ return;
288
+ translateToCommand(lastNl, perms, sessionEntries).catch(() => { });
289
+ }
290
+ // ── explain ───────────────────────────────────────────────────────────────────
291
+ export async function explainCommand(command) {
292
+ const provider = getProvider();
293
+ const routing = pickModel("explain"); // simple = fast model
294
+ return provider.complete(command, {
295
+ model: routing.fast,
296
+ maxTokens: 128,
297
+ system: "Explain what this shell command does in one plain English sentence. No markdown, no code blocks.",
298
+ });
299
+ }
300
+ // ── auto-fix ──────────────────────────────────────────────────────────────────
301
+ export async function fixCommand(originalNl, failedCommand, errorOutput, perms, sessionEntries) {
302
+ const provider = getProvider();
303
+ const routing = pickModel(originalNl);
304
+ const text = await provider.complete(`I wanted to: ${originalNl}\nI ran: ${failedCommand}\nError:\n${errorOutput}\n\nGive me the corrected command only.`, {
305
+ model: routing.smart, // always use smart model for fixes
306
+ maxTokens: 256,
307
+ system: buildSystemPrompt(perms, sessionEntries, originalNl),
308
+ });
309
+ if (text.startsWith("BLOCKED:"))
310
+ throw new Error(text);
311
+ return text;
312
+ }
313
+ // ── summarize output (for MCP/agent use) ──────────────────────────────────────
314
+ export async function summarizeOutput(command, output, maxTokens = 200) {
315
+ const provider = getProvider();
316
+ const routing = pickModel("summarize");
317
+ return provider.complete(`Command: ${command}\nOutput:\n${output}\n\nSummarize this output concisely for an AI agent. Focus on: status, key results, errors. Be terse.`, {
318
+ model: routing.fast,
319
+ maxTokens,
320
+ system: "You summarize command output for AI agents. Be extremely concise. Return structured info. No prose.",
321
+ });
322
+ }
package/dist/cache.js ADDED
@@ -0,0 +1,41 @@
1
+ // In-memory LRU cache + disk persistence for command translations
2
+ import { existsSync, readFileSync, writeFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ const CACHE_FILE = join(homedir(), ".terminal", "cache.json");
6
+ const MAX_ENTRIES = 500;
7
+ let mem = {};
8
+ export function loadCache() {
9
+ if (!existsSync(CACHE_FILE))
10
+ return;
11
+ try {
12
+ mem = JSON.parse(readFileSync(CACHE_FILE, "utf8"));
13
+ }
14
+ catch { }
15
+ }
16
+ function persistCache() {
17
+ try {
18
+ writeFileSync(CACHE_FILE, JSON.stringify(mem));
19
+ }
20
+ catch { }
21
+ }
22
+ /** Normalize a natural language query for cache lookup */
23
+ export function normalizeNl(nl) {
24
+ return nl
25
+ .toLowerCase()
26
+ .trim()
27
+ .replace(/[^a-z0-9\s]/g, "") // strip punctuation
28
+ .replace(/\s+/g, " ");
29
+ }
30
+ export function cacheGet(nl) {
31
+ return mem[normalizeNl(nl)] ?? null;
32
+ }
33
+ export function cacheSet(nl, command) {
34
+ const key = normalizeNl(nl);
35
+ // evict oldest if full
36
+ const keys = Object.keys(mem);
37
+ if (keys.length >= MAX_ENTRIES)
38
+ delete mem[keys[0]];
39
+ mem[key] = command;
40
+ persistCache();
41
+ }
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;
@@ -0,0 +1,64 @@
1
+ // Command rewriter — auto-optimize commands to produce less output
2
+ // Only rewrites when semantic result is identical
3
+ const rules = [
4
+ // find | grep -v node_modules → find -not -path
5
+ {
6
+ pattern: /find\s+(\S+)\s+(.*?)\|\s*grep\s+-v\s+node_modules/,
7
+ rewrite: (m, cmd) => cmd.replace(m[0], `find ${m[1]} ${m[2]}-not -path '*/node_modules/*'`),
8
+ reason: "avoid pipe, filter in-kernel",
9
+ },
10
+ // cat file | grep X → grep X file
11
+ {
12
+ pattern: /cat\s+(\S+)\s*\|\s*grep\s+(.*)/,
13
+ rewrite: (m) => `grep ${m[2]} ${m[1]}`,
14
+ reason: "useless cat",
15
+ },
16
+ // find without node_modules exclusion → add it
17
+ {
18
+ pattern: /^find\s+\.\s+(.*)(?!.*node_modules)/,
19
+ rewrite: (m, cmd) => {
20
+ if (cmd.includes("node_modules") || cmd.includes("-not -path"))
21
+ return cmd;
22
+ return cmd.replace(/^find\s+\.\s+/, "find . -not -path '*/node_modules/*' -not -path '*/.git/*' ");
23
+ },
24
+ reason: "auto-exclude node_modules and .git",
25
+ },
26
+ // git log without limit → add --oneline -20
27
+ {
28
+ pattern: /^git\s+log\s*$/,
29
+ rewrite: () => "git log --oneline -20",
30
+ reason: "prevent unbounded log output",
31
+ },
32
+ // git diff without stat → add --stat for overview
33
+ {
34
+ pattern: /^git\s+diff\s*$/,
35
+ rewrite: () => "git diff --stat",
36
+ reason: "stat overview is usually sufficient",
37
+ },
38
+ // npm ls without depth → add --depth=0
39
+ {
40
+ pattern: /^npm\s+ls\s*$/,
41
+ rewrite: () => "npm ls --depth=0",
42
+ reason: "full tree is massive, top-level usually enough",
43
+ },
44
+ // ps aux without filter → sort by memory and head (macOS compatible)
45
+ {
46
+ pattern: /^ps\s+aux\s*$/,
47
+ rewrite: () => "ps aux | sort -k4 -rn | head -20",
48
+ reason: "full process list is noise, show top consumers",
49
+ },
50
+ ];
51
+ /** Rewrite a command to produce less output */
52
+ export function rewriteCommand(cmd) {
53
+ const trimmed = cmd.trim();
54
+ for (const rule of rules) {
55
+ const match = trimmed.match(rule.pattern);
56
+ if (match) {
57
+ const rewritten = rule.rewrite(match, trimmed);
58
+ if (rewritten !== trimmed) {
59
+ return { original: trimmed, rewritten, changed: true, reason: rule.reason };
60
+ }
61
+ }
62
+ }
63
+ return { original: trimmed, rewritten: trimmed, changed: false };
64
+ }
@@ -0,0 +1,86 @@
1
+ // Command validator — catch invalid commands BEFORE executing
2
+ // Prevents shell errors from hallucinated flags, wrong paths, bad syntax
3
+ import { existsSync } from "fs";
4
+ import { join } from "path";
5
+ /** Extract file paths referenced in a command */
6
+ function extractPaths(command) {
7
+ const paths = [];
8
+ // Match quoted paths
9
+ const quoted = command.match(/["']([^"']+\.\w+)["']/g);
10
+ if (quoted)
11
+ paths.push(...quoted.map(q => q.replace(/["']/g, "")));
12
+ // Match unquoted paths with extensions or directory separators
13
+ const tokens = command.split(/\s+/);
14
+ for (const t of tokens) {
15
+ if (t.includes("/") && !t.startsWith("-") && !t.startsWith("|") && !t.startsWith("&")) {
16
+ // Clean shell operators from end
17
+ const clean = t.replace(/[;|&>]+$/, "");
18
+ if (clean && !clean.startsWith("-"))
19
+ paths.push(clean);
20
+ }
21
+ }
22
+ return [...new Set(paths)];
23
+ }
24
+ /** Check for obviously broken shell syntax */
25
+ function checkSyntax(command) {
26
+ const issues = [];
27
+ // Unmatched quotes
28
+ const singleQuotes = (command.match(/'/g) || []).length;
29
+ const doubleQuotes = (command.match(/"/g) || []).length;
30
+ if (singleQuotes % 2 !== 0)
31
+ issues.push("unmatched single quote");
32
+ if (doubleQuotes % 2 !== 0)
33
+ issues.push("unmatched double quote");
34
+ // Unmatched parentheses
35
+ const openParens = (command.match(/\(/g) || []).length;
36
+ const closeParens = (command.match(/\)/g) || []).length;
37
+ if (openParens !== closeParens)
38
+ issues.push("unmatched parentheses");
39
+ // Empty pipe targets
40
+ if (/\|\s*$/.test(command))
41
+ issues.push("pipe with no target");
42
+ if (/^\s*\|/.test(command))
43
+ issues.push("pipe with no source");
44
+ return issues;
45
+ }
46
+ /** Validate a command before execution */
47
+ export function validateCommand(command, cwd) {
48
+ const issues = [];
49
+ // Check syntax
50
+ issues.push(...checkSyntax(command));
51
+ // Check file paths exist
52
+ const paths = extractPaths(command);
53
+ for (const p of paths) {
54
+ const fullPath = p.startsWith("/") ? p : join(cwd, p);
55
+ if (p.includes("*") || p.includes("?"))
56
+ continue; // skip globs
57
+ if (p.startsWith("-"))
58
+ continue; // skip flags
59
+ if ([".", "..", "/", "~"].includes(p))
60
+ continue; // skip special
61
+ if (!existsSync(fullPath) && !existsSync(p)) {
62
+ // Only flag source file paths, not output paths
63
+ if (/\.(ts|tsx|js|jsx|json|md|yaml|yml|py|go|rs)$/.test(p)) {
64
+ issues.push(`file not found: ${p}`);
65
+ }
66
+ }
67
+ }
68
+ // Check for common GNU flags on macOS
69
+ const gnuFlags = command.match(/--max-depth|--color=|--sort=|--field-type|--no-deps/g);
70
+ if (gnuFlags) {
71
+ issues.push(`GNU flag on macOS: ${gnuFlags.join(", ")}`);
72
+ }
73
+ // Complexity guard — extreme pipe chains are fragile
74
+ const pipeCount = (command.match(/\|/g) || []).length;
75
+ if (pipeCount > 7) {
76
+ issues.push(`too complex: ${pipeCount} pipes — simplify`);
77
+ }
78
+ // grep -P (PCRE) doesn't exist on macOS
79
+ if (/grep\s+.*-[a-zA-Z]*P/.test(command)) {
80
+ issues.push("grep -P (PCRE) not available on macOS — use grep -E");
81
+ }
82
+ return {
83
+ valid: issues.length === 0,
84
+ issues,
85
+ };
86
+ }