Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# One key is required. The key's prefix picks the host, whichever variable holds it.
# One key is required, in its host's variable. The variable decides the host, not the key's shape.
# TypeSafe direct: https://console.typesafe.ai/keys
TYPESAFE_API_KEY=
# OpenRouter: https://openrouter.ai/settings/keys
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

### Fixed

- A key's shape no longer decides where it is sent. TypeSafe issues `apikey_...` keys as well as
`ts_...`, and shapes change, so the variable a key sits in now decides its host; a key that
looks like another host's only earns a hint in doctor. `TYPESAFE_API_KEY` keeps following
`TYPESAFE_BASE_URL` and `JEV_CODE_PROVIDER` the way the TypeSafe SDK does, so the OpenRouter and
Vercel setup guides for the SDK work unchanged; host-specific variables never travel.
- `setup` asks which host a pasted key is for, with its best guess as the default, instead of
deciding from the key's shape. Both setup prompts now settle when input ends, and a paste
holding both answers is read as two.
- With `JEV_CODE_PROVIDER=ollama`, a stale `TYPESAFE_BASE_URL` on another known host is refused
like everywhere else instead of carrying Ollama's key and the payload there.

## [0.4.0] - 2026-10-03

Two new hosts, both opt-in. OpenAI's Decisions API, verified end to end against a stand-in that
Expand Down
34 changes: 19 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ npx -y @french-castle/jev-code@latest setup
```

That detects the harnesses on your machine and, for each one, copies the skill and registers the
tool. When no API key is in your shell, it asks for one right there (hidden input). Any key
works: TypeSafe (`ts_...`), OpenRouter (`sk-or-...`), or Vercel AI Gateway (`vck_...`); the prefix
picks the host, so there is nothing else to configure.
tool. When no API key is in your shell, it asks for one right there (hidden input) and which host
it belongs to: TypeSafe, OpenRouter, Vercel AI Gateway, or OpenAI. There is nothing else to
configure.

Add harness names to be explicit (`setup claude codex pi opencode`), `--project` to install into
the current repository instead of your user profile, or `--dry-run` to see the plan first.
Expand All @@ -64,7 +64,7 @@ without a prompt. This is also what Pi and the CLI read, so `setup` prints the l
shell profile after you paste a key:

```bash
export TYPESAFE_API_KEY=ts_... # TypeSafe direct: console.typesafe.ai/keys
export TYPESAFE_API_KEY=... # TypeSafe direct: console.typesafe.ai/keys
export OPENROUTER_API_KEY=sk-or-... # OpenRouter: already set if you use it elsewhere
export AI_GATEWAY_API_KEY=vck_... # Vercel AI Gateway
```
Expand Down Expand Up @@ -236,26 +236,30 @@ tools behave identically and only the account you pay through changes. The last
models that answer the same kinds of questions: OpenAI's Decisions API, and Ollama's local
decision models, which need no key at all.

| Host | Key variable | Key prefix | Default model | Get a key |
| --- | --- | --- | --- | --- |
| TypeSafe (direct) | `TYPESAFE_API_KEY` | `ts_` | `jev-latest` | [console.typesafe.ai/keys](https://console.typesafe.ai/keys) |
| OpenRouter | `OPENROUTER_API_KEY` | `sk-or-` | `jev-latest` | [openrouter.ai/settings/keys](https://openrouter.ai/settings/keys) |
| Vercel AI Gateway | `AI_GATEWAY_API_KEY` | `vck_` | `typesafe-ai/jev` | [AI Gateway API keys](https://vercel.com/docs/ai-gateway/authentication-and-byok/api-keys) |
| OpenAI Decisions API (preview, opt-in) | `OPENAI_API_KEY` + `JEV_CODE_PROVIDER=openai` | `sk-proj-` | `gpt-6-luna` | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) |
| Ollama (local, opt-in) | no key; `JEV_CODE_PROVIDER=ollama`, `OLLAMA_HOST` optional | none | `nimble` | [docs.ollama.com/capabilities/decision](https://docs.ollama.com/capabilities/decision) |
| Host | Key variable | Default model | Get a key |
| --- | --- | --- | --- |
| TypeSafe (direct) | `TYPESAFE_API_KEY` | `jev-latest` | [console.typesafe.ai/keys](https://console.typesafe.ai/keys) |
| OpenRouter | `OPENROUTER_API_KEY` | `jev-latest` | [openrouter.ai/settings/keys](https://openrouter.ai/settings/keys) |
| Vercel AI Gateway | `AI_GATEWAY_API_KEY` | `typesafe-ai/jev` | [AI Gateway API keys](https://vercel.com/docs/ai-gateway/authentication-and-byok/api-keys) |
| OpenAI Decisions API (preview, opt-in) | `OPENAI_API_KEY` + `JEV_CODE_PROVIDER=openai` | `gpt-6-luna` | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) |
| Ollama (local, opt-in) | no key; `JEV_CODE_PROVIDER=ollama`, `OLLAMA_HOST` optional | `nimble` | [docs.ollama.com/capabilities/decision](https://docs.ollama.com/capabilities/decision) |

How the host is chosen:

- The key's prefix decides, whichever variable holds it: `TYPESAFE_API_KEY=sk-or-...` goes to
OpenRouter.
- The variable decides. A key is sent to the host of the variable it sits in, whatever it looks
like; key shapes change (TypeSafe issues both `ts_...` and `apikey_...` keys), so jev-code never
guesses from them. `doctor` adds a hint when a key looks like it belongs elsewhere.
- `TYPESAFE_API_KEY` is the generic variable the TypeSafe SDK reads, so it follows
`TYPESAFE_BASE_URL` and `JEV_CODE_PROVIDER` the way the SDK does: pointing the base URL at
OpenRouter or Vercel AI Gateway sends it there. A host-specific variable never travels.
- When several keys are set, the first row in the table wins. `doctor` says which one is in use.
- `JEV_CODE_PROVIDER=openrouter` (or `typesafe`, `vercel`, `openai`, `ollama`) forces a host.
OpenAI is never picked up from an ambient `OPENAI_API_KEY`: that variable is set in many shells
for other reasons, so sending agent evidence there is a decision you make with
`JEV_CODE_PROVIDER=openai` (or by pasting an OpenAI key when `setup` asks, which stores both).
Ollama has no key to detect, so it is chosen the same way.
- A key is only ever sent to the host that issued it; a mismatch is refused before any request.
The exact rules are in [SECURITY.md](SECURITY.md).
- A host-specific key is only ever sent to its own host; a base URL that disagrees is refused
before any request. The exact rules are in [SECURITY.md](SECURITY.md).

| Variable | Default | Purpose |
| --- | --- | --- |
Expand Down
20 changes: 12 additions & 8 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,28 @@ One `POST .../v1/systemone` per tool call, to the host your key belongs to:
- `https://api.openai.com` (OpenAI Decisions API), only when `JEV_CODE_PROVIDER=openai`
- `http://localhost:11434`, or `OLLAMA_HOST` (Ollama), only when `JEV_CODE_PROVIDER=ollama`; no key
is sent unless `OLLAMA_API_KEY` is set
- or `TYPESAFE_BASE_URL` when set, under the proxy rule below.
- or `TYPESAFE_BASE_URL` when set, under the proxy rule below. A base URL on one of the known
hosts above while `JEV_CODE_PROVIDER` names another is refused, for Ollama too.

