---
title: CLI
description: The published tool that creates and updates projects.
type: reference
related:
- /en/docs/setup/quickstart
- /en/docs/updates
---
# CLI
`packages/cli` is published to npm as
[`@kreotic/kreogen`](https://www.npmjs.com/package/@kreotic/kreogen):
```sh
npx @kreotic/kreogen@latest init # create a project
npx @kreotic/kreogen@latest update # apply template changes
npx @kreotic/kreogen@latest audit # measure a repository against the standard
npx @kreotic/kreogen@latest sprint # emit a handoff pack from that audit
npx @kreotic/kreogen@latest plan # interview a brief into a recipe init can build
npx @kreotic/kreogen@latest doctor # check what the optional parts can do
npx @kreotic/kreogen@latest standard # print the conventions the audit enforces
npx @kreotic/kreogen@latest mcp # serve the audit as MCP tools over stdio
npx @kreotic/kreogen@latest review # open-ended agentic review of a repository
```
`init` is covered by the [quickstart](/en/docs/setup/quickstart); `update` has
[its own page](/en/docs/updates). This page is about the package itself — read
it if you are changing the template rather than using it.
The CLI is template-only. `init` strips `packages/cli` from generated projects,
so it does not appear in a project built from kreogen.
## Two engines
Every command runs on one of two engines, decided by a single call site
(`engine/select.ts`'s `selectEngine`) rather than each command detecting a
credential its own way:
* **Deterministic** — `audit`, `standard` and `mcp` never touch a model.
Same input, same output, every time; this is what the CI gate stands on.
* **Agentic** — `plan` and `review` need a credential and refuse without one,
since their entire output is a model's opinion, not an enrichment of
something else. `sprint` and `init` are blended: a complete, correct run
with no model at all, plus optional prose or capabilities layered on top
when a credential happens to be available.
```sh
kreogen doctor # node, git, package managers, disk space, both engines, ...
kreogen doctor --probe # also spend one request proving the credential actually works
```
A provider is detected from whichever of these credentials is set, in this
order: `OPENROUTER_API_KEY`, `AI_GATEWAY_API_KEY`, `ANTHROPIC_API_KEY`,
`OPENAI_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY`, then `OLLAMA_HOST` for a
local runtime. `--model provider:model` and `KREOGEN_AI_MODEL` /
`KREOGEN_AI_PROVIDER` both override detection, in that order.
`--model` and `--max-cost` are defined once, in `cli/program.ts`'s
`withModelOptions`, and applied to every command that can reach a provider --
`init`, `sprint`, `plan` and `review` -- so their help text cannot drift apart
per command. The order that change has to happen in is worth knowing before a
fifth command joins them: the command's own options type has to carry `model`
and pass it to `selectEngine()` first, because registering the flag alone
parses it and throws it away, which is a worse surface than not having it. On
`init` both flags bound the marketing-copy pass and nothing else; without
`--agentic-edit` it reaches no provider at all.
`--max-cost ` is honest rather than universal: only OpenRouter and the
Vercel AI Gateway bill per request and report what they billed, so only
against those two is a ceiling actually enforced. Against any other provider
the meter counts tokens and says so — a ceiling that silently did nothing
would be worse than no ceiling at all, since someone would set it and believe
it. `sprint --apply`, `plan` and `init --agentic-edit` all warn on stderr when
`--max-cost` was set against a provider that cannot honor it.
`review --max-tokens ` is the ceiling that is universal. Token counts come
back on every step from every provider, so it binds where `--max-cost` cannot —
and it binds between steps as well as between units, not only at a unit
boundary. That distinction is the whole reason it exists: one 20-step review
consumed 622,824 input tokens under a `--max-cost` that could never be reached,
and a decomposed run in this sprint consumed 1,990,168.
`KREOGEN_AI_REPLAY=1` is how every AI-touching test in this repository runs:
it disables outbound requests entirely and serves only what is already
cached, degrading (never failing) on a cache miss. It exists so the test
suite needs neither a live credential nor a mock of the whole AI SDK, and it
is safe to set the same way in any CI job that should never make a live model
call by accident.
## stdout is the machine channel
Every command's `--json` document is the only thing on its stdout. Progress
lines, warnings, the intro and outro, the live panel and the spend summary all
go to stderr. A terminal interleaves the two, so an interactive run looks
exactly as it did.
This was a contract in intent and not in fact until this release.
`cli/tui/components/status-line.tsx` wrote to stdout, because it was built as a
drop-in for `@clack/prompts` and that is where `@clack` defaulted — so
`kreogen review --json > out.json` produced a file with three human lines ahead
of the document and a spend line after it, and did not parse. `audit --json`
looked clean only because it returns before any of those calls are reached.
If you were scraping human output off stdout — `kreogen sprint | grep`,
`kreogen init | tee` — it is now on stderr. Redirect with `2>&1`.
`packages/cli/__tests__/cli/json-output.test.ts` is the gate: one row per
command exposing `--json`, each asserting that the whole of stdout parses as a
single JSON document. Adding a `--json` command means adding a row.
A command whose entire output *is* a document keeps that document on stdout
without `--json` too — `kreogen standard` prints its listing there, so
`kreogen standard > standard.txt` works. What moved is everything a human reads
*around* a result, never the result.
## Auditing a repository
`audit`, `sprint`, `standard` and `mcp` all sit on top of the same
deterministic core: `kreogen audit .` fingerprints a Next.js repository (no
model, no network) and evaluates it against the hand-authored rule set,
printing a score, a tier and a gate verdict. `kreogen standard` prints the
rules themselves — `kreogen standard` lists all of them, `kreogen standard
explain ` prints one in full. The same rule set, rendered, is the
[standard page](/en/docs/standard).
```sh
kreogen audit . # score, tier, gate verdict
kreogen audit . --json # the report as JSON, nothing else
kreogen audit . --only data,delivery # just these rule ids or categories
kreogen audit . --fail-on blocker # exit 3 only at or above this severity
kreogen audit . --profile kreogen # add the rules that are kreogen's taste
```
### Profiles
`--profile general` is the default, and it is the set of rules that hold in
*any* Next.js repository. `--profile kreogen` is a superset: it adds the rules
that encode a house preference rather than a general convention.
Exactly one rule is in the kreogen profile today,
`tooling/formatter-is-ultracite`. Ultracite is a Biome preset, so a repository
already running Biome has made the choice that rule argues for and differs only
in which preset it extends — charging a stranger's repository for that spends
the audit's credibility on nothing. The mechanism exists so the next such rule
has somewhere to go other than out.
`kreogen sprint` takes the same flag and the same default, because a handoff
pack is a charge against a repository in exactly the way a report is -- more so,
since `--apply` acts on it. The MCP server's audit tools are pinned to general
and take no flag.
`--only` naming a rule outside the active profile is a usage error rather than
an empty report and exit 0. `--skip` is checked against the whole standard,
since skipping an already-excluded rule is a no-op. `kreogen standard` lists
every rule regardless of profile — it is what generates the
[standard page](/en/docs/standard) — and takes the same `--profile` flag to
filter.
`sprint` turns a report into a Claude Code handoff pack — one document per
finding, grouped into workstreams, written to `/.kreogen` by default:
```sh
kreogen sprint . # write the pack
kreogen sprint . --check # exit 5 if regenerating it in memory would change anything
kreogen sprint . --apply # run the eligible fixes, one commit each
```
A finding is eligible for `--apply` only when its rule declares a single
command that produces the target state, declares that command idempotent, *and*
declares `interactive: false` — its own assertion that the command completes
without asking a question. Silence is read as "do not run": a rule that says
nothing about prompting has asserted nothing, and `ultracite init` under an
explicit `--yes` is what taught that, stopping on "Which linter do you want to
use?". Everything `--apply` declines to touch is listed with the reason.
`--check` is the CI gate: it regenerates the pack in memory and diffs it
against what is on disk, so a handoff pack going stale is a build failure
rather than something someone notices weeks later. That comparison has to be
stable run to run with an unchanged repository, which is why the model layer
below is deliberately not part of it by default.
`kreogen mcp --root ` serves the same audit as MCP tools over stdio, for
an agent (a connected Claude Code session, for instance) already working
inside the repository being audited: `kreogen_audit`, `kreogen_fingerprint`,
`kreogen_standard`, `kreogen_read_file`, `kreogen_grep` and `kreogen_explain`
(which cross-references a file against every rule whose evidence names it, in
one call, instead of three). Every tool is read-only and jailed to the root
the server was started with — realpath-checked on both ends, so a symlink
inside the root that points outside it cannot be used to escape it.
## `sprint`'s narration can read the repository
`sprint`'s model prose (the paragraph under each finding explaining what it
looks like in *this* repository) is not handed a pre-serialized blob and
nothing else — it also has `read_file`, `list_directory`, `grep` and
`git_log` tools, jailed to the repository being audited, to verify a specific
claim before writing about it. The loop is bounded on two independent axes so
neither a cost surprise nor a runaway trajectory is possible: a hard step
ceiling, and a live budget check between steps once `--max-cost` is set. A
trajectory that used a tool is not written to the model-response cache, since
the next run may see a genuinely different repository; a tool-free response
is cached exactly as before. None of this changes what a model is allowed to
say — `parseSynthesis` still drops anything naming a finding the audit did
not raise, and the pack renders identically whether or not a tool ever fired.
## `kreogen review`
The rule set can only ever encode patterns someone thought to write ahead of
time. `kreogen review` is a different, additive kind of value: an open-ended
agentic pass with the same explore tools as `sprint`'s narration and no
findings to narrate — just the repository, and instructions to find things a
fixed rule set would not catch (a subtle bug, an over-engineered abstraction, a
misleading name).
```sh
kreogen review . # needs --max-cost or -y; there is no free-running default
kreogen review . --max-cost 2 --json
kreogen review . -y --max-tokens 500000
kreogen review . --scope packages/auth -y # one workspace, in full
kreogen review . -y --no-decompose # one pass over everything, as it used to run
```
### One pass per workspace
The default is one bounded pass per workspace, merged. That is not a
performance tweak — it is the difference between the command working and not.
The same model, the same prompt and the same planted `x-forwarded-for`
off-by-one: found and explained when the run was scoped to the one package, and
`findings: []` when it was pointed at the monorepo that package lives in.
Breadth was the binding constraint, not the prompt and not the model.
A review unit is a workspace, because a workspace is the only decomposition the
repository itself declares. Each pass is jailed to that directory and seeded
with that directory's own file inventory, and it is told which workspace it is
in and that the others are reviewed separately — a model handed one package's
file list without being told so reports its unreachable dependencies as
findings.
`--max-steps` is a **total across units**, not a per-unit ceiling. The default
is 12 steps per unit, so it scales with the repository. A total that divides to
fewer than 5 steps each is refused with a usage error naming the arithmetic and
both escape hatches, rather than running a dozen passes too short to read a
file.
A unit whose pass fails — a provider error, an unparseable response, a content
filter — degrades that unit and the merge proceeds. The rendered document
carries a `## Coverage` section naming what was and was not reviewed, because a
run that covered 22 of 25 workspaces reads exactly like one that covered 25 and
found nothing unless it says so. Findings are merged and de-duplicated on path
and title, each keeping its unit as provenance.
`--scope ` narrows to one workspace and `--no-decompose` forces the old
single pass. Both are escape hatches, not modes.
Its findings use a deliberately different vocabulary from an audit finding —
`worth-fixing` / `worth-discussing` / `minor`, not
`blocker`/`high`/`medium`/`low`/`info` — so the two can never be mistaken for
each other. They are written to `/review/findings.md` and
`findings.json`, outside `sprint`'s pack entirely: `review` never affects
`sprint`'s score, gate or exit code, and its own exit code never depends on
what it found — only on whether it produced something usable at all. The
rendered document opens by saying so.
Run it from a real terminal and each step updates a live panel in place —
current step, the tool call it's making, running spend, and how much of
`--max-cost` is left. Piped or redirected output (a CI log, `> out.txt`)
gets the same plain `step N/max: toolName` line this command has always
written, so tailing a long run in CI still shows it making progress rather
than nothing until the very end.
## `kreogen plan` and agentic branding
`kreogen plan ""` interviews a brief into two files —
`kreogen.recipe.json` and `PROJECT-BRIEF.md` — and writes nothing else; it
never calls `init`, and no model runs after the recipe is written. That is
the safety argument for the whole command: a model's opinion becomes a JSON
file someone can read and edit, and the step that actually generates a
repository (`init --recipe kreogen.recipe.json`) takes that file and no model
at all.
The interview can propose more than scope and capabilities — a `branding`
object (product name, description, legal name, support email, url, social
links) if the brief gives it enough to work with. `init` applies that
proposal in two different ways, chosen deliberately per field:
* **Identity is deterministic.** `packages/branding/index.ts` — the single
source every generated project's footer, header and page metadata read
from — is rewritten by a targeted, template-fill pass, not a model call.
No brief means no rewrite: the template's placeholder identity ships
unchanged, exactly as it always has.
* **Marketing copy is agentic, and opt-in.** The hero and footer prose in
`packages/internationalization/dictionaries/en.json` is genuinely
free-text, so generating it requires `--agentic-edit` explicitly:
```sh
kreogen plan "a booking product for clinics, Stripe, no blog"
kreogen init --recipe kreogen.recipe.json --agentic-edit
```
Without `--agentic-edit`, `init` never imports the editing capability and
never reaches a provider for this purpose, even if a credential is present
and even with `--yes` — `--agentic-edit` is a separate, independent flag
from `--yes`, never folded into it. A CI script already passing `--yes` to
skip prompts must not silently start making model calls and writing files
the moment editing is also unlocked. `--agentic-edit` without a TTY and
without `--yes` fails immediately with a usage error rather than hanging on
a confirmation nobody can answer; with `--yes`, the generated diff is
applied without asking, same as `update`'s own `-y`.
Like `review`, a failed or empty agentic-edit run degrades: project
generation still finishes, and a one-line warning explains that the copy
step produced nothing.
## Agentic file editing
`init --agentic-edit` is powered by a small, general editing capability
(`engine/agentic/capabilities/edit.ts`) built on two tools —
`write_file` (full create/replace) and `apply_patch` (a unified diff, applied
through `git apply`) — bounded exactly like `review`'s loop (a hard step
count plus a live budget check, a write consuming a step the same as a read).
The loop never touches the real project directory while it runs. It operates
against a filtered, disposable shadow copy; the caller only applies the
resulting changes to the real directory after computing a diff per file and
getting it approved (a prompt, or `--yes`) — the same dry-run-then-confirm
shape `kreogen update` already uses for template updates. Every write is also
jailed to its root the same way the read-only explore tools are: `.git` and
`node_modules` are refused outright, and a write over the size cap is
rejected rather than silently truncated.
## What counts as template-only
`bootstrap/constants.ts` is the list, and it is the file to edit when adding
tooling that belongs to kreogen rather than to projects:
```ts
export const INTERNAL_DIRS = [
'.gitlab', '.claude', 'ci', 'apps/docs', 'packages/cli', '.turbo',
];
export const INTERNAL_FILES = [
'.gitlab-ci.yml', 'release.config.js', 'commitlint.config.js',
'renovate.json', 'CHANGELOG.md', 'CODEOWNERS', 'CONTRIBUTING.md',
'SECURITY.md', 'LICENSE', 'CLAUDE.md',
];
```
Add template-only tooling here, or `strip.test.ts` and the `cli:smoke` CI job
fail. That is deliberate — the test is what stops kreogen's own pipeline and
documentation from shipping inside a client project.
`LICENSE` is stripped because a generated project picks its own. The upstream
grants it must still carry travel in `LICENSES/` and the swapped-in `NOTICE`,
so **do not** add `LICENSES/` to that list.
## Template swaps
Some files must exist in a generated project but with different content. Those
ship under `templates/` so the template's own copy can be stripped, then are
renamed into place:
| Shipped as | Becomes |
| ---------------------------------------- | ---------------------- |
| `templates/project.gitlab-ci.yml` | `.gitlab-ci.yml` |
| `templates/project.README.md` | `README.md` |
| `templates/project.NOTICE` | `NOTICE` |
| `templates/project.CLAUDE.md` | `CLAUDE.md` |
| `templates/project.AGENTS.md` | `AGENTS.md` |
| `templates/project.commitlint.config.js` | `commitlint.config.js` |
| `templates/project.renovate.json` | `renovate.json` |
Without this, every client project would inherit kreogen's CI pipeline, its
README, and a `CLAUDE.md` describing a repository the project does not have —
which taught every coding agent in a client project about the wrong codebase.
The last two rows are the ones that cost most when they are missing. Stripping
`commitlint.config.js` without replacing it is worse than shipping kreogen's:
lefthook still runs commitlint on `commit-msg`, and commitlint with no config
exits 9 on `empty-rules`, so `init` could not make the project's own first
commit. And without `renovate.json` a generated project has no dependency
updates at all — which the audit reports as a finding kreogen itself does not
have, and which nobody notices until the first CVE.
`update` derives its never-touch list from this table rather than restating it,
so a swap added later is protected without anyone remembering to.
## Package managers
bun, pnpm and npm. yarn is deliberately unsupported: Classic 1.22 is
end-of-life and has no real workspace-protocol support.
Choosing anything but bun triggers a conversion — `workspace:*` ranges
rewritten, bun-specific scripts rewritten, `bun.lock` removed. pnpm
additionally gets a `pnpm-workspace.yaml` with
`onlyBuiltDependencies`, because pnpm blocks lifecycle scripts by default and
without it `prisma generate` and `sharp` silently never run.
The pinned versions live in `bootstrap/constants.ts` and are kept current by a
[Renovate custom manager](/en/docs/packages/security/dependencies#versions-that-are-not-in-a-manifest),
since no dependency manager would look for a version inside a TypeScript file.
## Versioning
`packages/cli/package.json` deliberately carries no `version` field in git. It
is written during release.
`bun.lock` records versions for workspace packages but not for the root, so
bumping the root version is lockfile-safe while bumping the CLI's would dirty
the lockfile and break the next `--frozen-lockfile` install.
The published build reads its own version from `package.json` at startup rather
than having it baked in by the bundler, so `kreogen --version` always reports
what npm actually installed.
## Developing on it
```sh
bun run cli:build # tsup
bun test --filter @kreotic/kreogen
```
`src/` is organized around the two-engine split above:
`cli/` (Commander wiring, errors, exit codes), `engine/deterministic/`
(fingerprint, rules, evaluate, the handoff pack), `engine/agentic/`
(provider resolution, budget, the bounded tool loop, and one file per
capability — `narrate`, `review`, `interview`, `edit`), `bootstrap/`
(everything project-generation, engine-agnostic), `mcp/` (the MCP server),
and `infra/` (filesystem jail, subprocess spawning, diffing — no CLI or
engine concept). `__tests__/` mirrors that tree 1:1.
The tests are the interesting part: `strip.test.ts` asserts the internal
content list against the real repository, `update.test.ts` covers the diff
parser — including the cases that broke it, such as a filename containing a
space and `core.quotepath` escaping non-ASCII into C-style octal — and
`engine/agentic/runtime/loop.test.ts` proves the shared bounded tool loop's
step and budget ceilings against a scripted mock model rather than merely
asserting them.
---
For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)
For an index of all available documentation, see [/llms.txt](/llms.txt)