# CLI reference (https://www.lurq.run/docs/cli)



The lurq engine is usable standalone from the terminal, with the same scoring and
retrieval the MCP tools expose. Add `--json` to the query and check commands for
machine-readable output.

<Callout type="info">
  Run `lurq setup` first. The query commands read whichever index is configured: the hosted service
  via your stored API key, or your own Postgres if `DATABASE_URL` is set and no key is. A key wins
  over `DATABASE_URL`: otherwise, running `lurq` inside your own Postgres-backed app would point
  package lookups at that app's database. Operators holding both can force the local index with
  `LURQ_LOCAL=1`. See [Self-hosting](/self-hosting).
</Callout>

## Querying the index [#querying-the-index]

```bash
lurq verify lodahs
lurq evaluate zod
lurq compare drizzle-orm prisma typeorm
```

Not sure which command answers your question? Ask in plain words:

```bash
lurq can "check a major upgrade"
```

`can` searches the catalog that ships with the CLI, so it works offline and
before setup.

## API surfaces [#api-surfaces]

What a version actually exports, and what moved since the version you know:

```bash
lurq usage zod                       # the current export surface
lurq usage zod --known 3.22.4        # …plus the exact delta from the version you know
lurq usage react --target 19.0.0     # pin the target version
lurq versions react                  # the stored version timeline (local index only)
```

`--known` is the one worth remembering: pass the version your model thinks it
knows and get the precise added / removed / renamed / changed list for the
version you're actually installing.

## Whole stacks and projects [#whole-stacks-and-projects]

```bash
lurq compat next react react-dom             # do these install together?
lurq compat next react --pin next=15.5.4     # check exact versions
lurq audit                                   # everything this project depends on
lurq audit --project-only                    # …ignoring user-level agent configs
```

`--pin` requires an exact published version. A range would silently resolve to
`latest` and report the wrong version as pinned, so it's rejected outright.

`audit` reads the project's `package.json`, lockfile, and the MCP servers
configured for it, and reports what is outdated, deprecated, carries an advisory
for the installed version, or has drifted. Anything it could not assess is
counted as such, never reported clean.

## Upgrades [#upgrades]

These run where your code is — a laptop or a CI runner:

```bash
# what's behind, and what each upgrade removes from its API  (needs your lurq key)
lurq upgrade-plan . --json > lurq-plan.json

# do those upgrades remove symbols YOUR code references?      (needs nothing)
lurq check-upgrade . --plan lurq-plan.json --exit-code

# or name the upgrades directly, no plan needed
lurq check-upgrade . --upgrade commander@11.1.0..12.1.0 --upgrade zod@3.23.8..4.0.0

# and write the changes that need no judgement
lurq fix .
```

`check-upgrade` compares both versions' npm tarballs against the symbols your
codebase actually imports, and reports them at `file:line`. It needs no API key,
no network to lurq, and **no test suite** — which is the point: it catches
breakage in code no test covers. `--exit-code` exits 1 when the report is not
safe, for use as a CI gate. `--upgrade` takes `pkg@from..to`, and repeats.

When the package itself proves a rename (the old version exported both names
from the same function), the replacement is printed next to the call site:

```text
BLOCKING  cookie  1.1.1 → 2.0.1
  Removes 1 symbol(s) your code references:
    · cookie.parse → parseCookie    src/session.ts:239
```

It also type-checks the files that import each package, twice: once against the
old version's type definitions and once against the new one's, straight from the
tarballs, with nothing installed. Only errors the upgrade introduces are
reported, so a renamed option or a narrowed parameter shows up at `file:line`
even though the runtime exports never changed. It uses your project's own
`typescript` when it has one, needs a `tsconfig.json`, and skips packages that
ship no type definitions of their own. `--no-types` turns it off, for a faster,
runtime-only check.

What it reports:

| Severity     | What changed                                                                                                                  | Real example                                         |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| **blocking** | a symbol your code uses is gone                                                                                               | `cookie.parse`, removed in cookie 2                  |
| **blocking** | a deep import is no longer offered                                                                                            | `require('uuid/v4')` on uuid 8                       |
| **blocking** | the package went ESM-only, and your code calls what `require()` returned or reads a property it no longer exports             | `fetch(url)` on node-fetch 3, `chalk.red` on chalk 5 |
| **warning**  | a call passes an argument count the new version no longer accepts. Calls that still fit are not reported                      | `src/log.ts:31 passes 1`                             |
| **warning**  | the type check finds a new compiler error                                                                                     | `TS2554 Expected 2-3 arguments` on zod 4             |
| **warning**  | `require()` of a now-ESM package, which throws on Node before 20.19 / 22.12. Skipped when your `engines.node` rules those out | execa 9                                              |
| **warning**  | a new `engines.node` or peer range this project does not meet                                                                 | react-dom 19 with react 18 installed                 |

