@zachjxyz/moxie 0.4.9 → 0.5.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/bin/moxie +1 -1
- package/lib/phases.sh +263 -408
- package/package.json +1 -1
package/bin/moxie
CHANGED
package/lib/phases.sh
CHANGED
|
@@ -211,125 +211,203 @@ _select_context_docs() {
|
|
|
211
211
|
|
|
212
212
|
# ---- Agent selection TUI ----
|
|
213
213
|
# Populates SELECTED_AGENT_INDICES (CLI) and SELECTED_GATEWAY_INDICES (gateway).
|
|
214
|
-
# Shows CLI agents found on PATH + gateway
|
|
214
|
+
# Shows CLI agents found on PATH + inline gateway model search.
|
|
215
|
+
# Typing filters gateway models live (fzf-style). Requires minimum 2 total.
|
|
215
216
|
|
|
216
217
|
_select_agents() {
|
|
217
218
|
SELECTED_AGENT_INDICES=()
|
|
218
219
|
SELECTED_GATEWAY_INDICES=()
|
|
219
220
|
SELECTED_CUSTOM_GATEWAY_INDICES=()
|
|
220
221
|
|
|
221
|
-
#
|
|
222
|
-
local
|
|
223
|
-
local
|
|
224
|
-
local
|
|
225
|
-
local
|
|
222
|
+
# ---- CLI agent items ----
|
|
223
|
+
local cli_labels=()
|
|
224
|
+
local cli_meta=()
|
|
225
|
+
local cli_sources=()
|
|
226
|
+
local cli_selected=()
|
|
226
227
|
local max_label_len=0
|
|
227
228
|
|
|
228
|
-
# CLI agents header
|
|
229
|
-
item_labels+=("--- Detected CLI agents ---")
|
|
230
|
-
item_meta+=("")
|
|
231
|
-
item_types+=("separator")
|
|
232
|
-
item_sources+=("-1")
|
|
233
|
-
|
|
234
|
-
# CLI agents on PATH
|
|
235
229
|
for idx in "${AVAILABLE_AGENT_INDICES[@]}"; do
|
|
236
230
|
local label="${KNOWN_AGENT_LABELS[$idx]}"
|
|
237
231
|
local binary="${KNOWN_AGENT_BINARIES[$idx]}"
|
|
238
|
-
|
|
232
|
+
cli_labels+=("$label")
|
|
239
233
|
local ver
|
|
240
234
|
ver=$("$binary" --version 2>&1 | head -1 | grep -oE '[0-9]+\.[0-9]+[0-9.]*' | head -1) || ver=""
|
|
241
235
|
[ -z "$ver" ] && ver="installed"
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
236
|
+
cli_meta+=("$ver")
|
|
237
|
+
cli_sources+=("$idx")
|
|
238
|
+
cli_selected+=(1)
|
|
245
239
|
local len=${#label}
|
|
246
240
|
[ "$len" -gt "$max_label_len" ] && max_label_len=$len
|
|
247
241
|
done
|
|
248
242
|
|
|
249
|
-
#
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
243
|
+
local cli_count=${#cli_labels[@]}
|
|
244
|
+
|
|
245
|
+
# ---- Gateway model catalog ----
|
|
246
|
+
local gw_ids=()
|
|
247
|
+
local gw_labels=()
|
|
248
|
+
local gw_selected=()
|
|
249
|
+
local gw_loaded=0
|
|
250
|
+
local gw_error=""
|
|
251
|
+
|
|
252
|
+
# Try to fetch gateway models eagerly if key exists
|
|
253
|
+
if command -v node &>/dev/null && gateway_has_key "vercel-ai-gateway"; then
|
|
254
|
+
local _gw_key=""
|
|
255
|
+
_gw_key=$(gateway_get_key "vercel-ai-gateway" 2>/dev/null) || true
|
|
256
|
+
if [ -n "$_gw_key" ]; then
|
|
257
|
+
printf "Fetching gateway models..." >&2
|
|
258
|
+
local _mj=""
|
|
259
|
+
_mj=$(GATEWAY_API_KEY="$_gw_key" node "$MOXIE_LIB/gateway-models.mjs" "https://ai-gateway.vercel.sh" "" 2>/dev/null) || true
|
|
260
|
+
printf "\\r\\033[K" >&2
|
|
261
|
+
if [ -n "$_mj" ]; then
|
|
262
|
+
eval "$(python3 -c "
|
|
263
|
+
import json, sys, shlex
|
|
264
|
+
data = json.loads(sys.stdin.read())
|
|
265
|
+
models = data.get('models', [])
|
|
266
|
+
ids = [m['id'] for m in models]
|
|
267
|
+
labels = [m.get('name', m['id']) for m in models]
|
|
268
|
+
print('gw_ids=(' + ' '.join(shlex.quote(x) for x in ids) + ')')
|
|
269
|
+
print('gw_labels=(' + ' '.join(shlex.quote(x) for x in labels) + ')')
|
|
270
|
+
" <<< "$_mj" 2>/dev/null)" && gw_loaded=1
|
|
271
|
+
else
|
|
272
|
+
gw_error="Failed to fetch models. Check API key or network."
|
|
273
|
+
fi
|
|
277
274
|
fi
|
|
278
|
-
|
|
275
|
+
fi
|
|
279
276
|
|
|
280
|
-
#
|
|
281
|
-
|
|
282
|
-
|
|
277
|
+
local gw_count=${#gw_ids[@]}
|
|
278
|
+
for (( i = 0; i < gw_count; i++ )); do
|
|
279
|
+
gw_selected+=(0)
|
|
283
280
|
done
|
|
284
281
|
|
|
282
|
+
# ---- Layout constants ----
|
|
283
|
+
local gw_search=""
|
|
284
|
+
local gw_cursor=0 # cursor within filtered gateway results
|
|
285
|
+
local MAX_GW_VISIBLE=10 # fixed-height gateway results area
|
|
286
|
+
|
|
287
|
+
# Cursor zones: "cli" (index 0..cli_count-1), "gw" (filtered results), "submit"
|
|
288
|
+
local zone="cli"
|
|
289
|
+
local cli_cursor=0
|
|
290
|
+
# Skip to first CLI agent if available
|
|
291
|
+
[ "$cli_count" -eq 0 ] && zone="gw"
|
|
292
|
+
|
|
285
293
|
local sep=""
|
|
286
|
-
for (( i = 0; i < max_label_len +
|
|
294
|
+
for (( i = 0; i < max_label_len + 50; i++ )); do sep="${sep}-"; done
|
|
295
|
+
|
|
296
|
+
# Fixed total lines: header(1) + cli(cli_count) + sep(1) + search(1) + gw_area(MAX_GW_VISIBLE) + scroll(1) + sep(1) + submit(1) + blank(1) + hint(1)
|
|
297
|
+
local total_lines=$(( 1 + cli_count + 1 + 1 + MAX_GW_VISIBLE + 1 + 1 + 1 + 1 + 1 ))
|
|
298
|
+
|
|
299
|
+
_build_gw_filtered() {
|
|
300
|
+
# Populates gw_filtered array with indices matching gw_search
|
|
301
|
+
gw_filtered=()
|
|
302
|
+
local lc_search
|
|
303
|
+
lc_search=$(echo "$gw_search" | tr '[:upper:]' '[:lower:]')
|
|
304
|
+
for (( i = 0; i < gw_count; i++ )); do
|
|
305
|
+
if [ -z "$gw_search" ]; then
|
|
306
|
+
gw_filtered+=("$i")
|
|
307
|
+
else
|
|
308
|
+
local lc_id
|
|
309
|
+
lc_id=$(echo "${gw_ids[$i]}" | tr '[:upper:]' '[:lower:]')
|
|
310
|
+
if [[ "$lc_id" == *"$lc_search"* ]]; then
|
|
311
|
+
gw_filtered+=("$i")
|
|
312
|
+
fi
|
|
313
|
+
fi
|
|
314
|
+
done
|
|
315
|
+
}
|
|
287
316
|
|
|
288
|
-
|
|
289
|
-
|
|
317
|
+
local gw_filtered=()
|
|
318
|
+
_build_gw_filtered
|
|
290
319
|
|
|
291
|
-
|
|
320
|
+
_sa_render() {
|
|
321
|
+
# Count total selected
|
|
292
322
|
local sel_count=0
|
|
293
|
-
for (( i = 0; i <
|
|
294
|
-
[ "${
|
|
323
|
+
for (( i = 0; i < cli_count; i++ )); do
|
|
324
|
+
[ "${cli_selected[$i]}" = "1" ] && sel_count=$(( sel_count + 1 ))
|
|
325
|
+
done
|
|
326
|
+
for (( i = 0; i < gw_count; i++ )); do
|
|
327
|
+
[ "${gw_selected[$i]}" = "1" ] && sel_count=$(( sel_count + 1 ))
|
|
295
328
|
done
|
|
296
329
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
330
|
+
# CLI header
|
|
331
|
+
printf "\\r\\033[K \\033[2m--- Detected CLI agents ---\\033[0m\\n" >&2
|
|
332
|
+
|
|
333
|
+
# CLI agents
|
|
334
|
+
for (( i = 0; i < cli_count; i++ )); do
|
|
302
335
|
local marker=" "
|
|
303
|
-
[ "$
|
|
304
|
-
if [ "${item_types[$i]}" = "custom_add" ]; then
|
|
305
|
-
printf "\\r\\033[K %s \\033[36m%s\\033[0m \\033[2m(%s)\\033[0m\\n" "$marker" "${item_labels[$i]}" "${item_meta[$i]}" >&2
|
|
306
|
-
continue
|
|
307
|
-
fi
|
|
336
|
+
[ "$zone" = "cli" ] && [ "$cli_cursor" -eq "$i" ] && marker="> "
|
|
308
337
|
local check="[ ]"
|
|
309
|
-
[ "${
|
|
310
|
-
|
|
311
|
-
|
|
338
|
+
[ "${cli_selected[$i]}" = "1" ] && check="[x]"
|
|
339
|
+
printf "\\r\\033[K %s%s %-${max_label_len}s (%s)\\n" "$marker" "$check" "${cli_labels[$i]}" "${cli_meta[$i]}" >&2
|
|
340
|
+
done
|
|
341
|
+
|
|
342
|
+
# Gateway header + search
|
|
343
|
+
printf "\\r\\033[K \\033[2m--- Vercel AI Gateway ---\\033[0m\\n" >&2
|
|
344
|
+
|
|
345
|
+
if [ "$gw_loaded" = "1" ]; then
|
|
346
|
+
printf "\\r\\033[K Search: \\033[36m%s\\033[0m\\033[2m|\\033[0m \\033[2m(%d of %d)\\033[0m\\n" "$gw_search" "${#gw_filtered[@]}" "$gw_count" >&2
|
|
347
|
+
|
|
348
|
+
# Clamp gw_cursor
|
|
349
|
+
local fcount=${#gw_filtered[@]}
|
|
350
|
+
[ "$gw_cursor" -ge "$fcount" ] && gw_cursor=$(( fcount > 0 ? fcount - 1 : 0 ))
|
|
351
|
+
[ "$gw_cursor" -lt 0 ] && gw_cursor=0
|
|
352
|
+
|
|
353
|
+
# Scroll window
|
|
354
|
+
local vstart=0
|
|
355
|
+
if [ "$gw_cursor" -ge "$MAX_GW_VISIBLE" ]; then
|
|
356
|
+
vstart=$(( gw_cursor - MAX_GW_VISIBLE + 1 ))
|
|
357
|
+
fi
|
|
358
|
+
local vend=$(( vstart + MAX_GW_VISIBLE ))
|
|
359
|
+
[ "$vend" -gt "$fcount" ] && vend=$fcount
|
|
360
|
+
|
|
361
|
+
local rendered=0
|
|
362
|
+
for (( vi = vstart; vi < vend; vi++ )); do
|
|
363
|
+
local ri=${gw_filtered[$vi]}
|
|
364
|
+
local marker=" "
|
|
365
|
+
[ "$zone" = "gw" ] && [ "$vi" -eq "$gw_cursor" ] && marker="> "
|
|
366
|
+
local check="[ ]"
|
|
367
|
+
[ "${gw_selected[$ri]}" = "1" ] && check="[x]"
|
|
368
|
+
printf "\\r\\033[K %s%s %s\\n" "$marker" "$check" "${gw_ids[$ri]}" >&2
|
|
369
|
+
rendered=$(( rendered + 1 ))
|
|
370
|
+
done
|
|
371
|
+
|
|
372
|
+
# Pad to fixed height
|
|
373
|
+
while [ "$rendered" -lt "$MAX_GW_VISIBLE" ]; do
|
|
374
|
+
printf "\\r\\033[K\\n" >&2
|
|
375
|
+
rendered=$(( rendered + 1 ))
|
|
376
|
+
done
|
|
377
|
+
|
|
378
|
+
# Scroll indicator
|
|
379
|
+
if [ "$fcount" -gt "$MAX_GW_VISIBLE" ]; then
|
|
380
|
+
printf "\\r\\033[K \\033[2m(%d-%d of %d · ↑↓ to scroll)\\033[0m\\n" "$(( vstart + 1 ))" "$vend" "$fcount" >&2
|
|
312
381
|
else
|
|
313
|
-
printf "\\r\\033[K
|
|
382
|
+
printf "\\r\\033[K\\n" >&2
|
|
314
383
|
fi
|
|
315
|
-
|
|
384
|
+
elif [ -n "$gw_error" ]; then
|
|
385
|
+
printf "\\r\\033[K \\033[31m%s\\033[0m\\n" "$gw_error" >&2
|
|
386
|
+
for (( i = 0; i < MAX_GW_VISIBLE; i++ )); do printf "\\r\\033[K\\n" >&2; done
|
|
387
|
+
printf "\\r\\033[K\\n" >&2
|
|
388
|
+
else
|
|
389
|
+
printf "\\r\\033[K \\033[2mNo API key configured. Select CLI agents or set up a key with 'moxie init'.\\033[0m\\n" >&2
|
|
390
|
+
for (( i = 0; i < MAX_GW_VISIBLE; i++ )); do printf "\\r\\033[K\\n" >&2; done
|
|
391
|
+
printf "\\r\\033[K\\n" >&2
|
|
392
|
+
fi
|
|
316
393
|
|
|
394
|
+
# Separator + submit
|
|
317
395
|
printf "\\r\\033[K %s\\n" "$sep" >&2
|
|
318
396
|
|
|
319
397
|
local submit_marker=" "
|
|
320
|
-
[ "$
|
|
398
|
+
[ "$zone" = "submit" ] && submit_marker="> "
|
|
321
399
|
if [ "$sel_count" -ge 2 ]; then
|
|
322
400
|
printf "\\r\\033[K %s%s\\n" "$submit_marker" "Submit ($sel_count selected)" >&2
|
|
323
401
|
else
|
|
324
402
|
printf "\\r\\033[K %s\\033[2m%s\\033[0m\\n" "$submit_marker" "Submit (need at least 2)" >&2
|
|
325
403
|
fi
|
|
326
404
|
|
|
327
|
-
printf "\\r\\033[K\\n\\033[K \\033[2m↑↓ navigate · space toggle · enter submit
|
|
405
|
+
printf "\\r\\033[K\\n\\033[K \\033[2m↑↓ navigate · space toggle · type to search · enter submit\\033[0m" >&2
|
|
328
406
|
}
|
|
329
407
|
|
|
330
|
-
printf "Select agents (at least 2)
|
|
408
|
+
printf "Select agents (at least 2):\\n\\n" >&2
|
|
331
409
|
printf "\\033[?25l" >&2
|
|
332
|
-
|
|
410
|
+
_sa_render
|
|
333
411
|
|
|
334
412
|
local old_stty
|
|
335
413
|
old_stty=$(stty -g < /dev/tty 2>/dev/null)
|
|
@@ -350,361 +428,130 @@ _select_agents() {
|
|
|
350
428
|
dir=$(dd bs=1 count=1 2>/dev/null < /dev/tty) || true
|
|
351
429
|
if [ "$bracket" = "[" ]; then
|
|
352
430
|
case "$dir" in
|
|
353
|
-
A) # Up
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
431
|
+
A) # Up
|
|
432
|
+
if [ "$zone" = "submit" ]; then
|
|
433
|
+
if [ "$gw_loaded" = "1" ] && [ ${#gw_filtered[@]} -gt 0 ]; then
|
|
434
|
+
zone="gw"
|
|
435
|
+
gw_cursor=$(( ${#gw_filtered[@]} - 1 ))
|
|
436
|
+
elif [ "$cli_count" -gt 0 ]; then
|
|
437
|
+
zone="cli"
|
|
438
|
+
cli_cursor=$(( cli_count - 1 ))
|
|
439
|
+
fi
|
|
440
|
+
elif [ "$zone" = "gw" ]; then
|
|
441
|
+
if [ "$gw_cursor" -gt 0 ]; then
|
|
442
|
+
gw_cursor=$(( gw_cursor - 1 ))
|
|
443
|
+
elif [ "$cli_count" -gt 0 ]; then
|
|
444
|
+
zone="cli"
|
|
445
|
+
cli_cursor=$(( cli_count - 1 ))
|
|
446
|
+
fi
|
|
447
|
+
elif [ "$zone" = "cli" ]; then
|
|
448
|
+
[ "$cli_cursor" -gt 0 ] && cli_cursor=$(( cli_cursor - 1 ))
|
|
449
|
+
fi
|
|
359
450
|
;;
|
|
360
|
-
B) # Down
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
451
|
+
B) # Down
|
|
452
|
+
if [ "$zone" = "cli" ]; then
|
|
453
|
+
if [ "$cli_cursor" -lt $(( cli_count - 1 )) ]; then
|
|
454
|
+
cli_cursor=$(( cli_cursor + 1 ))
|
|
455
|
+
elif [ "$gw_loaded" = "1" ] && [ ${#gw_filtered[@]} -gt 0 ]; then
|
|
456
|
+
zone="gw"
|
|
457
|
+
gw_cursor=0
|
|
458
|
+
else
|
|
459
|
+
zone="submit"
|
|
460
|
+
fi
|
|
461
|
+
elif [ "$zone" = "gw" ]; then
|
|
462
|
+
if [ "$gw_cursor" -lt $(( ${#gw_filtered[@]} - 1 )) ]; then
|
|
463
|
+
gw_cursor=$(( gw_cursor + 1 ))
|
|
464
|
+
else
|
|
465
|
+
zone="submit"
|
|
466
|
+
fi
|
|
467
|
+
fi
|
|
366
468
|
;;
|
|
367
469
|
esac
|
|
368
470
|
fi
|
|
369
471
|
elif [ "$key" = " " ]; then
|
|
370
|
-
|
|
371
|
-
|
|
472
|
+
# Toggle selection
|
|
473
|
+
if [ "$zone" = "cli" ]; then
|
|
474
|
+
[ "${cli_selected[$cli_cursor]}" = "0" ] && cli_selected[$cli_cursor]=1 || cli_selected[$cli_cursor]=0
|
|
475
|
+
elif [ "$zone" = "gw" ] && [ ${#gw_filtered[@]} -gt 0 ]; then
|
|
476
|
+
local ri=${gw_filtered[$gw_cursor]}
|
|
477
|
+
[ "${gw_selected[$ri]}" = "0" ] && gw_selected[$ri]=1 || gw_selected[$ri]=0
|
|
372
478
|
fi
|
|
373
479
|
elif [ "$key" = "" ]; then
|
|
374
|
-
if
|
|
375
|
-
|
|
480
|
+
# Enter — submit if on submit row, else toggle
|
|
481
|
+
if [ "$zone" = "submit" ]; then
|
|
376
482
|
local sel_count=0
|
|
377
|
-
for (( i = 0; i <
|
|
378
|
-
[ "${
|
|
379
|
-
done
|
|
380
|
-
[ "$sel_count" -ge 2 ] && break
|
|
381
|
-
elif [ "${item_types[$cursor]}" = "custom_add" ]; then
|
|
382
|
-
# Live search picker — fetch models from gateway, filter interactively
|
|
383
|
-
stty "$old_stty" < /dev/tty 2>/dev/null
|
|
384
|
-
printf "\\033[?25h" >&2
|
|
385
|
-
|
|
386
|
-
# Ensure we have an API key
|
|
387
|
-
local _gw_key=""
|
|
388
|
-
if gateway_has_key "vercel-ai-gateway"; then
|
|
389
|
-
_gw_key=$(gateway_get_key "vercel-ai-gateway" 2>/dev/null) || true
|
|
390
|
-
fi
|
|
391
|
-
if [ -z "$_gw_key" ]; then
|
|
392
|
-
printf "\\n Gateway API key needed to search models.\\n\\n" >&2
|
|
393
|
-
gateway_store_key "vercel-ai-gateway" || {
|
|
394
|
-
printf " \\033[31mFailed to store key. Skipping search.\\033[0m\\n" >&2
|
|
395
|
-
sleep 1
|
|
396
|
-
stty -echo -icanon min 1 < /dev/tty 2>/dev/null
|
|
397
|
-
printf "\\033[?25l" >&2
|
|
398
|
-
printf "\\033[%dA" "$jump_back" >&2
|
|
399
|
-
_agent_render
|
|
400
|
-
continue
|
|
401
|
-
}
|
|
402
|
-
_gw_key=$(gateway_get_key "vercel-ai-gateway" 2>/dev/null) || true
|
|
403
|
-
fi
|
|
404
|
-
|
|
405
|
-
# Fetch model list
|
|
406
|
-
printf "\\n Fetching models from AI Gateway..." >&2
|
|
407
|
-
local _models_json=""
|
|
408
|
-
_models_json=$(GATEWAY_API_KEY="$_gw_key" node "$MOXIE_LIB/gateway-models.mjs" "https://ai-gateway.vercel.sh" "" 2>/dev/null) || true
|
|
409
|
-
printf "\\r\\033[K" >&2
|
|
410
|
-
|
|
411
|
-
if [ -z "$_models_json" ]; then
|
|
412
|
-
printf " \\033[31mFailed to fetch models. Check your API key or network.\\033[0m\\n" >&2
|
|
413
|
-
sleep 2
|
|
414
|
-
stty -echo -icanon min 1 < /dev/tty 2>/dev/null
|
|
415
|
-
printf "\\033[?25l" >&2
|
|
416
|
-
printf "\\033[%dA" "$((jump_back + 2))" >&2
|
|
417
|
-
_agent_render
|
|
418
|
-
continue
|
|
419
|
-
fi
|
|
420
|
-
|
|
421
|
-
# Parse into arrays using python3
|
|
422
|
-
local _all_model_ids=()
|
|
423
|
-
local _all_model_labels=()
|
|
424
|
-
eval "$(python3 -c "
|
|
425
|
-
import json, sys, shlex
|
|
426
|
-
data = json.loads(sys.stdin.read())
|
|
427
|
-
models = data.get('models', [])
|
|
428
|
-
ids = []
|
|
429
|
-
labels = []
|
|
430
|
-
for m in models:
|
|
431
|
-
mid = m['id']
|
|
432
|
-
ids.append(mid)
|
|
433
|
-
labels.append(m.get('name', mid))
|
|
434
|
-
# Output as bash array assignments
|
|
435
|
-
print('_all_model_ids=(' + ' '.join(shlex.quote(x) for x in ids) + ')')
|
|
436
|
-
print('_all_model_labels=(' + ' '.join(shlex.quote(x) for x in labels) + ')')
|
|
437
|
-
" <<< "$_models_json" 2>/dev/null)"
|
|
438
|
-
|
|
439
|
-
if [ ${#_all_model_ids[@]} -eq 0 ]; then
|
|
440
|
-
printf " \\033[31mNo models returned from gateway.\\033[0m\\n" >&2
|
|
441
|
-
sleep 1
|
|
442
|
-
stty -echo -icanon min 1 < /dev/tty 2>/dev/null
|
|
443
|
-
printf "\\033[?25l" >&2
|
|
444
|
-
printf "\\033[%dA" "$((jump_back + 2))" >&2
|
|
445
|
-
_agent_render
|
|
446
|
-
continue
|
|
447
|
-
fi
|
|
448
|
-
|
|
449
|
-
# Build set of already-selected model IDs
|
|
450
|
-
local _existing_ids=""
|
|
451
|
-
for _eid in "${custom_gateway_models[@]}"; do
|
|
452
|
-
_existing_ids="${_existing_ids}|${_eid}"
|
|
483
|
+
for (( i = 0; i < cli_count; i++ )); do
|
|
484
|
+
[ "${cli_selected[$i]}" = "1" ] && sel_count=$(( sel_count + 1 ))
|
|
453
485
|
done
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
local _search=""
|
|
457
|
-
local _scursor=0
|
|
458
|
-
local _sselected=()
|
|
459
|
-
for (( _si = 0; _si < ${#_all_model_ids[@]}; _si++ )); do
|
|
460
|
-
_sselected+=(0)
|
|
486
|
+
for (( i = 0; i < gw_count; i++ )); do
|
|
487
|
+
[ "${gw_selected[$i]}" = "1" ] && sel_count=$(( sel_count + 1 ))
|
|
461
488
|
done
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
fi
|
|
479
|
-
fi
|
|
480
|
-
done
|
|
481
|
-
|
|
482
|
-
local _fcount=${#_filtered[@]}
|
|
483
|
-
|
|
484
|
-
# Clamp cursor
|
|
485
|
-
[ "$_scursor" -ge "$_fcount" ] && _scursor=$(( _fcount > 0 ? _fcount - 1 : 0 ))
|
|
486
|
-
[ "$_scursor" -lt 0 ] && _scursor=0
|
|
487
|
-
|
|
488
|
-
# Visible window
|
|
489
|
-
local _vstart=0
|
|
490
|
-
if [ "$_scursor" -ge "$_max_visible" ]; then
|
|
491
|
-
_vstart=$(( _scursor - _max_visible + 1 ))
|
|
492
|
-
fi
|
|
493
|
-
local _vend=$(( _vstart + _max_visible ))
|
|
494
|
-
[ "$_vend" -gt "$_fcount" ] && _vend=$_fcount
|
|
495
|
-
|
|
496
|
-
# Count selected
|
|
497
|
-
local _sel_count=0
|
|
498
|
-
for (( _si = 0; _si < ${#_sselected[@]}; _si++ )); do
|
|
499
|
-
[ "${_sselected[$_si]}" = "1" ] && _sel_count=$(( _sel_count + 1 ))
|
|
500
|
-
done
|
|
501
|
-
|
|
502
|
-
# Render
|
|
503
|
-
printf "\\r\\033[K \\033[1mSearch gateway models\\033[0m (%d available, %d selected)\\n" "$_fcount" "$_sel_count" >&2
|
|
504
|
-
printf "\\r\\033[K Search: \\033[36m%s\\033[0m\\033[2m|\\033[0m\\n\\n" "$_search" >&2
|
|
505
|
-
|
|
506
|
-
local _rendered=0
|
|
507
|
-
for (( _vi = _vstart; _vi < _vend; _vi++ )); do
|
|
508
|
-
local _ri=${_filtered[$_vi]}
|
|
509
|
-
local _mid="${_all_model_ids[$_ri]}"
|
|
510
|
-
local _marker=" "
|
|
511
|
-
[ "$_vi" -eq "$_scursor" ] && _marker="> "
|
|
512
|
-
local _check="[ ]"
|
|
513
|
-
[ "${_sselected[$_ri]}" = "1" ] && _check="[x]"
|
|
514
|
-
local _exists_marker=""
|
|
515
|
-
if [[ "$_existing_ids" == *"|${_mid}"* ]]; then
|
|
516
|
-
_exists_marker=" \\033[2m(added)\\033[0m"
|
|
517
|
-
fi
|
|
518
|
-
printf "\\r\\033[K %s%s %s%b\\n" "$_marker" "$_check" "$_mid" "$_exists_marker" >&2
|
|
519
|
-
_rendered=$(( _rendered + 1 ))
|
|
520
|
-
done
|
|
521
|
-
|
|
522
|
-
# Pad remaining lines
|
|
523
|
-
while [ "$_rendered" -lt "$_max_visible" ]; do
|
|
524
|
-
printf "\\r\\033[K\\n" >&2
|
|
525
|
-
_rendered=$(( _rendered + 1 ))
|
|
526
|
-
done
|
|
527
|
-
|
|
528
|
-
if [ "$_vstart" -gt 0 ] || [ "$_vend" -lt "$_fcount" ]; then
|
|
529
|
-
printf "\\r\\033[K \\033[2m(%d-%d of %d · scroll for more)\\033[0m\\n" "$(( _vstart + 1 ))" "$_vend" "$_fcount" >&2
|
|
530
|
-
else
|
|
531
|
-
printf "\\r\\033[K\\n" >&2
|
|
532
|
-
fi
|
|
533
|
-
|
|
534
|
-
printf "\\r\\033[K \\033[2m↑↓ navigate · space select · enter confirm · esc cancel · type to filter\\033[0m" >&2
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
# Total lines rendered by _search_render: 3 header + _max_visible + 1 scroll + 1 hint = _max_visible + 5
|
|
538
|
-
local _search_lines=$(( _max_visible + 5 ))
|
|
539
|
-
|
|
540
|
-
printf "\\033[?25l" >&2
|
|
541
|
-
stty -echo -icanon min 1 < /dev/tty 2>/dev/null
|
|
542
|
-
|
|
543
|
-
printf "\\n" >&2
|
|
544
|
-
_search_render
|
|
545
|
-
|
|
546
|
-
local _search_done=0
|
|
547
|
-
local _search_cancelled=0
|
|
548
|
-
while [ "$_search_done" = "0" ]; do
|
|
549
|
-
local _skey
|
|
550
|
-
_skey=$(dd bs=1 count=1 2>/dev/null < /dev/tty) || break
|
|
551
|
-
|
|
552
|
-
if [ "$_skey" = $'\033' ]; then
|
|
553
|
-
local _sbracket _sdir
|
|
554
|
-
_sbracket=$(dd bs=1 count=1 2>/dev/null < /dev/tty) || true
|
|
555
|
-
_sdir=$(dd bs=1 count=1 2>/dev/null < /dev/tty) || true
|
|
556
|
-
if [ "$_sbracket" = "[" ]; then
|
|
557
|
-
case "$_sdir" in
|
|
558
|
-
A) _scursor=$(( _scursor - 1 )); [ "$_scursor" -lt 0 ] && _scursor=0 ;;
|
|
559
|
-
B) _scursor=$(( _scursor + 1 )) ;;
|
|
560
|
-
esac
|
|
561
|
-
else
|
|
562
|
-
# Bare escape — cancel
|
|
563
|
-
_search_cancelled=1
|
|
564
|
-
_search_done=1
|
|
565
|
-
fi
|
|
566
|
-
elif [ "$_skey" = " " ]; then
|
|
567
|
-
# Toggle selection at cursor
|
|
568
|
-
local _filtered_for_toggle=()
|
|
569
|
-
local _lc_s2
|
|
570
|
-
_lc_s2=$(echo "$_search" | tr '[:upper:]' '[:lower:]')
|
|
571
|
-
for (( _si = 0; _si < ${#_all_model_ids[@]}; _si++ )); do
|
|
572
|
-
if [ -z "$_search" ]; then
|
|
573
|
-
_filtered_for_toggle+=("$_si")
|
|
574
|
-
else
|
|
575
|
-
local _lc2
|
|
576
|
-
_lc2=$(echo "${_all_model_ids[$_si]}" | tr '[:upper:]' '[:lower:]')
|
|
577
|
-
[[ "$_lc2" == *"$_lc_s2"* ]] && _filtered_for_toggle+=("$_si")
|
|
578
|
-
fi
|
|
579
|
-
done
|
|
580
|
-
if [ "$_scursor" -lt "${#_filtered_for_toggle[@]}" ]; then
|
|
581
|
-
local _tidx=${_filtered_for_toggle[$_scursor]}
|
|
582
|
-
[ "${_sselected[$_tidx]}" = "0" ] && _sselected[$_tidx]=1 || _sselected[$_tidx]=0
|
|
583
|
-
fi
|
|
584
|
-
elif [ "$_skey" = "" ]; then
|
|
585
|
-
# Confirm
|
|
586
|
-
_search_done=1
|
|
587
|
-
elif [ "$_skey" = $'\177' ] || [ "$_skey" = $'\010' ]; then
|
|
588
|
-
# Backspace
|
|
589
|
-
if [ -n "$_search" ]; then
|
|
590
|
-
_search="${_search%?}"
|
|
591
|
-
_scursor=0
|
|
592
|
-
fi
|
|
593
|
-
else
|
|
594
|
-
# Printable character — append to search
|
|
595
|
-
if [[ "$_skey" =~ [[:print:]] ]]; then
|
|
596
|
-
_search="${_search}${_skey}"
|
|
597
|
-
_scursor=0
|
|
598
|
-
fi
|
|
599
|
-
fi
|
|
600
|
-
|
|
601
|
-
printf "\\033[%dA" "$_search_lines" >&2
|
|
602
|
-
_search_render
|
|
603
|
-
done
|
|
604
|
-
|
|
605
|
-
# Clear the search sub-TUI
|
|
606
|
-
printf "\\033[%dA" "$_search_lines" >&2
|
|
607
|
-
for (( _ci = 0; _ci <= _search_lines; _ci++ )); do
|
|
608
|
-
printf "\\r\\033[K\\n" >&2
|
|
609
|
-
done
|
|
610
|
-
printf "\\033[%dA" "$(( _search_lines + 1 ))" >&2
|
|
611
|
-
|
|
612
|
-
# Insert selected models into the main list
|
|
613
|
-
if [ "$_search_cancelled" = "0" ]; then
|
|
614
|
-
for (( _si = 0; _si < ${#_sselected[@]}; _si++ )); do
|
|
615
|
-
[ "${_sselected[$_si]}" != "1" ] && continue
|
|
616
|
-
local _new_model="${_all_model_ids[$_si]}"
|
|
617
|
-
local _new_label="${_all_model_labels[$_si]}"
|
|
618
|
-
|
|
619
|
-
# Skip if already in list
|
|
620
|
-
if [[ "$_existing_ids" == *"|${_new_model}"* ]]; then
|
|
621
|
-
continue
|
|
622
|
-
fi
|
|
623
|
-
_existing_ids="${_existing_ids}|${_new_model}"
|
|
624
|
-
|
|
625
|
-
# Derive slug for TOML key
|
|
626
|
-
local _cprov="${_new_model%%/*}"
|
|
627
|
-
local _cname="${_new_model#*/}"
|
|
628
|
-
local _cslug
|
|
629
|
-
_cslug=$(echo "${_cprov}-${_cname}" | tr '[:upper:]' '[:lower:]' | tr ' .' '-' | tr -cd 'a-z0-9-')
|
|
630
|
-
_cslug="${_cslug}-gw"
|
|
631
|
-
|
|
632
|
-
# Insert before the custom_add row
|
|
633
|
-
local _ins=$cursor
|
|
634
|
-
local new_labels=() new_meta=() new_types=() new_sources=() new_selected=()
|
|
635
|
-
for (( _ii = 0; _ii < count; _ii++ )); do
|
|
636
|
-
if [ "$_ii" -eq "$_ins" ]; then
|
|
637
|
-
new_labels+=("$_new_label")
|
|
638
|
-
new_meta+=("$_new_model")
|
|
639
|
-
new_types+=("custom_gateway")
|
|
640
|
-
new_sources+=("${#custom_gateway_names[@]}")
|
|
641
|
-
new_selected+=(1)
|
|
642
|
-
fi
|
|
643
|
-
new_labels+=("${item_labels[$_ii]}")
|
|
644
|
-
new_meta+=("${item_meta[$_ii]}")
|
|
645
|
-
new_types+=("${item_types[$_ii]}")
|
|
646
|
-
new_sources+=("${item_sources[$_ii]}")
|
|
647
|
-
new_selected+=("${selected[$_ii]}")
|
|
648
|
-
done
|
|
649
|
-
|
|
650
|
-
item_labels=("${new_labels[@]}")
|
|
651
|
-
item_meta=("${new_meta[@]}")
|
|
652
|
-
item_types=("${new_types[@]}")
|
|
653
|
-
item_sources=("${new_sources[@]}")
|
|
654
|
-
selected=("${new_selected[@]}")
|
|
655
|
-
|
|
656
|
-
custom_gateway_names+=("$_cslug")
|
|
657
|
-
custom_gateway_models+=("$_new_model")
|
|
658
|
-
|
|
659
|
-
count=${#item_labels[@]}
|
|
660
|
-
cursor=$(( cursor + 1 ))
|
|
661
|
-
|
|
662
|
-
local _ll=${#_new_label}
|
|
663
|
-
[ "$_ll" -gt "$max_label_len" ] && max_label_len=$_ll
|
|
664
|
-
done
|
|
665
|
-
|
|
666
|
-
jump_back=$(( count + 3 ))
|
|
667
|
-
sep=""
|
|
668
|
-
for (( _si = 0; _si < max_label_len + 40; _si++ )); do sep="${sep}-"; done
|
|
489
|
+
[ "$sel_count" -ge 2 ] && break
|
|
490
|
+
elif [ "$zone" = "cli" ]; then
|
|
491
|
+
[ "${cli_selected[$cli_cursor]}" = "0" ] && cli_selected[$cli_cursor]=1 || cli_selected[$cli_cursor]=0
|
|
492
|
+
elif [ "$zone" = "gw" ] && [ ${#gw_filtered[@]} -gt 0 ]; then
|
|
493
|
+
local ri=${gw_filtered[$gw_cursor]}
|
|
494
|
+
[ "${gw_selected[$ri]}" = "0" ] && gw_selected[$ri]=1 || gw_selected[$ri]=0
|
|
495
|
+
fi
|
|
496
|
+
elif [ "$key" = $'\177' ] || [ "$key" = $'\010' ]; then
|
|
497
|
+
# Backspace — remove from search
|
|
498
|
+
if [ -n "$gw_search" ]; then
|
|
499
|
+
gw_search="${gw_search%?}"
|
|
500
|
+
gw_cursor=0
|
|
501
|
+
_build_gw_filtered
|
|
502
|
+
# Jump to gateway zone if we have results
|
|
503
|
+
if [ ${#gw_filtered[@]} -gt 0 ]; then
|
|
504
|
+
zone="gw"
|
|
669
505
|
fi
|
|
670
|
-
|
|
671
|
-
stty -echo -icanon min 1 < /dev/tty 2>/dev/null
|
|
672
|
-
printf "\\033[?25l" >&2
|
|
673
|
-
elif [ "${item_types[$cursor]}" != "separator" ]; then
|
|
674
|
-
[ "${selected[$cursor]}" = "0" ] && selected[$cursor]=1 || selected[$cursor]=0
|
|
675
506
|
fi
|
|
676
|
-
elif [ "$key" = "q" ]; then
|
|
677
|
-
|
|
507
|
+
elif [ "$key" = "q" ] && [ -z "$gw_search" ]; then
|
|
508
|
+
# Quit only when search is empty (otherwise 'q' is a search char)
|
|
509
|
+
for (( i = 0; i < cli_count; i++ )); do cli_selected[$i]=0; done
|
|
510
|
+
for (( i = 0; i < gw_count; i++ )); do gw_selected[$i]=0; done
|
|
678
511
|
break
|
|
512
|
+
elif [[ "$key" =~ [[:print:]] ]] && [ "$gw_loaded" = "1" ]; then
|
|
513
|
+
# Printable char — append to gateway search
|
|
514
|
+
gw_search="${gw_search}${key}"
|
|
515
|
+
gw_cursor=0
|
|
516
|
+
_build_gw_filtered
|
|
517
|
+
# Auto-switch to gateway zone
|
|
518
|
+
if [ ${#gw_filtered[@]} -gt 0 ]; then
|
|
519
|
+
zone="gw"
|
|
520
|
+
fi
|
|
679
521
|
fi
|
|
680
522
|
|
|
681
|
-
printf "\\033[%dA" "$
|
|
682
|
-
|
|
523
|
+
printf "\\033[%dA" "$total_lines" >&2
|
|
524
|
+
_sa_render
|
|
683
525
|
done
|
|
684
526
|
|
|
685
527
|
_agent_cleanup
|
|
686
528
|
trap - INT TERM
|
|
687
529
|
printf "\\n\\n" >&2
|
|
688
530
|
|
|
689
|
-
#
|
|
690
|
-
|
|
691
|
-
|
|
531
|
+
# ---- Collect selections ----
|
|
532
|
+
for (( i = 0; i < cli_count; i++ )); do
|
|
533
|
+
[ "${cli_selected[$i]}" = "1" ] && SELECTED_AGENT_INDICES+=("${cli_sources[$i]}")
|
|
534
|
+
done
|
|
692
535
|
|
|
693
|
-
#
|
|
536
|
+
# Build custom gateway selections
|
|
537
|
+
CUSTOM_GATEWAY_NAMES=()
|
|
538
|
+
CUSTOM_GATEWAY_MODELS=()
|
|
539
|
+
SELECTED_CUSTOM_GATEWAY_INDICES=()
|
|
694
540
|
local has_gateway=0
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
541
|
+
local gw_order=0
|
|
542
|
+
for (( i = 0; i < gw_count; i++ )); do
|
|
543
|
+
[ "${gw_selected[$i]}" != "1" ] && continue
|
|
544
|
+
has_gateway=1
|
|
545
|
+
local mid="${gw_ids[$i]}"
|
|
546
|
+
local prov="${mid%%/*}"
|
|
547
|
+
local mname="${mid#*/}"
|
|
548
|
+
local slug
|
|
549
|
+
slug=$(echo "${prov}-${mname}" | tr '[:upper:]' '[:lower:]' | tr ' .' '-' | tr -cd 'a-z0-9-')
|
|
550
|
+
slug="${slug}-gw"
|
|
551
|
+
CUSTOM_GATEWAY_NAMES+=("$slug")
|
|
552
|
+
CUSTOM_GATEWAY_MODELS+=("$mid")
|
|
553
|
+
SELECTED_CUSTOM_GATEWAY_INDICES+=("$gw_order")
|
|
554
|
+
gw_order=$(( gw_order + 1 ))
|
|
708
555
|
done
|
|
709
556
|
|
|
710
557
|
# If gateway models selected, ensure API key is stored
|
|
@@ -712,8 +559,9 @@ print('_all_model_labels=(' + ' '.join(shlex.quote(x) for x in labels) + ')')
|
|
|
712
559
|
printf "Gateway models selected. Setting up API key...\\n\\n" >&2
|
|
713
560
|
gateway_store_key "vercel-ai-gateway" || {
|
|
714
561
|
echo "ERROR: Failed to store gateway key. Gateway models will not work." >&2
|
|
715
|
-
SELECTED_GATEWAY_INDICES=()
|
|
716
562
|
SELECTED_CUSTOM_GATEWAY_INDICES=()
|
|
563
|
+
CUSTOM_GATEWAY_NAMES=()
|
|
564
|
+
CUSTOM_GATEWAY_MODELS=()
|
|
717
565
|
}
|
|
718
566
|
fi
|
|
719
567
|
}
|
|
@@ -896,11 +744,15 @@ cmd_init() {
|
|
|
896
744
|
# Non-interactive: auto-select all available CLI agents
|
|
897
745
|
SELECTED_AGENT_INDICES=("${AVAILABLE_AGENT_INDICES[@]}")
|
|
898
746
|
SELECTED_GATEWAY_INDICES=()
|
|
747
|
+
SELECTED_CUSTOM_GATEWAY_INDICES=()
|
|
748
|
+
CUSTOM_GATEWAY_NAMES=()
|
|
749
|
+
CUSTOM_GATEWAY_MODELS=()
|
|
899
750
|
fi
|
|
900
751
|
|
|
901
752
|
local _cli_count=${#SELECTED_AGENT_INDICES[@]}
|
|
902
753
|
local _gw_count=${#SELECTED_GATEWAY_INDICES[@]}
|
|
903
|
-
local
|
|
754
|
+
local _cgw_count=${#SELECTED_CUSTOM_GATEWAY_INDICES[@]}
|
|
755
|
+
local total_selected=$(( _cli_count + _gw_count + _cgw_count ))
|
|
904
756
|
if [ "$total_selected" -lt 2 ]; then
|
|
905
757
|
echo "ERROR: At least 2 agents must be selected." >&2
|
|
906
758
|
echo "moxie requires at least 2 agents for cross-model verification." >&2
|
|
@@ -931,6 +783,9 @@ cmd_init() {
|
|
|
931
783
|
for idx in "${SELECTED_GATEWAY_INDICES[@]}"; do
|
|
932
784
|
echo " - ${KNOWN_GATEWAY_LABELS[$idx]} (${KNOWN_GATEWAY_MODELS[$idx]}, AI Gateway)"
|
|
933
785
|
done
|
|
786
|
+
for idx in "${SELECTED_CUSTOM_GATEWAY_INDICES[@]}"; do
|
|
787
|
+
echo " - ${CUSTOM_GATEWAY_MODELS[$idx]} (AI Gateway)"
|
|
788
|
+
done
|
|
934
789
|
echo ""
|
|
935
790
|
|
|
936
791
|
# Create directory structure
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zachjxyz/moxie",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Run multiple AI coding agents through spec-driven phases with quorum convergence. Supports CLI agents (Claude, Codex, Qwen, Aider, Goose, Amp, Cline, Roo) and Vercel AI Gateway models.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"moxie": "bin/moxie"
|