Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ local/
*.db-shm
.DS_Store
bun.lockb
chat-ui/node_modules
chat-ui/dist
chat-ui/.vite
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ ANTHROPIC_API_KEY=
# Owner's Slack user ID (starts with U) - only this user can talk to Phantom
# OWNER_SLACK_USER_ID=

# ========================
# OPTIONAL: Web Chat Login
# ========================
# If Slack is not configured, Phantom can send you a magic link email
# to log into the web chat at /chat. Set your email and a Resend API key.
# Without Resend, a bootstrap token is printed to container logs instead.

# OWNER_EMAIL=
# RESEND_API_KEY is also used for the phantom_email tool (see below).

# ========================
# OPTIONAL: Identity
# ========================
Expand Down
33 changes: 29 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Phantom

Phantom is an autonomous AI co-worker that runs as a persistent Bun process on a VM. It wraps the Claude Agent SDK as a subprocess (Anthropic by default, swappable via a `provider:` config block to Z.AI/GLM-5.1, OpenRouter, Ollama, vLLM, LiteLLM, or any Anthropic Messages API compatible endpoint). It maintains vector-backed memory across sessions, rewrites its own configuration through a validated self-evolution engine, communicates via Slack/Telegram/Email/Webhook, and exposes all capabilities as an MCP server. 27,000+ lines of TypeScript, 875 tests, v0.18.2. Apache 2.0, repo at ghostwright/phantom.
Phantom is an autonomous AI co-worker that runs as a persistent Bun process on a VM. It wraps the Claude Agent SDK as a subprocess (Anthropic by default, swappable via a `provider:` config block to Z.AI/GLM-5.1, OpenRouter, Ollama, vLLM, LiteLLM, or any Anthropic Messages API compatible endpoint). It maintains vector-backed memory across sessions, rewrites its own configuration through a validated self-evolution engine, communicates via Slack/Web Chat/Telegram/Email/Webhook, and exposes all capabilities as an MCP server. 30,000+ lines of TypeScript, 1,584 tests, v0.19.0. Apache 2.0, repo at ghostwright/phantom.

## Tech Stack

Expand All @@ -10,7 +10,8 @@ Phantom is an autonomous AI co-worker that runs as a persistent Bun process on a
| Agent | Claude Agent SDK (`@anthropic-ai/claude-agent-sdk`) subprocess. Provider is configurable via `src/config/providers.ts`: Anthropic (default), Z.AI, OpenRouter, Ollama, vLLM, LiteLLM, custom. |
| Memory | Qdrant (vector DB, Docker) + Ollama (nomic-embed-text, local embeddings) |
| State | SQLite via Bun (sessions, tasks, metrics, evolution versions, scheduled jobs) |
| Channels | Slack (Socket Mode, primary), Telegram (long polling), Email (IMAP/SMTP), Webhook (HMAC-SHA256), CLI |
| Channels | Slack (Socket Mode), Web Chat (SSE streaming), Telegram (long polling), Email (IMAP/SMTP), Webhook (HMAC-SHA256), CLI |
| Chat Client | React 19 + Vite + shadcn/ui + Tailwind v4 SPA at `/chat` |
| Web UI | Tailwind v4 Browser CDN + DaisyUI v5, static files from public/ |
| MCP | Streamable HTTP on /mcp, bearer token auth, 17+ tools |
| Infrastructure | Docker (compose), Specter VMs (Hetzner), systemd (bare metal) |
Expand Down Expand Up @@ -41,13 +42,22 @@ If you find yourself writing a function that does something the agent can do bet

