Skip to main content
Glama
README.md
# issuehub

`issuehub` is a standalone, provider-agnostic issue-tracker CLI. It caches,
indexes, syncs, searches, and exports issues from Jira, Redmine, GitHub, and
GitLab into a plain-file workspace that any tool can read. Attachment
*metadata* travels with each issue; bytes download only on explicit request
(`issuehub attachments --fetch`/`--all`), and always land under `.state/`,
never the workspace — see `docs/FORMATS.md` §Attachments.

The workspace also carries a per-issue memo, metadata, and (produced by an AI
backend on the other side of this CLI, never by this CLI itself)
`translations/<lang>.md` files — this CLI reads and full-text-indexes those,
and validates a language tag before it ever becomes a filename, but does not
generate them (see `docs/FORMATS.md` §Translations).

**If you keep your workspace in a shared or public repository:** translated
issue titles/descriptions, and analysis responses, are Git-tracked by
design (so a translation can be hand-corrected and an analysis kept for
reference) — so issue content you translate or analyze ends up in that
repository's history, not only in the local, git-ignored cache. Optionally
including comments in a translation also carries commenter names into the
tracked file. None of this is a bug; it just means "private tracker" and
"public repository" are two different choices, and it is worth picking the
one you mean. See `docs/FORMATS.md` §What lands in Git, and what leaves the
machine for the full picture.

## Design stance

- **One invocation per command.** No daemon, no persistent process. `issuehub`
  reads its inputs, does the work, prints a result, and exits.
- **Zero required external binaries.** HTTP is in-process (`httpx`) and the
  index uses the stdlib `sqlite3` module, not a `sqlite3` CLI. `git` is purely
  informational (the workspace is *designed* to be git-managed, not required
  to be) and `rg` is optional — without it, `search` falls back to a
  pure-Python scan, so its absence is a warning, never an error.
- **Machine-readable by default when asked.** Every verb has a `--json` mode
  producing stable, documented output, so `issuehub` composes into scripts,
  CI, and other tools without scraping human-oriented text.
- **The on-disk workspace is a published interface**, not an implementation
  detail — other tools may read and write the same files, so its format is a
  compatibility contract (see `docs/FORMATS.md`).
- **Credentials never reach argv, logs, or disk.**

## Install

```sh
pip install issuehub-cli
```

The PyPI distribution is named `issuehub-cli`, but the import package and the
console script are both still `issuehub` — that split is intentional and
common (the package you `pip install` need not match the name you `import`
or run), so after installing you still `import issuehub` and run
`issuehub <verb>`.

or, from a checkout:

```sh
pip install -e .
```

Requires Python 3.10+. See `pyproject.toml` for the optional `analysis` extra
(`pandas`-backed export aggregates), the optional `mcp` extra (`fastmcp`-backed
MCP server, `issuehub mcp serve` — see `docs/MCP.md`), and the `dev` extra
(test tooling).

## Verification status

This CLI has been run against a **live GitHub instance**, which surfaced four
real bugs (now fixed — see `docs/CORRECTNESS.md`). The Jira, Redmine, and
GitLab providers, by contrast, have **only** been exercised against recorded
payloads in the shared conformance corpus (`corpus/`) — they have not been
run against a live Jira, Redmine, or GitLab instance. If you're picking this
tool for one of those three providers, treat it as conformance-tested but not
yet field-tested, and please report anything that doesn't match a live
server's behavior.

## Usage

Point `issuehub` at a workspace directory (`--workspace <path>` or
`$ISSUEHUB_WORKSPACE`) and, if you have provider credentials, a config file
(`--config <path>`, `$ISSUEHUB_CONFIG`, or `<workspace>/.issuehub/config.yaml`
— see `docs/CONTRACT.md` §Config for the full shape):

```yaml
# ~/notes/issuehub/.issuehub/config.yaml
workspace: ~/notes/issuehub

providers:
  jira:
    type: jira
    url: https://your-org.atlassian.net
    user: you@example.com
    token_env: JIRA_TOKEN
```

```sh
export ISSUEHUB_WORKSPACE=~/notes/issuehub

issuehub health --json                 # readiness, no network I/O
issuehub reindex --json                # rebuild the search index from the cache
issuehub list --provider jira --json   # query the provider (one page by default)
issuehub get jira://PROJ-123 --json    # one complete issue, cached unless --refresh
issuehub sync --json                   # re-fetch known issues, report what changed
issuehub fetch --provider jira --json  # page a whole query into the cache, resumably
issuehub search "login bug" --json     # local full-text search (cache + notes)
issuehub search 認証 --json             # non-ASCII queries route around FTS5's tokeniser gap
issuehub changed --json                # issues that moved since you last opened them
issuehub collection add sprint1 jira://PROJ-1 jira://PROJ-2
issuehub collection show sprint1 --json
issuehub export --source all --format csv -o issues.csv
issuehub import issues.csv --dry-run --json  # merge edits back after spreadsheet triage
issuehub summarize --source all --by status --json
issuehub attachments jira://PROJ-123 --json        # list attachment metadata (no network)
issuehub attachments jira://PROJ-123 --fetch 10001  # download one attachment's bytes
issuehub attachments jira://PROJ-123 --all --json  # download every not-yet-downloaded attachment
```

