Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a403e1a
feat(cli): agent contract — exit codes, error envelope, confirmation …
Domantas Oct 2, 2026
e68cf87
test(cli): agent-mode tests — exit codes, envelope, dry run, destruct…
Domantas Oct 2, 2026
d237297
docs(cli): task file for the agent-native CLI work
Domantas Oct 2, 2026
36c54e5
feat(cli): hub/api — request fields from the SDK, typed values, --body
Domantas Oct 2, 2026
5b831e8
feat(cli): `norbix schema [command] --json` — the command surface as …
Domantas Oct 2, 2026
0f61bca
feat(cli): --dry-run on every mutating command, realistic help examples
Domantas Oct 2, 2026
5d46b38
test(cli): schema snapshot, parser typing, dry-run on mutating commands
Domantas Oct 2, 2026
63fea3a
docs(cli): schema, typed hub values and --body in the README; work log
Domantas Oct 2, 2026
a479538
perf(cli): --version fast path and the oclif manifest in every build
Domantas Oct 2, 2026
0ec3efc
feat(cli): `norbix ai init` — set a project up for Claude Code and ot…
Domantas Oct 2, 2026
09e9c14
docs(cli): the agent contract, the agent recipe, and a README section
Domantas Oct 2, 2026
199c9d7
Merge remote-tracking branch 'origin/main' into feat/agent-native-cli
Domantas Oct 3, 2026
f81d7d8
test(agent-mode): give the spawn-every-destructive-command tests 120 s
Domantas Oct 3, 2026
fac66e5
Merge remote-tracking branch 'origin/feat/agent-native-cli' into feat…
Domantas Oct 3, 2026
44b6d04
feat(cli): the agent contract for the commands main added (ai, email,…
Domantas Oct 3, 2026
343bd67
Merge remote-tracking branch 'origin/feat/agent-native-cli-2' into fe…
Domantas Oct 3, 2026
38e283c
test(schema): snapshot with ai init and the commands from main
Domantas Oct 3, 2026
e9e8529
Merge remote-tracking branch 'origin/main' into feat/agent-native-cli-3
Domantas Oct 4, 2026
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ norbix scheduler list
| `norbix hub <module> <words...>` | Call ANY hub endpoint with plain words (see below) |
| `norbix api <module> <words...>` | Same for the data-plane API |
| `norbix raw <path>` | Low-level HTTP escape hatch (hub by default, `--api` for API) |
| `norbix schema [command]` | Describe every command as data (`--json`) — for scripts and agents |
| `norbix ai init` | Set up a project for Claude Code / Cursor / AGENTS.md agents |

### Plain-word access to every endpoint

Expand Down Expand Up @@ -178,6 +180,23 @@ nothing** unless you pass `--yes`. Preview any of them with `--dry-run`:
the request is built with your real auth, region and project, printed, and
not sent.

## Use with Claude Code / AI agents

The CLI is built to be driven by a coding agent from the shell — no MCP
server needed: one JSON document per call (`--json`), documented exit codes
(0–9), nothing destructive without `--yes`, `--dry-run` on every change, and
`norbix schema --json` to discover every command. Set a project up once:

```sh
norbix ai init # writes .claude/skills/norbix/SKILL.md + a CLAUDE.md block
norbix ai init --target all # also AGENTS.md (Codex, OpenCode) and a Cursor rule
```

Then give the agent credentials with a profile (`norbix login --api-key … --profile ci`)
or `NORBIX_API_KEY` / `NORBIX_PROJECT_ID` / `NORBIX_REGION`. The recipe the
agent follows is in [docs/AGENTS.md](docs/AGENTS.md); the guarantees it
relies on are in [docs/agent-contract.md](docs/agent-contract.md).

## Versioning

The major version is frozen at **v1** until the public launch.
Expand Down
13 changes: 11 additions & 2 deletions bin/run.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
#!/usr/bin/env node
import {execute} from '@oclif/core'

await execute({dir: import.meta.url})
// Fast path: `norbix --version` answers from package.json without loading
// oclif (the agent contract asks for < 200 ms; oclif alone costs more on a
// slow machine). Same string oclif prints: name/version platform-arch node-v.
if (process.argv.length === 3 && process.argv[2] === '--version') {
const {readFileSync} = await import('node:fs')
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
process.stdout.write(`${pkg.name}/${pkg.version} ${process.platform}-${process.arch} node-${process.version}\n`)
} else {
const {execute} = await import('@oclif/core')
await execute({dir: import.meta.url})
}
95 changes: 95 additions & 0 deletions docs/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Using the Norbix CLI from a coding agent

The recipe a coding agent (Claude Code, Cursor, Codex, OpenCode) follows to
work on a Norbix backend from the shell. `norbix ai init` writes a short
version of this into your project (a Claude Code skill, a `CLAUDE.md` /
`AGENTS.md` block, a Cursor rule). The guarantees behind it are in
[`agent-contract.md`](agent-contract.md).

## 0. Install

```sh
npm install -g @norbix.ai/cli # or: npx @norbix.ai/cli …
norbix --version
```

## 1. Authenticate without a terminal

Agents cannot answer prompts, so never run `norbix login` or
`norbix configure` interactively. Give the agent one of:

- a **profile** created once by a human: `norbix login --api-key nbk_… --project <id> --region nb-eu-germany --profile ci`, then every call takes `--profile ci`;
- **environment variables**: `NORBIX_API_KEY`, `NORBIX_PROJECT_ID`, `NORBIX_REGION` (and `NORBIX_ENV`, `NORBIX_ACCOUNT_ID` when needed).

First call of a session: `norbix whoami --json`. Exit 4 means no usable
credentials — stop and ask.

## 2. Always `--json`

Stdout is then exactly one JSON document: the result, or
`{"error": {code, message, status, exit, fieldErrors, url, traceId, hint, docs}}`.
Nothing else is printed on stdout; stderr carries progress and warnings.

## 3. Discover, do not guess

```sh
norbix schema --json # every command: args, flags, examples, destructive, supportsDryRun
norbix schema users delete --json # one command
norbix users delete --help # the human page, with a realistic example
norbix hub scheduler --json # SDK methods of a module with their request fields
norbix hub scheduler task delete --help
```

Prefer the named commands (`db`, `users`, `files`, `email`, `sms`, `push`,
`scheduler`, `logs`, `env`, `apikeys`, `webhooks`, `payments`, `account`).
When there is none, every SDK endpoint is reachable with plain words:

```sh
norbix hub scheduler task delete 66b2f0a1c3d4e5f6a7b8c9d0 --yes --json
norbix hub scheduler task save --body '{"name":"nightly","cronExpression":"0 2 * * *"}' --json
```

`--body` takes the whole request as JSON (nothing is merged with
`--field` flags); `--field:str value` forces a string when a value looks
like a number.

## 4. Change things in two steps

```sh
norbix db update orders --id 66b2f0a1c3d4e5f6a7b8c9d0 --update '{"$set":{"status":"shipped"}}' --dry-run --json
# inspect "http" and "request" …
norbix db update orders --id 66b2f0a1c3d4e5f6a7b8c9d0 --update '{"$set":{"status":"shipped"}}' --json
```

A **destructive** command (`destructive: true` in the schema) additionally
needs `--yes`; without it, and without a terminal, it exits 3 and sends
nothing. Never add `--yes` to a first attempt — dry-run first.

## 5. Branch on the exit code

| exit | meaning | next step |
|---|---|---|
| 0 | ok | |
| 2 | usage error | read `error.message`, fix the call (`--help`) |
| 3 | confirmation required | re-run with `--yes` after a dry run |
| 4 | not authenticated / forbidden | ask for credentials or another profile |
| 5 | not found | check the id, `--env`, `--project`; list first |
| 6 | validation | `error.fieldErrors` names the fields |
| 7 | network | check `--region`, endpoints, connectivity |
| 8 | server / rate limit | retry with backoff; keep `error.traceId` |
| 9 | cancelled | |
| 1 | internal | report with `error.traceId` |

`error.hint` says what to do; `error.docs` says where to read.

## 6. Set up a project once

```sh
cd my-project
norbix ai init # Claude Code skill + CLAUDE.md block
norbix ai init --target all # also AGENTS.md and a Cursor rule
norbix ai init --dry-run # see what it would write
```

Files are never overwritten without `--force`; an existing block is left
alone.
139 changes: 139 additions & 0 deletions docs/agent-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# The agent contract

What a script or a coding agent (Claude Code, Cursor, Codex, OpenCode) can
rely on when it runs `norbix` from a shell. Read it once; every error the CLI
prints points back here through its `hint` and `docs` fields.
`norbix schema --json` names this file too.

## 1. The rules

1. **Never hang, never act silently.** In a non-interactive shell (stdout or
stdin is not a terminal, or `CI` is set, or `--json` is passed) a command
that needs a confirmation and did not get `--yes` exits **3** with one
line on stderr that names `--yes`. It sends nothing. In a terminal
without `--yes` it asks, as before.
2. **`--json` means stdout is exactly one JSON document, always.** Success:
the result. Failure: `{"error": {...}}` (section 3). No text on stdout in
JSON mode, ever. Progress and warnings go to stderr. The exit code is the
same with and without `--json`.
3. **Errors carry everything the SDK knows** — `status`, `code`,
`fieldErrors`, `url`, `traceId` — plus a `hint` (what to do next) and
`docs` (`norbix <command> --help` or a URL).
4. **Documented exit codes** (section 2). Branch on them.
5. **`--dry-run` on every mutating command.** It resolves auth, region and
project exactly as a real call would (so a dry run that would fail for
real fails here too), prints the request that would be sent, sends
nothing, exits 0.
6. **Discoverable without reading source.** `norbix schema --json` returns
the whole command surface (args, flags, types, required, env var,
default, examples, `destructive`, `supportsDryRun`); `norbix schema
<command> --json` one command. `--help` on every command has an example
with real-looking values and, for mutating commands, the `--dry-run` /
`--yes` pattern. `norbix hub <module> --json` lists the SDK methods with
their request fields.
7. **Cheap to run.** `norbix --version` answers in well under 200 ms;
prompts and other heavy modules load only when used.
8. **No ANSI codes** with `--json`, with `NO_COLOR` set, or when stdout is
not a terminal.

## 2. Exit codes

| code | meaning | what to do |
|---|---|---|
| 0 | success | |
| 1 | unexpected / internal error | file an issue, include `traceId` |
| 2 | usage: bad flags or args, invalid JSON input, unknown command, missing config | `norbix <command> --help` / `norbix schema <command> --json` |
| 3 | confirmation required (non-interactive shell, no `--yes`) | re-run with `--yes` after a `--dry-run` |
| 4 | not authenticated / auth rejected (401, 403, no key) | `norbix login`, `--api-key`, `--profile`, `NORBIX_API_KEY` |
| 5 | not found (404) | check the id, `--env`, `--project` |
| 6 | validation rejected by the server (400 / 422, or a 200 with `isSuccess: false`) | `fieldErrors` lists the fields |
| 7 | network / timeout / endpoint unreachable | the error shows the URL; check `--region`, `api_url` / `hub_url`, connectivity |
| 8 | rate limited / server error (429, 5xx) | retry with backoff; include `traceId` |
| 9 | cancelled by the user at a prompt | |

Exit codes are identical with and without `--json`. The table is also in
`norbix schema --json` under `exitCodes`, and in code at
`src/lib/exit-codes.ts`.

## 3. The JSON error envelope

One document on stdout, nothing on stderr:

```json
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User not found",
"status": 404,
"exit": 5,
"fieldErrors": {"email": ["is required"]},
"url": "https://nb-eu-germany.api.norbix.ai/v2/membership/auth/66b2f0a1c3d4e5f6a7b8c9d0",
"traceId": "7f3c…",
"hint": "Check the id, the --env and the --project. List items first (e.g. `norbix users list --json`).",
"docs": "norbix users get --help"
}
}
```

- `code` — the gateway's or the SDK's own code when there is one
(`CM-ERRORS-FILES-016`, `NORBIX_NETWORK_ERROR`), else derived from the
category (`NOT_FOUND`, `USAGE_ERROR`, `CONFIRMATION_REQUIRED`,
`UNKNOWN_COMMAND`, `CANCELLED`, `INTERNAL_ERROR`).
- `status` — HTTP status, only when the server answered.
- `exit` — the process exit code (section 2).
- `fieldErrors` — only when the server named fields: `{field: [messages]}`.
- `url` — the URL that was called, when known (network errors always have it).
- `traceId` — when the server returned one (`traceId`, `correlationId`,
or `responseStatus.meta.correlationId`).
- `hint`, `docs` — always present.

Without `--json` the same information goes to **stderr** as text:

```
Error: User not found
code: USER_NOT_FOUND
status: 404
url: https://…/v2/membership/auth/66b2f0a1c3d4e5f6a7b8c9d0
traceId: 7f3c…
Hint: Check the id, the --env and the --project. …
Docs: norbix users get --help
```

## 4. What a dry run prints

```json
{
"dryRun": true,
"method": "api.membership.deleteUser",
"request": {"id": "66b2f0a1c3d4e5f6a7b8c9d0"},
"http": {
"method": "DELETE",
"url": "https://nb-eu-germany.api.norbix.ai/v2/membership/auth?id=66b2f0a1c3d4e5f6a7b8c9d0",
"headers": {"accept": "application/json", "authorization": "Bearer ***", "norbix-project-id": "…"},
"body": null
}
}
```

`method` is the SDK call (or a local action such as `config.set`); `http` is
the request as it would leave the machine, with the token redacted. A
command that makes several calls (`files upload`) shows the first.

## 5. Which commands confirm, which dry-run

- **Destructive** (`destructive: true` in the schema; take `--yes` and
`--dry-run`): `delete`, `stop`, `regenerate`, `rotate`, `block`,
`disable`, `remove`, `db update --many`, `db delete`, and any `hub` /
`api` method whose verb is one of those.
- **Mutating** (`supportsDryRun: true`, no confirmation): `create`,
`insert`, `update --id`, `replace`, `archive`, `unarchive`, `clone`,
`enable`, `invite`, `unblock`, `upload`, `publish`, `unpublish`,
`config set / unset`, `env use`, `logout`, `raw`.
- **Read-only**: everything else; no `--yes`, no `--dry-run`.

## 6. Shared code

`src/lib/exit-codes.ts`, `src/lib/cli-error.ts` (envelope),
`src/lib/schema.ts` (schema document) and `src/lib/request-fields.ts`
(SDK method → request fields) have no oclif dependency, so the MCP server
can import them and speak the same contract.
24 changes: 21 additions & 3 deletions docs/tasks/agent-native-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ code (parallel work), colour, new runtime dependencies.
realistic `--help` examples, hub/api parser fixes (`--body`, typed
fields, boolean traps) — **done**
3. PR 3 — startup (`oclif manifest` in build, lazy prompts), docs
(`agent-contract.md`, `AGENTS.md`), `norbix ai init`, README section — todo
(`agent-contract.md`, `AGENTS.md`), `norbix ai init`, README section — **done**

### Changes

Expand Down Expand Up @@ -397,6 +397,13 @@ code (parallel work), colour, new runtime dependencies.
| 35 mutating command files | `...BaseCommand.dryRunFlags`; explicit dry runs in `config set/unset`, `env use`, `logout`, `raw`, `files publish/unpublish` (`publicFilesRequest`) | 2 |
| every command file | realistic example values; a `--dry-run` example on every mutating command, `--yes` on every destructive one | 2 |
| `tests/schema.test.ts`, `tests/dispatch.test.ts`, `tests/agent-mode.test.ts` | schema snapshot + completeness, parser unit tests, dry-run on non-destructive / local / raw-fetch commands, `--body` | 2 |
| `bin/run.js` | `--version` fast path (answers from package.json, no oclif load) | 3 |
| `package.json` | `build` ends with `oclif manifest` (manifest stays gitignored, `postpack` still cleans it); `prepack` = build; `ai` topic | 3 |
| `src/lib/agent-files.ts` | new: the skill, the CLAUDE.md / AGENTS.md block, the Cursor rule, and the plan per target (no oclif import) | 3 |
| `src/commands/ai/init.ts` | new: `norbix ai init [--target claude\|agents\|cursor\|all] [--dry-run] [--force] [--dir]` | 3 |
| `docs/agent-contract.md`, `docs/AGENTS.md` | new: the contract (§1 + §3.1 + §3.2 + dry-run shape + which commands confirm) and the agent recipe | 3 |
| `README.md` | "Use with Claude Code / AI agents" section; `schema` and `ai init` in the command table | 3 |
| `tests/ai-init.test.ts` | dry-run lists, writes, refuses without `--force`, appends to an existing CLAUDE.md, `--force` replaces the block in place, `--target all`, `--version` string | 3 |

### Findings

Expand All @@ -421,7 +428,15 @@ code (parallel work), colour, new runtime dependencies.
"See more help with --help"); `toEnvelope` strips it.
- `--version` timings on this machine vary 0.5 s – 3.2 s between runs
(heavy load); the before/after numbers in the report use the best of
several runs.
several runs. Idle machine: oclif's own `--version` is 0.17 s with or
without a manifest (loading @oclif/core dominates), so the fast path in
`bin/run.js` answers from package.json in 0.03 s. `users delete --help`:
0.18 s without manifest → 0.12 s with.
- `@inquirer/prompts` was already lazy after PR 1 (dynamic import in
`confirmOrFail`, `login`, `configure`); no static import is left.
- `oclif manifest` choice: generated by `npm run build`, never committed
(`.gitignore` unchanged, `postpack` still removes it) — a committed
manifest goes stale with every flag change.

