The CLI
Run the same queries the web flow generates, from your own terminal, against your own warehouse. Read-only by construction rather than by promise — and the construction is below, before anything asks you to install it.
Try it without installing anything
On npmnpx nemulai verify --dialect postgres --dry-runThat prints the exact statement it would send and exits. No credential is read. No connection is opened. It is the first thing to run and the thing to hand to your security reviewer.
Or install it: npm i -g nemulai · Node 20+ · npmjs.com/package/nemulai
Your first real analysis
Start with one table and one closed period. Use a secret-backed environment or an existing dbt profile for the connection; do not paste a production DSN into an interactive command where the shell can retain it.
1. Preview the read-only surface.
npx nemulai verify --dialect postgres --dry-run2. Map one usage table and preview the extraction.
npx nemulai extract --dialect postgres --dry-run \ --table public.usage_events --tenant-id customer_id \ --timestamp created_at --cost cost_usd \ --from 2026-07-01 --to 2026-08-01 --out usage.csv3. Run the same command without
--dry-run, then verify its manifest.npx nemulai verify-bundle .nemulai/run-<id>.json4. Open the local margin flow, select the manifest and CSV together, add revenue for the identical period and enter the matching provider-statement total. The browser holds the files in memory; reloading clears them.
How read-only is enforced
Four layers, listed strongest first. We do not claim the parser is the guarantee — it protects against our bugs, not against your policy. The role you provision is what holds regardless of whether this tool is correct.
A role your DBA provisions
The guaranteenemulai grant --print emits least-privilege read-only DDL for your warehouse. You run it; we never do. This is the only layer that holds against a bug in our own templates, which is why it is layer one and not a footnote.
Server-enforced session mode
Server-sideEvery statement runs inside BEGIN TRANSACTION READ ONLY with a statement timeout and application_name=nemulai-cli, so you can find our queries in your own logs afterwards. Snowflake has no session read-only flag — there, layer 1 is the enforcement, and the CLI says so.
Client-side statement parsing
Defence in depthPostgres and Redshift are parsed with libpg_query — Postgres's own grammar — and refused unless the statement is SELECT or WITH … SELECT. A data-modifying CTE and SELECT … INTO both parse as SELECT statements and both write; both are caught. A statement we cannot parse is refused, never waved through.
--dry-run
AuditablePrints every statement the command would send, then exits before opening a socket — and before reading any credential. A reviewer with zero access to your warehouse can audit the full surface. Run it first; hand the output to whoever must approve it.
What it never does
- Never prompts for a password. A password typed at a prompt lands in shell history, CI logs and process listings. Prefer an environment supplied by your secret manager or an existing
~/.dbt/profiles.yml. The legacy--connection-stringoverride remains available, but a literal DSN can be retained by the shell and should not be pasted interactively. The credential source is printed before anything connects. - Never prints a credential. The display target is rebuilt from scheme, host, port, database and user only. The password field is never read for display, so there is nothing to leak into
--jsonoutput or a CI log. - Never executes DDL. Both
grantandinstall-scriptprint SQL for you to run. The parser refuses our own DDL too — an allowlist for the writes we authored is exactly the hole a reviewer would find. - No raw rows, no credentials, no content — and nothing you have not seen listed and approved. Not “nothing leaves your environment”: once you upload a bundle, something does. What crosses is aggregates you produced, verified offline, and dragged in by hand.
verifyandverify-bundlesend nothing at all; there is no background sync and no stored warehouse credential.
Which warehouses actually work
Two that work perfectly beats four with caveats. The caveats are here rather than in a footnote, and the CLI prints them again before an unverified dialect runs.
Postgres's own grammar (libpg_query)
The golden path. Every command exercised end to end against a real Postgres. If you have a choice, choose this.
Same grammar; unparseable statements are refused
Postgres-family, so the statement check is the strong one. The full extract path has not been run against a real Redshift cluster — only against Postgres.
Lexical only — weaker
Code exists and is unit-tested. No credential has ever been used against a real Snowflake account, so connection, quoting, type mapping and the read-only session guard are unproven in practice. Snowflake also has no session read-only flag: there, the role you provision is the enforcement.
Lexical only — weaker
The dry-run scan estimate and the maximumBytesBilled cap are implemented and tested against fakes. Neither has ever run against the Google API.
Commands
nemulai verifyAvailableGenerates the schema metadata query, checks it against the read-only parser, prints it, and runs it. Reports readiness with the gaps named first — what is missing, and what adding it would unlock. Add --forward <file.html> to also write a self-contained page you can send to whoever owns the revenue number or grants database access: no scripts, no external resources, nothing that phones anywhere. Postgres and Redshift connect directly; Snowflake and BigQuery print the statement for you to run.
nemulai grant --print · --forwardAvailableThe read-only role DDL for your dialect — Postgres, Redshift, Snowflake roles, or BigQuery IAM bindings. Emit-only; the CLI never runs it. --forward writes it as a page for whoever provisions access, carrying the four things a reviewer asks before running DDL: what it grants, what it deliberately does not, who runs it, and how to undo it. The person who can create a role is usually not the person running the audit, and that handoff is the step the flow used to leave invisible.
nemulai install-scriptAvailableThe aggregate install DDL (CREATE SCHEMA, TABLE, VIEW) for your DBA. The CLI executes no DDL — and its own parser refuses this file too, with no exemption for SQL we wrote ourselves.
nemulai extractAvailableAggregates in your warehouse and never pulls raw rows: per tenant, model, provider, workload and hour bucket — plus a fixed-boundary log-scale histogram of per-request cost, so dispersion survives aggregation and percentiles are reconstructable without a single row leaving. Scans every value before writing and refuses any column that looks like free text.
nemulai verify-bundleAvailableYou can verify our output on an air-gapped machine, with no network and no credential, against files you already hold. It re-checks every statement digest, every artifact digest, that no credential was written into the provenance record — and then it OPENS the CSVs and re-scans every column, so the report of "0 free-text columns, 0 credentials" is counted from the bytes rather than inherited from the run that produced them. That matters because the person checking a bundle is usually not the person who made it.
Related libraries
Neither of these is an SDK. Nothing here is a supported integration surface — the CLI is the product boundary.
@nemulai/audit-coreInternalThe dialect SQL generation, canonical field mapping, readiness scoring and extraction planning that the web flow and the CLI both import. It is NOT published to npm and there is no stable API to build against — it ships compiled into the CLI binary. It is listed here because it is why the query the CLI runs is byte-for-byte the query /audit/new generates: one module, not two implementations.
nemulai-agent (PyPI)OptionalA separate tool for hardware you own: reads NVML, AMD SMI or Apple silicon counters for measured device time instead of modelled cost. Nothing in the audit requires it, and no part of this page depends on it. It used to install a command called `nemulai`, which collided with this CLI; the command is now `nemulai-agent` and the old name still works with a deprecation notice.
Exit codes
Code 3 is the one that matters in CI: a scheduled audit that silently passes on unusable data is worse than one that fails.
0 ok
1 usage error
2 connection failure
3 readiness below threshold
4 statement refused by the read-only check
6 egress scan refused a column5 is deliberately absent. It used to mean “upload”, and nothing in this CLI has ever uploaded anything — a reserved code for a capability that does not exist is a promise in the interface, and this one pointed at the exact behaviour the design refuses: the bundle is handed over by you, because the manual step is the approval moment. 6 was not renumbered down, because exit codes are an API and someone's CI may already branch on it.
Prefer not to install anything? The web flow generates the identical query — the CLI imports the same module, so the two cannot drift. See also security & data handling.