Metadata-Version: 2.4
Name: jean-agent
Version: 0.16.0
Summary: Drop-in Slack-fronted Claude Code agent. Add a skills/ directory, run `jean init`, deploy to Fly.io.
Project-URL: Homepage, https://github.com/shinkansenfinance/jean
Project-URL: Repository, https://github.com/shinkansenfinance/jean
Project-URL: Issues, https://github.com/shinkansenfinance/jean/issues
Author-email: Leo Soto <leo.soto@shinkansen.finance>
License: MIT License
        
        Copyright (c) 2026 Shinkansen Finance
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: agent,anthropic,bot,claude,claude-agent-sdk,fly.io,skills,slack
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications :: Chat
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Requires-Dist: aiohttp>=3.9
Requires-Dist: aiosqlite>=0.20
Requires-Dist: authlib>=1.3
Requires-Dist: claude-agent-sdk>=0.2
Requires-Dist: cryptography>=42
Requires-Dist: fastapi>=0.115
Requires-Dist: httpx>=0.27
Requires-Dist: itsdangerous>=2.2
Requires-Dist: jinja2>=3.1
Requires-Dist: keyring>=25
Requires-Dist: pillow>=10
Requires-Dist: pydantic-settings>=2.4
Requires-Dist: pydantic>=2.7
Requires-Dist: pymupdf>=1.24
Requires-Dist: slack-bolt>=1.20
Requires-Dist: typer>=0.12
Requires-Dist: uvicorn[standard]>=0.30
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest-mock>=3.14; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# Jean

A drop-in Slack-fronted Claude Code agent for your repo. Add a `skills/` directory, run `uvx jean-agent init`, deploy to Fly.io — your team gets a Slack bot that runs Claude Code in your codebase, with conversation history in a web UI.

## What it does

- **Slack bot.** DM it or `@mention` it in a channel. Each Slack thread is one persistent Claude session.
- **Skills.** Drops the host repo's `skills/` directory into Claude's project skill discovery — no per-skill registration. A bundled `skill-builder` skill lets non-tech users **create + edit** skills by DM'ing the bot.
- **Owner-gated rollout.** Private mode + per-channel approval (the owner gets a DM with Approve/Deny buttons when jean is mentioned in a new channel) + per-thread `cc <@owner>` when non-owners invoke jean for the first time.
- **Two-way attachments.** Slack file uploads stage to disk so Claude can `Read` them; skills can call `send_file_to_user` to upload artifacts (PDFs, images, charts, CSVs) back into the thread.
- **Thread-aware.** Jean fetches prior human messages in the thread as context. When the SDK session is lost (deploy SIGKILL, volume migration, etc.), jean rebuilds context from the full Slack thread so conversations stay coherent.
- **Live-edited replies + progress hourglass.** Throttled `chat.update` so Claude's output streams without hitting Slack rate limits; the ⏳ ↔ ⌛ alternation on the user's message indicates the bot is alive even before the first text lands.
- **Web UI.** Sign in with Slack. Users see their own conversations; admins (managed by the owner) see all. `/admin/skills`, `/admin/channels`, `/admin/users` for governance.
- **Self-diagnosing.** `uvx jean-agent doctor --setup-guide` walks you through every secret with the Slack manifest inlined; `uvx jean-agent doctor --deep` runs live API checks. Misconfigured installs render an owner-only fix-list page instead of the normal UI.

## Quick start

