aish-cli 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/aish.plugin.zsh +427 -0
- package/dist/index.js +286 -78
- package/package.json +14 -3
package/aish.plugin.zsh
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# aish - AI Shell Integration for Zsh
|
|
2
|
+
# Press Ctrl+G to convert natural language to shell commands
|
|
3
|
+
|
|
4
|
+
# Check if aish is installed
|
|
5
|
+
if ! command -v aish &>/dev/null; then
|
|
6
|
+
print -P "%F{yellow}[aish]%f aish command not found. Install with: npm install -g aish-cli"
|
|
7
|
+
return 1
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
# History file
|
|
11
|
+
AISH_HISTFILE="${AISH_HISTFILE:-$HOME/.aish_history}"
|
|
12
|
+
AISH_HISTSIZE="${AISH_HISTSIZE:-100}"
|
|
13
|
+
|
|
14
|
+
# Shared state
|
|
15
|
+
typeset -g _aish_input_query=""
|
|
16
|
+
|
|
17
|
+
# Load history into array
|
|
18
|
+
typeset -ga _aish_history
|
|
19
|
+
_aish_load_history() {
|
|
20
|
+
_aish_history=()
|
|
21
|
+
[[ -f "$AISH_HISTFILE" ]] && _aish_history=("${(@f)$(< "$AISH_HISTFILE")}")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_aish_save_history() {
|
|
25
|
+
local entry="$1"
|
|
26
|
+
[[ -z "$entry" ]] && return
|
|
27
|
+
_aish_history=("${(@)_aish_history:#$entry}")
|
|
28
|
+
_aish_history+=("$entry")
|
|
29
|
+
while (( ${#_aish_history} > AISH_HISTSIZE )); do
|
|
30
|
+
shift _aish_history
|
|
31
|
+
done
|
|
32
|
+
printf '%s\n' "${_aish_history[@]}" > "$AISH_HISTFILE"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_aish_load_history
|
|
36
|
+
|
|
37
|
+
# Fill placeholders like <message>, <url>, etc.
|
|
38
|
+
# Sets _aish_input_query with result, returns 1 if cancelled
|
|
39
|
+
_aish_fill_placeholders() {
|
|
40
|
+
local cmd="$1"
|
|
41
|
+
local result="$cmd"
|
|
42
|
+
|
|
43
|
+
# Find all unique placeholders
|
|
44
|
+
local placeholders=()
|
|
45
|
+
local seen=()
|
|
46
|
+
while [[ "$result" =~ '<([^>]+)>' ]]; do
|
|
47
|
+
local match="${MATCH}"
|
|
48
|
+
local name="${match:1:-1}"
|
|
49
|
+
|
|
50
|
+
# Check if already seen
|
|
51
|
+
local found=0
|
|
52
|
+
for s in "${seen[@]}"; do
|
|
53
|
+
[[ "$s" == "$match" ]] && found=1 && break
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
if (( !found )); then
|
|
57
|
+
seen+=("$match")
|
|
58
|
+
placeholders+=("$match:$name")
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Move past this match
|
|
62
|
+
result="${result#*$match}"
|
|
63
|
+
done
|
|
64
|
+
|
|
65
|
+
result="$cmd"
|
|
66
|
+
|
|
67
|
+
# If no placeholders, return as-is
|
|
68
|
+
if (( ${#placeholders[@]} == 0 )); then
|
|
69
|
+
_aish_input_query="$cmd"
|
|
70
|
+
return 0
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Prompt for each placeholder
|
|
74
|
+
local total=${#placeholders[@]}
|
|
75
|
+
local current=0
|
|
76
|
+
|
|
77
|
+
for entry in "${placeholders[@]}"; do
|
|
78
|
+
(( current++ ))
|
|
79
|
+
local placeholder="${entry%%:*}"
|
|
80
|
+
local name="${entry#*:}"
|
|
81
|
+
|
|
82
|
+
# Read the value with inline display
|
|
83
|
+
local value=""
|
|
84
|
+
local vcursor=0
|
|
85
|
+
local char
|
|
86
|
+
|
|
87
|
+
# Helper to update display with current value
|
|
88
|
+
_aish_placeholder_display() {
|
|
89
|
+
# Replace first occurrence of placeholder with value for display
|
|
90
|
+
local before="${result%%$placeholder*}"
|
|
91
|
+
local after="${result#*$placeholder}"
|
|
92
|
+
local display_val="$value"
|
|
93
|
+
[[ -z "$display_val" ]] && display_val="$placeholder"
|
|
94
|
+
|
|
95
|
+
BUFFER="${before}${display_val}${after}"
|
|
96
|
+
CURSOR=$(( ${#before} + vcursor ))
|
|
97
|
+
|
|
98
|
+
POSTDISPLAY=$'\n '"$name ($current/$total)"' │ enter: continue │ esc: cancel'
|
|
99
|
+
region_highlight=()
|
|
100
|
+
|
|
101
|
+
# Highlight the value/placeholder area
|
|
102
|
+
local start=${#before}
|
|
103
|
+
local end=$(( start + ${#display_val} ))
|
|
104
|
+
if [[ -z "$value" ]]; then
|
|
105
|
+
region_highlight+=("${start} ${end} fg=yellow,bold")
|
|
106
|
+
else
|
|
107
|
+
region_highlight+=("${start} ${end} fg=green")
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# Dim the hint text
|
|
111
|
+
local hint_start=${#BUFFER}
|
|
112
|
+
local hint_end=$(( hint_start + ${#POSTDISPLAY} ))
|
|
113
|
+
region_highlight+=("${hint_start} ${hint_end} fg=8")
|
|
114
|
+
|
|
115
|
+
zle -R
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_aish_placeholder_display
|
|
119
|
+
|
|
120
|
+
while read -k1 char; do
|
|
121
|
+
# Handle escape sequences (arrow keys)
|
|
122
|
+
if [[ "$char" == $'\e' ]]; then
|
|
123
|
+
read -k1 -t 0.1 char2
|
|
124
|
+
if [[ "$char2" == '[' ]]; then
|
|
125
|
+
read -k1 -t 0.1 char3
|
|
126
|
+
case "$char3" in
|
|
127
|
+
C) (( vcursor < ${#value} )) && (( vcursor++ )) ;; # Right
|
|
128
|
+
D) (( vcursor > 0 )) && (( vcursor-- )) ;; # Left
|
|
129
|
+
esac
|
|
130
|
+
_aish_placeholder_display
|
|
131
|
+
continue
|
|
132
|
+
else
|
|
133
|
+
# Escape key - cancel
|
|
134
|
+
_aish_input_query=""
|
|
135
|
+
POSTDISPLAY=""
|
|
136
|
+
return 1
|
|
137
|
+
fi
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
case "$char" in
|
|
141
|
+
$'\n'|$'\r')
|
|
142
|
+
break ;;
|
|
143
|
+
$'\x7f'|$'\b')
|
|
144
|
+
if (( vcursor > 0 )); then
|
|
145
|
+
value="${value:0:$((vcursor-1))}${value:$vcursor}"
|
|
146
|
+
(( vcursor-- ))
|
|
147
|
+
fi ;;
|
|
148
|
+
$'\x03')
|
|
149
|
+
_aish_input_query=""
|
|
150
|
+
POSTDISPLAY=""
|
|
151
|
+
return 1 ;;
|
|
152
|
+
$'\x01') vcursor=0 ;; # Ctrl+A
|
|
153
|
+
$'\x05') vcursor=${#value} ;; # Ctrl+E
|
|
154
|
+
*)
|
|
155
|
+
value="${value:0:$vcursor}${char}${value:$vcursor}"
|
|
156
|
+
(( vcursor++ )) ;;
|
|
157
|
+
esac
|
|
158
|
+
_aish_placeholder_display
|
|
159
|
+
done
|
|
160
|
+
|
|
161
|
+
unset -f _aish_placeholder_display
|
|
162
|
+
|
|
163
|
+
# Cancel if empty value
|
|
164
|
+
if [[ -z "$value" ]]; then
|
|
165
|
+
_aish_input_query=""
|
|
166
|
+
POSTDISPLAY=""
|
|
167
|
+
return 1
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
# Replace all occurrences of this placeholder
|
|
171
|
+
result="${result//$placeholder/$value}"
|
|
172
|
+
done
|
|
173
|
+
|
|
174
|
+
POSTDISPLAY=""
|
|
175
|
+
_aish_input_query="$result"
|
|
176
|
+
return 0
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
_aish_update_display() {
|
|
180
|
+
local prefix="$1"
|
|
181
|
+
local query="$2"
|
|
182
|
+
local qcursor="$3"
|
|
183
|
+
|
|
184
|
+
BUFFER="${prefix}${query}"
|
|
185
|
+
CURSOR=$(( ${#prefix} + qcursor ))
|
|
186
|
+
region_highlight=("0 ${#prefix} fg=magenta,bold")
|
|
187
|
+
zle -R
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
_aish_read_input() {
|
|
191
|
+
local prefix="$1"
|
|
192
|
+
local initial_query="${2:-}"
|
|
193
|
+
|
|
194
|
+
local query="$initial_query"
|
|
195
|
+
local qcursor=${#query}
|
|
196
|
+
local char
|
|
197
|
+
local hist_idx=$(( ${#_aish_history} + 1 ))
|
|
198
|
+
local saved_query=""
|
|
199
|
+
|
|
200
|
+
_aish_update_display "$prefix" "$query" "$qcursor"
|
|
201
|
+
|
|
202
|
+
while read -k1 char; do
|
|
203
|
+
if [[ "$char" == $'\e' ]]; then
|
|
204
|
+
read -k1 -t 0.1 char2
|
|
205
|
+
if [[ "$char2" == '[' ]]; then
|
|
206
|
+
read -k1 -t 0.1 char3
|
|
207
|
+
case "$char3" in
|
|
208
|
+
A) # Up
|
|
209
|
+
if (( hist_idx > 1 )); then
|
|
210
|
+
(( hist_idx == ${#_aish_history} + 1 )) && saved_query="$query"
|
|
211
|
+
(( hist_idx-- ))
|
|
212
|
+
query="${_aish_history[$hist_idx]}"
|
|
213
|
+
qcursor=${#query}
|
|
214
|
+
fi ;;
|
|
215
|
+
B) # Down
|
|
216
|
+
if (( hist_idx <= ${#_aish_history} )); then
|
|
217
|
+
(( hist_idx++ ))
|
|
218
|
+
if (( hist_idx > ${#_aish_history} )); then
|
|
219
|
+
query="$saved_query"
|
|
220
|
+
else
|
|
221
|
+
query="${_aish_history[$hist_idx]}"
|
|
222
|
+
fi
|
|
223
|
+
qcursor=${#query}
|
|
224
|
+
fi ;;
|
|
225
|
+
C) (( qcursor < ${#query} )) && (( qcursor++ )) ;;
|
|
226
|
+
D) (( qcursor > 0 )) && (( qcursor-- )) ;;
|
|
227
|
+
esac
|
|
228
|
+
_aish_update_display "$prefix" "$query" "$qcursor"
|
|
229
|
+
continue
|
|
230
|
+
else
|
|
231
|
+
_aish_input_query=""
|
|
232
|
+
return 1
|
|
233
|
+
fi
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
case "$char" in
|
|
237
|
+
$'\n'|$'\r')
|
|
238
|
+
_aish_input_query="$query"
|
|
239
|
+
return 0 ;;
|
|
240
|
+
$'\x7f'|$'\b')
|
|
241
|
+
if (( qcursor > 0 )); then
|
|
242
|
+
query="${query:0:$((qcursor-1))}${query:$qcursor}"
|
|
243
|
+
(( qcursor-- ))
|
|
244
|
+
fi ;;
|
|
245
|
+
$'\x03')
|
|
246
|
+
_aish_input_query=""
|
|
247
|
+
return 1 ;;
|
|
248
|
+
$'\x01') qcursor=0 ;;
|
|
249
|
+
$'\x05') qcursor=${#query} ;;
|
|
250
|
+
$'\x15') query=""; qcursor=0 ;;
|
|
251
|
+
*)
|
|
252
|
+
query="${query:0:$qcursor}${char}${query:$qcursor}"
|
|
253
|
+
(( qcursor++ ))
|
|
254
|
+
hist_idx=$(( ${#_aish_history} + 1 )) ;;
|
|
255
|
+
esac
|
|
256
|
+
_aish_update_display "$prefix" "$query" "$qcursor"
|
|
257
|
+
done
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_aish_widget() {
|
|
261
|
+
setopt LOCAL_OPTIONS NO_NOTIFY NO_MONITOR
|
|
262
|
+
|
|
263
|
+
local cmd
|
|
264
|
+
local saved_buffer="$BUFFER"
|
|
265
|
+
local saved_cursor="$CURSOR"
|
|
266
|
+
local saved_region_highlight=("${region_highlight[@]}")
|
|
267
|
+
local context=""
|
|
268
|
+
|
|
269
|
+
while true; do
|
|
270
|
+
local prefix="AI› "
|
|
271
|
+
[[ -n "$context" ]] && prefix="AI+ "
|
|
272
|
+
|
|
273
|
+
if ! _aish_read_input "$prefix" ""; then
|
|
274
|
+
region_highlight=("${saved_region_highlight[@]}")
|
|
275
|
+
BUFFER="$saved_buffer"
|
|
276
|
+
CURSOR="$saved_cursor"
|
|
277
|
+
zle -R
|
|
278
|
+
return
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
local query="$_aish_input_query"
|
|
282
|
+
|
|
283
|
+
if [[ -z "$query" ]]; then
|
|
284
|
+
region_highlight=("${saved_region_highlight[@]}")
|
|
285
|
+
BUFFER="$saved_buffer"
|
|
286
|
+
CURSOR="$saved_cursor"
|
|
287
|
+
zle -R
|
|
288
|
+
return
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
# Build query with context
|
|
292
|
+
local full_query="$query"
|
|
293
|
+
if [[ -n "$context" ]]; then
|
|
294
|
+
full_query="Previous conversation:
|
|
295
|
+
${context}
|
|
296
|
+
|
|
297
|
+
User's new request: $query
|
|
298
|
+
|
|
299
|
+
Respond with only the updated command."
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
# Show spinner in buffer while loading
|
|
303
|
+
local spinner='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
|
|
304
|
+
local spin_i=0
|
|
305
|
+
local tmpfile=$(mktemp)
|
|
306
|
+
local errfile=$(mktemp)
|
|
307
|
+
|
|
308
|
+
# Hide cursor
|
|
309
|
+
print -n $'\e[?25l'
|
|
310
|
+
|
|
311
|
+
aish --print "$full_query" > "$tmpfile" 2>"$errfile" &
|
|
312
|
+
local pid=$!
|
|
313
|
+
|
|
314
|
+
while kill -0 $pid 2>/dev/null; do
|
|
315
|
+
local spin_char="${spinner:$spin_i:1}"
|
|
316
|
+
BUFFER="${prefix}${query} ${spin_char}"
|
|
317
|
+
CURSOR=${#BUFFER}
|
|
318
|
+
region_highlight=("0 ${#prefix} fg=magenta,bold")
|
|
319
|
+
zle -R
|
|
320
|
+
spin_i=$(( (spin_i + 1) % 10 ))
|
|
321
|
+
sleep 0.08
|
|
322
|
+
done
|
|
323
|
+
|
|
324
|
+
# Show cursor
|
|
325
|
+
print -n $'\e[?25h'
|
|
326
|
+
|
|
327
|
+
wait $pid 2>/dev/null
|
|
328
|
+
local exit_code=$?
|
|
329
|
+
cmd=$(<"$tmpfile")
|
|
330
|
+
local stats=$(<"$errfile")
|
|
331
|
+
rm -f "$tmpfile" "$errfile"
|
|
332
|
+
|
|
333
|
+
# Strip ANSI codes from stats for clean display
|
|
334
|
+
stats="${stats//$'\e[2m'/}"
|
|
335
|
+
stats="${stats//$'\e[0m'/}"
|
|
336
|
+
stats="${stats## }" # trim leading space
|
|
337
|
+
stats="${stats%%$'\n'}" # trim trailing newline
|
|
338
|
+
|
|
339
|
+
if [[ $exit_code -ne 0 ]]; then
|
|
340
|
+
# Show error briefly
|
|
341
|
+
BUFFER=""
|
|
342
|
+
POSTDISPLAY=$'\n '"Error: ${stats:-aish command failed}"
|
|
343
|
+
region_highlight=()
|
|
344
|
+
zle -R
|
|
345
|
+
sleep 2
|
|
346
|
+
POSTDISPLAY=""
|
|
347
|
+
region_highlight=("${saved_region_highlight[@]}")
|
|
348
|
+
BUFFER="$saved_buffer"
|
|
349
|
+
CURSOR="$saved_cursor"
|
|
350
|
+
zle -R
|
|
351
|
+
return
|
|
352
|
+
fi
|
|
353
|
+
|
|
354
|
+
if [[ -n "$cmd" ]]; then
|
|
355
|
+
_aish_save_history "$query"
|
|
356
|
+
|
|
357
|
+
# Fill placeholders if any
|
|
358
|
+
if ! _aish_fill_placeholders "$cmd"; then
|
|
359
|
+
# Cancelled during placeholder fill
|
|
360
|
+
region_highlight=("${saved_region_highlight[@]}")
|
|
361
|
+
BUFFER="$saved_buffer"
|
|
362
|
+
CURSOR="$saved_cursor"
|
|
363
|
+
zle -R
|
|
364
|
+
return
|
|
365
|
+
fi
|
|
366
|
+
cmd="$_aish_input_query"
|
|
367
|
+
|
|
368
|
+
if [[ -n "$context" ]]; then
|
|
369
|
+
context="${context}
|
|
370
|
+
|
|
371
|
+
User refinement: ${query}
|
|
372
|
+
Command: ${cmd}"
|
|
373
|
+
else
|
|
374
|
+
context="User request: ${query}
|
|
375
|
+
Command: ${cmd}"
|
|
376
|
+
fi
|
|
377
|
+
|
|
378
|
+
# Show result with hints and stats
|
|
379
|
+
BUFFER="$cmd"
|
|
380
|
+
CURSOR=${#BUFFER}
|
|
381
|
+
local hints=$'\n tab: refine │ enter: accept │ esc: cancel'
|
|
382
|
+
[[ -n "$stats" ]] && hints+=" │ ${stats}"
|
|
383
|
+
POSTDISPLAY="$hints"
|
|
384
|
+
# Highlight POSTDISPLAY area (starts after BUFFER)
|
|
385
|
+
local hint_start=${#BUFFER}
|
|
386
|
+
local hint_end=$(( hint_start + ${#hints} ))
|
|
387
|
+
region_highlight=("${saved_region_highlight[@]}" "${hint_start} ${hint_end} fg=8")
|
|
388
|
+
zle -R
|
|
389
|
+
|
|
390
|
+
# Wait for user choice - read into REPLY (no variable assignment shown)
|
|
391
|
+
zle -R
|
|
392
|
+
read -sk1
|
|
393
|
+
|
|
394
|
+
# Clear hints
|
|
395
|
+
POSTDISPLAY=""
|
|
396
|
+
|
|
397
|
+
# Handle key based on REPLY
|
|
398
|
+
if [[ "$REPLY" == $'\t' ]]; then
|
|
399
|
+
# Tab - refine
|
|
400
|
+
BUFFER=""
|
|
401
|
+
CURSOR=0
|
|
402
|
+
zle -R
|
|
403
|
+
continue
|
|
404
|
+
elif [[ "$REPLY" == $'\e' ]]; then
|
|
405
|
+
region_highlight=("${saved_region_highlight[@]}")
|
|
406
|
+
BUFFER="$saved_buffer"
|
|
407
|
+
CURSOR="$saved_cursor"
|
|
408
|
+
zle -R
|
|
409
|
+
return
|
|
410
|
+
elif [[ "$REPLY" == $'\n' || "$REPLY" == $'\r' ]]; then
|
|
411
|
+
return
|
|
412
|
+
else
|
|
413
|
+
[[ "$REPLY" == [[:print:]] ]] && BUFFER="${cmd}${REPLY}" && CURSOR=${#BUFFER}
|
|
414
|
+
return
|
|
415
|
+
fi
|
|
416
|
+
else
|
|
417
|
+
region_highlight=("${saved_region_highlight[@]}")
|
|
418
|
+
BUFFER="$saved_buffer"
|
|
419
|
+
CURSOR="$saved_cursor"
|
|
420
|
+
zle -R
|
|
421
|
+
return
|
|
422
|
+
fi
|
|
423
|
+
done
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
zle -N _aish_widget
|
|
427
|
+
bindkey '^G' _aish_widget
|
package/dist/index.js
CHANGED
|
@@ -2,61 +2,159 @@
|
|
|
2
2
|
|
|
3
3
|
// src/ai.ts
|
|
4
4
|
import { execFile, spawn } from "child_process";
|
|
5
|
-
import { readFile, unlink } from "fs/promises";
|
|
6
|
-
import {
|
|
5
|
+
import { readFile, writeFile, unlink, access, mkdir } from "fs/promises";
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
import { tmpdir, homedir } from "os";
|
|
7
8
|
import { join } from "path";
|
|
8
9
|
var SYSTEM_PROMPT = `You are a CLI assistant that converts natural language into the exact shell commands to run.
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
1.
|
|
12
|
-
2.
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
GUIDELINES:
|
|
12
|
+
1. For standard commands (git, ls, curl, etc.), respond immediately without reading files.
|
|
13
|
+
2. For project-specific commands (build, test, run scripts, etc.), ALWAYS read README.md first (if it exists), then the relevant config file:
|
|
14
|
+
- Node.js: package.json
|
|
15
|
+
- Python: pyproject.toml, setup.py, or Pipfile
|
|
16
|
+
- Rust: Cargo.toml
|
|
17
|
+
- Go: go.mod
|
|
18
|
+
- Ruby: Gemfile
|
|
19
|
+
- Make: Makefile
|
|
20
|
+
- Other: Justfile, Taskfile.yml
|
|
21
|
+
3. ALWAYS prefer project scripts over direct tool invocation:
|
|
22
|
+
- Use "npm run build" not "tsup" or "tsc"
|
|
23
|
+
- Use "make test" not the underlying test command
|
|
24
|
+
- Use "cargo build" not "rustc" directly
|
|
25
|
+
This ensures correct flags, environment, and project configuration.
|
|
26
|
+
4. If unsure about exact flags, provide multiple options.
|
|
15
27
|
|
|
16
|
-
RESPONSE FORMAT:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
RESPONSE FORMAT \u2014 THIS IS CRITICAL:
|
|
29
|
+
Your final message MUST be ONLY a JSON object. No prose, no explanation, no "Based on...", no markdown.
|
|
30
|
+
Exactly this format: {"commands": ["command1"]}
|
|
31
|
+
If you are unsure which command the user wants, return multiple options and the user will pick: {"commands": ["option1", "option2"]}
|
|
32
|
+
For values the user hasn't specified, use <placeholders> like: git commit -m "<message>" or curl <url>. Use descriptive names inside the angle brackets.`;
|
|
33
|
+
var TOOL_HINTS = [
|
|
34
|
+
{ files: ["bun.lockb", "bun.lock"], hint: "Use bun (not npm/yarn/pnpm)" },
|
|
35
|
+
{ files: ["pnpm-lock.yaml"], hint: "Use pnpm (not npm/yarn)" },
|
|
36
|
+
{ files: ["yarn.lock"], hint: "Use yarn (not npm/pnpm)" },
|
|
37
|
+
{ files: ["package-lock.json"], hint: "Use npm (not yarn/pnpm)" },
|
|
38
|
+
{ files: ["Cargo.lock"], hint: "Use cargo for Rust commands" },
|
|
39
|
+
{ files: ["poetry.lock"], hint: "Use poetry (not pip)" },
|
|
40
|
+
{ files: ["Pipfile.lock"], hint: "Use pipenv (not pip)" },
|
|
41
|
+
{ files: ["uv.lock"], hint: "Use uv (not pip/poetry)" },
|
|
42
|
+
{ files: ["Gemfile.lock"], hint: "Use bundle exec for Ruby commands" },
|
|
43
|
+
{ files: ["go.sum"], hint: "Use go modules" },
|
|
44
|
+
{ files: ["flake.lock"], hint: "Nix flake project - use nix commands" }
|
|
45
|
+
];
|
|
46
|
+
async function gatherProjectContext(cwd) {
|
|
47
|
+
const hints = [];
|
|
48
|
+
for (const { files, hint } of TOOL_HINTS) {
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
try {
|
|
51
|
+
await access(join(cwd, file));
|
|
52
|
+
hints.push(hint);
|
|
53
|
+
break;
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (hints.length > 0) logVerbose(` hints: ${hints.join(", ")}`);
|
|
59
|
+
if (hints.length === 0) return "";
|
|
60
|
+
return `
|
|
61
|
+
|
|
62
|
+
Tool hints:
|
|
63
|
+
- ${hints.join("\n- ")}`;
|
|
64
|
+
}
|
|
20
65
|
var verbose = false;
|
|
21
66
|
function setVerbose(v) {
|
|
22
67
|
verbose = v;
|
|
23
68
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
69
|
+
function tryParseJson(text) {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(text);
|
|
72
|
+
if (typeof parsed.command === "string") {
|
|
73
|
+
return [parsed.command];
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(parsed.commands) && parsed.commands.every((c) => typeof c === "string")) {
|
|
76
|
+
return parsed.commands;
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
28
79
|
}
|
|
29
|
-
return
|
|
30
|
-
execFile(cmd, args, { cwd, maxBuffer: 1024 * 1024, timeout: 12e4 }, (err, stdout, stderr) => {
|
|
31
|
-
if (verbose) {
|
|
32
|
-
if (stderr) console.error(`\x1B[2mstderr: ${stderr}\x1B[0m`);
|
|
33
|
-
if (stdout) console.error(`\x1B[2mstdout: ${stdout.slice(0, 500)}\x1B[0m`);
|
|
34
|
-
if (err) console.error(`\x1B[2merror: ${err.message}\x1B[0m`);
|
|
35
|
-
}
|
|
36
|
-
if (err) reject(err);
|
|
37
|
-
else resolve({ stdout, stderr });
|
|
38
|
-
});
|
|
39
|
-
});
|
|
80
|
+
return null;
|
|
40
81
|
}
|
|
41
|
-
function
|
|
42
|
-
let
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
|
|
82
|
+
function parseResponse(raw) {
|
|
83
|
+
let text = raw.trim();
|
|
84
|
+
let commands = tryParseJson(text);
|
|
85
|
+
if (commands) return { commands };
|
|
86
|
+
const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
87
|
+
if (fenceMatch) {
|
|
88
|
+
commands = tryParseJson(fenceMatch[1].trim());
|
|
89
|
+
if (commands) return { commands };
|
|
48
90
|
}
|
|
49
|
-
|
|
50
|
-
|
|
91
|
+
const jsonMatch = text.match(/\{[\s\S]*"commands?"\s*:[\s\S]*\}/);
|
|
92
|
+
if (jsonMatch) {
|
|
93
|
+
commands = tryParseJson(jsonMatch[0]);
|
|
94
|
+
if (commands) return { commands };
|
|
51
95
|
}
|
|
52
|
-
|
|
96
|
+
const backtickCmds = [...text.matchAll(/`([^`]+)`/g)].map((m) => m[1].trim()).filter((c) => c.length > 2 && !c.includes("{") && !c.startsWith("//"));
|
|
97
|
+
if (backtickCmds.length > 0) return { commands: backtickCmds };
|
|
98
|
+
throw new Error("Could not parse AI response. Run with -v to debug.");
|
|
53
99
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
100
|
+
var DIM = "\x1B[2m";
|
|
101
|
+
var R = "\x1B[0m";
|
|
102
|
+
function logVerbose(msg) {
|
|
103
|
+
if (verbose) console.error(`${DIM}${msg}${R}`);
|
|
104
|
+
}
|
|
105
|
+
function formatStats(wrapper) {
|
|
106
|
+
const lines = [];
|
|
107
|
+
const duration = wrapper.duration_ms;
|
|
108
|
+
const apiDuration = wrapper.duration_api_ms;
|
|
109
|
+
const turns = wrapper.num_turns;
|
|
110
|
+
const cost = wrapper.total_cost_usd;
|
|
111
|
+
const usage = wrapper.usage;
|
|
112
|
+
if (duration != null) {
|
|
113
|
+
const secs = (duration / 1e3).toFixed(1);
|
|
114
|
+
const apiSecs = apiDuration ? ` (api: ${(apiDuration / 1e3).toFixed(1)}s)` : "";
|
|
115
|
+
lines.push(` time: ${secs}s${apiSecs}`);
|
|
116
|
+
}
|
|
117
|
+
if (turns != null) lines.push(` turns: ${turns}`);
|
|
118
|
+
if (cost != null) lines.push(` cost: $${cost.toFixed(4)}`);
|
|
119
|
+
if (usage) {
|
|
120
|
+
const input2 = (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
|
|
121
|
+
const output = usage.output_tokens || 0;
|
|
122
|
+
lines.push(` tokens: ${input2} in / ${output} out`);
|
|
123
|
+
}
|
|
124
|
+
return lines.join("\n");
|
|
125
|
+
}
|
|
126
|
+
var CACHE_DIR = join(homedir(), ".cache", "aish");
|
|
127
|
+
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
128
|
+
function cacheKey(query, context, model) {
|
|
129
|
+
return createHash("sha256").update(`${model}:${query}:${context}`).digest("hex").slice(0, 16);
|
|
130
|
+
}
|
|
131
|
+
async function cacheGet(key) {
|
|
132
|
+
try {
|
|
133
|
+
const filePath = join(CACHE_DIR, `${key}.json`);
|
|
134
|
+
const stat = await access(filePath).then(() => true).catch(() => false);
|
|
135
|
+
if (!stat) return null;
|
|
136
|
+
const raw = await readFile(filePath, "utf-8");
|
|
137
|
+
const entry = JSON.parse(raw);
|
|
138
|
+
if (Date.now() - entry.ts > CACHE_TTL_MS) {
|
|
139
|
+
await unlink(filePath).catch(() => {
|
|
140
|
+
});
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
return entry.result;
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
59
146
|
}
|
|
147
|
+
}
|
|
148
|
+
async function cacheSet(key, result) {
|
|
149
|
+
try {
|
|
150
|
+
await mkdir(CACHE_DIR, { recursive: true });
|
|
151
|
+
await writeFile(join(CACHE_DIR, `${key}.json`), JSON.stringify({ ts: Date.now(), result }));
|
|
152
|
+
} catch {
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function spawnWithStdin(cmd, args, input2, cwd) {
|
|
156
|
+
logVerbose(`$ ${cmd} ${args.join(" ")}`);
|
|
157
|
+
logVerbose(` cwd: ${cwd}`);
|
|
60
158
|
return new Promise((resolve, reject) => {
|
|
61
159
|
const child = spawn(cmd, args, { cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
62
160
|
let stdout = "";
|
|
@@ -64,10 +162,7 @@ function spawnWithStdin(cmd, args, input2, cwd) {
|
|
|
64
162
|
child.stdout.on("data", (d) => stdout += d);
|
|
65
163
|
child.stderr.on("data", (d) => stderr += d);
|
|
66
164
|
child.on("close", (code) => {
|
|
67
|
-
if (verbose) {
|
|
68
|
-
if (stderr) console.error(`\x1B[2mstderr: ${stderr.slice(0, 500)}\x1B[0m`);
|
|
69
|
-
if (stdout) console.error(`\x1B[2mstdout: ${stdout.slice(0, 500)}\x1B[0m`);
|
|
70
|
-
}
|
|
165
|
+
if (verbose && stderr) logVerbose(`stderr: ${stderr.slice(0, 500)}`);
|
|
71
166
|
if (code !== 0) reject(new Error(`${cmd} exited with code ${code}
|
|
72
167
|
${stderr}`));
|
|
73
168
|
else resolve({ stdout, stderr });
|
|
@@ -78,48 +173,80 @@ ${stderr}`));
|
|
|
78
173
|
});
|
|
79
174
|
}
|
|
80
175
|
async function queryClaude(query, cwd, model) {
|
|
81
|
-
const
|
|
176
|
+
const effectiveModel = model || "sonnet";
|
|
177
|
+
const context = await gatherProjectContext(cwd);
|
|
178
|
+
const key = cacheKey(query, context, effectiveModel);
|
|
179
|
+
const cached = await cacheGet(key);
|
|
180
|
+
if (cached) {
|
|
181
|
+
logVerbose(" cache: hit");
|
|
182
|
+
return { ...cached, stats: { cached: true } };
|
|
183
|
+
}
|
|
184
|
+
const prompt = `${SYSTEM_PROMPT}${context}
|
|
82
185
|
|
|
83
186
|
User request: ${query}`;
|
|
84
187
|
const args = [
|
|
85
188
|
"-p",
|
|
86
189
|
"--output-format",
|
|
87
|
-
"json"
|
|
190
|
+
"json",
|
|
191
|
+
"--model",
|
|
192
|
+
effectiveModel,
|
|
193
|
+
"--allowedTools",
|
|
194
|
+
"Read,Glob"
|
|
88
195
|
];
|
|
89
|
-
if (model) {
|
|
90
|
-
args.push("--model", model);
|
|
91
|
-
} else {
|
|
92
|
-
args.push("--model", "sonnet");
|
|
93
|
-
}
|
|
94
196
|
const { stdout } = await spawnWithStdin("claude", args, prompt, cwd);
|
|
95
197
|
let text = stdout.trim();
|
|
198
|
+
let stats = {};
|
|
96
199
|
try {
|
|
97
200
|
const wrapper = JSON.parse(text);
|
|
201
|
+
if (verbose) {
|
|
202
|
+
logVerbose(formatStats(wrapper));
|
|
203
|
+
if (wrapper.result) logVerbose(` result: ${wrapper.result}`);
|
|
204
|
+
}
|
|
205
|
+
stats.durationMs = wrapper.duration_ms;
|
|
206
|
+
stats.cost = wrapper.total_cost_usd;
|
|
207
|
+
if (wrapper.usage) {
|
|
208
|
+
stats.inputTokens = (wrapper.usage.input_tokens || 0) + (wrapper.usage.cache_creation_input_tokens || 0) + (wrapper.usage.cache_read_input_tokens || 0);
|
|
209
|
+
stats.outputTokens = wrapper.usage.output_tokens || 0;
|
|
210
|
+
}
|
|
98
211
|
if (wrapper.result) {
|
|
99
212
|
text = wrapper.result;
|
|
100
213
|
}
|
|
101
214
|
} catch {
|
|
215
|
+
if (verbose) logVerbose(` raw: ${text.slice(0, 500)}`);
|
|
102
216
|
}
|
|
103
|
-
|
|
217
|
+
const result = parseResponse(text);
|
|
218
|
+
result.cacheKey = key;
|
|
219
|
+
result.stats = stats;
|
|
220
|
+
return result;
|
|
104
221
|
}
|
|
105
222
|
async function queryCodex(query, cwd, model) {
|
|
223
|
+
const startTime = Date.now();
|
|
224
|
+
const context = await gatherProjectContext(cwd);
|
|
106
225
|
const resultFile = join(tmpdir(), `aish-codex-${Date.now()}.txt`);
|
|
226
|
+
const prompt = `${SYSTEM_PROMPT}${context}
|
|
227
|
+
|
|
228
|
+
User request: ${query}`;
|
|
107
229
|
const args = [
|
|
108
230
|
"exec",
|
|
231
|
+
"--sandbox",
|
|
232
|
+
"read-only",
|
|
233
|
+
"-c",
|
|
234
|
+
'model_reasoning_effort="low"',
|
|
109
235
|
"-o",
|
|
110
236
|
resultFile,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
User request: ${query}`
|
|
237
|
+
"-"
|
|
238
|
+
// Read prompt from stdin
|
|
114
239
|
];
|
|
115
240
|
if (model) {
|
|
116
241
|
args.push("--model", model);
|
|
117
242
|
}
|
|
118
|
-
await
|
|
243
|
+
await spawnWithStdin("codex", args, prompt, cwd);
|
|
119
244
|
const text = await readFile(resultFile, "utf-8");
|
|
120
245
|
await unlink(resultFile).catch(() => {
|
|
121
246
|
});
|
|
122
|
-
|
|
247
|
+
const result = parseResponse(text);
|
|
248
|
+
result.stats = { durationMs: Date.now() - startTime };
|
|
249
|
+
return result;
|
|
123
250
|
}
|
|
124
251
|
async function queryAi(provider, query, cwd, model) {
|
|
125
252
|
if (provider === "codex") {
|
|
@@ -129,39 +256,75 @@ async function queryAi(provider, query, cwd, model) {
|
|
|
129
256
|
}
|
|
130
257
|
|
|
131
258
|
// src/ui.ts
|
|
259
|
+
import { createInterface } from "readline";
|
|
132
260
|
import { select, input } from "@inquirer/prompts";
|
|
133
261
|
var CYAN = "\x1B[36m";
|
|
262
|
+
var DIM2 = "\x1B[2m";
|
|
263
|
+
var YELLOW = "\x1B[33m";
|
|
134
264
|
var RESET = "\x1B[0m";
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
265
|
+
var PLACEHOLDER_RE = /<([^>]+)>/g;
|
|
266
|
+
async function fillPlaceholders(cmd) {
|
|
267
|
+
const placeholders = [...cmd.matchAll(PLACEHOLDER_RE)];
|
|
268
|
+
if (placeholders.length === 0) return cmd;
|
|
269
|
+
console.log(`${DIM2} Fill in the placeholders:${RESET}`);
|
|
270
|
+
let result = cmd;
|
|
271
|
+
const seen = /* @__PURE__ */ new Set();
|
|
272
|
+
for (const match of placeholders) {
|
|
273
|
+
const full = match[0];
|
|
274
|
+
const name = match[1];
|
|
275
|
+
if (seen.has(full)) continue;
|
|
276
|
+
seen.add(full);
|
|
277
|
+
const value = await input({
|
|
278
|
+
message: `${YELLOW}${name}${RESET}`
|
|
279
|
+
});
|
|
280
|
+
if (!value.trim()) return null;
|
|
281
|
+
result = result.replaceAll(full, value);
|
|
282
|
+
}
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
function promptEdit(cmd) {
|
|
286
|
+
return new Promise((resolve) => {
|
|
287
|
+
const rl = createInterface({
|
|
288
|
+
input: process.stdin,
|
|
289
|
+
output: process.stdout,
|
|
290
|
+
terminal: true
|
|
291
|
+
});
|
|
292
|
+
rl.write(cmd);
|
|
293
|
+
rl.on("line", (line) => {
|
|
294
|
+
rl.close();
|
|
295
|
+
const trimmed = line.trim();
|
|
296
|
+
resolve(trimmed || null);
|
|
297
|
+
});
|
|
298
|
+
rl.on("close", () => resolve(null));
|
|
139
299
|
});
|
|
140
|
-
const trimmed = edited.trim();
|
|
141
|
-
return trimmed || null;
|
|
142
300
|
}
|
|
143
301
|
async function promptAction(cmd) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
`);
|
|
302
|
+
const hasPlaceholders = PLACEHOLDER_RE.test(cmd);
|
|
303
|
+
PLACEHOLDER_RE.lastIndex = 0;
|
|
147
304
|
const action = await select({
|
|
148
305
|
message: "Action:",
|
|
149
306
|
choices: [
|
|
150
|
-
{ name: "Run", value: "run" },
|
|
151
|
-
{ name:
|
|
307
|
+
{ name: hasPlaceholders ? "Run (fill placeholders)" : "Run", value: "run" },
|
|
308
|
+
{ name: `Edit ${DIM2}(modify command before running)${RESET}`, value: "edit" },
|
|
152
309
|
{ name: "Cancel", value: "cancel" }
|
|
153
310
|
]
|
|
154
311
|
});
|
|
155
|
-
if (action === "run") return cmd;
|
|
156
|
-
if (action === "edit")
|
|
312
|
+
if (action === "run") return fillPlaceholders(cmd);
|
|
313
|
+
if (action === "edit") {
|
|
314
|
+
process.stdout.write(`${DIM2}> ${RESET}`);
|
|
315
|
+
return promptEdit(cmd);
|
|
316
|
+
}
|
|
157
317
|
return null;
|
|
158
318
|
}
|
|
159
319
|
async function promptCommand(commands) {
|
|
160
320
|
if (commands.length === 1) {
|
|
321
|
+
console.log(`
|
|
322
|
+
${CYAN}${commands[0]}${RESET}
|
|
323
|
+
`);
|
|
161
324
|
return promptAction(commands[0]);
|
|
162
325
|
}
|
|
163
326
|
const choice = await select({
|
|
164
|
-
message: "Select a command
|
|
327
|
+
message: "Select a command:",
|
|
165
328
|
choices: [
|
|
166
329
|
...commands.map((cmd) => ({
|
|
167
330
|
name: `${CYAN}${cmd}${RESET}`,
|
|
@@ -171,6 +334,9 @@ async function promptCommand(commands) {
|
|
|
171
334
|
]
|
|
172
335
|
});
|
|
173
336
|
if (choice === "__cancel__") return null;
|
|
337
|
+
console.log(`
|
|
338
|
+
${CYAN}${choice}${RESET}
|
|
339
|
+
`);
|
|
174
340
|
return promptAction(choice);
|
|
175
341
|
}
|
|
176
342
|
|
|
@@ -190,13 +356,13 @@ function execCommand(cmd, cwd) {
|
|
|
190
356
|
|
|
191
357
|
// src/index.ts
|
|
192
358
|
var BRAILLE = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
193
|
-
var
|
|
359
|
+
var DIM3 = "\x1B[2m";
|
|
194
360
|
var RESET2 = "\x1B[0m";
|
|
195
361
|
var RED = "\x1B[31m";
|
|
196
362
|
function startSpinner(message) {
|
|
197
363
|
let i = 0;
|
|
198
364
|
const interval = setInterval(() => {
|
|
199
|
-
process.stderr.write(`\r${
|
|
365
|
+
process.stderr.write(`\r${DIM3}${BRAILLE[i++ % BRAILLE.length]} ${message}${RESET2}`);
|
|
200
366
|
}, 80);
|
|
201
367
|
return () => {
|
|
202
368
|
clearInterval(interval);
|
|
@@ -209,6 +375,7 @@ function parseArgs(argv) {
|
|
|
209
375
|
let cwd = process.cwd();
|
|
210
376
|
let model = process.env.AISH_MODEL || void 0;
|
|
211
377
|
let verbose2 = false;
|
|
378
|
+
let print = false;
|
|
212
379
|
const queryParts = [];
|
|
213
380
|
for (let i = 0; i < args.length; i++) {
|
|
214
381
|
const arg = args[i];
|
|
@@ -225,6 +392,8 @@ function parseArgs(argv) {
|
|
|
225
392
|
model = args[++i];
|
|
226
393
|
} else if (arg === "-v" || arg === "--verbose") {
|
|
227
394
|
verbose2 = true;
|
|
395
|
+
} else if (arg === "--print") {
|
|
396
|
+
print = true;
|
|
228
397
|
} else if (arg === "-h" || arg === "--help") {
|
|
229
398
|
console.log(`Usage: aish [options] <query...>
|
|
230
399
|
|
|
@@ -232,38 +401,74 @@ Options:
|
|
|
232
401
|
-p, --provider <claude|codex> AI provider (default: claude, env: AISH_PROVIDER)
|
|
233
402
|
-m, --model <model> Model override (env: AISH_MODEL)
|
|
234
403
|
--cwd <dir> Working directory
|
|
404
|
+
--print Output command only (for shell integration)
|
|
235
405
|
-v, --verbose Show debug output
|
|
236
|
-
-h, --help Show help
|
|
406
|
+
-h, --help Show help
|
|
407
|
+
|
|
408
|
+
Zsh Integration:
|
|
409
|
+
Add to .zshrc: source "$(npm root -g)/aish-cli/aish.plugin.zsh"
|
|
410
|
+
Then press Ctrl+G to activate AI mode`);
|
|
237
411
|
process.exit(0);
|
|
238
412
|
} else {
|
|
239
413
|
queryParts.push(arg);
|
|
240
414
|
}
|
|
241
415
|
}
|
|
242
|
-
return { query: queryParts.join(" "), provider, cwd, model, verbose: verbose2 };
|
|
416
|
+
return { query: queryParts.join(" "), provider, cwd, model, verbose: verbose2, print };
|
|
243
417
|
}
|
|
244
418
|
async function main() {
|
|
245
|
-
const { query, provider, cwd, model, verbose: verbose2 } = parseArgs(process.argv);
|
|
419
|
+
const { query, provider, cwd, model, verbose: verbose2, print } = parseArgs(process.argv);
|
|
246
420
|
setVerbose(verbose2);
|
|
247
421
|
if (!query) {
|
|
248
422
|
console.error(`${RED}Usage: aish <query>${RESET2}`);
|
|
249
423
|
process.exit(1);
|
|
250
424
|
}
|
|
251
|
-
const stopSpinner = verbose2 ? () => {
|
|
425
|
+
const stopSpinner = verbose2 || print ? () => {
|
|
252
426
|
} : startSpinner("Thinking...");
|
|
253
427
|
let commands;
|
|
428
|
+
let resultCacheKey;
|
|
429
|
+
let stats;
|
|
254
430
|
try {
|
|
255
431
|
const result = await queryAi(provider, query, cwd, model);
|
|
256
432
|
commands = result.commands;
|
|
433
|
+
resultCacheKey = result.cacheKey;
|
|
434
|
+
stats = result.stats;
|
|
257
435
|
} catch (err) {
|
|
258
436
|
stopSpinner();
|
|
437
|
+
if (print) {
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
259
440
|
console.error(`${RED}Error: ${err.message}${RESET2}`);
|
|
260
441
|
process.exit(1);
|
|
261
442
|
}
|
|
262
443
|
stopSpinner();
|
|
444
|
+
if (stats) {
|
|
445
|
+
const parts = [];
|
|
446
|
+
if (stats.cached) {
|
|
447
|
+
parts.push("cached");
|
|
448
|
+
} else {
|
|
449
|
+
if (stats.durationMs) parts.push(`${(stats.durationMs / 1e3).toFixed(1)}s`);
|
|
450
|
+
if (stats.inputTokens || stats.outputTokens) {
|
|
451
|
+
parts.push(`${stats.inputTokens || 0}\u2192${stats.outputTokens || 0} tok`);
|
|
452
|
+
}
|
|
453
|
+
if (stats.cost) parts.push(`$${stats.cost.toFixed(4)}`);
|
|
454
|
+
}
|
|
455
|
+
if (parts.length > 0) {
|
|
456
|
+
const output = print ? process.stderr : process.stdout;
|
|
457
|
+
output.write(`${DIM3} ${parts.join(" \xB7 ")}${RESET2}
|
|
458
|
+
`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
263
461
|
if (commands.length === 0) {
|
|
462
|
+
if (print) {
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
264
465
|
console.error(`${RED}No commands suggested.${RESET2}`);
|
|
265
466
|
process.exit(1);
|
|
266
467
|
}
|
|
468
|
+
if (print) {
|
|
469
|
+
console.log(commands[0]);
|
|
470
|
+
process.exit(0);
|
|
471
|
+
}
|
|
267
472
|
let chosen;
|
|
268
473
|
try {
|
|
269
474
|
chosen = await promptCommand(commands);
|
|
@@ -274,6 +479,9 @@ async function main() {
|
|
|
274
479
|
if (!chosen) {
|
|
275
480
|
process.exit(0);
|
|
276
481
|
}
|
|
482
|
+
if (resultCacheKey) {
|
|
483
|
+
await cacheSet(resultCacheKey, { commands });
|
|
484
|
+
}
|
|
277
485
|
const exitCode = await execCommand(chosen, cwd);
|
|
278
486
|
process.exit(exitCode);
|
|
279
487
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aish-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "AI Shell - convert natural language to bash commands using Claude or Codex",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,11 +8,22 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/janicduplessis/aish.git"
|
|
10
10
|
},
|
|
11
|
-
"keywords": [
|
|
11
|
+
"keywords": [
|
|
12
|
+
"ai",
|
|
13
|
+
"shell",
|
|
14
|
+
"cli",
|
|
15
|
+
"claude",
|
|
16
|
+
"codex",
|
|
17
|
+
"bash",
|
|
18
|
+
"natural-language"
|
|
19
|
+
],
|
|
12
20
|
"bin": {
|
|
13
21
|
"aish": "./dist/index.js"
|
|
14
22
|
},
|
|
15
|
-
"files": [
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"aish.plugin.zsh"
|
|
26
|
+
],
|
|
16
27
|
"scripts": {
|
|
17
28
|
"build": "tsup",
|
|
18
29
|
"dev": "tsx src/index.ts",
|