@zachjxyz/moxie 0.4.4 → 0.4.6

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.4"
20
+ MOXIE_VERSION="0.4.6"
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,19 @@ _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 _custom_label="+ Add custom model..."
272
+ local len=${#_custom_label}
273
+ [ "$len" -gt "$max_label_len" ] && max_label_len=$len
274
+
275
+ # Track custom models added during this session
276
+ local custom_gateway_names=()
277
+ local custom_gateway_models=()
278
+
265
279
  local count=${#item_labels[@]}
266
280
 
267
281
  # State: pre-select CLI agents, gateway unselected
@@ -299,6 +313,10 @@ _select_agents() {
299
313
  fi
300
314
  local marker=" "
301
315
  [ "$cursor" -eq "$i" ] && marker="> "
316
+ if [ "${item_types[$i]}" = "custom_add" ]; then
317
+ printf "\\r\\033[K %s \\033[36m%s\\033[0m \\033[2m(%s)\\033[0m\\n" "$marker" "${item_labels[$i]}" "${item_meta[$i]}" >&2
318
+ continue
319
+ fi
302
320
  local check="[ ]"
303
321
  [ "${selected[$i]}" = "1" ] && check="[x]"
304
322
  if [ -n "${item_meta[$i]}" ]; then
@@ -318,7 +336,7 @@ _select_agents() {
318
336
  printf "\\r\\033[K %s\\033[2m%s\\033[0m\\n" "$submit_marker" "Submit (need at least 2)" >&2
319
337
  fi
320
338
 
321
- printf "\\r\\033[K\\n\\033[K \\033[2m↑↓ navigate · space/enter toggle · enter submit (min 2)\\033[0m" >&2
339
+ printf "\\r\\033[K\\n\\033[K \\033[2m↑↓ navigate · space toggle · enter submit/add · 'moxie models' to browse\\033[0m" >&2
322
340
  }
323
341
 
324
342
  printf "Select agents (at least 2). CLI agents pre-selected, gateway models available below:\\n\\n" >&2
@@ -361,16 +379,82 @@ _select_agents() {
361
379
  esac
362
380
  fi
363
381
  elif [ "$key" = " " ]; then
364
- if [ "$cursor" -lt "$count" ] && [ "${item_types[$cursor]}" != "separator" ]; then
382
+ if [ "$cursor" -lt "$count" ] && [ "${item_types[$cursor]}" != "separator" ] && [ "${item_types[$cursor]}" != "custom_add" ]; then
365
383
  [ "${selected[$cursor]}" = "0" ] && selected[$cursor]=1 || selected[$cursor]=0
366
384
  fi
367
385
  elif [ "$key" = "" ]; then
368
386
  if [ "$cursor" -eq "$count" ]; then
387
+ # Submit
369
388
  local sel_count=0
370
389
  for (( i = 0; i < count; i++ )); do
371
390
  [ "${selected[$i]}" = "1" ] && sel_count=$(( sel_count + 1 ))
372
391
  done
373
392
  [ "$sel_count" -ge 2 ] && break
393
+ elif [ "${item_types[$cursor]}" = "custom_add" ]; then
394
+ # Prompt for custom model string
395
+ printf "\\033[?25h" >&2
396
+ stty echo icanon < /dev/tty 2>/dev/null
397
+ printf "\\n\\r\\033[K Model ID (provider/model-name): " >&2
398
+ local custom_model=""
399
+ read -r custom_model < /dev/tty
400
+ stty -echo -icanon min 1 < /dev/tty 2>/dev/null
401
+ printf "\\033[?25l" >&2
402
+
403
+ if [ -n "$custom_model" ] && [[ "$custom_model" == */* ]]; then
404
+ # Derive a short name from the model ID: provider-modelbase
405
+ local custom_provider="${custom_model%%/*}"
406
+ local custom_name_part="${custom_model#*/}"
407
+ local custom_slug="${custom_provider}-${custom_name_part}"
408
+ # Sanitize slug for TOML key
409
+ custom_slug=$(echo "$custom_slug" | tr '[:upper:]' '[:lower:]' | tr ' .' '-' | tr -cd 'a-z0-9-')
410
+ custom_slug="${custom_slug}-gw"
411
+
412
+ # Insert before the custom_add row (which is at current cursor)
413
+ local insert_at=$cursor
414
+
415
+ # Shift arrays to insert new item
416
+ local new_labels=() new_meta=() new_types=() new_sources=() new_selected=()
417
+ for (( i = 0; i < count; i++ )); do
418
+ if [ "$i" -eq "$insert_at" ]; then
419
+ new_labels+=("$custom_name_part")
420
+ new_meta+=("$custom_model")
421
+ new_types+=("custom_gateway")
422
+ new_sources+=("${#custom_gateway_names[@]}")
423
+ new_selected+=(1)
424
+ fi
425
+ new_labels+=("${item_labels[$i]}")
426
+ new_meta+=("${item_meta[$i]}")
427
+ new_types+=("${item_types[$i]}")
428
+ new_sources+=("${item_sources[$i]}")
429
+ new_selected+=("${selected[$i]}")
430
+ done
431
+
432
+ item_labels=("${new_labels[@]}")
433
+ item_meta=("${new_meta[@]}")
434
+ item_types=("${new_types[@]}")
435
+ item_sources=("${new_sources[@]}")
436
+ selected=("${new_selected[@]}")
437
+
438
+ custom_gateway_names+=("$custom_slug")
439
+ custom_gateway_models+=("$custom_model")
440
+
441
+ count=${#item_labels[@]}
442
+ jump_back=$(( count + 3 ))
443
+
444
+ # Update max_label_len if needed
445
+ local len=${#custom_name_part}
446
+ [ "$len" -gt "$max_label_len" ] && max_label_len=$len
447
+ sep=""
448
+ for (( i = 0; i < max_label_len + 40; i++ )); do sep="${sep}-"; done
449
+ else
450
+ # Invalid — erase the prompt line
451
+ if [ -n "$custom_model" ]; then
452
+ printf "\\r\\033[K \\033[31mInvalid format. Use provider/model-name (e.g. anthropic/claude-sonnet-4-6)\\033[0m" >&2
453
+ sleep 1
454
+ fi
455
+ fi
456
+ # Erase the prompt lines and re-render
457
+ printf "\\033[2A" >&2
374
458
  elif [ "${item_types[$cursor]}" != "separator" ]; then
375
459
  [ "${selected[$cursor]}" = "0" ] && selected[$cursor]=1 || selected[$cursor]=0
376
460
  fi
@@ -387,6 +471,10 @@ _select_agents() {
387
471
  trap - INT TERM
388
472
  printf "\\n\\n" >&2
389
473
 
474
+ # Export custom models for config writer
475
+ CUSTOM_GATEWAY_NAMES=("${custom_gateway_names[@]}")
476
+ CUSTOM_GATEWAY_MODELS=("${custom_gateway_models[@]}")
477
+
390
478
  # Collect selections by type
391
479
  local has_gateway=0
392
480
  for (( i = 0; i < count; i++ )); do
@@ -397,6 +485,10 @@ _select_agents() {
397
485
  SELECTED_GATEWAY_INDICES+=("${item_sources[$i]}")
398
486
  has_gateway=1
399
487
  ;;
488
+ custom_gateway)
489
+ SELECTED_CUSTOM_GATEWAY_INDICES+=("${item_sources[$i]}")
490
+ has_gateway=1
491
+ ;;
400
492
  esac
401
493
  done
402
494
 
@@ -406,6 +498,7 @@ _select_agents() {
406
498
  gateway_store_key "vercel-ai-gateway" || {
407
499
  echo "ERROR: Failed to store gateway key. Gateway models will not work." >&2
408
500
  SELECTED_GATEWAY_INDICES=()
501
+ SELECTED_CUSTOM_GATEWAY_INDICES=()
409
502
  }
410
503
  fi
411
504
  }
@@ -424,7 +517,10 @@ path = "spec.md"
424
517
  HEADER
425
518
 
426
519
  # Gateway section (if any gateway models selected)
427
- if [ ${#SELECTED_GATEWAY_INDICES[@]} -gt 0 ]; then
520
+ local has_any_gw=0
521
+ [ ${#SELECTED_GATEWAY_INDICES[@]} -gt 0 ] && has_any_gw=1
522
+ [ ${#SELECTED_CUSTOM_GATEWAY_INDICES[@]} -gt 0 ] && has_any_gw=1
523
+ if [ "$has_any_gw" = "1" ]; then
428
524
  local run_id
429
525
  run_id="run-$(date +%Y%m%d-%H%M%S)-$$"
430
526
  cat >> "$config_file" <<GATEWAY
@@ -458,7 +554,7 @@ AGENT
458
554
  done
459
555
  fi
460
556
 
461
- # Gateway agents
557
+ # Gateway agents (built-in)
462
558
  for idx in "${SELECTED_GATEWAY_INDICES[@]}"; do
463
559
  local name="${KNOWN_GATEWAY_NAMES[$idx]}"
464
560
  local model="${KNOWN_GATEWAY_MODELS[$idx]}"
@@ -468,6 +564,20 @@ type = "gateway"
468
564
  model = "${model}"
469
565
  order = ${order}
470
566
 
567
+ GWAGENT
568
+ order=$((order + 1))
569
+ done
570
+
571
+ # Custom gateway agents (added during init)
572
+ for idx in "${SELECTED_CUSTOM_GATEWAY_INDICES[@]}"; do
573
+ local name="${CUSTOM_GATEWAY_NAMES[$idx]}"
574
+ local model="${CUSTOM_GATEWAY_MODELS[$idx]}"
575
+ cat >> "$config_file" <<GWAGENT
576
+ [agents.${name}]
577
+ type = "gateway"
578
+ model = "${model}"
579
+ order = ${order}
580
+
471
581
  GWAGENT
472
582
  order=$((order + 1))
473
583
  done
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zachjxyz/moxie",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
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"