Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1964163
docs: add agent CLI specification
matej21 Aug 4, 2026
70252f8
feat: add shared types for the agent CLI
matej21 Aug 4, 2026
d9f81e4
refactor: extract transport-agnostic RPC dispatch
matej21 Aug 4, 2026
b4c2b24
feat: engine-enforced read-only database sessions
matej21 Aug 4, 2026
3155d1e
feat: add write proposals and the agent/ui RPC surface
matej21 Aug 4, 2026
8fbef75
feat: serve a local control endpoint for the CLI
matej21 Aug 4, 2026
106b5ee
feat: approve agent write proposals in the app
matej21 Aug 4, 2026
c3f1882
refactor: split the UI snapshot publisher from its pure mapping
matej21 Aug 4, 2026
0bbd43e
feat: add the dotaz CLI client
matej21 Aug 4, 2026
6cd71bc
docs: add the dotaz agent skill and document the CLI package
matej21 Aug 4, 2026
0d97fc2
docs: describe the approval-gated write path, not just reads
matej21 Aug 4, 2026
a679beb
feat: cap read-only sessions with an engine statement timeout
matej21 Aug 4, 2026
3ad93b8
fix: one control endpoint per instance, not per user
matej21 Aug 4, 2026
6db7a7a
fix: cancel abandoned queries, push --limit into SQL, add bookmarks
matej21 Aug 4, 2026
4ad08ba
fix: wire --instance and document what shutdown actually cleans up
matej21 Aug 4, 2026
67d1e0a
fix: invalidate an approval banner whose proposal is already gone
matej21 Aug 4, 2026
1b7b1dc
fix: make read-only sessions survive statements that try to revoke them
matej21 Aug 5, 2026
6b6911e
fix: enforce the CLI's read-only guarantee at the endpoint, not in th…
matej21 Aug 5, 2026
f646c9f
fix: resolve a proposal only when the proposed SQL is what ran
matej21 Aug 5, 2026
d5742de
fix: stop --quiet hiding truncation, and --timeout meaning two things
matej21 Aug 5, 2026
666140a
docs: say what actually enforces the read-only invariant
matej21 Aug 5, 2026
4aa3fe3
fix: make agent sessions backend-owned
matej21 Aug 25, 2026
e17a0bc
feat: publish the agent CLI on npm
matej21 Aug 25, 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
85 changes: 85 additions & 0 deletions .claude/skills/dotaz/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
name: dotaz
description: Read databases and submit writes for approval through the running Dotaz app via the `dotaz` CLI. Use when you need to inspect a database the user has configured in Dotaz — list tables, read schema, sample rows, run SELECTs — to change data (submit the SQL with `dotaz propose` and the user approves it in the app), or to open a table or SQL console in their app window. Triggers include "look at the database", "co je v tabulce X", "run this query", "show me the schema", "uprav ten záznam", "fix this row", "open this in Dotaz".
---

# Dotaz CLI

`dotaz` attaches to the user's running Dotaz desktop app and reuses its configured
connections. You never need credentials — the app already has them.

## Before anything else

```bash
dotaz status
```

Exit code 5 means the app is not running, or CLI access is off. Tell the user to launch
Dotaz and enable **Settings → Allow CLI access**. Do not try to connect to their database
another way.

## Reading data

Navigate with paths — `connection/database/schema/table`:

```bash
dotaz ls # connections
dotaz ls prod # databases
dotaz ls prod/app/public # tables
dotaz describe prod/app/public/orders # columns, PK, indexes, FKs both directions
dotaz rows prod/app/public/orders --where "status='new'" --limit 20
dotaz query prod "SELECT count(*) FROM orders WHERE created_at > $1" --param 2024-01-01
```

Rules that matter:

