@zachjxyz/moxie 0.4.3 → 0.4.5

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 CHANGED
@@ -12,11 +12,12 @@
12
12
  # moxie status
13
13
  # moxie cost
14
14
  # moxie logs [phase]
15
+ # moxie models [--filter <search>]
15
16
  # moxie agents
16
17
 
17
18
  set -euo pipefail
18
19
 
19
- MOXIE_VERSION="0.4.3"
20
+ MOXIE_VERSION="0.4.5"
20
21
  # Resolve symlinks (npm installs bin as a symlink)
21
22
  _self="$0"
22
23
  while [ -L "$_self" ]; do
@@ -48,6 +49,7 @@ case "$COMMAND" in
48
49
  status) cmd_status "$@" ;;
49
50
  cost) cmd_cost "$@" ;;
50
51
  logs) cmd_logs "$@" ;;
52
+ models) cmd_models "$@" ;;
51
53
  agents) cmd_agents "$@" ;;
52
54
  doctor) cmd_doctor "$@" ;;
53
55
  version) echo "moxie $MOXIE_VERSION" ;;
@@ -63,6 +65,7 @@ Commands:
63
65
  status Show phase progress, quorum state, and running status
64
66
  cost Token usage breakdown by phase and agent
65
67
  logs Tail or view logs for a phase
68
+ models List available AI Gateway models
66
69
  agents List configured agents
67
70
  doctor Check agent CLIs, auth, and project health
68
71
  version Print version
@@ -80,6 +83,8 @@ Usage:
80
83
  moxie run --dry-run Trace without spawning agents
81
84
  moxie status Show progress and quorum
82
85
  moxie cost Show token usage summary
86
+ moxie models List all AI Gateway models
87
+ moxie models -f anthropic Filter models by provider/name
83
88
  moxie doctor Check agents and dependencies
84
89
 
85
90
  Pipeline: rfc → audit → fix → plan → build
package/lib/agents.sh CHANGED
@@ -554,6 +554,109 @@ cmd_agents() {
554
554
  done
555
555
  }
556
556
 
557
+ # ---- List available gateway models ----
558
+
559
+ cmd_models() {
560
+ local filter=""
561
+ while [ $# -gt 0 ]; do
562
+ case "$1" in
563
+ --filter|-f) filter="$2"; shift 2 ;;
564
+ --help|-h)
565
+ cat <<'EOF'
566
+ Usage: moxie models [--filter <search>]
567
+
568
+ List models available through your Vercel AI Gateway.
569
+ Requires a stored gateway API key (run 'moxie init' to set one up).
570
+
571
+ Options:
572
+ --filter, -f <search> Filter models by provider or name (e.g. "anthropic", "gpt")
573
+
574
+ Examples:
575
+ moxie models List all available models
576
+ moxie models -f anthropic Show only Anthropic models
577
+ moxie models -f gemini Search for Gemini models
578
+ EOF
579
+ return 0
580
+ ;;
581
+ *) filter="$1"; shift ;;
582
+ esac
583
+ done
584
+
585
+ if ! command -v node &>/dev/null; then
586
+ echo "ERROR: node not found on PATH. Required for gateway API calls." >&2
587
+ return 1
588
+ fi
589
+
590
+ if ! gateway_has_key "vercel-ai-gateway"; then
591
+ echo "ERROR: No AI Gateway API key configured." >&2
592
+ echo "Run 'moxie init' to set one up, or store a key with:" >&2
593
+ echo " moxie doctor (will prompt for key setup)" >&2
594
+ return 1
595
+ fi
596
+
597
+ local api_key
598
+ api_key=$(gateway_get_key "vercel-ai-gateway") || {
599
+ echo "ERROR: Failed to retrieve gateway API key." >&2
600
+ return 1
601
+ }
602
+
603
+ local endpoint="https://ai-gateway.vercel.sh"
604
+ if [ -f "${MOXIE_CONFIG:-}" ]; then
605
+ local cfg_endpoint
606
+ cfg_endpoint=$(toml_get "$MOXIE_CONFIG" "gateway.endpoint" "") || true
607
+ [ -n "$cfg_endpoint" ] && endpoint="$cfg_endpoint"
608
+ fi
609
+
610
+ local json
611
+ json=$(GATEWAY_API_KEY="$api_key" node "$MOXIE_LIB/gateway-models.mjs" "$endpoint" "$filter" 2>&1) || {
612
+ echo "ERROR: Failed to fetch models from gateway." >&2
613
+ echo "$json" >&2
614
+ return 1
615
+ }
616
+
617
+ # Parse JSON with python3 for pretty display
618
+ python3 -c "
619
+ import json, sys
620
+
621
+ data = json.loads(sys.stdin.read())
622
+ models = data.get('models', [])
623
+
624
+ if not models:
625
+ print('No models found.' + (' Try a different filter.' if '$filter' else ''))
626
+ sys.exit(0)
627
+
628
+ # Group by provider
629
+ providers = {}
630
+ for m in models:
631
+ p = m['provider']
632
+ providers.setdefault(p, []).append(m)
633
+
634
+ # Hardcoded models for marking
635
+ hardcoded = set('''$(printf '%s\n' "${KNOWN_GATEWAY_MODELS[@]}")'''.strip().split('\n'))
636
+
637
+ total = len(models)
638
+ filter_note = ' matching \"$filter\"' if '$filter' else ''
639
+ print(f'Available gateway models ({total}{filter_note}):')
640
+ print()
641
+
642
+ for provider in sorted(providers.keys()):
643
+ pmodels = providers[provider]
644
+ print(f' \033[1m{provider}\033[0m ({len(pmodels)} models)')
645
+ for m in pmodels:
646
+ marker = ' \033[32m●\033[0m' if m['id'] in hardcoded else ' '
647
+ print(f' {marker} {m[\"id\"]}')
648
+ print()
649
+
650
+ print('\033[32m●\033[0m = currently in moxie defaults')
651
+ print()
652
+ print('To use any model, add it to .moxie/config.toml:')
653
+ print(' [agents.my-model]')
654
+ print(' type = \"gateway\"')
655
+ print(' model = \"provider/model-name\"')
656
+ print(' order = 10')
657
+ " <<< "$json"
658
+ }
659
+
557
660
  # ---- Doctor: health checks ----
