@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.
Files changed (3) hide show
  1. package/bin/moxie +1 -1
  2. package/lib/phases.sh +263 -408
  3. package/package.json +1 -1
package/bin/moxie CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  set -euo pipefail
19
19
 
20
- MOXIE_VERSION="0.4.9"
20
+ MOXIE_VERSION="0.5.0"
21
21
  # Resolve symlinks (npm installs bin as a symlink)
22
22
  _self="$0"
23
23
  while [ -L "$_self" ]; do
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 models. Requires minimum 2 total.
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
- # Build unified item list: CLI agents, separator, gateway models
222
- local item_labels=()
223
- local item_meta=()
224
- local item_types=() # "cli", "separator", "gateway"
225
- local item_sources=() # index into KNOWN_AGENT_* or KNOWN_GATEWAY_*
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
- item_labels+=("$label")
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
- item_meta+=("$ver")
243
- item_types+=("cli")
244
- item_sources+=("$idx")
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
- # Separator + gateway search
250
- item_labels+=("--- Vercel AI Gateway ---")
251
- item_meta+=("")
252
- item_types+=("separator")
253
- item_sources+=("-1")
254
-
255
- item_labels+=("+ Search gateway models...")
256
- item_meta+=("browse all available models")
257
- item_types+=("custom_add")
258
- item_sources+=("-1")
259
- local _custom_label="+ Search gateway models..."
260
- local len=${#_custom_label}
261
- [ "$len" -gt "$max_label_len" ] && max_label_len=$len
262
-
263
- # Track custom models added during this session
264
- local custom_gateway_names=()
265
- local custom_gateway_models=()
266
-
267
- local count=${#item_labels[@]}
268
-
269
- # State: pre-select CLI agents, gateway unselected
270
- local cursor=0
271
- local selected=()
272
- for (( i = 0; i < count; i++ )); do
273
- if [ "${item_types[$i]}" = "cli" ]; then
274
- selected+=(1)
275
- else
276
- selected+=(0)
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
- done
275
+ fi
279
276
 
280
- # Skip separators for initial cursor position
281
- while [ "$cursor" -lt "$count" ] && [ "${item_types[$cursor]}" = "separator" ]; do
282
- cursor=$((cursor + 1))
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 + 40; i++ )); do sep="${sep}-"; done
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
- # submit row is at index $count
289
- local jump_back=$(( count + 3 ))
317
+ local gw_filtered=()
318
+ _build_gw_filtered
290
319
 
291
- _agent_render() {
320
+ _sa_render() {
321
+ # Count total selected
292
322
  local sel_count=0
293
- for (( i = 0; i < count; i++ )); do
294
- [ "${selected[$i]}" = "1" ] && sel_count=$(( sel_count + 1 ))
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
- for (( i = 0; i < count; i++ )); do
298
- if [ "${item_types[$i]}" = "separator" ]; then
299
- printf "\\r\\033[K \\033[2m%s\\033[0m\\n" "${item_labels[$i]}" >&2
300
- continue
301
- fi
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
- [ "$cursor" -eq "$i" ] && marker="> "
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
- [ "${selected[$i]}" = "1" ] && check="[x]"
310
- if [ -n "${item_meta[$i]}" ]; then
311
- printf "\\r\\033[K %s%s %-${max_label_len}s (%s)\\n" "$marker" "$check" "${item_labels[$i]}" "${item_meta[$i]}" >&2
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 %s%s %s\\n" "$marker" "$check" "${item_labels[$i]}" >&2
382
+ printf "\\r\\033[K\\n" >&2
314
383
  fi
315
- done
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
- [ "$cursor" -eq "$count" ] && submit_marker="> "
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/search\\033[0m" >&2
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). Search gateway models to add more:\\n\\n" >&2
408
+ printf "Select agents (at least 2):\\n\\n" >&2
331
409
  printf "\\033[?25l" >&2
332
- _agent_render
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: skip separators
354
- local new_cursor=$((cursor - 1))
355
- while [ "$new_cursor" -ge 0 ] && [ "${item_types[$new_cursor]}" = "separator" ]; do
356
- new_cursor=$((new_cursor - 1))
357
- done
358
- [ "$new_cursor" -ge 0 ] && cursor=$new_cursor
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: skip separators
361
- local new_cursor=$((cursor + 1))
362
- while [ "$new_cursor" -lt "$count" ] && [ "${item_types[$new_cursor]}" = "separator" ]; do
363
- new_cursor=$((new_cursor + 1))
364
- done
365
- [ "$new_cursor" -le "$count" ] && cursor=$new_cursor
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
- if [ "$cursor" -lt "$count" ] && [ "${item_types[$cursor]}" != "separator" ] && [ "${item_types[$cursor]}" != "custom_add" ]; then
371
- [ "${selected[$cursor]}" = "0" ] && selected[$cursor]=1 || selected[$cursor]=0
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 [ "$cursor" -eq "$count" ]; then
375
- # Submit
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 < count; i++ )); do
378
- [ "${selected[$i]}" = "1" ] && sel_count=$(( sel_count + 1 ))
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
- # Interactive search sub-TUI
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
- local _max_visible=15
464
-
465
- _search_render() {
466
- # Build filtered index list
467
- local _filtered=()
468
- local _lc_search
469
- _lc_search=$(echo "$_search" | tr '[:upper:]' '[:lower:]')
470
- for (( _si = 0; _si < ${#_all_model_ids[@]}; _si++ )); do
471
- if [ -z "$_search" ]; then
472
- _filtered+=("$_si")
473
- else
474
- local _lc_id
475
- _lc_id=$(echo "${_all_model_ids[$_si]}" | tr '[:upper:]' '[:lower:]')
476
- if [[ "$_lc_id" == *"$_lc_search"* ]]; then
477
- _filtered+=("$_si")
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
- for (( i = 0; i < count; i++ )); do selected[$i]=0; done
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" "$jump_back" >&2
682
- _agent_render
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
- # Export custom models for config writer
690
- CUSTOM_GATEWAY_NAMES=("${custom_gateway_names[@]}")
691
- CUSTOM_GATEWAY_MODELS=("${custom_gateway_models[@]}")
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
- # Collect selections by type
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
- for (( i = 0; i < count; i++ )); do
696
- [ "${selected[$i]}" != "1" ] && continue
697
- case "${item_types[$i]}" in
698
- cli) SELECTED_AGENT_INDICES+=("${item_sources[$i]}") ;;
699
- gateway)
700
- SELECTED_GATEWAY_INDICES+=("${item_sources[$i]}")
701
- has_gateway=1
702
- ;;
703
- custom_gateway)
704
- SELECTED_CUSTOM_GATEWAY_INDICES+=("${item_sources[$i]}")
705
- has_gateway=1
706
- ;;
707
- esac
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 total_selected=$(( _cli_count + _gw_count ))
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.4.9",
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"