@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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -1
  3. package/bin/spatz.js +40 -0
  4. 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
- Placeholder. See https://github.com/lorenzh/spatz
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
- { "name": "@spatz/cli", "version": "0.0.0", "description": "Placeholder for spatz. Real releases are published from GitHub Actions.", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/lorenzh/spatz.git" } }
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
+ }