Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3781ad8
docs: add agent CLI specification
matej21 Aug 4, 2026
bafa3d9
feat: add shared types for the agent CLI
matej21 Aug 4, 2026
c02599c
refactor: extract transport-agnostic RPC dispatch
matej21 Aug 4, 2026
c25b9ed
feat: engine-enforced read-only database sessions
matej21 Aug 4, 2026
cfdd035
feat: add write proposals and the agent/ui RPC surface
matej21 Aug 4, 2026
14bc126
feat: serve a local control endpoint for the CLI
matej21 Aug 4, 2026
5e0834a
feat: approve agent write proposals in the app
matej21 Aug 4, 2026
3bfe647
refactor: split the UI snapshot publisher from its pure mapping
matej21 Aug 4, 2026
8a8b608
feat: add the dotaz CLI client
matej21 Aug 4, 2026
3ace25e
docs: add the dotaz agent skill and document the CLI package
matej21 Aug 4, 2026
aa9615b
docs: describe the approval-gated write path, not just reads
matej21 Aug 4, 2026
f76a06c
feat: cap read-only sessions with an engine statement timeout
matej21 Aug 4, 2026
d7be653
fix: one control endpoint per instance, not per user
matej21 Aug 4, 2026
212c1ab
fix: cancel abandoned queries, push --limit into SQL, add bookmarks
matej21 Aug 4, 2026
a7a7515
fix: wire --instance and document what shutdown actually cleans up
matej21 Aug 4, 2026
5dca4e3
fix: invalidate an approval banner whose proposal is already gone
matej21 Aug 4, 2026
cdbf2a5
fix: make read-only sessions survive statements that try to revoke them
matej21 Aug 5, 2026
af49a78
fix: enforce the CLI's read-only guarantee at the endpoint, not in th…
matej21 Aug 5, 2026
e0ecd87
fix: resolve a proposal only when the proposed SQL is what ran
matej21 Aug 5, 2026
65cda15
fix: stop --quiet hiding truncation, and --timeout meaning two things
matej21 Aug 5, 2026
f42f0fb
docs: say what actually enforces the read-only invariant
matej21 Aug 5, 2026
26954bd
fix: make agent sessions backend-owned
matej21 Aug 25, 2026
0f31dd3
feat: publish the agent CLI on npm
matej21 Aug 25, 2026
a837a03
fix: close the two holes in the where-fragment scanner
matej21 Sep 8, 2026
60fab01
fix: reject multi-statement SQL in read-only agent sessions
jonasnobile Sep 12, 2026
76f7259
fix: stop the CLI control server timing out slow requests
jonasnobile Sep 12, 2026
4b7bb2e
fix: close the remaining ways agent SQL could write without approval
matej21 Nov 2, 2026
1d59703
fix: claim a proposal before its SQL runs
matej21 Nov 2, 2026
c9f967d
fix: report whether a CLI query cancel actually stopped anything
matej21 Nov 2, 2026
035b977
fix: parse CLI endpoint params where they enter
matej21 Nov 2, 2026
bc50d8e
chore: drop the control server's type cast and a raw NUL byte
matej21 Nov 2, 2026
2dea90b
fix: fail closed on three more scanner and engine lexer disagreements
matej21 Nov 2, 2026
e746ce7
fix: close the proposal paths that skipped the claim
matej21 Nov 2, 2026
33c5c5b
fix: fail closed on MySQL executable comments and SQLite Tcl variables
matej21 Nov 2, 2026
9e02747
fix: refuse multi-statement EXPLAIN and multibyte charsets in read-on…
matej21 Nov 2, 2026
b21bda7
fix: keep a claimed run's outcome, and refuse pieces of a cancelled p…
matej21 Nov 2, 2026
dd32e5f
fix: keep a reconnected MySQL read-only session read-only
matej21 Nov 2, 2026
9f187ef
fix: fail closed on SQLite Tcl variables whose name contains `$`
matej21 Nov 2, 2026
3863280
fix: keep a formatted cancelled proposal refused
matej21 Nov 2, 2026
2cecc87
fix: keep reconnected read-only sessions read-only inside transactions
matej21 Nov 2, 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
88 changes: 88 additions & 0 deletions .claude/skills/dotaz/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
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 open --where` and `ui console --sql` only prefill — the user applies or runs them. To read
data yourself use `dotaz query`; to change it, `dotaz propose`.

`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 @@ -58,10 +58,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 @@ -243,8 +243,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 @@ -274,9 +274,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 @@ -92,6 +95,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 @@ -111,6 +115,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 @@ -142,6 +147,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