Metadata-Version: 2.4
Name: attune-gui
Version: 0.8.0
Summary: Local dashboard for attune-rag / attune-help / attune-author. Server-rendered Jinja2 UI — ships clean via PyPI with no npm step.
Project-URL: Homepage, https://github.com/Smart-AI-Memory/attune-gui
Project-URL: Repository, https://github.com/Smart-AI-Memory/attune-gui
Project-URL: Issues, https://github.com/Smart-AI-Memory/attune-gui/issues
Author-email: Patrick Roebuck <admin@smartaimemory.com>
License: Apache-2.0
License-File: LICENSE
Keywords: attune,developer-tools,documentation,gui,rag
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Requires-Dist: attune-author[ai]<0.15,>=0.14.0
Requires-Dist: attune-help<1.0,>=0.10.0
Requires-Dist: attune-rag<0.3,>=0.1.22
Requires-Dist: fastapi<1.0,>=0.110
Requires-Dist: jinja2<4.0,>=3.1
Requires-Dist: markdown<4.0,>=3.5
Requires-Dist: mcp>=0.9.0
Requires-Dist: pydantic<3.0,>=2.0
Requires-Dist: python-frontmatter<2.0,>=1.1
Requires-Dist: structlog<26.0,>=24.0
Requires-Dist: uvicorn[standard]<1.0,>=0.27
Provides-Extra: dev
Requires-Dist: build<2.0,>=1.0; extra == 'dev'
Requires-Dist: httpx<1.0,>=0.27; extra == 'dev'
Requires-Dist: pytest-asyncio<2.0,>=0.23; extra == 'dev'
Requires-Dist: pytest-cov<7.0,>=5.0; extra == 'dev'
Requires-Dist: pytest<10.0,>=8.0; extra == 'dev'
Requires-Dist: ruff<1.0,>=0.5; extra == 'dev'
Requires-Dist: twine<7.0,>=5.0; extra == 'dev'
Description-Content-Type: text/markdown

# attune-gui

Local dashboard for the `attune-*` documentation family
(`attune-rag`, `attune-help`, `attune-author`). Server-rendered Jinja2
UI ("Cowork dashboard") backed by a FastAPI sidecar — ships clean via
PyPI with no `npm` step required to run.

## What it does

Sidebar nav with seven pages, each consuming the existing JSON API:

| Page | What it shows |
|------|---------------|
| **Health** | Cross-layer health (rag/help/author/gui versions) + corpus snapshot |
| **Templates** | Markdown templates with mtime staleness, tags, and a manual-pin toggle |
| **Specs** | Feature specs in `specs/` with phase + status badges. **+ New spec** bootstraps from `TEMPLATE.md`; **+ Design / + Tasks** inline; status dropdown in Preview |
| **Summaries** | Inline-editable `summaries.json` with overwrite warning |
| **Living Docs** | Workspace editor, scan trigger, document health, review queue, RAG quality bars |
| **Commands** | Run any registered command from a card grid (RAG queries, regen, maintain, …) |
| **Jobs** | Job history with per-feature progress, last-output column, Cancel button, auto-refresh |

Click any spec or template to open the **Preview / Edit** panel — server-side
Markdown rendering plus a raw `<textarea>` for editing.

## Template editor (`/editor`)

