typesafeai-cli 0.3.1__tar.gz → 0.3.3__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.
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/.gitignore +1 -0
- typesafeai_cli-0.3.3/PKG-INFO +107 -0
- typesafeai_cli-0.3.3/README.md +83 -0
- typesafeai_cli-0.3.3/examples/README.md +12 -0
- typesafeai_cli-0.3.3/examples/fan-out/questions.json +28 -0
- typesafeai_cli-0.3.3/examples/fan-out/state.json +10 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/install.sh +3 -7
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/pyproject.toml +1 -1
- typesafeai_cli-0.3.3/src/typesafe_cli/__init__.py +1 -0
- typesafeai_cli-0.3.3/src/typesafe_cli/agent_files.py +69 -0
- typesafeai_cli-0.3.3/src/typesafe_cli/answers.py +86 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/cli.py +1 -1
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/client.py +4 -1
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/ask.py +5 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/decide.py +6 -1
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/eval.py +3 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/find.py +1 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/skills.py +42 -14
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/data/typesafe-ai/CLI.md +1 -1
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/data/typesafe-cli/SKILL.md +13 -5
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/recipes/decide.py +49 -4
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/recipes/extract.py +3 -1
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/recipes/find.py +29 -10
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/recipes/screen.py +8 -4
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/recipes/verify.py +3 -1
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/schema.py +2 -2
- typesafeai_cli-0.3.1/PKG-INFO +0 -137
- typesafeai_cli-0.3.1/README.md +0 -113
- typesafeai_cli-0.3.1/examples/README.md +0 -15
- typesafeai_cli-0.3.1/src/typesafe_cli/__init__.py +0 -1
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/LICENSE +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/examples/code-change/questions.json +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/examples/code-change/state.json +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/examples/ticket/questions.json +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/examples/ticket/state.json +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/__init__.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/agent.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/auth.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/extract.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/models.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/oneshot.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/rank.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/screen.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/smoke.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/suggest_skill.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/commands/verify.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/config.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/data/__init__.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/data/typesafe-ai/SKILL.md +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/detect.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/format.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/help.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/io.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/questions.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/recipes/__init__.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/recipes/rank.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/recipes/skills.py +0 -0
- {typesafeai_cli-0.3.1 → typesafeai_cli-0.3.3}/src/typesafe_cli/recipes/thresholds.py +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: typesafeai-cli
|
|
3
|
+
Version: 0.3.3
|
|
4
|
+
Summary: Agent CLI for TypeSafe System One (Jev)
|
|
5
|
+
Project-URL: Homepage, https://github.com/maddygoround/typesafeai-cli
|
|
6
|
+
Project-URL: Repository, https://github.com/maddygoround/typesafeai-cli
|
|
7
|
+
Project-URL: Issues, https://github.com/maddygoround/typesafeai-cli/issues
|
|
8
|
+
Project-URL: Releases, https://github.com/maddygoround/typesafeai-cli/releases
|
|
9
|
+
Author: safeaiforeveryone
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: cli,jev,typesafe
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: typer>=0.12
|
|
22
|
+
Requires-Dist: typesafe-sdk>=0.6.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# typesafe
|
|
26
|
+
|
|
27
|
+
CLI for [TypeSafe](https://typesafe.ai) Jev. Run it from a shell, or let an agent run it.
|
|
28
|
+
|
|
29
|
+
Jev answers `noul`, `choice`, and `score` questions over JSON `state`. It does not chat, and it only sees what you send. Invalid answers fail closed. Unused speculative heads cannot act. Ranked `find` lines are not evidence when the document has no answer.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
Python 3.10+. API key: [TypeSafe console](https://console.typesafe.ai/settings/keys).
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
curl -fsSL https://github.com/maddygoround/typesafeai-cli/releases/latest/download/install.sh | bash
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pipx install typesafeai-cli
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`pip install typesafeai-cli` is the same package.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
export TYPESAFE_API_KEY=apikey_…
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
`typesafe ask` takes a state file and a questions file. Point each instruction at state with backticks.
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"message": "I was charged twice for order A-104. Please refund the duplicate.",
|
|
56
|
+
"policy": "Duplicate charges are eligible for an immediate refund."
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"refund_requested": {
|
|
63
|
+
"type": "noul",
|
|
64
|
+
"instructions": "Does `message` request a refund?"
|
|
65
|
+
},
|
|
66
|
+
"intent": {
|
|
67
|
+
"type": "choice",
|
|
68
|
+
"instructions": "What does `message` ask for?",
|
|
69
|
+
"criteria": {
|
|
70
|
+
"refund": "The customer wants money returned",
|
|
71
|
+
"information": "Explanation only",
|
|
72
|
+
"other": "Something else"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
[`examples/ticket`](examples/ticket), [`examples/code-change`](examples/code-change), and [`examples/fan-out`](examples/fan-out) are full copies.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
typesafe ask --state-file state.json --questions-file questions.json
|
|
82
|
+
typesafe noul "Does this request a refund?" --state "I was charged twice."
|
|
83
|
+
typesafe decide --answers-file last.json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Keep those JSON files under `${TMPDIR:-/tmp}/codex/<project>/`. `typesafe skills install` matches `npx skills add`: `--project` (default) or `--global` / `-g`. A TTY asks which. Global also writes a pointer into the user agent file (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, or `~/.grok/rules/typesafe-cli.md`) so investigations load `typesafe`. `typesafe auth status` reports `has_key` and nothing else.
|
|
87
|
+
|
|
88
|
+
## Commands
|
|
89
|
+
|
|
90
|
+
| | |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `ask` | State file + questions file |
|
|
93
|
+
| `noul` / `choice` / `score` | One question |
|
|
94
|
+
| `find` | Search a file |
|
|
95
|
+
| `rank` | Order a JSON shortlist |
|
|
96
|
+
| `extract` | Pick a span already in the text |
|
|
97
|
+
| `verify` | Check a claim against a source |
|
|
98
|
+
| `screen` | Jailbreak / injection / sensitive-data / harm |
|
|
99
|
+
| `suggest-skill` | At most one skill name |
|
|
100
|
+
| `decide` | Map answers to yes / no / uncertain (no HTTP) |
|
|
101
|
+
| `models` | Model aliases |
|
|
102
|
+
| `smoke` | Live docs quickstart |
|
|
103
|
+
| `auth status` | Whether a key is loaded |
|
|
104
|
+
| `agent schema` | JSON command tree |
|
|
105
|
+
| `skills install` | Official TypeSafe skill + typesafe-cli (`-g` for all projects) |
|
|
106
|
+
|
|
107
|
+
Exit `0` answered, `1` request failed or invalid answer, `2` bad flags or questions.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# typesafe
|
|
2
|
+
|
|
3
|
+
CLI for [TypeSafe](https://typesafe.ai) Jev. Run it from a shell, or let an agent run it.
|
|
4
|
+
|
|
5
|
+
Jev answers `noul`, `choice`, and `score` questions over JSON `state`. It does not chat, and it only sees what you send. Invalid answers fail closed. Unused speculative heads cannot act. Ranked `find` lines are not evidence when the document has no answer.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Python 3.10+. API key: [TypeSafe console](https://console.typesafe.ai/settings/keys).
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
curl -fsSL https://github.com/maddygoround/typesafeai-cli/releases/latest/download/install.sh | bash
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pipx install typesafeai-cli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`pip install typesafeai-cli` is the same package.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
export TYPESAFE_API_KEY=apikey_…
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
`typesafe ask` takes a state file and a questions file. Point each instruction at state with backticks.
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"message": "I was charged twice for order A-104. Please refund the duplicate.",
|
|
32
|
+
"policy": "Duplicate charges are eligible for an immediate refund."
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"refund_requested": {
|
|
39
|
+
"type": "noul",
|
|
40
|
+
"instructions": "Does `message` request a refund?"
|
|
41
|
+
},
|
|
42
|
+
"intent": {
|
|
43
|
+
"type": "choice",
|
|
44
|
+
"instructions": "What does `message` ask for?",
|
|
45
|
+
"criteria": {
|
|
46
|
+
"refund": "The customer wants money returned",
|
|
47
|
+
"information": "Explanation only",
|
|
48
|
+
"other": "Something else"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
[`examples/ticket`](examples/ticket), [`examples/code-change`](examples/code-change), and [`examples/fan-out`](examples/fan-out) are full copies.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
typesafe ask --state-file state.json --questions-file questions.json
|
|
58
|
+
typesafe noul "Does this request a refund?" --state "I was charged twice."
|
|
59
|
+
typesafe decide --answers-file last.json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Keep those JSON files under `${TMPDIR:-/tmp}/codex/<project>/`. `typesafe skills install` matches `npx skills add`: `--project` (default) or `--global` / `-g`. A TTY asks which. Global also writes a pointer into the user agent file (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, or `~/.grok/rules/typesafe-cli.md`) so investigations load `typesafe`. `typesafe auth status` reports `has_key` and nothing else.
|
|
63
|
+
|
|
64
|
+
## Commands
|
|
65
|
+
|
|
66
|
+
| | |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `ask` | State file + questions file |
|
|
69
|
+
| `noul` / `choice` / `score` | One question |
|
|
70
|
+
| `find` | Search a file |
|
|
71
|
+
| `rank` | Order a JSON shortlist |
|
|
72
|
+
| `extract` | Pick a span already in the text |
|
|
73
|
+
| `verify` | Check a claim against a source |
|
|
74
|
+
| `screen` | Jailbreak / injection / sensitive-data / harm |
|
|
75
|
+
| `suggest-skill` | At most one skill name |
|
|
76
|
+
| `decide` | Map answers to yes / no / uncertain (no HTTP) |
|
|
77
|
+
| `models` | Model aliases |
|
|
78
|
+
| `smoke` | Live docs quickstart |
|
|
79
|
+
| `auth status` | Whether a key is loaded |
|
|
80
|
+
| `agent schema` | JSON command tree |
|
|
81
|
+
| `skills install` | Official TypeSafe skill + typesafe-cli (`-g` for all projects) |
|
|
82
|
+
|
|
83
|
+
Exit `0` answered, `1` request failed or invalid answer, `2` bad flags or questions.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
| Directory | |
|
|
2
|
+
| --- | --- |
|
|
3
|
+
| [ticket/](ticket/) | Support message, refund / intent / frustration |
|
|
4
|
+
| [code-change/](code-change/) | A skill hunk, whether the change needs real code in state |
|
|
5
|
+
| [fan-out/](fan-out/) | One operation Choice plus speculative `*_target` heads |
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
WORKDIR="${TMPDIR:-/tmp}/codex/typesafeai-cli"
|
|
9
|
+
mkdir -p "$WORKDIR"
|
|
10
|
+
cp examples/ticket/*.json "$WORKDIR/"
|
|
11
|
+
typesafe ask --state-file "$WORKDIR/state.json" --questions-file "$WORKDIR/questions.json"
|
|
12
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"operation": {
|
|
3
|
+
"type": "choice",
|
|
4
|
+
"instructions": "Given `task` and `files[0].hunk`, what should the agent do next?",
|
|
5
|
+
"criteria": {
|
|
6
|
+
"EDIT": "Change the skill text so agents attach real code slices",
|
|
7
|
+
"ASK": "Ask the human before changing anything",
|
|
8
|
+
"SKIP": "Do nothing; the skill is already clear",
|
|
9
|
+
"abstain": "Not enough in state to decide"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"edit_target": {
|
|
13
|
+
"type": "choice",
|
|
14
|
+
"instructions": "If the next operation is EDIT, which observed hunk should be changed? This question chooses only a target; another question chooses the operation.",
|
|
15
|
+
"criteria": {
|
|
16
|
+
"files[0]": "The skill hunk already in `files[0].hunk`",
|
|
17
|
+
"none": "No offered hunk should be edited"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"ask_target": {
|
|
21
|
+
"type": "choice",
|
|
22
|
+
"instructions": "If the next operation is ASK, what should we ask the human? This question chooses only a target; another question chooses the operation.",
|
|
23
|
+
"criteria": {
|
|
24
|
+
"scope": "Ask whether the skill should stay questions-first",
|
|
25
|
+
"none": "Do not ask"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"task": "A coding agent is about to edit a skill. Decide the next operation, then which hunk to touch if the operation is EDIT.",
|
|
3
|
+
"files": [
|
|
4
|
+
{
|
|
5
|
+
"path": "src/typesafe_cli/data/typesafe-cli/SKILL.md",
|
|
6
|
+
"line_range": "50-74",
|
|
7
|
+
"hunk": "If a path is code (`files[0].hunk`), the value must be the slice (path + line range + body), not a filename and not \"see the PR.\""
|
|
8
|
+
}
|
|
9
|
+
]
|
|
10
|
+
}
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# pipx install typesafeai-cli
|
|
2
|
+
# Install the typesafe CLI from a GitHub release (default) or PyPI.
|
|
4
3
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# ./install.sh
|
|
9
|
-
# TYPESAFE_CLI_VERSION=v0.2.0 ./install.sh
|
|
4
|
+
# curl -fsSL https://github.com/maddygoround/typesafeai-cli/releases/latest/download/install.sh | bash
|
|
5
|
+
# TYPESAFE_CLI_VERSION=v0.3.1 ./install.sh
|
|
10
6
|
# TYPESAFE_CLI_FROM=pypi ./install.sh
|
|
11
7
|
#
|
|
12
8
|
# Env:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.3"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Idempotent TypeSafe pointer in the user's global agent files (not the repo)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
START = "<!-- typesafe-cli:start -->"
|
|
8
|
+
END = "<!-- typesafe-cli:end -->"
|
|
9
|
+
|
|
10
|
+
SNIPPET = """<!-- typesafe-cli:start -->
|
|
11
|
+
## TypeSafe CLI
|
|
12
|
+
|
|
13
|
+
When the user asks to investigate, triage, verify a claim, rank options, extract a span, screen a message, route a task, or make a typed yes/no or score judgment (Jev, noul, choice, TypeSafe), follow the **typesafe-cli** skill and run `typesafe`. Collect only the named state first. Do not curl TypeSafe.
|
|
14
|
+
<!-- typesafe-cli:end -->
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def global_agent_paths(*, home: Path, agent: str) -> list[Path]:
|
|
19
|
+
"""Home-level files each agent loads on every session.
|
|
20
|
+
|
|
21
|
+
Same split as `npx skills add -g`: user config, not the git tree.
|
|
22
|
+
"""
|
|
23
|
+
claude = home / ".claude" / "CLAUDE.md"
|
|
24
|
+
codex = home / ".codex" / "AGENTS.md"
|
|
25
|
+
grok_rule = home / ".grok" / "rules" / "typesafe-cli.md"
|
|
26
|
+
agents = home / ".agents" / "AGENTS.md"
|
|
27
|
+
cursor = home / ".cursor" / "AGENTS.md"
|
|
28
|
+
gemini = home / ".gemini" / "GEMINI.md"
|
|
29
|
+
by_agent = {
|
|
30
|
+
"claude-code": [claude],
|
|
31
|
+
"codex": [codex],
|
|
32
|
+
"grok": [grok_rule, claude],
|
|
33
|
+
"cursor": [cursor],
|
|
34
|
+
"gemini-code": [gemini],
|
|
35
|
+
"opencode": [agents],
|
|
36
|
+
"generic-agent": [agents],
|
|
37
|
+
}
|
|
38
|
+
if agent in by_agent:
|
|
39
|
+
return by_agent[agent]
|
|
40
|
+
existing = [p for p in (claude, codex, grok_rule, agents, cursor, gemini) if p.is_file()]
|
|
41
|
+
return existing or [grok_rule]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def upsert_global_agent_files(*, home: Path, agent: str) -> list[Path]:
|
|
45
|
+
written: list[Path] = []
|
|
46
|
+
for path in global_agent_paths(home=home, agent=agent):
|
|
47
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
_upsert_snippet(path)
|
|
49
|
+
written.append(path)
|
|
50
|
+
return written
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _upsert_snippet(path: Path) -> None:
|
|
54
|
+
body = path.read_text(encoding="utf-8") if path.is_file() else ""
|
|
55
|
+
block = SNIPPET.strip() + "\n"
|
|
56
|
+
start = body.find(START)
|
|
57
|
+
end = body.find(END)
|
|
58
|
+
if start != -1 and end != -1 and end > start:
|
|
59
|
+
end += len(END)
|
|
60
|
+
while end < len(body) and body[end] == "\n":
|
|
61
|
+
end += 1
|
|
62
|
+
updated = body[:start].rstrip() + "\n\n" + block + body[end:].lstrip("\n")
|
|
63
|
+
if not updated.endswith("\n"):
|
|
64
|
+
updated += "\n"
|
|
65
|
+
elif body.strip():
|
|
66
|
+
updated = body.rstrip() + "\n\n" + block
|
|
67
|
+
else:
|
|
68
|
+
updated = block
|
|
69
|
+
path.write_text(updated, encoding="utf-8")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Fail-closed checks on TypeSafe answers. Invalid JSON is not a decision."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AnswerError(ValueError):
|
|
10
|
+
"""The envelope is not a usable typed answer; do not act on it."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validate_answers(answers: dict[str, Any], *, questions: dict[str, dict[str, Any]] | None = None) -> None:
|
|
14
|
+
if not isinstance(answers, dict) or not answers:
|
|
15
|
+
raise AnswerError("Invalid TypeSafe response; no action executed.")
|
|
16
|
+
questions = questions or {}
|
|
17
|
+
for qid, answer in answers.items():
|
|
18
|
+
if not isinstance(qid, str) or not qid or not isinstance(answer, dict):
|
|
19
|
+
raise AnswerError("Invalid TypeSafe response; no action executed.")
|
|
20
|
+
qtype = answer.get("type") or (questions.get(qid) or {}).get("type")
|
|
21
|
+
if qtype == "choice":
|
|
22
|
+
ids = _choice_ids(qid, answer, questions)
|
|
23
|
+
_validate_choice(answer, ids)
|
|
24
|
+
elif qtype == "noul":
|
|
25
|
+
_validate_noul(answer)
|
|
26
|
+
elif qtype == "score":
|
|
27
|
+
_validate_score(answer)
|
|
28
|
+
else:
|
|
29
|
+
raise AnswerError("Invalid TypeSafe response; no action executed.")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _choice_ids(qid: str, answer: dict[str, Any], questions: dict[str, dict[str, Any]]) -> set[str]:
|
|
33
|
+
criteria = (questions.get(qid) or {}).get("criteria")
|
|
34
|
+
if isinstance(criteria, dict) and criteria:
|
|
35
|
+
return {str(key) for key in criteria}
|
|
36
|
+
probabilities = answer.get("probabilities") or {}
|
|
37
|
+
if isinstance(probabilities, dict) and probabilities:
|
|
38
|
+
return {str(key) for key in probabilities}
|
|
39
|
+
raise AnswerError("Invalid TypeSafe response; no action executed.")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _validate_choice(answer: dict[str, Any], ids: set[str]) -> None:
|
|
43
|
+
try:
|
|
44
|
+
probabilities = {str(k): v for k, v in dict(answer["probabilities"]).items()}
|
|
45
|
+
confidence = answer["confidence"]
|
|
46
|
+
numbers = [*probabilities.values(), confidence]
|
|
47
|
+
choice = str(answer["choice"])
|
|
48
|
+
valid = (
|
|
49
|
+
choice in ids
|
|
50
|
+
and set(probabilities) == ids
|
|
51
|
+
and all(isinstance(n, (int, float)) and math.isfinite(n) and 0 <= n <= 1 for n in numbers)
|
|
52
|
+
and abs(sum(probabilities.values()) - 1) < 0.02
|
|
53
|
+
and probabilities[choice] >= max(probabilities.values()) - 1e-6
|
|
54
|
+
)
|
|
55
|
+
except (KeyError, TypeError, ValueError):
|
|
56
|
+
valid = False
|
|
57
|
+
if not valid:
|
|
58
|
+
raise AnswerError("Invalid TypeSafe response; no action executed.")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _validate_noul(answer: dict[str, Any]) -> None:
|
|
62
|
+
try:
|
|
63
|
+
value = answer["noul"]
|
|
64
|
+
valid = isinstance(value, (int, float)) and math.isfinite(value) and 0 <= value <= 1
|
|
65
|
+
except (KeyError, TypeError):
|
|
66
|
+
valid = False
|
|
67
|
+
if not valid:
|
|
68
|
+
raise AnswerError("Invalid TypeSafe response; no action executed.")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _validate_score(answer: dict[str, Any]) -> None:
|
|
72
|
+
try:
|
|
73
|
+
score = answer["score"]
|
|
74
|
+
confidence = answer.get("confidence")
|
|
75
|
+
valid = isinstance(score, (int, float)) and math.isfinite(score)
|
|
76
|
+
if confidence is not None:
|
|
77
|
+
valid = (
|
|
78
|
+
valid
|
|
79
|
+
and isinstance(confidence, (int, float))
|
|
80
|
+
and math.isfinite(confidence)
|
|
81
|
+
and 0 <= confidence <= 1
|
|
82
|
+
)
|
|
83
|
+
except (KeyError, TypeError):
|
|
84
|
+
valid = False
|
|
85
|
+
if not valid:
|
|
86
|
+
raise AnswerError("Invalid TypeSafe response; no action executed.")
|
|
@@ -25,7 +25,7 @@ app = typer.Typer(
|
|
|
25
25
|
no_args_is_help=True,
|
|
26
26
|
add_completion=False,
|
|
27
27
|
pretty_exceptions_enable=False,
|
|
28
|
-
help="Agent CLI for TypeSafe System One (Jev). Typed judgments, not chat. Agents must not read TYPESAFE_* env vars or key files; use typesafe auth status.",
|
|
28
|
+
help="Agent CLI for TypeSafe System One (Jev). Typed judgments, not chat. Invalid answers fail closed. Agents must not read TYPESAFE_* env vars or key files; use typesafe auth status.",
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
|
|
@@ -4,6 +4,7 @@ from typing import Any
|
|
|
4
4
|
|
|
5
5
|
from typesafe_sdk import Choice, Noul, Score, TypeSafeClient
|
|
6
6
|
|
|
7
|
+
from typesafe_cli.answers import validate_answers
|
|
7
8
|
from typesafe_cli.config import Config
|
|
8
9
|
|
|
9
10
|
|
|
@@ -87,7 +88,9 @@ def system_one(
|
|
|
87
88
|
questions=to_sdk_questions(questions),
|
|
88
89
|
model=resolved_model,
|
|
89
90
|
)
|
|
90
|
-
|
|
91
|
+
data = answers_to_dict(response)
|
|
92
|
+
validate_answers(data["answers"], questions=questions)
|
|
93
|
+
return data
|
|
91
94
|
|
|
92
95
|
|
|
93
96
|
def list_models(*, config: Config) -> dict[str, Any]:
|
|
@@ -7,6 +7,7 @@ from typing import Any
|
|
|
7
7
|
import typer
|
|
8
8
|
from typesafe_sdk import TypeSafeError
|
|
9
9
|
|
|
10
|
+
from typesafe_cli.answers import AnswerError
|
|
10
11
|
from typesafe_cli.client import system_one
|
|
11
12
|
from typesafe_cli.config import load_config
|
|
12
13
|
from typesafe_cli.io import emit_success, fail
|
|
@@ -38,6 +39,8 @@ def ask(
|
|
|
38
39
|
|
|
39
40
|
try:
|
|
40
41
|
data = system_one(config=config, state=resolved_state, questions=questions, model=model)
|
|
42
|
+
except AnswerError as exc:
|
|
43
|
+
fail(code="invalid_answer", message=str(exc), exit_code=1)
|
|
41
44
|
except TypeSafeError as exc:
|
|
42
45
|
fail(code="request", message=str(exc), exit_code=1)
|
|
43
46
|
except Exception as exc: # noqa: BLE001 — surface SDK/network failures as exit 1
|
|
@@ -83,6 +86,8 @@ def run_evaluation(
|
|
|
83
86
|
fail(code="auth", message="missing TYPESAFE_API_KEY (or --key / --creds)", exit_code=1)
|
|
84
87
|
try:
|
|
85
88
|
data = system_one(config=config, state=state, questions=questions, model=model)
|
|
89
|
+
except AnswerError as exc:
|
|
90
|
+
fail(code="invalid_answer", message=str(exc), exit_code=1)
|
|
86
91
|
except TypeSafeError as exc:
|
|
87
92
|
fail(code="request", message=str(exc), exit_code=1)
|
|
88
93
|
except Exception as exc: # noqa: BLE001
|
|
@@ -7,7 +7,7 @@ import typer
|
|
|
7
7
|
|
|
8
8
|
from typesafe_cli.io import emit_success, fail
|
|
9
9
|
from typesafe_cli.questions import QuestionError
|
|
10
|
-
from typesafe_cli.recipes.decide import apply_decide, extract_answers, parse_noul_band
|
|
10
|
+
from typesafe_cli.recipes.decide import apply_decide, decide_action, extract_answers, parse_noul_band
|
|
11
11
|
from typesafe_cli.recipes.thresholds import CHOICE_MIN_TOP_P, NOUL_UNCERTAIN_HIGH, NOUL_UNCERTAIN_LOW
|
|
12
12
|
|
|
13
13
|
|
|
@@ -34,9 +34,14 @@ def decide(
|
|
|
34
34
|
decisions = apply_decide(answers, noul_low=low, noul_high=high, choice_min_p=choice_min_p)
|
|
35
35
|
except (OSError, json.JSONDecodeError, QuestionError) as exc:
|
|
36
36
|
fail(code="usage", message=str(exc), exit_code=2)
|
|
37
|
+
gate = decide_action(decisions)
|
|
37
38
|
emit_success(
|
|
38
39
|
data={
|
|
39
40
|
"decisions": decisions,
|
|
41
|
+
"active": gate["active"],
|
|
42
|
+
"ignored": gate["ignored"],
|
|
43
|
+
"action": gate["action"],
|
|
44
|
+
"needs_verify": gate["needs_verify"],
|
|
40
45
|
"policy": {"noul_band": [low, high], "choice_min_p": choice_min_p},
|
|
41
46
|
},
|
|
42
47
|
command="decide",
|
|
@@ -5,6 +5,7 @@ from typing import Any
|
|
|
5
5
|
|
|
6
6
|
from typesafe_sdk import TypeSafeError
|
|
7
7
|
|
|
8
|
+
from typesafe_cli.answers import AnswerError
|
|
8
9
|
from typesafe_cli.client import system_one
|
|
9
10
|
from typesafe_cli.config import load_config
|
|
10
11
|
from typesafe_cli.io import fail
|
|
@@ -31,6 +32,8 @@ def call_system_one(
|
|
|
31
32
|
config = ready_config(key=key, creds=creds, model=model)
|
|
32
33
|
try:
|
|
33
34
|
return system_one(config=config, state=state, questions=questions, model=model)
|
|
35
|
+
except AnswerError as exc:
|
|
36
|
+
fail(code="invalid_answer", message=str(exc), exit_code=1)
|
|
34
37
|
except TypeSafeError as exc:
|
|
35
38
|
fail(code="request", message=str(exc), exit_code=1)
|
|
36
39
|
except Exception as exc: # noqa: BLE001
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import sys
|
|
3
4
|
import urllib.error
|
|
4
5
|
import urllib.request
|
|
5
6
|
from importlib.resources import files
|
|
@@ -7,6 +8,7 @@ from pathlib import Path
|
|
|
7
8
|
|
|
8
9
|
import typer
|
|
9
10
|
|
|
11
|
+
from typesafe_cli.agent_files import upsert_global_agent_files
|
|
10
12
|
from typesafe_cli.detect import detect_agent_info, resolve_agent
|
|
11
13
|
from typesafe_cli.io import emit_success, fail
|
|
12
14
|
|
|
@@ -25,16 +27,19 @@ SKILL_DIR_BY_AGENT = {
|
|
|
25
27
|
"generic-agent": ".agents/skills",
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
def _user_skill_dir(agent: str, *, home: Path | None = None) -> Path:
|
|
31
|
+
root = home or Path.home()
|
|
32
|
+
mapping = {
|
|
33
|
+
"claude-code": root / ".claude" / "skills",
|
|
34
|
+
"cursor": root / ".cursor" / "skills",
|
|
35
|
+
"windsurf": root / ".windsurf" / "skills",
|
|
36
|
+
"gemini-code": root / ".gemini" / "skills",
|
|
37
|
+
"codex": root / ".codex" / "skills",
|
|
38
|
+
"opencode": root / ".agents" / "skills",
|
|
39
|
+
"grok": root / ".agents" / "skills",
|
|
40
|
+
"generic-agent": root / ".agents" / "skills",
|
|
41
|
+
}
|
|
42
|
+
return mapping.get(agent, root / ".agents" / "skills")
|
|
38
43
|
|
|
39
44
|
EXISTING_PROJECT_DIRS = (
|
|
40
45
|
".agents/skills",
|
|
@@ -48,7 +53,11 @@ EXISTING_PROJECT_DIRS = (
|
|
|
48
53
|
def install(
|
|
49
54
|
target: str = typer.Option("auto", "--target", help="claude, cursor, codex, grok, or auto"),
|
|
50
55
|
directory: Path | None = typer.Option(None, "--dir", help="Explicit skills directory"),
|
|
51
|
-
|
|
56
|
+
global_install: bool = typer.Option(
|
|
57
|
+
False, "--global", "-g", help="Install for this user, all projects (same as npx skills add -g)"
|
|
58
|
+
),
|
|
59
|
+
project: bool = typer.Option(False, "--project", help="Install into the current project only"),
|
|
60
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Do not ask global vs project"),
|
|
52
61
|
offline: bool = typer.Option(False, "--offline", help="Use the vendored official skill, skip GitHub"),
|
|
53
62
|
) -> None:
|
|
54
63
|
try:
|
|
@@ -59,7 +68,8 @@ def install(
|
|
|
59
68
|
fail(code="request", message=f"could not load skills: {exc}", exit_code=1)
|
|
60
69
|
|
|
61
70
|
agent = resolve_agent(target=target)
|
|
62
|
-
|
|
71
|
+
scope = _resolve_scope(global_install=global_install, project=project, yes=yes)
|
|
72
|
+
dest_root = directory if directory is not None else _skills_root(agent=agent, project=scope == "project")
|
|
63
73
|
written: list[str] = []
|
|
64
74
|
try:
|
|
65
75
|
pairs = [
|
|
@@ -71,15 +81,19 @@ def install(
|
|
|
71
81
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
72
82
|
path.write_text(body, encoding="utf-8")
|
|
73
83
|
written.append(str(path))
|
|
84
|
+
if scope == "global" and directory is None:
|
|
85
|
+
for path in upsert_global_agent_files(home=Path.home(), agent=agent):
|
|
86
|
+
written.append(str(path))
|
|
74
87
|
except OSError as exc:
|
|
75
88
|
fail(code="request", message=str(exc), exit_code=1)
|
|
76
89
|
|
|
77
90
|
emit_success(
|
|
78
91
|
data={
|
|
79
92
|
"agent": agent,
|
|
93
|
+
"scope": scope,
|
|
80
94
|
"written": written,
|
|
81
95
|
"source": "offline-vendored" if offline else "github-or-vendored",
|
|
82
|
-
"note": "Installed typesafe-ai (design) and typesafe-cli (collect state, then typesafe ask). Agents cannot access TYPESAFE_* env vars.",
|
|
96
|
+
"note": "Installed typesafe-ai (design) and typesafe-cli (collect state, then typesafe ask). Global installs also write a TypeSafe pointer into the user agent file (CLAUDE.md / AGENTS.md / ~/.grok/rules). Agents cannot access TYPESAFE_* env vars.",
|
|
83
97
|
},
|
|
84
98
|
command="skills install",
|
|
85
99
|
)
|
|
@@ -135,4 +149,18 @@ def _skills_root(*, agent: str, project: bool) -> Path:
|
|
|
135
149
|
return candidate
|
|
136
150
|
rel = SKILL_DIR_BY_AGENT.get(agent, ".agents/skills")
|
|
137
151
|
return cwd / rel
|
|
138
|
-
return
|
|
152
|
+
return _user_skill_dir(agent)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _resolve_scope(*, global_install: bool, project: bool, yes: bool) -> str:
|
|
156
|
+
if global_install and project:
|
|
157
|
+
fail(code="usage", message="use only one of --global or --project", exit_code=2)
|
|
158
|
+
if global_install:
|
|
159
|
+
return "global"
|
|
160
|
+
if project:
|
|
161
|
+
return "project"
|
|
162
|
+
if yes or not sys.stdin.isatty():
|
|
163
|
+
return "project"
|
|
164
|
+
if typer.confirm("Install globally for all projects on this machine?", default=False):
|
|
165
|
+
return "global"
|
|
166
|
+
return "project"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Using the typesafe CLI
|
|
2
2
|
|
|
3
|
-
Live evaluations: follow the **typesafe-cli** skill. Prefer `find`, `rank`, `extract`, `verify`, `screen`, `suggest-skill`, and `decide` when those flags fit; otherwise collect local state, write JSON under `${TMPDIR:-/tmp}/codex/<project>/`, then `typesafe ask`. Never drop those files in the git tree. This sidecar does not replace that skill or the official TypeSafe skill.
|
|
3
|
+
Live evaluations: follow the **typesafe-cli** skill. Prefer `find`, `rank`, `extract`, `verify`, `screen`, `suggest-skill`, and `decide` when those flags fit; otherwise collect local state, write JSON under `${TMPDIR:-/tmp}/codex/<project>/`, then `typesafe ask`. Invalid answers fail closed. Never drop those files in the git tree. This sidecar does not replace that skill or the official TypeSafe skill.
|
|
4
4
|
|
|
5
5
|
## Secrets
|
|
6
6
|
|