Metadata-Version: 2.5
Name: project-hub
Version: 0.18.1
Summary: Multi-repo workspace manager with MCP server + web dashboard
Project-URL: Repository, https://gitlab.com/b.rajaut/project-hub
Author-email: Baptiste Rajaut <baptiste@rajaut.fr>
License-Expression: Unlicense
License-File: LICENSE
Keywords: dashboard,git,gitlab,mcp,workspace
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Version Control :: Git
Requires-Python: >=3.11
Requires-Dist: aiosqlite>=0.20
Requires-Dist: atlassian-python-api>=3.41
Requires-Dist: click>=8.1
Requires-Dist: fastapi>=0.115
Requires-Dist: fastmcp<4,>=3.1.1
Requires-Dist: gql[requests]<5,>=4
Requires-Dist: jinja2>=3.1
Requires-Dist: keyring>=25.0
Requires-Dist: mcp>=1.26.0
Requires-Dist: pygithub>=2.1
Requires-Dist: python-gitlab>=8.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: requests>=2.32.5
Requires-Dist: uvicorn[standard]>=0.30
Description-Content-Type: text/markdown

# <img src="https://gitlab.com/b.rajaut/project-hub/-/raw/main/project_hub/web/static/logo.svg" width="32" height="32" align="absmiddle"> project-hub

Multi-repo workspace manager that unifies local git state and forge data (MRs, issues, pipelines, vulnerabilities) into a single **MCP server + web dashboard**.

Designed for engineers managing many repos under one parent directory — across multiple forges.

