Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
202 changes: 139 additions & 63 deletions .agents/skills/xsql/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,109 +1,185 @@
---
name: xsql
description: Use when inspecting MySQL or PostgreSQL with the xsql CLI, especially for safe read-only querying, schema discovery, profile inspection, machine-readable JSON output, stable error-code handling, SSH-backed connections, or MCP server setup in this repository.
description: >
AI-first database CLI for MySQL and PostgreSQL via xsql. Use this skill whenever the user wants to
analyze, inspect, or query a database — including "分析数据库", "看下表结构", "查下数据量", "数据库概览",
"表大小排行", "数据库健康检查", "碎片化分析", "查下索引", "慢查询", "优化建议", "表关系",
schema discovery, data profiling, slow query investigation, index analysis, fragmentation check,
foreign key analysis, or any task involving SQL databases. Also use when the user mentions xsql,
database profiles, SSH-tunneled DB access, or read-only database operations. Trigger even if the
user just names a database or environment (e.g. "生产环境数据库", "dev库", "帮我看下这个库",
"staging DB") without explicitly saying "xsql". Also trigger for xsql web UI or MCP server setup.
---

# xsql

Use `xsql` as an AI-first database CLI. Prefer it when the task requires stable JSON output, explicit error codes, schema discovery, or profile-aware access to MySQL/PostgreSQL.
`xsql` is an AI-first database CLI that provides stable JSON output, explicit error codes, schema discovery, and profile-aware access to MySQL/PostgreSQL over SSH.

## Default Operating Rules

- Prefer `--format json` for agent work, even if TTY would default to table.
- Treat `stdout` as data and `stderr` as logs; never parse logs as result data.
- Check `ok`, `schema_version`, and `error.code` instead of relying on human-readable text.
- Assume read-only mode unless the user explicitly requests write behavior.
- Do not suggest or use `--unsafe-allow-write` unless the task truly requires writes and the user has made that intent explicit.
- Do not leak secrets, full DSNs, passwords, private keys, or passphrases in output, logs, or summaries.
- Treat `--ssh-skip-known-hosts-check` as a risky last resort and call out the security tradeoff if it is required.
- Use `--format json` (`-f json`) for all agent-driven work — it returns structured `ok`/`data`/`error` envelopes. YAML format (`-f yaml`) is also available with the same envelope structure.
- `stdout` is data; `stderr` is logs. Never parse stderr as result data.
- Validate responses by checking `ok`, `schema_version`, and `error.code` — not by string matching.
- Assume read-only mode. Only suggest `--unsafe-allow-write` when the user has explicitly requested writes.
- Never leak secrets, full DSNs, passwords, or private keys in output or summaries.
- `--ssh-skip-known-hosts-check` is a risky last resort — call out the security tradeoff if it's needed.

## Working Sequence

Use this sequence unless the user already knows the exact profile and SQL to run:
Follow this sequence unless the user already knows the exact profile and SQL.

1. Identify the profile with `xsql profile list --format json`.
2. Inspect the chosen profile with `xsql profile show <profile> --format json`.
3. Discover schema with `xsql schema dump -p <profile> -f json`.
4. Write a minimal read-only query against the confirmed tables and columns.
5. Run `xsql query "<SQL>" -p <profile> -f json`.
6. Validate `ok`, `schema_version`, returned columns/rows, or `error.code`.

Prefer `schema dump` over guessing table names or dialect details.

## Command Patterns
### Step 1: Resolve the profile

```bash
# List available profiles
xsql profile list --format json
```

# Inspect a profile before querying
Match the user's intent to a profile by checking each profile's `description` and `database` fields. If the user says "生产环境" or "prod", look for a profile whose description contains those keywords. If ambiguous, ask the user to choose.

Then confirm the profile details:

```bash
xsql profile show <profile> --format json
```

Note the `db` field (`mysql` or `pg`) — it determines which SQL dialect to use. Also note the `database` name for use in queries.

### Step 2: Discover schema (adapt to database size)

**Small databases (under ~20 tables):** Full dump is fine.

```bash
xsql schema dump -p <profile> -f json
```

**Medium/large databases (20+ tables):** Full dump may be too large for context. Use a two-pass approach:

Pass 1 — Get table list only (lightweight, via information_schema):

```bash
# MySQL
xsql query "SELECT TABLE_NAME, TABLE_ROWS, DATA_LENGTH, INDEX_LENGTH FROM information_schema.TABLES WHERE TABLE_SCHEMA = '<database>' ORDER BY TABLE_ROWS DESC" -p <profile> -f json

# PostgreSQL
xsql query "SELECT relname AS table_name, reltuples::bigint AS row_estimate, pg_total_relation_size(relid) AS total_bytes FROM pg_stat_user_tables ORDER BY reltuples DESC" -p <profile> -f json
```

Pass 2 — Get detailed schema only for the tables you actually need:

```bash
xsql schema dump -p <profile> -f json --table "user*"
```

Additional schema dump flags: `--include-system` to include system tables, `--schema-timeout <seconds>` to override the default 60s timeout.

# Discover schema before writing SQL
xsql schema dump --profile <profile> --format json
### Step 3: Write and run targeted queries

# Filter schema discovery when the table family is known
xsql schema dump --profile <profile> --table "user*" --format json
Write minimal, narrow queries with explicit columns, predicates, and `LIMIT`. Match SQL syntax to the profile's engine (MySQL vs PostgreSQL).

# Run a read-only query
xsql query "SELECT id, name FROM users LIMIT 10" --profile <profile> --format json
```bash
xsql query "<SQL>" -p <profile> -f json
```

# Export tool metadata for agent integration
xsql spec --format json
For long-running queries, set a timeout (default: 30s):

# Start MCP server
xsql mcp server
```bash
xsql query "<SQL>" -p <profile> -f json --query-timeout 60
```

### Step 4: Validate the response

Check the JSON envelope:
- `ok: true` → use `data.rows` and `data.columns`
- `ok: false` → inspect `error.code` and `error.message`

## Health Check Workflow

When the user asks for a database analysis or health check, run multiple analysis patterns together. Run independent queries in parallel for efficiency.

The SQL patterns for each engine are in the reference files — read only the one matching the profile's `db` field:
- **MySQL**: Read `references/mysql-patterns.md`
- **PostgreSQL**: Read `references/postgresql-patterns.md`

### Recommended health check scope

For a comprehensive health check, include these areas in order of impact:

1. **Database Overview** — table sizes and row counts, identify the biggest tables
2. **Fragmentation Analysis** — detect wasted space and performance degradation
3. **Missing Index Detection** — find tables that need better indexing
4. **Stale Table Detection** — identify potentially abandoned tables

For specific investigations, also consider:
- **Growth Trend Analysis** — when the user asks about data growth or capacity planning
- **Column Distribution Analysis** — when the user asks about data distribution or skew
- **Server Status** — for deep performance diagnostics

## Querying Guidance

- Start with schema discovery, then query.
- Keep queries narrow: explicit columns, predicates, and `LIMIT`.
- Match SQL syntax to the target engine after confirming whether the profile is MySQL or PostgreSQL.
- Use aggregation or sampling before asking for large result sets.
- Expect read-only enforcement to block writes through both SQL analysis and read-only transactions.
- Schema discovery first, query second — never guess table or column names.
- Keep queries narrow: explicit columns, WHERE predicates, and LIMIT.
- For large tables, prefer aggregation (`COUNT`, `GROUP BY`) over `SELECT *`.
- Use `--query-timeout` for long-running queries (default: 30s).
- Use `--schema-timeout` for large schema dumps (default: 60s).
- Run `xsql spec --format json` to discover all available commands and flags.

## Output And Error Handling

For machine-readable formats, expect:
JSON responses follow this envelope:

```json
{"ok":true,"schema_version":1,"data":{...}}
{"ok": true, "schema_version": 1, "data": {"columns": [...], "rows": [...]}}
```

```json
{"ok":false,"schema_version":1,"error":{"code":"...","message":"...","details":{...}}}
{"ok": false, "schema_version": 1, "error": {"code": "...", "message": "...", "details": {...}}}
```

Handle failures by `error.code` and exit status, not by fragile string matching. Important exit codes in this repo:
Handle failures by `error.code` and exit status:

| Exit Code | Meaning |
|-----------|---------|
| 0 | success |
| 2 | argument/config error |
| 3 | DB or SSH connection error |
| 4 | read-only policy blocked a write |
| 5 | database execution error |
| 10 | internal error |

- `0`: success
- `2`: argument/config error
- `3`: DB or SSH connection error
- `4`: read-only policy blocked a write
- `5`: database execution error
- `10`: internal error
Table and CSV formats are for humans — they lack the `ok`/`schema_version` envelope.

Table and CSV output are for humans; they do not include `ok` or `schema_version`.
## Additional Commands

```bash
# MCP server for AI assistant integration
xsql mcp server # stdio transport (default)
xsql mcp server --transport streamable_http # HTTP transport
xsql mcp server --transport streamable_http \
--http-addr 127.0.0.1:8787 --http-auth-token <token> # HTTP with auth

# Web UI
xsql web # start web server and open browser
xsql serve # start web server (headless, no browser)

# Configuration
xsql config init # create template config file
xsql config set profile.dev.host localhost # set a config value by dot-notation key

# Tool metadata and version
xsql spec --format json # export tool spec for AI/agents
xsql version # print version info
```

## Config And Profile Rules