- **Always bound your reads.** `rows` and `query` cap output, but a query that scans a huge
table still costs the user time. Add `--limit`, and prefer `describe` over `SELECT *` when
you only need shape.
- **Parameterise.** Use `--param` instead of pasting values into SQL.
- `--json` when you need to parse the result; the default table output is for humans.
- Truncation is always reported, but where depends on the format: `table`/`md` on the last
line of stdout, `--json` in the `truncated`/`shown`/`total` fields, `csv`/`jsonl` on stderr.
`--quiet` never hides it. Do not conclude "the table has 20 rows" from a truncated result.

## Writes need the user

The CLI session is read-only at the database level. An INSERT/UPDATE/DELETE/DDL exits with
code 4. That is not a bug to route around — propose it instead:

```bash
dotaz propose prod "UPDATE orders SET status='paid' WHERE id=42" --reason "user asked to mark order 42 paid"
```

This opens the SQL in the user's app with Run/Reject buttons. `dotaz approvals wait <id>`
blocks until they decide (exit 7 = still pending, 8 = rejected). Tell the user you are
waiting on their approval rather than silently polling.

Never try to get around the read-only session — no `--param` injection, no DDL disguised as
a read, no asking the user for direct database credentials.

## Driving the app

```bash
dotaz ui state # what the user currently has open
dotaz ui open prod/app/public/orders # open a data grid tab
dotaz ui console prod --sql "SELECT …" # open a SQL console, prefilled
```

`ui state` before `ui open` is usually worth it — it tells you which connection and database
the user is actually working in, so you can act in their context instead of guessing.

## Exit codes

| Code | Meaning |
| --- | --- |
| 0 | success |
| 2 | usage error |
| 3 | database error |
| 4 | read-only violation — use `dotaz propose` |
| 5 | Dotaz not running or CLI access disabled |
| 6 | timeout |
| 7 | proposal still pending |
| 8 | proposal rejected |

Full reference: `docs/agent-cli.md` in the dotaz repo.
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,15 @@ jobs:
- name: Start docker-compose services
run: docker compose up -d --wait

# readonly-session also runs in the `check` job, where its PostgreSQL and MySQL blocks
# skip for want of docker. It has to run here too, or engine-enforced read-only — the
# guarantee the agent CLI rests on — is never exercised in CI for anything but SQLite.
- name: Test (integration — requires docker)
env:
DOTAZ_REQUIRE_DB: '1'
run: |
shopt -s extglob
bun test tests/@(pg-*|postgres-*|mysql-*|multi-database|driver-iterate).test.ts
bun test tests/@(pg-*|postgres-*|mysql-*|multi-database|driver-iterate|readonly-session).test.ts

- name: Show docker-compose logs on failure
if: failure()
Expand Down
47 changes: 44 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,8 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max

# ── npm package (trusted publisher — no NPM_TOKEN needed)
npm:
# ── Server npm package (trusted publisher) ──────────────
npm-server:
needs: prepare
runs-on: ubuntu-latest
permissions:
Expand Down Expand Up @@ -264,9 +264,50 @@ jobs:
run: npm publish --tag ${{ needs.prepare.outputs.npm-tag }} --access public --provenance
working-directory: dist-server

# ── Agent CLI npm package (trusted publisher) ────────────
npm-cli:
needs: prepare
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ env.TAG }}

- uses: oven-sh/setup-bun@v2

- uses: actions/setup-node@v4
with:
node-version: '24.x'
registry-url: 'https://registry.npmjs.org'

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Build agent CLI package
run: bun run build:agent-cli
env:
VERSION: ${{ needs.prepare.outputs.version }}

- name: Smoke-test agent CLI package
working-directory: dist-agent-cli
run: |
test "$(./bin/dotaz.js --version)" = "dotaz ${{ needs.prepare.outputs.version }}"
./bin/dotaz.js --help > /dev/null
PACKAGE_TARBALL=$(npm pack --silent)
TEST_DIR=$(mktemp -d)
npm install --prefix "$TEST_DIR" "$PWD/$PACKAGE_TARBALL"
test "$(cd "$TEST_DIR" && bunx --no-install @dotaz/cli --version)" = "dotaz ${{ needs.prepare.outputs.version }}"