```bash
bun install # Install dependencies
bun test # Run 875 tests
bun test # Run 1,584 tests
bun run src/index.ts # Start the server
bun run src/cli/main.ts init --yes # Initialize config (reads env vars)
bun run src/cli/main.ts doctor # Check all subsystems
bun run src/cli/main.ts status # Quick one-liner status
bun run lint # Biome check
bun run typecheck # tsc --noEmit

# Chat UI (separate build)
cd chat-ui && bun install # Install chat-ui dependencies
cd chat-ui && bun run build # Build production SPA to chat-ui/dist/
cd chat-ui && bun run typecheck # Type-check the chat client
cd chat-ui && bun run dev # Dev server on :5173 (proxy to :3100)

# Chat-UI dev loop: run Phantom on :3100 in one terminal, Vite on :5173 in another.
# Vite proxies /chat/* API calls to :3100. Open http://localhost:5173/chat.
```

## Project Structure
Expand All @@ -60,6 +70,19 @@ src/
prompt-assembler.ts # System prompt: base + role + evolved + memory context
hooks.ts # Safety hooks (dangerous command blocker, file tracker)
in-process-tools.ts # In-process MCP tool servers (dynamic, scheduler, web UI)
chat/
http.ts # SSE endpoint, session management, message routing
http-handlers.ts # Request handlers for chat API routes
types.ts # 32-event SSE wire format (discriminated union)
sdk-to-wire.ts # Translates Agent SDK events to chat wire frames
session-store.ts # In-memory chat session state
message-store.ts # Persistent message history (SQLite)
stream-bus.ts # Fan-out SSE event bus (multi-tab support)
upload.ts # File attachment upload handler
validators.ts # Type allowlist and size limits for attachments
first-run.ts # Email-based first login (Resend)
email-login.ts # Magic link email delivery
notifications/ # Web Push (VAPID keys, subscriptions, triggers)
channels/
slack.ts # Slack Socket Mode (primary channel, owner access control)
telegram.ts # Telegram via Telegraf
Expand Down Expand Up @@ -116,6 +139,7 @@ src/
config/ # YAML configs (phantom.yaml, channels.yaml, mcp.yaml, roles/)
phantom-config/ # Evolved agent config (constitution, persona, domain knowledge)
public/ # Web UI files (_base.html template, index.html)
chat-ui/ # React 19 SPA (Vite + shadcn + Tailwind v4). Built to public/chat/
scripts/
install.sh # Standalone install script for Ubuntu/Debian
docker-entrypoint.sh # Docker bootstrap (wait for deps, model pull, init)
Expand All @@ -124,7 +148,7 @@ docs/ # Documentation (architecture, channels, mcp, security,

## Architecture Overview

Message flow: Slack message -> SlackChannel adapter -> ChannelRouter -> SessionManager (find/create session) -> PromptAssembler (base + role + evolved config + memory context) -> AgentRuntime.query() (Opus 4.6 with full tools) -> response -> ChannelRouter -> Slack thread reply.
Message flow: Slack message -> SlackChannel adapter -> ChannelRouter -> SessionManager (find/create session) -> PromptAssembler (base + role + evolved config + memory context) -> AgentRuntime.query() (Opus 4.6 with full tools) -> response -> ChannelRouter -> Slack thread reply. Web chat uses the same flow: POST /chat/sessions/:id/message -> SSE stream of wire frames -> React client renders in real time. Two separate transcripts (wire-format message store for the client, SDK conversation for the agent) are kept in sync.

After each session: EvolutionEngine runs 6-step reflection pipeline -> 5-gate validation -> approved changes applied to phantom-config/ -> version bumped.

Expand Down Expand Up @@ -193,6 +217,7 @@ Production deployments are managed internally. Do NOT modify production deployme
| `src/channels/slack.ts` | Primary channel. Owner access control, threading, reactions. |
| `src/mcp/server.ts` | MCP server setup. Tool registration, auth integration. |
| `src/memory/system.ts` | Memory coordinator. How the three tiers connect. |
| `src/chat/http.ts` | Web chat backend. SSE streaming, session routing, API handlers. |
| `src/core/server.ts` | HTTP server. Routes, health endpoint, version. |
| `config/roles/swe.yaml` | SWE role template. Onboarding questions, tools, evolution focus. |
| `phantom-config/constitution.md` | Immutable principles the evolution engine cannot modify. |
Expand Down
7 changes: 6 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ If you are unsure whether something belongs in TypeScript or in a prompt, open a
## Running Tests