Downloads are retried on registry errors and checked against the integrity hash
the registry publishes. A version range (`^2.0.0`) is refused rather than
guessed at, and a scan that stops at its file limit says so.

An upgrade it could not establish is reported as `unverified` and is **never**
counted as safe.

`--report` sends the result to your lurq dashboard as well (it needs an API key,
from `--api-key` or `LURQ_API_KEY`). It's opt-in and never changes the outcome of
the check.

### Writing the fix [#writing-the-fix]

`check-upgrade` tells you what breaks. `lurq fix` writes the part of it that
needs no judgement, and hands the rest to your agent with the facts it needs.

```bash
# work out what moved, and show the diff       (one lookup needs your lurq key)
lurq fix .

# name the versions yourself, and it needs nothing
lurq fix . --upgrade cookie@1.1.1..2.0.1

# write the files
lurq fix . --apply

# file every finding as a GitHub code scanning alert
lurq fix . --sarif lurq.sarif
```

With neither `--plan` nor `--upgrade` it works out what moved by itself, so you
never have to look up a version to pass one in. That single lookup is the only
part that talks to lurq, and it applies your repo's selection policy — an
upgrade the policy holds is not attempted. Naming the versions yourself keeps
the entire run offline, exactly like `check-upgrade`.

Nothing is written until `--apply`; the default prints a unified diff, and
`git apply` accepts it:

```text
Would write 2 file(s) (pass --apply):
  rename parse to parseCookie — src/alias.ts, src/app.ts

--- a/src/app.ts
+++ b/src/app.ts
@@ -1,4 +1,4 @@
-import { parse } from 'cookie';
+import { parseCookie } from 'cookie';
```

It bumps the declared range in the same run. Rewriting the imports and leaving
`"cookie": "^1.1.1"` in `package.json` is a tree that does not build: the code
now calls an export the installed version lacks, and the next `npm install`
faithfully reinstalls the old one. Every manifest declaring the package is
bumped, across workspaces, keeping the operator you wrote (`^`, `~`, or an exact
pin). A range that already admits the new version is left alone rather than
narrowed, and one whose intent is a judgement — `>=1.0.0 <2`, `1 || 2`, `1.x` —
is reported instead of guessed at.

Only a rename the package itself proves is ever written — the old version
exported both names from one declaration and the new one still exports the
survivor. That makes the rewrite mechanical rather than a guess. Whether a given
upgrade qualifies depends on the exact versions: cookie 1.1.1 → 2.0.1 does,
because 1.1.x shipped `parseCookie` alongside `parse`; 1.0.2 → 2.0.0 does not.

Everything else becomes a brief instead of an edit, carrying the exports the new
version actually ships with their kinds and arities — the facts an agent doing
the rewrite cannot look up, since it has no network and its training data is the
stale source this is correcting. Two candidates is a choice, not a fix, so it is
briefed too.

Refusals are per file. A file where the imported name is declared twice can't
have its call sites attributed to the import, so that file is briefed while the
rest are still rewritten:

```text
1 change(s) lurq will not make for you:
  parse was renamed to parseCookie, but 1 file(s) need a person:
  src/shadowed.ts (parse is declared more than once)
```

An aliased import is rewritten once, at the import, because every call site
still reads the local name. Type-only references are skipped, subpath imports
keep their own surface, and every edit records the bytes it was computed from —
so if a file changes between the scan and the write, nothing is written at all
rather than a rename landing in the wrong place.

Three kinds of upgrade it will not attempt at all — and it names each one rather
than passing over it in silence:

| Not attempted                                    | Why                                                                                                                                                                |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| A multi-major upgrade (`hops` in the plan)       | It has to be walked one major at a time. Jumping straight to the target skips whatever each intermediate release removed.                                          |
| One the plan could not sequence (`sequenceNote`) | That is a migration, not an upgrade, and nothing mechanical should attempt it.                                                                                     |
| Anything past `--max <n>`                        | The blast radius. A deterministic edit is safer than a model's, but a diff touching forty packages is still a review nobody finishes, and the number is your call. |

`check-upgrade` still reports on all three: checking an upgrade is free and tells
you something, so only *performing* one is capped. If every upgrade in a plan
falls into these, `fix` says so and names them — it never reports "nothing to
fix" for work it declined to do.

