RomanRG008 commited on
Commit
cbc7dfe
·
verified ·
1 Parent(s): 140e782

Gyra v0.2 (H17)

Browse files
Files changed (1) hide show
  1. harness/gyra_install.py +83 -0
harness/gyra_install.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Wire the Gyra hook into Claude Code or Codex for one repository (or for the user).
2
+
3
+ python3 gyra_install.py --harness claude [--dir REPO] [--scope project|user] [--env GYRA_MODE=headless ...]
4
+ python3 gyra_install.py --harness codex [--dir REPO] [--scope project|user]
5
+ python3 gyra_install.py --harness claude --remove
6
+
7
+ Claude Code: hooks in <repo>/.claude/settings.local.json (user scope: ~/.claude/settings.json).
8
+ Codex: <repo>/.codex/hooks.json (same hook contract as Claude Code) and `[features] hooks = true` in .codex/config.toml.
9
+ Existing hooks are kept; running it again replaces only Gyra's own entries.
10
+ """
11
+ import argparse
12
+ import json
13
+ import re
14
+ import shlex
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ HOOK = Path(__file__).resolve().parent / "gyra_hook.py"
19
+ MARK = "gyra_hook.py"
20
+
21
+
22
+ def command(env):
23
+ pre = " ".join("%s=%s" % (k, shlex.quote(v)) for k, v in env)
24
+ return ("%s %s %s" % (pre, shlex.quote(sys.executable), shlex.quote(str(HOOK)))).strip()
25
+
26
+
27
+ def spec(harness):
28
+ post = "Bash|Read|WebFetch|WebSearch|mcp__.*" if harness == "claude" else "Bash|mcp__.*" # Codex reads files through Bash
29
+ return [("UserPromptSubmit", None, 5), ("PreToolUse", "Bash", 5), ("PostToolUse", post, 5)]
30
+
31
+
32
+ def merge(hooks, harness, cmd, remove):
33
+ for event, matcher, timeout in spec(harness):
34
+ groups = [g for g in hooks.get(event, []) if not any(MARK in h.get("command", "") for h in g.get("hooks", []))]
35
+ if not remove:
36
+ groups.append(dict({"matcher": matcher} if matcher else {}, hooks=[{"type": "command", "command": cmd, "timeout": timeout}]))
37
+ if groups:
38
+ hooks[event] = groups
39
+ else:
40
+ hooks.pop(event, None)
41
+ return hooks
42
+
43
+
44
+ def load(p):
45
+ return json.loads(p.read_text()) if p.exists() and p.read_text().strip() else {}
46
+
47
+
48
+ def main():
49
+ ap = argparse.ArgumentParser()
50
+ ap.add_argument("--harness", choices=["claude", "codex"], required=True)
51
+ ap.add_argument("--dir", default=".")
52
+ ap.add_argument("--scope", choices=["project", "user"], default="project")
53
+ ap.add_argument("--env", action="append", default=[], help="KEY=VALUE passed to the hook, e.g. GYRA_MODE=headless")
54
+ ap.add_argument("--remove", action="store_true")
55
+ a = ap.parse_args()
56
+ env = [tuple(e.split("=", 1)) for e in a.env]
57
+ cmd = command(env)
58
+ root = Path.home() if a.scope == "user" else Path(a.dir).resolve()
59
+ if a.harness == "claude":
60
+ path = root / ".claude" / ("settings.json" if a.scope == "user" else "settings.local.json")
61
+ cfg = load(path)
62
+ cfg["hooks"] = merge(cfg.get("hooks", {}), "claude", cmd, a.remove)
63
+ else:
64
+ path = root / ".codex" / "hooks.json"
65
+ cfg = load(path)
66
+ cfg["hooks"] = merge(cfg.get("hooks", {}), "codex", cmd, a.remove)
67
+ toml = root / ".codex" / "config.toml"
68
+ cur = toml.read_text() if toml.exists() else ""
69
+ if not a.remove:
70
+ if re.search(r"^\[features\]", cur, re.M):
71
+ if not re.search(r"^hooks\s*=", cur, re.M):
72
+ cur = re.sub(r"^\[features\]\s*$", "[features]\nhooks = true", cur, count=1, flags=re.M)
73
+ else:
74
+ cur = cur.rstrip() + ("\n\n" if cur.strip() else "") + "[features]\nhooks = true\n"
75
+ toml.parent.mkdir(parents=True, exist_ok=True)
76
+ toml.write_text(cur)
77
+ path.parent.mkdir(parents=True, exist_ok=True)
78
+ path.write_text(json.dumps(cfg, indent=2) + "\n")
79
+ print("%s Gyra hooks %s %s" % ("removed from" if a.remove else "written to", a.harness, path))
80
+
81
+
82
+ if __name__ == "__main__":
83
+ main()