From b2e6c6e36d291b6bc152a9d09cf7b54e4dd75cd8 Mon Sep 17 00:00:00 2001 From: Richie627 Date: Thu, 23 Apr 2026 17:21:54 -0700 Subject: [PATCH 1/6] feat(integrations): add Streamlit UI wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Paste text → click Humanize → get the final rewritten prose back. The wrapper shells out to the local `claude` CLI with a prompt that invokes the humanizer skill but suppresses its usual draft / audit / summary output, leaving only the rewritten text for copy-paste workflows. Two ways to run: - Local: ./run.sh (venv + streamlit) - Docker: docker compose up --build (mounts ~/.claude for skill + auth) No API key required — uses the host's existing Claude Code login. Co-Authored-By: Claude Opus 4.7 (1M context) --- integrations/streamlit/.dockerignore | 6 ++ integrations/streamlit/Dockerfile | 25 +++++ integrations/streamlit/README.md | 95 +++++++++++++++++++ integrations/streamlit/app.py | 106 ++++++++++++++++++++++ integrations/streamlit/docker-compose.yml | 21 +++++ integrations/streamlit/requirements.txt | 1 + integrations/streamlit/run.sh | 37 ++++++++ 7 files changed, 291 insertions(+) create mode 100644 integrations/streamlit/.dockerignore create mode 100644 integrations/streamlit/Dockerfile create mode 100644 integrations/streamlit/README.md create mode 100644 integrations/streamlit/app.py create mode 100644 integrations/streamlit/docker-compose.yml create mode 100644 integrations/streamlit/requirements.txt create mode 100755 integrations/streamlit/run.sh diff --git a/integrations/streamlit/.dockerignore b/integrations/streamlit/.dockerignore new file mode 100644 index 00000000..e5c392e3 --- /dev/null +++ b/integrations/streamlit/.dockerignore @@ -0,0 +1,6 @@ +.venv/ +__pycache__/ +*.pyc +.git/ +.gitignore +README.md diff --git a/integrations/streamlit/Dockerfile b/integrations/streamlit/Dockerfile new file mode 100644 index 00000000..c507cfac --- /dev/null +++ b/integrations/streamlit/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.11-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl ca-certificates git \ + && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* + +RUN npm install -g @anthropic-ai/claude-code + +WORKDIR /app +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py ./ + +ENV HOME=/root +EXPOSE 8501 + +CMD ["streamlit", "run", "app.py", \ + "--server.address=0.0.0.0", \ + "--server.port=8501", \ + "--server.headless=true", \ + "--browser.gatherUsageStats=false", \ + "--server.fileWatcherType=none"] diff --git a/integrations/streamlit/README.md b/integrations/streamlit/README.md new file mode 100644 index 00000000..d6aec4df --- /dev/null +++ b/integrations/streamlit/README.md @@ -0,0 +1,95 @@ +# Streamlit wrapper + +A minimal web UI for the `humanizer` skill. Paste text, click Humanize, get the rewritten prose back — without the skill's default draft / audit / summary sections. + +No API key is required. The wrapper shells out to your local `claude` CLI, so it uses your existing Claude Code login. + +![flow](https://img.shields.io/badge/input-paste%20text-blue) → `claude -p` + humanizer skill → ![flow](https://img.shields.io/badge/output-final%20prose-brightgreen) + +## Prerequisites (both local and Docker) + +1. [Claude Code](https://claude.com/claude-code) installed and logged in. Verify with: + ```bash + claude --version + ``` +2. The `humanizer` skill installed (see repo root `README.md` → _Installation_). +3. Python 3.9+ (for the local run) **or** Docker 24+ (for the container run). + +--- + +## Option A — Run locally + +```bash +cd integrations/streamlit +./run.sh +``` + +`run.sh` will: + +1. Confirm `claude` is on `$PATH`. +2. Create a `.venv/` and install `streamlit`. +3. Launch the app on http://localhost:8501. + +Press `Ctrl+C` in the terminal to stop. + +## Option B — Run with Docker + +```bash +cd integrations/streamlit +docker compose up --build +``` + +Then open http://localhost:8501. + +The compose file mounts your host's `~/.claude/` into the container so the already-installed `humanizer` skill is available inside. + +### Auth inside the container + +- **Linux hosts:** Claude Code stores auth tokens in `~/.claude/`, which is mounted — you should be logged in automatically. +- **macOS hosts:** OAuth credentials live in the macOS Keychain (not in `~/.claude/`). On first run the CLI inside the container won't be logged in. Fix either way: + - Interactive login into the container: + ```bash + docker compose exec humanizer claude /login + ``` + The credentials persist in the mounted `.claude/` volume. + - Or use an API key fallback: uncomment the `ANTHROPIC_API_KEY` line in `docker-compose.yml`, then: + ```bash + ANTHROPIC_API_KEY=sk-ant-... docker compose up + ``` + +Stop with `Ctrl+C`, or `docker compose down`. + +--- + +## How it works + +`app.py` wraps the user's text in a prompt that: + +1. Tells Claude to invoke the `humanizer` skill on the text. +2. Constrains output to only the final rewritten prose — no draft, no "what makes this AI" audit, no summary of changes, no preamble. + +It then runs: + +```bash +claude -p "" +``` + +…and streams stdout into the right-hand panel. Closing the browser tab (or clicking Humanize again) kills the in-flight subprocess. + +## Limitations + +- Each click spawns a fresh `claude -p` process (no persistent session). First call has the usual Claude Code startup cost (plugin sync, CLAUDE.md discovery, etc.); subsequent calls are faster. +- Long texts are passed as a positional argument, so input is capped by your shell's `ARG_MAX` (≈256 KB on macOS, ≈128 KB on Linux). Enough for a typical academic paragraph or essay; swap to `--input-format stream-json` if you need more. + +## Files + +``` +integrations/streamlit/ +├── app.py # Streamlit UI + subprocess call to `claude -p` +├── run.sh # Local launcher (venv + streamlit) +├── Dockerfile # Container with python + node + claude CLI + streamlit +├── docker-compose.yml # Mounts ~/.claude, exposes :8501 +├── .dockerignore +├── requirements.txt # streamlit +└── README.md # This file +``` diff --git a/integrations/streamlit/app.py b/integrations/streamlit/app.py new file mode 100644 index 00000000..8f5cbf30 --- /dev/null +++ b/integrations/streamlit/app.py @@ -0,0 +1,106 @@ +""" +Streamlit UI that forwards text to the local `claude` CLI and invokes the +`humanizer` skill. No API key: relies on the user's existing Claude Code login. + +Paste text → click Humanize → get the final rewritten prose, with the +skill's usual draft / audit / summary suppressed. +""" + +import atexit +import os +import shutil +import subprocess + +import streamlit as st + +CLAUDE_BIN = shutil.which("claude") or os.path.expanduser("~/.local/bin/claude") + +PROMPT_TEMPLATE = """Invoke the humanizer skill on the text below and rewrite it so it does not look AI-generated. + +OUTPUT RULES (STRICT): +- Output ONLY the final rewritten prose. +- No draft version, no "what makes it AI" analysis, no summary of changes. +- No preamble ("Here is...", "I've rewritten..."), no closing remarks. +- No markdown headers, no --- separators, no surrounding quotes. +- First character of your response must be the first character of the rewritten text. +- Last character must be the last character of the rewritten text. + +Text to humanize: + +{text} +""" + + +def _kill(proc): + if proc is None: + return + try: + if proc.poll() is None: + proc.kill() + except Exception: + pass + + +st.set_page_config(page_title="Humanizer", layout="wide") +st.title("Humanizer") +st.caption( + "Paste text → click Humanize → get the final rewritten prose. " + "Uses your local `claude` CLI + the `humanizer` skill. No API key." +) + +col_in, col_out = st.columns(2) + +with col_in: + st.subheader("Input") + text = st.text_area( + "Input", + height=560, + key="input_text", + label_visibility="collapsed", + placeholder="Paste the AI-sounding text here…", + ) + go = st.button("Humanize", type="primary", use_container_width=True) + +with col_out: + st.subheader("Output") + output_slot = st.empty() + +if go and text.strip(): + _kill(st.session_state.get("proc")) + + prompt = PROMPT_TEMPLATE.format(text=text) + proc = subprocess.Popen( + [CLAUDE_BIN, "-p", prompt], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + bufsize=0, + ) + st.session_state["proc"] = proc + atexit.register(_kill, proc) + + def stream(): + while True: + chunk = proc.stdout.read(32) + if not chunk: + break + yield chunk + proc.wait() + + with col_out: + output_slot.empty() + final = st.write_stream(stream()) + + if proc.returncode != 0: + err = (proc.stderr.read() or "").strip() + st.error( + f"`claude` exited with code {proc.returncode}.\n\n" + f"Check that Claude Code is installed (`which claude`) and logged in.\n\n" + f"stderr:\n```\n{err}\n```" + ) + else: + st.session_state["final_text"] = (final or "").strip() + +elif "final_text" in st.session_state: + with col_out: + output_slot.markdown(st.session_state["final_text"]) diff --git a/integrations/streamlit/docker-compose.yml b/integrations/streamlit/docker-compose.yml new file mode 100644 index 00000000..bca9975a --- /dev/null +++ b/integrations/streamlit/docker-compose.yml @@ -0,0 +1,21 @@ +services: + humanizer: + build: . + image: humanizer-streamlit + container_name: humanizer-streamlit + ports: + - "8501:8501" + volumes: + # Mount the host's Claude Code config so the container inherits: + # - the installed `humanizer` skill (~/.claude/skills/humanizer/) + # - any file-based auth tokens + # On macOS, OAuth credentials live in the Keychain (not in ~/.claude), + # so the first run may still need `docker compose exec humanizer claude /login`. + - ${HOME}/.claude:/root/.claude + environment: + # Fallback auth — uncomment (and export ANTHROPIC_API_KEY) to skip OAuth. + # - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + - HOME=/root + stdin_open: true + tty: true + restart: unless-stopped diff --git a/integrations/streamlit/requirements.txt b/integrations/streamlit/requirements.txt new file mode 100644 index 00000000..86d09369 --- /dev/null +++ b/integrations/streamlit/requirements.txt @@ -0,0 +1 @@ +streamlit>=1.30 diff --git a/integrations/streamlit/run.sh b/integrations/streamlit/run.sh new file mode 100755 index 00000000..5c5ce1ca --- /dev/null +++ b/integrations/streamlit/run.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# One-click launcher for the Streamlit wrapper. Spins up a local venv, +# installs Streamlit, and runs app.py which shells out to `claude -p`. + +set -e +cd "$(dirname "$0")" + +if ! command -v claude >/dev/null 2>&1; then + echo "claude CLI not found in PATH. Install Claude Code first:" + echo " https://claude.com/claude-code" + exit 1 +fi + +if ! command -v python3 >/dev/null 2>&1; then + echo "python3 not found. Install Python 3 and retry." + exit 1 +fi + +if [ ! -d ".venv" ]; then + echo "Creating local Python venv..." + python3 -m venv .venv +fi +# shellcheck disable=SC1091 +source .venv/bin/activate + +if ! python -c "import streamlit" >/dev/null 2>&1; then + echo "Installing dependencies..." + pip install --quiet --upgrade pip + pip install --quiet -r requirements.txt +fi + +echo "Ready. Opening http://localhost:8501 — Ctrl+C here to stop." + +exec streamlit run app.py \ + --server.headless false \ + --browser.gatherUsageStats false \ + --server.fileWatcherType none From c3cec28b07c7eb99c321b9badc1747e5f55707ac Mon Sep 17 00:00:00 2001 From: Richie627 Date: Thu, 23 Apr 2026 17:26:21 -0700 Subject: [PATCH 2/6] docs(streamlit): document lifecycle + both auth paths; clean output README now explains: - exactly when the `claude` subprocess starts/stops (per-click one-shot) - OAuth (`claude /login`) vs ANTHROPIC_API_KEY, including the macOS Docker keychain caveat and how to work around it - what guarantees the output stays free of draft/audit/summary noise app.py adds a `_clean_output()` pass that strips leaked preambles and trailing "Summary of changes" blocks, and replaces the streamed view with a copy-friendly textarea once generation finishes. Co-Authored-By: Claude Opus 4.7 (1M context) --- integrations/streamlit/README.md | 124 +++++++++++++++++++++++-------- integrations/streamlit/app.py | 68 ++++++++++++++--- 2 files changed, 148 insertions(+), 44 deletions(-) diff --git a/integrations/streamlit/README.md b/integrations/streamlit/README.md index d6aec4df..e417686b 100644 --- a/integrations/streamlit/README.md +++ b/integrations/streamlit/README.md @@ -1,19 +1,26 @@ # Streamlit wrapper -A minimal web UI for the `humanizer` skill. Paste text, click Humanize, get the rewritten prose back — without the skill's default draft / audit / summary sections. +A minimal web UI for the `humanizer` skill. Paste text, click **Humanize**, get the rewritten prose back — without the skill's default draft / audit / summary sections. -No API key is required. The wrapper shells out to your local `claude` CLI, so it uses your existing Claude Code login. +Paste text → `claude -p` + humanizer skill → clean final prose. -![flow](https://img.shields.io/badge/input-paste%20text-blue) → `claude -p` + humanizer skill → ![flow](https://img.shields.io/badge/output-final%20prose-brightgreen) +--- + +## Prerequisites -## Prerequisites (both local and Docker) +Both the local and Docker paths need: -1. [Claude Code](https://claude.com/claude-code) installed and logged in. Verify with: +1. [Claude Code](https://claude.com/claude-code) installed. Verify with: ```bash claude --version ``` -2. The `humanizer` skill installed (see repo root `README.md` → _Installation_). -3. Python 3.9+ (for the local run) **or** Docker 24+ (for the container run). +2. The `humanizer` skill installed (see the repo root `README.md` → _Installation_). +3. Either **one** of: + - A completed Claude Code login (`claude /login`), **or** + - `ANTHROPIC_API_KEY` exported in your shell. +4. Python 3.9+ (for local) **or** Docker 24+ (for container). + +See [Authentication](#authentication) below for details on the two login options. --- @@ -27,7 +34,7 @@ cd integrations/streamlit `run.sh` will: 1. Confirm `claude` is on `$PATH`. -2. Create a `.venv/` and install `streamlit`. +2. Create a local `.venv/` and install `streamlit`. 3. Launch the app on http://localhost:8501. Press `Ctrl+C` in the terminal to stop. @@ -41,51 +48,104 @@ docker compose up --build Then open http://localhost:8501. -The compose file mounts your host's `~/.claude/` into the container so the already-installed `humanizer` skill is available inside. +The compose file mounts your host's `~/.claude/` into the container so any file-based auth + your installed skills carry over. See [Authentication](#authentication) for the macOS caveat. + +Stop with `Ctrl+C`, or `docker compose down`. + +--- -### Auth inside the container +## Lifecycle — when does `claude` run? -- **Linux hosts:** Claude Code stores auth tokens in `~/.claude/`, which is mounted — you should be logged in automatically. -- **macOS hosts:** OAuth credentials live in the macOS Keychain (not in `~/.claude/`). On first run the CLI inside the container won't be logged in. Fix either way: - - Interactive login into the container: - ```bash - docker compose exec humanizer claude /login - ``` - The credentials persist in the mounted `.claude/` volume. - - Or use an API key fallback: uncomment the `ANTHROPIC_API_KEY` line in `docker-compose.yml`, then: - ```bash - ANTHROPIC_API_KEY=sk-ant-... docker compose up - ``` +The wrapper keeps **no** persistent `claude` session. Each click is self-contained: -Stop with `Ctrl+C`, or `docker compose down`. +| Moment | What happens | +|---|---| +| You open the browser | Streamlit boots. **No `claude` process yet.** | +| You paste text | Nothing yet — text just sits in the textarea. | +| You click **Humanize** | `app.py` spawns `claude -p ""` as a subprocess. | +| Generation runs (~5–15 s) | stdout streams into the right-hand panel. | +| Subprocess finishes | Process exits on its own. Cleaned output is shown in a copy-friendly textarea. | +| You click **Humanize** again | Any still-running subprocess is killed first, then a fresh one spawns. | +| You close the browser tab | Streamlit detects the disconnect. Any in-flight subprocess is killed on Streamlit shutdown (best-effort via `atexit`). | +| You `Ctrl+C` the terminal | Streamlit shuts down, killing any remaining subprocess. | + +In Claude Code terms: **each click is a brand-new one-shot session** with no memory of previous clicks. That's intentional — it keeps cost predictable and avoids stale context. --- -## How it works +## Authentication + +`claude -p` needs credentials to call the model. Pick one path: + +### Option 1 — OAuth (default, same login as your normal terminal use) + +Log in **once on your host**, before ever opening the app: + +```bash +claude /login +``` + +This opens a browser, completes OAuth, and stores credentials in: +- **macOS:** the system Keychain. +- **Linux / WSL:** `~/.claude/`. -`app.py` wraps the user's text in a prompt that: +After that, every `claude -p` call (including the ones this app makes) reuses that login silently. You can verify by running `claude -p "hi"` from any terminal — if it answers without asking for login, you're set. -1. Tells Claude to invoke the `humanizer` skill on the text. -2. Constrains output to only the final rewritten prose — no draft, no "what makes this AI" audit, no summary of changes, no preamble. +### Option 2 — `ANTHROPIC_API_KEY` -It then runs: +If you'd rather skip OAuth, or you're on a headless box where the Keychain isn't available (Docker on Linux, CI, a remote server), export an API key from [console.anthropic.com](https://console.anthropic.com/): ```bash -claude -p "" +export ANTHROPIC_API_KEY=sk-ant-... +./run.sh ``` -…and streams stdout into the right-hand panel. Closing the browser tab (or clicking Humanize again) kills the in-flight subprocess. +`claude -p` picks it up automatically. No `/login` needed. The subprocess inherits the env var from the Streamlit process. + +### Authentication inside Docker + +- **Linux host:** Claude Code's file-based auth lives in `~/.claude/`, which `docker-compose.yml` mounts into the container. OAuth works out of the box. +- **macOS host:** OAuth credentials live in the macOS Keychain, which the container cannot read. Two fixes: + + **Fix 1 — Log in once inside the container.** The credentials will persist in the mounted `.claude/` volume: + ```bash + docker compose up -d --build + docker compose exec humanizer claude /login + ``` + + **Fix 2 — Use an API key.** Uncomment the `ANTHROPIC_API_KEY` line in `docker-compose.yml`, then: + ```bash + ANTHROPIC_API_KEY=sk-ant-... docker compose up --build + ``` + +--- + +## Output — do I get just the result? + +Yes. Two layers enforce this: + +1. **Prompt-level:** The request wrapper explicitly tells Claude to output **only** the final rewritten prose — no draft, no "what makes it AI" audit, no summary of changes, no preamble, no markdown headers, no `---` separators, no surrounding quotes. +2. **Post-process-level:** Even if the model leaks a "Here is…" opener or the skill's default "**Summary of changes:**" trailer, `_clean_output()` in `app.py` strips them before the textarea is populated. + +During generation you'll see text stream into the right panel. Once it finishes, that display is replaced by a clean `text_area` containing only the rewritten prose, ready to copy. + +If you ever see leftover garbage in the cleaned output, that's a prompt-obedience failure — tighten `PROMPT_TEMPLATE` or extend `_PREAMBLE_RE` / `_TRAILING_RE` in `app.py`. + +--- ## Limitations -- Each click spawns a fresh `claude -p` process (no persistent session). First call has the usual Claude Code startup cost (plugin sync, CLAUDE.md discovery, etc.); subsequent calls are faster. -- Long texts are passed as a positional argument, so input is capped by your shell's `ARG_MAX` (≈256 KB on macOS, ≈128 KB on Linux). Enough for a typical academic paragraph or essay; swap to `--input-format stream-json` if you need more. +- First call has the usual Claude Code startup cost (plugin sync, CLAUDE.md discovery, keychain read); subsequent calls are faster. +- Long texts are passed as a positional argument, so input is bounded by your shell's `ARG_MAX` (≈256 KB on macOS, ≈128 KB on Linux). Enough for any single essay; swap to `--input-format stream-json` if you need more. +- No history / undo. Each click overwrites the previous output. + +--- ## Files ``` integrations/streamlit/ -├── app.py # Streamlit UI + subprocess call to `claude -p` +├── app.py # Streamlit UI + subprocess call to `claude -p` + output cleanup ├── run.sh # Local launcher (venv + streamlit) ├── Dockerfile # Container with python + node + claude CLI + streamlit ├── docker-compose.yml # Mounts ~/.claude, exposes :8501 diff --git a/integrations/streamlit/app.py b/integrations/streamlit/app.py index 8f5cbf30..5560b6a7 100644 --- a/integrations/streamlit/app.py +++ b/integrations/streamlit/app.py @@ -1,13 +1,20 @@ """ Streamlit UI that forwards text to the local `claude` CLI and invokes the -`humanizer` skill. No API key: relies on the user's existing Claude Code login. +`humanizer` skill. No API key required by default; uses the user's existing +Claude Code login. ANTHROPIC_API_KEY is also honored if set. -Paste text → click Humanize → get the final rewritten prose, with the -skill's usual draft / audit / summary suppressed. +Paste text → click Humanize → get the final rewritten prose. + +Lifecycle: +- No `claude` process runs at idle. +- Each click spawns a one-shot `claude -p ""` subprocess. +- The subprocess exits on its own once the answer is finished. +- Clicking again (or closing Streamlit) kills the in-flight subprocess. """ import atexit import os +import re import shutil import subprocess @@ -30,6 +37,32 @@ {text} """ +_PREAMBLE_RE = re.compile( + r"^(?:Here(?:'s| is)\b|Below is\b|Sure[,!.]?|Certainly[,!.]?|" + r"I(?:'ve| have)\s+rewritten\b|Got it[,!.]?|Okay[,!.]?|Alright[,!.]?|" + r"Rewritten text:|Output:|Result:)[^\n]*\n+", + re.IGNORECASE, +) +_TRAILING_RE = re.compile( + r"\n+(?:---+\s*\n+)?\*{0,2}(?:Summary|Changes?|Notes?|What makes)[^\n]*\n[\s\S]*$", + re.IGNORECASE, +) +_CODEFENCE_RE = re.compile(r"^\s*```[a-zA-Z]*\n(.*?)\n```\s*$", re.DOTALL) + + +def _clean_output(text: str) -> str: + """Defensive post-processing in case the model leaks preambles or the + skill's default 'Summary of changes' block despite the strict prompt.""" + t = (text or "").strip() + m = _CODEFENCE_RE.match(t) + if m: + t = m.group(1).strip() + t = _PREAMBLE_RE.sub("", t, count=1).strip() + t = _TRAILING_RE.sub("", t, count=1).strip() + if len(t) >= 2 and t[0] in '"\'' and t[-1] == t[0]: + t = t[1:-1].strip() + return t + def _kill(proc): if proc is None: @@ -45,7 +78,7 @@ def _kill(proc): st.title("Humanizer") st.caption( "Paste text → click Humanize → get the final rewritten prose. " - "Uses your local `claude` CLI + the `humanizer` skill. No API key." + "Uses your local `claude` CLI + the `humanizer` skill. No API key required." ) col_in, col_out = st.columns(2) @@ -87,20 +120,31 @@ def stream(): yield chunk proc.wait() - with col_out: - output_slot.empty() - final = st.write_stream(stream()) + raw = output_slot.write_stream(stream()) if proc.returncode != 0: err = (proc.stderr.read() or "").strip() - st.error( + output_slot.error( f"`claude` exited with code {proc.returncode}.\n\n" - f"Check that Claude Code is installed (`which claude`) and logged in.\n\n" + f"Check that Claude Code is installed and logged in (`claude /login`) " + f"or that `ANTHROPIC_API_KEY` is set.\n\n" f"stderr:\n```\n{err}\n```" ) + st.session_state.pop("final_text", None) else: - st.session_state["final_text"] = (final or "").strip() + cleaned = _clean_output(raw or "") + st.session_state["final_text"] = cleaned + output_slot.text_area( + "Output", + value=cleaned, + height=560, + label_visibility="collapsed", + ) elif "final_text" in st.session_state: - with col_out: - output_slot.markdown(st.session_state["final_text"]) + output_slot.text_area( + "Output", + value=st.session_state["final_text"], + height=560, + label_visibility="collapsed", + ) From 921755a129374b04508661b4d602e916199bda22 Mon Sep 17 00:00:00 2001 From: Richie627 Date: Thu, 23 Apr 2026 17:32:31 -0700 Subject: [PATCH 3/6] feat(streamlit): add copy button, streaming UX, clearer stop docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Output now renders via st.code (wrap_lines=True) so it has a native copy-to-clipboard icon in the top-right corner. - Add a visible "Running humanizer skill…" status message + spinner so users know work is in progress instead of only seeing the tiny Stop button in the Streamlit header. - Streaming response is preserved — text appears live as it arrives, then gets replaced by the copy-ready code block on completion. - README's Lifecycle section now spells out exactly how to stop: Ctrl+C for local, docker compose down / stop / kill for container, and notes that closing the browser tab alone doesn't stop Streamlit or the subprocess (which will self-terminate within seconds anyway). - Bump streamlit minimum to 1.56 (wrap_lines support + general latest). Co-Authored-By: Claude Opus 4.7 (1M context) --- integrations/streamlit/README.md | 39 ++++++++++++++++--------- integrations/streamlit/app.py | 37 +++++++++++------------ integrations/streamlit/requirements.txt | 2 +- 3 files changed, 45 insertions(+), 33 deletions(-) diff --git a/integrations/streamlit/README.md b/integrations/streamlit/README.md index e417686b..8c7c43d4 100644 --- a/integrations/streamlit/README.md +++ b/integrations/streamlit/README.md @@ -2,7 +2,7 @@ A minimal web UI for the `humanizer` skill. Paste text, click **Humanize**, get the rewritten prose back — without the skill's default draft / audit / summary sections. -Paste text → `claude -p` + humanizer skill → clean final prose. +Paste text → `claude -p` + humanizer skill → clean final prose, with a copy button. --- @@ -37,7 +37,7 @@ cd integrations/streamlit 2. Create a local `.venv/` and install `streamlit`. 3. Launch the app on http://localhost:8501. -Press `Ctrl+C` in the terminal to stop. +**To stop:** press `Ctrl+C` in the same terminal. That sends SIGINT to Streamlit, Streamlit's `atexit` hook kills any in-flight `claude -p` subprocess, and the port is released. ## Option B — Run with Docker @@ -50,26 +50,36 @@ Then open http://localhost:8501. The compose file mounts your host's `~/.claude/` into the container so any file-based auth + your installed skills carry over. See [Authentication](#authentication) for the macOS caveat. -Stop with `Ctrl+C`, or `docker compose down`. +**To stop:** two options, either works. + +- **From the terminal running `docker compose up`** — press `Ctrl+C`. Compose sends SIGTERM to the container, waits a few seconds, then SIGKILL if needed. When the container's PID 1 dies, the kernel kills every process inside the namespace, including any in-flight `claude -p`. +- **From a separate terminal** (or if `up` is detached with `-d`): + ```bash + docker compose down # stop + remove container (recommended) + # or + docker compose stop # stop but keep container for a quick restart + # or (last resort, hard kill) + docker kill humanizer-streamlit + ``` --- -## Lifecycle — when does `claude` run? +## Lifecycle — when does `claude` run, and when does it stop? -The wrapper keeps **no** persistent `claude` session. Each click is self-contained: +The wrapper keeps **no** persistent `claude` session. Each click is self-contained. | Moment | What happens | |---|---| | You open the browser | Streamlit boots. **No `claude` process yet.** | -| You paste text | Nothing yet — text just sits in the textarea. | -| You click **Humanize** | `app.py` spawns `claude -p ""` as a subprocess. | -| Generation runs (~5–15 s) | stdout streams into the right-hand panel. | -| Subprocess finishes | Process exits on its own. Cleaned output is shown in a copy-friendly textarea. | +| You paste text | Nothing happens — text just sits in the textarea. | +| You click **Humanize** | `app.py` spawns `claude -p ""` as a subprocess. Right panel shows a spinner + "Running humanizer skill…" status message, then streams the rewrite live as it arrives. | +| Generation finishes (~5–15 s) | Subprocess exits on its own. Status flips to "Done". Output is re-rendered in a code block with a native copy icon in the top-right corner. | | You click **Humanize** again | Any still-running subprocess is killed first, then a fresh one spawns. | -| You close the browser tab | Streamlit detects the disconnect. Any in-flight subprocess is killed on Streamlit shutdown (best-effort via `atexit`). | -| You `Ctrl+C` the terminal | Streamlit shuts down, killing any remaining subprocess. | +| You close **only** the browser tab | Streamlit server stays up. A subprocess that was already running **will finish on its own within seconds** (its stdout just gets discarded). **Closing the tab alone does not stop Streamlit.** | +| You `Ctrl+C` the local terminal | Streamlit shuts down → `atexit` fires → any in-flight subprocess is killed → port released. | +| You `docker compose down` / `Ctrl+C docker compose up` | Container is stopped → kernel kills every process in its namespace, subprocess included. | -In Claude Code terms: **each click is a brand-new one-shot session** with no memory of previous clicks. That's intentional — it keeps cost predictable and avoids stale context. +In Claude Code terms: **each click is a brand-new one-shot session** with no memory of previous clicks. --- @@ -125,9 +135,9 @@ export ANTHROPIC_API_KEY=sk-ant-... Yes. Two layers enforce this: 1. **Prompt-level:** The request wrapper explicitly tells Claude to output **only** the final rewritten prose — no draft, no "what makes it AI" audit, no summary of changes, no preamble, no markdown headers, no `---` separators, no surrounding quotes. -2. **Post-process-level:** Even if the model leaks a "Here is…" opener or the skill's default "**Summary of changes:**" trailer, `_clean_output()` in `app.py` strips them before the textarea is populated. +2. **Post-process-level:** Even if the model leaks a "Here is…" opener or the skill's default "**Summary of changes:**" trailer, `_clean_output()` in `app.py` strips them before the output block is populated. -During generation you'll see text stream into the right panel. Once it finishes, that display is replaced by a clean `text_area` containing only the rewritten prose, ready to copy. +During generation you'll see text stream into the right panel as it arrives. Once it finishes, that streamed view is replaced by a `st.code` block — this gives you a **native copy-to-clipboard icon in the top-right corner** of the output so you can paste the result straight into your doc. If you ever see leftover garbage in the cleaned output, that's a prompt-obedience failure — tighten `PROMPT_TEMPLATE` or extend `_PREAMBLE_RE` / `_TRAILING_RE` in `app.py`. @@ -137,6 +147,7 @@ If you ever see leftover garbage in the cleaned output, that's a prompt-obedienc - First call has the usual Claude Code startup cost (plugin sync, CLAUDE.md discovery, keychain read); subsequent calls are faster. - Long texts are passed as a positional argument, so input is bounded by your shell's `ARG_MAX` (≈256 KB on macOS, ≈128 KB on Linux). Enough for any single essay; swap to `--input-format stream-json` if you need more. +- Closing the browser tab alone does not stop an in-flight subprocess (it will self-terminate within seconds anyway). If you need a hard stop, use `Ctrl+C` / `docker compose down` — see [How to stop](#option-a--run-locally) above. - No history / undo. Each click overwrites the previous output. --- diff --git a/integrations/streamlit/app.py b/integrations/streamlit/app.py index 5560b6a7..7930b387 100644 --- a/integrations/streamlit/app.py +++ b/integrations/streamlit/app.py @@ -1,15 +1,17 @@ """ Streamlit UI that forwards text to the local `claude` CLI and invokes the `humanizer` skill. No API key required by default; uses the user's existing -Claude Code login. ANTHROPIC_API_KEY is also honored if set. - -Paste text → click Humanize → get the final rewritten prose. +Claude Code login. `ANTHROPIC_API_KEY` is also honored if set. Lifecycle: - No `claude` process runs at idle. - Each click spawns a one-shot `claude -p ""` subprocess. - The subprocess exits on its own once the answer is finished. -- Clicking again (or closing Streamlit) kills the in-flight subprocess. +- Clicking Humanize again kills any still-running subprocess first. +- Closing the browser alone does NOT kill an in-flight subprocess — it + will self-terminate within seconds when claude finishes the request. + To stop the server itself, Ctrl+C the terminal (local) or + `docker compose down` (container). See README for details. """ import atexit @@ -77,7 +79,7 @@ def _kill(proc): st.set_page_config(page_title="Humanizer", layout="wide") st.title("Humanizer") st.caption( - "Paste text → click Humanize → get the final rewritten prose. " + "Paste text → click **Humanize** → get the final rewritten prose. " "Uses your local `claude` CLI + the `humanizer` skill. No API key required." ) @@ -96,6 +98,7 @@ def _kill(proc): with col_out: st.subheader("Output") + status_slot = st.empty() output_slot = st.empty() if go and text.strip(): @@ -112,6 +115,11 @@ def _kill(proc): st.session_state["proc"] = proc atexit.register(_kill, proc) + status_slot.info( + "⏳ Running humanizer skill via `claude -p`… first call has ~3–5 s " + "startup overhead, then streams." + ) + def stream(): while True: chunk = proc.stdout.read(32) @@ -120,10 +128,12 @@ def stream(): yield chunk proc.wait() - raw = output_slot.write_stream(stream()) + with st.spinner("Humanizing…"): + raw = output_slot.write_stream(stream()) if proc.returncode != 0: err = (proc.stderr.read() or "").strip() + status_slot.empty() output_slot.error( f"`claude` exited with code {proc.returncode}.\n\n" f"Check that Claude Code is installed and logged in (`claude /login`) " @@ -134,17 +144,8 @@ def stream(): else: cleaned = _clean_output(raw or "") st.session_state["final_text"] = cleaned - output_slot.text_area( - "Output", - value=cleaned, - height=560, - label_visibility="collapsed", - ) + status_slot.success("Done. Use the copy icon in the top-right of the box below.") + output_slot.code(cleaned, language=None, wrap_lines=True) elif "final_text" in st.session_state: - output_slot.text_area( - "Output", - value=st.session_state["final_text"], - height=560, - label_visibility="collapsed", - ) + output_slot.code(st.session_state["final_text"], language=None, wrap_lines=True) diff --git a/integrations/streamlit/requirements.txt b/integrations/streamlit/requirements.txt index 86d09369..1ee5b87b 100644 --- a/integrations/streamlit/requirements.txt +++ b/integrations/streamlit/requirements.txt @@ -1 +1 @@ -streamlit>=1.30 +streamlit>=1.56 From 62cd7f76a988bafadc90137413cf31ff6525ecb6 Mon Sep 17 00:00:00 2001 From: Richie627 Date: Thu, 23 Apr 2026 17:44:06 -0700 Subject: [PATCH 4/6] fix(streamlit): address review findings + cleaner status UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundles the code-review + security-review fixes with a couple of usability nits reported during testing. Correctness: - Tighten `_TRAILING_RE` so it only strips the humanizer skill's actual trailer blocks (bolded "Summary of changes" / "Changes made" / "What makes … AI …" / "Draft rewrite" / "Now make it not …", with or without a leading `---` separator). Previous pattern ate any paragraph whose first word was "Notes" / "Summary" / "Changes" — a real problem for a humanizer whose job is preserving user prose. - Add `--no-session-persistence` to the `claude -p` call so no session file is written to disk, guaranteeing each click is fully stateless with zero possibility of one run's context leaking into the next. Resource hygiene: - Register atexit exactly once via `@st.cache_resource` instead of per click. Previous code appended a new atexit hook to every Humanize press for the life of the Streamlit process. Docker hardening: - Dockerfile now creates an unprivileged `app` user (uid 1000), chowns /app, and runs Streamlit + claude CLI as that user. Previously ran as root, so a dependency bug could write host files as uid 0 via the ~/.claude mount. - docker-compose.yml binds the published port to 127.0.0.1 only so the unauthenticated UI isn't exposed to the LAN by default, and adjusts the mount target + HOME to the new non-root home. UX: - Status message no longer wraps "claude -p" in backticks (rendered as tiny inline code and broke mid-sentence in the info block). Plain English now. - README: new Security considerations section covering the port bind, the ~/.claude mount trust boundary, the non-root user, and the single-user scope. Lifecycle section mentions --no-session-persistence explicitly so users don't worry about state leaking between clicks. Co-Authored-By: Claude Opus 4.7 (1M context) --- integrations/streamlit/Dockerfile | 8 +++-- integrations/streamlit/README.md | 11 ++++++- integrations/streamlit/app.py | 36 ++++++++++++++++++----- integrations/streamlit/docker-compose.yml | 15 ++++++---- 4 files changed, 55 insertions(+), 15 deletions(-) diff --git a/integrations/streamlit/Dockerfile b/integrations/streamlit/Dockerfile index c507cfac..96a2bcc4 100644 --- a/integrations/streamlit/Dockerfile +++ b/integrations/streamlit/Dockerfile @@ -8,13 +8,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ RUN npm install -g @anthropic-ai/claude-code +RUN useradd -m -u 1000 app + WORKDIR /app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt - COPY app.py ./ +RUN chown -R app:app /app + +USER app +ENV HOME=/home/app -ENV HOME=/root EXPOSE 8501 CMD ["streamlit", "run", "app.py", \ diff --git a/integrations/streamlit/README.md b/integrations/streamlit/README.md index 8c7c43d4..ab80ae16 100644 --- a/integrations/streamlit/README.md +++ b/integrations/streamlit/README.md @@ -79,7 +79,7 @@ The wrapper keeps **no** persistent `claude` session. Each click is self-contain | You `Ctrl+C` the local terminal | Streamlit shuts down → `atexit` fires → any in-flight subprocess is killed → port released. | | You `docker compose down` / `Ctrl+C docker compose up` | Container is stopped → kernel kills every process in its namespace, subprocess included. | -In Claude Code terms: **each click is a brand-new one-shot session** with no memory of previous clicks. +In Claude Code terms: **each click is a brand-new one-shot session** with no memory of previous clicks. The subprocess is launched with `--no-session-persistence`, so no session file is written to disk either — there is no way for the output of one click to leak into the context of the next. --- @@ -143,6 +143,15 @@ If you ever see leftover garbage in the cleaned output, that's a prompt-obedienc --- +## Security considerations + +Scoped for **single-user, local use**. A few things to know before you deploy this further: + +- **No authentication on the UI.** Anyone who can reach `http://:8501` can humanize text against your Claude Code credentials. The Docker compose file publishes the port on `127.0.0.1` by default so only your machine can hit it. Don't remove that bind unless you're putting auth in front of it. +- **`~/.claude` is mounted read-write into the container.** This gives the container full access to your host Claude Code auth tokens and installed skills. It needs to be writable because the macOS `claude /login`-inside-container fix writes back there. Treat the container image as trust-equivalent to the host for those credentials — don't run a random image against this compose file. +- **Container runs as a non-root user** (`app`, uid 1000) so a bug in a dependency can't trivially write to host files as root. (Your mounted `~/.claude/` files end up owned by uid 1000; that's normal.) +- **Prompt injection isn't a security issue here** — you are both the user *and* the "attacker". If you paste text that tries to redirect the skill, the worst case is your own output is weird. Don't deploy this in a multi-user setting without thinking that through. + ## Limitations - First call has the usual Claude Code startup cost (plugin sync, CLAUDE.md discovery, keychain read); subsequent calls are faster. diff --git a/integrations/streamlit/app.py b/integrations/streamlit/app.py index 7930b387..ada5e019 100644 --- a/integrations/streamlit/app.py +++ b/integrations/streamlit/app.py @@ -45,8 +45,16 @@ r"Rewritten text:|Output:|Result:)[^\n]*\n+", re.IGNORECASE, ) +# Match the humanizer skill's own trailing metadata blocks only. Require +# either a `---` separator + bold header, or a bolded skill-specific phrase +# ("Summary of changes", "Changes made", "What makes … AI …", "Draft rewrite", +# "Now make it not"). This avoids truncating legitimate prose that merely +# begins with a word like "Notes" or "Summary". _TRAILING_RE = re.compile( - r"\n+(?:---+\s*\n+)?\*{0,2}(?:Summary|Changes?|Notes?|What makes)[^\n]*\n[\s\S]*$", + r"\n+(?:---+\s*\n+)?\*{2}" + r"(?:Summary\s+of\s+changes?|Changes?\s+made|What\s+makes[^\n]*AI[^\n]*|" + r"Draft\s+rewrite|Now\s+make\s+it\s+not)" + r"[^\n]*\*{2}[\s\S]*$", re.IGNORECASE, ) _CODEFENCE_RE = re.compile(r"^\s*```[a-zA-Z]*\n(.*?)\n```\s*$", re.DOTALL) @@ -76,6 +84,16 @@ def _kill(proc): pass +@st.cache_resource +def _proc_holder(): + """Single shared handle for the current subprocess. `atexit` is + registered exactly once (via cache_resource) so it doesn't accumulate + across Streamlit reruns.""" + holder = {"proc": None} + atexit.register(lambda: _kill(holder["proc"])) + return holder + + st.set_page_config(page_title="Humanizer", layout="wide") st.title("Humanizer") st.caption( @@ -102,22 +120,26 @@ def _kill(proc): output_slot = st.empty() if go and text.strip(): - _kill(st.session_state.get("proc")) + holder = _proc_holder() + _kill(holder["proc"]) prompt = PROMPT_TEMPLATE.format(text=text) + # --no-session-persistence keeps each invocation fully ephemeral: no + # session file is written to disk, and no prior session can be picked + # up by the next click. Combined with the default absence of -c / -r, + # each Humanize click is a brand-new stateless call. proc = subprocess.Popen( - [CLAUDE_BIN, "-p", prompt], + [CLAUDE_BIN, "-p", "--no-session-persistence", prompt], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=0, ) - st.session_state["proc"] = proc - atexit.register(_kill, proc) + holder["proc"] = proc status_slot.info( - "⏳ Running humanizer skill via `claude -p`… first call has ~3–5 s " - "startup overhead, then streams." + "⏳ Running humanizer skill — first call has ~3–5 s startup " + "overhead, then output streams in." ) def stream(): diff --git a/integrations/streamlit/docker-compose.yml b/integrations/streamlit/docker-compose.yml index bca9975a..c37a4b40 100644 --- a/integrations/streamlit/docker-compose.yml +++ b/integrations/streamlit/docker-compose.yml @@ -4,18 +4,23 @@ services: image: humanizer-streamlit container_name: humanizer-streamlit ports: - - "8501:8501" + # Published on 127.0.0.1 only so the UI isn't exposed to the LAN. + # Remove the 127.0.0.1 prefix if you actually want LAN access AND + # have put authentication / network controls in front of it. + - "127.0.0.1:8501:8501" volumes: # Mount the host's Claude Code config so the container inherits: # - the installed `humanizer` skill (~/.claude/skills/humanizer/) # - any file-based auth tokens - # On macOS, OAuth credentials live in the Keychain (not in ~/.claude), - # so the first run may still need `docker compose exec humanizer claude /login`. - - ${HOME}/.claude:/root/.claude + # This is read-write because macOS users may need to run + # `claude /login` inside the container (see README), and that writes + # back to this directory. Treat the container as a trust-equivalent + # of the host for these credentials. + - ${HOME}/.claude:/home/app/.claude environment: # Fallback auth — uncomment (and export ANTHROPIC_API_KEY) to skip OAuth. # - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} - - HOME=/root + - HOME=/home/app stdin_open: true tty: true restart: unless-stopped From 6886463ccb5567029790278e8f46b51e3548bbf7 Mon Sep 17 00:00:00 2001 From: Richie627 Date: Thu, 23 Apr 2026 17:46:10 -0700 Subject: [PATCH 5/6] fix(streamlit): drop streaming pretense, use spinner + final render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `claude -p` text mode doesn't actually deliver token-by-token streaming through the PIPE in a way Streamlit's write_stream can render incrementally, so the "streaming" UX was a lie. Swap in a plain `proc.communicate()` inside st.spinner, render once on completion via st.code (which still has the native copy button). Also update README — lifecycle table + Output section no longer claim text "streams in live". Co-Authored-By: Claude Opus 4.7 (1M context) --- integrations/streamlit/README.md | 6 +++--- integrations/streamlit/app.py | 18 ++++-------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/integrations/streamlit/README.md b/integrations/streamlit/README.md index ab80ae16..e11bba29 100644 --- a/integrations/streamlit/README.md +++ b/integrations/streamlit/README.md @@ -72,8 +72,8 @@ The wrapper keeps **no** persistent `claude` session. Each click is self-contain |---|---| | You open the browser | Streamlit boots. **No `claude` process yet.** | | You paste text | Nothing happens — text just sits in the textarea. | -| You click **Humanize** | `app.py` spawns `claude -p ""` as a subprocess. Right panel shows a spinner + "Running humanizer skill…" status message, then streams the rewrite live as it arrives. | -| Generation finishes (~5–15 s) | Subprocess exits on its own. Status flips to "Done". Output is re-rendered in a code block with a native copy icon in the top-right corner. | +| You click **Humanize** | `app.py` spawns `claude -p ""` as a subprocess. Right panel shows a spinner + "Running humanizer skill…" status message. | +| Generation finishes (~5–15 s) | Subprocess exits on its own. Status flips to "Done". Output appears in a code block with a native copy icon in the top-right corner. | | You click **Humanize** again | Any still-running subprocess is killed first, then a fresh one spawns. | | You close **only** the browser tab | Streamlit server stays up. A subprocess that was already running **will finish on its own within seconds** (its stdout just gets discarded). **Closing the tab alone does not stop Streamlit.** | | You `Ctrl+C` the local terminal | Streamlit shuts down → `atexit` fires → any in-flight subprocess is killed → port released. | @@ -137,7 +137,7 @@ Yes. Two layers enforce this: 1. **Prompt-level:** The request wrapper explicitly tells Claude to output **only** the final rewritten prose — no draft, no "what makes it AI" audit, no summary of changes, no preamble, no markdown headers, no `---` separators, no surrounding quotes. 2. **Post-process-level:** Even if the model leaks a "Here is…" opener or the skill's default "**Summary of changes:**" trailer, `_clean_output()` in `app.py` strips them before the output block is populated. -During generation you'll see text stream into the right panel as it arrives. Once it finishes, that streamed view is replaced by a `st.code` block — this gives you a **native copy-to-clipboard icon in the top-right corner** of the output so you can paste the result straight into your doc. +While the subprocess is running you'll see a spinner and a status message. Once it finishes, the rewritten prose appears in a `st.code` block — this gives you a **native copy-to-clipboard icon in the top-right corner** of the output so you can paste the result straight into your doc. If you ever see leftover garbage in the cleaned output, that's a prompt-obedience failure — tighten `PROMPT_TEMPLATE` or extend `_PREAMBLE_RE` / `_TRAILING_RE` in `app.py`. diff --git a/integrations/streamlit/app.py b/integrations/streamlit/app.py index ada5e019..c1c0b7ec 100644 --- a/integrations/streamlit/app.py +++ b/integrations/streamlit/app.py @@ -133,38 +133,28 @@ def _proc_holder(): stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, - bufsize=0, ) holder["proc"] = proc status_slot.info( "⏳ Running humanizer skill — first call has ~3–5 s startup " - "overhead, then output streams in." + "overhead. Output appears when the run finishes." ) - def stream(): - while True: - chunk = proc.stdout.read(32) - if not chunk: - break - yield chunk - proc.wait() - with st.spinner("Humanizing…"): - raw = output_slot.write_stream(stream()) + stdout, stderr = proc.communicate() if proc.returncode != 0: - err = (proc.stderr.read() or "").strip() status_slot.empty() output_slot.error( f"`claude` exited with code {proc.returncode}.\n\n" f"Check that Claude Code is installed and logged in (`claude /login`) " f"or that `ANTHROPIC_API_KEY` is set.\n\n" - f"stderr:\n```\n{err}\n```" + f"stderr:\n```\n{(stderr or '').strip()}\n```" ) st.session_state.pop("final_text", None) else: - cleaned = _clean_output(raw or "") + cleaned = _clean_output(stdout or "") st.session_state["final_text"] = cleaned status_slot.success("Done. Use the copy icon in the top-right of the box below.") output_slot.code(cleaned, language=None, wrap_lines=True) From f4d381f7dbf66d9c38e032fbbdb66f92df9bb9c7 Mon Sep 17 00:00:00 2001 From: Richie627 Date: Thu, 23 Apr 2026 17:55:54 -0700 Subject: [PATCH 6/6] fix(streamlit): drop misleading 'No API key required' caption The app supports both OAuth login and ANTHROPIC_API_KEY paths, so phrasing the caption as "No API key required" understates the API-key path and can mislead users who already have a key set. Co-Authored-By: Claude Opus 4.7 (1M context) --- integrations/streamlit/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/streamlit/app.py b/integrations/streamlit/app.py index c1c0b7ec..2ac12095 100644 --- a/integrations/streamlit/app.py +++ b/integrations/streamlit/app.py @@ -98,7 +98,7 @@ def _proc_holder(): st.title("Humanizer") st.caption( "Paste text → click **Humanize** → get the final rewritten prose. " - "Uses your local `claude` CLI + the `humanizer` skill. No API key required." + "Uses your local `claude` CLI + the `humanizer` skill." ) col_in, col_out = st.columns(2)