--- 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)