@zachjxyz/moxie 0.2.4 → 0.3.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.
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env bash
2
+ # moxie/lib/platform.sh — Platform detection and OS-specific shims
3
+ # Compatible with Bash 3.2 (macOS default).
4
+
5
+ # ---- Platform detection ----
6
+
7
+ detect_platform() {
8
+ local kernel
9
+ kernel=$(uname -s 2>/dev/null || echo "unknown")
10
+ case "$kernel" in
11
+ Darwin) MOXIE_PLATFORM="darwin" ;;
12
+ Linux) MOXIE_PLATFORM="linux" ;;
13
+ *) MOXIE_PLATFORM="unknown" ;;
14
+ esac
15
+ }
16
+
17
+ # Run at source-time so MOXIE_PLATFORM is always set
18
+ detect_platform
19
+
20
+ # ---- stat wrappers ----
21
+
22
+ # Returns epoch mtime (integer) for a file.
23
+ stat_mtime() {
24
+ local file="$1"
25
+ case "$MOXIE_PLATFORM" in
26
+ darwin) stat -f "%m" "$file" 2>/dev/null || echo "0" ;;
27
+ linux) stat -c "%Y" "$file" 2>/dev/null || echo "0" ;;
28
+ *) echo "0" ;;
29
+ esac
30
+ }
31
+
32
+ # Returns human-readable modification date (e.g., "Apr 11") for a file.
33
+ stat_date() {
34
+ local file="$1"
35
+ case "$MOXIE_PLATFORM" in
36
+ darwin) stat -f "%Sm" -t "%b %d" "$file" 2>/dev/null || echo "unknown" ;;
37
+ linux) date -r "$file" "+%b %d" 2>/dev/null || echo "unknown" ;;
38
+ *) echo "unknown" ;;
39
+ esac
40
+ }
41
+
42
+ # ---- Package manager hints ----
43
+
44
+ # Returns the appropriate install command for coreutils on this platform.
45
+ coreutils_install_hint() {
46
+ case "$MOXIE_PLATFORM" in
47
+ darwin) echo "brew install coreutils" ;;
48
+ linux)
49
+ if command -v apt-get &>/dev/null; then
50
+ echo "sudo apt-get install coreutils"
51
+ elif command -v dnf &>/dev/null; then
52
+ echo "sudo dnf install coreutils"
53
+ elif command -v pacman &>/dev/null; then
54
+ echo "sudo pacman -S coreutils"
55
+ else
56
+ echo "Install coreutils via your package manager"
57
+ fi
58
+ ;;
59
+ *) echo "Install coreutils via your package manager" ;;
60
+ esac
61
+ }
62
+
63
+ # ---- Sleep inhibitor ----
64
+
65
+ # Returns a human-readable label for the sleep inhibitor on this platform.
66
+ sleep_inhibit_name() {
67
+ case "$MOXIE_PLATFORM" in
68
+ darwin) echo "caffeinate" ;;
69
+ linux)
70
+ if command -v systemd-inhibit &>/dev/null; then
71
+ echo "systemd-inhibit"
72
+ else
73
+ echo "(none)"
74
+ fi
75
+ ;;
76
+ *) echo "(none)" ;;
77
+ esac
78
+ }
package/lib/tokens.sh CHANGED
@@ -14,38 +14,64 @@ extract_tokens() {
14
14
  return
15
15
  fi
16
16
 
17
- # ---- Strategy 2: Claude JSON output ----
18
- # If output-format=json, extract from usage block
17
+ # ---- Strategy 2: NDJSON / JSON array with usage block ----
18
+ # Claude (--output-format json) and Qwen (-o json) emit NDJSON or a JSON
19
+ # array. The final "type":"result" object contains a top-level "usage" key.
20
+ # Logs may also contain non-JSON noise (e.g. hook errors on stderr).
19
21
  tokens=$(python3 -c "
20
22
  import json, sys
21
23
  try:
22
- # The log may have non-JSON preamble; find the JSON object
23
24
  with open('$logfile') as f:
24
25
  text = f.read()
25
- # Try parsing the whole thing as JSON first
26
+
27
+ # Collect candidate JSON objects from three formats:
28
+ # 1) Single JSON object 2) JSON array 3) NDJSON (one obj per line)
29
+ candidates = []
30
+ # 1) Single object
26
31
  try:
27
- d = json.loads(text)
28
- except:
29
- # Find the last { ... } block (Claude JSON output is at the end)
30
- import re
31
- matches = list(re.finditer(r'\{', text))
32
- d = None
33
- for m in reversed(matches):
32
+ obj = json.loads(text)
33
+ if isinstance(obj, list):
34
+ candidates.extend(obj) # 2) JSON array
35
+ elif isinstance(obj, dict):
36
+ candidates.append(obj)
37
+ except Exception:
38
+ # 3) NDJSON — try each line independently (skips noise)
39
+ for line in text.splitlines():
40
+ line = line.strip()
41
+ if not line or line[0] not in ('{', '['):
42
+ continue
34
43
  try:
35
- d = json.loads(text[m.start():])
36
- break
37
- except:
44
+ obj = json.loads(line)
45
+ if isinstance(obj, list):
46
+ candidates.extend(obj)
47
+ elif isinstance(obj, dict):
48
+ candidates.append(obj)
49
+ except Exception:
38
50
  continue
39
- if d and 'usage' in d:
40
- u = d['usage']
41
- total = u.get('input_tokens', 0) + u.get('output_tokens', 0)
42
- total += u.get('cache_creation_input_tokens', 0)
43
- total += u.get('cache_read_input_tokens', 0)
51
+
52
+ # Find the last 'result' object with a usage block
53
+ result_usage = None
54
+ for obj in reversed(candidates):
55
+ if isinstance(obj, dict) and obj.get('type') == 'result' and 'usage' in obj:
56
+ result_usage = obj['usage']
57
+ break
58
+
59
+ # Fallback: any object with a top-level 'usage' dict
60
+ if result_usage is None:
61
+ for obj in reversed(candidates):
62
+ if isinstance(obj, dict) and isinstance(obj.get('usage'), dict):
63
+ result_usage = obj['usage']
64
+ break
65
+
66
+ if result_usage:
67
+ total = result_usage.get('input_tokens', 0) + result_usage.get('output_tokens', 0)
68
+ total += result_usage.get('cache_creation_input_tokens', 0)
69
+ total += result_usage.get('cache_read_input_tokens', 0)
44
70
  if total > 0:
45
71
  print(total)
46
72
  sys.exit(0)
47
73
  sys.exit(1)
48
- except:
74
+ except Exception:
49
75
  sys.exit(1)
50
76
  " 2>/dev/null)
51
77
  if [ -n "$tokens" ] && [ "$tokens" -gt 0 ] 2>/dev/null; then
@@ -159,4 +185,79 @@ for f in glob.glob('$MOXIE_DIR/phases/*/token-usage.csv'):
159
185
  suffix = f' (+{unknown} unknown turns)' if unknown > 0 else ''
160
186
  print(f' {grand:,} tokens{suffix}')
161
187
  " 2>/dev/null || echo " (unknown)"
188
+
189
+ # Gateway cost reporting (if gateway agents are configured)
190
+ _show_gateway_cost
191
+ }
192
+
193
+ _show_gateway_cost() {
194
+ # Check if gateway section exists in config
195
+ local gw_endpoint
196
+ gw_endpoint=$(toml_get "$MOXIE_CONFIG" "gateway.endpoint" "")
197
+ [ -z "$gw_endpoint" ] && return
198
+
199
+ # Get API key (silently fail if not available)
200
+ local api_key
201
+ api_key=$(gateway_get_key "vercel-ai-gateway" 2>/dev/null) || return
202
+
203
+ # Find earliest timestamp across all CSV files to determine date range
204
+ local start_date
205
+ start_date=$(python3 -c "
206
+ import csv, glob, datetime
207
+ earliest = None
208
+ for f in glob.glob('$MOXIE_DIR/phases/*/token-usage.csv'):
209
+ with open(f) as fh:
210
+ for row in csv.DictReader(fh):
211
+ ts = row.get('timestamp', '')
212
+ if len(ts) >= 13:
213
+ try:
214
+ dt = datetime.datetime.strptime(ts[:13], '%m%d%y-%H%M%S')
215
+ if earliest is None or dt < earliest:
216
+ earliest = dt
217
+ except ValueError:
218
+ pass
219
+ if earliest:
220
+ print(earliest.strftime('%Y-%m-%d'))
221
+ else:
222
+ print(datetime.date.today().strftime('%Y-%m-%d'))
223
+ " 2>/dev/null)
224
+
225
+ local end_date
226
+ end_date=$(date +"%Y-%m-%d")
227
+
228
+ [ -z "$start_date" ] && return
229
+
230
+ # Call the reporting API
231
+ local cost_json
232
+ cost_json=$(GATEWAY_API_KEY="$api_key" node "$MOXIE_ROOT/lib/gateway-cost.mjs" "$gw_endpoint" "$start_date" "$end_date" 2>/dev/null) || {
233
+ echo ""
234
+ echo "Gateway cost:"
235
+ echo " (unavailable — check your API key or AI Gateway dashboard)"
236
+ return
237
+ }
238
+
239
+ [ -z "$cost_json" ] && return
240
+
241
+ # Parse and display
242
+ echo ""
243
+ echo "Gateway cost (via Vercel AI Gateway):"
244
+ python3 -c "
245
+ import json, sys
246
+ data = json.loads('''$cost_json''')
247
+ costs = data.get('costs', [])
248
+ if not costs:
249
+ print(' (no gateway usage recorded)')
250
+ sys.exit(0)
251
+ total = 0.0
252
+ for c in sorted(costs, key=lambda x: x.get('agent', '')):
253
+ agent = c.get('agent', 'unknown')
254
+ cost = c.get('total_cost', 0.0) or c.get('market_cost', 0.0)
255
+ inp = c.get('input_tokens', 0)
256
+ out = c.get('output_tokens', 0)
257
+ total += cost
258
+ print(f' {agent:<20s} \${cost:>8.2f} ({inp:,} input + {out:,} output tokens)')
259
+ print(f' {\"TOTAL\":<20s} \${total:>8.2f}')
260
+ print()
261
+ print(' Dashboard: https://vercel.com/~/ai')
262
+ " 2>/dev/null || echo " (could not parse cost data)"
162
263
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zachjxyz/moxie",
3
- "version": "0.2.4",
4
- "description": "Run multiple AI coding agents (Claude, Codex, Qwen) through spec-driven phases with unanimous quorum convergence",
3
+ "version": "0.3.0",
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"
7
7
  },
@@ -22,7 +22,13 @@
22
22
  "qwen",
23
23
  "spec-driven",
24
24
  "quorum",
25
- "ensemble"
25
+ "ensemble",
26
+ "gateway-agent",
27
+ "vercel",
28
+ "aider",
29
+ "goose",
30
+ "cline",
31
+ "roo"
26
32
  ],
27
33
  "author": "Zach Johnson",
28
34
  "license": "MIT",