Metadata-Version: 2.4
Name: scopuli
Version: 0.1.1
Summary: Static-HTML-site CMS with visual editor, AI edits, and surge.sh publishing
Author: Ikakke Ikpe
License: MIT
Project-URL: Homepage, https://github.com/ikakke/scopuli
Project-URL: Repository, https://github.com/ikakke/scopuli
Project-URL: Issues, https://github.com/ikakke/scopuli/issues
Project-URL: Changelog, https://github.com/ikakke/scopuli/blob/main/CHANGELOG.md
Keywords: cms,static-site,flask,surge,editor
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Flask
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: flask>=3.0
Requires-Dist: beautifulsoup4>=4.12
Requires-Dist: tomli>=2.0; python_version < "3.11"
Requires-Dist: tomli-w>=1.0
Requires-Dist: click>=8.1
Requires-Dist: requests>=2.31
Requires-Dist: werkzeug>=3.0.6
Requires-Dist: urllib3>=2.6.0
Provides-Extra: anthropic
Requires-Dist: anthropic>=0.40; extra == "anthropic"
Provides-Extra: openai
Requires-Dist: openai>=1.50; extra == "openai"
Provides-Extra: gemini
Requires-Dist: google-generativeai>=0.8; extra == "gemini"
Provides-Extra: all
Requires-Dist: anthropic>=0.40; extra == "all"
Requires-Dist: openai>=1.50; extra == "all"
Requires-Dist: google-generativeai>=0.8; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7.4; extra == "dev"
Requires-Dist: pytest-cov>=4.1; extra == "dev"
Requires-Dist: pip-audit>=2.7; extra == "dev"
Dynamic: license-file

# Scopuli

