jev-tools-setup 0.3.0__py3-none-any.whl

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.
jev_report.py ADDED
@@ -0,0 +1,240 @@
1
+ """Write LOG.md: a plain-English report of what jev-tools did and what is wrong. Stdlib only, read-only except for the output file.
2
+
3
+ python jev_report.py [--out PATH] [--last N] (also: `jev-tools-setup report`, or the `report` skill inside Claude Code)
4
+
5
+ Sources: the decision log(s) the hooks and skills append to (JSONL), ~/.jev-tools/config.json, and environment variable NAMES.
6
+ Never included: the API key, file contents, prompts. Home directory paths are shortened to ~ and key-shaped strings are redacted,
7
+ so the file is safe to attach to an issue.
8
+ """
9
+ import argparse
10
+ import json
11
+ import os
12
+ import platform
13
+ import re
14
+ import statistics
15
+ import sys
16
+ import time
17
+ from collections import Counter
18
+ from datetime import datetime
19
+ from pathlib import Path
20
+
21
+ HOME = Path.home()
22
+ JEV = HOME / ".jev-tools"
23
+ KEYISH = re.compile(r"\b(?:sk|pk|rk)[-_][A-Za-z0-9_-]{16,}|(?i:bearer)\s+[A-Za-z0-9._~+/=-]{16,}")
24
+ INSTALLER = {"setup_start", "setup_end", "plugin_step", "key_saved", "probe_failed", "config_saved", "serve_start", "local_install"}
25
+
26
+
27
+ def clean(text):
28
+ """Shorten the home path and redact anything key-shaped."""
29
+ return KEYISH.sub("[REDACTED]", str(text).replace(str(HOME), "~"))
30
+
31
+
32
+ def log_files():
33
+ cands = [os.environ.get("JEV_LOG"), (Path(os.environ["CLAUDE_PLUGIN_DATA"]) / "log.jsonl") if os.environ.get("CLAUDE_PLUGIN_DATA") else None,
34
+ JEV / "log.jsonl", *sorted((HOME / ".claude" / "plugins" / "data").glob("jev-tools*/log.jsonl"))]
35
+ seen, out = set(), []
36
+ for c in cands:
37
+ if c and Path(c).is_file() and str(Path(c).resolve()) not in seen:
38
+ seen.add(str(Path(c).resolve()))
39
+ out.append(Path(c))
40
+ return out
41
+
42
+
43
+ def load(files):
44
+ """All events, oldest first. The same line in two files counts once (the plugin dir and ~/.jev-tools can overlap),
45
+ but repeated identical lines inside one file are real repeats and all count."""
46
+ rows, have = [], Counter()
47
+ for f in files:
48
+ local = Counter()
49
+ for line in f.read_text(encoding="utf-8", errors="replace").splitlines():
50
+ local[line] += 1
51
+ if local[line] <= have[line]:
52
+ continue
53
+ have[line] += 1
54
+ try:
55
+ e = json.loads(line)
56
+ if isinstance(e, dict) and "event" in e:
57
+ rows.append(e)
58
+ except ValueError:
59
+ pass
60
+ return sorted(rows, key=lambda e: e.get("t", 0))
61
+
62
+
63
+ def when(t):
64
+ return datetime.fromtimestamp(t).astimezone().strftime("%Y-%m-%d %H:%M:%S") if t else "?"
65
+
66
+
67
+ def say(e):
68
+ """One human sentence for one log event."""
69
+ k = e["event"]
70
+ if k == "ask":
71
+ return f"model call ok: {e.get('n')} question(s), {e.get('ms')} ms, {e.get('tok')} input tokens"
72
+ if k == "ask_error":
73
+ return f"model call FAILED: {e.get('err')} {e.get('msg', '')}".strip()
74
+ if k == "no_key":
75
+ return "no API key found, so the call was skipped (the hook did nothing)"
76
+ if k == "offline":
77
+ return "offline backend: model call skipped on purpose"
78
+ if k == "hook_error":
79
+ return f"{e.get('hook')} hook crashed with {e.get('err')} and failed open (your edit/prompt was not blocked)"
80
+ if k == "rule_check":
81
+ s = f"rule check on {e.get('file')}: {e.get('rules')} rule(s)"
82
+ if e.get("offline"):
83
+ s += f", {e.get('hits')} literal match(es)"
84
+ else:
85
+ s += f", top score {e.get('top')}" + (f", second look {e.get('confirm')}" if e.get("confirm") is not None else "")
86
+ if e.get("rule"):
87
+ s += f" [{e['rule'][:60]}]"
88
+ return s + (" (enforcing)" if e.get("enforce") else " (shadow: logged only)")
89
+ if k == "skill_pick":
90
+ s = f"skill pick: {e.get('pick')}" + (f" (p={e['p']})" if "p" in e else "")
91
+ return s + (" vetoed by second check" if e.get("vetoed") else " (shadow: not injected)" if e.get("shadow") else " (injected)" if e.get("pick") != "none" else "")
92
+ if k == "precheck":
93
+ return f"review pre-check: {e.get('route')} review, {e.get('flags')} flag(s)" + (" (offline patterns)" if e.get("offline") else "")
94
+ if k == "plugin_step":
95
+ return f"plugin {e.get('step')} " + ("ok" if e.get("ok") else f"FAILED: {e.get('msg', '')}")
96
+ if k == "config_saved":
97
+ return f"config saved: backend={e.get('backend')}, model={e.get('model')}, mode={e.get('mode')}"
98
+ if k == "key_saved":
99
+ return "API key validated and saved to the credentials file (the value is never logged)"
100
+ if k == "probe_failed":
101
+ return f"key/server check failed: {e.get('err')}"
102
+ if k == "local_install":
103
+ return f"local model {e.get('model')} install " + ("ok" if e.get("ok") else "FAILED or declined")
104
+ if k == "serve_start":
105
+ return f"local server started: {e.get('model')} at {e.get('url')}"
106
+ if k == "setup_start":
107
+ return f"setup started (backend flag: {e.get('backend')})"
108
+ if k == "setup_end":
109
+ return f"setup finished with exit code {e.get('rc')}"
110
+ if k == "state_cut":
111
+ return f"input trimmed to fit {e.get('model')}'s context window (was {e.get('chars')} characters)"
112
+ if k == "too_many_options":
113
+ return f"question declined: more options than {e.get('model')} can take"
114
+ return k + ": " + ", ".join(f"{a}={b}" for a, b in e.items() if a not in ("t", "event"))
115
+
116
+
117
+ def classify(msg):
118
+ m = str(msg).lower()
119
+ for pat, label in ((r"401|403|unauthor|forbidden|rejected", "key rejected"), (r"429|quota|rate", "quota or rate limit"),
120
+ (r"timed out|timeout", "timeout"), (r"refus|10061|connection|getaddrinfo|urlerror|name or service|unreachable", "cannot reach the server"),
121
+ (r"500|502|503|504|529|overload", "server error")):
122
+ if re.search(pat, m):
123
+ return label
124
+ return "other"
125
+
126
+
127
+ FIX = {
128
+ "key rejected": "The key was refused. Get a fresh one at https://codiv.ai/dashboard and run `jev-tools-setup` (api).",
129
+ "quota or rate limit": "Quota or rate limit hit. Wait, or check usage on the Codiv dashboard. Quota errors are not retried.",
130
+ "timeout": "The server answered too slowly. Try again; for a local server check that it is running and not still loading the model.",
131
+ "cannot reach the server": "Nothing is answering. API: check your network. Local: start the server with `jev-tools-setup serve` (small models) or docker compose (full model).",
132
+ "server error": "The server had an error or is overloaded. It usually clears by itself; hooks fail open meanwhile.",
133
+ "other": "See the message in the recent events table.",
134
+ }
135
+
136
+
137
+ def problems(rows, cfg, day, backend, mode="shadow"):
138
+ out, d = [], [e for e in rows if e.get("t", 0) >= day]
139
+ c = Counter(e["event"] for e in d)
140
+ if mode == "off":
141
+ src = "the JEV_MODE environment variable" if os.environ.get("JEV_MODE") == "off" else "the plugin option or saved config"
142
+ out.append(("warn", "Mode is OFF: every hook does nothing and nothing is sent", f"Set by {src}. Hooks return early, so no events are logged either.",
143
+ "Unset JEV_MODE (or set it to shadow/active) and restart Claude Code."))
144
+ elif rows and not d:
145
+ out.append(("info", "No activity in the last 24 hours", "Either you have not edited files or sent prompts in a session with the plugin, or the hooks are not loading.",
146
+ "Restart Claude Code and check `/plugin` shows jev-tools enabled."))
147
+ if not rows:
148
+ out.append(("warn", "No events recorded yet", "No hook or skill has run since install, or none could write a log.",
149
+ "Restart Claude Code (hooks load at session start), confirm the plugin is enabled with `/plugin`, then do an edit and re-run this report."))
150
+ if c["no_key"] and backend == "api":
151
+ out.append(("error", f"API key missing ({c['no_key']} skipped calls in 24h)", "Every hook silently does nothing without a key.",
152
+ "Run `jev-tools-setup` and choose api, or set OPENJEV_API_KEY."))
153
+ errs = Counter(classify(e.get("msg", "") or e.get("err", "")) for e in d if e["event"] == "ask_error")
154
+ for label, n in errs.most_common():
155
+ sample = next(clean(f"{e.get('err')} {e.get('msg', '')}".strip()) for e in reversed(d) if e["event"] == "ask_error" and classify(e.get("msg", "") or e.get("err", "")) == label)
156
+ out.append(("error", f"Model calls failing: {label} ({n} in 24h)", f"Latest: {sample}", FIX[label]))
157
+ hooks = Counter(e.get("hook") for e in d if e["event"] == "hook_error")
158
+ for h, n in hooks.most_common():
159
+ out.append(("error", f"The {h} hook crashed {n} time(s) in 24h", "It failed open, so nothing was blocked, but it is not doing its job.",
160
+ "Send this report with the `err` text from the recent events table when you file an issue."))
161
+ if c["state_cut"] or c["too_many_options"]:
162
+ out.append(("warn", "A small local model is trimming or declining inputs", f"{c['state_cut']} trimmed, {c['too_many_options']} declined in 24h.",
163
+ "Expected with Verdict/Laya (512/1024-token windows). Verdicts on long diffs are less reliable; stay in shadow mode."))
164
+ env_mode = os.environ.get("JEV_MODE")
165
+ if env_mode and cfg.get("mode") and env_mode != cfg["mode"]:
166
+ out.append(("warn", f"JEV_MODE={env_mode} overrides the saved mode ({cfg['mode']})", "The environment variable always wins.", "Unset JEV_MODE to use the saved mode."))
167
+ if backend == "offline" and c["offline"]:
168
+ out.append(("info", f"Offline backend: {c['offline']} model call(s) skipped in 24h", "Expected: no model is configured.", "Run `jev-tools-setup` to pick api or local."))
169
+ return out
170
+
171
+
172
+ def build(last=40):
173
+ files, now = log_files(), time.time()
174
+ rows = load(files)
175
+ cfg = {}
176
+ try:
177
+ cfg = json.loads((JEV / "config.json").read_text(encoding="utf-8"))
178
+ except (OSError, ValueError):
179
+ pass
180
+ backend = cfg.get("backend") if cfg.get("backend") in ("api", "local", "offline") else "api"
181
+ day = now - 86400
182
+ mode = os.environ.get("JEV_MODE") or os.environ.get("CLAUDE_PLUGIN_OPTION_MODE") or cfg.get("mode") or "shadow"
183
+ probs = problems(rows, cfg, day, backend, mode)
184
+ level = "error" if any(p[0] == "error" for p in probs) else "warn" if any(p[0] == "warn" for p in probs) else "ok"
185
+ head = {"ok": "✅ Healthy: nothing needs attention.", "warn": "⚠️ Works, but check the notes below.", "error": "❌ Something is wrong: see Problems."}[level]
186
+ ver = "unknown"
187
+ for pj in (Path(__file__).resolve().parent.parent / ".claude-plugin" / "plugin.json",):
188
+ try:
189
+ ver = json.loads(pj.read_text(encoding="utf-8")).get("version", ver)
190
+ except (OSError, ValueError):
191
+ pass
192
+ keysrc = [n for n in ("OPENJEV_API_KEY", "TYPESAFE_API_KEY", "CLAUDE_PLUGIN_OPTION_API_KEY") if os.environ.get(n)]
193
+ if (JEV / "credentials").exists():
194
+ keysrc.append("credentials file")
195
+ L = [f"# jev-tools log", "", f"_Generated {when(now)} on {platform.system()} {platform.release()}, Python {sys.version.split()[0]}, plugin {ver}._",
196
+ "_Contains no API key, no file contents and no prompts. Safe to attach to an issue._", "", f"## {head}", ""]
197
+ L += ["## Setup", "", "| | |", "|---|---|", f"| Backend | {backend}" + (f" ({cfg.get('model')})" if cfg.get("model") else "") + " |",
198
+ f"| Mode | {mode} (shadow = log only, active = block/inject, off = nothing sent) |",
199
+ f"| API key found in | {', '.join(keysrc) or 'nowhere'} |", f"| Config file | {'present' if cfg else 'absent'} |",
200
+ f"| Log files read | {', '.join(clean(f) for f in files) or 'none'} |", ""]
201
+ L += ["## Problems", ""]
202
+ L += [f"- {'❌' if s == 'error' else '⚠️' if s == 'warn' else 'ℹ️'} **{clean(t)}**: {clean(d)}\n - Fix: {f}" for s, t, d, f in probs] or ["None found."]
203
+ d24 = [e for e in rows if e.get("t", 0) >= day]
204
+ asks = [e["ms"] for e in d24 if e["event"] == "ask" and isinstance(e.get("ms"), (int, float))]
205
+ L += ["", "## What happened", "", f"{len(rows)} event(s) in total, {len(d24)} in the last 24 hours.", "", "| Event | Last 24h | All time |", "|---|---:|---:|"]
206
+ allc, c24 = Counter(e["event"] for e in rows), Counter(e["event"] for e in d24)
207
+ L += [f"| {k} | {c24[k]} | {allc[k]} |" for k in sorted(allc, key=lambda k: -allc[k])] or ["| (none) | 0 | 0 |"]
208
+ if asks:
209
+ L += ["", f"Model calls last 24h: {len(asks)} ok, median {int(statistics.median(asks))} ms, slowest {int(max(asks))} ms."]
210
+ rc = [e for e in d24 if e["event"] == "rule_check"]
211
+ if rc:
212
+ flagged = [e for e in rc if (e.get("top") or 0) >= 0.8 or e.get("hits")]
213
+ L += [f"Rule checks last 24h: {len(rc)}, {len(flagged)} flagged a rule, enforcement {'on' if any(e.get('enforce') for e in rc) else 'off (shadow)'}."]
214
+ inst = [e for e in rows if e["event"] in INSTALLER]
215
+ if inst:
216
+ L += ["", "## Setup history", "", "| When | What |", "|---|---|"] + [f"| {when(e.get('t'))} | {clean(say(e))} |" for e in inst[-10:]]
217
+ L += ["", f"## Recent events (newest first, last {last})", "", "| When | What |", "|---|---|"]
218
+ L += [f"| {when(e.get('t'))} | {clean(say(e)).replace('|', '/')} |" for e in reversed(rows[-last:])] or ["| - | nothing logged yet |"]
219
+ L += ["", "## How to use this", "",
220
+ "- Something does nothing? Read **Problems** first: the usual causes are a missing key, an unreachable server, or a hook that was not reloaded (restart Claude Code).",
221
+ "- Hooks fail open: an outage never blocks your work, it just means the hook did nothing.",
222
+ "- Run `jev-tools-setup check` to test the machine, or the `status` skill for a one-screen summary. Re-run this report any time.", ""]
223
+ return "\n".join(L), level
224
+
225
+
226
+ def main(argv=None):
227
+ ap = argparse.ArgumentParser()
228
+ ap.add_argument("--out", help=f"default {JEV / 'LOG.md'}")
229
+ ap.add_argument("--last", type=int, default=40)
230
+ a = ap.parse_args(argv)
231
+ text, level = build(a.last)
232
+ out = Path(a.out) if a.out else JEV / "LOG.md"
233
+ out.parent.mkdir(parents=True, exist_ok=True)
234
+ out.write_text(text, encoding="utf-8")
235
+ print(f"wrote {out} [{ {'ok': 'healthy', 'warn': 'notes', 'error': 'PROBLEMS'}[level] }]")
236
+ return 0
237
+
238
+
239
+ if __name__ == "__main__":
240
+ sys.exit(main())
jev_tools_cli.py ADDED
@@ -0,0 +1,427 @@
1
+ """jev-tools installer: one command to install the Claude Code plugin and choose how Jev runs. Stdlib only.
2
+
3
+ uv tool install jev-tools-setup once (or: pipx install jev-tools-setup; no install: uvx jev-tools-setup)
4
+
5
+ jev-tools-setup check read-only: is this machine ready? (python, claude CLI, git, network, GPU, docker)
6
+ jev-tools-setup install the plugin and pick api / local / offline
7
+ jev-tools-setup serve run the small local model you chose (verdict-1.4 / laya-1.0) in the foreground
8
+ jev-tools-setup report write ~/.jev-tools/LOG.md: what happened, what is wrong, how to fix it
9
+ jev-tools-setup uninstall remove ~/.jev-tools (key, config, log); the plugin itself: /plugin uninstall
10
+
11
+ The plugin files (skills, scripts, hooks) are downloaded by the `claude` CLI from the GitHub marketplace, so terminal
12
+ and desktop share one install. This tool adds what the plugin cannot do for itself: the backend choice and the key.
13
+ Non-interactive flags exist for scripts and tests; the API key is only ever read from a hidden prompt or stdin, never argv.
14
+ """
15
+ import argparse
16
+ import getpass
17
+ import json
18
+ import os
19
+ import shutil
20
+ import socket
21
+ import subprocess
22
+ import sys
23
+ import time
24
+ import urllib.error
25
+ import urllib.parse
26
+ import urllib.request
27
+ from pathlib import Path
28
+
29
+ MARKET = "Rcidshacker/jev-tools"
30
+ PLUGIN = "jev-tools@jev-tools"
31
+ HOST = "api.codiv.ai"
32
+ LOCAL_URL = "http://127.0.0.1:8080" # what OpenJev's docker compose listens on
33
+ LOOPBACK = ("localhost", "127.0.0.1", "::1")
34
+ SELF_HOST_DOCS = "https://codiv.ai/docs/guides/self-hosting"
35
+ OPENJEV_REPO = "https://github.com/razorback16/openjev"
36
+ # small encoder models: id -> (OpenJev extra / OPENJEV_BACKEND value, label, fp32 weight size estimate = params x 4 bytes)
37
+ SMALL = {"verdict-1.4": ("verdict", "Verdict 151M", "about 0.6 GB"), "laya-1.0": ("laya", "Laya 421M", "about 1.7 GB")}
38
+ SELF_HOST_CMD = "git clone https://github.com/razorback16/openjev && cd openjev && docker compose up -d"
39
+ OFFLINE_NOTE = ("offline means no model (not no internet). Pattern-based fallbacks still run: find-files ranks by keywords, "
40
+ "review-precheck flags secrets/dependencies/auth/schema/test-weakening/swallowed errors (fast only for a small clean diff), "
41
+ "the skill hook picks by keyword, the rule hook blocks only a literal token a 'never/avoid X' rule forbids, "
42
+ "browser-nav gives an unsure keyword hint. rule-calibrate and any judgment that needs understanding are unavailable.")
43
+
44
+
45
+ def run_out(cmd, timeout=20):
46
+ """(returncode, combined output) without ever raising. UTF-8 with replacement: Windows defaults to cp1252 and crashes on Claude's output."""
47
+ try:
48
+ r = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout)
49
+ return r.returncode, ((r.stdout or "") + (r.stderr or "")).strip()
50
+ except (OSError, subprocess.TimeoutExpired) as e:
51
+ return 1, str(e)
52
+
53
+
54
+ def home():
55
+ return Path.home() / ".jev-tools"
56
+
57
+
58
+ def note(event, **kw):
59
+ """Append one line to the decision log that LOG.md is built from. Never raises, never takes secrets."""
60
+ try:
61
+ home().mkdir(exist_ok=True)
62
+ with (home() / "log.jsonl").open("a", encoding="utf-8") as f:
63
+ f.write(json.dumps({"t": int(time.time()), "event": event, **kw}) + "\n")
64
+ except OSError:
65
+ pass
66
+
67
+
68
+ def lock_down(path):
69
+ """Owner-only access. icacls on Windows, mode bits elsewhere."""
70
+ if os.name == "nt":
71
+ grant = f"{os.environ.get('USERNAME', '')}:" + ("F" if path.is_file() else "(OI)(CI)F") # dirs hand it down to new files
72
+ subprocess.run(["icacls", str(path), "/inheritance:r", "/grant:r", grant], capture_output=True, check=False)
73
+ else:
74
+ os.chmod(path, 0o600 if path.is_file() else 0o700)
75
+
76
+
77
+ def save_key(key):
78
+ d = home()
79
+ d.mkdir(exist_ok=True)
80
+ lock_down(d)
81
+ p = d / "credentials"
82
+ fd = os.open(p, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) # created owner-only, no window where it is readable
83
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
84
+ f.write(key.strip() + "\n")
85
+ lock_down(p)
86
+ return p
87
+
88
+
89
+ def read_config():
90
+ try:
91
+ c = json.loads((home() / "config.json").read_text(encoding="utf-8"))
92
+ return c if isinstance(c, dict) else {}
93
+ except (OSError, ValueError):
94
+ return {}
95
+
96
+
97
+ def venv_python():
98
+ return home() / "venv" / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
99
+
100
+
101
+ def save_config(**cfg):
102
+ home().mkdir(exist_ok=True)
103
+ p = home() / "config.json"
104
+ p.write_text(json.dumps(cfg, indent=2) + "\n", encoding="utf-8")
105
+ return p
106
+
107
+
108
+ def probe(base, key):
109
+ """One tiny System One call. Returns None on success or a short reason. Never follows redirects, never prints the key."""
110
+ u = urllib.parse.urlsplit(base)
111
+ if not (u.hostname in LOOPBACK or (u.hostname == HOST and u.scheme == "https")):
112
+ return f"refusing to contact {u.hostname!r}: only {HOST} or loopback"
113
+
114
+ class NoRedirect(urllib.request.HTTPRedirectHandler):
115
+ def redirect_request(self, *a, **k):
116
+ return None
117
+
118
+ body = json.dumps({"model": "openjev-latest", "state": "The build finished and all tests passed.",
119
+ "questions": {"ok": {"type": "noul", "instructions": "The text reports a successful outcome"}}}).encode()
120
+ headers = {"Content-Type": "application/json", "User-Agent": "jev-tools/0.1"}
121
+ if key:
122
+ headers["Authorization"] = f"Bearer {key}"
123
+ try:
124
+ with urllib.request.build_opener(NoRedirect).open(
125
+ urllib.request.Request(base.rstrip("/") + "/v1/systemone", body, headers, method="POST"), timeout=20) as r:
126
+ return None if "answers" in json.load(r) else "unexpected response"
127
+ except urllib.error.HTTPError as e:
128
+ return {401: "key rejected (401)", 403: "key rejected (403)", 429: "quota or rate limit (429)"}.get(e.code, f"HTTP {e.code}")
129
+ except Exception as e:
130
+ return f"{type(e).__name__}: {str(e)[:80]}"
131
+
132
+
133
+ def gpu():
134
+ """(name, is_blackwell) from nvidia-smi, or (None, False). Blackwell = compute capability 10.x/12.x. A heuristic, not a promise."""
135
+ if not shutil.which("nvidia-smi"):
136
+ return None, False
137
+ rc, text = run_out(["nvidia-smi", "--query-gpu=name,compute_cap", "--format=csv,noheader"])
138
+ if rc or not text:
139
+ return None, False
140
+ name, _, cap = text.splitlines()[0].partition(",")
141
+ try:
142
+ return name.strip(), int(cap.strip().split(".")[0]) >= 10
143
+ except ValueError:
144
+ return name.strip(), False
145
+
146
+
147
+ def ask(prompt, options, default=None):
148
+ while True:
149
+ a = input(f"{prompt} [{'/'.join(options)}]{f' ({default})' if default else ''}: ").strip().lower() or default
150
+ if a in options:
151
+ return a
152
+ print(f" choose one of: {', '.join(options)}")
153
+
154
+
155
+ def install_plugin():
156
+ claude = shutil.which("claude")
157
+ if not claude:
158
+ note("plugin_step", step="install", ok=False, msg="claude CLI not found")
159
+ print("! `claude` CLI not found. Install Claude Code, then run inside it:\n"
160
+ f" /plugin marketplace add {MARKET}\n /plugin install {PLUGIN}")
161
+ return False
162
+ for args in (["marketplace", "add", MARKET], ["install", PLUGIN]):
163
+ rc, text = run_out([claude, "plugin", *args], timeout=180)
164
+ # re-running setup must not fail just because the marketplace or plugin is already there
165
+ if rc and "already" not in text.lower():
166
+ print(f"! `claude plugin {' '.join(args)}` failed:\n{text[-400:]}")
167
+ note("plugin_step", step=args[0], ok=False, msg=text[-200:])
168
+ return False
169
+ note("plugin_step", step=args[0], ok=True)
170
+ print(f" ok claude plugin {args[0]}")
171
+ return True
172
+
173
+
174
+ def choose_backend(a):
175
+ if a.backend:
176
+ return a.backend
177
+ name, blackwell = gpu()
178
+ print("\nHow should Jev run?\n"
179
+ " api hosted OpenJev at api.codiv.ai (free tier, needs your key; sends text off-machine)\n"
180
+ " local self-hosted OpenJev on this machine (needs an NVIDIA Blackwell-class GPU and Docker)\n"
181
+ " offline no model: keyword and pattern fallbacks only, nothing is sent anywhere")
182
+ if name:
183
+ print(f" detected GPU: {name} -> {'looks Blackwell-class' if blackwell else 'NOT Blackwell-class, so local will not run the model'}")
184
+ else:
185
+ print(" no NVIDIA GPU detected, so local will not work here")
186
+ return ask("Backend", ("api", "local", "offline"), "api")
187
+
188
+
189
+ def choose_model(a):
190
+ if a.model:
191
+ return a.model
192
+ print("\nWhich local model?\n"
193
+ " verdict-1.4 Verdict 151M: smallest, 512-token context, up to 24 options, ignores yes/no criteria. CPU or any GPU.\n"
194
+ " laya-1.0 Laya 421M: 1,024-token context, about 20 options, heavier. CPU or any GPU.\n"
195
+ " openjev-latest the full OpenJev model: needs an NVIDIA Blackwell-class GPU and Docker.\n"
196
+ " jev-tools trims long diffs to fit the small models and starts them in shadow mode. They are unmeasured with its thresholds.")
197
+ return ask("Model", ("verdict-1.4", "laya-1.0", "openjev-latest"))
198
+
199
+
200
+ def install_small(model, yes):
201
+ """Clone OpenJev, make a venv, install the model's extra. Streams pip output. True when everything succeeded."""
202
+ extra, label, size = SMALL[model]
203
+ repo, venv = home() / "openjev", home() / "venv"
204
+ print(f"\n{label} runs on the CPU or any GPU. Setup will:\n 1. git clone {OPENJEV_REPO} -> {repo}\n"
205
+ f" 2. create a Python venv -> {venv}\n 3. pip install openjev[{extra}] (brings PyTorch: can be several GB)\n"
206
+ f"The model weights ({size} as fp32) download from Hugging Face the first time the server starts.")
207
+ if not yes and input("Proceed? [y/N]: ").strip().lower() != "y":
208
+ print(" skipped: nothing was changed")
209
+ return False
210
+ if not shutil.which("git"):
211
+ print("! git not found: install git, then re-run setup")
212
+ return False
213
+ steps = []
214
+ if not repo.exists():
215
+ steps.append(["git", "clone", "--depth", "1", OPENJEV_REPO, str(repo)])
216
+ if not venv_python().exists():
217
+ steps.append([sys.executable, "-m", "venv", str(venv)])
218
+ steps.append([str(venv_python()), "-m", "pip", "install", "-e", f"{repo}[{extra}]"])
219
+ for cmd in steps:
220
+ print(f" $ {' '.join(cmd)}")
221
+ if subprocess.call(cmd):
222
+ print("! that step failed; fix the error above and re-run setup (finished steps are skipped)")
223
+ return False
224
+ return True
225
+
226
+
227
+ def serve(a):
228
+ """Run the small local model in the foreground. Leave it open while you use Claude Code."""
229
+ cfg = read_config()
230
+ model = cfg.get("model")
231
+ if cfg.get("backend") != "local" or model not in SMALL or not venv_python().exists():
232
+ print("! nothing to serve: run `jev-tools-setup`, choose local, and pick verdict-1.4 or laya-1.0")
233
+ return 1
234
+ url = urllib.parse.urlsplit(cfg.get("base_url") or LOCAL_URL)
235
+ env = {**os.environ, "OPENJEV_BACKEND": SMALL[model][0], "OPENJEV_PORT": str(url.port or 8080), "OPENJEV_HOST": url.hostname or "127.0.0.1"}
236
+ if a.device:
237
+ env["OPENJEV_DEVICE"] = a.device
238
+ note("serve_start", model=model, url=url.geturl())
239
+ print(f"serving {model} on {url.geturl()} (Ctrl-C stops it; first start downloads the weights)")
240
+ try:
241
+ return subprocess.call([str(venv_python()), "-m", "openjev"], env=env)
242
+ except KeyboardInterrupt:
243
+ return 0
244
+
245
+
246
+ def setup(a):
247
+ """Run setup, then always write LOG.md so a failed attempt leaves something to read and attach."""
248
+ note("setup_start", backend=a.backend)
249
+ rc = 1
250
+ try:
251
+ rc = _setup(a)
252
+ return rc
253
+ finally:
254
+ note("setup_end", rc=rc)
255
+ report(None, quiet=True)
256
+ print(f"\nWhat happened is written to {home() / 'LOG.md'} (attach it if you report a problem; `jev-tools-setup report` refreshes it)")
257
+
258
+
259
+ def _setup(a):
260
+ print("jev-tools setup")
261
+ if not a.skip_plugin:
262
+ print("\n1. Installing the plugin (skills, scripts, hooks) with the claude CLI")
263
+ install_plugin()
264
+ kind = choose_backend(a)
265
+ cfg = {"backend": kind}
266
+ if kind == "api":
267
+ print(f"\nGet a key at https://codiv.ai/dashboard (starts with sk-codiv-). It is stored only in {home() / 'credentials'}, owner-only.")
268
+ key = (sys.stdin.readline() if a.key_stdin else getpass.getpass("Paste your key (hidden): ")).strip()
269
+ if not key:
270
+ print("! no key given: choose offline, or re-run setup")
271
+ return 1
272
+ err = probe(os.environ.get("OPENJEV_BASE_URL") or f"https://{HOST}", key)
273
+ if err:
274
+ note("probe_failed", err=err)
275
+ print(f"! the key did not work: {err}\n nothing was saved.")
276
+ return 1
277
+ print(f" ok key works, saved to {save_key(key)}")
278
+ note("key_saved")
279
+ elif kind == "local":
280
+ model = choose_model(a)
281
+ cfg["model"], cfg["base_url"] = model, a.base_url or LOCAL_URL
282
+ if model in SMALL:
283
+ ok = install_small(model, a.yes)
284
+ note("local_install", model=model, ok=ok)
285
+ if not ok:
286
+ return 1
287
+ print(f"\n ok installed. Start the server in its own terminal and keep it open:\n jev-tools-setup serve")
288
+ else:
289
+ name, blackwell = gpu()
290
+ if not blackwell:
291
+ print(f"! The full OpenJev model needs an NVIDIA Blackwell-class GPU (NVFP4); {'found ' + name if name else 'none found'}.\n"
292
+ f" Pick verdict-1.4 or laya-1.0 instead (they run on CPU), or api / offline. Details: {SELF_HOST_DOCS}")
293
+ return 1
294
+ if not shutil.which("docker"):
295
+ print(f"! Docker not found. Install it, then: {SELF_HOST_CMD}\n Guide: {SELF_HOST_DOCS}")
296
+ return 1
297
+ print(f"\nStart the server if it is not running:\n {SELF_HOST_CMD}\nChecking {cfg['base_url']} ...")
298
+ err = probe(cfg["base_url"], None)
299
+ print(" ok server answers" if not err else f"! not answering yet ({err}). Config is saved; start the server, then run the `status` skill.")
300
+ else:
301
+ print(f"\n{OFFLINE_NOTE}")
302
+ if cfg.get("model") in SMALL and a.mode == "active":
303
+ print("! small models are unmeasured with jev-tools' thresholds: run the rule-calibrate skill in shadow mode before trusting active.")
304
+ cfg["mode"] = a.mode or ("shadow" if a.backend else ask("\nMode (shadow = log only, active = block/inject, off = nothing)", ("shadow", "active", "off"), "shadow"))
305
+ print(f"\nconfig saved to {save_config(**cfg)} (mode: {cfg['mode']}; shadow only logs and never blocks)")
306
+ note("config_saved", backend=cfg["backend"], model=cfg.get("model"), mode=cfg["mode"])
307
+ print("The skill-pick hook stays OFF unless you enable it: it sends every prompt you type to the API.")
308
+ print("\nDone. Restart Claude Code (terminal or desktop), then run the `status` skill to confirm.")
309
+ return 0
310
+
311
+
312
+ def report(a, quiet=False):
313
+ """Write ~/.jev-tools/LOG.md from the decision logs."""
314
+ import jev_report
315
+ out = home() / "LOG.md"
316
+ text, level = jev_report.build()
317
+ home().mkdir(exist_ok=True)
318
+ out.write_text(text, encoding="utf-8")
319
+ if not quiet:
320
+ print(f"wrote {out} [{ {'ok': 'healthy', 'warn': 'notes to read', 'error': 'PROBLEMS found'}[level] }]")
321
+ return 0
322
+
323
+
324
+ def uninstall(_):
325
+ shutil.rmtree(home(), ignore_errors=True)
326
+ print(f"removed {home()}. To remove the plugin: /plugin uninstall {PLUGIN}")
327
+ return 0
328
+
329
+
330
+ def system_python():
331
+ """The `python` the hooks will get. Under uvx/pipx the tool's own venv is first on PATH, so skip it."""
332
+ dirs = os.environ.get("PATH", "").split(os.pathsep)
333
+ if sys.prefix != sys.base_prefix:
334
+ own = str(Path(sys.prefix).resolve())
335
+ dirs = [d for d in dirs if d and not str(Path(d).resolve()).startswith(own)]
336
+ return shutil.which("python", path=os.pathsep.join(dirs))
337
+
338
+
339
+ def checks():
340
+ """Read-only prerequisite report: list of (level, name, detail, hint), level in ok|warn|fail|info. Sends and changes nothing."""
341
+ out = []
342
+ add = lambda level, name, detail, hint="": out.append((level, name, detail, hint))
343
+ # the hooks run the literal command `python`, so it must resolve to 3.10+ (on Windows the Store stub resolves but does not run)
344
+ py = system_python()
345
+ rc, ver = run_out([py, "-c", "import sys;print('%d.%d' % sys.version_info[:2])"]) if py else (1, "")
346
+ try:
347
+ ok = rc == 0 and tuple(int(x) for x in ver.split(".")) >= (3, 10)
348
+ except ValueError:
349
+ ok = False
350
+ if ok:
351
+ add("ok", "python on PATH", f"{ver} ({py})")
352
+ else:
353
+ add("fail", "python on PATH", f"{'found ' + py + ' but it does not run 3.10+' if py else 'not found'}",
354
+ "install Python 3.10+ and make sure the command `python` works (the hooks call exactly that)")
355
+ claude = shutil.which("claude")
356
+ if not claude:
357
+ add("fail", "claude CLI", "not found", "install Claude Code, or install the plugin by hand with /plugin inside it")
358
+ else:
359
+ rc, ver = run_out([claude, "--version"])
360
+ add("ok" if rc == 0 else "fail", "claude CLI", ver.splitlines()[0] if ver else claude, "" if rc == 0 else "reinstall Claude Code")
361
+ rc, listing = run_out([claude, "plugin", "list"])
362
+ if rc:
363
+ add("warn", "claude plugin", "`claude plugin` is not available", "update Claude Code, or use /plugin inside it")
364
+ else:
365
+ add("ok", "jev-tools plugin", "installed" if "jev-tools" in listing else "not installed yet (setup installs it)")
366
+ add("ok" if shutil.which("git") else "warn", "git", shutil.which("git") or "not found",
367
+ "" if shutil.which("git") else "review-precheck and rule-calibrate read git history; the other pieces work without it")
368
+ try:
369
+ socket.create_connection((HOST, 443), timeout=5).close()
370
+ add("ok", f"network to {HOST}", "reachable")
371
+ except OSError as e:
372
+ add("warn", f"network to {HOST}", str(e)[:60], "only the api backend needs it")
373
+ name, blackwell = gpu()
374
+ add("ok" if blackwell else "info", "GPU for local mode",
375
+ f"{name} (Blackwell-class)" if blackwell else f"{name} (not Blackwell-class: use api or offline)" if name else "no NVIDIA GPU: use api or offline")
376
+ add("ok" if shutil.which("docker") else "info", "docker for local mode", shutil.which("docker") or "not found (only needed for local)")
377
+ saved = read_config()
378
+ if saved.get("backend") == "local":
379
+ err = probe(saved.get("base_url") or LOCAL_URL, None)
380
+ add("ok" if not err else "warn", "local server", f"{saved.get('model')} at {saved.get('base_url')}" + ("" if not err else f": {err}"),
381
+ "" if not err else "start it with `jev-tools-setup serve` (small models) or docker compose (full model)")
382
+ cfg = home() / "config.json"
383
+ add("info", "setup state", f"config {'present' if cfg.exists() else 'absent'}, key file {'present' if (home() / 'credentials').exists() else 'absent'}, "
384
+ f"env key {'set' if os.environ.get('OPENJEV_API_KEY') or os.environ.get('TYPESAFE_API_KEY') else 'unset'}")
385
+ if os.environ.get("JEV_MODE"):
386
+ add("warn", "JEV_MODE env var", f"set to {os.environ['JEV_MODE']!r}", "it overrides the mode saved by setup; unset it to use the saved mode")
387
+ return out
388
+
389
+
390
+ def check(_):
391
+ rows = checks()
392
+ tag = {"ok": "[ ok ]", "warn": "[warn]", "fail": "[FAIL]", "info": "[info]"}
393
+ for level, name, detail, hint in rows:
394
+ print(f"{tag[level]} {name:<22} {detail}" + (f"\n -> {hint}" if hint and level in ("warn", "fail") else ""))
395
+ bad = sum(r[0] == "fail" for r in rows)
396
+ print(f"\n{'Ready to run: jev-tools-setup' if not bad else str(bad) + ' required check(s) failed: fix them, then run check again'}"
397
+ f" ({sum(r[0] == 'warn' for r in rows)} warning(s))")
398
+ return 1 if bad else 0
399
+
400
+
401
+ def main(argv=None):
402
+ argv = list(sys.argv[1:] if argv is None else argv)
403
+ if not argv or argv[0] not in ("setup", "check", "serve", "report", "uninstall", "-h", "--help"):
404
+ argv.insert(0, "setup") # bare `jev-tools-setup` means setup
405
+ ap = argparse.ArgumentParser(prog="jev-tools-setup")
406
+ sub = ap.add_subparsers(dest="cmd", required=True)
407
+ s = sub.add_parser("setup", help="install the plugin and configure the backend")
408
+ s.add_argument("--backend", choices=("api", "local", "offline"), help="skip the backend question")
409
+ s.add_argument("--mode", choices=("shadow", "active", "off"))
410
+ s.add_argument("--base-url", help="local server URL (default %s)" % LOCAL_URL)
411
+ s.add_argument("--model", choices=("verdict-1.4", "laya-1.0", "openjev-latest"), help="local model, skips the question")
412
+ s.add_argument("--yes", action="store_true", help="do not ask before downloading/installing the local model")
413
+ s.add_argument("--key-stdin", action="store_true", help="read the API key from one line of stdin")
414
+ s.add_argument("--skip-plugin", action="store_true", help="do not call the claude CLI")
415
+ s.set_defaults(fn=setup)
416
+ sv = sub.add_parser("serve", help="run the local small model (verdict-1.4 or laya-1.0) in the foreground")
417
+ sv.add_argument("--device", choices=("cpu", "cuda"), help="force the device (default: GPU if PyTorch sees one)")
418
+ sv.set_defaults(fn=serve)
419
+ sub.add_parser("report", help="write ~/.jev-tools/LOG.md: what happened and what is wrong").set_defaults(fn=report)
420
+ sub.add_parser("check", help="read-only check of everything setup needs").set_defaults(fn=check)
421
+ sub.add_parser("uninstall", help="remove ~/.jev-tools").set_defaults(fn=uninstall)
422
+ a = ap.parse_args(argv)
423
+ return a.fn(a)
424
+
425
+
426
+ if __name__ == "__main__":
427
+ sys.exit(main())
@@ -0,0 +1,334 @@
1
+ Metadata-Version: 2.4
2
+ Name: jev-tools-setup
3
+ Version: 0.3.0
4
+ Summary: One-command installer for the jev-tools Claude Code plugin: pick the OpenJev (Codiv) API, a local model, or offline mode.
5
+ Author-email: Rcidshacker <ruchitdas36@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Rcidshacker/jev-tools
8
+ Project-URL: Issues, https://github.com/Rcidshacker/jev-tools/issues
9
+ Project-URL: Changelog, https://github.com/Rcidshacker/jev-tools/blob/main/CHANGELOG.md
10
+ Keywords: claude-code,openjev,codiv,plugin,installer
11
+ Classifier: Environment :: Console
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ <div align="center">
20
+
21
+ # jev-tools
22
+
23
+ **Let a tiny decision model make the cheap, frequent judgments, so Claude doesn't have to.**
24
+
25
+ A [Claude Code](https://code.claude.com) plugin for the terminal and the desktop app.
26
+ Does this edit break a project rule? Which skill fits this prompt? Which file matters? Does this diff need a careful review?
27
+
28
+ [![PyPI](https://img.shields.io/pypi/v/jev-tools-setup?label=jev-tools-setup)](https://pypi.org/project/jev-tools-setup/)
29
+ ![License MIT](https://img.shields.io/badge/license-MIT-green)
30
+ ![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)
31
+ ![stdlib only](https://img.shields.io/badge/dependencies-none-lightgrey)
32
+ ![Claude Code plugin](https://img.shields.io/badge/Claude%20Code-plugin-d97757)
33
+ ![shadow-first](https://img.shields.io/badge/ships%20in-shadow%20mode-orange)
34
+
35
+ [Quick start](#-quick-start) · [Choose a backend](#-choose-how-jev-runs) · [What's inside](#-whats-inside) · [Privacy](#-privacy-what-leaves-your-machine) · [Troubleshooting](#-when-something-looks-wrong) · [Reference](#-reference)
36
+
37
+ </div>
38
+
39
+ ---
40
+
41
+ jev-tools asks **OpenJev** ([Codiv](https://codiv.ai) "System One") typed questions (yes/no, pick-one, score) and gets calibrated probabilities back instead of generated text. There is nothing to parse and nothing to hallucinate; your own code applies the thresholds. It runs against the **hosted API**, a **model on your own machine**, or in an **offline** pattern-only mode.
42
+
43
+ > [!IMPORTANT]
44
+ > **Early and shadow-first.** Everything was built and measured against a live Codiv key (numbers in [MEASUREMENTS.md](https://github.com/Rcidshacker/jev-tools/blob/main/docs/MEASUREMENTS.md)), but the samples are small and OpenJev is a young model. The plugin ships in **shadow mode**: hooks only *log* what they would have done. Nothing blocks or injects until you switch to `active`.
45
+ > Independent project, not affiliated with Codiv, OpenJev or TypeSafe AI.
46
+
47
+ Write-up with the numbers, failures and privacy notes: [I built a Claude Code plugin around a model that only answers yes/no](https://dev.to/rcids/i-built-a-claude-code-plugin-around-a-model-that-only-answers-yesno-what-worked-what-failed-1ecj) · feedback welcome in [issue #1](https://github.com/Rcidshacker/jev-tools/issues/1).
48
+
49
+ ## 🚀 Quick start
50
+
51
+ **You need:** Python 3.10+ as the command `python` (the hooks call exactly that), the [`claude` CLI](https://code.claude.com), and [`uv`](https://docs.astral.sh/uv/) or `pipx`.
52
+
53
+ ```bash
54
+ # 1. install the setup tool (once)
55
+ uv tool install jev-tools-setup # or: pipx install jev-tools-setup
56
+
57
+ # 2. optional, read-only: is this machine ready?
58
+ jev-tools-setup check
59
+
60
+ # 3. install the plugin and choose how Jev runs
61
+ jev-tools-setup
62
+ ```
63
+
64
+ Setup installs the skills, scripts and hooks through Claude Code (so **terminal and desktop share one install**), asks which backend you want, validates it, and finishes by writing `~/.jev-tools/LOG.md`. **Restart Claude Code**, then run the `status` skill to confirm everything is wired.
65
+
66
+ > Prefer a one-shot run without installing anything? `uvx jev-tools-setup check` and `uvx jev-tools-setup` work too. To run the latest unreleased code straight from GitHub, use `uvx --from git+https://github.com/Rcidshacker/jev-tools jev-tools-setup`.
67
+
68
+ <details>
69
+ <summary><b>No uv or pipx? Install the plugin by hand</b></summary>
70
+
71
+ Inside Claude Code:
72
+
73
+ ```text
74
+ /plugin marketplace add Rcidshacker/jev-tools
75
+ /plugin install jev-tools@jev-tools
76
+ ```
77
+
78
+ Then give it a key (or a local server) yourself, see [Reference](#-reference). Try it for one session without installing: `claude --plugin-dir /path/to/jev-tools`.
79
+
80
+ </details>
81
+
82
+ ## 🔀 Choose how Jev runs
83
+
84
+ | | **`api`** | **`local`** | **`offline`** |
85
+ |---|---|---|---|
86
+ | **What it is** | Hosted OpenJev at `api.codiv.ai` | A model on your machine | No model, pattern fallbacks |
87
+ | **Best for** | Best accuracy, zero setup beyond a key | Privacy, no key, no quota | Plugin installed but nothing sent anywhere |
88
+ | **Needs** | A free [Codiv key](https://codiv.ai/dashboard) (100M input tokens) | Verdict/Laya: any CPU or GPU. Full OpenJev: NVIDIA Blackwell-class GPU + Docker | Nothing |
89
+ | **Leaves your machine** | Diffs, prompts, rules (see [Privacy](#-privacy-what-leaves-your-machine)) | Nothing (weights download once from Hugging Face) | Nothing |
90
+ | **Model quality** | Full OpenJev (all measurements below) | Verdict and Laya are **small and unmeasured here** | n/a |
91
+
92
+ <details>
93
+ <summary><b>api</b>: paste a key, it is checked and stored safely</summary>
94
+
95
+ Setup asks for your key in a **hidden prompt**, validates it with one live call, and stores it in `~/.jev-tools/credentials` with owner-only permissions (`chmod 600`, or `icacls` on Windows). A rejected key saves nothing. The key is never passed on a command line and never logged.
96
+
97
+ </details>
98
+
99
+ <details>
100
+ <summary><b>local</b>: Verdict 151M, Laya 421M, or the full OpenJev model</summary>
101
+
102
+ Setup asks which model:
103
+
104
+ | Model | Size | Context | Options | Notes |
105
+ |---|---|---|---|---|
106
+ | `verdict-1.4` | 151M | 512 tokens | up to 24 | Smallest. Ignores a yes/no question's `criteria`. Runs on CPU or any GPU |
107
+ | `laya-1.0` | 421M | 1,024 tokens | about 20 | Heavier. Runs on CPU or any GPU |
108
+ | `openjev-latest` | 26B | 65,536 tokens | 255 | Needs an NVIDIA **Blackwell-class** GPU (NVFP4) and Docker ([self-hosting guide](https://codiv.ai/docs/guides/self-hosting)) |
109
+
110
+ For Verdict/Laya, setup shows the plan and the download size, waits for your **yes**, then clones [OpenJev](https://github.com/razorback16/openjev) into `~/.jev-tools`, creates a venv and installs the model's extra (PyTorch comes with it and can be several GB; weights are about 0.6 GB / 1.7 GB as fp32). Start the server in its own terminal and keep it open:
111
+
112
+ ```bash
113
+ jev-tools-setup serve # add --device cpu to force the CPU
114
+ ```
115
+
116
+ **What changes with a small model.** Each question is read with its own copy of the state, cut from the end at the model's window. jev-tools therefore trims long diffs and file excerpts to fit, sends fewer files per `find-files` request, caps option lists (23 for Verdict, 19 for Laya, so `browser-nav` sees fewer elements) and declines a question it cannot shrink. `review-precheck` will mostly say "full review" because its diff is usually cut. **All thresholds and measurements come from the full OpenJev model**, so keep small models in `shadow` and run the `rule-calibrate` skill before switching to `active`.
117
+
118
+ > [!NOTE]
119
+ > The small-model install follows OpenJev's documented steps but is new in 0.3.0 and has not been measured on real projects yet.
120
+
121
+ </details>
122
+
123
+ <details>
124
+ <summary><b>offline</b>: "offline" means no model, not no internet</summary>
125
+
126
+ Nothing is sent anywhere. Deterministic fallbacks still run, and each says what it cannot do:
127
+
128
+ | Piece | Offline behaviour |
129
+ |---|---|
130
+ | `review-precheck` | Regex checks for secrets, dependency files, auth keywords, schema/migration changes, test weakening and swallowed errors. Says "fast" **only** for a small (30 lines or fewer) clean diff |
131
+ | Rule hook | Blocks only a **literal token** that a "never / don't / avoid X" rule forbids (e.g. `console.log`). No judgment |
132
+ | Skill hook | Picks by keyword match |
133
+ | `find-files` | Ranks by keyword count (in our measurements the model was no better, see below) |
134
+ | `browser-nav` | Suggests the element whose name matches the goal, always marked *unsure* |
135
+ | `rule-calibrate` | Unavailable: it needs a model, and says so |
136
+
137
+ These are patterns, not understanding. They miss anything phrased unusually, so keep offline in `shadow`.
138
+
139
+ </details>
140
+
141
+ ## 🧰 What's inside
142
+
143
+ | Piece | Kind | What it does |
144
+ |---|---|---|
145
+ | **Rule hook** `rule_enforcer.py` | `PreToolUse` on `Edit\|Write\|MultiEdit` | Reads your `CLAUDE.md` / `AGENTS.md` rules, asks whether the pending edit breaks one, re-checks any hit with a stricter question, and in `active` mode blocks the write with the rule it broke |
146
+ | **Skill hook** `skill_picker.py` | `UserPromptSubmit` (**opt-in**) | Picks the one installed skill that fits your prompt, confirms it, and in `active` mode adds a one-line pointer to the context |
147
+ | `find-files` | skill | Keyword candidates, then two-stage scoring, prints the most relevant files. A hint, not an oracle |
148
+ | `browser-nav` | skill | Picks each next click from the page's interactive elements; Claude executes and judges pass/fail |
149
+ | `review-precheck` | skill | Seven yes/no policy questions on a git diff decide *fast pass* vs *full review* |
150
+ | `rule-calibrate` | skill | Replays recent commits against your rules: which are decisive, noisy, weak or quiet, *before* you enforce anything |
151
+ | `status` | skill | One screen: backend, key found (by name), mode, hook state, recent events and errors |
152
+ | `report` | skill | Writes the plain-English [`LOG.md`](#-when-something-looks-wrong) |
153
+
154
+ ## ⚙️ How it works
155
+
156
+ ```mermaid
157
+ flowchart LR
158
+ A["Claude Code event<br/>edit · prompt · skill"] --> B["jev-tools script"]
159
+ B --> C{"backend"}
160
+ C -->|api| D["api.codiv.ai"]
161
+ C -->|local| E["Verdict · Laya · OpenJev<br/>127.0.0.1:8080"]
162
+ C -->|offline| F["keyword and pattern<br/>fallbacks"]
163
+ D --> G["typed answers<br/>yes/no · pick · score"]
164
+ E --> G
165
+ G --> H["thresholds in plain code"]
166
+ F --> H
167
+ H --> I{"mode"}
168
+ I -->|shadow| J["log only"]
169
+ I -->|active| K["block or inject"]
170
+ ```
171
+
172
+ Each piece turns a fuzzy judgment into typed questions, sends them with the relevant text as `state` to `POST /v1/systemone`, and applies thresholds in ordinary code. **Every failure path is explicit:** hooks **fail open** (an outage never blocks your edit or prompt) and the review pre-check **fails safe** (an outage routes to full review). Details and the reasoning behind each threshold: [HOW_IT_WORKS.md](https://github.com/Rcidshacker/jev-tools/blob/main/docs/HOW_IT_WORKS.md).
173
+
174
+ ### Modes
175
+
176
+ | Mode | Rule hook | Skill hook |
177
+ |---|---|---|
178
+ | `shadow` (default) | logs the verdict, never blocks | logs the pick, injects nothing |
179
+ | `active` | blocks an edit flagged at ≥ 0.80 **and** confirmed at ≥ 0.70 | injects `Jev skill pick: <name>` (if enabled) |
180
+ | `off` | does nothing, makes no call | does nothing |
181
+
182
+ **Recommended path:** run in `shadow` for a week, read `LOG.md`, run `rule-calibrate` on a project, then switch that setup to `active`. Set the mode with the plugin's `mode` option or `JEV_MODE` (the variable wins).
183
+
184
+ ## 🔒 Privacy: what leaves your machine
185
+
186
+ With the **`api`** backend this plugin sends text to a third party (`api.codiv.ai`). **`local` and `offline` send nothing.** Read this before enabling `api` on any project.
187
+
188
+ | Piece | What is sent (api only) |
189
+ |---|---|
190
+ | Rule hook (every `Edit`/`Write`) | file name, the unified diff (up to 8,000 characters) and the text of your project rules |
191
+ | Skill hook (**opt-in**, every prompt) | your prompt, plus the names and descriptions of your installed skills |
192
+ | `find-files` | the query and short excerpts of candidate files |
193
+ | `review-precheck` / `rule-calibrate` | the git diff (up to 30k characters) / recent commit hunks |
194
+ | `browser-nav` | the goal, the URL and the names of the page's interactive elements |
195
+
196
+ - **Shadow mode still sends.** The hook needs the model's answer to log it. Only `mode: off` sends nothing.
197
+ - **A local seatbelt runs first.** Before sending, these become `[REDACTED]`: private-key blocks, vendor-style keys (`sk-`/`sk_live_`, AWS, Google, GitHub, Slack, npm), JWTs, `Bearer` tokens, credentials in connection strings, assignments to names containing password/secret/token/api key, and long hex or base64-looking strings. Files named like secrets (`.env*`, `*.pem`, `*.key`, `id_rsa*`, `credentials*`, `secrets*` …) are never read into a request. It is a pattern match: a bare token in an unusual shape, **names, emails, customer or employee records and internal business data are not caught**, and about 0.2% of ordinary code lines that mention `token`/`secret` get partly redacted. Risk reduction, not a guarantee.
198
+ - **Retention is unknown to us.** Codiv's public API docs say nothing about how request data is stored or used. Check their terms before sending anything you would not paste into a public forum.
199
+ - **Nothing sensitive is stored locally.** The decision log holds verdicts, probabilities, latency and token counts, never file contents and never the key.
200
+
201
+ Turn it off for one project (regulated, customer, employee or client data) in that project's `.claude/settings.local.json`:
202
+
203
+ ```json
204
+ { "env": { "JEV_MODE": "off" } }
205
+ ```
206
+
207
+ ## 🩺 When something looks wrong
208
+
209
+ `jev-tools-setup` always ends by writing **`~/.jev-tools/LOG.md`**, even when setup fails. Refresh it any time with `jev-tools-setup report`, or run the **`report` skill** inside Claude Code. It merges every decision log (the plugin data directory and `~/.jev-tools`) into plain English:
210
+
211
+ - a one-line **health verdict**: ✅ healthy · ⚠️ notes · ❌ problems
212
+ - your **setup**: backend, model, mode, which variable the key came from (never the key), which logs were read
213
+ - **problems, each with a fix**
214
+ - **what happened**: event counts, median latency, rule checks flagged
215
+ - your **setup history** and the **last 40 events**, one sentence each
216
+
217
+ It contains no API key, file contents or prompts; home paths show as `~` and key-shaped strings are redacted, so it is **safe to attach to an issue**.
218
+
219
+ | Symptom | Likely cause | Fix |
220
+ |---|---|---|
221
+ | Hooks do nothing and nothing is logged | `JEV_MODE=off` (the variable beats the saved mode), or Claude Code was not restarted | Unset `JEV_MODE`, restart Claude Code |
222
+ | `status` says the key is **MISSING** | No key in the environment, plugin option or credentials file | `jev-tools-setup`, choose `api` |
223
+ | 401 / 403 in `LOG.md` | Key rejected | New key at [codiv.ai/dashboard](https://codiv.ai/dashboard), re-run setup |
224
+ | 429 in `LOG.md` | Quota or rate limit (quota errors are not retried) | Wait, or check usage on the dashboard |
225
+ | `python` not found, or opens the Microsoft Store | The hooks call the literal command `python` | Install Python 3.10+; `jev-tools-setup check` tests it |
226
+ | Local server not answering | It is not running, or still loading weights | `jev-tools-setup serve` |
227
+ | Bad edits are never blocked | You are in `shadow` (the default) | Calibrate, then switch to `active` |
228
+ | `review-precheck` always says "full" | Offline backend, a small model cutting the diff, or an outage | `LOG.md` says which |
229
+
230
+ Hooks **fail open**, so a broken install looks identical to a working one until you look at the log. That is exactly what `LOG.md` and the `status` skill are for. To remove everything: `jev-tools-setup uninstall` deletes `~/.jev-tools` (key, config, log); remove the plugin with `/plugin uninstall jev-tools@jev-tools`.
231
+
232
+ ## 📚 Reference
233
+
234
+ <details>
235
+ <summary><b>Giving it a key by hand</b></summary>
236
+
237
+ Any one of these, never in a repo. Lookup order: environment, plugin option, then the setup credentials file.
238
+
239
+ - **Environment variable:** a user-level `OPENJEV_API_KEY`. Windows: `setx OPENJEV_API_KEY "sk-codiv-..."` then restart Claude Code. macOS/Linux: export it in your shell profile.
240
+ - **Plugin option:** Claude Code prompts for the `api_key` setting and stores it as sensitive.
241
+ - **`jev-tools-setup`:** writes the owner-only credentials file for you.
242
+
243
+ Do not put the key in `settings.json`, `CLAUDE.md` or any file in a repo. The client only sends it to `api.codiv.ai` (or loopback), refuses redirects and never logs it. `python scripts/jevlib.py` makes one live call and prints `OK`.
244
+
245
+ </details>
246
+
247
+ <details>
248
+ <summary><b>Environment variables</b></summary>
249
+
250
+ | Variable | Default | Meaning |
251
+ |---|---|---|
252
+ | `OPENJEV_API_KEY` | none | API key (also `TYPESAFE_API_KEY`, or the plugin's `api_key` option) |
253
+ | `OPENJEV_BASE_URL` | `https://api.codiv.ai` | Must be `api.codiv.ai` or loopback |
254
+ | `JEV_MODE` | `shadow` | `shadow`, `active` or `off`; overrides the saved mode |
255
+ | `JEV_SKILL_PICKER` | off | `1` enables the skill hook (also the `skill_picker` option) |
256
+ | `JEV_THRESHOLD` | `0.80` | Rule-violation probability that can block |
257
+ | `JEV_CONFIRM` | `0.70` | Second-look probability required to block |
258
+ | `JEV_SKILL_MIN` | `0.60` | Minimum probability to inject a skill pick |
259
+ | `JEV_PRECHECK_MIN` | `0.25` | Yes-probability that flags a diff for full review |
260
+ | `JEV_LOG` | plugin data dir | Where decisions are appended (JSONL) |
261
+
262
+ </details>
263
+
264
+ <details>
265
+ <summary><b>Files jev-tools writes</b></summary>
266
+
267
+ | File | Holds |
268
+ |---|---|
269
+ | `~/.jev-tools/config.json` | `backend`, `model`, `base_url` (local only), `mode` |
270
+ | `~/.jev-tools/credentials` | your API key, owner-only |
271
+ | `~/.jev-tools/log.jsonl` and the plugin data dir's `log.jsonl` | one JSON line per decision: verdict, probability, latency, token counts |
272
+ | `~/.jev-tools/LOG.md` | the plain-English report built from the logs |
273
+ | `~/.jev-tools/openjev`, `~/.jev-tools/venv` | local small-model install |
274
+
275
+ </details>
276
+
277
+ <details>
278
+ <summary><b>What we measured</b> (full OpenJev, small samples, live against Codiv)</summary>
279
+
280
+ All reproducible from the scripts; method and caveats in [MEASUREMENTS.md](https://github.com/Rcidshacker/jev-tools/blob/main/docs/MEASUREMENTS.md).
281
+
282
+ | Component | Result |
283
+ |---|---|
284
+ | Rule enforcer | 4 of 4 planted violations blocked, 4 of 4 clean edits allowed after the second-look check was added; also blocked and allowed correctly inside real headless Claude Code sessions |
285
+ | Skill picker | 3 of 3 correct on a large real skill roster (two matches, one correct "none") |
286
+ | Review pre-check | benign rename routed *fast*; a diff with a hardcoded key, swallowed exception and emptied tests routed *full* with the right flags |
287
+ | Browser navigator | 5 of 5 steps correct on a synthetic login flow (never run against a real browser) |
288
+ | File discovery | **no better than plain keyword counting** on 8 labelled queries (top-3 hits 4 to 6 of 8 vs 4 of 8); repeat runs differ by up to 2 |
289
+ | Cost and speed | about 1 s per prompt or edit, 2 s when a violation is confirmed; about 5k input tokens per edit at 20 rules |
290
+
291
+ Not measured: the small local models (Verdict, Laya) and the offline fallbacks on real projects.
292
+
293
+ </details>
294
+
295
+ <details>
296
+ <summary><b>Repository layout</b></summary>
297
+
298
+ ```text
299
+ .claude-plugin/plugin.json plugin manifest and user settings (api_key, mode, skill_picker)
300
+ .claude-plugin/marketplace.json single-plugin marketplace so /plugin marketplace add works
301
+ installer/jev_tools_cli.py the jev-tools-setup command (setup, check, serve, report, uninstall)
302
+ installer/jev_report.py builds LOG.md (also run by the report skill)
303
+ hooks/hooks.json the two hooks
304
+ scripts/ jevlib.py (client) and one script per piece, review_policy.json
305
+ skills/<name>/SKILL.md six skills
306
+ tests/test_all.py offline suite against a local mock of /v1/systemone
307
+ docs/ how it works, measurements, build log, Codiv API notes
308
+ pyproject.toml packages the installer as jev-tools-setup
309
+ ```
310
+
311
+ </details>
312
+
313
+ ## 🛠️ Development
314
+
315
+ ```bash
316
+ python tests/test_all.py # offline suite, no network, no key needed
317
+ python scripts/jevlib.py # one live call, needs OPENJEV_API_KEY
318
+ uv build # builds the jev-tools-setup wheel and sdist
319
+ # releases: publishing a GitHub release runs .github/workflows/publish.yml (PyPI trusted publishing, no token)
320
+ ```
321
+
322
+ The tests spin up a local server that mimics `/v1/systemone`, so they prove the logic and the wire format, not OpenJev's accuracy. Accuracy claims come only from the live runs recorded in the docs. See the [CHANGELOG](https://github.com/Rcidshacker/jev-tools/blob/main/CHANGELOG.md) for what changed in each version.
323
+
324
+ ## 🙏 Credits
325
+
326
+ Ideas and hard-won numbers borrowed, with thanks, from projects that got there first (exactly what was taken from each: [BUILD_LOG.md](https://github.com/Rcidshacker/jev-tools/blob/main/docs/BUILD_LOG.md)):
327
+ [abide](https://github.com/coldteadotai/abide) (rule compilation, calibration verdicts, second look) ·
328
+ [hermes-jev-skills](https://github.com/kerpopule/hermes-jev-skills) (confidence floor, two-stage retrieval) ·
329
+ [jev-kit](https://github.com/jonathanavis96/jev-kit) (shadow-first rollout) ·
330
+ [jevgate](https://github.com/Tech-Byte-Frontier/jevgate) (confirming before acting) ·
331
+ [jev-skill-router](https://github.com/shimo4228/jev-skill-router) (plugin layout, honest field report on skill routing).
332
+ The small local models are [Verdict](https://github.com/Heman10x-NGU/Verdict-open-jev) by Heman10x and [Laya](https://github.com/NandhaKishorM/laya) by Nandakishor M / Convai Innovations, served by [OpenJev](https://github.com/razorback16/openjev).
333
+
334
+ Built with [Claude Code](https://claude.com/claude-code). MIT licensed, see [LICENSE](https://github.com/Rcidshacker/jev-tools/blob/main/LICENSE).
@@ -0,0 +1,8 @@
1
+ jev_report.py,sha256=LrsxV90yexnrN_WWNTSjpWsroCidb3V99lXgr8h0hz0,14297
2
+ jev_tools_cli.py,sha256=38Bc_DRTfygoDx_JRJMJo3oz3mZmNymxS3R4k8W3na0,21793
3
+ jev_tools_setup-0.3.0.dist-info/licenses/LICENSE,sha256=obggFVbV9pdR4lZqO0Q_HmdGBuBnqnUETIuLKf_NriA,1079
4
+ jev_tools_setup-0.3.0.dist-info/METADATA,sha256=3MZ4EzbxZjjPCg54zY_CfcRlcfY6BMxwYCw86tTPuAk,21610
5
+ jev_tools_setup-0.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
6
+ jev_tools_setup-0.3.0.dist-info/entry_points.txt,sha256=6sIci6yrkypDdKQeEV2wnRIi1vygP8yjUD8mn9VXFOg,55
7
+ jev_tools_setup-0.3.0.dist-info/top_level.txt,sha256=w2qIGmmsq6xQHDaKnWcB2hU8ZcNfnes0J9yF7-W6GM8,25
8
+ jev_tools_setup-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ jev-tools-setup = jev_tools_cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jev-tools contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ jev_report
2
+ jev_tools_cli