A first-class CodeMirror 6 editor for attune-help-style markdown templates.
Triggered by `attune-author edit <path>` or by navigating to
`/editor?corpus=<id>&path=<rel>`. Reads from / writes back to any registered
corpus on disk, with all retrieval/lint/refactor smarts coming from
[`attune-rag`](https://pypi.org/project/attune-rag/)'s `editor` toolkit.

Developer features:

| Feature | What it does |
|---------|---------------|
| **Schema-driven frontmatter form** | Reads `attune_rag.editor.load_schema()` at request time and renders typed inputs (select / chip-input / textarea / text). Unknown frontmatter keys are preserved verbatim. Raw-YAML toggle round-trips byte-for-byte. |
| **CodeMirror 6 + Lezer extension** | Composes the standard `@codemirror/lang-markdown` with a custom Lezer extension for YAML frontmatter delimiters, `## Depth N` markers, and `[[alias]]` refs (excluding fenced code, supporting `\[[` escape). |
| **Server-side lint** | 300 ms debounced POST to `/api/corpus/<id>/lint`. Diagnostics paint as squiggles in the editor + a clickable strip at the bottom. Local fast-path skips the round-trip for YAML parse errors. |
| **Tag + alias autocomplete** | Context-aware completions inside `tags:` / `aliases:` frontmatter fields and `[[…]]` body refs. Per-prefix LRU cache invalidates on WS file-change events. |
| **Per-hunk save modal** | `/api/corpus/<id>/template/diff` returns stable hunk ids; the modal shows each hunk with a checkbox and runs a *projected-state* lint so a partial save can't write known-broken frontmatter. Atomic save via `/template/save` (409 on `base_hash` mismatch → conflict mode). |
| **3-way merge conflict mode** | A WebSocket at `/ws/corpus/<id>?path=<rel>` pushes `file_changed` events from `watchfiles`. On conflict (or 409), a banner offers Reload / Keep / Resolve. Resolve uses [`node-diff3`](https://github.com/bhousel/node-diff3) for per-region accept-disk / accept-editor / keep-both. |
| **Cross-corpus rename refactor** | Right-click any tag/alias chip → "Rename …". `/api/corpus/<id>/refactor/rename/preview` returns a multi-file diff; apply is atomic across files (per-file tempfile + sequential rename + drift-detection rollback). 409 with `owning_path` on alias collisions. |
| **Corpus switcher** | Top-bar dropdown lists registered corpora (`~/.attune/corpora.json`). Search input materializes above 10 corpora. "+ Add corpus…" registers a new root via `/api/corpus/register`. Switching with unsaved edits prompts Save / Discard / Cancel. |
| **Generated-corpus advisory** | Persistent, non-dismissible banner when the active corpus has `kind: "generated"` — flags that edits will be overwritten on the next `attune-author maintain`. |
| **Read-only on duplicate session** | A second tab opening the same `(corpus, path)` receives a `duplicate_session` WS message and goes read-only with a banner — first tab keeps full control. |
| **Keyboard shortcuts** | `⌘/Ctrl-S` opens the save modal. `beforeunload` warns on unsaved edits. |
| **Pre-bundled, no Node at install** | `editor-frontend/` is Vite + TypeScript; `make build-editor` produces a hashed-filename bundle into `sidecar/attune_gui/static/editor/` that's checked into the repo. PyPI consumers don't need `npm`. |

### Editor frontend dev loop

```bash
# In one shell — sidecar with auto-reload:
uv run attune-gui --port 8765 --reload

# In another — vitest watch (92 unit tests, ~2s):
cd editor-frontend && npm run test --watch

# Rebuild the bundle (deterministic; output committed):
make build-editor

# Run the Playwright e2e suite against a fresh sidecar
# (auto-spawned by playwright.config.ts):
cd editor-frontend && npm run e2e
```

The editor bundle is ~210 KB gzipped (budget: 600 KB). Schema and Lezer
grammar parse fixtures live in `editor-frontend/src/grammar/`; merge
correctness in `three-way-merge.test.ts`. The Playwright suite under
`editor-frontend/e2e/` exercises the end-to-end flows (open→edit→save,
conflict resolve via WS-pushed `file_changed`, rename refactor
preview+apply, corpus switcher with unsaved-edits guard, save-modal
controls, keyboard shortcuts + advisories) — it spins up a real
sidecar with an isolated `ATTUNE_CORPORA_REGISTRY` so your dev
registry isn't touched.

### Endpoint summary

| Method | Path | Purpose |
|--------|------|---------|
| GET    | `/editor?corpus=<id>&path=<rel>` | Editor shell (Jinja) |
| GET    | `/api/editor/template-schema` | Frontmatter JSON Schema |
| GET    | `/api/corpus` | List registered corpora + active id |
| POST   | `/api/corpus/active` | Switch active corpus |
| POST   | `/api/corpus/register` | Add a new corpus root |
| POST   | `/api/corpus/resolve` | Map an absolute path → `(corpus_id, rel_path)` |
| GET    | `/api/corpus/<id>/template?path=<rel>` | Read template + base hash + mtime |
| POST   | `/api/corpus/<id>/template/diff` | Compute unified-diff hunks vs disk |
| POST   | `/api/corpus/<id>/template/save` | Atomic save (`base_hash` 409-guarded) |
| POST   | `/api/corpus/<id>/lint` | Lint a template body |
| GET    | `/api/corpus/<id>/autocomplete?kind=tag\|alias&prefix=…` | Autocomplete |
| POST   | `/api/corpus/<id>/refactor/rename/preview` | Multi-file rename plan |
| POST   | `/api/corpus/<id>/refactor/rename/apply` | Atomic rename across files |
| WS     | `/ws/corpus/<id>?path=<rel>` | `file_changed` + `duplicate_session` push |

> Looking for AI dev workflows (code review, security audits, refactor
> planning, multi-agent orchestration)? Those live in
> [`attune-ai`](https://pypi.org/project/attune-ai/) — a separate
> product. attune-gui is deliberately scoped to the documentation lifecycle.

## Quickstart

```bash
pip install attune-gui
attune-gui
# Or pick a specific port:
attune-gui --port 8765
```

The sidecar binds to `127.0.0.1`, prints `SIDECAR_URL=…`, and serves the
new dashboard at `/`. Use `--open` to auto-open your browser.

## Configuration

### `.env` auto-loading

The sidecar loads `KEY=value` lines from the first `.env` it finds, in this order:

1. `./.env` (current working directory)
2. `<repo-root>/.env` (the attune-gui checkout root)
3. `~/.attune-gui/.env`
4. `~/.attune/.env`

Existing real env values are preserved; empty/whitespace-only values are
treated as unset and overwritten. Common keys:

```
ANTHROPIC_API_KEY=sk-ant-…   # required for author.regen / author.maintain
ATTUNE_SPECS_ROOT=/path/to/your/repo/specs
ATTUNE_WORKSPACE=/path/to/your/project
```

### `~/.attune-gui/config.json`

A single typed config file holds the keys below. Each key has a matching
environment variable that overrides the file at runtime — useful for CI
or one-off runs. Precedence: **env > file > default**.

| Key | Env var | Purpose |
|-----|---------|---------|
| `workspace` | `ATTUNE_WORKSPACE` | Project the sidecar watches (Living Docs, templates) |
| `corpora_registry` | `ATTUNE_CORPORA_REGISTRY` | Path to corpora registry (default: `~/.attune/corpora.json`) |
| `specs_root` | `ATTUNE_SPECS_ROOT` | Where the **Specs** page reads from (default: `<workspace>/specs/`, then walks up from cwd) |

Manage the file from the CLI — no hand-editing JSON:

```bash
attune-gui config list                               # show resolved values + source
attune-gui config get workspace                      # print one value
attune-gui config set workspace /path/to/project     # persist a value
attune-gui config unset specs_root                   # remove a key
```

`config set workspace` validates that the path is a real directory; the
others trust you. Workspace can also be set via **Living Docs →
Workspace** in the UI — both surfaces write to the same file.

## Development

```bash
git clone https://github.com/Smart-AI-Memory/attune-gui
cd attune-gui
uv sync
uv run attune-gui --port 8765 --reload
```

Templates auto-reload — edit anything under `sidecar/attune_gui/templates/`
and refresh the browser. Python code changes reload automatically with
`--reload`.

### Tests

```bash
uv run pytest                # 124 tests, ~2s
uv run ruff check .          # lint
```

### Stacked pull requests

If PR B was opened on top of PR A's branch and A has since merged to
main, B will show conflicts against main even though your own commits
are clean. The conflicts are A's commits, now reachable from main via
the merge, replaying against themselves.

Fix it by replaying only your commits onto main with
`git rebase --onto main <old-base-sha> <branch>`. `<old-base-sha>` is
the tip of A's branch at the moment you forked B — find it with
`git merge-base <branch> <A-branch>` while A's branch still exists
locally, or read it from the PR's "compare" base on github.com before
the branch is deleted.

```bash
# B = feature/b, was stacked on feature/a (now merged + deleted)
git fetch origin
old_base=$(git merge-base feature/b origin/main~1)  # or paste the SHA
git rebase --onto origin/main "$old_base" feature/b
```

Then force-push with lease — never plain `--force`:
`git push --force-with-lease`.

## Architecture

```
┌──────────────────────────────────────┐
│  Cowork dashboard (Jinja2)  /        │
└──────────────────┬───────────────────┘
                   │  /api/*
┌──────────────────▼───────────────────┐
│  FastAPI sidecar — 127.0.0.1         │
│  ├─ routes/system, rag, help, …      │
│  ├─ routes/cowork_health             │
│  ├─ routes/cowork_specs              │
│  ├─ routes/cowork_templates          │
│  ├─ routes/cowork_files              │
│  └─ routes/cowork_pages  (HTML)      │
└──────────────────┬───────────────────┘
                   │
┌──────────────────▼───────────────────┐
│  attune-rag · attune-help            │
│  attune-author[ai]                   │
└──────────────────────────────────────┘
```

### Frontend boundary

Two rendering surfaces, one rule:

- **Template editor at `/editor`** is a Vite + TypeScript SPA
  (CodeMirror 6, WebSocket conflict mode, per-hunk diff). Source in
  `editor-frontend/`, pre-bundled into `sidecar/attune_gui/static/editor/`
  and committed so PyPI consumers do not need Node.
- **Everything else** (Health, Templates, Specs, Summaries, Living Docs,
  Commands, Jobs) is server-rendered Jinja2 with light vanilla-JS for
  inline actions and polling.

**New UI defaults to Jinja.** Reach for the SPA only when the surface
needs editor-grade interactivity — rich text editing, real-time conflict
resolution, multi-file refactor previews. Inline actions, polling, and
form-driven dashboards stay server-rendered.

## MCP integration

attune-gui ships an MCP server (`attune-gui-mcp`) that exposes a small,
read-mostly tool surface for Claude Code and other MCP clients. Same
data the dashboard renders — specs and living docs — but addressable
programmatically. `mcp>=0.9.0` is a core dependency, so `pip install
attune-gui` is enough to get both the dashboard and the MCP server.

### Tools

| Tool | What it returns |
|---|---|
| `gui_list_specs` | All feature specs across configured roots (federated). Each tagged with project, root, phase, status. |
| `gui_get_spec` | All phase-file contents (`requirements.md` / `design.md` / `tasks.md`) for one spec. |
| `gui_get_spec_status` | The `**Status**:` value for one phase. Default: most-advanced phase. |
| `gui_list_living_docs` | Doc registry — generated docs the workspace tracks, with status (current/stale/missing) and persona. |
| `gui_get_living_doc` | One living-doc's file content. Path-traversal guarded. |

Every tool returns a JSON envelope shaped `{"success": bool, ...}`.
See [`docs/specs/mcp-server-scope/`](docs/specs/mcp-server-scope/) for
the scope decisions and tool schemas.

### Wiring into Claude Code

Add to your project's `.mcp.json` (or `~/.claude/mcp.json` for global):

```json
{
  "mcpServers": {
    "attune-gui": {
      "command": "attune-gui-mcp"
    }
  }
}
```

The server boots on stdio and logs to
`<tmpdir>/attune-gui/attune-gui-mcp.log` so the MCP transport stays
clean. Restart Claude Code after editing the config; the five tools
appear under the `attune-gui` server in `/mcp`.

### Related work

attune-ai's [ops-specs-features](https://github.com/Smart-AI-Memory/attune-ai/blob/main/docs/specs/ops-specs-features/decisions.md)
spec is the complement to this one: that work brings attune-gui's
spec views into the attune-ai ops dashboard for humans; this MCP
server exposes the same views to agents. Both serve the spec-driven
workflow, just for different clients.

## Security notes

This is a **single-user, local-only** app. Not designed for multi-user
deployment, not hardened against a motivated attacker on the same machine.

- Binds **only** to `127.0.0.1` — not reachable from other machines
- An `Origin` header guard rejects browser requests from non-localhost origins
- Mutating endpoints require the `X-Attune-Client` header to match a
  per-process token (served from `/api/session/token`)
- File API enforces a path-traversal guard against three named roots
  (`templates`, `specs`, `summaries`); writes outside those roots return 400

## Related packages

- [`attune-rag`](https://pypi.org/project/attune-rag/) — RAG pipeline
- [`attune-help`](https://pypi.org/project/attune-help/) — help runtime
- [`attune-author`](https://pypi.org/project/attune-author/) — doc authoring
- [`attune-gui-plugin`](https://github.com/Smart-AI-Memory/attune-gui-plugin) — Claude Code plugin that launches the dashboard inside Cowork's preview pane
- [`attune-ai`](https://pypi.org/project/attune-ai/) — separate AI dev workflow product (not used by attune-gui)

## License

Apache-2.0
