create-merlin-brain 5.3.3 → 5.3.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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # duo-mode-read.sh — reads duo-mode.json, applies 24h auto-expire (read-time only, never modifies file)
3
3
  # Without --pair: prints exactly "enabled" or "disabled" to stdout
4
- # With --pair: prints the pair value: "claude+codex", "claude+claude", or "none"
4
+ # With --pair: prints the pair value: "claude+codex", "claude+claude", "codex+codex", or "none"
5
5
 
6
6
  set -euo pipefail
7
7
 
@@ -24,48 +24,48 @@ if [[ ! -f "$STATE_FILE" ]]; then
24
24
  exit 0
25
25
  fi
26
26
 
27
- python3 - "$STATE_FILE" "$PAIR_MODE" <<'PYEOF'
28
- import sys
29
- import json
30
- from datetime import datetime, timezone, timedelta
27
+ # Parse JSON using portable bash + sed/grep (no jq, no python required)
28
+ enabled=$(grep -o '"enabled"[[:space:]]*:[[:space:]]*\(true\|false\)' "$STATE_FILE" 2>/dev/null | grep -o 'true\|false' || echo "false")
29
+ since=$(grep -o '"sinceISO"[[:space:]]*:[[:space:]]*"[^"]*"' "$STATE_FILE" 2>/dev/null | sed 's/.*"\([^"]*\)"/\1/' || echo "")
30
+ pair=$(grep -o '"pair"[[:space:]]*:[[:space:]]*"[^"]*"' "$STATE_FILE" 2>/dev/null | sed 's/.*"\([^"]*\)"/\1/' || echo "")
31
31
 
32
- state_path = sys.argv[1]
33
- pair_mode = sys.argv[2] == "true"
32
+ # If not enabled, or sinceISO is missing/null, return default
33
+ [[ "$enabled" != "true" ]] && { if [[ "$PAIR_MODE" == "true" ]]; then echo "none"; else echo "disabled"; fi; exit 0; }
34
+ [[ -z "$since" || "$since" == "null" ]] && { if [[ "$PAIR_MODE" == "true" ]]; then echo "none"; else echo "disabled"; fi; exit 0; }
34
35
 
35
- try:
36
- with open(state_path, "r") as f:
37
- data = json.load(f)
38
- except (json.JSONDecodeError, OSError):
39
- print("none" if pair_mode else "disabled")
40
- sys.exit(0)
41
-
42
- enabled = data.get("enabled", False)
43
- since_iso = data.get("sinceISO")
44
- pair = data.get("pair")
36
+ # Check 24h expiry using portable date commands (macOS has date -j, Linux has date -d)
37
+ since_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$since" "+%s" 2>/dev/null || date -d "$since" "+%s" 2>/dev/null || echo "")
38
+ if [[ -z "$since_epoch" ]]; then
39
+ # Unparseable timestamp — treat as expired
40
+ if [[ "$PAIR_MODE" == "true" ]]; then
41
+ echo "none"
42
+ else
43
+ echo "disabled"
44
+ fi
45
+ exit 0
46
+ fi
45
47
 
46
- if not enabled or since_iso is None:
47
- print("none" if pair_mode else "disabled")
48
- sys.exit(0)
48
+ now_epoch=$(date +%s)
49
+ age=$((now_epoch - since_epoch))
49
50
 
50
- # Parse sinceISO and apply 24h auto-expire (read-time interpretation, no file write)
51
- try:
52
- since_str = since_iso.replace("Z", "+00:00")
53
- since_dt = datetime.fromisoformat(since_str)
54
- now_dt = datetime.now(timezone.utc)
55
- if (now_dt - since_dt) > timedelta(hours=24):
56
- print("none" if pair_mode else "disabled")
57
- sys.exit(0)
58
- except (ValueError, TypeError):
59
- # Unparseable timestamp — treat as expired
60
- print("none" if pair_mode else "disabled")
61
- sys.exit(0)
51
+ # 24 hours = 86400 seconds
52
+ if [[ $age -gt 86400 ]]; then
53
+ if [[ "$PAIR_MODE" == "true" ]]; then
54
+ echo "none"
55
+ else
56
+ echo "disabled"
57
+ fi
58
+ exit 0
59
+ fi
62
60
 
63
- if pair_mode:
61
+ # Still enabled and within 24h
62
+ if [[ "$PAIR_MODE" == "true" ]]; then
64
63
  # Legacy state (no pair field) when enabled defaults to claude+codex
65
- if pair is None:
66
- print("claude+codex")
67
- else:
68
- print(pair)
69
- else:
70
- print("enabled")
71
- PYEOF
64
+ if [[ -z "$pair" || "$pair" == "null" ]]; then
65
+ echo "claude+codex"
66
+ else
67
+ echo "$pair"
68
+ fi
69
+ else
70
+ echo "enabled"
71
+ fi
@@ -72,10 +72,43 @@ fi
72
72
  # Otherwise prepend to the full prompt string.
73
73
  if [ "${FIRST_ARG}" = "exec" ]; then
74
74
  shift
75
- # $@ is now the prompt(s) passed to codex exec
75
+ # Parse out flags from prompt args (preserve them for codex exec)
76
+ CODEX_FLAGS=()
77
+ while [[ $# -gt 0 ]]; do
78
+ case "$1" in
79
+ -s|--sandbox|--model|--cd|-c)
80
+ # Flag with value
81
+ CODEX_FLAGS+=("$1" "$2")
82
+ shift 2
83
+ ;;
84
+ --skip-git-repo-check|--full-auto|--dangerously-bypass-approvals-and-sandbox)
85
+ # Boolean flag
86
+ CODEX_FLAGS+=("$1")
87
+ shift
88
+ ;;
89
+ --*=*)
90
+ # Flag with =value
91
+ CODEX_FLAGS+=("$1")
92
+ shift
93
+ ;;
94
+ --*|-*)
95
+ # Unknown flag — pass through with value if next arg doesn't look like a flag
96
+ CODEX_FLAGS+=("$1")
97
+ shift
98
+ if [[ $# -gt 0 && "$1" != -* ]]; then
99
+ CODEX_FLAGS+=("$1")
100
+ shift
101
+ fi
102
+ ;;
103
+ *)
104
+ # First non-flag arg = prompt; break and treat rest as prompt
105
+ break
106
+ ;;
107
+ esac
108
+ done
76
109
  ORIGINAL_PROMPT="${*:-}"
77
110
  FULL_PROMPT="${MERLIN_PROMPT}"$'\n\n'"---USER---"$'\n\n'"${ORIGINAL_PROMPT}"
78
- exec codex exec "${FULL_PROMPT}"
111
+ exec codex exec "${CODEX_FLAGS[@]}" "${FULL_PROMPT}"
79
112
  else
80
113
  # Treat remaining args as a single prompt string
81
114
  ORIGINAL_PROMPT="${*:-}"
@@ -93,6 +93,14 @@ TEST_CASES = [
93
93
  ("React component refactor", "react", "merlin-frontend", 25),
94
94
  ("useState hook pattern", "react", "merlin-frontend", 25),
95
95
  ("Next.js server component", "react", "merlin-frontend", 25),
96
+ ("build Android app with Kotlin", "android", "android-expert", 25),
97
+ ("Jetpack Compose UI for Material You", "android", "android-expert", 25),
98
+ ("iOS SwiftUI development", "apple", "apple-swift-expert", 25),
99
+ ("macOS AppKit interface", "apple", "apple-swift-expert", 25),
100
+ ("Electron app development", "desktop", "desktop-app-expert", 25),
101
+ ("Tauri system tray application", "desktop", "desktop-app-expert", 25),
102
+ ("email marketing campaign setup", "marketing", "marketing-automation", 25),
103
+ ("drip sequence for onboarding", "marketing", "marketing-automation", 25),
96
104
  ("fix the database query", "none", "", 0),
97
105
  ("rename a variable", "none", "", 0),
98
106
  ("update README", "none", "", 0),
@@ -161,6 +169,42 @@ def main():
161
169
  print(f"FAIL: '{task[:50]}...' => {reason}")
162
170
  failed += 1
163
171
 
172
+ # Wiring integrity: every agent in registry must have a corresponding agent file
173
+ print("\n" + "-" * 60)
174
+ print("Wiring Integrity Check")
175
+ print("-" * 60)
176
+ try:
177
+ with open(registry_path, 'r') as f:
178
+ registry = json.load(f)
179
+
180
+ registry_dir = os.path.dirname(registry_path)
181
+ agent_dir = os.path.join(registry_dir, '../../agents') # files/merlin/skills/TASK-OPTIMIZER.json → files/agents/
182
+ if os.path.isabs(registry_dir):
183
+ agent_dir = os.path.abspath(agent_dir)
184
+
185
+ # Collect all agents referenced in registry
186
+ agents_in_registry = set()
187
+ for skill in registry.get('skills', []):
188
+ agent = skill.get('agent', '')
189
+ if agent:
190
+ agents_in_registry.add(agent)
191
+
192
+ integrity_fail = 0
193
+ for agent in sorted(agents_in_registry):
194
+ agent_file = os.path.join(agent_dir, f'{agent}.md')
195
+ if not os.path.isfile(agent_file):
196
+ print(f"✗ WIRING FAIL: agent '{agent}' referenced in registry but not in {agent_dir}/")
197
+ integrity_fail += 1
198
+
199
+ if integrity_fail > 0:
200
+ print(f"✗ WIRING INTEGRITY CHECK FAILED ({integrity_fail} agents missing)")
201
+ failed += 1
202
+ else:
203
+ total_agents = len(agents_in_registry)
204
+ print(f"✓ wiring integrity: all {total_agents} agents in registry exist on disk")
205
+ except Exception as e:
206
+ print(f"⚠ wiring integrity check skipped: {e}")
207
+
164
208
  print("\n" + "=" * 60)
165
209
  print(f"RESULTS: {passed} passed, {failed} failed")
166
210
  print("=" * 60)
@@ -19,9 +19,9 @@ if ! [[ "$SECS" =~ ^[0-9]+$ ]]; then
19
19
  fi
20
20
 
21
21
  if command -v gtimeout >/dev/null 2>&1; then
22
- exec gtimeout --preserve-status --signal=TERM "${SECS}s" "$@"
22
+ exec gtimeout --signal=TERM "${SECS}s" "$@"
23
23
  elif command -v timeout >/dev/null 2>&1; then
24
- exec timeout --preserve-status --signal=TERM "${SECS}s" "$@"
24
+ exec timeout --signal=TERM "${SECS}s" "$@"
25
25
  else
26
26
  # Perl fork+alarm fallback (always available on macOS+Linux).
27
27
  # Must fork — exec replaces the perl process and discards the SIGALRM handler,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-merlin-brain",
3
- "version": "5.3.3",
3
+ "version": "5.3.5",
4
4
  "description": "Merlin - The Ultimate AI Brain for Claude Code, Codex, and other AI CLIs. One install: workflows, agents, loop, and Sights MCP server.",
5
5
  "type": "module",
6
6
  "main": "./dist/server/index.js",
@@ -17,8 +17,7 @@
17
17
  "typecheck": "tsc --noEmit",
18
18
  "test": "vitest run",
19
19
  "test:watch": "vitest",
20
- "prepublishOnly": "npm run build",
21
- "postinstall": "node bin/install.cjs"
20
+ "prepublishOnly": "npm run build"
22
21
  },
23
22
  "keywords": [
24
23
  "claude",