terminal-ai-helper 1.3.4 → 1.3.6

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.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 1.3.2
1
+ 1.3.6
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env bash
2
2
  # npm's postinstall hook: a global install of the published package finishes
3
- # the job itself, so `npm i -g terminal-ai-helper` is the whole install — the
4
- # same steps, the same welcome, the same report as the other two doors.
3
+ # the job itself, so `npm i -g terminal-ai-helper --allow-scripts=terminal-ai-helper`
4
+ # is the whole install — the same steps, the same welcome, the same report
5
+ # as the other two doors.
5
6
  #
6
7
  # Two guards stand before the installer:
7
8
  # * only a *global* install reaches it. npm hands every script the config as
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-ai-helper",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "description": "The terminal that knows your next command Bash/Zsh suggestions that learn from your own history Offline and private with no account required",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -514,6 +514,15 @@ typeset -gi _TAI_LOOSE_MAX_LINE=200
514
514
  # the same visible answer for exact text, 85ms rather than 34s on a 6KB token.
515
515
  # It is matched, not dropped — see the note where `rxs` is built.
516
516
  typeset -gi _TAI_LOOSE_MAX_WORD=32
517
+ # The most lines one tier's loop will regex-test for one word. The filters
518
+ # before each loop are necessary conditions, so what they hand over is bounded
519
+ # only by arithmetic: a word whose letters sit in order across a long history
520
+ # leaves the loop hundreds of candidates and every one of them costs a regex.
521
+ # Measured on a 7,600-line index, one such keystroke cost 1.2s. Past the cap
522
+ # the loop stops testing — the tier answers _TAI_LOOSE_CAP lines either way,
523
+ # and lines the cap cuts were offered to it by coincidence of letters, not by
524
+ # anything the user remembered.
525
+ typeset -gi _TAI_LOOSE_FLOOD=400
517
526
 
518
527
  # The in-order check for one word, as a regex: its letters joined by a bounded
519
528
  # gap, each escaped so a word holding `-`, `/` or `[` is letters and not syntax.
