@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.
- package/README.md +280 -0
- package/bin/moxie +4 -2
- package/lib/agents.sh +378 -46
- package/lib/gateway-agent.mjs +549 -0
- package/lib/gateway-cost.mjs +78 -0
- package/lib/gateway-keys.sh +118 -0
- package/lib/phases.sh +569 -75
- package/lib/platform.sh +78 -0
- package/lib/tokens.sh +121 -20
- package/package.json +9 -3
package/lib/platform.sh
ADDED
|
@@ -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:
|
|
18
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
|
4
|
-
"description": "Run multiple AI coding agents (Claude, Codex, Qwen
|
|
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",
|