aish-cli 1.0.0 → 1.1.0
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 +420 -0
- package/dist/index.js +251 -78
- package/package.json +14 -3
package/aish.plugin.zsh
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
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 errmsg=$(<"$errfile")
|
|
331
|
+
rm -f "$tmpfile" "$errfile"
|
|
332
|
+
|
|
333
|
+
if [[ $exit_code -ne 0 ]]; then
|
|
334
|
+
# Show error briefly
|
|
335
|
+
BUFFER=""
|
|
336
|
+
POSTDISPLAY=$'\n '"Error: ${errmsg:-aish command failed}"
|
|
337
|
+
region_highlight=()
|
|
338
|
+
zle -R
|
|
339
|
+
sleep 2
|
|
340
|
+
POSTDISPLAY=""
|
|
341
|
+
region_highlight=("${saved_region_highlight[@]}")
|
|
342
|
+
BUFFER="$saved_buffer"
|
|
343
|
+
CURSOR="$saved_cursor"
|
|
344
|
+
zle -R
|
|
345
|
+
return
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
if [[ -n "$cmd" ]]; then
|
|
349
|
+
_aish_save_history "$query"
|
|
350
|
+
|
|
351
|
+
# Fill placeholders if any
|
|
352
|
+
if ! _aish_fill_placeholders "$cmd"; then
|
|
353
|
+
# Cancelled during placeholder fill
|
|
354
|
+
region_highlight=("${saved_region_highlight[@]}")
|
|
355
|
+
BUFFER="$saved_buffer"
|
|
356
|
+
CURSOR="$saved_cursor"
|
|
357
|
+
zle -R
|
|
358
|
+
return
|
|
359
|
+
fi
|
|
360
|
+
cmd="$_aish_input_query"
|
|
361
|
+
|
|
362
|
+
if [[ -n "$context" ]]; then
|
|
363
|
+
context="${context}
|
|
364
|
+
|
|
365
|
+
User refinement: ${query}
|
|
366
|
+
Command: ${cmd}"
|
|
367
|
+
else
|
|
368
|
+
context="User request: ${query}
|
|
369
|
+
Command: ${cmd}"
|
|
370
|
+
fi
|
|
371
|
+
|
|
372
|
+
# Show result with hints
|
|
373
|
+
BUFFER="$cmd"
|
|
374
|
+
CURSOR=${#BUFFER}
|
|
375
|
+
local hints=$'\n tab: refine │ enter: accept │ esc: cancel'
|
|
376
|
+
POSTDISPLAY="$hints"
|
|
377
|
+
# Highlight POSTDISPLAY area (starts after BUFFER)
|
|
378
|
+
local hint_start=${#BUFFER}
|
|
379
|
+
local hint_end=$(( hint_start + ${#hints} ))
|
|
380
|
+
region_highlight=("${saved_region_highlight[@]}" "${hint_start} ${hint_end} fg=8")
|
|
381
|
+
zle -R
|
|
382
|
+
|
|
383
|
+
# Wait for user choice - read into REPLY (no variable assignment shown)
|
|
384
|
+
zle -R
|
|
385
|
+
read -sk1
|
|
386
|
+
|
|
387
|
+
# Clear hints
|
|
388
|
+
POSTDISPLAY=""
|
|
389
|
+
|
|
390
|
+
# Handle key based on REPLY
|
|
391
|
+
if [[ "$REPLY" == $'\t' ]]; then
|
|
392
|
+
# Tab - refine
|
|
393
|
+
BUFFER=""
|
|
394
|
+
CURSOR=0
|
|
395
|
+
zle -R
|
|
396
|
+
continue
|
|
397
|
+
elif [[ "$REPLY" == $'\e' ]]; then
|
|
398
|
+
region_highlight=("${saved_region_highlight[@]}")
|
|
399
|
+
BUFFER="$saved_buffer"
|
|
400
|
+
CURSOR="$saved_cursor"
|
|
401
|
+
zle -R
|
|
402
|
+
return
|
|
403
|
+
elif [[ "$REPLY" == $'\n' || "$REPLY" == $'\r' ]]; then
|
|
404
|
+
return
|
|
405
|
+
else
|
|
406
|
+
[[ "$REPLY" == [[:print:]] ]] && BUFFER="${cmd}${REPLY}" && CURSOR=${#BUFFER}
|
|
407
|
+
return
|
|
408
|
+
fi
|
|
409
|
+
else
|
|
410
|
+
region_highlight=("${saved_region_highlight[@]}")
|
|
411
|
+
BUFFER="$saved_buffer"
|
|
412
|
+
CURSOR="$saved_cursor"
|
|
413
|
+
zle -R
|
|
414
|
+
return
|
|
415
|
+
fi
|
|
416
|
+
done
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
zle -N _aish_widget
|
|
420
|
+
bindkey '^G' _aish_widget
|
package/dist/index.js
CHANGED
|
@@ -2,61 +2,156 @@
|
|
|
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. Read the README.md (or README) file thoroughly \u2014 it often documents available commands, flags, and workflows.
|
|
12
|
-
2. Read the Makefile, package.json scripts, Justfile, Taskfile.yml, docker-compose.yml, Cargo.toml, or pyproject.toml \u2014 whichever exist \u2014 to find available targets/scripts.
|
|
13
|
-
3. When the user's request maps to a specific command or script, run it with --help to discover the exact flags and options available.
|
|
14
|
-
4. Cross-reference what you found: use the exact flag names and syntax from --help output and documentation, not guesses.
|
|
11
|
+
Some project files are provided below for reference so you don't need to read them yourself.
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Prefer existing scripts/targets
|
|
13
|
+
GUIDELINES:
|
|
14
|
+
1. Review the project files provided below to understand available scripts and commands.
|
|
15
|
+
2. If you need more context, use the Read tool to look at relevant files (scripts, configs, docs).
|
|
16
|
+
3. Prefer existing scripts/targets from package.json, Makefile, etc. over raw commands.
|
|
17
|
+
4. If unsure about exact flags, use common/standard ones or provide multiple options.
|
|
18
|
+
|
|
19
|
+
RESPONSE FORMAT \u2014 THIS IS CRITICAL:
|
|
20
|
+
Your final message MUST be ONLY a JSON object. No prose, no explanation, no "Based on...", no markdown.
|
|
21
|
+
Exactly this format: {"commands": ["command1"]}
|
|
22
|
+
If you are unsure which command the user wants, return multiple options and the user will pick: {"commands": ["option1", "option2"]}
|
|
23
|
+
Prefer existing scripts/targets with correct flags over raw commands.
|
|
24
|
+
For values the user hasn't specified, use <placeholders> like: git commit -m "<message>" or curl <url>. Use descriptive names inside the angle brackets.`;
|
|
25
|
+
var PROJECT_FILES = [
|
|
26
|
+
"Makefile",
|
|
27
|
+
"package.json",
|
|
28
|
+
"README.md",
|
|
29
|
+
"Justfile",
|
|
30
|
+
"Taskfile.yml",
|
|
31
|
+
"docker-compose.yml",
|
|
32
|
+
"Cargo.toml",
|
|
33
|
+
"pyproject.toml",
|
|
34
|
+
"Gemfile"
|
|
35
|
+
];
|
|
36
|
+
var MAX_FILE_SIZE = 4e3;
|
|
37
|
+
async function gatherProjectContext(cwd) {
|
|
38
|
+
const sections = [];
|
|
39
|
+
const found = [];
|
|
40
|
+
for (const file of PROJECT_FILES) {
|
|
41
|
+
const filePath = join(cwd, file);
|
|
42
|
+
try {
|
|
43
|
+
await access(filePath);
|
|
44
|
+
let content = await readFile(filePath, "utf-8");
|
|
45
|
+
if (content.length > MAX_FILE_SIZE) {
|
|
46
|
+
content = content.slice(0, MAX_FILE_SIZE) + "\n...(truncated)";
|
|
47
|
+
}
|
|
48
|
+
sections.push(`--- ${file} ---
|
|
49
|
+
${content}`);
|
|
50
|
+
found.push(file);
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (found.length > 0) logVerbose(` context: ${found.join(", ")}`);
|
|
55
|
+
if (sections.length === 0) return "";
|
|
56
|
+
return `
|
|
57
|
+
|
|
58
|
+
Project files:
|
|
59
|
+
|
|
60
|
+
${sections.join("\n\n")}`;
|
|
61
|
+
}
|
|
20
62
|
var verbose = false;
|
|
21
63
|
function setVerbose(v) {
|
|
22
64
|
verbose = v;
|
|
23
65
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
66
|
+
function tryParseJson(text) {
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(text);
|
|
69
|
+
if (typeof parsed.command === "string") {
|
|
70
|
+
return [parsed.command];
|
|
71
|
+
}
|
|
72
|
+
if (Array.isArray(parsed.commands) && parsed.commands.every((c) => typeof c === "string")) {
|
|
73
|
+
return parsed.commands;
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
28
76
|
}
|
|
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
|
-
});
|
|
77
|
+
return null;
|
|
40
78
|
}
|
|
41
|
-
function
|
|
42
|
-
let
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
|
|
79
|
+
function parseResponse(raw) {
|
|
80
|
+
let text = raw.trim();
|
|
81
|
+
let commands = tryParseJson(text);
|
|
82
|
+
if (commands) return { commands };
|
|
83
|
+
const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
84
|
+
if (fenceMatch) {
|
|
85
|
+
commands = tryParseJson(fenceMatch[1].trim());
|
|
86
|
+
if (commands) return { commands };
|
|
48
87
|
}
|
|
49
|
-
|
|
50
|
-
|
|
88
|
+
const jsonMatch = text.match(/\{[\s\S]*"commands?"\s*:[\s\S]*\}/);
|
|
89
|
+
if (jsonMatch) {
|
|
90
|
+
commands = tryParseJson(jsonMatch[0]);
|
|
91
|
+
if (commands) return { commands };
|
|
51
92
|
}
|
|
52
|
-
|
|
93
|
+
const backtickCmds = [...text.matchAll(/`([^`]+)`/g)].map((m) => m[1].trim()).filter((c) => c.length > 2 && !c.includes("{") && !c.startsWith("//"));
|
|
94
|
+
if (backtickCmds.length > 0) return { commands: backtickCmds };
|
|
95
|
+
throw new Error("Could not parse AI response. Run with -v to debug.");
|
|
53
96
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
97
|
+
var DIM = "\x1B[2m";
|
|
98
|
+
var R = "\x1B[0m";
|
|
99
|
+
function logVerbose(msg) {
|
|
100
|
+
if (verbose) console.error(`${DIM}${msg}${R}`);
|
|
101
|
+
}
|
|
102
|
+
function formatStats(wrapper) {
|
|
103
|
+
const lines = [];
|
|
104
|
+
const duration = wrapper.duration_ms;
|
|
105
|
+
const apiDuration = wrapper.duration_api_ms;
|
|
106
|
+
const turns = wrapper.num_turns;
|
|
107
|
+
const cost = wrapper.total_cost_usd;
|
|
108
|
+
const usage = wrapper.usage;
|
|
109
|
+
if (duration != null) {
|
|
110
|
+
const secs = (duration / 1e3).toFixed(1);
|
|
111
|
+
const apiSecs = apiDuration ? ` (api: ${(apiDuration / 1e3).toFixed(1)}s)` : "";
|
|
112
|
+
lines.push(` time: ${secs}s${apiSecs}`);
|
|
113
|
+
}
|
|
114
|
+
if (turns != null) lines.push(` turns: ${turns}`);
|
|
115
|
+
if (cost != null) lines.push(` cost: $${cost.toFixed(4)}`);
|
|
116
|
+
if (usage) {
|
|
117
|
+
const input2 = (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
|
|
118
|
+
const output = usage.output_tokens || 0;
|
|
119
|
+
lines.push(` tokens: ${input2} in / ${output} out`);
|
|
120
|
+
}
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
123
|
+
var CACHE_DIR = join(homedir(), ".cache", "aish");
|
|
124
|
+
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
125
|
+
function cacheKey(query, context, model) {
|
|
126
|
+
return createHash("sha256").update(`${model}:${query}:${context}`).digest("hex").slice(0, 16);
|
|
127
|
+
}
|
|
128
|
+
async function cacheGet(key) {
|
|
129
|
+
try {
|
|
130
|
+
const filePath = join(CACHE_DIR, `${key}.json`);
|
|
131
|
+
const stat = await access(filePath).then(() => true).catch(() => false);
|
|
132
|
+
if (!stat) return null;
|
|
133
|
+
const raw = await readFile(filePath, "utf-8");
|
|
134
|
+
const entry = JSON.parse(raw);
|
|
135
|
+
if (Date.now() - entry.ts > CACHE_TTL_MS) {
|
|
136
|
+
await unlink(filePath).catch(() => {
|
|
137
|
+
});
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return entry.result;
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function cacheSet(key, result) {
|
|
146
|
+
try {
|
|
147
|
+
await mkdir(CACHE_DIR, { recursive: true });
|
|
148
|
+
await writeFile(join(CACHE_DIR, `${key}.json`), JSON.stringify({ ts: Date.now(), result }));
|
|
149
|
+
} catch {
|
|
59
150
|
}
|
|
151
|
+
}
|
|
152
|
+
function spawnWithStdin(cmd, args, input2, cwd) {
|
|
153
|
+
logVerbose(`$ ${cmd} ${args.join(" ")}`);
|
|
154
|
+
logVerbose(` cwd: ${cwd}`);
|
|
60
155
|
return new Promise((resolve, reject) => {
|
|
61
156
|
const child = spawn(cmd, args, { cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
62
157
|
let stdout = "";
|
|
@@ -64,10 +159,7 @@ function spawnWithStdin(cmd, args, input2, cwd) {
|
|
|
64
159
|
child.stdout.on("data", (d) => stdout += d);
|
|
65
160
|
child.stderr.on("data", (d) => stderr += d);
|
|
66
161
|
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
|
-
}
|
|
162
|
+
if (verbose && stderr) logVerbose(`stderr: ${stderr.slice(0, 500)}`);
|
|
71
163
|
if (code !== 0) reject(new Error(`${cmd} exited with code ${code}
|
|
72
164
|
${stderr}`));
|
|
73
165
|
else resolve({ stdout, stderr });
|
|
@@ -78,48 +170,67 @@ ${stderr}`));
|
|
|
78
170
|
});
|
|
79
171
|
}
|
|
80
172
|
async function queryClaude(query, cwd, model) {
|
|
81
|
-
const
|
|
173
|
+
const effectiveModel = model || "sonnet";
|
|
174
|
+
const context = await gatherProjectContext(cwd);
|
|
175
|
+
const key = cacheKey(query, context, effectiveModel);
|
|
176
|
+
const cached = await cacheGet(key);
|
|
177
|
+
if (cached) {
|
|
178
|
+
logVerbose(" cache: hit");
|
|
179
|
+
return cached;
|
|
180
|
+
}
|
|
181
|
+
const prompt = `${SYSTEM_PROMPT}${context}
|
|
82
182
|
|
|
83
183
|
User request: ${query}`;
|
|
84
184
|
const args = [
|
|
85
185
|
"-p",
|
|
86
186
|
"--output-format",
|
|
87
|
-
"json"
|
|
187
|
+
"json",
|
|
188
|
+
"--model",
|
|
189
|
+
effectiveModel,
|
|
190
|
+
"--allowedTools",
|
|
191
|
+
"Read,Glob"
|
|
88
192
|
];
|
|
89
|
-
if (model) {
|
|
90
|
-
args.push("--model", model);
|
|
91
|
-
} else {
|
|
92
|
-
args.push("--model", "sonnet");
|
|
93
|
-
}
|
|
94
193
|
const { stdout } = await spawnWithStdin("claude", args, prompt, cwd);
|
|
95
194
|
let text = stdout.trim();
|
|
96
195
|
try {
|
|
97
196
|
const wrapper = JSON.parse(text);
|
|
197
|
+
if (verbose) {
|
|
198
|
+
logVerbose(formatStats(wrapper));
|
|
199
|
+
if (wrapper.result) logVerbose(` result: ${wrapper.result}`);
|
|
200
|
+
}
|
|
98
201
|
if (wrapper.result) {
|
|
99
202
|
text = wrapper.result;
|
|
100
203
|
}
|
|
101
204
|
} catch {
|
|
205
|
+
if (verbose) logVerbose(` raw: ${text.slice(0, 500)}`);
|
|
102
206
|
}
|
|
103
|
-
|
|
207
|
+
const result = parseResponse(text);
|
|
208
|
+
result.cacheKey = key;
|
|
209
|
+
return result;
|
|
104
210
|
}
|
|
105
211
|
async function queryCodex(query, cwd, model) {
|
|
212
|
+
const context = await gatherProjectContext(cwd);
|
|
106
213
|
const resultFile = join(tmpdir(), `aish-codex-${Date.now()}.txt`);
|
|
214
|
+
const prompt = `${SYSTEM_PROMPT}${context}
|
|
215
|
+
|
|
216
|
+
User request: ${query}`;
|
|
107
217
|
const args = [
|
|
108
218
|
"exec",
|
|
219
|
+
"--sandbox",
|
|
220
|
+
"read-only",
|
|
109
221
|
"-o",
|
|
110
222
|
resultFile,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
User request: ${query}`
|
|
223
|
+
"-"
|
|
224
|
+
// Read prompt from stdin
|
|
114
225
|
];
|
|
115
226
|
if (model) {
|
|
116
227
|
args.push("--model", model);
|
|
117
228
|
}
|
|
118
|
-
await
|
|
229
|
+
await spawnWithStdin("codex", args, prompt, cwd);
|
|
119
230
|
const text = await readFile(resultFile, "utf-8");
|
|
120
231
|
await unlink(resultFile).catch(() => {
|
|
121
232
|
});
|
|
122
|
-
return
|
|
233
|
+
return parseResponse(text);
|
|
123
234
|
}
|
|
124
235
|
async function queryAi(provider, query, cwd, model) {
|
|
125
236
|
if (provider === "codex") {
|
|
@@ -129,39 +240,75 @@ async function queryAi(provider, query, cwd, model) {
|
|
|
129
240
|
}
|
|
130
241
|
|
|
131
242
|
// src/ui.ts
|
|
243
|
+
import { createInterface } from "readline";
|
|
132
244
|
import { select, input } from "@inquirer/prompts";
|
|
133
245
|
var CYAN = "\x1B[36m";
|
|
246
|
+
var DIM2 = "\x1B[2m";
|
|
247
|
+
var YELLOW = "\x1B[33m";
|
|
134
248
|
var RESET = "\x1B[0m";
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
249
|
+
var PLACEHOLDER_RE = /<([^>]+)>/g;
|
|
250
|
+
async function fillPlaceholders(cmd) {
|
|
251
|
+
const placeholders = [...cmd.matchAll(PLACEHOLDER_RE)];
|
|
252
|
+
if (placeholders.length === 0) return cmd;
|
|
253
|
+
console.log(`${DIM2} Fill in the placeholders:${RESET}`);
|
|
254
|
+
let result = cmd;
|
|
255
|
+
const seen = /* @__PURE__ */ new Set();
|
|
256
|
+
for (const match of placeholders) {
|
|
257
|
+
const full = match[0];
|
|
258
|
+
const name = match[1];
|
|
259
|
+
if (seen.has(full)) continue;
|
|
260
|
+
seen.add(full);
|
|
261
|
+
const value = await input({
|
|
262
|
+
message: `${YELLOW}${name}${RESET}`
|
|
263
|
+
});
|
|
264
|
+
if (!value.trim()) return null;
|
|
265
|
+
result = result.replaceAll(full, value);
|
|
266
|
+
}
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
function promptEdit(cmd) {
|
|
270
|
+
return new Promise((resolve) => {
|
|
271
|
+
const rl = createInterface({
|
|
272
|
+
input: process.stdin,
|
|
273
|
+
output: process.stdout,
|
|
274
|
+
terminal: true
|
|
275
|
+
});
|
|
276
|
+
rl.write(cmd);
|
|
277
|
+
rl.on("line", (line) => {
|
|
278
|
+
rl.close();
|
|
279
|
+
const trimmed = line.trim();
|
|
280
|
+
resolve(trimmed || null);
|
|
281
|
+
});
|
|
282
|
+
rl.on("close", () => resolve(null));
|
|
139
283
|
});
|
|
140
|
-
const trimmed = edited.trim();
|
|
141
|
-
return trimmed || null;
|
|
142
284
|
}
|
|
143
285
|
async function promptAction(cmd) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
`);
|
|
286
|
+
const hasPlaceholders = PLACEHOLDER_RE.test(cmd);
|
|
287
|
+
PLACEHOLDER_RE.lastIndex = 0;
|
|
147
288
|
const action = await select({
|
|
148
289
|
message: "Action:",
|
|
149
290
|
choices: [
|
|
150
|
-
{ name: "Run", value: "run" },
|
|
151
|
-
{ name:
|
|
291
|
+
{ name: hasPlaceholders ? "Run (fill placeholders)" : "Run", value: "run" },
|
|
292
|
+
{ name: `Edit ${DIM2}(modify command before running)${RESET}`, value: "edit" },
|
|
152
293
|
{ name: "Cancel", value: "cancel" }
|
|
153
294
|
]
|
|
154
295
|
});
|
|
155
|
-
if (action === "run") return cmd;
|
|
156
|
-
if (action === "edit")
|
|
296
|
+
if (action === "run") return fillPlaceholders(cmd);
|
|
297
|
+
if (action === "edit") {
|
|
298
|
+
process.stdout.write(`${DIM2}> ${RESET}`);
|
|
299
|
+
return promptEdit(cmd);
|
|
300
|
+
}
|
|
157
301
|
return null;
|
|
158
302
|
}
|
|
159
303
|
async function promptCommand(commands) {
|
|
160
304
|
if (commands.length === 1) {
|
|
305
|
+
console.log(`
|
|
306
|
+
${CYAN}${commands[0]}${RESET}
|
|
307
|
+
`);
|
|
161
308
|
return promptAction(commands[0]);
|
|
162
309
|
}
|
|
163
310
|
const choice = await select({
|
|
164
|
-
message: "Select a command
|
|
311
|
+
message: "Select a command:",
|
|
165
312
|
choices: [
|
|
166
313
|
...commands.map((cmd) => ({
|
|
167
314
|
name: `${CYAN}${cmd}${RESET}`,
|
|
@@ -171,6 +318,9 @@ async function promptCommand(commands) {
|
|
|
171
318
|
]
|
|
172
319
|
});
|
|
173
320
|
if (choice === "__cancel__") return null;
|
|
321
|
+
console.log(`
|
|
322
|
+
${CYAN}${choice}${RESET}
|
|
323
|
+
`);
|
|
174
324
|
return promptAction(choice);
|
|
175
325
|
}
|
|
176
326
|
|
|
@@ -190,13 +340,13 @@ function execCommand(cmd, cwd) {
|
|
|
190
340
|
|
|
191
341
|
// src/index.ts
|
|
192
342
|
var BRAILLE = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
193
|
-
var
|
|
343
|
+
var DIM3 = "\x1B[2m";
|
|
194
344
|
var RESET2 = "\x1B[0m";
|
|
195
345
|
var RED = "\x1B[31m";
|
|
196
346
|
function startSpinner(message) {
|
|
197
347
|
let i = 0;
|
|
198
348
|
const interval = setInterval(() => {
|
|
199
|
-
process.stderr.write(`\r${
|
|
349
|
+
process.stderr.write(`\r${DIM3}${BRAILLE[i++ % BRAILLE.length]} ${message}${RESET2}`);
|
|
200
350
|
}, 80);
|
|
201
351
|
return () => {
|
|
202
352
|
clearInterval(interval);
|
|
@@ -209,6 +359,7 @@ function parseArgs(argv) {
|
|
|
209
359
|
let cwd = process.cwd();
|
|
210
360
|
let model = process.env.AISH_MODEL || void 0;
|
|
211
361
|
let verbose2 = false;
|
|
362
|
+
let print = false;
|
|
212
363
|
const queryParts = [];
|
|
213
364
|
for (let i = 0; i < args.length; i++) {
|
|
214
365
|
const arg = args[i];
|
|
@@ -225,6 +376,8 @@ function parseArgs(argv) {
|
|
|
225
376
|
model = args[++i];
|
|
226
377
|
} else if (arg === "-v" || arg === "--verbose") {
|
|
227
378
|
verbose2 = true;
|
|
379
|
+
} else if (arg === "--print") {
|
|
380
|
+
print = true;
|
|
228
381
|
} else if (arg === "-h" || arg === "--help") {
|
|
229
382
|
console.log(`Usage: aish [options] <query...>
|
|
230
383
|
|
|
@@ -232,38 +385,55 @@ Options:
|
|
|
232
385
|
-p, --provider <claude|codex> AI provider (default: claude, env: AISH_PROVIDER)
|
|
233
386
|
-m, --model <model> Model override (env: AISH_MODEL)
|
|
234
387
|
--cwd <dir> Working directory
|
|
388
|
+
--print Output command only (for shell integration)
|
|
235
389
|
-v, --verbose Show debug output
|
|
236
|
-
-h, --help Show help
|
|
390
|
+
-h, --help Show help
|
|
391
|
+
|
|
392
|
+
Zsh Integration:
|
|
393
|
+
Add to .zshrc: source "$(npm root -g)/aish-cli/aish.plugin.zsh"
|
|
394
|
+
Then press Ctrl+G to activate AI mode`);
|
|
237
395
|
process.exit(0);
|
|
238
396
|
} else {
|
|
239
397
|
queryParts.push(arg);
|
|
240
398
|
}
|
|
241
399
|
}
|
|
242
|
-
return { query: queryParts.join(" "), provider, cwd, model, verbose: verbose2 };
|
|
400
|
+
return { query: queryParts.join(" "), provider, cwd, model, verbose: verbose2, print };
|
|
243
401
|
}
|
|
244
402
|
async function main() {
|
|
245
|
-
const { query, provider, cwd, model, verbose: verbose2 } = parseArgs(process.argv);
|
|
403
|
+
const { query, provider, cwd, model, verbose: verbose2, print } = parseArgs(process.argv);
|
|
246
404
|
setVerbose(verbose2);
|
|
247
405
|
if (!query) {
|
|
248
406
|
console.error(`${RED}Usage: aish <query>${RESET2}`);
|
|
249
407
|
process.exit(1);
|
|
250
408
|
}
|
|
251
|
-
const stopSpinner = verbose2 ? () => {
|
|
409
|
+
const stopSpinner = verbose2 || print ? () => {
|
|
252
410
|
} : startSpinner("Thinking...");
|
|
253
411
|
let commands;
|
|
412
|
+
let resultCacheKey;
|
|
254
413
|
try {
|
|
255
414
|
const result = await queryAi(provider, query, cwd, model);
|
|
256
415
|
commands = result.commands;
|
|
416
|
+
resultCacheKey = result.cacheKey;
|
|
257
417
|
} catch (err) {
|
|
258
418
|
stopSpinner();
|
|
419
|
+
if (print) {
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
259
422
|
console.error(`${RED}Error: ${err.message}${RESET2}`);
|
|
260
423
|
process.exit(1);
|
|
261
424
|
}
|
|
262
425
|
stopSpinner();
|
|
263
426
|
if (commands.length === 0) {
|
|
427
|
+
if (print) {
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
264
430
|
console.error(`${RED}No commands suggested.${RESET2}`);
|
|
265
431
|
process.exit(1);
|
|
266
432
|
}
|
|
433
|
+
if (print) {
|
|
434
|
+
console.log(commands[0]);
|
|
435
|
+
process.exit(0);
|
|
436
|
+
}
|
|
267
437
|
let chosen;
|
|
268
438
|
try {
|
|
269
439
|
chosen = await promptCommand(commands);
|
|
@@ -274,6 +444,9 @@ async function main() {
|
|
|
274
444
|
if (!chosen) {
|
|
275
445
|
process.exit(0);
|
|
276
446
|
}
|
|
447
|
+
if (resultCacheKey) {
|
|
448
|
+
await cacheSet(resultCacheKey, { commands });
|
|
449
|
+
}
|
|
277
450
|
const exitCode = await execCommand(chosen, cwd);
|
|
278
451
|
process.exit(exitCode);
|
|
279
452
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aish-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|