```bash
# Run the full suite (770 tests)
# Run the full suite (1,584 tests)
bun test

# Run a single test file
Expand All @@ -101,6 +101,11 @@ bun run lint

# Typecheck
bun run typecheck

# Chat UI (separate build, separate package.json)
cd chat-ui && bun install # Install chat-ui dependencies
cd chat-ui && bun run build # Build production SPA
cd chat-ui && bun run typecheck # Type-check the chat client
```

All three must pass before submitting a PR: tests, lint, and typecheck.
Expand Down
7 changes: 5 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ RUN DPKG_ARCH=$(dpkg --print-architecture) && \
# Claude Code CLI refuses --dangerously-skip-permissions when running as root,
# so the container MUST run as a non-root user. Docker socket access is granted
# via group_add in docker-compose.yaml (matching the host's docker GID).
RUN groupadd --system phantom && \
useradd --system --gid phantom --create-home --home-dir /home/phantom phantom && \
RUN groupadd --system --gid 999 phantom && \
useradd --system --uid 999 --gid phantom --create-home --home-dir /home/phantom phantom && \
mkdir -p /home/phantom/.claude && \
chown -R phantom:phantom /home/phantom

Expand Down Expand Up @@ -124,6 +124,9 @@ RUN mkdir -p /app/data /app/repos && \
# Backup phantom-config defaults so they survive empty volume mount
RUN cp -r /app/phantom-config /app/phantom-config-defaults

# Backup image-bundled public assets for entrypoint seeding
RUN cp -r /app/public /app/public-defaults

# Make entrypoint executable
RUN chmod +x /app/scripts/docker-entrypoint.sh

Expand Down
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

<p align="center">
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-blue.svg" alt="License"></a>
<img src="https://img.shields.io/badge/tests-875%20passed-brightgreen.svg" alt="Tests">
<img src="https://img.shields.io/badge/tests-1584%20passed-brightgreen.svg" alt="Tests">
<a href="https://hub.docker.com/r/ghostwright/phantom"><img src="https://img.shields.io/docker/pulls/ghostwright/phantom.svg" alt="Docker Pulls"></a>
<img src="https://img.shields.io/badge/version-0.18.2-orange.svg" alt="Version">
<img src="https://img.shields.io/badge/version-0.19.0-orange.svg" alt="Version">
</p>

<p align="center">
Expand All @@ -27,7 +27,7 @@ AI agents today are disposable. You open a chat, get an answer, close the tab, a

Phantom takes a different approach: **give the AI its own computer.** A dedicated machine where it installs software, spins up databases, builds dashboards, remembers what you told it last week, and gets measurably better at your job every day. Your laptop stays yours. The agent's workspace is its own.

This is not a chatbot. It is a co-worker that runs on Slack, has its own email address, creates its own tools, and builds infrastructure without asking for permission. Don't take our word for it - scroll down to see what production Phantoms have actually built.
This is not a chatbot. It is a co-worker that runs on Slack, has a web chat interface at `/chat`, has its own email address, creates its own tools, and builds infrastructure without asking for permission. Don't take our word for it - scroll down to see what production Phantoms have actually built.

## What This Actually Looks Like

Expand Down Expand Up @@ -220,6 +220,7 @@ Because the agent that can only use pre-built tools hits a ceiling. Phantom buil
| **Dynamic tools** | Creates and registers its own MCP tools at runtime. Tools survive restarts and work across sessions. |
| **Encrypted secrets** | AES-256-GCM encrypted forms with magic-link auth. No plain-text credentials in config files. |
| **Email identity** | Every Phantom has its own email address. Send reports to people outside your Slack workspace. |
| **Web chat** | A full browser-based chat client at `/chat` with SSE streaming, file attachments, and Web Push notifications. No Slack required. |
| **Shareable pages** | Generates dashboards and tools on a public URL with auth. Share a link, anyone can see it. |
| **MCP server** | Claude Code connects to your Phantom. Other Phantoms connect to your Phantom. It is an API, not a dead end. |