- name: Publish to npm
run: npm publish --tag ${{ needs.prepare.outputs.npm-tag }} --access public --provenance
working-directory: dist-agent-cli

# ── GitHub Release ────────────────────────────────────────
release:
needs: [prepare, desktop, docker, npm]
needs: [prepare, desktop, docker, npm-server, npm-cli]
runs-on: ubuntu-latest
permissions:
contents: write
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules
dist/
dist-agent-cli/
dist-server/
dist-electron/
build/
Expand Down
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ bun run build:canary
# Production build (web server)
bun run build:server

# Production build (agent CLI npm package)
bun run build:agent-cli

# Type checking (must pass with zero errors)
bunx tsc --noEmit

Expand Down Expand Up @@ -81,6 +84,7 @@ src/
backend-desktop/ ← Electrobun backend entry point
backend-web/ ← HTTP/WebSocket server entry point
cli/ ← CLI entry point (bunx @dotaz/server)
cli-agent/ ← `dotaz` agent CLI — attaches to the running desktop app (docs/agent-cli.md)
frontend-shared/ ← Solid.js UI: components, stores, lib (transport/storage registries)
frontend-desktop/ ← Desktop entry: setTransport(electrobun) + setStorage(rpc)
frontend-web/ ← Web entry: setTransport(websocket) + setStorage(indexeddb)
Expand All @@ -100,6 +104,7 @@ frontend-demo ← frontend-shared + backend-shared (runtime — createHan
backend-desktop ← backend-shared
backend-web ← backend-shared
cli ← backend-web (starts server with CLI argument parsing)
cli-agent ← shared (talks to a running app over HTTP — no backend imports)
```

### Transport & storage — registration pattern
Expand Down Expand Up @@ -131,6 +136,7 @@ This triggers the release workflow which:
- Builds desktop apps for 5 platforms (Linux x64/ARM64, macOS x64/ARM64, Windows x64)
- Publishes Docker image to `ghcr.io/contember/dotaz`
- Publishes `@dotaz/server` npm package
- Publishes `@dotaz/cli` npm package
- Creates GitHub Release with all artifacts

Pre-release tags (containing `-beta`, `-alpha`, `-rc`) get `canary` electrobun env and `beta` npm tag.
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ A lightweight Bun HTTP server you can self-host or run via Docker. Like [Adminer

**Navigation** — Connection tree with databases, schemas, and tables. Schema viewer showing columns, indexes, and foreign keys. Command palette, query history, saved views, bookmarks, cross-table search. Dark theme throughout.

**Agent CLI** — A `dotaz` command-line client that attaches to the running app so an AI coding agent can work with your databases without ever holding your credentials. Reads run directly against a session the database itself enforces as read-only. Writes go through you: the agent submits the SQL, it opens in the app with Run/Reject, and only your click executes it. Off by default — see [docs/agent-cli.md](docs/agent-cli.md).

## Install

### Desktop app
Expand Down Expand Up @@ -75,6 +77,17 @@ Request isolation depends on how the server is bound:
- **Non-loopback (e.g. `--host 0.0.0.0`)** — `DOTAZ_ENCRYPTION_KEY` is required so saved credentials remain decryptable across restarts. Dotaz still rejects cross-site browser requests, but it does not implement user authentication; put it behind your own auth/proxy if the URL is not trusted.
- **Behind a reverse proxy** — serve the UI and `/rpc`/`/api` under the same origin. Keep the loopback bind (default); if the proxy preserves a public `Host` header, allow that host with `DOTAZ_ALLOWED_HOSTS`.

### Agent CLI

With the desktop app running and CLI access enabled in Settings:

```sh
bunx @dotaz/cli status
bunx @dotaz/cli rows local/users --limit 20
```

The CLI reads through backend-owned read-only sessions. Writes are submitted to the desktop app for explicit approval.

### Docker

```sh
Expand Down
16 changes: 14 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading