@zachjxyz/moxie 0.2.3 → 0.2.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
@@ -16,7 +16,7 @@
16
16
 
17
17
  set -euo pipefail
18
18
 
19
- MOXIE_VERSION="0.2.3"
19
+ MOXIE_VERSION="0.2.5"
20
20
  # Resolve symlinks (npm installs bin as a symlink)
21
21
  _self="$0"
22
22
  while [ -L "$_self" ]; do
package/lib/agents.sh CHANGED
@@ -211,15 +211,14 @@ cmd_doctor() {
211
211
  echo "Dependencies:"
212
212
  _check_dep "python3" "Required for ledger parsing and token tracking" || all_ok=0
213
213
  _check_dep "caffeinate" "Keeps machine awake during runs (macOS)" || true # non-fatal
214
- if ! command -v timeout &>/dev/null; then
215
- if command -v gtimeout &>/dev/null; then
216
- echo " [OK] gtimeout (aliased as timeout)"
217
- else
218
- echo " [!!] timeout — not found. Install coreutils: brew install coreutils"
219
- echo " Agent turns will have no timeout protection."
220
- fi
221
- else
214
+ if command -v timeout &>/dev/null; then
222
215
  echo " [OK] timeout"
216
+ elif command -v gtimeout &>/dev/null; then
217
+ echo " [OK] coreutils (gtimeout aliased as timeout)"
218
+ else
219
+ echo " [!!] coreutils — not found. Install: brew install coreutils"
220
+ echo " Provides timeout protection for agent turns (gtimeout)."
221
+ all_ok=0
223
222
  fi
224
223
  echo ""
225
224
 
package/lib/phases.sh CHANGED
@@ -324,11 +324,11 @@ command = 'codex exec -c model_reasoning_effort="xhigh" --dangerously-bypass-app
324
324
  order = 1
325
325
 
326
326
  [agents.claude]
327
- command = "claude --dangerously-skip-permissions --effort max -p"
327
+ command = "claude --dangerously-skip-permissions --effort max --output-format json -p"
328
328
  order = 2
329
329
 
330
330
  [agents.qwen]
331
- command = "qwen --yolo"
331
+ command = "qwen --yolo -o json"
332
332
  order = 3
333
333
 
334
334
  [settings]
@@ -378,11 +378,12 @@ TOML
378
378
 
379
379
  echo ""
380
380
  echo "Next steps:"
381
- echo " 1. Review .moxie/config.toml (agent commands, timeouts)"
381
+ echo " 1. Run: moxie doctor (verify dependencies and environment)"
382
+ echo " 2. Review .moxie/config.toml (agent commands, timeouts)"
382
383
  if [ ! -d "$MOXIE_DIR/context" ] || [ -z "$(ls -A "$MOXIE_DIR/context" 2>/dev/null)" ]; then
383
384
  echo " Tip: add context docs to .moxie/context/ (roadmaps, PRDs, etc.)"
384
385
  fi
385
- echo " 2. Run: moxie start (background, caffeinated)"
386
+ echo " 3. Run: moxie start (background, caffeinated)"
386
387
  echo " Or: moxie run (foreground, caffeinated)"
387
388
  }
388
389
 
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zachjxyz/moxie",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Run multiple AI coding agents (Claude, Codex, Qwen) through spec-driven phases with unanimous quorum convergence",
5
5
  "bin": {
6
6
  "moxie": "bin/moxie"