@spatz/cli 0.0.0 → 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +227 -1
- package/bin/spatz.js +40 -0
- package/package.json +25 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lorenz Hilpert
|
|
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.
|
package/README.md
CHANGED
|
@@ -1 +1,227 @@
|
|
|
1
|
-
|
|
1
|
+
# spatz
|
|
2
|
+
|
|
3
|
+
spatz recommends a model and effort pair for a coding-agent task, and learns which pairs succeed.
|
|
4
|
+
|
|
5
|
+
The motto is: do not use a cannon to shoot sparrows. Do not use the strongest model when a cheaper model is good enough.
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
spatz is a proof of concept. Expect changes to commands, output and the database schema.
|
|
10
|
+
|
|
11
|
+
What works:
|
|
12
|
+
|
|
13
|
+
- Recommendations from the candidates that you give with `--models`.
|
|
14
|
+
- Task classification with Jev, or with local keyword rules when Jev is off.
|
|
15
|
+
- Learning from Claude Code and Codex CLI hooks, and from `spatz report`.
|
|
16
|
+
- Statistics per task type with `spatz stats`.
|
|
17
|
+
|
|
18
|
+
What does not work yet:
|
|
19
|
+
|
|
20
|
+
- Signal collection works only with Claude Code and Codex CLI hooks. Other coding agents can only use `spatz report`.
|
|
21
|
+
- There is no MCP server.
|
|
22
|
+
|
|
23
|
+
## How it works
|
|
24
|
+
|
|
25
|
+
You give spatz a task text and a list of candidate pairs. A local filter first checks the task text for secrets. If the filter finds no secret, Jev classifies the task. The classification gives a task type, a difficulty and a criticality. Jev is a TypeSafe AI model that returns typed answers with calibrated probabilities. spatz then reads the learned success estimates for each candidate in that task class and ranks the candidates. spatz never runs the task and never switches the model. You or your agent pick the model. Claude Code hooks and `spatz report` send the outcome back, and spatz updates its estimates.
|
|
26
|
+
|
|
27
|
+
```mermaid
|
|
28
|
+
flowchart LR
|
|
29
|
+
T[Task text] --> F[Privacy filter]
|
|
30
|
+
F -->|no secret, Jev on| J[Jev classification]
|
|
31
|
+
F -->|secret found, Jev off or no key| K[Keyword rules]
|
|
32
|
+
J -->|Jev error| K
|
|
33
|
+
J --> E[Learned estimates]
|
|
34
|
+
K --> E
|
|
35
|
+
E --> R[Ranking of --models candidates]
|
|
36
|
+
R --> U[You or your agent pick a pair]
|
|
37
|
+
U --> H[Claude Code or Codex hooks]
|
|
38
|
+
U --> S[spatz report]
|
|
39
|
+
H --> O[(Outcomes in ~/.spatz/spatz.db)]
|
|
40
|
+
S --> O
|
|
41
|
+
O --> E
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
[docs/how-it-works.md](docs/how-it-works.md) and [docs/recommendation.md](docs/recommendation.md) explain the details.
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
- Node.js 18 or newer for npm installation. The platform package includes the Bun runtime.
|
|
49
|
+
- Bun 1.4 for development or installation from source. Release archives include the runtime.
|
|
50
|
+
- Optional: a TypeSafe AI API key for Jev classification. Jev is in early access.
|
|
51
|
+
|
|
52
|
+
spatz works without a key. Without a key, spatz uses the keyword rules. These rules set only the criticality. The task type is always `other`.
|
|
53
|
+
|
|
54
|
+
## Releases
|
|
55
|
+
|
|
56
|
+
Download an archive and its matching `.sha256` file from [GitHub Releases](https://github.com/lorenzh/spatz/releases).
|
|
57
|
+
Choose `linux` or `darwin` (macOS), then `x64` (Intel/AMD) or `arm64` (including Apple Silicon). Windows x64 archives are experimental.
|
|
58
|
+
Linux builds need glibc. They do not support Alpine Linux.
|
|
59
|
+
|
|
60
|
+
Check the checksum before you extract the archive. This Linux x64 example uses version `0.1.0`. Use your downloaded version:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
version=0.1.0
|
|
64
|
+
archive="spatz-cli-$version-linux-x64.tar.gz"
|
|
65
|
+
sha256sum --check "$archive.sha256"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
On macOS, use `shasum -a 256 --check "$archive.sha256"` and a `darwin` archive.
|
|
69
|
+
The release also contains `SHA256SUMS` with checksums for all archives.
|
|
70
|
+
On Windows, download the `win32-x64.zip` archive and its `.sha256` file. Windows support is experimental; hooks are untested on Windows.
|
|
71
|
+
|
|
72
|
+
Keep the archive contents together. Put a symlink to the executable on your `PATH`:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
mkdir -p ~/.local/lib/spatz ~/.local/bin
|
|
76
|
+
tar -xzf "$archive" -C ~/.local/lib/spatz
|
|
77
|
+
ln -sfn "$HOME/.local/lib/spatz/${archive%.tar.gz}/spatz" ~/.local/bin/spatz
|
|
78
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
79
|
+
spatz --version
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
If needed, add the `PATH` line to your shell profile.
|
|
83
|
+
Each archive includes `spatz`, `LICENSE`, `README.md`, the DuckDB binding, and the DuckDB shared library.
|
|
84
|
+
`spatz stats` needs the binding and shared library beside the executable.
|
|
85
|
+
The Windows archive contains `spatz.exe` and the DuckDB Windows binding and DLL beside it.
|
|
86
|
+
On first use, it downloads DuckDB's SQLite extension into `~/.spatz/duckdb-extensions`.
|
|
87
|
+
Later runs can use that extension offline.
|
|
88
|
+
|
|
89
|
+
The fixed [`nightly` release](https://github.com/lorenzh/spatz/releases/tag/nightly) is unstable.
|
|
90
|
+
When code or release inputs change on `main`, it updates at the next daily run at 03:00 UTC.
|
|
91
|
+
You can also start a manual workflow run with `force` enabled.
|
|
92
|
+
See [RELEASING.md](RELEASING.md) for the release procedure.
|
|
93
|
+
|
|
94
|
+
## Install and quickstart
|
|
95
|
+
|
|
96
|
+
Install the stable version with npm:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm i -g @spatz/cli
|
|
100
|
+
spatz --version
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
For the unstable nightly version:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm i -g @spatz/cli@nightly
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Keep optional dependencies enabled. npm selects the package for your OS and architecture.
|
|
110
|
+
Linux needs glibc and does not support Alpine Linux.
|
|
111
|
+
Windows x64 is experimental. Some releases omit it.
|
|
112
|
+
You can also [download a GitHub release archive](#releases). Archives need neither Node.js nor Bun installed.
|
|
113
|
+
|
|
114
|
+
To install from source instead:
|
|
115
|
+
|
|
116
|
+
1. Clone the repository and install the dependencies.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
git clone https://github.com/lorenzh/spatz.git
|
|
120
|
+
cd spatz
|
|
121
|
+
bun install
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
2. Put `spatz` on your `PATH` with a small wrapper. Hooks run in a non-interactive shell, so a shell alias does not work.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
mkdir -p ~/.local/bin
|
|
128
|
+
printf '#!/bin/sh\nexec bun "%s/packages/cli/src/cli.ts" "$@"\n' "$PWD" > ~/.local/bin/spatz
|
|
129
|
+
chmod +x ~/.local/bin/spatz
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
3. Optional: set your TypeSafe AI key to turn on Jev.
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
export TYPESAFE_AI_API_KEY=<your-key>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
4. Ask for a recommendation. `--models` lists the pairs that you can use, in the form `<id>[:<effort>+<effort>...]`.
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
spatz "Fix the off-by-one error in src/list.ts" \
|
|
142
|
+
--models claude-opus-5-5:high+medium,claude-sonnet-5-5:medium+low --dry-run
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Example output without a key (`SPATZ_NO_JEV=1`, empty database):
|
|
146
|
+
|
|
147
|
+
```text
|
|
148
|
+
suggestion_id: fd8b7c1f-1f93-44f6-ac7b-b77ee287d1bb
|
|
149
|
+
1. anthropic/claude-opus-5.5:high estimate=0.50 n=0
|
|
150
|
+
reason: Without Jev and learned data the most expensive pair anthropic/claude-opus-5.5 (high) is recommended.
|
|
151
|
+
task_type: other difficulty: medium criticality: none
|
|
152
|
+
explored: false control: false fallback_used: true (dry-run)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`--dry-run` marks the recommendation as a test. A test never counts for learning or statistics. Remove the flag for real use.
|
|
156
|
+
|
|
157
|
+
spatz gets model prices from the OpenRouter model list and keeps them for 24 hours in `~/.spatz/openrouter-models.json`. To fill this cache, spatz needs network access. Without network access, you still get a recommendation. spatz uses the cached prices, even from a stale cache. If no cache exists, all prices are unknown and the models rank as most expensive.
|
|
158
|
+
|
|
159
|
+
5. After the task, report the pair that you used and the result.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
spatz report <suggestion_id> --model claude-sonnet-5-5 --effort medium --result pass
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
A report overrides all hook signals for that recommendation.
|
|
166
|
+
|
|
167
|
+
## Use with Claude Code
|
|
168
|
+
|
|
169
|
+
You can connect spatz to Claude Code in three ways. They can run alone or together.
|
|
170
|
+
|
|
171
|
+
- **Hooks only.** The hooks in your Claude Code settings watch Bash calls and record test and build results, models and tokens. See [docs/hooks.md](docs/hooks.md).
|
|
172
|
+
- **Mod only.** The `spatz` mod in `packages/claude-mod` asks spatz for each decision. In `apply` mode it sets model and effort for subagents or for the main session. It records usage itself. See [docs/claude-mod.md](docs/claude-mod.md).
|
|
173
|
+
- **Both.** The mod routes and the hooks record. With `record: auto` the mod stops recording when the `spatz-hooks` plugin is enabled, so nothing is counted twice.
|
|
174
|
+
|
|
175
|
+
The mod has five routing scopes: `step`, `turn`, `subagent` (default), `session` and `escalate`. `spatz stats --by scope` compares them.
|
|
176
|
+
|
|
177
|
+
## Commands
|
|
178
|
+
|
|
179
|
+
The suggestion, report, usage, link and stats commands accept `--json` for machine-readable output.
|
|
180
|
+
|
|
181
|
+
| Command | Purpose |
|
|
182
|
+
| --- | --- |
|
|
183
|
+
| `spatz --version` | Print the CLI version. |
|
|
184
|
+
| `spatz "<task>" --models <list> [--scope <scope>] [--session <id>] [--turn <id>] [--agent-id <id>] [--source <agent>] [--dry-run]` | Rank candidate pairs and optionally store routing attribution. |
|
|
185
|
+
| `spatz report <suggestion_id> --model <m> --effort <e> --result pass\|partial\|fail [--rounds <n>] [--note <t>] [--turn <id> --source claude-code-mod]` | Record the pair that you used and the result. |
|
|
186
|
+
| `spatz usage <suggestion_id> ... --turn <id> --source claude-code-mod` | Record direct model usage and token counts. |
|
|
187
|
+
| `spatz link <suggestion_id> --agent-id <id> --session <id>` | Link a subagent suggestion after its id is known. |
|
|
188
|
+
| `spatz hook <event> [--agent codex]` | Read a Claude Code or Codex hook event from stdin. Prints nothing and always exits 0. |
|
|
189
|
+
| `spatz stats [--type <t>] [--by scope]` | Show results per task type or routing scope. |
|
|
190
|
+
|
|
191
|
+
Exit codes: 0 for success, 1 for a runtime error (for example an unknown `suggestion_id`), 2 for a usage error. [docs/cli.md](docs/cli.md) is the full reference.
|
|
192
|
+
|
|
193
|
+
## Configuration and privacy
|
|
194
|
+
|
|
195
|
+
spatz reads four environment variables. `HOME` sets the location of `~/.spatz`. `TYPESAFE_AI_API_KEY` turns on Jev. `SPATZ_NO_JEV=1` turns off Jev. If you set `OPENROUTER_API_KEY`, spatz sends it with the OpenRouter model-list request. A project can also turn off Jev with `{"jev": false}` in `.spatz.json` in the working directory. All data stays in `~/.spatz`. [docs/configuration.md](docs/configuration.md) lists all settings and files.
|
|
196
|
+
|
|
197
|
+
When Jev is on, spatz sends the task text and the candidate list to TypeSafe AI. The candidate list holds the `model:effort` labels and a short description per model. If Jev is off, the key is missing or the secret filter finds a secret, spatz sends nothing to TypeSafe AI. spatz does not store the task text. The OpenRouter model-list request holds no task data. Turning off Jev does not stop this request. The hooks store only derived signals, model names, efforts and token counts. [docs/privacy.md](docs/privacy.md) describes each data flow.
|
|
198
|
+
|
|
199
|
+
## Documentation
|
|
200
|
+
|
|
201
|
+
- [docs/how-it-works.md](docs/how-it-works.md): the architecture and the flow from task to outcome.
|
|
202
|
+
- [docs/recommendation.md](docs/recommendation.md): how spatz ranks candidates, explores and uses control groups.
|
|
203
|
+
- [docs/privacy.md](docs/privacy.md): what data leaves your machine and what spatz stores.
|
|
204
|
+
- [docs/cli.md](docs/cli.md): all commands, flags, output fields and exit codes.
|
|
205
|
+
- [docs/hooks.md](docs/hooks.md): the Claude Code hooks setup and the signals they record.
|
|
206
|
+
- [docs/claude-mod.md](docs/claude-mod.md): the Claude Code mod, its modes, routing scopes and `/spatz` commands.
|
|
207
|
+
- [docs/configuration.md](docs/configuration.md): environment variables, project file and files in `~/.spatz`.
|
|
208
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md): development setup, tests and the change process.
|
|
209
|
+
- [SECURITY.md](SECURITY.md): how to report a vulnerability.
|
|
210
|
+
|
|
211
|
+
## Development
|
|
212
|
+
|
|
213
|
+
The repository is a Bun workspace with two packages. `packages/core` (`@spatz/core`) holds all logic. `packages/cli` (`@spatz/cli`) is a thin CLI on top of it.
|
|
214
|
+
|
|
215
|
+
Run these gates before you push:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
bun test # all tests, including end-to-end tests of the CLI
|
|
219
|
+
bun run typecheck # tsc --noEmit
|
|
220
|
+
bun run lint # biome check
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Work test first. Write a failing test and make it pass with the minimum code. Then clean up. [CONTRIBUTING.md](CONTRIBUTING.md) has the details.
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
MIT. See [LICENSE](LICENSE).
|
package/bin/spatz.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { spawnSync } = require("node:child_process");
|
|
3
|
+
const { constants } = require("node:os");
|
|
4
|
+
const { dirname, join } = require("node:path");
|
|
5
|
+
|
|
6
|
+
const packages = {
|
|
7
|
+
"linux-x64": "@spatz/cli-linux-x64",
|
|
8
|
+
"linux-arm64": "@spatz/cli-linux-arm64",
|
|
9
|
+
"darwin-arm64": "@spatz/cli-darwin-arm64",
|
|
10
|
+
"darwin-x64": "@spatz/cli-darwin-x64",
|
|
11
|
+
"win32-x64": "@spatz/cli-win32-x64",
|
|
12
|
+
};
|
|
13
|
+
const target = `${process.platform}-${process.arch}`;
|
|
14
|
+
const pkg = packages[target];
|
|
15
|
+
if (!pkg) {
|
|
16
|
+
console.error(`spatz: unsupported platform ${target}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
let dir;
|
|
20
|
+
try {
|
|
21
|
+
dir = dirname(require.resolve(`${pkg}/package.json`));
|
|
22
|
+
} catch {
|
|
23
|
+
console.error(
|
|
24
|
+
`spatz: missing ${pkg}. Reinstall @spatz/cli with optional dependencies enabled (omit --no-optional / --omit=optional). The experimental Windows package may be unavailable for this release.`,
|
|
25
|
+
);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const child = spawnSync(
|
|
29
|
+
join(dir, process.platform === "win32" ? "spatz.exe" : "spatz"),
|
|
30
|
+
process.argv.slice(2),
|
|
31
|
+
{ stdio: "inherit" },
|
|
32
|
+
);
|
|
33
|
+
if (child.error) {
|
|
34
|
+
console.error(`spatz: ${child.error.message}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
if (child.signal) {
|
|
38
|
+
process.kill(process.pid, child.signal);
|
|
39
|
+
process.exit(128 + constants.signals[child.signal]);
|
|
40
|
+
} else process.exit(child.status ?? 1);
|
package/package.json
CHANGED
|
@@ -1 +1,25 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@spatz/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Model and effort recommendations for coding agents",
|
|
5
|
+
"bin": {
|
|
6
|
+
"spatz": "bin/spatz.js"
|
|
7
|
+
},
|
|
8
|
+
"optionalDependencies": {
|
|
9
|
+
"@spatz/cli-linux-x64": "0.1.0",
|
|
10
|
+
"@spatz/cli-linux-arm64": "0.1.0",
|
|
11
|
+
"@spatz/cli-darwin-arm64": "0.1.0",
|
|
12
|
+
"@spatz/cli-darwin-x64": "0.1.0"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/lorenzh/spatz.git"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"bin"
|
|
24
|
+
]
|
|
25
|
+
}
|