Query Jira with SQL.
jai syncs your Jira Cloud data to a local SQLite database and exposes it through SQL queries, a full-screen TUI, and structured JSON output for AI agents.
$ jai sync
Synced ROX: 1,234 issues (45 new, 189 updated) in 12.3s
$ jai query "SELECT key, summary, status FROM issues WHERE assignee_email = '{{me}}' AND status_category != 'Done' ORDER BY priority"
KEY SUMMARY STATUS
ROX-4821 Fix auth token expiry bug In Progress
ROX-4756 Migrate CI to new runner pool To Do
ROX-4712 Update RBAC docs for v4.5 To Do
(3 rows)
$ jai tui
Jira's REST API returns 50KB per issue. JQL can't join tables, aggregate, or rank search results. Existing CLI tools mirror the web UI's complexity and emit unstructured, token-heavy output that's expensive for AI agents to parse.
jai takes a different approach: sync once to a local SQLite database, then query instantly — with full SQL, no round trips, no API tokens burned per read.
- 10–50× fewer tokens for AI agents. Select exactly the fields you need.
- Instant queries from local SQLite. No waiting for the API.
- Full SQL power — JOINs, aggregations, CTEs, window functions, FTS5. Things JQL will never do.
- Works offline. Writes queue locally and sync to Jira when you're back online.
- Full-screen TUI that replaces the Jira web UI for daily workflows.
brew install sthadka/tap/jaiOr build from source (requires Go 1.23+ and CGO):
git clone https://github.com/sthadka/jai
cd jai
make install# 1. Set your API token
export JAI_TOKEN=your-jira-api-token
# 2. Run the setup wizard
jai init
# 3. Query
jai query "SELECT key, summary, status FROM issues LIMIT 10"
# 4. Launch the TUI
jai tuijai init walks through connection setup, project selection, and runs the first sync. Takes under 5 minutes.
jai stores every Jira issue as a row with denormalized columns. Query it like any database.
Your open work, sorted by priority:
jai query "
SELECT key, summary, status, priority
FROM issues
WHERE assignee_email = '{{me}}'
AND status_category != 'Done'
ORDER BY priority"Bug velocity — opened vs closed per week:
jai query "
WITH opened AS (
SELECT strftime('%Y-%W', created) AS week, COUNT(*) AS n
FROM issues WHERE type = 'Bug' GROUP BY week
),
closed AS (
SELECT strftime('%Y-%W', resolved) AS week, COUNT(*) AS n
FROM issues WHERE type = 'Bug' AND status = 'Done' GROUP BY week
)
SELECT o.week, o.n AS opened, COALESCE(c.n, 0) AS closed
FROM opened o LEFT JOIN closed c ON o.week = c.week
ORDER BY o.week DESC LIMIT 12"Issues with comments mentioning a topic:
jai query "
SELECT i.key, i.summary, c.author, c.body
FROM issues i
JOIN comments c ON i.key = c.issue_key
WHERE c.body LIKE '%memory leak%'"Stale issues by team, ranked:
jai query "
SELECT key, summary, team, updated,
ROW_NUMBER() OVER (PARTITION BY team ORDER BY updated ASC) AS staleness_rank
FROM issues
WHERE status_category = 'In Progress'"Full-text search with ranking:
jai search "authentication token expired"Every command supports --json for compact, structured output and --fields to select specific columns.
# Get a single issue
jai get ROX-4821 --json --fields key,summary,status,assignee{"ok":true,"data":{"key":"ROX-4821","summary":"Fix auth token expiry bug","status":"In Progress","assignee":"Jane Doe"}}# Run a query
jai query "SELECT key, summary, status FROM issues WHERE type = 'Bug' LIMIT 3" --json{"ok":true,"columns":["key","summary","status"],"rows":[["ROX-4821","Fix auth token expiry","In Progress"],["ROX-4756","Null pointer in scheduler","To Do"],["ROX-4701","Race condition in cache","Done"]],"count":3}Errors are structured too:
{"ok":false,"error":{"type":"QueryError","message":"no such column: statuss — did you mean 'status'?"}}Agents can discover the full command surface without documentation:
jai schema get{"command":"get","params":{"key":{"type":"string","required":true,"description":"Issue key (e.g. ROX-123)"}},"flags":{"json":{"type":"bool"},"fields":{"type":"string","description":"Comma-separated field names"}}}jai tui opens a full-screen terminal UI powered by local SQLite — instant, smooth, works offline.
Features:
- Tab-based navigation between named views
- Sortable, scrollable, filterable table (
/to filter,sto sort) - Issue detail pane with full field list (
Enterto open) - Group by any column (
g) - Color rules — stale items, blockers, priorities highlighted automatically
- Status summary bar — "12 To Do | 8 In Progress | 3 Done"
- Background sync with live refresh indicator
- Quick actions:
eedit field,ccomment,oopen in browser - Vim-style navigation (
j/k,gg/G,Ctrl-d/Ctrl-u)
Views are defined in YAML and shared between TUI tabs and jai view <name>:
views:
- name: my-work
title: My Work
query: |
SELECT key, summary, status, priority
FROM issues
WHERE assignee_email = '{{me}}'
AND status_category != 'Done'
ORDER BY priority DESC, updated DESC
columns: [key, summary, status, priority]
status_summary: true
color_rules:
- field: priority
condition: equals
value: Blocker
color: "#dd4444"
- name: stale-bugs
title: Stale Bugs
query: |
SELECT key, summary, assignee, updated
FROM issues
WHERE type = 'Bug'
AND status_category = 'In Progress'
AND updated < datetime('now', '-28 days')
ORDER BY updated ASC
columns: [key, summary, assignee, updated]
color_rules:
- field: updated
condition: older_than
value: 56d
color: "#dd4444"
- field: updated
condition: older_than
value: 28d
color: "#e1c233"Changes queue locally and sync to Jira on the next jai push or background sync cycle.
# Create a new issue (hits Jira API directly, returns key immediately)
jai create ROX --type Bug --summary "Login fails on SSO" --priority High --labels backend,auth
# → ✓ Created ROX-4901: Login fails on SSO
# Create with all the bells and whistles
jai create ROX --type Story \
--summary "Add search" \
--description "Implement full-text search across all fields" \
--parent ROX-100 \
--labels backend,search \
--components Platform \
--priority Medium \
--assignee user@example.com \
--fix-version v4.6 \
--due-date 2025-03-15 \
--field customfield_10001='{"value":"Team Alpha"}' \
--json
# → {"ok":true,"data":{"key":"ROX-4902","id":"12345","project":"ROX","status":"created"}}
# Update a field
jai set ROX-4821 status "In Progress"
# → ROX-4821: status → "In Progress" (pending sync)
# Add a comment
jai comment ROX-4821 "Fixed in PR #4892, deploying to staging"
# → ROX-4821: comment added (pending sync)
# Push all pending changes
jai push
# → ✓ ROX-4821: status → "In Progress"
# → ✓ ROX-4821: comment added
# → 2 succeeded, 0 failed# ~/.config/jai/config.yaml
jira:
url: https://mycompany.atlassian.net
email: me@company.com
token: ${JAI_TOKEN} # never store tokens in plaintext
sync:
interval: 15m # auto-sync interval
rate_limit: 10 # requests/second (Jira Cloud limit)
me: me@company.com # used in {{me}} template variable
sync_sources:
- name: my-project
jql: project = MYPROJ ORDER BY updated DESC
- name: another-team
jql: project = OTHER AND team = "Platform"
fields:
overrides:
customfield_12345: team # override auto-inferred field namesjai init generates this file interactively.
Data files:
| File | Default path |
|---|---|
| Config | ~/.config/jai/config.yaml |
| Database | ~/.local/share/jai/jai.db |
Both paths can be overridden with --config and --db flags, or by setting db.path in the config file.
Custom fields are auto-discovered from Jira's field metadata API during the first sync. Run jai fields to see all available columns with their Jira IDs, types, and FTS flags. Override any name in config if the auto-inferred name isn't right.
| Command | Description |
|---|---|
jai init |
Interactive setup wizard |
jai sync |
Incremental sync from Jira |
jai sync --full |
Full resync with deletion detection |
jai query <sql> |
Execute SQL against local DB |
jai get <key> |
Fetch a single issue |
jai search <text> |
FTS5 full-text search |
jai view <name> |
Run a named view |
jai fields |
List available fields and mappings |
jai schema <command> |
Command schema for agents |
jai status |
Sync status and pending changes |
jai create <project> |
Create a new issue |
jai set <key> <field> <value> |
Update an issue field |
jai comment <key> <text> |
Add a comment |
jai push |
Push pending changes to Jira |
jai tui |
Launch full-screen TUI |
Global flags: --json, --fields, --no-sync, --config, --db
Requires Go 1.23+ and CGO (for SQLite FTS5 support).
git clone https://github.com/sthadka/jai
cd jai
make build # builds ./jai
make test # runs all tests
make install # installs to $GOPATH/binFor cross-compilation (linux from macOS), install zig and use goreleaser:
goreleaser build --snapshot --cleanMIT — see LICENSE.