- The SDK's `hub2.dtos.d.ts` names some request DTOs without the `Request`
suffix (`DeleteSchedulerTask`) — the generator keys on the JSDoc
Expand All @@ -444,4 +459,7 @@ code (parallel work), colour, new runtime dependencies.

### Open questions

- (none blocking)
- Where will `docs/AGENTS.md` live for the hub/portal docs team? Not
blocking: the README links to the file in this repo; recommended default
is to publish it unchanged at docs.norbix.ai/cli/agents and keep the repo
copy as the source.
15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@
],
"scripts": {
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
"build": "npm run clean && tsc && node -e \"require('node:fs').cpSync('src/generated','dist/generated',{recursive:true})\"",
"build": "npm run clean && tsc && node -e \"require('node:fs').cpSync('src/generated','dist/generated',{recursive:true})\" && oclif manifest",
"typecheck": "tsc --noEmit",
"dev": "npm run build && node ./bin/run.js",
"prepack": "npm run build && oclif manifest",
"prepack": "npm run build",
"postpack": "node -e \"require('node:fs').rmSync('oclif.manifest.json',{force:true})\"",
"test": "vitest run",
"test:watch": "vitest",
Expand Down Expand Up @@ -81,7 +81,7 @@
"description": "Account profile, projects, team, usage, billing"
},
"ai": {
"description": "LLM and MCP server integrations, AI service users",
"description": "LLM and MCP server integrations, AI service users, and coding-agent setup (ai init)",
"subtopics": {
"llm": {
"description": "Create, test, turn on/off and delete an LLM integration"
Expand Down Expand Up @@ -111,6 +111,12 @@
"db": {
"description": "Work with database collections (find, insert, update, delete)"
},
"email": {
"description": "Email templates and campaigns"
},
"env": {
"description": "Manage project environments (PROD, TEST, ...)"
},
"files": {
"description": "Upload, download and manage files",
"subtopics": {
Expand All @@ -119,9 +125,6 @@
}
}
},
"env": {
"description": "Manage project environments (PROD, TEST, ...)"
},
"logs": {
"description": "Read project logs"
},
Expand Down
Loading
Loading