jev-cli 0.4.1__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_cli/__init__.py +288 -0
- jev_cli/bundled_skills/jev-cli/SKILL.md +61 -0
- jev_cli/bundled_skills/jev-cli/references/cli.md +142 -0
- jev_cli/py.typed +0 -0
- jev_cli-0.4.1.dist-info/METADATA +302 -0
- jev_cli-0.4.1.dist-info/RECORD +9 -0
- jev_cli-0.4.1.dist-info/WHEEL +4 -0
- jev_cli-0.4.1.dist-info/entry_points.txt +3 -0
- jev_cli-0.4.1.dist-info/licenses/LICENSE +21 -0
jev_cli/__init__.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Small, dependency-free CLI for TypeSafe Jev."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import getpass
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import urllib.error
|
|
14
|
+
import urllib.request
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
API_URL = "https://api.typesafe.ai/v1/systemone"
|
|
19
|
+
CREDENTIALS_FILE = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "jev-cli" / "credentials.json"
|
|
20
|
+
BUNDLED_SKILLS = Path(__file__).with_name("bundled_skills")
|
|
21
|
+
INSTALL_MARKER = ".jev-cli-managed"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CliError(Exception):
|
|
25
|
+
def __init__(self, message: str, exit_code: int = 2) -> None:
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
self.exit_code = exit_code
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def api_key() -> str:
|
|
31
|
+
if value := os.environ.get("TYPESAFE_API_KEY"):
|
|
32
|
+
return value
|
|
33
|
+
try:
|
|
34
|
+
data = json.loads(CREDENTIALS_FILE.read_text())
|
|
35
|
+
value = data["api_key"]
|
|
36
|
+
except (OSError, json.JSONDecodeError, KeyError, TypeError) as exc:
|
|
37
|
+
raise CliError(
|
|
38
|
+
"TypeSafe API key is not stored; run: jev auth set",
|
|
39
|
+
3,
|
|
40
|
+
) from exc
|
|
41
|
+
if not isinstance(value, str) or not value:
|
|
42
|
+
raise CliError("stored TypeSafe API key is empty", 3)
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def set_api_key() -> None:
|
|
47
|
+
value = getpass.getpass("TypeSafe API key: ").strip() if sys.stdin.isatty() else sys.stdin.read().strip()
|
|
48
|
+
if not value:
|
|
49
|
+
raise CliError("API key is empty")
|
|
50
|
+
CREDENTIALS_FILE.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
51
|
+
try:
|
|
52
|
+
fd, temporary = tempfile.mkstemp(dir=CREDENTIALS_FILE.parent)
|
|
53
|
+
with os.fdopen(fd, "w") as file:
|
|
54
|
+
json.dump({"api_key": value}, file)
|
|
55
|
+
file.write("\n")
|
|
56
|
+
os.chmod(temporary, 0o600)
|
|
57
|
+
os.replace(temporary, CREDENTIALS_FILE)
|
|
58
|
+
except OSError as exc:
|
|
59
|
+
raise CliError("could not write the Jev credential store", 3) from exc
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def install_skills(
|
|
63
|
+
*, global_install: bool, claude: bool, cwd: Path | None = None, home: Path | None = None
|
|
64
|
+
) -> dict[str, Any]:
|
|
65
|
+
base = (home or Path.home()) if global_install else (cwd or Path.cwd())
|
|
66
|
+
flavor = ".claude" if claude else ".agents"
|
|
67
|
+
destination_root = base / flavor / "skills"
|
|
68
|
+
installed: list[str] = []
|
|
69
|
+
paths: list[str] = []
|
|
70
|
+
for source in sorted(BUNDLED_SKILLS.iterdir()):
|
|
71
|
+
if not source.is_dir() or not (source / "SKILL.md").is_file():
|
|
72
|
+
continue
|
|
73
|
+
destination = destination_root / source.name
|
|
74
|
+
if destination.exists() and not (destination / INSTALL_MARKER).is_file():
|
|
75
|
+
raise CliError(f"refusing to overwrite unmanaged skill: {destination}")
|
|
76
|
+
destination_root.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
staged = Path(tempfile.mkdtemp(prefix=f".{source.name}-", dir=destination_root))
|
|
78
|
+
try:
|
|
79
|
+
shutil.copytree(source, staged, dirs_exist_ok=True)
|
|
80
|
+
(staged / INSTALL_MARKER).write_text("managed by jev install-skills\n")
|
|
81
|
+
if destination.exists():
|
|
82
|
+
shutil.rmtree(destination)
|
|
83
|
+
staged.replace(destination)
|
|
84
|
+
except OSError as exc:
|
|
85
|
+
shutil.rmtree(staged, ignore_errors=True)
|
|
86
|
+
raise CliError(f"could not install skill: {exc}") from exc
|
|
87
|
+
installed.append(source.name)
|
|
88
|
+
paths.append(str(destination))
|
|
89
|
+
return {
|
|
90
|
+
"ok": True,
|
|
91
|
+
"scope": "global" if global_install else "local",
|
|
92
|
+
"flavor": "claude" if claude else "agents",
|
|
93
|
+
"destination": str(destination_root),
|
|
94
|
+
"installed": installed,
|
|
95
|
+
"paths": paths,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def read_text(value: str | None) -> str:
|
|
100
|
+
if value is None or value == "-":
|
|
101
|
+
if sys.stdin.isatty():
|
|
102
|
+
raise CliError("state is required as an argument, @file, or stdin")
|
|
103
|
+
return sys.stdin.read()
|
|
104
|
+
if value.startswith("@"):
|
|
105
|
+
try:
|
|
106
|
+
return Path(value[1:]).read_text()
|
|
107
|
+
except OSError as exc:
|
|
108
|
+
raise CliError(f"cannot read state file: {exc}") from exc
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def state_value(value: str | None, force_json: bool) -> Any:
|
|
113
|
+
text = read_text(value)
|
|
114
|
+
if force_json:
|
|
115
|
+
try:
|
|
116
|
+
return json.loads(text)
|
|
117
|
+
except json.JSONDecodeError as exc:
|
|
118
|
+
raise CliError(f"invalid JSON state: {exc}") from exc
|
|
119
|
+
return text
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def split_pair(value: str) -> tuple[str, str]:
|
|
123
|
+
key, separator, description = value.partition("=")
|
|
124
|
+
if not separator or not key or not description:
|
|
125
|
+
raise argparse.ArgumentTypeError("expected KEY=DESCRIPTION")
|
|
126
|
+
return key, description
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def load_request(path: str) -> dict[str, Any]:
|
|
130
|
+
text = read_text("-" if path == "-" else f"@{path}")
|
|
131
|
+
try:
|
|
132
|
+
payload = json.loads(text)
|
|
133
|
+
except json.JSONDecodeError as exc:
|
|
134
|
+
raise CliError(f"invalid request JSON: {exc}") from exc
|
|
135
|
+
if not isinstance(payload, dict) or "state" not in payload or "questions" not in payload:
|
|
136
|
+
raise CliError("request must be an object containing state and questions")
|
|
137
|
+
return payload
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def call(payload: dict[str, Any], endpoint: str) -> dict[str, Any]:
|
|
141
|
+
request = urllib.request.Request(
|
|
142
|
+
endpoint,
|
|
143
|
+
data=json.dumps(payload, ensure_ascii=False).encode(),
|
|
144
|
+
headers={
|
|
145
|
+
"Authorization": f"Bearer {api_key()}",
|
|
146
|
+
"Content-Type": "application/json",
|
|
147
|
+
"User-Agent": "jev-cli/0.4.1",
|
|
148
|
+
},
|
|
149
|
+
method="POST",
|
|
150
|
+
)
|
|
151
|
+
try:
|
|
152
|
+
with urllib.request.urlopen(request, timeout=60) as response:
|
|
153
|
+
result = json.load(response)
|
|
154
|
+
except urllib.error.HTTPError as exc:
|
|
155
|
+
body = exc.read().decode(errors="replace")
|
|
156
|
+
try:
|
|
157
|
+
detail = json.loads(body)
|
|
158
|
+
except json.JSONDecodeError:
|
|
159
|
+
detail = body
|
|
160
|
+
code = 3 if exc.code in (401, 403) else 4 if exc.code in (429, 500, 502, 503, 504) else 1
|
|
161
|
+
raise CliError(f"API HTTP {exc.code}: {json.dumps(detail, ensure_ascii=False)}", code) from exc
|
|
162
|
+
except (urllib.error.URLError, TimeoutError) as exc:
|
|
163
|
+
raise CliError(f"API connection failed: {exc}", 4) from exc
|
|
164
|
+
if not isinstance(result, dict):
|
|
165
|
+
raise CliError("API returned a non-object response", 1)
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def common_parser() -> argparse.ArgumentParser:
|
|
170
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
171
|
+
parser.add_argument("--model", default="jev-latest", help="model name (default: jev-latest)")
|
|
172
|
+
parser.add_argument("--json-state", action="store_true", help="parse state as JSON")
|
|
173
|
+
parser.add_argument("--pretty", action="store_true", help="pretty-print JSON")
|
|
174
|
+
parser.add_argument("--value", action="store_true", help="print only the primary answer value")
|
|
175
|
+
parser.add_argument("--endpoint", default=os.environ.get("TYPESAFE_API_URL", API_URL), help=argparse.SUPPRESS)
|
|
176
|
+
return parser
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def parser() -> argparse.ArgumentParser:
|
|
180
|
+
root = argparse.ArgumentParser(
|
|
181
|
+
prog="jev",
|
|
182
|
+
description="Evaluate text or JSON with TypeSafe Jev. Uses its own local credential store.",
|
|
183
|
+
)
|
|
184
|
+
root.add_argument("--version", action="version", version="jev 0.4.1")
|
|
185
|
+
sub = root.add_subparsers(dest="command", required=True)
|
|
186
|
+
common = common_parser()
|
|
187
|
+
|
|
188
|
+
auth = sub.add_parser("auth", help="manage the API key in the Jev credential store")
|
|
189
|
+
auth_sub = auth.add_subparsers(dest="auth_command", required=True)
|
|
190
|
+
auth_sub.add_parser("set", help="store an API key from a hidden prompt or stdin")
|
|
191
|
+
auth_sub.add_parser("status", help="check whether an API key is stored")
|
|
192
|
+
auth_sub.add_parser("test", help="connect to Jev and verify the API key")
|
|
193
|
+
|
|
194
|
+
skills = sub.add_parser("install-skills", help="install bundled agent skills")
|
|
195
|
+
skills.add_argument("-g", "--global", dest="global_install", action="store_true", help="install in the user home")
|
|
196
|
+
skills.add_argument("--claude", action="store_true", help="install for Claude instead of .agents")
|
|
197
|
+
|
|
198
|
+
noul = sub.add_parser("noul", parents=[common], help="answer one yes/no question with a probability")
|
|
199
|
+
noul.add_argument("-q", "--question", required=True, help="question to answer")
|
|
200
|
+
noul.add_argument("-s", "--state", help="text, @file, or - for stdin (default: stdin)")
|
|
201
|
+
|
|
202
|
+
choice = sub.add_parser("choice", parents=[common], help="choose one option")
|
|
203
|
+
choice.add_argument("-q", "--question", required=True, help="question to answer")
|
|
204
|
+
choice.add_argument("-s", "--state", help="text, @file, or - for stdin (default: stdin)")
|
|
205
|
+
choice.add_argument("-o", "--option", action="append", type=split_pair, required=True, metavar="KEY=DESCRIPTION")
|
|
206
|
+
|
|
207
|
+
score = sub.add_parser("score", parents=[common], help="score against ordered levels")
|
|
208
|
+
score.add_argument("-q", "--question", required=True, help="question to answer")
|
|
209
|
+
score.add_argument("-s", "--state", help="text, @file, or - for stdin (default: stdin)")
|
|
210
|
+
score.add_argument("-l", "--level", action="append", required=True, metavar="DESCRIPTION")
|
|
211
|
+
|
|
212
|
+
run = sub.add_parser("run", parents=[common], help="send a complete request JSON for batched questions")
|
|
213
|
+
run.add_argument("request", help="JSON file or - for stdin")
|
|
214
|
+
return root
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def request_for(args: argparse.Namespace) -> tuple[dict[str, Any], str | None]:
|
|
218
|
+
if args.command == "run":
|
|
219
|
+
payload = load_request(args.request)
|
|
220
|
+
payload.setdefault("model", args.model)
|
|
221
|
+
return payload, None
|
|
222
|
+
|
|
223
|
+
question: dict[str, Any] = {"type": args.command, "instructions": args.question}
|
|
224
|
+
if args.command == "choice":
|
|
225
|
+
question["criteria"] = dict(args.option)
|
|
226
|
+
elif args.command == "score":
|
|
227
|
+
question["criteria"] = args.level
|
|
228
|
+
payload = {
|
|
229
|
+
"state": state_value(args.state, args.json_state),
|
|
230
|
+
"model": args.model,
|
|
231
|
+
"questions": {"answer": question},
|
|
232
|
+
}
|
|
233
|
+
return payload, args.command
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def primary_value(result: dict[str, Any], kind: str | None) -> Any:
|
|
237
|
+
if kind is None:
|
|
238
|
+
raise CliError("--value is available only for noul, choice, and score")
|
|
239
|
+
answer = result["answers"]["answer"]
|
|
240
|
+
return answer[{"noul": "noul", "choice": "choice", "score": "score"}[kind]]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def main() -> int:
|
|
244
|
+
try:
|
|
245
|
+
args = parser().parse_args()
|
|
246
|
+
if args.command == "auth":
|
|
247
|
+
if args.auth_command == "set":
|
|
248
|
+
set_api_key()
|
|
249
|
+
print(json.dumps({"ok": True, "stored": True, "store": str(CREDENTIALS_FILE)}))
|
|
250
|
+
elif args.auth_command == "test":
|
|
251
|
+
result = call(
|
|
252
|
+
{
|
|
253
|
+
"state": "authentication test",
|
|
254
|
+
"model": "jev-latest",
|
|
255
|
+
"questions": {
|
|
256
|
+
"answer": {
|
|
257
|
+
"type": "noul",
|
|
258
|
+
"instructions": "Is this an authentication test?",
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
API_URL,
|
|
263
|
+
)
|
|
264
|
+
print(json.dumps({"ok": True, "valid": True, "model": result.get("model")}))
|
|
265
|
+
else:
|
|
266
|
+
api_key()
|
|
267
|
+
print(json.dumps({"ok": True, "stored": True, "store": str(CREDENTIALS_FILE)}))
|
|
268
|
+
return 0
|
|
269
|
+
if args.command == "install-skills":
|
|
270
|
+
print(json.dumps(install_skills(global_install=args.global_install, claude=args.claude)))
|
|
271
|
+
return 0
|
|
272
|
+
payload, kind = request_for(args)
|
|
273
|
+
result = call(payload, args.endpoint)
|
|
274
|
+
if args.value:
|
|
275
|
+
print(primary_value(result, kind))
|
|
276
|
+
else:
|
|
277
|
+
print(json.dumps(result, ensure_ascii=False, indent=2 if args.pretty else None))
|
|
278
|
+
return 0
|
|
279
|
+
except CliError as exc:
|
|
280
|
+
print(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False), file=sys.stderr)
|
|
281
|
+
return exc.exit_code
|
|
282
|
+
except (KeyError, TypeError) as exc:
|
|
283
|
+
print(json.dumps({"ok": False, "error": f"unexpected API response: {exc}"}), file=sys.stderr)
|
|
284
|
+
return 1
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
if __name__ == "__main__":
|
|
288
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: jev-cli
|
|
3
|
+
description: Use when classifying or scoring state with the `jev` CLI.
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
author: tumf
|
|
6
|
+
license: MIT
|
|
7
|
+
metadata:
|
|
8
|
+
hermes:
|
|
9
|
+
tags: [jev, cli, classification, scoring]
|
|
10
|
+
related_skills: []
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# jev-cli
|
|
14
|
+
|
|
15
|
+
## Overview
|
|
16
|
+
|
|
17
|
+
Use the `jev` command to ask TypeSafe Jev typed questions about text or JSON state. Prefer this skill when a task needs a compact machine-readable probability, one choice from explicit criteria, an ordered score, or several typed answers in one request.
|
|
18
|
+
|
|
19
|
+
The package and repository are named `jev-cli`; the installed command is `jev`.
|
|
20
|
+
|
|
21
|
+
Install this bundled skill with `jev install-skills`. Add `--global` for the user-wide target and `--claude` for Claude skill directories.
|
|
22
|
+
|
|
23
|
+
## Workflow
|
|
24
|
+
|
|
25
|
+
1. Confirm the command is installed with `jev --version`. If it is missing, install from the repository root with `make install`. Completion: `jev --version` exits successfully.
|
|
26
|
+
2. Confirm key availability with `jev auth status`, then use `jev auth test` when API validity must be verified. If absent or invalid, ask the user to configure a TypeSafe API key; never print, infer, or commit it. Completion: the selected command returns `ok: true`.
|
|
27
|
+
3. Choose the narrowest question type:
|
|
28
|
+
- `noul` for a yes/no probability.
|
|
29
|
+
- `choice` for one key from explicit options.
|
|
30
|
+
- `score` for an ordered numeric level.
|
|
31
|
+
- `run` for a complete request object or multiple questions.
|
|
32
|
+
4. Send only the state needed for the judgment. TypeSafe receives the submitted state and questions, so do not send secrets or private data without explicit authorization.
|
|
33
|
+
5. Use default JSON output for automation. Use `--value` only when a scalar is sufficient. Completion: parse the result and account for the exit code before acting on it.
|
|
34
|
+
|
|
35
|
+
Read [references/cli.md](references/cli.md) for copy-paste command forms, inputs, output controls, and exit codes.
|
|
36
|
+
|
|
37
|
+
## Decision rules
|
|
38
|
+
|
|
39
|
+
- Give `choice` options stable keys and descriptions that make the categories mutually understandable.
|
|
40
|
+
- Give `score` levels in ascending order; the returned number may be fractional.
|
|
41
|
+
- Use `--json-state` only when the state must retain JSON structure.
|
|
42
|
+
- Use `@file` or stdin for long input instead of embedding it in shell arguments.
|
|
43
|
+
- Set `TYPESAFE_API_KEY` only for process-level overrides. The normal credential store remains under the user's config directory.
|
|
44
|
+
- Treat model output as a structured judgment, not verified fact. Keep consequential actions behind the caller's own validation and authorization rules.
|
|
45
|
+
|
|
46
|
+
## Common pitfalls
|
|
47
|
+
|
|
48
|
+
1. **Calling the executable `jev-cli`.** The package is `jev-cli`, but the executable is `jev`.
|
|
49
|
+
2. **Using `--value` with `run`.** Batched requests require the JSON response.
|
|
50
|
+
3. **Treating transport failures as negative answers.** Exit code `4` means retryable API or network failure, not a model judgment.
|
|
51
|
+
4. **Leaking state through convenience.** Inspect file and stdin content before submission when it may contain credentials or private records.
|
|
52
|
+
5. **Embedding an API key in examples.** Use `jev auth set` through stdin or the environment variable without writing its value into scripts or documentation.
|
|
53
|
+
|
|
54
|
+
## Verification checklist
|
|
55
|
+
|
|
56
|
+
- [ ] `jev --version` succeeds.
|
|
57
|
+
- [ ] `jev auth status` or `jev auth test` succeeds without exposing the key.
|
|
58
|
+
- [ ] The selected question type matches the desired answer shape.
|
|
59
|
+
- [ ] Submitted state contains no unauthorized secret or private data.
|
|
60
|
+
- [ ] The caller handles JSON and nonzero exit codes.
|
|
61
|
+
- [ ] Consequential follow-up actions retain their own authorization checks.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# CLI reference
|
|
2
|
+
|
|
3
|
+
## Authentication
|
|
4
|
+
|
|
5
|
+
Check whether authentication is available without printing the key:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
jev auth status
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Connect to Jev and verify that the resolved API key is accepted:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
jev auth test
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
In a terminal, enter the API key at the hidden prompt:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
jev auth set
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For non-interactive automation, stdin remains supported:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
printf '%s' "$TYPESAFE_API_KEY" | jev auth set
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Resolution order:
|
|
30
|
+
|
|
31
|
+
1. `TYPESAFE_API_KEY`
|
|
32
|
+
2. `$XDG_CONFIG_HOME/jev-cli/credentials.json`
|
|
33
|
+
3. `~/.config/jev-cli/credentials.json`
|
|
34
|
+
|
|
35
|
+
## Typed questions
|
|
36
|
+
|
|
37
|
+
Use `-q` for `--question` and `-s` for `--state` when a shorter command is useful. `--value` has no short form.
|
|
38
|
+
|
|
39
|
+
Return a yes/no probability from `0` to `1`:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
jev noul --question 'Does this message require an urgent response?' --state 'Please respond today.'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Choose one stable key from explicit criteria:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
jev choice --question 'Which team should receive this?' --state 'Checkout returns HTTP 500.' \
|
|
49
|
+
-o engineering='Software defect or technical failure' \
|
|
50
|
+
-o sales='Purchase or pricing question'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Score against ordered levels:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
jev score --question 'How dissatisfied is this customer?' --state 'This is the third failure.' \
|
|
57
|
+
-l 'Not dissatisfied' \
|
|
58
|
+
-l 'Slightly dissatisfied' \
|
|
59
|
+
-l 'Clearly dissatisfied' \
|
|
60
|
+
-l 'Extremely dissatisfied'
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Send a complete request from a file:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
jev run request.json
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Input forms
|
|
70
|
+
|
|
71
|
+
Literal text:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
jev noul --question 'Does this require action?' --state 'Please investigate.'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
File content:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
jev noul --question 'Does this require action?' --state @message.txt
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Standard input:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
printf '%s' 'Please investigate.' | jev noul --question 'Does this require action?' --state -
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Structured JSON state:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
printf '%s' '{"message":"Please investigate","priority":"high"}' | \
|
|
93
|
+
jev noul --question 'Does this require action?' --state - --json-state
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Output controls
|
|
97
|
+
|
|
98
|
+
Pretty JSON:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
jev noul --question 'Does this require action?' --state 'Please investigate.' --pretty
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Primary scalar only:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
jev noul --question 'Does this require action?' --state 'Please investigate.' --value
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`--value` applies to `noul`, `choice`, and `score`, not `run`.
|
|
111
|
+
|
|
112
|
+
## Exit codes
|
|
113
|
+
|
|
114
|
+
| Code | Meaning |
|
|
115
|
+
|---:|---|
|
|
116
|
+
| `0` | Success |
|
|
117
|
+
| `1` | Invalid response or non-retryable API error |
|
|
118
|
+
| `2` | Local input or credential error |
|
|
119
|
+
| `3` | Authentication or authorization failure |
|
|
120
|
+
| `4` | Retryable network, rate-limit, or server failure |
|
|
121
|
+
|
|
122
|
+
Read stderr as JSON on failure. Do not interpret a nonzero exit as a model answer.
|
|
123
|
+
|
|
124
|
+
## Installation
|
|
125
|
+
|
|
126
|
+
From the repository root:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
make install
|
|
130
|
+
jev --version
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The project requires Python 3.13 or later and `uv`.
|
|
134
|
+
|
|
135
|
+
Install the bundled `jev-cli` Agent Skill:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
jev install-skills
|
|
139
|
+
jev install-skills --global
|
|
140
|
+
jev install-skills --claude
|
|
141
|
+
jev install-skills --global --claude
|
|
142
|
+
```
|
jev_cli/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jev-cli
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Small dependency-free CLI for TypeSafe Jev
|
|
5
|
+
Keywords: typesafe,jev,cli,ai
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Requires-Python: >=3.13
|
|
14
|
+
Project-URL: Homepage, https://github.com/tumf/jev-cli
|
|
15
|
+
Project-URL: Issues, https://github.com/tumf/jev-cli/issues
|
|
16
|
+
Project-URL: Repository, https://github.com/tumf/jev-cli
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# jev-cli
|
|
20
|
+
|
|
21
|
+
A small, dependency-free CLI for [TypeSafe Jev](https://docs.typesafe.ai/introduction). Send text or JSON state, ask typed questions, and receive machine-readable `noul`, `choice`, or `score` answers.
|
|
22
|
+
|
|
23
|
+
The `jev` command is useful when application code needs a fast classification or judgment instead of generated prose.
|
|
24
|
+
|
|
25
|
+
> **Unofficial:** This is an independent community project. It is not affiliated with, maintained by, or endorsed by TypeSafe AI.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- Supports all three Jev primitives: `noul`, `choice`, and `score`
|
|
30
|
+
- Sends multiple questions in one request with `run`
|
|
31
|
+
- Accepts text, JSON, files, and stdin
|
|
32
|
+
- Emits compact JSON by default
|
|
33
|
+
- Can print only the primary value for shell scripts
|
|
34
|
+
- Uses structured stderr errors and meaningful exit codes
|
|
35
|
+
- Has no runtime dependencies outside Python's standard library
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
- Python 3.13 or later
|
|
40
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
41
|
+
- GNU Make
|
|
42
|
+
- A TypeSafe API key
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
Clone the repository and install the command with `uv tool` through the Makefile. This installs `jev` into uv's executable directory and verifies the installed version.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/tumf/jev-cli.git
|
|
50
|
+
cd jev-cli
|
|
51
|
+
make install
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Verify that the command is available:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
jev --version
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Expected output:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
jev 0.4.1
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Authentication
|
|
67
|
+
|
|
68
|
+
The recommended approach for automation is the `TYPESAFE_API_KEY` environment variable. It takes precedence over the credential file.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
export TYPESAFE_API_KEY='your-api-key'
|
|
72
|
+
jev auth status
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
For local use, enter the key at the hidden prompt. `auth set` does not accept the key as a command-line argument, which keeps it out of process arguments and shell history.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
jev auth set
|
|
79
|
+
jev auth status
|
|
80
|
+
jev auth test
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For non-interactive automation, piping the key to `jev auth set` remains supported.
|
|
84
|
+
|
|
85
|
+
`auth status` reports only whether a key is available. `auth test` sends a minimal request to Jev and verifies that the key is accepted. Neither command prints the key.
|
|
86
|
+
|
|
87
|
+
The fallback credential path follows XDG conventions:
|
|
88
|
+
|
|
89
|
+
- `$XDG_CONFIG_HOME/jev-cli/credentials.json` when `XDG_CONFIG_HOME` is set
|
|
90
|
+
- `~/.config/jev-cli/credentials.json` otherwise
|
|
91
|
+
|
|
92
|
+
The credential directory is created with mode `0700`; the file is written atomically with mode `0600`.
|
|
93
|
+
|
|
94
|
+
## Quick start
|
|
95
|
+
|
|
96
|
+
`--question` and `--state` also accept the short forms `-q` and `-s`. `--value` has no short form.
|
|
97
|
+
|
|
98
|
+
## Install the bundled Agent Skill
|
|
99
|
+
|
|
100
|
+
`jev-cli` currently bundles the `jev-cli` skill. Install it for the current project or globally:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
jev install-skills
|
|
104
|
+
jev install-skills --global
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Use `--claude` to target Claude's skill directory instead:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
jev install-skills --claude
|
|
111
|
+
jev install-skills --global --claude
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The command prints JSON. It refreshes only copies it previously installed and refuses to overwrite an unmanaged skill directory.
|
|
115
|
+
|
|
116
|
+
Ask whether a message expresses urgency. `--value` prints only the resulting probability from `0` to `1`.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
jev noul \
|
|
120
|
+
--question 'Does this message express urgency?' \
|
|
121
|
+
--state 'Please restore service today.' \
|
|
122
|
+
--value
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Example output:
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
0.98
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Without `--value`, the command returns the complete API response as JSON, including model and token usage.
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
jev noul \
|
|
135
|
+
--question 'Does this message express urgency?' \
|
|
136
|
+
--state 'Please restore service today.' \
|
|
137
|
+
--pretty
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Question types
|
|
141
|
+
|
|
142
|
+
### Noul: yes/no probability
|
|
143
|
+
|
|
144
|
+
Use `noul` for one focused yes/no judgment. The value is the probability that the answer is yes.
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
jev noul \
|
|
148
|
+
--question 'Does this message request a refund?' \
|
|
149
|
+
--state 'The integration is broken, but I do not want a refund.' \
|
|
150
|
+
--value
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Choice: select one option
|
|
154
|
+
|
|
155
|
+
Use `choice` when the answer must be one of a known set. Each option uses `KEY=DESCRIPTION` syntax.
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
jev choice \
|
|
159
|
+
--question 'Which team should handle this?' \
|
|
160
|
+
--state 'The payment integration keeps failing.' \
|
|
161
|
+
-o 'billing=Payment, charge, or refund issues' \
|
|
162
|
+
-o 'technical=Bugs or integration failures' \
|
|
163
|
+
-o 'other=None of these' \
|
|
164
|
+
--pretty
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Score: evaluate ordered levels
|
|
168
|
+
|
|
169
|
+
Use `score` for an ordered scale. Levels are numbered from zero in the order supplied.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
jev score \
|
|
173
|
+
--question 'How frustrated is the customer?' \
|
|
174
|
+
--state 'This has failed for three days. Please help.' \
|
|
175
|
+
-l 'Calm' \
|
|
176
|
+
-l 'Concerned but civil' \
|
|
177
|
+
-l 'Very angry' \
|
|
178
|
+
--value
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Input formats
|
|
182
|
+
|
|
183
|
+
### Standard input
|
|
184
|
+
|
|
185
|
+
Omit the state or pass `-` to read it from stdin. This is useful for pipelines and avoids putting sensitive input in shell history.
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
printf '%s' 'Please resolve this today.' | \
|
|
189
|
+
jev noul --question 'Does this message express urgency?' --value
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### File input
|
|
193
|
+
|
|
194
|
+
Prefix a path with `@` to read its contents.
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
jev noul \
|
|
198
|
+
--question 'Does this document mention security risks?' \
|
|
199
|
+
--state @document.txt \
|
|
200
|
+
--value
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### JSON state
|
|
204
|
+
|
|
205
|
+
Use `--json-state` to parse the state as JSON. Instructions can refer to named fields.
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
printf '%s' '{"message":"Please respond today"}' | \
|
|
209
|
+
jev noul \
|
|
210
|
+
--question 'Does `message` express urgency?' \
|
|
211
|
+
--json-state \
|
|
212
|
+
--value
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Batch questions
|
|
216
|
+
|
|
217
|
+
Jev evaluates questions independently against the same state. Use `run` to send a complete System One request and avoid one API call per question.
|
|
218
|
+
|
|
219
|
+
Create `request.json`:
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"state": {
|
|
224
|
+
"message": "The payment integration has failed for three days. Please fix it today."
|
|
225
|
+
},
|
|
226
|
+
"model": "jev-latest",
|
|
227
|
+
"questions": {
|
|
228
|
+
"department": {
|
|
229
|
+
"type": "choice",
|
|
230
|
+
"instructions": "Which team should handle `message`?",
|
|
231
|
+
"criteria": {
|
|
232
|
+
"billing": "Payment, charge, or refund issues",
|
|
233
|
+
"technical": "Bugs or integration failures",
|
|
234
|
+
"other": "None of these"
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
"urgent": {
|
|
238
|
+
"type": "noul",
|
|
239
|
+
"instructions": "Does `message` express urgency?"
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Send it in one request:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
jev run request.json --pretty
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
A request can also be piped through stdin:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
cat request.json | jev run - --pretty
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Output and automation
|
|
258
|
+
|
|
259
|
+
The default stdout is one JSON object. Logs and structured errors go to stderr, so stdout can be piped directly into another program.
|
|
260
|
+
|
|
261
|
+
Use `--value` with `noul`, `choice`, or `score` when a script needs only the primary answer:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
if awk 'BEGIN { exit !(ARGV[1] >= 0.9) }' \
|
|
265
|
+
"$(jev noul --question 'Is this urgent?' --state 'Restore service today.' --value)"; then
|
|
266
|
+
echo urgent
|
|
267
|
+
fi
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Use `--model` to select another model available to the account:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
jev noul --question 'Is this urgent?' --state 'Restore service today.' \
|
|
274
|
+
--model jev-latest \
|
|
275
|
+
--pretty
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Exit codes
|
|
279
|
+
|
|
280
|
+
| Code | Meaning |
|
|
281
|
+
|---:|---|
|
|
282
|
+
| `0` | Success |
|
|
283
|
+
| `1` | Unexpected API response or other error |
|
|
284
|
+
| `2` | Invalid arguments or input |
|
|
285
|
+
| `3` | Missing or rejected authentication |
|
|
286
|
+
| `4` | Connection, rate-limit, or transient server error |
|
|
287
|
+
|
|
288
|
+
An error is emitted as JSON on stderr:
|
|
289
|
+
|
|
290
|
+
```json
|
|
291
|
+
{"ok": false, "error": "TypeSafe API key is not stored; run: jev auth set"}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Scope and limitations
|
|
295
|
+
|
|
296
|
+
The `jev` command is a thin client for focused System One judgments. It does not generate prose, perform arithmetic, compare dates, or replace application-level validation. Keep deterministic work in code and use Jev for semantic judgments.
|
|
297
|
+
|
|
298
|
+
The CLI sends the supplied state and questions to the TypeSafe API. Do not submit data that your organization is not permitted to send to that service.
|
|
299
|
+
|
|
300
|
+
## License
|
|
301
|
+
|
|
302
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
jev_cli/__init__.py,sha256=7HUg9ytx1qciSS6pktmaSj0dJWU0qD9z1-YIM0R06P8,11869
|
|
2
|
+
jev_cli/bundled_skills/jev-cli/SKILL.md,sha256=w6yne7l1IA02c3rlhldNtJTN650GdE6-0N19duHkgiA,3550
|
|
3
|
+
jev_cli/bundled_skills/jev-cli/references/cli.md,sha256=3t7z046QqYovrRjvsl7_MDwMf_KnH0o-Unx3ADF0dc0,2931
|
|
4
|
+
jev_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
jev_cli-0.4.1.dist-info/licenses/LICENSE,sha256=oKoTGKD5xnfr8R9BogCNrZGMoJ9oCYg-tLqJxoeDYwQ,1061
|
|
6
|
+
jev_cli-0.4.1.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
7
|
+
jev_cli-0.4.1.dist-info/entry_points.txt,sha256=PJeuUSh7E8GY0ifzE742Z7NL_42yPSw_XPYapVdfd0I,38
|
|
8
|
+
jev_cli-0.4.1.dist-info/METADATA,sha256=Dt0i_f3cdc9DLznd4o2f9qqj3v0vV8Y7GVoqtmlBZ3Q,8155
|
|
9
|
+
jev_cli-0.4.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tumf
|
|
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.
|