`--sarif` writes SARIF 2.1.0 for `github/codeql-action/upload-sarif`, which
gives each finding dedup, assignment and closing itself when it stops
reproducing. Proven renames carry the replacement as a SARIF fix, so the
suggested change is visible in the alert; briefs carry the agent instruction
instead. `--exit-code` exits 1 when anything is left for a person or an agent to
do, and `--json` prints the whole result.

## Before you publish [#before-you-publish]

The same diff, pointed the other way: will your change break the people who
depend on you?

```bash
# does the version in package.json match what this release did to the API?
lurq check-release --exit-code
lurq check-release --against 2.3.0           # compare with a specific published version

# does this change break callers of your own HTTP API?
lurq check-api openapi.yaml --against main --exit-code
```

`check-release` compares the package in the directory with its latest published
version (or `--against`), and `--exit-code` exits 1 when the version bump
understates the change — a removal shipped as a minor, say. Use it in
`prepublishOnly` or CI. It needs no key and no network to lurq.

`check-api` compares an OpenAPI document with the same file at a git revision
(`--against`, default `HEAD`), and exits 1 on a breaking change with
`--exit-code`. With no argument it uses the first OpenAPI document it finds;
`-C <dir>` points at another repository. Nothing leaves the machine.

## Environment [#environment]

```bash
lurq check-env .
```

Which variables does this project read that nothing declares — the "works on my
machine" gap. It reads your own source and your `.env*` files, needs no API key
and no network, and never looks at a value: a declared name is the text left of
the first `=`, and the rest is not parsed.

A commented declaration counts. `.env.example` files conventionally document
optional variables as `# REDIS_URL=…`, and that still tells a fresh clone the
variable exists.

Three things are deliberately never reported, because a noisy check gets
switched off and a check nobody runs is worth less than no check:

| Not reported                                                                                                       | Why                                                                     |
| ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
| Platform variables — `NODE_ENV`, `CI`, `PORT`, the `GITHUB_*`, `npm_*`, `VERCEL_*` families, XDG and Windows paths | Supplied by the runtime or the runner; they belong in no `.env.example` |
| Reads inside tests, specs and fixtures                                                                             | The harness that runs that code sets them itself                        |
| A read with its fallback beside it — `process.env.CACHE_TTL ?? '60'`                                               | The code already states what happens without it                         |

A variable defaulted at every site is optional; one unguarded read anywhere
makes it required, and only those sites are cited. Reads are found through the
TypeScript AST, so a `process.env.X` inside a comment or a string is not a read,
and `process.env[key]` is skipped rather than guessed at.

`--exit-code` exits 1 when anything is undeclared **or** when the scan stopped
at its file limit — a file nobody opened is not a clean result. `--sarif` files
each variable as a GitHub code scanning alert; `--json` prints every finding
with all of its read sites.

## MCP servers [#mcp-servers]

```bash
lurq mcp-scan                                  # read every configured server's real contract
lurq mcp-ci                                    # a GitHub Actions workflow that rescans daily
lurq mcp-stack                                 # do your configured servers collide?
lurq mcp-surface @modelcontextprotocol/server-filesystem
lurq mcp-drift @modelcontextprotocol/server-filesystem --from <version> --to <version>
lurq connect-check https://mcp.example.com/mcp --client chatgpt
lurq mcp-pin io.github.acme/weather            # tell me when this server changes
lurq mcp-pins                                  # what changed since you pinned it
lurq mcp-unpin io.github.acme/weather
```

`mcp-scan` and `mcp-ci` have their own page: see
[Scanning your MCP servers](/mcp-scan). `mcp-surface` returns a published
server's tool contract as lurq probed it (`--version` for an exact version);
`mcp-drift` needs both `--from` and `--to`, and reports what moved between them,
including silent schema changes and tools that stopped being read-only.

`connect-check` takes an endpoint URL, an official registry name or an npm
package name, and answers for every MCP client lurq has a profile for — or one
client with `--client` — whether the server works there, what setup it needs (a
key to send as a header, an OAuth client to pre-register and the redirect URIs
to allow), or why it is blocked. With `--client` it also prints the config to
paste in that client's own format. A URL lurq has not probed is read live and
not stored.

`mcp-pin` records a remote server as it is now, so lurq tells this account when
its tools, its sign-in path or its availability change; `mcp-pins` lists what
changed since, and re-running `mcp-pin` approves a change you reviewed. Servers
in your uploaded `mcp-scan` results are watched the same way without pinning.
These three need an API key, because a pin belongs to an account.

## Scoring controls [#scoring-controls]

```bash
lurq weights                                  # show & explain the weight model
lurq edit-weights --set composite.lambda=0.5  # override a weight (or --reset)
lurq edit-weights --explain adoption          # what a component measures
```

