terminal-ai-helper 1.3.1 → 1.3.3
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/LICENSE +2 -0
- package/VERSION +1 -1
- package/bin/npm-postinstall +36 -0
- package/install.sh +7 -5
- package/package.json +4 -6
- package/plugins/bash/files.bash +0 -0
- package/plugins/bash/index.bash +0 -0
- package/plugins/bash/keys.bash +0 -0
- package/plugins/bash/lookup.bash +0 -0
- package/plugins/bash/units.bash +0 -0
- package/plugins/tai.bash +0 -0
- package/plugins/tai.zsh +0 -0
- package/plugins/zsh/files.zsh +0 -0
- package/plugins/zsh/index.zsh +0 -0
- package/plugins/zsh/lookup.zsh +0 -0
- package/plugins/zsh/menu.zsh +81 -0
- package/plugins/zsh/units.zsh +0 -0
- package/plugins/zsh/widgets.zsh +12 -1
- package/readme.md +39 -38
- package/tai/__init__.py +0 -0
- package/tai/bench.py +0 -0
- package/tai/cli.py +99 -5
- package/tai/decisions.py +0 -0
- package/tai/engcache.py +0 -0
- package/tai/engine.py +0 -0
- package/tai/evaluate.py +0 -0
- package/tai/evaluate_jev.py +0 -0
- package/tai/fresh.py +0 -0
- package/tai/helptext.py +0 -0
- package/tai/index.py +0 -0
- package/tai/jev.py +0 -0
- package/tai/knowledge.py +0 -0
- package/tai/maintenance.py +0 -0
- package/tai/paths.py +0 -0
- package/tai/predictor.py +0 -0
- package/tai/seed.py +0 -0
- package/tai/spool.py +0 -0
- package/tai/store.py +0 -0
- package/tai/tune.py +0 -0
- package/tai/typo.py +25 -11
- package/tai/web.py +0 -0
- package/bin/tai-install +0 -13
package/LICENSE
CHANGED
|
@@ -19,3 +19,5 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
Other parts like zsh/bash/vite/... have their own license
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.3.
|
|
1
|
+
1.3.2
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
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.
|
|
5
|
+
#
|
|
6
|
+
# Two guards stand before the installer:
|
|
7
|
+
# * only a *global* install reaches it. npm hands every script the config as
|
|
8
|
+
# npm_config_* environment, so npm_config_global tells a `-g` install from
|
|
9
|
+
# a dev `npm i` in a checkout of this repo — which must never edit anyone's
|
|
10
|
+
# rc files. The node_modules check is the second answer, for a package
|
|
11
|
+
# manager that speaks the same layout but not the same variable.
|
|
12
|
+
# * TAI_SKIP_POSTINSTALL=1 skips it, for scripts and containers that want
|
|
13
|
+
# the files on disk only.
|
|
14
|
+
set -u
|
|
15
|
+
here="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)"
|
|
16
|
+
|
|
17
|
+
global=""
|
|
18
|
+
case "${npm_config_global:-}" in
|
|
19
|
+
true | 1 | yes) global=1 ;;
|
|
20
|
+
esac
|
|
21
|
+
if [ -z "$global" ]; then
|
|
22
|
+
case "$here" in */node_modules/*) global=1 ;; esac
|
|
23
|
+
fi
|
|
24
|
+
[ -n "$global" ] || exit 0
|
|
25
|
+
[ "${TAI_SKIP_POSTINSTALL:-0}" = "1" ] && exit 0
|
|
26
|
+
|
|
27
|
+
if TAI_INSTALL_METHOD=npm bash "$here/install.sh"; then
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
status=$?
|
|
31
|
+
# The package itself is on disk; an installer that could not finish (no
|
|
32
|
+
# writable rc file, an odd HOME) must not fail the npm install over it — that
|
|
33
|
+
# would roll the package back and leave nothing at all. The retry is the same
|
|
34
|
+
# installer, named by its path in the package npm just unpacked.
|
|
35
|
+
echo "tai: automatic setup did not finish (exit $status) — finish with: bash \"$here/install.sh\"" >&2
|
|
36
|
+
exit 0
|
package/install.sh
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# tai installer — Linux, stdlib python only, <30 seconds.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
4
|
+
# Three doors, one install: from a checkout (./install.sh), the one line the
|
|
5
5
|
# readme leads with —
|
|
6
6
|
# curl -fsSL https://raw.githubusercontent.com/mlibre/Terminal-AI-Helper/main/install.sh | bash
|
|
7
7
|
# — where curl's output reaches bash on stdin and the repository has to be
|
|
8
|
-
# fetched before anything below can run
|
|
8
|
+
# fetched before anything below can run, and npm's postinstall hook, which
|
|
9
|
+
# runs this script from the package npm just unpacked (bin/npm-postinstall
|
|
10
|
+
# guards it). The piped door clones to ~/.tai
|
|
9
11
|
# (TAI_DIR moves it; TAI_REMOTE points it at a fork) and then executes the
|
|
10
|
-
# cloned copy of this very script, so
|
|
12
|
+
# cloned copy of this very script, so every door runs the same steps from a
|
|
11
13
|
# real checkout and there is exactly one installer to read.
|
|
12
14
|
set -euo pipefail
|
|
13
15
|
|
|
@@ -130,7 +132,7 @@ if add:
|
|
|
130
132
|
kept.append("# asynchronously, so tai's own hints never reach the screen. Both work.\n")
|
|
131
133
|
kept.append("# Removed by `tai uninstall`, and by TAI_KEEP_AUTOSUGGEST=1.\n")
|
|
132
134
|
kept.append("add-zsh-hook -d precmd _zsh_autosuggest_start 2>/dev/null\n")
|
|
133
|
-
tmp = path.with_name(path.name + ".tai-
|
|
135
|
+
tmp = path.with_name(path.name + ".tai-new")
|
|
134
136
|
tmp.write_text("".join(kept), encoding="utf-8", errors="surrogateescape")
|
|
135
137
|
os.replace(tmp, path)
|
|
136
138
|
PY
|
|
@@ -181,7 +183,7 @@ if not has_source:
|
|
|
181
183
|
# Replaced atomically, the same rule the index builder follows. This file is the
|
|
182
184
|
# user's shell: an interrupt between truncate and write would otherwise leave a
|
|
183
185
|
# half-written .zshrc and a shell that cannot start.
|
|
184
|
-
tmp = path.with_name(path.name + ".tai-
|
|
186
|
+
tmp = path.with_name(path.name + ".tai-new")
|
|
185
187
|
tmp.write_text("".join(cleaned), encoding="utf-8", errors="surrogateescape")
|
|
186
188
|
os.replace(tmp, path)
|
|
187
189
|
PY
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terminal-ai-helper",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"description": "The terminal that knows your next command
|
|
3
|
+
"version": "1.3.3",
|
|
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": {
|
|
7
7
|
"type": "git",
|
|
@@ -17,9 +17,6 @@
|
|
|
17
17
|
"suggestions",
|
|
18
18
|
"history"
|
|
19
19
|
],
|
|
20
|
-
"bin": {
|
|
21
|
-
"tai-install": "bin/tai-install"
|
|
22
|
-
},
|
|
23
20
|
"files": [
|
|
24
21
|
"bin",
|
|
25
22
|
"tai",
|
|
@@ -33,9 +30,10 @@
|
|
|
33
30
|
"docs:dev": "vitepress dev docs",
|
|
34
31
|
"docs:build": "vitepress build docs",
|
|
35
32
|
"docs:preview": "vitepress preview docs",
|
|
33
|
+
"postinstall": "bash bin/npm-postinstall",
|
|
36
34
|
"prepublishOnly": "find plugins -name '*.zwc' -delete && find . -name __pycache__ -type d -prune -exec rm -rf {} +"
|
|
37
35
|
},
|
|
38
36
|
"devDependencies": {
|
|
39
37
|
"vitepress": "^1.6.3"
|
|
40
38
|
}
|
|
41
|
-
}
|
|
39
|
+
}
|
package/plugins/bash/files.bash
CHANGED
|
File without changes
|
package/plugins/bash/index.bash
CHANGED
|
File without changes
|
package/plugins/bash/keys.bash
CHANGED
|
File without changes
|
package/plugins/bash/lookup.bash
CHANGED
|
File without changes
|
package/plugins/bash/units.bash
CHANGED
|
File without changes
|
package/plugins/tai.bash
CHANGED
|
File without changes
|
package/plugins/tai.zsh
CHANGED
|
File without changes
|
package/plugins/zsh/files.zsh
CHANGED
|
File without changes
|
package/plugins/zsh/index.zsh
CHANGED
|
File without changes
|
package/plugins/zsh/lookup.zsh
CHANGED
|
File without changes
|
package/plugins/zsh/menu.zsh
CHANGED
|
@@ -30,6 +30,13 @@ typeset -ga _TAI_MENU
|
|
|
30
30
|
# _tai_menu_open for why the flag travels with the entry.
|
|
31
31
|
typeset -ga _TAI_MENU_Q
|
|
32
32
|
typeset -gi _TAI_MENU_IDX=0 _TAI_MENU_FROM=0 _TAI_MENU_TO=0 _TAI_MENU_LOOSE=0
|
|
33
|
+
# 1 when the rows are whole learned lines that commit onto the word span as
|
|
34
|
+
# whole lines (the first-word menu): _tai_menu_prefix refuses those, because a
|
|
35
|
+
# shared head written into the line would then be written a second time by
|
|
36
|
+
# _tai_menu_commit, which lays the whole entry over the span. Only a per-word
|
|
37
|
+
# menu — every row one word, written over exactly the span the prefix now
|
|
38
|
+
# fills — can take the extension.
|
|
39
|
+
typeset -gi _TAI_MENU_WHOLE=0
|
|
33
40
|
# How deep one first-word key is read, and how many learned whole lines the
|
|
34
41
|
# first-word menu carries ahead of the installed names. Three per key is what
|
|
35
42
|
# keeps a two-line `go mod …` pair beside each other — one per key is the
|
|
@@ -62,6 +69,7 @@ _tai_menu_close() {
|
|
|
62
69
|
_TAI_MENU_IDX=0
|
|
63
70
|
_TAI_MENU_ARMED=0
|
|
64
71
|
_TAI_MENU_LOOSE=0
|
|
72
|
+
_TAI_MENU_WHOLE=0
|
|
65
73
|
_TAI_MENU_STEM=""
|
|
66
74
|
}
|
|
67
75
|
|
|
@@ -286,6 +294,7 @@ _tai_menu_open() {
|
|
|
286
294
|
local -A isdir dup
|
|
287
295
|
local word before c rest p real
|
|
288
296
|
local -i len=${#BUFFER} i skip width term limit added=0 cap=0
|
|
297
|
+
_TAI_MENU_WHOLE=0
|
|
289
298
|
|
|
290
299
|
# The width of the terminal, which the layout below is built to. Read once
|
|
291
300
|
# here, because the command list is capped by it before the menu knows how
|
|
@@ -371,6 +380,7 @@ _tai_menu_open() {
|
|
|
371
380
|
out+=( "${_TAI_MENU_LEARNED[@]}" )
|
|
372
381
|
repeat ${#_TAI_MENU_LEARNED[@]}; do outq+=( 0 ); done
|
|
373
382
|
learned_whole=1
|
|
383
|
+
_TAI_MENU_WHOLE=1
|
|
374
384
|
fi
|
|
375
385
|
# An installed command the history has never seen. Worth offering only once
|
|
376
386
|
# the command name is complete, which is the same rule _tai_query keeps so
|
|
@@ -589,6 +599,7 @@ _tai_menu_open_loose() {
|
|
|
589
599
|
_TAI_MENU_TO=${#BUFFER}
|
|
590
600
|
_TAI_MENU_ARMED=0
|
|
591
601
|
_TAI_MENU_LOOSE=1
|
|
602
|
+
_TAI_MENU_WHOLE=0
|
|
592
603
|
_tai_menu_layout ""
|
|
593
604
|
_TAI_MENU_ARMED=0
|
|
594
605
|
}
|
|
@@ -666,6 +677,76 @@ _tai_menu_paint() {
|
|
|
666
677
|
fi
|
|
667
678
|
}
|
|
668
679
|
|
|
680
|
+
# Write into the line what every entry already shares, before the list is drawn.
|
|
681
|
+
#
|
|
682
|
+
# The report: `cat .zs` answered with `.zsh/` and `.zshrc` — two candidates that
|
|
683
|
+
# are the same word until the `h` — and the menu showed them under a line still
|
|
684
|
+
# reading `.zs`. Every shell completes the part that is certain first, and
|
|
685
|
+
# readline's was already doing it here (the bash listing test pins `tzz` becoming
|
|
686
|
+
# `tzz_`); the menu made the user type the shared tail themselves, or take a whole
|
|
687
|
+
# entry to get it. So the press that opens the list also advances the line to the
|
|
688
|
+
# longest head every candidate starts with — `.zs` becomes `.zsh`, the list stands
|
|
689
|
+
# under it, and the next press is the choosing press it always was.
|
|
690
|
+
#
|
|
691
|
+
# The head is computed over the forms that would be *written*, not the raw rows:
|
|
692
|
+
# an entry a shell would split — `wombat book.txt` — is written quoted, and a
|
|
693
|
+
# head cut from the quoted forms is the one string that is safe to leave in the
|
|
694
|
+
# line when the user keeps typing instead of choosing. `womb` advances to
|
|
695
|
+
# `wombat\ ` there, one shell word, every entry still extending it.
|
|
696
|
+
#
|
|
697
|
+
# Three refusals, each its own way of being wrong answered:
|
|
698
|
+
# * a word that is empty (the line ends in a space, or there is no line):
|
|
699
|
+
# nothing typed is nothing to extend, and the listing is the answer;
|
|
700
|
+
# * the loose list and the first-word menu, whose rows are whole lines laid
|
|
701
|
+
# over the whole span — a head written into the line would be written a
|
|
702
|
+
# second time by _tai_menu_commit, which replaces the span with the entire
|
|
703
|
+
# entry;
|
|
704
|
+
# * a head that does not reach past the word, or does not start with it: the
|
|
705
|
+
# first is no extension at all, and the second is a word the user did not
|
|
706
|
+
# type — `my doc` answered with the quoted `My\ Documents` head reads as
|
|
707
|
+
# two words where one was typed, so the list answers instead.
|
|
708
|
+
#
|
|
709
|
+
# The span moves with the text: _TAI_MENU_TO follows the head, the cursor lands
|
|
710
|
+
# after it, and the layout runs again so the stem is cut against what the line
|
|
711
|
+
# now shows and _TAI_MENU_LINE describes the line that is actually standing.
|
|
712
|
+
# Committing an entry afterwards lays the whole entry over the span — the head
|
|
713
|
+
# included — so nothing is written twice.
|
|
714
|
+
_tai_menu_prefix() {
|
|
715
|
+
(( ${#_TAI_MENU} > 1 )) || return 1
|
|
716
|
+
(( _TAI_MENU_LOOSE || _TAI_MENU_WHOLE )) && return 1
|
|
717
|
+
local -i from=$_TAI_MENU_FROM to=$_TAI_MENU_TO k
|
|
718
|
+
# An empty word — the line ends in a space — has no span to extend.
|
|
719
|
+
(( from <= to )) || return 1
|
|
720
|
+
local -a forms
|
|
721
|
+
local c rest typed
|
|
722
|
+
forms=()
|
|
723
|
+
for (( k = 1; k <= ${#_TAI_MENU}; k++ )); do
|
|
724
|
+
c="${_TAI_MENU[k]}"
|
|
725
|
+
(( ${_TAI_MENU_Q[k]} )) && { _tai_quote "$c"; c="$_TAI_QUOTED" }
|
|
726
|
+
forms+=( "$c" )
|
|
727
|
+
done
|
|
728
|
+
# The longest head every written form starts with, found by shrinking the
|
|
729
|
+
# first form until the rest still begin with it — the same walk the stem
|
|
730
|
+
# makes over the raw rows.
|
|
731
|
+
rest="${forms[1]}"
|
|
732
|
+
for c in "${forms[@]}"; do
|
|
733
|
+
while [[ -n "$rest" && "$c" != "$rest"* ]]; do rest="${rest[1,-2]}"; done
|
|
734
|
+
[[ -n "$rest" ]] || break
|
|
735
|
+
done
|
|
736
|
+
[[ -n "$rest" ]] || return 1
|
|
737
|
+
typed="${BUFFER[from,to]}"
|
|
738
|
+
(( ${#rest} > ${#typed} )) || return 1
|
|
739
|
+
# Folded, like every lookup: `down` advances to `Downloads` when the disk
|
|
740
|
+
# says that is the word, and the case the candidates carry is the case the
|
|
741
|
+
# line takes — the same rule the commit below this one keeps.
|
|
742
|
+
[[ "${rest:l}" == "${typed:l}"* ]] || return 1
|
|
743
|
+
BUFFER="${BUFFER[1,$(( from - 1 ))]}$rest${BUFFER[$(( to + 1 )),-1]}"
|
|
744
|
+
CURSOR=${#BUFFER}
|
|
745
|
+
_TAI_MENU_TO=$(( from + ${#rest} - 1 ))
|
|
746
|
+
_tai_menu_layout "$rest"
|
|
747
|
+
return 0
|
|
748
|
+
}
|
|
749
|
+
|
|
669
750
|
# Put the selected entry where the word was, and close the menu. Nothing runs: the
|
|
670
751
|
# line is now `cd Movies/` with the cursor after it, which is the point — a
|
|
671
752
|
# completion is not a decision to execute.
|
package/plugins/zsh/units.zsh
CHANGED
|
File without changes
|
package/plugins/zsh/widgets.zsh
CHANGED
|
@@ -272,7 +272,13 @@ tai-tab() {
|
|
|
272
272
|
zle -N tai-tab
|
|
273
273
|
|
|
274
274
|
# Tab opens the menu, and every Tab after that moves the selection one entry
|
|
275
|
-
# along. The
|
|
275
|
+
# along. The one thing the first press writes is the part every entry already
|
|
276
|
+
# shares: `cat .zs` answered by `.zsh/` and `.zshrc` advances to `cat .zsh`
|
|
277
|
+
# before the list draws, because two candidates that are the same word until
|
|
278
|
+
# the `h` are a prefix the user should not have to type (see _tai_menu_prefix,
|
|
279
|
+
# which refuses it where writing it would lie — an empty word, a whole-line
|
|
280
|
+
# list, a head the typed word does not start with). After that the line stands:
|
|
281
|
+
# a menu is a way of choosing, not of typing.
|
|
276
282
|
#
|
|
277
283
|
# When there is nothing to offer, the key goes back to zsh's own completion.
|
|
278
284
|
# `docker` has nothing to complete *to*, because everything tai knows about it
|
|
@@ -319,6 +325,11 @@ tai-menu() {
|
|
|
319
325
|
_tai_menu_commit
|
|
320
326
|
return
|
|
321
327
|
fi
|
|
328
|
+
# Several entries: the line takes what they all already say (the shared
|
|
329
|
+
# head — `.zsh` under `.zsh/` and `.zshrc`), and the list draws under it.
|
|
330
|
+
# _tai_menu_prefix refuses it where the line must not be written; the menu
|
|
331
|
+
# then behaves exactly as it did.
|
|
332
|
+
_tai_menu_prefix
|
|
322
333
|
_tai_menu_paint
|
|
323
334
|
}
|
|
324
335
|
zle -N tai-menu
|
package/readme.md
CHANGED
|
@@ -21,13 +21,29 @@ 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
|
|
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
30
|
```
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
```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
|
|
46
|
+
```
|
|
31
47
|
|
|
32
48
|
The line downloads TAI to `~/.tai` and runs the installer from there; run it
|
|
33
49
|
again later and it updates in place. It enables both shells, imports your
|
|
@@ -65,23 +81,24 @@ machine. New commands show up within seconds; no restart, ever.
|
|
|
65
81
|
|
|
66
82
|
## The keys
|
|
67
83
|
|
|
68
|
-
| key | what it does
|
|
69
|
-
| ----------------------- |
|
|
70
|
-
| `→` | take the hint at the end of the line, else move right
|
|
71
|
-
| `Ctrl-F` | take the hint
|
|
72
|
-
| `Alt-F` / `Ctrl-Right` | take **one word** of the hint, leaving the rest to type (bash: `Ctrl-Right`)
|
|
73
|
-
| `Tab` | cycle files, folders and options; on a bare first word the learned lines come first, ranked like the dashboard |
|
|
74
|
-
| `Ctrl-Space` / `Ctrl-T` | open the list, hint or no hint; press again to move the selection
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
84
|
+
| key | what it does |
|
|
85
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
86
|
+
| `→` | take the hint at the end of the line, else move right |
|
|
87
|
+
| `Ctrl-F` | take the hint |
|
|
88
|
+
| `Alt-F` / `Ctrl-Right` | take **one word** of the hint, leaving the rest to type (bash: `Ctrl-Right`) |
|
|
89
|
+
| `Tab` | write what every candidate shares, then cycle files, folders and options; on a bare first word the learned lines come first, ranked like the dashboard |
|
|
90
|
+
| `Ctrl-Space` / `Ctrl-T` | open the list, hint or no hint; press again to move the selection (zsh) |
|
|
91
|
+
| `Down` | peek at what usually follows — opens the list when the typed line shows no hint; history otherwise (zsh) |
|
|
92
|
+
| `Enter` | take the selected entry and stop; a second `Enter` runs the line |
|
|
93
|
+
| `Alt-G` | the command that followed the last one (bash) |
|
|
77
94
|
|
|
78
95
|
A paste is not a query — pasted text never opens a list, and one typed
|
|
79
96
|
character brings it back. `Enter` fills a selection in and stops; running it
|
|
80
97
|
is always a second, deliberate `Enter`.
|
|
81
98
|
|
|
82
|
-
**bash** ranks identically and takes the
|
|
83
|
-
a list below the line: `Tab` completes directly — after
|
|
84
|
-
after every command with `TAI_COMPLETE_ALL=1`.
|
|
99
|
+
**bash** ranks identically and takes the hint keys, but readline cannot draw
|
|
100
|
+
a list below the line and has no menu key: `Tab` completes directly — after
|
|
101
|
+
`tai` by default, after every command with `TAI_COMPLETE_ALL=1`.
|
|
85
102
|
|
|
86
103
|
## What it suggests, and what it will not
|
|
87
104
|
|
|
@@ -104,6 +121,9 @@ memory, an array hit in microseconds.
|
|
|
104
121
|
- **A path is answered by the filesystem** — what is actually there, newest
|
|
105
122
|
first. A `cd` is answered by directories that exist here, judged from where
|
|
106
123
|
you stand, not offered two directories away.
|
|
124
|
+
- **`systemctl` is answered by the machine's own unit list.**
|
|
125
|
+
`systemctl restart her` cycles `nginx.service` and friends — read once,
|
|
126
|
+
held in the shell.
|
|
107
127
|
- **A typo is answered by what it is a typo of.** When nothing extends the
|
|
108
128
|
line, the learned lines that hold every word you typed appear; the one-off
|
|
109
129
|
typo ranks below the habit it shadows, in the index itself.
|
|
@@ -126,33 +146,13 @@ memory, an array hit in microseconds.
|
|
|
126
146
|
tai web # or: tai dashboard → http://127.0.0.1:8247/
|
|
127
147
|
```
|
|
128
148
|
|
|
129
|
-

|
|
131
151
|
|
|
132
152
|
One server, read-only: it binds `127.0.0.1`, answers GETs only, and writes
|
|
133
153
|
nothing. Clicking a suggestion copies it; `--port N` moves it; `--no-browser`
|
|
134
154
|
skips the auto-open.
|
|
135
155
|
|
|
136
|
-
## Commands
|
|
137
|
-
|
|
138
|
-
```sh
|
|
139
|
-
tai refresh # import new history rows, then rebuild the indexes
|
|
140
|
-
tai discover # learn --help from installed tools (cached, ~5-35s)
|
|
141
|
-
tai doctor # what is indexed, and what is held back
|
|
142
|
-
tai bench # build time, latency, memory, each shell's index read time
|
|
143
|
-
tai web # a read-only localhost dashboard (alias: tai dashboard)
|
|
144
|
-
tai eval # candidate coverage against your own history
|
|
145
|
-
tai update # newest version from GitHub, then reinstall (alias: tai upgrade)
|
|
146
|
-
tai version # print the release this install is running
|
|
147
|
-
tai purge # drop unusable rows, then rebuild
|
|
148
|
-
tai forget git sta # drop every stored row for a command, exactly as typed
|
|
149
|
-
tai uninstall # remove everything tai added
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
The plugins call `flush` themselves, and every command drains what is pending
|
|
153
|
-
first — no answer lags a few records behind what you just did, and no restart
|
|
154
|
-
is needed: the index is re-read at the next prompt.
|
|
155
|
-
|
|
156
156
|
## Configuration
|
|
157
157
|
|
|
158
158
|
All have working defaults; they are for when yours is wrong.
|
|
@@ -191,7 +191,7 @@ release; it puts `tai` on `PATH` and prints the two `source` lines for your
|
|
|
191
191
|
rc file.
|
|
192
192
|
|
|
193
193
|
```sh
|
|
194
|
-
tai update # newest version
|
|
194
|
+
tai update # newest version — GitHub for a checkout, npm for an npm install; alias: tai upgrade
|
|
195
195
|
```
|
|
196
196
|
|
|
197
197
|
It stops rather than overwrite a copy you have edited yourself.
|
|
@@ -201,8 +201,9 @@ tai uninstall
|
|
|
201
201
|
```
|
|
202
202
|
|
|
203
203
|
Removes the shell integration, the wrapper, the indexes, the SQLite history
|
|
204
|
-
and the cached knowledge. The `~/.tai` folder stays
|
|
205
|
-
|
|
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.
|
|
206
207
|
|
|
207
208
|
## Why it stays fast
|
|
208
209
|
|
package/tai/__init__.py
CHANGED
|
File without changes
|
package/tai/bench.py
CHANGED
|
File without changes
|
package/tai/cli.py
CHANGED
|
@@ -10,7 +10,8 @@ updating, and diagnostics.
|
|
|
10
10
|
tai record "<cmd>" [--cwd X --git Y --branch B --exit 0]
|
|
11
11
|
tai flush ingest the records the plugins spooled, then maintain
|
|
12
12
|
tai refresh [--quiet]
|
|
13
|
-
tai update | upgrade pull the latest version from GitHub
|
|
13
|
+
tai update | upgrade pull the latest version and reinstall — from GitHub
|
|
14
|
+
for a checkout install, from npm for an npm one
|
|
14
15
|
tai version print the release this install is running
|
|
15
16
|
tai discover [tool ...]
|
|
16
17
|
tai purge [--stale] [--rebuild/--no-rebuild]
|
|
@@ -149,14 +150,107 @@ def cmd_refresh(a) -> int:
|
|
|
149
150
|
return 0
|
|
150
151
|
|
|
151
152
|
|
|
153
|
+
def _install_method(repo: "Path") -> str:
|
|
154
|
+
"""'npm', 'git', or 'none' — decided by where the running code lives.
|
|
155
|
+
|
|
156
|
+
The wrapper execs one cli.py, and that file's location is the copy an
|
|
157
|
+
update has to replace, so the method is a fact about the path, not about
|
|
158
|
+
the user's memory of how they installed: npm keeps every global package
|
|
159
|
+
under a node_modules directory, a checkout keeps its .git, and anything
|
|
160
|
+
else (the .deb, a hand-copied tree) is 'none' — nothing here can pull it
|
|
161
|
+
forward, and the answer says so instead of guessing.
|
|
162
|
+
"""
|
|
163
|
+
if "node_modules" in repo.parts:
|
|
164
|
+
return "npm"
|
|
165
|
+
if (repo / ".git").exists():
|
|
166
|
+
return "git"
|
|
167
|
+
return "none"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _npm_configured() -> bool:
|
|
171
|
+
"""True when npm's postinstall hook already ran the installer from the tree.
|
|
172
|
+
|
|
173
|
+
The hook runs install.sh inside the npm install itself, and rerunning it
|
|
174
|
+
from here would print the welcome twice and rebuild an index that is
|
|
175
|
+
current. The check is the installer's own two products — the wrapper
|
|
176
|
+
naming this tree and one rc line per shell sourcing it — asked of the text
|
|
177
|
+
npm's layout guarantees is unquoted through the middle: a prefix path with
|
|
178
|
+
a space in it comes out of printf %q escaped, but everything from
|
|
179
|
+
node_modules down survives intact.
|
|
180
|
+
"""
|
|
181
|
+
from pathlib import Path
|
|
182
|
+
|
|
183
|
+
tail = "node_modules/terminal-ai-helper"
|
|
184
|
+
wrapper = (Path(os.environ.get("BIN_DIR") or Path.home() / ".local" / "bin")
|
|
185
|
+
/ "tai")
|
|
186
|
+
try:
|
|
187
|
+
text = wrapper.read_text(encoding="utf-8", errors="surrogateescape")
|
|
188
|
+
except OSError:
|
|
189
|
+
return False
|
|
190
|
+
if f"{tail}/tai/cli.py" not in text:
|
|
191
|
+
return False
|
|
192
|
+
home = Path.home()
|
|
193
|
+
for rc, plugin in ((".zshrc", "tai.zsh"), (".bashrc", "tai.bash")):
|
|
194
|
+
try:
|
|
195
|
+
lines = (home / rc).read_text(
|
|
196
|
+
encoding="utf-8", errors="surrogateescape").splitlines()
|
|
197
|
+
except OSError:
|
|
198
|
+
return False
|
|
199
|
+
if not any("source" in line and f"{tail}/plugins/{plugin}" in line
|
|
200
|
+
for line in lines):
|
|
201
|
+
return False
|
|
202
|
+
return True
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _update_npm(repo: "Path") -> int:
|
|
206
|
+
"""The npm door of `tai update`: let the registry replace the package.
|
|
207
|
+
|
|
208
|
+
npm's own output streams through — the install can take several seconds
|
|
209
|
+
and its progress is the only thing the user sees while it runs — and the
|
|
210
|
+
postinstall hook finishes the reinstall from the new tree unless scripts
|
|
211
|
+
were skipped (--ignore-scripts) or it failed, in which case the installer
|
|
212
|
+
runs here instead: idempotent, and the same report as every other door.
|
|
213
|
+
"""
|
|
214
|
+
import shutil
|
|
215
|
+
import subprocess
|
|
216
|
+
|
|
217
|
+
npm = shutil.which("npm")
|
|
218
|
+
if not npm:
|
|
219
|
+
print("tai: npm is not installed, so there is nothing to update from")
|
|
220
|
+
print(" reinstall with: npm i -g terminal-ai-helper@latest")
|
|
221
|
+
return 1
|
|
222
|
+
before = _read_version()
|
|
223
|
+
pulled = subprocess.run([npm, "install", "-g", "terminal-ai-helper@latest"])
|
|
224
|
+
if pulled.returncode != 0:
|
|
225
|
+
print("tai: npm install failed — the previous version is still installed")
|
|
226
|
+
return 1
|
|
227
|
+
after = _read_version()
|
|
228
|
+
# Flushed before anything else prints: npm's own output above is the
|
|
229
|
+
# progress, and this line is the verdict.
|
|
230
|
+
print("✓ already up to date" if after == before else f"✓ updated to {after}",
|
|
231
|
+
flush=True)
|
|
232
|
+
if after == before or _npm_configured():
|
|
233
|
+
return 0
|
|
234
|
+
installer = repo / "install.sh"
|
|
235
|
+
if not installer.exists():
|
|
236
|
+
print(f"tai: {installer} is missing, so the new code was not installed")
|
|
237
|
+
return 1
|
|
238
|
+
# A subprocess, not a re-entrant call: the install just replaced the files
|
|
239
|
+
# this process was loaded from, so anything it calls now is the old code.
|
|
240
|
+
return subprocess.run(["bash", str(installer)]).returncode
|
|
241
|
+
|
|
242
|
+
|
|
152
243
|
def cmd_update(a) -> int:
|
|
153
|
-
"""Pull the latest version
|
|
244
|
+
"""Pull the latest version and reinstall it, from wherever tai came from.
|
|
154
245
|
|
|
155
|
-
The user should never have to remember which folder the project lives in
|
|
156
|
-
|
|
157
|
-
from a path they have to type.
|
|
246
|
+
The user should never have to remember which folder the project lives in —
|
|
247
|
+
or which package manager put it there — so this is driven from where tai
|
|
248
|
+
already is rather than from a path they have to type.
|
|
158
249
|
"""
|
|
159
250
|
repo = _repo_dir()
|
|
251
|
+
if _install_method(repo) == "npm":
|
|
252
|
+
return _update_npm(repo)
|
|
253
|
+
|
|
160
254
|
import shutil
|
|
161
255
|
import subprocess
|
|
162
256
|
|
package/tai/decisions.py
CHANGED
|
File without changes
|
package/tai/engcache.py
CHANGED
|
File without changes
|
package/tai/engine.py
CHANGED
|
File without changes
|
package/tai/evaluate.py
CHANGED
|
File without changes
|
package/tai/evaluate_jev.py
CHANGED
|
File without changes
|
package/tai/fresh.py
CHANGED
|
File without changes
|
package/tai/helptext.py
CHANGED
|
File without changes
|
package/tai/index.py
CHANGED
|
File without changes
|
package/tai/jev.py
CHANGED
|
File without changes
|
package/tai/knowledge.py
CHANGED
|
File without changes
|
package/tai/maintenance.py
CHANGED
|
File without changes
|
package/tai/paths.py
CHANGED
|
File without changes
|
package/tai/predictor.py
CHANGED
|
File without changes
|
package/tai/seed.py
CHANGED
|
File without changes
|
package/tai/spool.py
CHANGED
|
File without changes
|
package/tai/store.py
CHANGED
|
File without changes
|
package/tai/tune.py
CHANGED
|
File without changes
|
package/tai/typo.py
CHANGED
|
@@ -212,10 +212,18 @@ def shadow_map(eng, lines=None) -> dict[str, str]:
|
|
|
212
212
|
* the two share their first word and are within two edits whole-line,
|
|
213
213
|
which is the shape a mistyped word takes (`unsintall`/`uninstall` is
|
|
214
214
|
two adjacent swaps, invisible to a one-edit rule);
|
|
215
|
+
* the comparison folds case — every lookup in the product folds case,
|
|
216
|
+
and the pair loop must agree, or `LS -la` stands beside `ls -la` as
|
|
217
|
+
its own row (the first words land in different groups) and a case
|
|
218
|
+
diff longer than two letters clears the edit cap. A capital twin is
|
|
219
|
+
a typo of the lowercase habit no less than a misspelling is; the
|
|
220
|
+
map's keys keep the store's own spelling either way;
|
|
215
221
|
* the target is the *stronger* spelling: more successes, then more
|
|
216
222
|
runs, then more recent — and when the evidence ties outright, the
|
|
217
|
-
|
|
218
|
-
|
|
223
|
+
spelling that is all-lowercase stands (commands are lowercase; the
|
|
224
|
+
capitalised twin is the accident), byte order otherwise, which is
|
|
225
|
+
the one honest way to break a tie the store carries no other
|
|
226
|
+
evidence for;
|
|
219
227
|
* the population both sides come from is `lines`: the index passes the
|
|
220
228
|
lines the user actually ran (a corpus convention is not a typo and
|
|
221
229
|
not a habit one shadows), the engine passes the candidates it is
|
|
@@ -229,6 +237,9 @@ def shadow_map(eng, lines=None) -> dict[str, str]:
|
|
|
229
237
|
"""
|
|
230
238
|
names = list(eng.cmds) if lines is None else \
|
|
231
239
|
[c for c in lines if c in eng.cmds]
|
|
240
|
+
# The comparison runs on lowered copies — see the case bullet above —
|
|
241
|
+
# while every key this map returns is the store's own spelling.
|
|
242
|
+
low = {c: c.lower() for c in names}
|
|
232
243
|
# Per-line letter sets and counts: the two cheap gates before the edit
|
|
233
244
|
# distance. Two lines within two edits can differ by at most two distinct
|
|
234
245
|
# letters per side (the set gate) and by at most two in character surplus
|
|
@@ -238,20 +249,23 @@ def shadow_map(eng, lines=None) -> dict[str, str]:
|
|
|
238
249
|
# is a pair the distance would reject too. The counts also answer the
|
|
239
250
|
# deficit for free — surplus minus the length difference is deficit, by
|
|
240
251
|
# conservation of characters — so one walk per pair gates both sides.
|
|
241
|
-
sets = {c: set(c) for c in names}
|
|
252
|
+
sets = {c: set(low[c]) for c in names}
|
|
242
253
|
counts = {c: {} for c in names}
|
|
243
254
|
for c, bag in counts.items():
|
|
244
|
-
for ch in c:
|
|
255
|
+
for ch in low[c]:
|
|
245
256
|
bag[ch] = bag.get(ch, 0) + 1
|
|
246
257
|
groups: dict[str, dict[int, list[str]]] = {}
|
|
247
258
|
for c in names:
|
|
248
|
-
groups.setdefault(c.split(" ", 1)[0], {}).setdefault(len(c), []).append(c)
|
|
259
|
+
groups.setdefault(low[c].split(" ", 1)[0], {}).setdefault(len(c), []).append(c)
|
|
260
|
+
# The full-tie spelling to stand: all-lowercase first (commands are
|
|
261
|
+
# lowercase; the capitalised twin is the accident), byte order otherwise.
|
|
262
|
+
tkey = {c: (0, c) if low[c] == c else (1, c) for c in names}
|
|
249
263
|
out: dict[str, str] = {}
|
|
250
264
|
for cmd in names:
|
|
251
265
|
st = eng.cmds[cmd]
|
|
252
266
|
if st.freq > TYPO_FREQ_MAX:
|
|
253
267
|
continue
|
|
254
|
-
buckets = groups.get(cmd.split(" ", 1)[0])
|
|
268
|
+
buckets = groups.get(low[cmd].split(" ", 1)[0])
|
|
255
269
|
if not buckets:
|
|
256
270
|
continue
|
|
257
271
|
s_set = sets[cmd]
|
|
@@ -281,14 +295,14 @@ def shadow_map(eng, lines=None) -> dict[str, str]:
|
|
|
281
295
|
ost = eng.cmds[other]
|
|
282
296
|
oev = (ost.success, ost.freq, ost.last_ts)
|
|
283
297
|
# The target must be the stronger spelling — or, on a full
|
|
284
|
-
# tie, the
|
|
285
|
-
# shadow, not this line's habit.
|
|
286
|
-
if oev < ev or (oev == ev and other > cmd):
|
|
298
|
+
# tie, the likelier spelling (the tie bullet above). Anything
|
|
299
|
+
# weaker is somebody else's shadow, not this line's habit.
|
|
300
|
+
if oev < ev or (oev == ev and tkey[other] > tkey[cmd]):
|
|
287
301
|
continue
|
|
288
|
-
if lev1(cmd, other, 2) > 2:
|
|
302
|
+
if lev1(low[cmd], low[other], 2) > 2:
|
|
289
303
|
continue
|
|
290
304
|
if best is None or oev > best[0] or \
|
|
291
|
-
(oev == best[0] and other < best[1]):
|
|
305
|
+
(oev == best[0] and tkey[other] < tkey[best[1]]):
|
|
292
306
|
best = (oev, other)
|
|
293
307
|
if best is not None:
|
|
294
308
|
out[cmd] = best[1]
|
package/tai/web.py
CHANGED
|
File without changes
|
package/bin/tai-install
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# npm's bin shim is a symlink into the package directory, and install.sh reads
|
|
3
|
-
# its own location to decide what to install from — so resolve the symlink
|
|
4
|
-
# chain to the real file first, then hand over to the installer at the
|
|
5
|
-
# package root, one level above bin/.
|
|
6
|
-
set -e
|
|
7
|
-
src="${BASH_SOURCE[0]:-$0}"
|
|
8
|
-
while [ -L "$src" ]; do
|
|
9
|
-
dir="$(cd "$(dirname "$src")" && pwd)"
|
|
10
|
-
src="$(readlink "$src")"
|
|
11
|
-
[ "${src:0:1}" = "/" ] || src="$dir/$src"
|
|
12
|
-
done
|
|
13
|
-
exec bash "$(cd "$(dirname "$src")/.." && pwd)/install.sh" "$@"
|