![Dashboard](https://gitlab.com/b.rajaut/project-hub/-/raw/main/.gitlab/dash.jpg)

## Killer features

*aka why I built this*

**GitLab, GitHub, Bitbucket, Forgejo in a single interface.** No more context-switching between forges. project-hub reads each repo's remote, recognizes the public forges and any host with the forge in its name, and asks you about the rest once. Then it talks to all of them — MRs, issues, pipelines, everything in one dashboard. A repo on Bitbucket, another on GitLab, a third on your team's Forgejo: same view, same tools, same agent.

**Vulnerabilities that make sense.** GitLab's vulnerability report is per-repo, noisy, and doesn't tell you what's already being fixed. project-hub aggregates vulns across all your repos, cross-references them with open MR branches to flag what's already fixed but not yet merged ("fixed in branch"), and lets you filter by severity, state, or hide already-handled ones. You see what actually needs work, not what's already in flight.

**Stale branch detection.** After merging an MR you often forget to switch back to the default branch locally. project-hub detects repos still sitting on stale feature branches and shows ahead/behind relative to the default branch — with a one-click checkout to clean up.

**Events, not inbox.** Stop reminding yourself to check your cluttered GitLab inbox. project-hub diffs state on every refresh cycle and surfaces what changed: review requested from you, MR approved or merged, new comment or push, issue opened/closed/assigned, pipeline failed or fixed, new or resolved vuln, stale branch, auth expiry. Events live in a slide-in panel on the dashboard and persist across sessions until dismissed.

**MCP-powered multi-repo agent.** Everything visible in the dashboard is exposed as MCP tools. Claude Code sees your repos, MRs, issues, pipelines, job logs, vulns, and events, and can fetch and safely pull your local repos. Forge access is read-only: the agent never approves, merges, or comments. Events surface as MCP notifications, making the agent proactive instead of reactive.

## Features

- **Unified dashboard** — git status, MRs, issues, pipelines, vulns across all repos in one view
- **Merge request, issue & pipeline views** — cross-repo tables with filters (mine/all, MRs waiting for your review, failed only, current branches only)
- **Per-repo detail page** — MRs, issues, pipelines, active vulns for a single repo
- **Web UI** (htmx) with real-time sync, sortable tables, persistent filters
- **Non-destructive pull** — conflict detection via `git merge-tree`, no worktree mutations
- **Pin & reorder** repos, drag-and-drop card layout
- **Bulk actions** — pull all, unstale all in one click

## Quick start

**1. Register the MCP server once, for all directories:**

```bash
claude mcp add --scope user project-hub -- uvx project-hub --mcp
```

From source instead (picks up local changes without cache issues):

```bash
claude mcp add --scope user project-hub -- \
  uv run --project /path/to/project-hub project-hub --mcp
```

**2. Store a token for each forge you use.** Tokens are global (OS keyring, keyed by host), not per workspace:

```bash
uvx project-hub auth login gitlab.com
uvx project-hub auth login github.com
uvx project-hub auth login bitbucket.org
uvx project-hub auth login git.example.org --type forgejo   # self-hosted: which API validates the token
```

`--type` only picks how the token is checked. The workspace learns each host's forge type at `init` (step 3).

**3. Open Claude Code in the parent directory of your repos and ask:**

> Initialize a project-hub workspace here, then annotate each repo with its role and domain.

The agent calls `init`, which discovers the repos, writes `.project-hub/config.yaml`, and starts the refresh loop and the dashboard. Annotating is not optional: most tools skip or refuse a repo without a role and domain, because those annotations are what give the agent its context. List the repos that need no context under `annotation_not_required` in `config.yaml`.

`init` assumes GitLab for a self-hosted host it can't recognize and tells the agent so. Say which forge it really is ("git.example.org is a Forgejo") and the agent fixes it with `configure_forge`.

**4. The dashboard opens in your browser** at http://localhost:8400, or the next free port when 8400 is taken. Closed the tab? Ask the agent for the dashboard URL.

Without an agent, the CLI does the same and opens the browser:

```bash
cd ~/Projects                    # parent dir containing your repos
uvx project-hub init             # creates .project-hub/, prompts for unrecognized hosts
uvx project-hub                  # launch dashboard
```

### How the MCP server behaves per directory

Installing with `--scope user` is safe — the server adapts to the current directory:

| Directory state | Behavior |
|----------------|----------|
| Has `.project-hub/` | Active — full tools + refresh |
| Parent has `.project-hub/` | Disabled — zero tools (sub-repo) |
| Has `.no-project-hub` | Disabled — zero tools (opt-out) |
| Neither | Dormant — only `init` tool |

```bash
touch ~/some-project/.no-project-hub   # force disable in a specific dir
```

## MCP tools

| Tool | Description |
|------|-------------|
| `init` | Bootstrap `.project-hub/` workspace |
| `list_repos` | List discovered repos with forge info and annotation status |
| `repo_status` | Git status (branch, ahead/behind, dirty) |
| `fetch_all` | Trigger full refresh cycle |
| `pull_safe` | Pull with conflict detection |
| `list_mrs` | Merge requests (repo, state, author, awaiting-review filters) |
| `list_issues` | Issues (repo, state, author, label filters) |
| `list_pipelines` | Pipelines (repo, status, ref filters) |
| `get_vulnerabilities` | Vulns — defaults to high/critical + detected, includes solutions |
| `get_pipeline_jobs` | Jobs for a specific pipeline |
| `get_job_log` | Fetch log output for a specific CI job |
| `annotate_repo` | Set per-repo annotations (role, domain, links, notes) |
| `acknowledge_alerts` | Mark events as seen |
| `rescan` | Re-scan workspace for new/removed repos |
| `auth_status` | Check forge authentication |
| `auth_login` | Store PAT for a forge instance |
| `configure_forge` | Configure forge host mapping |

## Web dashboard

| Route | Description |
|-------|-------------|
| `/` | Dashboard with repo cards, pinning, drag-and-drop, events panel |
| `/mrs` | Merge requests table (mine/all filter) |
| `/issues` | Issues table (mine/all filter) |
| `/pipelines` | Pipelines table (failed only, current branches only) |
| `/vulns` | Vulnerabilities (severity, state, fixed-in-branch filters) |
| `/repo/{name}` | Per-repo detail: MRs, issues, pipelines, active vulns |
| `/settings` | Exclude/include repos, rescan workspace |

All filters and table sort are persisted in localStorage.

## Architecture

Single Python process, dual interfaces: MCP (stdio) + HTTP (FastAPI/htmx). Background asyncio task refreshes git+forge data on a TTL cycle, diffs with SQLite cache to generate events.

```
project_hub/
  cli.py              # Click entry points
  core/
    workspace.py       # Orchestrator: discover -> refresh loop -> stop
    cache.py           # SQLite (WAL mode, aiosqlite)
    config.py          # YAML config with defaults
    discovery.py       # Scan for .git/ repos, classify forges
    models.py          # Dataclasses: Repo, MR, Issue, Pipeline, Vulnerability, Event
    events.py          # Diff old vs new state -> Event list
  forge/
    provider.py        # ForgeProvider ABC (shared interface for all forges)
    auth.py            # Token resolution: keyring -> auth.json -> env vars -> ~/.python-gitlab.cfg
    gitlab.py          # python-gitlab + GraphQL
    github.py          # PyGithub
    bitbucket.py       # atlassian-python-api (Cloud)
    forgejo.py         # REST v1 over requests (Forgejo >= 16 for CI)
  git/
    ops.py             # fetch, status, pull_safe (merge-tree conflict detection)
  mcp/
    server.py          # FastMCP tools + resources
  web/
    app.py             # FastAPI factory, routes, htmx fragments
    templates/         # Jinja2 templates
    static/            # CSS + vendored JS
```

## Requirements

- Python 3.11+
- Git 2.38+ (for `git merge-tree --write-tree`)
- A token for each forge you use:
  - GitLab: PAT with `read_api` scope
  - GitHub: PAT with `repo` scope
  - Bitbucket Cloud: API token (app passwords deprecated June 2026)
  - Forgejo: token with `read:repository`, `read:issue`, `read:user`

### Tokens

`auth login` stores tokens in the OS keyring, or in `~/.config/project-hub/auth.json` when no keyring is available. Without a stored token, project-hub falls back to environment variables: `GITLAB_TOKEN` (gitlab.com only, then `~/.python-gitlab.cfg` for any GitLab host), `GITHUB_TOKEN`, `BITBUCKET_TOKEN`, `FORGEJO_TOKEN`.

### Forge notes

- **Self-hosted forges** are recognized only when the forge's name is in the hostname (`gitlab.example.com`, `forgejo.example.org`) or for `codeberg.org`. Declare the others in `.project-hub/config.yaml`:

  ```yaml
  forges:
    git.example.org:
      type: forgejo
  ```

- **Vulnerabilities** come from GitLab and GitHub only. Bitbucket and Forgejo have PRs, issues and pipelines.
- **Forgejo CI** needs Forgejo 16 or later, the first release with an API for Actions jobs and logs. Older instances keep PRs and issues, and the pipelines tab shows "CI disabled".

### Behind a TLS-intercepting proxy

A corporate proxy (Zscaler and the like) re-signs TLS with its own root, which Python's bundled CA list doesn't know: forge calls then fail with `CERTIFICATE_VERIFY_FAILED`. When IT has installed that root in the system store, point Python at it:

```bash
REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
```

Put it in `/etc/environment` (read at login) so that the MCP server, started by Claude Code, inherits it too.

## Development

```bash
uv run pytest tests/ -v
uv run --with 'pyflakes>=4,<5' pyflakes project_hub/
uv run --with 'pylint>=4,<5' pylint project_hub/
uv run --with 'radon>=6,<7' radon cc project_hub/ -s -n D   # must print nothing
```

CI runs the same commands as its lint and test gates.

## License

[Unlicense](https://unlicense.org/) — public domain.