Both work without an index: the weight model ships with the package. Overrides
are written to your user config, or to `.lurq/weights.json` in the project with
`--project`.

## Selection policy [#selection-policy]

Keep the rules your agents are held to in a file, review changes in a pull
request, and apply them from CI.

```bash
# download the current policy
lurq policy pull lurq.policy.json

# in a PR check: is the file a valid policy? sends nothing
lurq policy push lurq.policy.json --check

# after merge: replace the policy with the file
lurq policy push lurq.policy.json

# who changed the policy, from where, and what changed
lurq policy history

# what agents were refused, or warned about, in the last 30 days
lurq policy log --days 30
```

`push --check` prints the change the file would make when an API key is
available, and every `push` prints the change it made. An invalid file is
rejected with the field that is wrong.

Two fields are worth knowing when you edit the file by hand:

* `"mode": "warn"` reports what the rules would refuse without refusing it.
  Roll a new rule out in warn mode, read `lurq policy log`, then switch to
  `"enforce"`.
* An exception can carry a reason and an expiry:
  `{ "name": "moment", "reason": "legacy date code", "expires": "2027-01-01" }`.
  On the expiry date it stops applying, and stays in the file until you remove it.

`push` replaces the whole policy. It needs an API key with the `policy:write`
scope, created in the dashboard on a Team or Business plan. The key `lurq setup`
stores for your agents can read policy but never change it, so an agent cannot
loosen its own rules. How far back `policy log` reaches depends on the plan (see
[Plans & limits](/plans-and-troubleshooting)).

## Arming a repository [#arming-a-repository]

One command does the whole repository setup: the secret, the workflow file, and
the Anthropic credential when the mode needs one.

```bash
# in a checkout, with `gh` signed in
lurq autopilot init

# any number of repositories at once — no clone needed
lurq autopilot init --repo owner/api owner/web owner/worker

# review first: a pull request in each instead of a commit
lurq autopilot init --repo owner/api owner/web --pr

# fix (the default) needs no model; pr also runs an agent and sets its credential
lurq autopilot init --mode pr
```

For each repository it sets `LURQ_API_KEY`, commits
`.github/workflows/lurq-upgrade.yml` to the default branch through the GitHub
API, and starts the first run. A protected default branch gets a pull request
instead. Every write is made by **your** `gh` login, never by lurq's GitHub App,
which stays read-only. The token needs the `workflow` scope
(`gh auth refresh -s workflow`); the command checks before touching anything. An
already-committed workflow is left alone unless you pass `--force`. Secrets go to
`gh` on stdin, never in arguments, so they stay out of `ps` and your shell
history.

In `pr` mode it runs `claude setup-token` and sets
`CLAUDE_CODE_OAUTH_TOKEN` for you. That token **expires one year after
creation, with no warning** — on a weekly job that means it works for a year
and then stops. If anyone else maintains the repository, use an
`ANTHROPIC_API_KEY` from the Anthropic console instead: it does not expire and
does not belong to one person. If `claude` is not installed, the command tells
you that and sets nothing.

Run it again any time: an existing workflow file is left alone unless you pass
`--force`.

## Setup and credentials [#setup-and-credentials]

```bash
# interactive: sign in in the browser, then wire up every assistant found
lurq setup

# scriptable: no prompts, no browser
lurq setup --yes --no-open --api-key <key> --agent all

# just one assistant
lurq setup --agent claude-code

# point at your own serve-http instead of api.lurq.run
lurq setup --url https://lurq.internal/mcp

# forget the stored key on this machine (agents keep theirs)
lurq logout

# remove lurq from every agent and this machine
lurq uninstall
```

`--agent` takes `claude-code`, `cursor`, `windsurf`, `copilot`, `codex`,
`gemini-cli`, `antigravity`, `kiro`, or `all`. See
[per-agent configuration](/quickstart#per-agent-configuration) for what each one
gets, and [Uninstall](/quickstart#uninstall) for what `uninstall` removes.

The endpoint is stored with the key, so a later bare `lurq setup` stays on your
server. `LURQ_ENDPOINT` overrides it for a single command without changing what
is stored.

`lurq install` and `lurq login` are aliases for `lurq setup`.

`install-skill` is the older, narrower command: it writes MCP entries and skill
files but does **not** store the key for the CLI. Prefer `setup`.

```bash
# self-host: wire your agent to a local stdio server instead of the hosted API
lurq install-skill --local
```

## Output format [#output-format]

The query, check and scan commands support `--json`:

```bash
lurq evaluate zod --json
```

This emits structured output suitable for piping into other tools, the same
shape the MCP layer serializes.