Run `issuehub --help` (or see `docs/CONTRACT.md`) for the full verb list,
arguments, and exit-code conventions.

## MCP server

`issuehub mcp serve` runs an MCP (Model Context Protocol) server over stdio
— the same verbs above, reached as tools/resources by an agent or any
MCP-aware host instead of shelling out to argv. Needs the optional `mcp`
extra:

```sh
pip install "issuehub-cli[mcp]"
issuehub mcp serve --workspace ~/notes/issuehub
```

Full design (stateless-by-construction, `import`'s consent guardrails, why
`fetch`'s progress becomes MCP notifications instead of stdout JSONL) is in
`docs/MCP.md`.

## Corporate networks (proxy / TLS / client certs)

The full field list lives in `docs/CONTRACT.md` §Config; these are the three
situations people actually hit on their first run.

**Behind an authenticating proxy:**
```yaml
http:
  proxy: http://proxy.corp.example:8080
  proxy_user: "DOMAIN\\you"
  proxy_password_env: PROXY_PASSWORD   # or proxy_password_cmd / proxy_password
  proxy_auth: basic                    # the only scheme this CLI supports — see below
  no_proxy: "localhost,.internal"      # or "*" to bypass the proxy entirely for one provider
```
`proxy_auth` only accepts `"basic"` — this CLI talks HTTP in-process (`httpx`)
rather than shelling out to `curl`, so it never inherited curl's
NTLM/negotiate/digest support. Any other value is a loud validation error at
config load, never a silent fallthrough to a scheme it can't actually
perform. `no_proxy` also honours `$NO_PROXY` from the environment, and can be
set per-provider (`providers.<name>.http.no_proxy`) to override the global
value for just that one.

**Behind an internal CA:**
```yaml
http:
  cacert: ~/certs/root.pem   # one CA bundle file
  # or, if your CA is distributed as a hashed directory (e.g. /etc/ssl/certs):
  capath: /etc/ssl/certs
```
Use `cacert` when you have a single root/intermediate bundle file; use
`capath` when your CA is distributed as a hashed certificate *directory*
instead — the shape corporate CA distribution often takes. `ssl_verify:
false` also exists as an escape hatch, but it is deliberately noisy (it
prints a stderr warning on every use) and points you back at `cacert`/
`capath` as the real fix — reach for it only to unblock yourself
temporarily, not as the answer.

**Needing a client certificate:**
```yaml
http:
  client_cert: ~/certs/client.pem
  client_key: ~/certs/client.key
  client_key_password_env: CLIENT_KEY_PASSWORD  # or client_key_password_cmd / client_key_password
```

For any of these, a literal value, a `*_cmd` (an argv list, stdout trimmed),
and a `*_env` (an environment variable name) are all accepted for the
password/secret fields, resolved in that order — literal wins if present.

**Check it actually took effect:** `issuehub health --json` reports a
`network` section (proxy/ssl_verify/cacert in effect) and, per provider,
whether its credential *resolves* — never the value itself. That turns "did
my proxy config work?" into one command instead of a failed request.

## Documentation

The full spec of record lives in [`docs/`](docs/README.md):

- [`docs/CONTRACT.md`](docs/CONTRACT.md) — the CLI interface: verbs, JSON, exit codes.
- [`docs/FORMATS.md`](docs/FORMATS.md) — workspace and `.state/` on-disk formats.
- [`docs/PROVIDERS.md`](docs/PROVIDERS.md) — per-provider specifics.
- [`docs/CORRECTNESS.md`](docs/CORRECTNESS.md) — the ledger of hard-won correctness decisions.
- [`docs/PLAN.md`](docs/PLAN.md) — implementation plan and golden-fixture testing method.
- [`docs/CONFORMANCE.md`](docs/CONFORMANCE.md) — the shared conformance corpus this
  repo and the reference implementation both run, and how to add a case.
- [`docs/MCP.md`](docs/MCP.md) — the MCP stdio server (`issuehub mcp serve`).

## Development

The shared conformance corpus under `corpus/` (golden fixtures and recorded
provider payloads) is committed, so `pytest` runs standalone without any
external tooling. See `tools/README.md` if you need to regenerate the
harvested fixtures from the reference implementation — that path is dev-only
and not required to run the test suite.

Maintenance

ActivityStale
ResponsivenessNo issues