558
661
 
559
662
  cmd_doctor() {
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ // moxie/lib/gateway-models.mjs — List available models from Vercel AI Gateway
3
+ // Zero npm dependencies. Node 14+ compatible.
4
+ // Usage: GATEWAY_API_KEY=xxx node gateway-models.mjs [endpoint] [--filter provider]
5
+
6
+ import https from 'https';
7
+ import http from 'http';
8
+
9
+ const API_KEY = process.env.GATEWAY_API_KEY;
10
+ const endpoint = process.argv[2] || 'https://ai-gateway.vercel.sh';
11
+ const filter = process.argv[3] || '';
12
+
13
+ if (!API_KEY) { process.stderr.write('ERROR: GATEWAY_API_KEY not set\n'); process.exit(1); }
14
+
15
+ const url = new URL(`${endpoint}/v1/models`);
16
+
17
+ const isHttps = url.protocol === 'https:';
18
+ const mod = isHttps ? https : http;
19
+
20
+ const options = {
21
+ hostname: url.hostname,
22
+ port: url.port || (isHttps ? 443 : 80),
23
+ path: url.pathname + url.search,
24
+ method: 'GET',
25
+ headers: {
26
+ 'Authorization': `Bearer ${API_KEY}`,
27
+ 'Accept': 'application/json',
28
+ },
29
+ };
30
+
31
+ const req = mod.request(options, (res) => {
32
+ let body = '';
33
+ res.on('data', (chunk) => { body += chunk; });
34
+ res.on('end', () => {
35
+ if (res.statusCode !== 200) {
36
+ process.stderr.write(`API error ${res.statusCode}: ${body.slice(0, 500)}\n`);
37
+ process.exit(1);
38
+ }
39
+
40
+ try {
41
+ const data = JSON.parse(body);
42
+ // OpenAI-compatible /v1/models returns { data: [...] }
43
+ let models = data.data || data.models || data || [];
44
+ if (!Array.isArray(models)) models = [];
45
+
46
+ // Normalize each model to { id, provider, name, created }
47
+ const normalized = models.map((m) => {
48
+ const id = m.id || m.model || '';
49
+ const parts = id.split('/');
50
+ const provider = parts.length > 1 ? parts[0] : (m.owned_by || 'unknown');
51
+ const name = parts.length > 1 ? parts.slice(1).join('/') : id;
52
+ return {
53
+ id,
54
+ provider,
55
+ name,
56
+ created: m.created || 0,
57
+ };
58
+ });
59
+
60
+ // Apply filter if provided
61
+ const filtered = filter
62
+ ? normalized.filter((m) => m.id.toLowerCase().includes(filter.toLowerCase()))
63
+ : normalized;
64
+
65
+ // Sort by provider, then name
66
+ filtered.sort((a, b) => {
67
+ if (a.provider !== b.provider) return a.provider.localeCompare(b.provider);
68
+ return a.name.localeCompare(b.name);
69
+ });
70
+
71
+ process.stdout.write(JSON.stringify({ models: filtered }) + '\n');
72
+ } catch (err) {
73
+ process.stderr.write(`Failed to parse response: ${err.message}\n`);
74
+ process.exit(1);
75
+ }
76
+ });
77
+ });
78
+
79
+ req.on('error', (err) => {
80
+ process.stderr.write(`Network error: ${err.message}\n`);
81
+ process.exit(1);
82
+ });
83
+
84
+ req.end();
package/lib/phases.sh CHANGED
@@ -216,6 +216,7 @@ _select_context_docs() {
216
216
  _select_agents() {
217
217
  SELECTED_AGENT_INDICES=()
218
218
  SELECTED_GATEWAY_INDICES=()
219
+ SELECTED_CUSTOM_GATEWAY_INDICES=()
219
220
 
220
221
  # Build unified item list: CLI agents, separator, gateway models
221
222
  local item_labels=()
@@ -262,6 +263,18 @@ _select_agents() {
262
263
  [ "$len" -gt "$max_label_len" ] && max_label_len=$len
263
264
  done
264
265
 
266
+ # Custom model entry point
267
+ item_labels+=("+ Add custom model...")
268
+ item_meta+=("type provider/model-name")
269
+ item_types+=("custom_add")
270
+ item_sources+=("-1")
271
+ local len=${#item_labels[-1]}
272
+ [ "$len" -gt "$max_label_len" ] && max_label_len=$len
273
+
274
+ # Track custom models added during this session
275
+ local custom_gateway_names=()
276
+ local custom_gateway_models=()
277
+
265
278
  local count=${#item_labels[@]}
266
279
 
267
280
  # State: pre-select CLI agents, gateway unselected
@@ -299,6 +312,10 @@ _select_agents() {
299
312
  fi
300
313
  local marker=" "
301
314
  [ "$cursor" -eq "$i" ] && marker="> "
315
+ if [ "${item_types[$i]}" = "custom_add" ]; then
316
+ printf "\\r\\033[K %s \\033[36m%s\\033[0m \\033[2m(%s)\\033[0m\\n" "$marker" "${item_labels[$i]}" "${item_meta[$i]}" >&2
317
+ continue
318
+ fi
302
319
  local check="[ ]"
303
320
  [ "${selected[$i]}" = "1" ] && check="[x]"
304
321
  if [ -n "${item_meta[$i]}" ]; then
@@ -318,7 +335,7 @@ _select_agents() {
318
335
  printf "\\r\\033[K %s\\033[2m%s\\033[0m\\n" "$submit_marker" "Submit (need at least 2)" >&2
319
336
  fi
320
337
 
321
- printf "\\r\\033[K\\n\\033[K \\033[2m↑↓ navigate · space/enter toggle · enter submit (min 2)\\033[0m" >&2
338
+ printf "\\r\\033[K\\n\\033[K \\033[2m↑↓ navigate · space toggle · enter submit/add · 'moxie models' to browse\\033[0m" >&2
322
339
  }
323
340
 
324
341
  printf "Select agents (at least 2). CLI agents pre-selected, gateway models available below:\\n\\n" >&2
@@ -361,16 +378,82 @@ _select_agents() {
361
378
  esac
362
379
  fi
363
380
  elif [ "$key" = " " ]; then
364
- if [ "$cursor" -lt "$count" ] && [ "${item_types[$cursor]}" != "separator" ]; then
381
+ if [ "$cursor" -lt "$count" ] && [ "${item_types[$cursor]}" != "separator" ] && [ "${item_types[$cursor]}" != "custom_add" ]; then
365
382
  [ "${selected[$cursor]}" = "0" ] && selected[$cursor]=1 || selected[$cursor]=0
366
383
  fi
367
384
  elif [ "$key" = "" ]; then
368
385
  if [ "$cursor" -eq "$count" ]; then
386
+ # Submit
369
387
  local sel_count=0
370
388
  for (( i = 0; i < count; i++ )); do
371
389
  [ "${selected[$i]}" = "1" ] && sel_count=$(( sel_count + 1 ))
372
390
  done
373
391
  [ "$sel_count" -ge 2 ] && break
392
+ elif [ "${item_types[$cursor]}" = "custom_add" ]; then
393
+ # Prompt for custom model string
394
+ printf "\\033[?25h" >&2
395
+ stty echo icanon < /dev/tty 2>/dev/null
396
+ printf "\\n\\r\\033[K Model ID (provider/model-name): " >&2
397
+ local custom_model=""
398
+ read -r custom_model < /dev/tty
399
+ stty -echo -icanon min 1 < /dev/tty 2>/dev/null
400
+ printf "\\033[?25l" >&2
401
+
402
+ if [ -n "$custom_model" ] && [[ "$custom_model" == */* ]]; then
403
+ # Derive a short name from the model ID: provider-modelbase
404
+ local custom_provider="${custom_model%%/*}"
405
+ local custom_name_part="${custom_model#*/}"
406
+ local custom_slug="${custom_provider}-${custom_name_part}"
407
+ # Sanitize slug for TOML key
408
+ custom_slug=$(echo "$custom_slug" | tr '[:upper:]' '[:lower:]' | tr ' .' '-' | tr -cd 'a-z0-9-')
409
+ custom_slug="${custom_slug}-gw"
410
+
411
+ # Insert before the custom_add row (which is at current cursor)
412
+ local insert_at=$cursor
413
+
414
+ # Shift arrays to insert new item
415
+ local new_labels=() new_meta=() new_types=() new_sources=() new_selected=()
416
+ for (( i = 0; i < count; i++ )); do
417
+ if [ "$i" -eq "$insert_at" ]; then
418
+ new_labels+=("$custom_name_part")
419
+ new_meta+=("$custom_model")
420
+ new_types+=("custom_gateway")
421
+ new_sources+=("${#custom_gateway_names[@]}")
422
+ new_selected+=(1)
423
+ fi
424
+ new_labels+=("${item_labels[$i]}")
425
+ new_meta+=("${item_meta[$i]}")
426
+ new_types+=("${item_types[$i]}")
427
+ new_sources+=("${item_sources[$i]}")
428
+ new_selected+=("${selected[$i]}")
429
+ done
430
+
431
+ item_labels=("${new_labels[@]}")
432
+ item_meta=("${new_meta[@]}")
433
+ item_types=("${new_types[@]}")
434
+ item_sources=("${new_sources[@]}")
435
+ selected=("${new_selected[@]}")
436
+
437
+ custom_gateway_names+=("$custom_slug")
438
+ custom_gateway_models+=("$custom_model")
439
+
440
+ count=${#item_labels[@]}
441
+ jump_back=$(( count + 3 ))
442
+
443
+ # Update max_label_len if needed
444
+ local len=${#custom_name_part}
445
+ [ "$len" -gt "$max_label_len" ] && max_label_len=$len
446
+ sep=""
447
+ for (( i = 0; i < max_label_len + 40; i++ )); do sep="${sep}-"; done
448
+ else
449
+ # Invalid — erase the prompt line
450
+ if [ -n "$custom_model" ]; then
451
+ printf "\\r\\033[K \\033[31mInvalid format. Use provider/model-name (e.g. anthropic/claude-sonnet-4-6)\\033[0m" >&2
452
+ sleep 1
453
+ fi
454
+ fi
455
+ # Erase the prompt lines and re-render
456
+ printf "\\033[2A" >&2
374
457
  elif [ "${item_types[$cursor]}" != "separator" ]; then
375
458
  [ "${selected[$cursor]}" = "0" ] && selected[$cursor]=1 || selected[$cursor]=0
376
459
  fi
@@ -387,6 +470,10 @@ _select_agents() {
387
470
  trap - INT TERM
388
471
  printf "\\n\\n" >&2
389
472
 
473
+ # Export custom models for config writer
474
+ CUSTOM_GATEWAY_NAMES=("${custom_gateway_names[@]}")
475
+ CUSTOM_GATEWAY_MODELS=("${custom_gateway_models[@]}")
476
+
390
477
  # Collect selections by type
391
478
  local has_gateway=0
392
479
  for (( i = 0; i < count; i++ )); do
@@ -397,6 +484,10 @@ _select_agents() {
397
484
  SELECTED_GATEWAY_INDICES+=("${item_sources[$i]}")
398
485
  has_gateway=1
399
486
  ;;
487
+ custom_gateway)
488
+ SELECTED_CUSTOM_GATEWAY_INDICES+=("${item_sources[$i]}")
489
+ has_gateway=1
490
+ ;;
400
491
  esac
401
492
  done
402
493
 
@@ -406,6 +497,7 @@ _select_agents() {
406
497
  gateway_store_key "vercel-ai-gateway" || {
407
498
  echo "ERROR: Failed to store gateway key. Gateway models will not work." >&2
408
499
  SELECTED_GATEWAY_INDICES=()
500
+ SELECTED_CUSTOM_GATEWAY_INDICES=()
409
501
  }
410
502
  fi
411
503
  }
@@ -424,7 +516,10 @@ path = "spec.md"
424
516
  HEADER
425
517
 
426
518
  # Gateway section (if any gateway models selected)
427
- if [ ${#SELECTED_GATEWAY_INDICES[@]} -gt 0 ]; then
519
+ local has_any_gw=0
520
+ [ ${#SELECTED_GATEWAY_INDICES[@]} -gt 0 ] && has_any_gw=1
521
+ [ ${#SELECTED_CUSTOM_GATEWAY_INDICES[@]} -gt 0 ] && has_any_gw=1
522
+ if [ "$has_any_gw" = "1" ]; then
428
523
  local run_id
429
524
  run_id="run-$(date +%Y%m%d-%H%M%S)-$$"
430
525
  cat >> "$config_file" <<GATEWAY
@@ -458,7 +553,7 @@ AGENT
458
553
  done
459
554
  fi
460
555
 
461
- # Gateway agents
556
+ # Gateway agents (built-in)
462
557
  for idx in "${SELECTED_GATEWAY_INDICES[@]}"; do
463
558
  local name="${KNOWN_GATEWAY_NAMES[$idx]}"
464
559
  local model="${KNOWN_GATEWAY_MODELS[$idx]}"
@@ -468,6 +563,20 @@ type = "gateway"
468
563
  model = "${model}"
469
564
  order = ${order}
470
565
 
566
+ GWAGENT
567
+ order=$((order + 1))
568
+ done
569
+
570
+ # Custom gateway agents (added during init)
571
+ for idx in "${SELECTED_CUSTOM_GATEWAY_INDICES[@]}"; do
572
+ local name="${CUSTOM_GATEWAY_NAMES[$idx]}"
573
+ local model="${CUSTOM_GATEWAY_MODELS[$idx]}"
574
+ cat >> "$config_file" <<GWAGENT
575
+ [agents.${name}]
576
+ type = "gateway"
577
+ model = "${model}"
578
+ order = ${order}
579
+
471
580
  GWAGENT
472
581
  order=$((order + 1))
473
582
  done
@@ -1487,11 +1596,27 @@ cmd_status() {
1487
1596
  echo "Context: $ctx_count document(s) in .moxie/context/"
1488
1597
  fi
1489
1598
 
1490
- # Quick agent health check
1599
+ # Quick agent health check (includes persisted degradation)
1600
+ local deg_file="$MOXIE_DIR/degraded.json"
1491
1601
  local agent_health=""
1492
1602
  local all_healthy=1
1603
+ local any_degraded=0
1493
1604
  for name in "${AGENT_NAMES[@]}"; do
1494
- if _is_gateway_agent "$name"; then
1605
+ # Check if agent is degraded (persisted across phases)
1606
+ local is_deg=0
1607
+ if [ -f "$deg_file" ]; then
1608
+ is_deg=$(python3 -c "
1609
+ import json
1610
+ with open('$deg_file') as f:
1611
+ d = json.load(f)
1612
+ print('1' if d.get('$name', False) else '0')
1613
+ " 2>/dev/null)
1614
+ fi
1615
+
1616
+ if [ "$is_deg" = "1" ]; then
1617
+ agent_health="$agent_health $name[DEGRADED]"
1618
+ any_degraded=1
1619
+ elif _is_gateway_agent "$name"; then
1495
1620
  if command -v node &>/dev/null && gateway_has_key "vercel-ai-gateway"; then
1496
1621
  agent_health="$agent_health $name[ok]"
1497
1622
  else
@@ -1515,6 +1640,9 @@ cmd_status() {
1515
1640
  if [ "$all_healthy" = "0" ]; then
1516
1641
  echo " (some agents missing — run 'moxie doctor' for details)"
1517
1642
  fi
1643
+ if [ "$any_degraded" = "1" ]; then
1644
+ echo " (degraded agents are skipped — rm .moxie/degraded.json to reset)"
1645
+ fi
1518
1646
  echo ""
1519
1647
 
1520
1648
  for phase in "${PHASES[@]}"; do
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zachjxyz/moxie",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
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"