The payload is what the agent passed to a tool: items, questions, optional context. Through
OpenRouter or Vercel AI Gateway it transits that gateway on its way to TypeSafe. jev-code never
reads files, git state, or session history on its own.

### Credentials

- The key is read from `TYPESAFE_API_KEY`, `OPENROUTER_API_KEY`, or `AI_GATEWAY_API_KEY`. Its
prefix (`ts_`, `sk-or-`, `vck_`) says which host issued it; a key with an unknown prefix
belongs to the host of the variable it sits in.
- The key is read from `TYPESAFE_API_KEY`, `OPENROUTER_API_KEY`, or `AI_GATEWAY_API_KEY`. The
variable decides the host; a key's shape never does, because shapes change (TypeSafe issues
`ts_` and `apikey_` keys). A key that looks like another host's earns a hint in doctor, nothing
more.
- `OPENAI_API_KEY` is never adopted on its own, even when it is the only key set, because many
unrelated tools set it. OpenAI's Decisions API is used only with `JEV_CODE_PROVIDER=openai`;
until then doctor and the missing-key error say the key is there and how to opt in.
- A key is only ever sent to the host that issued it. A base URL on a different known host is
refused before any request. A base URL on an unrecognised host (a proxy) is honoured for
TypeSafe keys, and for other keys only together with `JEV_CODE_PROVIDER`, so a key another
tool exported never follows a stray override.
- `TYPESAFE_API_KEY` is the generic variable the TypeSafe SDK reads, so it follows
`TYPESAFE_BASE_URL` and `JEV_CODE_PROVIDER` as the SDK does, including to a proxy. A
host-specific variable (`OPENROUTER_API_KEY`, `AI_GATEWAY_API_KEY`, `OPENAI_API_KEY`) is sent
only to its own host: a base URL on another known host is refused before any request, and a
proxy needs `JEV_CODE_PROVIDER` to state the intent, so a key another tool exported never
follows a stray override.
- Keys are never echoed or logged. `doctor`, `setup`, and the MCP server's one stderr line show
a masked hint (`sk-o…1234`); printed commands show `<your key>`; a harness CLI's output is
scrubbed of the key before it is shown.
Expand Down
12 changes: 6 additions & 6 deletions docs/harnesses/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The skill is then invoked as `/jev-code:jev`, and the tools appear as
Register the server yourself:

```bash
claude mcp add --scope user jev -e TYPESAFE_API_KEY=ts_... -- npx -y @french-castle/jev-code@0.4.0 mcp
claude mcp add --scope user jev -e TYPESAFE_API_KEY=<your key> -- npx -y @french-castle/jev-code@0.4.0 mcp
```

or add it to a project's `.mcp.json`:
Expand All @@ -47,16 +47,16 @@ or add it to a project's `.mcp.json`:
"jev": {
"command": "npx",
"args": ["-y", "@french-castle/jev-code@0.4.0", "mcp"],
"env": { "TYPESAFE_API_KEY": "ts_..." }
"env": { "TYPESAFE_API_KEY": "<your key>" }
}
}
}
```

Any provider key works in place of `TYPESAFE_API_KEY`: `OPENROUTER_API_KEY=sk-or-...` routes
through OpenRouter and `AI_GATEWAY_API_KEY=vck_...` through Vercel AI Gateway. OpenAI's Decisions
API (preview) needs both `OPENAI_API_KEY` and `JEV_CODE_PROVIDER=openai` in the environment; a
local Ollama needs only `JEV_CODE_PROVIDER=ollama`.
Any provider key works in place of `TYPESAFE_API_KEY`: `OPENROUTER_API_KEY` routes through
OpenRouter and `AI_GATEWAY_API_KEY` through Vercel AI Gateway (the variable decides the host).
OpenAI's Decisions API (preview) needs both `OPENAI_API_KEY` and `JEV_CODE_PROVIDER=openai` in the
environment; a local Ollama needs only `JEV_CODE_PROVIDER=ollama`.

Install the skill with `npx skills add FrancoisChastel/jev-code --skill jev -a claude-code`, or
copy `skills/jev/` into `~/.claude/skills/`.
Expand Down
10 changes: 5 additions & 5 deletions docs/harnesses/codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ command = "npx"
args = ["-y", "@french-castle/jev-code@0.4.0", "mcp"]

[mcp_servers.jev.env]
TYPESAFE_API_KEY = "ts_..."
TYPESAFE_API_KEY = "<your key>"
```

Any provider key works in place of `TYPESAFE_API_KEY`: `OPENROUTER_API_KEY=sk-or-...` routes
through OpenRouter and `AI_GATEWAY_API_KEY=vck_...` through Vercel AI Gateway. OpenAI's Decisions
API (preview) needs both `OPENAI_API_KEY` and `JEV_CODE_PROVIDER=openai` in the environment; a
local Ollama needs only `JEV_CODE_PROVIDER=ollama`.
Any provider key works in place of `TYPESAFE_API_KEY`: `OPENROUTER_API_KEY` routes through
OpenRouter and `AI_GATEWAY_API_KEY` through Vercel AI Gateway (the variable decides the host).
OpenAI's Decisions API (preview) needs both `OPENAI_API_KEY` and `JEV_CODE_PROVIDER=openai` in the
environment; a local Ollama needs only `JEV_CODE_PROVIDER=ollama`.