@@ -642,23 +651,43 @@ _tai_loose() {
642
651
  # A pasted command is not a half-remembered one. Checked before the cache key
643
652
  # and before any line is read, so the answer is nothing and the cost is one
644
653
  # comparison.
654
+ local mkey="$prefix|$_TAI_LOOSE_GEN" mem_slot
645
655
  if (( ${#prefix} > _TAI_LOOSE_MAX_LINE )); then
646
- _TAI_LOOSE_LINES=(); _TAI_LOOSE_CACHED=()
647
- _TAI_LOOSE_FOR="$prefix|$_TAI_LOOSE_GEN"
648
- _TAI_LOOSE_PAR=""; _TAI_LOOSE_PAR_IDX=()
656
+ # The answer is nothing, and it is memoised like any other: redraws of the
657
+ # same pasted line ask again, and each of those used to pay the guard's
658
+ # cost of a comparison and then rebuild the empty answer's state.
659
+ _TAI_LOOSE_LINES=()
660
+ _TAI_LOOSE_PAR=""; _TAI_LOOSE_PAR_LOW=()
661
+ _TAI_MEM_PUT_KEY="$mkey"; _TAI_MEM_PUT_VAL=""; _TAI_MEM_PUT_LOW=""
662
+ _tai_loose_memo_put
649
663
  return
650
664
  fi
651
- # The same question twice has the same answer: ZLE redraws without the buffer
652
- # changing when the cursor moves, the window is resized or a menu is painted,
653
- # and each of those used to pay for a full pass over every learned line. The
654
- # answer is *restored*, not just skipped — `_tai_query` clears
655
- # `_TAI_LOOSE_LINES` before it calls, so returning early would hand back an
656
- # empty list and the glimpse would vanish on the second redraw. The generation
657
- # is part of the key because a reloaded index is a different index with the
658
- # same prefix: _tai_load_index bumps it, so a cached answer can never speak for
665
+ # The same question twice has the same answer — and in a storm of rapid
666
+ # edits, most questions ARE repeats. The report: a word typed out and then
667
+ # taken apart letter by letter, the fastest way to edit, retraces prefixes
668
+ # the scan has already answered — and every one of them paid the full
669
+ # three-tier scan again, because the parent shortcut below only serves a
670
+ # prefix that *extends* its parent, and a deletion never does. Measured on a
671
+ # 7,600-line index: 171ms per delete key, individual keys past a second, the
672
+ # prompt visibly dead under auto-repeat. So the answers live in a bounded
673
+ # memo instead of a single slot, and a revisit is a lookup. The answer is
674
+ # *restored*, not just skipped — `_tai_query` clears `_TAI_LOOSE_LINES`
675
+ # before it calls, so returning early would hand back an empty list and the
676
+ # glimpse would vanish on the second redraw. The parent state rides along:
677
+ # a memoised prefix was fully scanned under this generation, so an extension
678
+ # of it may scan its survivors, and without the restore the first retype
679
+ # after a delete would pay a full scan for nothing. The generation is part
680
+ # of the key because a reloaded index is a different index with the same
681
+ # prefix: _tai_load_index bumps it, so a memoised answer can never speak for
659
682
  # lines that are no longer installed.
660
- if [[ "$prefix|$_TAI_LOOSE_GEN" == "${_TAI_LOOSE_FOR-}" ]]; then
661
- _TAI_LOOSE_LINES=( "${_TAI_LOOSE_CACHED[@]}" )
683
+ for (( mem_slot = 1; mem_slot <= _TAI_LOOSE_MEM_N; mem_slot++ )); do
684
+ [[ "${_TAI_LOOSE_MEM_KEYS[mem_slot]}" == "$mkey" ]] && break
685
+ done
686
+ if (( mem_slot <= _TAI_LOOSE_MEM_N )); then
687
+ _tai_q_split "${_TAI_LOOSE_MEM_VALS[mem_slot]}"; _TAI_LOOSE_LINES=( "${REPLY[@]}" )
688
+ _tai_q_split "${_TAI_LOOSE_MEM_LOWS[mem_slot]}"; _TAI_LOOSE_PAR_LOW=( "${REPLY[@]}" )
689
+ _TAI_LOOSE_PAR="$prefix"
690
+ _TAI_LOOSE_PAR_GEN=$_TAI_LOOSE_GEN
662
691
  return
663
692
  fi
664
693
  for w in "${(@s: :)prefix}"; do
@@ -798,7 +827,17 @@ _tai_loose() {
798
827
  (( ${#cand_l[@]} )) || break
799
828
  cand_l=( "${(@M)cand_l:#*${~rxs_g[k]}*}" )
800
829
  done
830
+ # The flood bound. A word the history holds near-spelled can leave hundreds
831
+ # of candidates holding a one-edit spelling, and the loop below regex-tests
832
+ # them one by one — the answers are capped at _TAI_LOOSE_CAP anyway, so
833
+ # once the tier is full the tests buy nothing. The input cap bounds the
834
+ # same loop from the other side: it can only ever bite where the filter
835
+ # left hundreds of lines, which is the arithmetic-coincidence case this
836
+ # tier exists to be cautious about, not a memory it would have kept.
837
+ (( ${#cand_l[@]} > _TAI_LOOSE_FLOOD )) && \
838
+ cand_l=( "${cand_l[@]:0:_TAI_LOOSE_FLOOD}" )
801
839
  for ll in "${cand_l[@]}"; do
840
+ (( ${#var_lw[@]} >= _TAI_LOOSE_CAP )) && break
802
841
  ok=1
803
842
  for (( k = 1; k <= ${#want}; k++ )); do
804
843
  w=$want[k]; rxb=$rxs_b[k]
@@ -828,8 +867,8 @@ _tai_loose() {
828
867
  # matcher implies: a line the gap matcher accepts holds every letter of
829
868
  # every word in order, so the letters-in-order glob per word is one
830
869
  # C-level filter that hands the loop below the lines that can possibly
831
- # match. Measured, the loop over the whole history was the rest of a dead
832
- # word's cost; over the survivors it is the tail of a millisecond.
870
+ # match. The loop's cost is its survivors' count — bounded below, once
871
+ # the flood cap was measured into it.
833
872
  local -a gap_l
834
873
  gap_l=( "${rest3[@]}" )
835
874
  for (( k = 1; k <= ${#want}; k++ )); do
@@ -837,7 +876,18 @@ _tai_loose() {
837
876
  _tai_word_order_glob "${want[k]}"
838
877
  gap_l=( "${(@M)gap_l:#*${~_TAI_RE_OUT}*}" )
839
878
  done
879
+ # The same flood bound the one-edit tier got, and for the same measured
880
+ # reason: a word whose letters sit in order across hundreds of long lines
881
+ # (`opencode-s` against a history of `opencode serve --host …`) left the
882
+ # loop below regex-testing all of them — 1.2s for one keystroke on a
883
+ # 7,600-line index. The tier answers at most _TAI_LOOSE_CAP lines, so once
884
+ # it is full the tests buy nothing; and the input cap only bites where
885
+ # hundreds of lines already passed the letters-in-order glob, which is
886
+ # arithmetic offering itself as a memory, not one.
887
+ (( ${#gap_l[@]} > _TAI_LOOSE_FLOOD )) && \
888
+ gap_l=( "${gap_l[@]:0:_TAI_LOOSE_FLOOD}" )
840
889
  for ll in "${gap_l[@]}"; do
890
+ (( ${#fuzz_lw[@]} >= _TAI_LOOSE_CAP )) && break
841
891
  [[ -z "${_TAI_LOOSE_CLAIMED[$ll]:-}" ]] || continue
842
892
  ok=1
843
893
  for (( k = 1; k <= ${#want}; k++ )); do
@@ -886,11 +936,15 @@ _tai_loose() {
886
936
  local c
887
937
  _TAI_LOOSE_LINES=()
888
938
  for c in "${ranked[@]}"; do _tai_clean "$c" && _TAI_LOOSE_LINES+=( "$c" ); done
889
- _TAI_LOOSE_CACHED=( "${_TAI_LOOSE_LINES[@]}" )
890
- _TAI_LOOSE_FOR="$prefix|$_TAI_LOOSE_GEN"
891
939
  _TAI_LOOSE_PAR="$prefix"
892
940
  _TAI_LOOSE_PAR_GEN="$_TAI_LOOSE_GEN"
893
941
  _TAI_LOOSE_PAR_LOW=( "${hits_lw[@]}" )
942
+ # Into the ring: the answer and the scan set an extension of this prefix
943
+ # may walk, under the key the lookup above reads.
944
+ _TAI_MEM_PUT_KEY="$mkey"
945
+ _TAI_MEM_PUT_VAL="${(pj:\n:)_TAI_LOOSE_LINES}"
946
+ _TAI_MEM_PUT_LOW="${(pj:\n:)_TAI_LOOSE_PAR_LOW}"
947
+ _tai_loose_memo_put
894
948
  }
895
949
 
896
950
  # One line into the bounded, best-first list _TAI_RANKED, by _TAI_SCORE. The
@@ -930,12 +984,55 @@ _tai_ranked_insert() {
930
984
  (( ${#_TAI_RANKED} > $_TAI_LOOSE_CAP )) && \
931
985
  _TAI_RANKED=( "${_TAI_RANKED[@]:0:$_TAI_LOOSE_CAP}" )
932
986
  }
933
- # The generation the cache above is keyed on, and the key itself. A reload
934
- # bumps the generation because the answer for a prefix is an answer about the
935
- # lines that were installed when it was asked.
987
+ # The generation the memo is keyed on. A reload bumps the generation because
988
+ # the answer for a prefix is an answer about the lines that were installed
989
+ # when it was asked.
936
990
  typeset -gi _TAI_LOOSE_GEN=0
937
- typeset -g _TAI_LOOSE_FOR=""
938
- typeset -ga _TAI_LOOSE_CACHED=()
991
+ # The answers the scan has given, as a ring: three parallel arrays, one slot
992
+ # per remembered prefix, oldest overwritten first. One keystroke used to be
993
+ # one remembered answer; a delete run used to be none — the report that
994
+ # brought the memo in is at the lookup that reads it. Bounded, because a
995
+ # session edits a handful of words and the memo holds the neighbourhood of
996
+ # the current one: the paste's redraws, the delete run back down the word,
997
+ # the retype. The key carries its own generation, so an answer that outlives
998
+ # its index can never be served — it only waits to be overwritten.
999
+ #
1000
+ # Parallel arrays, and not an associative array, on purpose: the key is the
1001
+ # user's own text, and an assoc key comes back out through pattern and
1002
+ # subscript syntax on every removal — `foo]bar`, `pull --qqzz|0` (the pipe is
1003
+ # glob alternation, and `${(b)}` does not quote it) — where an array slot
1004
+ # holds the same text as plain data, compared with `==` and never parsed.
1005
+ typeset -ga _TAI_LOOSE_MEM_KEYS=()
1006
+ typeset -ga _TAI_LOOSE_MEM_VALS=()
1007
+ typeset -ga _TAI_LOOSE_MEM_LOWS=()
1008
+ typeset -gi _TAI_LOOSE_MEM_N=0
1009
+ typeset -gi _TAI_LOOSE_MEM_I=0
1010
+ typeset -gi _TAI_LOOSE_MEM_MAX=16
1011
+ # The three values a `_tai_loose_memo_put` call writes, set by the caller:
1012
+ # the answer, the scan set an extension of the prefix may walk, and the key
1013
+ # both are filed under.
1014
+ typeset -g _TAI_MEM_PUT_KEY=""
1015
+ typeset -g _TAI_MEM_PUT_VAL=""
1016
+ typeset -g _TAI_MEM_PUT_LOW=""
1017
+
1018
+ # One answer into the ring. Full: the cursor walks the oldest slot over.
1019
+ # An integer slot and plain assignment, never an `unset` through a subscript
1020
+ # built from the user's own text — see the ring note above.
1021
+ _tai_loose_memo_put() {
1022
+ # A cap that moved below the ring's count is honoured on the next put: the
1023
+ # live region is slots 1..N, and the lookup scans only those, so clamping N
1024
+ # is the whole change. Slots beyond it hold nothing the memo can serve.
1025
+ (( _TAI_LOOSE_MEM_N > _TAI_LOOSE_MEM_MAX )) && _TAI_LOOSE_MEM_N=_TAI_LOOSE_MEM_MAX
1026
+ if (( _TAI_LOOSE_MEM_N < _TAI_LOOSE_MEM_MAX )); then
1027
+ _TAI_LOOSE_MEM_I=$((_TAI_LOOSE_MEM_N + 1))
1028
+ _TAI_LOOSE_MEM_N=$((_TAI_LOOSE_MEM_N + 1))
1029
+ else
1030
+ _TAI_LOOSE_MEM_I=$((_TAI_LOOSE_MEM_I % _TAI_LOOSE_MEM_MAX + 1))
1031
+ fi
1032
+ _TAI_LOOSE_MEM_KEYS[_TAI_LOOSE_MEM_I]="$_TAI_MEM_PUT_KEY"
1033
+ _TAI_LOOSE_MEM_VALS[_TAI_LOOSE_MEM_I]="$_TAI_MEM_PUT_VAL"
1034
+ _TAI_LOOSE_MEM_LOWS[_TAI_LOOSE_MEM_I]="$_TAI_MEM_PUT_LOW"
1035
+ }
939
1036
  # The lowercase scan copy is rebuilt when the generation moves on, and not
940
1037
  # before: the first redraw after a reload pays for it.
941
1038
  typeset -gi _TAI_LOOSE_SNAP=-1
package/readme.md CHANGED
@@ -21,30 +21,23 @@ answered by the habit it shadows](assets/demo.gif)
21
21
 
22
22
  [Docs](docs/architecture.md) · [Development](docs/development.md) · [Docs site](https://mlibre.github.io/Terminal-AI-Helper/) · [فارسی](readme-fa.md)
23
23
 
24
- ## Install Or Update
24
+ ## Install or Update
25
25
 
26
26
  ```sh
27
27
  curl -fsSL https://raw.githubusercontent.com/mlibre/Terminal-AI-Helper/main/install.sh | bash
28
- # Or using npm
29
- npm i -g terminal-ai-helper@latest
28
+ exec zsh
30
29
  ```
31
30
 
32
- ## Commands
31
+ Or using npm
33
32
 
34
33
  ```sh
35
- tai update # newest version — GitHub for a checkout, npm for an npm install (alias: tai upgrade)
36
- tai refresh # import new history rows, then rebuild the indexes
37
- tai discover # learn --help from installed tools (cached, ~5-35s)
38
- tai doctor # what is indexed, and what is held back
39
- tai bench # build time, latency, memory, each shell's index read time
40
- tai web # a read-only localhost dashboard (alias: tai dashboard)
41
- tai eval # candidate coverage against your own history
42
- tai version # print the release this install is running
43
- tai purge # drop unusable rows, then rebuild
44
- tai forget git sta # drop every stored row for a command, exactly as typed
45
- tai uninstall # remove everything tai added
34
+ npm i -g terminal-ai-helper@latest --allow-scripts=terminal-ai-helper
35
+ exec zsh
46
36
  ```
47
37
 
38
+ The flag is npm's: newer npm blocks install scripts by default, and without
39
+ it the package unpacks and nothing is wired — no `tai` command, no rc lines.
40
+
48
41
  The line downloads TAI to `~/.tai` and runs the installer from there; run it
49
42
  again later and it updates in place. It enables both shells, imports your
50
43
  history, and builds the first index:
@@ -66,6 +59,22 @@ history, and builds the first index:
66
59
  That last line matters: a terminal that is already open keeps the old
67
60
  version until you open a new one — same after `tai update`.
68
61
 
62
+ ## Commands
63
+
64
+ ```sh
65
+ tai update # newest version — GitHub for a checkout, npm for an npm install (alias: tai upgrade)
66
+ tai refresh # import new history rows, then rebuild the indexes
67
+ tai discover # learn --help from installed tools (cached, ~5-35s)
68
+ tai doctor # what is indexed, and what is held back
69
+ tai bench # build time, latency, memory, each shell's index read time
70
+ tai web # a read-only localhost dashboard (alias: tai dashboard)
71
+ tai eval # candidate coverage against your own history
72
+ tai version # print the release this install is running
73
+ tai purge # drop unusable rows, then rebuild
74
+ tai forget git sta # drop every stored row for a command, exactly as typed
75
+ tai uninstall # remove everything tai added
76
+ ```
77
+
69
78
  ## Your first minute
70
79
 
71
80
  1. **Type a command you run often.** A grey hint shows the rest; `→` or
@@ -103,8 +112,7 @@ a list below the line and has no menu key: `Tab` completes directly — after
103
112
  ## What it suggests, and what it will not
104
113
 
105
114
  One loop makes all of it go. The shell appends each command to a local spool
106
- file and it lands in a local SQLite database seconds later — secrets,
107
- multi-line pastes and one-key accidents never become vocabulary. The first
115
+ file and it lands in a local SQLite database seconds later. The first
108
116
  time you run an unknown tool, its real `--help` is read in the background, so
109
117
  `tool <Tab>` knows its subcommands and flags from then on. `tai refresh`
110
118
  turns the database into two index files ranked your way — how often, how
@@ -143,7 +151,7 @@ memory, an array hit in microseconds.
143
151
  ## A dashboard of what it learned
144
152
 
145
153
  ```sh
146
- tai web # or: tai dashboard → http://127.0.0.1:8247/
154
+ tai web # → http://127.0.0.1:8247/
147
155
  ```
148
156
 
149
157
  ![the tai web dashboard — the try box answering `git`, the state of the
@@ -190,20 +198,11 @@ A ready-to-install `tai_<version>_all.deb` is attached to every GitHub
190
198
  release; it puts `tai` on `PATH` and prints the two `source` lines for your
191
199
  rc file.
192
200
 
193
- ```sh
194
- tai update # newest version — GitHub for a checkout, npm for an npm install; alias: tai upgrade
195
- ```
196
-
197
- It stops rather than overwrite a copy you have edited yourself.
198
-
199
- ```sh
200
- tai uninstall
201
- ```
202
-
203
- Removes the shell integration, the wrapper, the indexes, the SQLite history
204
- and the cached knowledge. The `~/.tai` folder stays — on an npm install,
205
- `npm rm -g terminal-ai-helper` removes the package itself. Restart the
206
- terminal afterwards.
201
+ `tai update` stops rather than overwrite a copy you have edited yourself.
202
+ `tai uninstall` removes the shell integration, the wrapper, the indexes, the
203
+ SQLite history and the cached knowledge. The `~/.tai` folder stays — on an
204
+ npm install, `npm rm -g terminal-ai-helper` removes the package itself.
205
+ Restart the terminal afterwards.
207
206
 
208
207
  ## Why it stays fast
209
208
 
package/tai/cli.py CHANGED
@@ -210,6 +210,9 @@ def _update_npm(repo: "Path") -> int:
210
210
  postinstall hook finishes the reinstall from the new tree unless scripts
211
211
  were skipped (--ignore-scripts) or it failed, in which case the installer
212
212
  runs here instead: idempotent, and the same report as every other door.
213
+ The allow-scripts flag rides along because npm's newer vintages block
214
+ install scripts by default — the same gate the readme's install line
215
+ opens — and a blocked hook would leave a replaced tree nobody wired.
213
216
  """
214
217
  import shutil
215
218
  import subprocess
@@ -217,10 +220,11 @@ def _update_npm(repo: "Path") -> int:
217
220
  npm = shutil.which("npm")
218
221
  if not npm:
219
222
  print("tai: npm is not installed, so there is nothing to update from")
220
- print(" reinstall with: npm i -g terminal-ai-helper@latest")
223
+ print(" reinstall with: npm i -g terminal-ai-helper@latest --allow-scripts=terminal-ai-helper")
221
224
  return 1
222
225
  before = _read_version()
223
- pulled = subprocess.run([npm, "install", "-g", "terminal-ai-helper@latest"])
226
+ pulled = subprocess.run([npm, "install", "-g", "terminal-ai-helper@latest",
227
+ "--allow-scripts=terminal-ai-helper"])
224
228
  if pulled.returncode != 0:
225
229
  print("tai: npm install failed — the previous version is still installed")
226
230
  return 1