You run jean **inside your own repo** — the one where your `skills/` directory
lives. Jean ships on PyPI as [`jean-agent`](https://pypi.org/project/jean-agent/);
invoke it without installing via `uvx` (npx-equivalent for Python).

**Prerequisites:**

- [uv](https://docs.astral.sh/uv/getting-started/installation/) installed (`brew install uv` or the upstream installer)
- A `skills/<name>/SKILL.md` somewhere in the repo where you'll run jean (or just create one as you go — the bundled `skill-builder` skill can help)

**Setup, in your host repo:**

```bash
# 1. Scaffold jean.toml + fly.toml + Dockerfile into the current directory
uvx jean-agent init           # interactive: Fly app name, owner, bot display name

# 2. Walk through every secret (Anthropic + 4 Slack values), browser-open + clipboard
uvx jean-agent configure-secrets

# 3. Verify everything is wired up
uvx jean-agent doctor --deep  # workspace identity, owner, scopes

# 4. Provision Fly app + volume + secrets, then deploy
uvx jean-agent deploy
```

> **Prefer a shorter command?** `uv tool install jean-agent` puts both
> `jean-agent` and `jean` on your PATH; both invoke the same Typer app.
> Everywhere this README says `uvx jean-agent <cmd>` you can run `jean <cmd>`.

### What `uvx jean-agent init` writes

- `jean.toml` — root-level config; commit it (no secrets). See [Configuration](#configuration) below.
- `fly.toml` — Fly machine + volume config (app name, region, mounts, env overrides for `/data/...` paths).
- `Dockerfile` — Python 3.12-slim + Node.js + poppler-utils + LibreOffice + gosu; runs as a non-root `jean` user.
- `.dockerignore`
- `jean_system_prompt.md` — the host's optional system prompt.

> **Dockerfile.** Two things are auto-managed by `upgrade-target`:
> - **Version pin**: `pip install 'jean-agent>=X.Y.Z,<X.(Y+1).0'`. Floor
>   moves with every release (one-line diff to commit), ceiling caps at
>   next minor (rebuilds within a minor pick up patches automatically;
>   minor bumps need an explicit `upgrade-target`).
> - **apt block** between `# jean-apt-start` / `# jean-apt-end`
>   markers. Rewritten end-to-end on every `upgrade-target` from
>   jean's canonical runtime deps (`bubblewrap`, `socat`, `libreoffice`,
>   etc) PLUS whatever you list in `[jean] custom_apt_packages` in
>   `jean.toml`. Don't edit between the markers — edit `jean.toml`.
>
> Everything outside the markers — base image, non-root user setup,
> entrypoint — is yours. For working off local unreleased changes
> (e.g. testing a PR before publishing), run `uvx jean-agent vendor`
> to snapshot the installed jean into `.jean-vendor/jean/` and swap
> the install line to `RUN pip install /app/.jean-vendor/jean`.

## Releases

`jean-agent` follows [semantic versioning](https://semver.org/). Releases live at
[pypi.org/project/jean-agent](https://pypi.org/project/jean-agent/).

Release notes live in [`CHANGELOG.md`](./CHANGELOG.md) (Keep a Changelog
format). After every deploy, jean reads the delta between the version
it last ran with and the version it's running now, and DMs the owner a
summary of what's new. The same content renders at `/admin/changelog`
in the web UI, and `uvx jean-agent upgrade-target` prints it in the
terminal during local upgrades. The release workflow refuses to publish
a tag that has no matching CHANGELOG section, so the changelog can't
silently drift out of sync.

The release flow (maintainers only):

```bash
# 1. Bump version in pyproject.toml (e.g., 0.1.0 → 0.2.0) and commit on main
# 2. Tag + push — the GitHub Actions workflow takes it from there
git tag v0.2.0
git push origin v0.2.0
```

The workflow at `.github/workflows/release.yml` validates that the tag matches
`pyproject.toml`'s version, builds the wheel + sdist, and publishes to PyPI via
[Trusted Publishing](https://docs.pypi.org/trusted-publishers/) — no API tokens
stored in repo secrets. Publishing rights are bound to (repo + workflow filename
+ environment) on the PyPI side.

The Dockerfile template (`jean/templates/Dockerfile.tmpl`) does NOT hardcode
a version — `init` computes the constraint at scaffold time from the version
of jean the user is running, so bumping `pyproject.toml` automatically flows
through to the next `uvx jean-agent init` for downstream users.

Existing installs upgrade in one step:

```bash
uvx jean-agent upgrade   # detects uv tool / pip and runs the right install,
                          # then re-invokes upgrade-target so the NEW jean
                          # refreshes the Dockerfile pin + README/AGENTS jean
                          # section + .github/workflows/jean-deploy.yml
```

"Target-repo updates" (what `upgrade-target` runs) are distinct from
**database migrations** (what `jean migrate` and app startup apply to
SQLite). See `AGENTS.md` § "Two distinct update pipelines" for the
disambiguation.

### Setting up the Slack app

Skip manual scope-clicking — let jean print the manifest:

```bash
uvx jean-agent print-manifest | pbcopy   # macOS; use xclip on Linux
```

Then at <https://api.slack.com/apps> → **Create New App → From a manifest**, paste, click Create. The manifest already includes:

- Bot scopes: `chat:write`, `app_mentions:read`, `channels:history`, `channels:read`, `groups:history`, `groups:read`, `im:*`, `mpim:history`, `reactions:write`, `files:read`, `files:write`, `users:read`, `users:read.email`
- User scopes (SIWS): `openid`, `email`, `profile`
- Redirect URLs (localhost + your Fly app)
- Socket Mode enabled
- Event subscriptions: `app_mention`, `message.im`
- Messages Tab enabled (so users can DM the bot from the App Home)

After creation:

1. **Install App** → copy the **Bot User OAuth Token** (`xoxb-…`)
2. **Basic Information → App-Level Tokens** → Generate one with `connections:write` scope; copy the `xapp-…` token
3. **Basic Information → App Credentials** → copy Client ID and Client Secret

`uvx jean-agent configure-secrets` walks you through each of these, opens the right pages, hides input, and live-validates the bot token via `auth.test`.

### Deploying to Fly

```bash
uvx jean-agent deploy            # one command
```

This wraps `flyctl` to:

1. Check `flyctl auth whoami`
2. Read `fly.toml` (app name, region, volume name)
3. Read encrypted credentials and push them as Fly secrets (staged for the deploy)
4. Create the app if missing
5. Create the persistent volume (`jean_data`, 3 GB default) if missing
6. `fly deploy`

Flags: `--yes`, `--skip-secrets`, `--skip-build`, `--volume-size`, `--org`. Idempotent — rerun safely.

After deploy, open `https://<your-app>.fly.dev/`, sign in with Slack, and DM the bot.

## Configuration

See `jean/templates/jean.toml.tmpl` for the canonical commented schema. Highlights:

| Section | Field | Default | What it does |
|---|---|---|---|
| `[jean]` | `owner` | git email | `@handle`, email, or U-id. Auto-promoted to `owner` role on first login. |
| `[jean]` | `bot_name` | `"Jean"` | Display name in Slack (also in the manifest). |
| `[jean]` | `private` | `false` | Only the owner can DM the bot. Everyone else gets a polite refusal. |
| `[jean]` | `channel_approval` | `true` | First mention in a new channel triggers owner DM with Approve/Deny buttons. |
| `[claude]` | `model` | `"claude-opus-4-8"` | All Opus 4.5–4.8 are $5/$25 per M tokens. Sonnet 4.6 ($3/$15) is cheaper. |
| `[claude]` | `thinking` | `"adaptive"` | Claude decides per-turn. Set `"off"` to disable extended thinking. |
| `[claude]` | `effort` | `"medium"` | `low/medium/high/xhigh/max` — guides how aggressively adaptive triggers. |
| `[claude]` | `permission_mode` | `"auto"` | `auto` lets Claude decide; `bypassPermissions` skips all checks. |
| `[claude]` | `skills_only` | `true` | Refuses anything that doesn't map to a skill (keeps token spend in scope). |
| `[claude]` | `user_skill_authors` | `"owner"` | `owner` / `admin` / `any` — who can create/edit skills via DM. |
| `[claude]` | `idle_timeout_sec` | `300` | Close SDK session after 5 min idle; resume on next message. |
| `[claude]` | `sandbox_bash` | `true` | Run the SDK's Bash tool inside bubblewrap with no network. Flip to `false` only if a skill needs outbound HTTP from Bash. |
| `[claude]` | `excluded_bash_commands` | `[]` | Commands the sandbox should leave alone (e.g. `["soffice"]` if a binary doesn't tolerate bwrap's namespacing). |
| `[claude]` | `use_anthropic_proxy` | `true` | Route SDK API calls through a local proxy so the real `sk-ant-…` key never enters the subprocess env. See [Security](#security) below. |
| `[jean]` | `custom_apt_packages` | `[]` | Extra OS packages for your skills (e.g. `["imagemagick", "tesseract-ocr"]`). Composed into the Dockerfile's apt block on the next `upgrade-target`. Don't list jean's own deps. |
| `[slack]` | `edit_throttle_sec` | `3.0` | Min seconds between `chat.update` calls during streaming. |
| `[slack]` | `show_tool_calls` | `"on_failure"` | `never` / `on_failure` / `always` |
| `[attachments]` | `max_size_mb` | `25` | Per-attachment cap. Bigger files get a `:warning:` reply. |

Local dev paths (`./.jean/jean.db`, `./.jean/uploads`) are overridden on Fly to `/data/...` via `fly.toml`'s `[env]` block — one `jean.toml` works in both environments.

## Secrets

Five secrets live in an encrypted local store at `~/.config/jean/<project-hash>/credentials.enc` (AES-256-GCM, master key in your OS keychain via the `keyring` package):

```
ANTHROPIC_API_KEY
SLACK_BOT_TOKEN
SLACK_APP_TOKEN
SLACK_CLIENT_ID
SLACK_CLIENT_SECRET
```

A sixth, `JEAN_SESSION_SECRET` (for the SIWS cookie), auto-generates and persists next to the DB.

`uvx jean-agent configure-secrets` is the canonical way to set them. **Env vars override the encrypted file** at runtime, so `fly secrets set …` continues to be the production path — `uvx jean-agent deploy` reads from the local store and pushes them as Fly secrets in one shot.

## Security

A few defenses worth knowing about — the operator doesn't have to do
anything to enable them, they're on by default:

- **Real `ANTHROPIC_API_KEY` never enters the SDK subprocess.** A local
  HTTP proxy bound to `127.0.0.1:<random>` holds the real key; each
  `LiveSession` mints an ephemeral `jean_<random>` token that the SDK
  sees as `ANTHROPIC_API_KEY`. The proxy validates the token, swaps to
  the real key, and forwards to `api.anthropic.com`. Tokens revoke
  when the session closes. A `Bash("env")` exfil only ever leaks a
  short-lived token bounded to the session. Disable with
  `[claude] use_anthropic_proxy = false` if you need to debug API
  errors without the hop.
- **Bash sandbox** via `bubblewrap` with no network. Even if a skill
  exfils env, the follow-up `curl evil` can't leave the container.
  `[claude] sandbox_bash = false` opts out;
  `excluded_bash_commands = ["soffice"]` exempts specific binaries.
- **Env scrub.** The SDK subprocess inherits only an allow-list:
  `ANTHROPIC_*`, `HOME`, `PATH`, `LANG`, `LC_*`, `TZ`, `NODE_OPTIONS`.
  Slack tokens, the session secret, and any operator-set env vars
  are stripped before the SDK starts.
- **Encrypted credentials store** uses AES-256-GCM bound to the
  project's path (so a blob from project A can't decrypt under
  project B's key). Master key in the OS keychain via `keyring`.

## Features the bot exposes

### Skills created by DM

Users with the configured role (`user_skill_authors`) can teach the bot new capabilities by DM'ing it. A built-in `skill-builder` skill drives the conversation:

> User: "I want to teach you a daily standup helper."
>
> Bot: "Got it — when I activate this, what should the output look like?"
>
> *(few clarifying turns)*
>
> Bot: shows the SKILL.md draft → user confirms → `save_skill_file` writes it to `/data/skills/standup/SKILL.md` and bumps the metadata in SQLite.

Skills can be multi-file. Editing works through the same surface (`list_user_skills` → `read_skill_file` → propose change → `save_skill_file`). Deletion is web-UI-only at `/admin/skills` (owner-only). The skill becomes available **on the next message** — jean automatically closes the session after a save so the new client re-discovers it.

DM-only enforcement is three-layered: visibility gate (skill-builder hidden from non-DM sessions), tool-registration gate (MCP server not attached in channels), tool-implementation gate (each mutating tool re-checks `is_dm`).

### Per-channel approval

When jean is invoked in a channel for the first time (`channel_approval = true`):

1. Bot replies `:hourglass: I need my owner to approve me before I can chat in this channel. I'll respond here once they give me the green light.`
2. The event gets queued in `pending_channel_messages`.
3. Owner receives a DM with Block Kit Approve / Deny buttons.
4. On approve: queued messages are replayed through the normal handler — the original asker gets their answer.
5. On deny: queued messages are dropped; that channel stays silent.

Revocation at `/admin/channels` (owner-only) — drops the approval row and any queued messages so the next mention restarts the flow.

### Files back to Slack (`send_file_to_user`)

Skills can call the `send_file_to_user(local_path, title?, comment?)` MCP tool to upload an artifact to the current thread via `files.upload_v2`. Typical pattern: a skill produces a PDF / image / CSV with `Bash`, then attaches it. No special wiring per skill.

### Progress hourglass

While Claude thinks or runs tools but hasn't produced text yet, jean alternates the ⏳ and ⌛ reactions on the user's message at a 4-second cadence (under Slack's reactions rate limit). Stops as soon as the first content lands.

### Owner CC

When a non-owner mentions jean in a channel for the first time in a thread, jean posts `_cc <@owner>_` so the owner is notified. One per thread; suppressed if the owner has already participated.

## Architecture

- **Process model.** Single Python process, single Fly machine, single uvicorn worker. SQLite + WAL on a Fly persistent volume at `/data`.
- **Slack transport.** Socket Mode. No public webhook URL for events; only the SIWS callback is browser-side HTTP.
- **Per-thread Claude sessions.** One `ClaudeSDKClient` per `(channel, thread_ts)` while warm; idle-reaped after `idle_timeout_sec` (default 5 min) and resumed via the SDK's on-disk transcript on next mention.
- **Web UI.** FastAPI + Jinja2 + Pico.css. Authenticated via Sign in with Slack (OIDC).
- **Container.** `python:3.12-slim` + Node.js + poppler-utils + LibreOffice + gosu. Runs as non-root `jean` (uid 1000); entrypoint chowns `/data` and symlinks `/home/jean/.claude → /data/home/.claude` so SDK transcripts land on the volume.

## Development

```bash
pip install -e .[dev]
pytest                     # 51 tests, ~0.7s
```

`pytest -k "not import"` skips the smoke import test (which requires runtime deps installed).

See `AGENTS.md` for architectural invariants and gotchas before changing the harness, handlers, or Docker bits.

## License

MIT.