- Resolve precedence as `CLI > ENV > Config`.
- Use `XSQL_`-prefixed env vars when environment configuration is needed.
- Default config lookup is `./xsql.yaml`, then `~/.config/xsql/xsql.yaml`, unless `--config` is provided.
- Prefer keyring-backed secrets. Plaintext secrets require explicit allowance.
- If no profile is passed and a `default` profile exists, xsql may use it automatically.
- Precedence: `CLI flags > ENV vars > Config file`. ENV vars use `XSQL_` prefix (e.g. `XSQL_PROFILE`, `XSQL_FORMAT`).
- Config lookup: `./xsql.yaml`, then `~/.config/xsql/xsql.yaml`.
- Prefer keyring-backed secrets. Plaintext secrets require `--allow-plaintext`.
- If no profile is specified and a `default` profile exists, xsql uses it automatically.

## SSH Rules

- Prefer the built-in SSH driver-dial path; it is the default design for MySQL/PostgreSQL.
- Use the local port-forwarding proxy mode only when the workflow explicitly needs `xsql proxy` semantics or a driver fallback.
- The built-in SSH driver-dial path is the default and preferred method.
- Use `xsql proxy` only when the workflow explicitly needs a local port-forward.
- Keep host-key verification enabled by default.
- For one-shot commands such as `query` and `schema dump`, expect fresh SSH/DB connections rather than long-lived reconnect behavior.

## If You Are Modifying xsql

- Keep CLI-layer logic in `cmd/xsql` thin.
- Keep core behavior in `internal/*`, not coupled to Cobra types.
- Preserve output contracts and stable error codes.
- Add or update tests for JSON output, exit codes, and read-only behavior.
- One-shot commands (`query`, `schema dump`) use fresh connections — don't expect long-lived sessions.
36 changes: 17 additions & 19 deletions cmd/xsql/web_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,9 @@ func TestResolveWebOptions_AddressResolution(t *testing.T) {
// Test all loopback address variations
func TestResolveWebOptions_LoopbackAddresses(t *testing.T) {
tests := []struct {
name string
addr string
shouldReq bool
name string
addr string
shouldReq bool
}{
{"IPv4 loopback", "127.0.0.1:8788", false},
{"IPv6 loopback", "[::1]:8788", false},
Expand Down Expand Up @@ -218,12 +218,12 @@ func TestResolveWebOptions_LoopbackAddresses(t *testing.T) {
// TestResolveWebOptions_TokenResolution tests token resolution priority: CLI > ENV > Config > None
func TestResolveWebOptions_TokenResolution(t *testing.T) {
tests := []struct {
name string
setEnv string
opts *webCommandOptions
cfg config.File
expectedTok string
expectErr bool
name string
setEnv string
opts *webCommandOptions
cfg config.File
expectedTok string
expectErr bool
}{
{
name: "CLI token takes priority",
Expand Down Expand Up @@ -271,10 +271,10 @@ func TestResolveWebOptions_TokenResolution(t *testing.T) {
expectedTok: "plaintext-token",
},
{
name: "Non-loopback without token fails",
setEnv: "",
opts: &webCommandOptions{addr: "0.0.0.0:8788", addrSet: true},
expectErr: true,
name: "Non-loopback without token fails",
setEnv: "",
opts: &webCommandOptions{addr: "0.0.0.0:8788", addrSet: true},
expectErr: true,
},
}

Expand Down Expand Up @@ -491,9 +491,9 @@ func TestWebCommand_AuthTokenSetFlag(t *testing.T) {
// TestRunWebCommand_ConfigLoadError tests error handling when config loading fails
func TestRunWebCommand_ConfigLoadError(t *testing.T) {
opts := &webCommandOptions{
addr: "127.0.0.1:0",
addrSet: true,
authToken: "",
addr: "127.0.0.1:0",
addrSet: true,
authToken: "",
authTokenSet: false,
}

Expand All @@ -508,7 +508,7 @@ func TestRunWebCommand_ConfigLoadError(t *testing.T) {
w := output.New(&buf, &bytes.Buffer{})

err := runWebCommand(opts, &w)

// Should return an error due to missing config file
if err == nil {
t.Error("expected error from loading invalid config path, got nil")
Expand Down Expand Up @@ -572,5 +572,3 @@ func TestRunWebCommand_ListenerCreationError(t *testing.T) {
t.Error("test timed out - runWebCommand likely blocked waiting for signals")
}
}


6 changes: 3 additions & 3 deletions internal/app/conn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,9 +367,9 @@ func TestResolveReconnectableSSH_PassphraseNotAllowed(t *testing.T) {
func TestResolveReconnectableSSH_ConnectFails(t *testing.T) {
profile := config.Profile{
SSHConfig: &config.SSHProxy{
Host: "127.0.0.1",
Port: 1, // unlikely to have SSH on port 1
User: "user",
Host: "127.0.0.1",
Port: 1, // unlikely to have SSH on port 1
User: "user",
},
}

Expand Down
Loading
Loading