Install the skill with `npx skills add FrancoisChastel/jev-code --skill jev -a codex`, or copy
`skills/jev/` into `~/.agents/skills/`.
Expand Down
10 changes: 5 additions & 5 deletions docs/harnesses/opencode.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ Add to `opencode.json`:
"type": "local",
"command": ["npx", "-y", "@french-castle/jev-code@0.4.0", "mcp"],
"enabled": true,
"environment": { "TYPESAFE_API_KEY": "ts_..." }
"environment": { "TYPESAFE_API_KEY": "<your key>" }
}
}
}
```

Any provider key works in place of `TYPESAFE_API_KEY`: `OPENROUTER_API_KEY=sk-or-...` routes
through OpenRouter and `AI_GATEWAY_API_KEY=vck_...` through Vercel AI Gateway. OpenAI's Decisions
API (preview) needs both `OPENAI_API_KEY` and `JEV_CODE_PROVIDER=openai` in the environment; a
local Ollama needs only `JEV_CODE_PROVIDER=ollama`.
Any provider key works in place of `TYPESAFE_API_KEY`: `OPENROUTER_API_KEY` routes through
OpenRouter and `AI_GATEWAY_API_KEY` through Vercel AI Gateway (the variable decides the host).
OpenAI's Decisions API (preview) needs both `OPENAI_API_KEY` and `JEV_CODE_PROVIDER=openai` in the
environment; a local Ollama needs only `JEV_CODE_PROVIDER=ollama`.

Install the skill with `npx skills add FrancoisChastel/jev-code --skill jev -a opencode`, or copy
`skills/jev/` into `~/.config/opencode/skills/`.
Expand Down
2 changes: 1 addition & 1 deletion skills/jev/references/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

When the `jev_*` tools are not registered in the current harness, the same operations are
available from bash. The CLI reads the API key from the environment (`TYPESAFE_API_KEY`,
`OPENROUTER_API_KEY`, or `AI_GATEWAY_API_KEY`; the key's prefix picks the host).
`OPENROUTER_API_KEY`, or `AI_GATEWAY_API_KEY`; the variable decides the host).

```bash
jev-code doctor # is the key set, which harnesses are wired
Expand Down
8 changes: 4 additions & 4 deletions src/cli/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ function providerLines(): string {
return PROVIDERS.map((provider) => {
const optIn = provider.explicitOnly ? ` (opt in: ${ENV.provider}=${provider.name})` : "";
const variable = provider.keyless ? `${provider.hostEnv ?? ""} (optional)` : provider.keyEnv;
const prefix = provider.keyPrefix ? `${provider.keyPrefix}...` : "no key";
return ` ${variable.padEnd(23)} ${prefix.padEnd(12)} ${provider.label.padEnd(20)} ${provider.keysUrl}${optIn}`;
return ` ${variable.padEnd(23)} ${provider.label.padEnd(20)} ${provider.keysUrl}${optIn}`;
}).join("\n");
}

Expand Down Expand Up @@ -49,10 +48,11 @@ Setup options
--pi-source <spec> Package spec for \`pi install\` (default: npm:${PACKAGE_NAME}).

Environment
One key is required. Its prefix picks the host, whichever variable holds it:
One key is required, in its host's variable; the variable decides, not the key's shape.
TYPESAFE_API_KEY also follows TYPESAFE_BASE_URL and JEV_CODE_PROVIDER, as the TypeSafe SDK does.
${providerLines()}
JEV_CODE_PROVIDER typesafe | openrouter | vercel | openai | ollama. Forces the host; required for openai and ollama.
TYPESAFE_BASE_URL Override the base URL (a proxy). TypeSafe keys only, unless JEV_CODE_PROVIDER is set.
TYPESAFE_BASE_URL Override the base URL (a proxy). Followed by TYPESAFE_API_KEY; other keys need JEV_CODE_PROVIDER.
TYPESAFE_DEFAULT_MODEL Override the host's default model (jev-latest; typesafe-ai/jev on Vercel).
JEV_CODE_TIMEOUT_MS Default 30000
JEV_CODE_MAX_RETRIES Default 2
Expand Down
Loading
Loading