Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Core product traits right now:
- static-export friendly
- fragment-based transport so artifact contents stay out of the request URL and off the server request path

The self-hosted variant at `selfhosted/` is an optional add-on that reuses the same viewer with SQLite-backed UUID links. The static fragment-based app remains the primary product.

## Product contract

Treat these as core constraints unless the owner explicitly changes the product direction.
Expand Down Expand Up @@ -128,6 +130,18 @@ If you change the payload contract, update the code, docs, examples, and the Ope
### Diff handling
- `src/lib/diff/git-patch.ts` - patch parsing support for diff rendering

### Self-hosted variant
- `selfhosted/` - separate Next.js app for the server-backed variant
- `selfhosted/src/lib/db.ts` - SQLite database initialization
- `selfhosted/src/lib/artifacts.ts` - artifact CRUD operations with TTL
- `selfhosted/src/app/api/artifacts/route.ts` - POST create endpoint
- `selfhosted/src/app/api/artifacts/[id]/route.ts` - GET/DELETE/PUT endpoints
- `selfhosted/src/app/api/cleanup/route.ts` - expired row cleanup endpoint
- `selfhosted/src/app/artifact/[id]/page.tsx` - server-rendered artifact viewer page
- `src/components/selfhosted-viewer-shell.tsx` - client shell accepting envelope prop
- `selfhosted/docker-compose.yml` - Docker Compose deployment config
- `skills/selfhosted-agent-render/SKILL.md` - self-hosted skill documentation

### Docs and external contract
- `README.md`
- `docs/architecture.md`
Expand Down Expand Up @@ -208,6 +222,7 @@ If any of these change, check the rest in the same pass:
- renderer capabilities
- local commands
- deployment assumptions
- `skills/selfhosted-agent-render/SKILL.md`

At minimum, verify alignment across:
- `README.md`
Expand All @@ -217,6 +232,7 @@ At minimum, verify alignment across:
- `docs/dependency-notes.md`
- `docs/testing.md`
- `skills/agent-render-linking/SKILL.md`
- `skills/selfhosted-agent-render/SKILL.md`

## Default contributor stance

Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@ Built for the OpenClaw ecosystem, `agent-render` focuses on fragment-based shari
- `csv` - parsed table view with sticky headers and horizontal overflow handling
- `json` - lightweight read-only tree view plus raw code view, with graceful malformed JSON fallback

The self-hosted variant reuses these same renderers.

## Principles

- Fully static export with Next.js App Router
- No backend, no database, no server-side persistence
- Fragment-based payloads (`#...`) so the server never receives artifact contents
- Public-safe naming and MIT-compatible dependencies
- Optional self-hosted variant available for UUID-based links with server persistence

## Local Development

Expand Down Expand Up @@ -83,6 +86,20 @@ The shell keeps first load lean and defers renderer-heavy code until needed. The
- `docs/dependency-notes.md` - major dependency and license notes
- `docs/testing.md` - test commands, screenshot workflow, and CI notes

## Self-Hosted Variant

An optional self-hosted, server-backed variant lives at `selfhosted/`. It reuses the same viewer and renderers as the static app but stores payloads in SQLite under UUIDs.

- Links use `https://{host}/{uuid}` instead of fragment URLs
- No fragment length limits
- 24-hour sliding TTL
- Simple REST API for create, read, and delete
- Deployment: `cd selfhosted && npm install && npm run build && npm run start` or Docker Compose

This is an optional add-on, not the default product. The static fragment-based app remains the primary deployment mode.

See `skills/selfhosted-agent-render/SKILL.md` for full deployment and usage guidance.

## Zero Retention

The project keeps artifact contents in the URL fragment so the static host does not receive the payload during the page request. This improves privacy for shared artifacts, but the link still lives in browser history, copied URLs, and any client-side telemetry you add later.
Expand Down
35 changes: 35 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,38 @@ The static host does not receive fragment contents as part of the request, but t
- GitHub Pages-compatible `basePath` and `assetPrefix`
- `.nojekyll` included for Pages compatibility
- Fragment size budget enforced before render

## Self-Hosted Variant

A separate Next.js app lives at `selfhosted/` in the same repo. It is an optional add-on; the static fragment-based app remains the primary product.

### Component sharing

The self-hosted app reuses the same viewer renderers and shell components from `src/`. Two webpack aliases make this work:

- `@shared` maps to the parent `src/` so the self-hosted app can import renderers and shell components directly.
- `@` also maps to parent `src/` so that internal imports inside shared components resolve correctly.

The self-hosted Next.js config sets `transpilePackages` so shared source is compiled without a separate build step.

### Server-rendered viewer route

`selfhosted/src/app/artifact/[id]/page.tsx` is a server-rendered route. It fetches the payload from SQLite, parses it into a `PayloadEnvelope`, and passes it to a new `SelfHostedViewerShell` component at `src/components/selfhosted-viewer-shell.tsx`. That shell accepts a pre-fetched `PayloadEnvelope` prop instead of reading from the URL fragment, which keeps the static app's fragment-driven contract untouched.

