haluguard 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 haluguard
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,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: haluguard
3
+ Version: 0.1.0
4
+ Summary: CLI for haluguard — verify LLM output for hallucinations from your terminal or CI/CD.
5
+ Project-URL: Homepage, https://haluguard.com
6
+ Project-URL: Documentation, https://haluguard.com
7
+ Project-URL: Repository, https://github.com/haluguard/haluguard-cli
8
+ Project-URL: Issues, https://haluguard.com
9
+ Author-email: haluguard <support@haluguard.com>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: cli,fact-checking,hallucination,haluguard,llm,verification
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Topic :: Software Development :: Quality Assurance
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+
24
+ # haluguard CLI
25
+
26
+ [![PyPI version](https://img.shields.io/pypi/v/haluguard.svg)](https://pypi.org/project/haluguard/)
27
+ [![License](https://img.shields.io/pypi/l/haluguard.svg)](./LICENSE)
28
+
29
+ Verify LLM output for hallucinations directly from your terminal or CI/CD pipeline.
30
+ Plugs into the [haluguard](https://haluguard.com) hosted API.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install haluguard
36
+ ```
37
+
38
+ Zero runtime dependencies — pure stdlib, installs in seconds.
39
+
40
+ ## Quick start
41
+
42
+ ```bash
43
+ # 1. Save your API token (get one at haluguard.com)
44
+ haluguard auth login
45
+
46
+ # 2. Verify a file
47
+ haluguard check report.md
48
+
49
+ # 3. Or pipe input
50
+ echo "Microsoft was founded in 1981 by Bill Gates." | haluguard check
51
+
52
+ # JSON output for scripts / CI
53
+ haluguard check report.md --json | jq .
54
+
55
+ # Exit non-zero only above a confidence threshold (useful in CI)
56
+ haluguard check report.md --threshold 0.7
57
+ ```
58
+
59
+ ## Output
60
+
61
+ Human-readable by default:
62
+
63
+ ```
64
+ ✗ HALLUCINATIONS severity=clear score=0.95 claims=1/2 latency=4ms
65
+ verdict source: data_contradicted
66
+
67
+ [CONTRADICTED] high · wikidata
68
+ claim: Microsoft was founded in 1981
69
+ evidence: Wikidata confirms Microsoft was founded in 1975
70
+ source: https://www.wikidata.org/wiki/Q2283
71
+ fix: 1975
72
+ ```
73
+
74
+ `--json` returns the raw API response (verdict, severity_tier, score, findings, etc).
75
+
76
+ ## Exit codes
77
+
78
+ | Code | Meaning |
79
+ |------|---------|
80
+ | 0 | Clean / no hallucinations above threshold |
81
+ | 1 | Hallucinations detected |
82
+ | 2 | Transient API error (rate-limited, 5xx, network) |
83
+ | 3 | Auth error (no token / invalid token) |
84
+ | 4 | Bad input / usage |
85
+
86
+ ## CI/CD example — GitHub Actions
87
+
88
+ ```yaml
89
+ - name: Verify generated docs
90
+ env:
91
+ HALUGUARD_TOKEN: ${{ secrets.HALUGUARD_TOKEN }}
92
+ run: |
93
+ pip install haluguard
94
+ haluguard check generated/release-notes.md --threshold 0.7
95
+ ```
96
+
97
+ For a dedicated GitHub Action with richer output, see
98
+ [haluguard-action](https://github.com/haluguard/check-action).
99
+
100
+ ## Env vars
101
+
102
+ | Var | Default | Purpose |
103
+ |-----|---------|---------|
104
+ | `HALUGUARD_TOKEN` | (required) | Bearer token. Used in CI. Overrides `~/.haluguard/config.json`. |
105
+ | `HALUGUARD_API_URL` | `https://api.haluguard.com` | Override for self-host / dev. |
106
+ | `NO_COLOR` | unset | Disable ANSI colors. |
107
+
108
+ ## License
109
+
110
+ MIT (the CLI itself). The hosted API behind it is a paid subscription
111
+ service — see [haluguard.com](https://haluguard.com).
@@ -0,0 +1,88 @@
1
+ # haluguard CLI
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/haluguard.svg)](https://pypi.org/project/haluguard/)
4
+ [![License](https://img.shields.io/pypi/l/haluguard.svg)](./LICENSE)
5
+
6
+ Verify LLM output for hallucinations directly from your terminal or CI/CD pipeline.
7
+ Plugs into the [haluguard](https://haluguard.com) hosted API.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install haluguard
13
+ ```
14
+
15
+ Zero runtime dependencies — pure stdlib, installs in seconds.
16
+
17
+ ## Quick start
18
+
19
+ ```bash
20
+ # 1. Save your API token (get one at haluguard.com)
21
+ haluguard auth login
22
+
23
+ # 2. Verify a file
24
+ haluguard check report.md
25
+
26
+ # 3. Or pipe input
27
+ echo "Microsoft was founded in 1981 by Bill Gates." | haluguard check
28
+
29
+ # JSON output for scripts / CI
30
+ haluguard check report.md --json | jq .
31
+
32
+ # Exit non-zero only above a confidence threshold (useful in CI)
33
+ haluguard check report.md --threshold 0.7
34
+ ```
35
+
36
+ ## Output
37
+
38
+ Human-readable by default:
39
+
40
+ ```
41
+ ✗ HALLUCINATIONS severity=clear score=0.95 claims=1/2 latency=4ms
42
+ verdict source: data_contradicted
43
+
44
+ [CONTRADICTED] high · wikidata
45
+ claim: Microsoft was founded in 1981
46
+ evidence: Wikidata confirms Microsoft was founded in 1975
47
+ source: https://www.wikidata.org/wiki/Q2283
48
+ fix: 1975
49
+ ```
50
+
51
+ `--json` returns the raw API response (verdict, severity_tier, score, findings, etc).
52
+
53
+ ## Exit codes
54
+
55
+ | Code | Meaning |
56
+ |------|---------|
57
+ | 0 | Clean / no hallucinations above threshold |
58
+ | 1 | Hallucinations detected |
59
+ | 2 | Transient API error (rate-limited, 5xx, network) |
60
+ | 3 | Auth error (no token / invalid token) |
61
+ | 4 | Bad input / usage |
62
+
63
+ ## CI/CD example — GitHub Actions
64
+
65
+ ```yaml
66
+ - name: Verify generated docs
67
+ env:
68
+ HALUGUARD_TOKEN: ${{ secrets.HALUGUARD_TOKEN }}
69
+ run: |
70
+ pip install haluguard
71
+ haluguard check generated/release-notes.md --threshold 0.7
72
+ ```
73
+
74
+ For a dedicated GitHub Action with richer output, see
75
+ [haluguard-action](https://github.com/haluguard/check-action).
76
+
77
+ ## Env vars
78
+
79
+ | Var | Default | Purpose |
80
+ |-----|---------|---------|
81
+ | `HALUGUARD_TOKEN` | (required) | Bearer token. Used in CI. Overrides `~/.haluguard/config.json`. |
82
+ | `HALUGUARD_API_URL` | `https://api.haluguard.com` | Override for self-host / dev. |
83
+ | `NO_COLOR` | unset | Disable ANSI colors. |
84
+
85
+ ## License
86
+
87
+ MIT (the CLI itself). The hosted API behind it is a paid subscription
88
+ service — see [haluguard.com](https://haluguard.com).
@@ -0,0 +1,3 @@
1
+ """haluguard CLI — verify LLM output from your terminal or CI/CD pipeline."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,292 @@
1
+ """haluguard CLI — entry point.
2
+
3
+ Usage:
4
+ haluguard auth login # paste your token, save to ~/.haluguard/config.json
5
+ haluguard auth logout
6
+ haluguard auth status
7
+
8
+ haluguard check FILE # verify a file
9
+ haluguard check # verify stdin
10
+ haluguard check --json # raw JSON output (for scripts)
11
+ haluguard check --threshold 0.7 # exit non-zero if score >= 0.7
12
+
13
+ haluguard version
14
+ haluguard help
15
+
16
+ Env vars:
17
+ HALUGUARD_TOKEN — overrides config (used in CI/CD)
18
+ HALUGUARD_API_URL — defaults to https://api.haluguard.com (override for self-host/dev)
19
+
20
+ Exit codes:
21
+ 0 clean / no hallucinations above threshold
22
+ 1 hallucinations detected at or above threshold
23
+ 2 transient API error (rate-limited, 5xx)
24
+ 3 auth error (401/403)
25
+ 4 bad input / usage
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import argparse
31
+ import json
32
+ import os
33
+ import sys
34
+ from pathlib import Path
35
+
36
+ import urllib.error
37
+ import urllib.request
38
+
39
+
40
+ __version__ = "0.1.0"
41
+
42
+ DEFAULT_API_URL = "https://api.haluguard.com"
43
+ CONFIG_DIR = Path.home() / ".haluguard"
44
+ CONFIG_FILE = CONFIG_DIR / "config.json"
45
+
46
+
47
+ # ---------- ANSI colors (auto-disable on non-TTY / NO_COLOR) ----------
48
+
49
+ def _supports_color() -> bool:
50
+ if os.environ.get("NO_COLOR"):
51
+ return False
52
+ return sys.stdout.isatty()
53
+
54
+ def _c(code: str, text: str) -> str:
55
+ if not _supports_color():
56
+ return text
57
+ return f"\033[{code}m{text}\033[0m"
58
+
59
+ def red(s): return _c("31", s)
60
+ def green(s): return _c("32", s)
61
+ def yellow(s): return _c("33", s)
62
+ def gray(s): return _c("90", s)
63
+ def bold(s): return _c("1", s)
64
+
65
+
66
+ # ---------- Config storage ----------
67
+
68
+ def load_config() -> dict:
69
+ if not CONFIG_FILE.exists():
70
+ return {}
71
+ try:
72
+ return json.loads(CONFIG_FILE.read_text())
73
+ except Exception: # noqa: BLE001
74
+ return {}
75
+
76
+ def save_config(cfg: dict) -> None:
77
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
78
+ CONFIG_FILE.write_text(json.dumps(cfg, indent=2))
79
+ try:
80
+ CONFIG_FILE.chmod(0o600)
81
+ except Exception: # noqa: BLE001
82
+ pass
83
+
84
+ def resolve_token() -> str | None:
85
+ return os.environ.get("HALUGUARD_TOKEN") or load_config().get("token")
86
+
87
+ def resolve_api_url() -> str:
88
+ return (
89
+ os.environ.get("HALUGUARD_API_URL")
90
+ or load_config().get("api_url")
91
+ or DEFAULT_API_URL
92
+ ).rstrip("/")
93
+
94
+
95
+ # ---------- HTTP ----------
96
+
97
+ def call_check(text: str, user_prompt: str | None = None,
98
+ token: str | None = None, api_url: str | None = None,
99
+ timeout: float = 60.0) -> dict:
100
+ payload = json.dumps({"text": text, "user_prompt": user_prompt}).encode("utf-8")
101
+ req = urllib.request.Request(
102
+ f"{api_url or resolve_api_url()}/v1/check",
103
+ data=payload,
104
+ headers={
105
+ "Content-Type": "application/json",
106
+ "Authorization": f"Bearer {token or resolve_token() or ''}",
107
+ "User-Agent": f"haluguard-cli/{__version__}",
108
+ },
109
+ method="POST",
110
+ )
111
+ try:
112
+ with urllib.request.urlopen(req, timeout=timeout) as r:
113
+ return json.loads(r.read().decode("utf-8"))
114
+ except urllib.error.HTTPError as e:
115
+ body = e.read().decode("utf-8", errors="ignore")
116
+ try:
117
+ j = json.loads(body)
118
+ detail = j.get("detail") or body
119
+ except Exception: # noqa: BLE001
120
+ detail = body
121
+ if e.code in (401, 403):
122
+ sys.stderr.write(red(f"auth error ({e.code}): {detail}\n"))
123
+ sys.stderr.write(gray("Run `haluguard auth login` or set HALUGUARD_TOKEN.\n"))
124
+ sys.exit(3)
125
+ if e.code == 429 or 500 <= e.code < 600:
126
+ sys.stderr.write(yellow(f"transient API error ({e.code}): {detail}\n"))
127
+ sys.exit(2)
128
+ sys.stderr.write(red(f"API error ({e.code}): {detail}\n"))
129
+ sys.exit(2)
130
+ except urllib.error.URLError as e:
131
+ sys.stderr.write(red(f"connection error: {e.reason}\n"))
132
+ sys.exit(2)
133
+
134
+
135
+ # ---------- Commands ----------
136
+
137
+ def cmd_auth_login(args) -> int:
138
+ if args.token:
139
+ token = args.token
140
+ else:
141
+ sys.stderr.write("Paste your haluguard API token (starts with hg_live_): ")
142
+ sys.stderr.flush()
143
+ token = sys.stdin.readline().strip()
144
+ if not token.startswith("hg_"):
145
+ sys.stderr.write(red("That doesn't look like a haluguard token.\n"))
146
+ return 4
147
+ cfg = load_config()
148
+ cfg["token"] = token
149
+ save_config(cfg)
150
+ print(green(f"✓ Token saved to {CONFIG_FILE}"))
151
+ return 0
152
+
153
+ def cmd_auth_logout(args) -> int:
154
+ cfg = load_config()
155
+ if cfg.pop("token", None) is None:
156
+ print(gray("No token was saved."))
157
+ return 0
158
+ save_config(cfg)
159
+ print(green("✓ Logged out (token removed)."))
160
+ return 0
161
+
162
+ def cmd_auth_status(args) -> int:
163
+ token = resolve_token()
164
+ if not token:
165
+ print(red("Not logged in. Run `haluguard auth login`."))
166
+ return 3
167
+ masked = token[:12] + "…" + token[-4:]
168
+ print(green(f"Logged in. Token: {masked}"))
169
+ print(gray(f"API: {resolve_api_url()}"))
170
+ return 0
171
+
172
+ def cmd_check(args) -> int:
173
+ if args.file == "-" or args.file is None:
174
+ if sys.stdin.isatty() and args.file is None:
175
+ sys.stderr.write(yellow("No input file or stdin. Pipe text or pass a file.\n"))
176
+ sys.stderr.write(gray("Examples:\n"))
177
+ sys.stderr.write(gray(" haluguard check report.md\n"))
178
+ sys.stderr.write(gray(' echo "Microsoft was founded in 1981" | haluguard check\n'))
179
+ return 4
180
+ text = sys.stdin.read()
181
+ else:
182
+ p = Path(args.file)
183
+ if not p.exists():
184
+ sys.stderr.write(red(f"File not found: {p}\n"))
185
+ return 4
186
+ text = p.read_text()
187
+
188
+ text = text.strip()
189
+ if not text:
190
+ sys.stderr.write(yellow("Empty input.\n"))
191
+ return 4
192
+
193
+ if not resolve_token():
194
+ sys.stderr.write(red("Not logged in. Run `haluguard auth login` or set HALUGUARD_TOKEN.\n"))
195
+ return 3
196
+
197
+ response = call_check(text, user_prompt=args.user_prompt)
198
+
199
+ if args.json:
200
+ print(json.dumps(response, indent=2))
201
+ else:
202
+ _print_human_report(response)
203
+
204
+ score = float(response.get("score") or 0)
205
+ is_hallu = response.get("verdict") == "hallucinations_found"
206
+ if args.threshold is not None:
207
+ return 1 if score >= args.threshold else 0
208
+ return 1 if is_hallu else 0
209
+
210
+ def _print_human_report(r: dict) -> None:
211
+ is_hallu = r.get("verdict") == "hallucinations_found"
212
+ tier = r.get("severity_tier") or "—"
213
+ score = r.get("score", 0)
214
+ n_h = r.get("n_hallucinations", 0)
215
+ n_c = r.get("n_claims", 0)
216
+ dur = r.get("duration_ms", 0)
217
+ src = r.get("verdict_source") or ""
218
+
219
+ head = red("✗ HALLUCINATIONS") if is_hallu else green("✓ CLEAN")
220
+ print(f"{bold(head)} severity={tier} score={score:.2f} claims={n_h}/{n_c} latency={int(dur)}ms")
221
+ if src:
222
+ print(gray(f"verdict source: {src}"))
223
+ print()
224
+
225
+ findings = r.get("findings") or []
226
+ if not findings:
227
+ print(gray("No findings."))
228
+ return
229
+
230
+ for f in findings:
231
+ verdict = (f.get("verdict") or "").upper()
232
+ if verdict == "CONTRADICTED":
233
+ label = red(f"[{verdict}]")
234
+ elif verdict == "GROUNDED":
235
+ label = green(f"[{verdict}]")
236
+ else:
237
+ label = gray(f"[{verdict}]")
238
+ sev = f.get("severity") or ""
239
+ sev_str = f" {sev}" if sev else ""
240
+ verifier = f.get("verifier") or ""
241
+ print(f" {label}{sev_str} {gray('· ' + verifier)}")
242
+ print(f" {bold('claim:')} {f.get('claim') or ''}")
243
+ if f.get("evidence"):
244
+ print(f" {bold('evidence:')} {f.get('evidence')}")
245
+ if f.get("source_url"):
246
+ print(f" {bold('source:')} {gray(f.get('source_url'))}")
247
+ if f.get("suggested_correction"):
248
+ print(f" {bold('fix:')} {f.get('suggested_correction')}")
249
+ print()
250
+
251
+
252
+ # ---------- Arg parser ----------
253
+
254
+ def build_parser() -> argparse.ArgumentParser:
255
+ p = argparse.ArgumentParser(
256
+ prog="haluguard",
257
+ description="Verify LLM output for hallucinations from your terminal or CI/CD.",
258
+ )
259
+ p.add_argument("--version", action="version", version=f"haluguard {__version__}")
260
+ sub = p.add_subparsers(dest="command", required=True)
261
+
262
+ # check
263
+ pc = sub.add_parser("check", help="Verify text from a file or stdin.")
264
+ pc.add_argument("file", nargs="?", default=None, help="File path, or '-' for stdin (default: stdin).")
265
+ pc.add_argument("--user-prompt", default=None, help="Optional user prompt that produced the text.")
266
+ pc.add_argument("--json", action="store_true", help="Output raw JSON.")
267
+ pc.add_argument("--threshold", type=float, default=None,
268
+ help="Exit 1 only if score >= THRESHOLD (default: any 'hallucinations_found' triggers exit 1).")
269
+ pc.set_defaults(func=cmd_check)
270
+
271
+ # auth
272
+ pa = sub.add_parser("auth", help="Manage authentication token.")
273
+ auth_sub = pa.add_subparsers(dest="auth_command", required=True)
274
+ pal = auth_sub.add_parser("login", help="Save your API token locally.")
275
+ pal.add_argument("--token", default=None, help="Pass token inline (otherwise prompts).")
276
+ pal.set_defaults(func=cmd_auth_login)
277
+ pao = auth_sub.add_parser("logout", help="Remove the saved token.")
278
+ pao.set_defaults(func=cmd_auth_logout)
279
+ pas = auth_sub.add_parser("status", help="Show whether a token is configured.")
280
+ pas.set_defaults(func=cmd_auth_status)
281
+
282
+ return p
283
+
284
+
285
+ def main(argv: list[str] | None = None) -> int:
286
+ parser = build_parser()
287
+ args = parser.parse_args(argv)
288
+ return args.func(args)
289
+
290
+
291
+ if __name__ == "__main__":
292
+ raise SystemExit(main())
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "haluguard"
3
+ version = "0.1.0"
4
+ description = "CLI for haluguard — verify LLM output for hallucinations from your terminal or CI/CD."
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ authors = [{ name = "haluguard", email = "support@haluguard.com" }]
8
+ requires-python = ">=3.9"
9
+ keywords = ["llm", "hallucination", "verification", "cli", "haluguard", "fact-checking"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Environment :: Console",
13
+ "Intended Audience :: Developers",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Operating System :: OS Independent",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ "Topic :: Software Development :: Quality Assurance",
19
+ ]
20
+ # Zero runtime deps — uses urllib from stdlib so it stays tiny and trivially installable.
21
+ dependencies = []
22
+
23
+ [project.urls]
24
+ Homepage = "https://haluguard.com"
25
+ Documentation = "https://haluguard.com"
26
+ Repository = "https://github.com/haluguard/haluguard-cli"
27
+ Issues = "https://haluguard.com"
28
+
29
+ [project.scripts]
30
+ haluguard = "haluguard_cli.__main__:main"
31
+
32
+ [build-system]
33
+ requires = ["hatchling"]
34
+ build-backend = "hatchling.build"
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["haluguard_cli"]