Turn a folder of static HTML/CSS/JS sites into a lightweight CMS. Edit
visually in the browser, push live to `surge.sh` (with optional custom
domain), and use AI for both per-element tweaks ("make this button red and
bigger") and whole-site work ("add `about.html` and `contact.html` matching
the home page style, then link to them from the hero").

The UI is themed with Radix Colors + Radix Themes design primitives, with a
vanilla-JS port of the Radix Select component (`RxSelect`) for searchable
dropdowns.

## Install

Pick whichever you have on hand:

```bash
uvx scopuli                          # zero-install — `uv` will fetch and run
pipx install scopuli                 # isolated, persistent install
pip  install scopuli                 # plain pip into your current env
```

Optional AI provider SDKs (install only the ones you'll use):

```bash
pip install "scopuli[anthropic]"     # or .[openai], .[gemini], .[all]
```

You'll also need the `surge` CLI for publishing (Node-based):

```bash
npm install -g surge
surge login        # one time
```

## Quickstart

```bash
cd ~/Sites           # or any folder you'd like to host sites from
scopuli              # http://127.0.0.1:5001 opens in your browser
```

On first run Scopuli **creates `./websites/example/`** with a small starter
site so you can immediately click **Edit** and try the editor. Drop your own
folders next to it later — anything containing an `index.html` (or any
`*.html`) shows up as a site card.

Other CLI options:

```bash
scopuli --port 8080 --host 0.0.0.0   # bind elsewhere (see Security)
scopuli --root /path/to/sites        # use a specific websites folder
scopuli --no-seed                    # skip auto-seed (empty folder)
scopuli --no-browser                 # don't auto-open the browser
python -m scopuli                    # alternative if the script isn't on PATH
```

A typical workspace ends up looking like:

```
my-workspace/
└── websites/
    ├── eros/
    │   ├── index.html
    │   ├── about.html
    │   ├── style.css
    │   └── img/
    │       └── logo.svg
    └── portfolio/
        └── index.html
```

## The home page

Each site card shows:

- **Primary domain** — the custom domain if you've set one, otherwise
  `<sub>.surge.sh`. When both are set, the custom domain shows on top with
  a small `via <sub>.surge.sh` caption underneath.
- **Edit** → opens the visual editor.
- **Publish** → runs `surge` against the site folder.
- **Open live** → opens the primary domain in a new tab.

Click **set / change** in the surge row to open the modal:

1. **Surge subdomain** (required): `your-name`.surge.sh.
2. **Custom domain** (optional): something like `myportfolio.com`. When set,
   Scopuli writes `<site>/CNAME` with a single line containing this domain;
   surge picks it up on publish and serves your site from it (you still
   need to point DNS at surge per their docs). Save with this field empty
   to remove an existing custom domain.

## The editor

The toolbar at the top: **← Scopuli** (back to home), the site name, the surge
domain pill, a **Preview** ↔ **Edit** toggle, an unsaved-changes pill,
**Discard**, **Save**, **Publish**.

### Preview / Edit toggle

Click **Preview** to disable selection so links navigate, forms submit, and
the iframe behaves like the live site. Click **Edit** to come back. The
mode persists across in-iframe link navigations.

### Side panel (Edit mode)

- Hover any element in the page → dashed outline.
- Click → solid outline, the **Style** tab populates.

The side panel has three tabs:

- **Style**: tweak text, color, background, font size, padding, margin,
  border radius. Changes apply live in the iframe.
- **Images**: lists every image found under
  `img/`, `imgs/`, `images/`, `assets/img/`, `assets/imgs/`,
  `assets/images/` inside the site directory. Click a thumbnail with an
  `<img>` selected to **replace its source**; click with any other element
  selected to **insert** the image as the last child. Newly inserted
  images become selectable after Save.
- **AI**: two scopes via a radio at the top.
  - **Selected element**: type "make this button red and twice as big",
    click Send. The active provider returns a patch; it's applied to the
    iframe and buffered for Save.
  - **Whole site** (works with no selection): type something like
    "I added 3 new pages — add thumbnail cards and links to them on the
    home page" or "Create a new `contact.html` that matches the style of
    `about.html`". The AI reads every `.html` in the site folder plus the
    image asset list and returns a **plan** of file writes (create or
    modify). Review the plan in the side panel and click **Apply changes**
    to write the files — nothing happens to disk until you do.

### Save / Publish

- **Save** rewrites the source HTML on disk (inline `style="…"` on the
  element, replaced `textContent` for plain-text edits, child node
  insertion for image inserts).
- **Publish** runs `surge <site_path> <surge_or_custom_domain>`.

## AI providers

Scopuli ships with four pluggable providers. Pick one from `/settings`:

| Provider  | Default model         | Auth                    |
|-----------|-----------------------|-------------------------|
| Anthropic | `claude-sonnet-4-6`   | API key                 |
| OpenAI    | `gpt-4o-mini`         | API key                 |
| Gemini    | `gemini-2.0-flash`    | API key                 |
| Ollama    | auto-detected         | none (local only)       |

Anthropic is the default. Switch any time from `/settings` — the active
provider is picked via a Radix segmented control.

You can set `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GEMINI_API_KEY` as
env vars; if the matching API key field in `/settings` is blank, the
provider falls back to the env var.

For **Ollama**, Scopuli queries the configured base URL's `/api/tags`
endpoint and presents installed models in a `RxSelect` dropdown. Install
new models with `ollama pull <name>` and they show up automatically; if
Ollama isn't reachable you get a friendly error and the previously-saved
model stays selectable.

## How edits persist

Edits **rewrite source HTML directly** in `websites/<site>/`. No overlay,
no build step. Element-scoped edits touch the entry HTML; site-scoped AI
edits can create or overwrite any `.html` file within the site folder.
There is no automatic undo — **commit `websites/` to git before any large
site-scoped prompt** so you can `git restore` if the AI takes the design
somewhere you don't want.

## Configuration

Created automatically on first launch at `~/.scopuli/config.toml`:

```toml
[ai]
provider = "anthropic"

[providers.anthropic]
model = "claude-sonnet-4-6"
api_key = ""           # blank → falls back to ANTHROPIC_API_KEY

[providers.openai]
model = "gpt-4o-mini"
api_key = ""
base_url = ""          # blank → default OpenAI

[providers.gemini]
model = "gemini-2.0-flash"
api_key = ""

[providers.ollama]
model = "llama3.2"
base_url = "http://localhost:11434"   # local only — no API key

[surge.sites]
eros = "eros.surge.sh"
```

Per-site custom domains live next to the site, not in this config:
`websites/<site>/CNAME` (single line, the custom domain).

## CLI

```
scopuli [--host 127.0.0.1] [--port 5001]
        [--root websites] [--no-seed]
        [--no-browser] [--debug]
```

No subcommands. `scopuli --help` prints the full options list.

## Security

- Default bind is `127.0.0.1`. The editor accepts arbitrary HTML/CSS
  patches and the AI endpoint will spend whatever API credit your key has
  — **do not expose Scopuli to the open internet**. If you bind `0.0.0.0`,
  put it behind a firewall or VPN.
- API keys live in `~/.scopuli/config.toml` in plaintext. `chmod 600` is
  recommended.
- Path-traversal is guarded on every file route; only
  `./websites/<site>/…` paths are reachable.
- The site-AI Apply step writes only `.html` files within the site folder
  (CSS / JS / image overwrites are refused per-entry on the server).

## Project layout

```
scopuli/
├── app.py            # Flask app factory
├── cli.py            # `scopuli` entry point
├── __main__.py       # `python -m scopuli` entry point
├── config.py         # ~/.scopuli/config.toml (tomli / tomllib)
├── routes.py         # HTML + /api/* blueprint
├── sites.py          # per-site discovery, traversal guards
├── pages.py          # per-page (.html) discovery + read/write
├── assets.py         # image asset discovery
├── editor.py         # HTML annotation + patch application
├── surge.py          # surge CLI wrapper + CNAME helpers
├── ai/
│   ├── base.py       # AIProvider + edit/compose contracts
│   ├── anthropic.py
│   ├── openai.py
│   ├── gemini.py
│   └── ollama.py
├── templates/
│   ├── base.html
│   ├── home.html
│   ├── editor_shell.html
│   └── settings.html
└── static/
    ├── radix-theme.css   # Radix Colors + Themes design tokens
    ├── base.css          # global components (buttons, fields, modals)
    ├── home.css          # site cards + surge modal
    ├── shell.css         # editor toolbar + side panel
    ├── shell.js          # parent-frame editor controller
    ├── settings.css
    ├── settings.js       # provider config + Ollama auto-detect
    ├── rx-select.css     # vanilla Radix Select component styles
    ├── rx-select.js      # vanilla Radix Select component controller
    ├── editor.css        # injected into edited pages (outlines)
    └── editor.js         # injected into edited pages (selection, postMessage)
```

See `PRD.md` for the design rationale and `CHANGELOG.md` for release notes.

## License

MIT.