### Persistence

SQLite persistence is provided by `better-sqlite3` with a single `artifacts` table.

### REST API

- `POST /api/artifacts` — create a new artifact
- `GET /api/artifacts` — read an artifact
- `DELETE /api/artifacts` — delete an artifact
- `PUT /api/artifacts` — update an artifact
- `POST /api/cleanup` — remove expired rows

Every successful GET/view extends `expires_at` by 24 hours (sliding TTL). The cleanup endpoint removes rows whose `expires_at` has passed.

### Hosting config

The self-hosted app does **not** use `output: "export"`. It uses `output: "standalone"` so it can run server-rendered routes backed by SQLite.
9 changes: 9 additions & 0 deletions docs/dependency-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,12 @@

- `rehype-highlight` was removed after review because markdown fences now reuse the CodeMirror viewer stack directly.
- `vanilla-jsoneditor` was removed because its bundle cost was too high for the default JSON tree-view use case in a viewer-first product.

## Self-Hosted Variant Dependencies

- `better-sqlite3` - MIT - synchronous SQLite3 bindings for Node.js
- `uuid` - MIT - UUID v4 generation for artifact identifiers
- `@types/better-sqlite3` - MIT (dev)
- `@types/uuid` - MIT (dev)

These dependencies are only installed in the `selfhosted/` directory and do not affect the static app's dependency tree.
38 changes: 38 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,41 @@ Cloudflare Pages works well with the current project shape.
- Environment variable: set `NEXT_PUBLIC_BASE_PATH` only if you intentionally deploy under a subpath

If you deploy at the domain root on Cloudflare Pages, leave `NEXT_PUBLIC_BASE_PATH` unset.

## Self-Hosted Variant

The self-hosted deployment is a separate Next.js server app that adds a REST API and SQLite-backed artifact storage. It does NOT use static export.

### Direct process

```bash
cd selfhosted
npm install
npm run build
ARTIFACTS_DB_PATH=./data/artifacts.db npm run start
# Runs on http://localhost:3001
```

### Docker Compose

```bash
cd selfhosted
docker compose up -d
```

### pm2

```bash
cd selfhosted
npm install && npm run build
pm2 start npm --name agent-render -- run start
```

### Key details

- SQLite database at `./data/artifacts.db` (configurable via `ARTIFACTS_DB_PATH`)
- Runs on port 3001 by default
- 24-hour sliding TTL on all stored artifacts
- No built-in auth; use perimeter protection (localhost binding, Cloudflare Tunnel + Zero Trust, reverse proxy with auth, or VPN/Tailscale)
- The self-hosted app is a separate Next.js server app; it does NOT use static export
- See `skills/selfhosted-agent-render/SKILL.md` for the full self-hosted skill
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const compat = new FlatCompat({
const config = [...compat.extends("next/core-web-vitals")];

config.push({
ignores: ["out/**", ".next/**"],
ignores: ["out/**", ".next/**", "selfhosted/**"],
});

export default config;
3 changes: 3 additions & 0 deletions selfhosted/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
data/
.next/
node_modules/
27 changes: 27 additions & 0 deletions selfhosted/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
FROM node:20-slim AS base
WORKDIR /app

# Install dependencies
COPY package.json package-lock.json ./
RUN npm ci

# Copy source
COPY . .

# Build
RUN npm run build

# Production
FROM node:20-slim AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV PORT=3001

COPY --from=base /app/.next/standalone ./
COPY --from=base /app/.next/static ./.next/static
COPY --from=base /app/public ./public

EXPOSE 3001

CMD ["node", "server.js"]
14 changes: 14 additions & 0 deletions selfhosted/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
services:
agent-render:
build: .
ports:
- "3001:3001"
volumes:
- artifact-data:/app/data
environment:
- NODE_ENV=production
- ARTIFACTS_DB_PATH=/app/data/artifacts.db
restart: unless-stopped

volumes:
artifact-data:
18 changes: 18 additions & 0 deletions selfhosted/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { FlatCompat } from "@eslint/eslintrc";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({
baseDirectory: __dirname,
});

const config = [...compat.extends("next/core-web-vitals")];

config.push({
ignores: [".next/**", "data/**"],
});

export default config;
5 changes: 5 additions & 0 deletions selfhosted/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
24 changes: 24 additions & 0 deletions selfhosted/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { NextConfig } from "next";
import path from "node:path";

const parentSrc = path.resolve(__dirname, "../src");

const nextConfig: NextConfig = {
output: "standalone",
images: {
unoptimized: true,
},
transpilePackages: ["@shared"],
webpack: (config) => {
config.resolve = config.resolve ?? {};
config.resolve.alias = {
...(config.resolve.alias ?? {}),
"@shared": parentSrc,
"@": parentSrc,
"@self": path.resolve(__dirname, "src"),
};
return config;
},
};

export default nextConfig;
Loading
Loading