Expand All @@ -238,10 +239,10 @@ Because the agent that can only use pre-built tools hits a ceiling. Phantom buil
| |
| Channels Agent Runtime |
| Slack query() + hooks |
| Telegram Prompt Assembler |
| Email base + role + evolved |
| Webhook + memory context |
| CLI |
| Web Chat Prompt Assembler |
| Telegram base + role + evolved |
| Email + memory context |
| Webhook / CLI |
| |
| Memory System Self-Evolution Engine |
| Qdrant 6-step pipeline |
Expand Down Expand Up @@ -379,7 +380,7 @@ bun run phantom start
```

```bash
bun test # 770 tests
bun test # 1584 tests
bun run lint # Biome
bun run typecheck # tsc --noEmit
```
Expand Down
3 changes: 3 additions & 0 deletions docker-compose.user.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ services:
ollama:
condition: service_started
restart: unless-stopped
oom_score_adj: -500
cpu_shares: 2048
deploy:
resources:
limits:
Expand All @@ -69,6 +71,7 @@ services:
# consolidation) spawned via runJudgeQuery. The prior 2 GiB cap
# cgroup-OOM-killed judge subprocesses under evolution load.
memory: 8G
pids: 256
reservations:
memory: 512M
networks:
Expand Down
22 changes: 18 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,26 @@ Phantom is a single Bun process that runs on a VM. It combines an agent runtime,

### HTTP Server

`src/core/server.ts` - Bun.serve() on port 3100. Three routes:
`src/core/server.ts` - Bun.serve() on port 3100. Key routes:
- `/health` - JSON health status (status, uptime, version, channels, memory, evolution)
- `/mcp` - MCP Streamable HTTP endpoint
- `/webhook` - Inbound webhook receiver
- `/chat/*` - Web chat API (SSE streaming, sessions, attachments, push subscriptions)
- `/ui/*` - Static pages and login (magic link auth)

### Channel Router

`src/channels/router.ts` - Multiplexes messages from all connected channels. Each channel implements the `Channel` interface: `connect()`, `disconnect()`, `send()`, `onMessage()`.

Channels: Slack (Socket Mode), Telegram (long polling), Email (IMAP/SMTP), Webhook (HTTP), CLI (readline).
Channels: Slack (Socket Mode), Web Chat (SSE streaming at `/chat`), Telegram (long polling), Email (IMAP/SMTP), Webhook (HTTP), CLI (readline).

### Web Chat Channel

`src/chat/` - A full browser-based chat channel with a React 19 SPA at `/chat`. The backend uses Server-Sent Events (SSE) to stream a 32-event wire format from the Agent SDK to the client in real time. Two independent transcripts are maintained: the wire-format message store (what the client sees) and the SDK conversation (what the agent sees). This two-transcript invariant means the client can render markdown, tool calls, thinking blocks, and subagent progress without coupling to SDK internals.

Auth uses cookie-based sessions with magic link login. On first run without Slack, a login email is sent via Resend (or a bootstrap token is printed to stdout). Web Push notifications (VAPID) alert users when the agent responds while the tab is in the background.

File attachments (images, PDFs, text files) are uploaded via multipart POST and passed to the agent as context. Type allowlist and size limits are enforced server-side.

### Agent Runtime

Expand Down Expand Up @@ -114,7 +124,7 @@ Embeddings via Ollama (nomic-embed-text, 768d vectors). Hybrid search using dens

## Data Flow

1. Message arrives via channel (Slack mention, webhook POST, etc.)
1. Message arrives via channel (Slack mention, webhook POST, web chat, etc.)
2. Channel router normalizes to `InboundMessage`
3. Session manager finds or creates a session
4. Prompt assembler builds the full system prompt
Expand All @@ -123,6 +133,8 @@ Embeddings via Ollama (nomic-embed-text, 768d vectors). Hybrid search using dens
7. Memory consolidation runs (non-blocking)
8. Evolution pipeline runs (non-blocking)

For web chat specifically: the client sends `POST /chat/sessions/:id/message`, the server starts an Agent SDK `query()`, and SDK events are translated to wire frames and pushed to all connected SSE streams for that session (supporting multi-tab). The wire format includes session lifecycle, text streaming, thinking blocks, tool calls with input streaming, and subagent progress.

## Technology Stack

| Component | Technology |
Expand All @@ -132,7 +144,8 @@ Embeddings via Ollama (nomic-embed-text, 768d vectors). Hybrid search using dens
| Vector DB | Qdrant (Docker) |
| Embeddings | Ollama (nomic-embed-text) |
| State DB | SQLite (Bun built-in) |
| Channels | Slack Bolt, Telegraf, ImapFlow, Nodemailer |
| Channels | Slack Bolt, Web Chat (SSE), Telegraf, ImapFlow, Nodemailer |
| Chat Client | React 19, Vite, shadcn/ui, Tailwind v4 |
| Config | YAML + Zod validation |
| Process | systemd (on Specter VMs) |

Expand All @@ -141,6 +154,7 @@ Embeddings via Ollama (nomic-embed-text, 768d vectors). Hybrid search using dens
```
src/
agent/ - Runtime, prompt assembler, hooks, cost tracking
chat/ - Web chat backend (SSE streaming, sessions, attachments, push notifications)
channels/ - Slack, Telegram, Email, Webhook, CLI, status reactions
cli/ - CLI commands (init, start, doctor, token, status)
config/ - YAML config loaders, Zod schemas
Expand Down
46 changes: 45 additions & 1 deletion docs/channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,53 @@

Phantom communicates through pluggable channel adapters. Each channel implements a standard interface, and the agent does not care where messages originate.

## Web Chat

A browser-based chat interface at `/chat`. No Slack required. This is the simplest way to talk to a Phantom - open the URL in a browser and start typing.

### Access

Navigate to `https://your-phantom-host/chat`. On first visit, you will be prompted to log in.

### Authentication

Cookie-based sessions with magic link login:

1. Enter your email address on the login page
2. Phantom sends a magic link via Resend (requires `RESEND_API_KEY` in `.env`)
3. Click the link to authenticate. The session cookie lasts 30 days.

On first run without Slack configured, Phantom sends a login email to `OWNER_EMAIL` automatically. If Resend is not configured, a bootstrap token is printed to stdout instead.

### Configuration

Set these in `.env`:

```
[email protected] # Required for email-based login
RESEND_API_KEY=re_... # Required for magic link emails
```

No channel YAML configuration is needed. The chat channel is always available when the HTTP server is running.

### Features

- **SSE streaming** - responses stream token-by-token via Server-Sent Events
- **32-event wire format** - session lifecycle, text, thinking blocks, tool calls with input streaming, subagent progress
- **Multi-tab support** - open the same session in multiple tabs, all stay in sync
- **File attachments** - upload images (JPEG, PNG, GIF, WebP up to 10 MB), PDFs (up to 32 MB), and text/code files (up to 1 MB). Up to 10 files per message.
- **Web Push notifications** - get notified when the agent responds while the tab is in the background. Uses VAPID keys stored in SQLite.
- **Session management** - create, rename, archive, and delete sessions from the sidebar
- **Markdown rendering** - full markdown with code syntax highlighting, tables, and lists
- **Auto-rename** - sessions are automatically titled based on the first exchange

### Tech Stack

The chat client is a React 19 SPA built with Vite, shadcn/ui, and Tailwind v4. The production build lives at `public/chat/` and is served as static files. The Dockerfile includes a dedicated build stage for the chat client.

## Slack

The primary channel. Uses Socket Mode (no public URL required).
Uses Socket Mode (no public URL required).

### Setup (App Manifest)

Expand Down
Loading
Loading