From 385d7035eed19c5d301c72a7d64c9551ff05a4c5 Mon Sep 17 00:00:00 2001 From: raynr7 Date: Wed, 25 Mar 2026 20:55:49 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20agent=20hygiene=20P0/P1=20=E2=80=94=20gi?= =?UTF-8?q?tignore,=20DATABASE=5FURL,=20CI=20lint,=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .gitignore: replace single .vercel entry with full ruleset covering node_modules, dist, .svelte-kit, .env*, *.db files, logs, OS, editor - src/main.ts: read DATABASE_URL env var for SQLite path (fallback to agentswarp.db) so the documented env var is actually respected - ci.yml: add bun lint and bun format --check before test/build steps; fix Windows CRLF line endings in the file - swarm-core.md: delete 180 KB raw code dump; replace with structured docs/phase-2-migration.md covering file inventory, memory budget, new env vars, and integration checklist - AGENTS.md: create agent guidance file (repo map, conventions, architecture constraints, tool-addition workflow, PR checklist) - AGENTS-IMPROVEMENT-SPEC.md: audit findings and remaining improvement backlog (P2/P3 items not yet implemented) Co-authored-by: Ona --- .github/workflows/ci.yml | 42 +- .gitignore | 36 + AGENTS-IMPROVEMENT-SPEC.md | 165 + AGENTS.md | 140 + docs/phase-2-migration.md | 95 + src/main.ts | 5 +- swarm-core.md | 7599 ------------------------------------ 7 files changed, 461 insertions(+), 7621 deletions(-) create mode 100644 AGENTS-IMPROVEMENT-SPEC.md create mode 100644 AGENTS.md create mode 100644 docs/phase-2-migration.md delete mode 100644 swarm-core.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f12c0e3..d7572b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,20 +1,22 @@ -name: CI -on: [push, pull_request] -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v1 - - run: bun install - - run: bun test - - run: bun run build - - docker: - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' - steps: - - uses: actions/checkout@v4 - - uses: docker/setup-buildx-action@v3 - - name: Build Docker image - run: docker build -t agentswarm/agentswarm:latest . +name: CI +on: [push, pull_request] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v1 + - run: bun install + - run: bun lint + - run: bun format --check + - run: bun test + - run: bun run build + + docker: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - name: Build Docker image + run: docker build -t agentswarm/agentswarm:latest . diff --git a/.gitignore b/.gitignore index 5a3590c..3041dae 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,37 @@ +# Dependencies +node_modules/ + +# Build output +dist/ +.next/ +.svelte-kit/ +build/ + +# Environment — never commit secrets +.env +.env.local +.env.*.local + +# Database files +*.db +*.db-shm +*.db-wal +data/ + +# Logs +*.log +npm-debug.log* +bun-debug.log* + +# OS +.DS_Store +Thumbs.db + +# Editor +.vscode/ +.idea/ +*.swp +*.swo + +# Vercel .vercel diff --git a/AGENTS-IMPROVEMENT-SPEC.md b/AGENTS-IMPROVEMENT-SPEC.md new file mode 100644 index 0000000..2bf98b6 --- /dev/null +++ b/AGENTS-IMPROVEMENT-SPEC.md @@ -0,0 +1,165 @@ +# AGENTS.md Improvement Spec + +This document records the audit findings and the concrete changes made or required to bring agent guidance up to a useful standard. + +--- + +## Audit Summary + +### What exists + +| Artifact | Location | Quality | +|----------|----------|---------| +| `AGENTS.md` | — | **Missing** (created in this session) | +| Agent skill files (`.ona/skills/`, `.cursor/rules/`) | — | **Missing** | +| `CONTRIBUTING.md` | `/CONTRIBUTING.md` | Good baseline; lacks agent-specific detail | +| `README.md` | `/README.md` | Good user-facing overview; not agent-oriented | +| `swarm-core.md` | `/swarm-core.md` | Phase 2 code reference dump; not structured guidance | +| `docs/` | `/docs/` | Two useful docs (`how-it-works.md`, `tools.md`); no index | +| `.devcontainer/devcontainer.json` | `/.devcontainer/` | Uses heavy universal image; no Bun feature pinned | +| `.gitignore` | `/.gitignore` | Only contains `.vercel` — critically incomplete | +| CI (`ci.yml`) | `.github/workflows/` | Functional but no lint step | +| PR template | `.github/PULL_REQUEST_TEMPLATE.md` | Good | + +--- + +## What Is Good + +- **CONTRIBUTING.md** covers setup, code style, PR process, and community links clearly. +- **PR template** is well-structured with a type-of-change checklist and a 512 MB RAM reminder. +- **Issue templates** (bug + feature) are thorough and include environment fields. +- **docs/how-it-works.md** explains the ReAct loop with a clear ASCII diagram. +- **docs/tools.md** lists all 14 tools with required env vars — useful for agents adding integrations. +- **CI** runs install + test + build on every push/PR, and builds Docker on `main`. +- **`.env.example`** documents all env vars with comments. + +--- + +## What Is Missing + +### 1. `AGENTS.md` — created in this session +No file existed to orient AI coding agents. Created at `/AGENTS.md` covering: +- Repo structure map +- Tech stack table +- Dev commands +- Code conventions +- Architecture constraints (memory budget, step limit, sandbox) +- Step-by-step guide for adding a new tool +- PR checklist +- CI expectations +- File ownership table + +### 2. `.gitignore` is nearly empty +Only `.vercel` is listed. The repo uses Bun, Node, TypeScript, and Docker. Without proper ignore rules, `node_modules/`, `dist/`, `.env`, `*.db`, and build artifacts will be committed accidentally. + +**Required additions:** +``` +# Dependencies +node_modules/ + +# Build output +dist/ +.next/ +.svelte-kit/ + +# Environment +.env +.env.local +.env.*.local + +# Database files +*.db +*.db-shm +*.db-wal +data/ + +# Logs +*.log +npm-debug.log* +bun-debug.log* + +# OS +.DS_Store +Thumbs.db + +# Editor +.vscode/ +.idea/ +*.swp +*.swo +``` + +### 3. No lint step in CI +`ci.yml` runs `bun test` and `bun build` but not `bun lint`. Code style violations pass CI undetected. + +**Required change** — add to the `test` job in `.github/workflows/ci.yml`: +```yaml +- run: bun lint +- run: bun format --check +``` + +### 4. Dev container uses a 10 GB universal image with no Bun feature +`devcontainer.json` uses `mcr.microsoft.com/devcontainers/universal:4.0.1-noble`. This is slow to pull and does not pin Bun. A Bun-specific or Node image with the Bun devcontainer feature is faster and more reproducible. + +**Recommended change:** +```json +{ + "name": "AgentSwarp", + "image": "mcr.microsoft.com/devcontainers/javascript-node:22", + "features": { + "ghcr.io/devcontainers/features/bun:1": { "version": "1" } + }, + "postCreateCommand": "bun install", + "forwardPorts": [3000, 3001] +} +``` + +### 5. `swarm-core.md` is a raw code dump, not structured guidance +The file contains 26 source files pasted inline. It is not useful as documentation and will confuse agents that read it as authoritative. It should either be deleted or replaced with a structured Phase 2 migration guide. + +### 6. No `docs/` index +`docs/` has two good files but no index or README. Agents and contributors cannot discover what is documented. + +**Required:** add `docs/README.md` listing available docs and their purpose. + +### 7. `src/main.ts` diverges from the documented architecture +README and `AGENTS.md` describe `apps/server/` as the API layer, but `src/main.ts` contains live route handlers and direct SQLite calls. This creates two competing entry points. The minimal `src/main.ts` should either be removed or reduced to a thin bootstrap that delegates to `apps/server/`. + +--- + +## What Is Wrong + +### Critical: `.gitignore` is dangerously incomplete +`node_modules/`, `.env`, `*.db`, and `dist/` are not ignored. Any `git add .` will stage secrets and large dependency trees. This must be fixed before any new contributor runs `bun install`. + +### Inconsistency: `package.json` version vs roadmap +`package.json` declares `"version": "2.0.0"` but the README roadmap shows v0.2 as "In progress" and v1.0 as "Planned". The version field should match the public release state. + +### Inconsistency: `DATABASE_URL` vs hardcoded path +`.env.example` sets `DATABASE_URL=./data/agentswarm.db` but `src/main.ts` opens `new Database("agentswarp.db")` ignoring the env var. The env var is documented but not respected. + +--- + +## Improvement Priority + +| Priority | Item | Effort | +|----------|------|--------| +| P0 | Fix `.gitignore` | 5 min | +| P0 | Respect `DATABASE_URL` in `src/main.ts` | 10 min | +| P1 | Add lint + format-check to CI | 5 min | +| P1 | Replace `swarm-core.md` with a structured migration guide or delete it | 30 min | +| P2 | Switch devcontainer to a lighter image with pinned Bun | 15 min | +| P2 | Add `docs/README.md` index | 10 min | +| P3 | Reconcile `src/main.ts` with `apps/server/` architecture | 1–2 h | +| P3 | Align `package.json` version with public roadmap | 5 min | + +--- + +## Changes Made in This Session + +| File | Action | +|------|--------| +| `AGENTS.md` | Created | +| `AGENTS-IMPROVEMENT-SPEC.md` | Created (this file) | + +All remaining items above are **not yet implemented** and require explicit follow-up. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d2289d9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,140 @@ +# AGENTS.md — AgentSwarp + +Guidance for AI coding agents working in this repository. + +--- + +## Repository Overview + +AgentSwarp is a self-hosted, no-code AI agent platform built with Bun, Hono, SvelteKit, and SQLite. It is designed to run on low RAM (512 MB minimum). + +``` +agentswarp/ +├── src/main.ts Entry point (Hono server, SQLite setup) +├── apps/ +│ ├── web/ SvelteKit frontend (port 3000) +│ └── server/ Bun + Hono API server (port 3001) +├── packages/ +│ ├── core/ Agent runner, LLM abstraction, memory, scheduler +│ └── tools/ Built-in tool implementations (14 tools) +├── docs/ Architecture and tool reference docs +├── public/ Static assets served by Hono +├── .devcontainer/ Dev container config +└── .github/ CI, PR template, issue templates +``` + +--- + +## Tech Stack + +| Layer | Technology | +|------------|-------------------------------------| +| Runtime | Bun 1.0+ | +| Backend | Hono (HTTP + WebSocket) | +| Frontend | SvelteKit | +| Database | SQLite via `better-sqlite3` | +| Language | TypeScript (strict, no `any`) | +| Container | Docker / Docker Compose | + +--- + +## Development Commands + +```bash +bun install # Install dependencies +bun dev # Start dev server with file watching (port 3000) +bun start # Start production server +bun build # Build to ./dist +bun test # Run tests +bun lint # ESLint +bun format # Prettier +``` + +--- + +## Code Conventions + +- **TypeScript everywhere** — no `any` unless unavoidable; document why if used. +- **Prettier + ESLint** — run `bun format` and `bun lint` before committing. +- **Small, named functions** — prefer composition over inheritance. +- **Comments explain why**, not what. +- **No placeholders or TODOs** in committed code — implement fully or open an issue. +- SQLite access goes through `packages/core/` — do not add raw DB calls in route handlers. +- New tools belong in `packages/tools/src/` and must be registered in `packages/core/src/tool-registry.ts`. +- New API routes belong in `apps/server/` (not `src/main.ts`, which is the minimal entry point). + +--- + +## Architecture Constraints + +- **Memory budget**: keep the runtime footprint under 512 MB. Avoid loading large assets or models into process memory. +- **Step limit**: agent runs are capped at 15 steps to prevent infinite loops. Do not raise this without a corresponding safety review. +- **Code execution sandbox**: `execute_code` tool runs in a sandboxed context — do not widen its permissions. +- **WAL mode**: SQLite is opened with `journal_mode = WAL`. Maintain this for concurrent read performance. + +--- + +## Environment Variables + +Copy `.env.example` to `.env` before running locally. Key variables: + +| Variable | Purpose | +|-----------------------|----------------------------------------------| +| `PORT` | Server port (default `3000`) | +| `SECRET_KEY` | Session/auth secret — change before deploy | +| `DATABASE_URL` | SQLite file path | +| `DEFAULT_LLM_PROVIDER`| `ollama` (default), `openai`, `groq`, etc. | +| `DEFAULT_MODEL` | Model name for the default provider | +| `OLLAMA_BASE_URL` | Ollama endpoint (default `localhost:11434`) | + +Never commit `.env` or any file containing real secrets. + +--- + +## Adding a New Tool + +1. Create `packages/tools/src/.ts` implementing the `Tool` interface from `packages/core/src/types.ts`. +2. Export it from `packages/tools/src/index.ts`. +3. Register it in `packages/core/src/tool-registry.ts`. +4. Add the required env vars to `.env.example` with empty values. +5. Document the tool in `docs/tools.md`. +6. Add a test in `packages/tools/src/__tests__/`. + +--- + +## Pull Request Checklist + +Before opening a PR, verify: + +- [ ] `bun test` passes +- [ ] `bun lint` passes with no errors +- [ ] `bun format` applied +- [ ] No `any` types introduced without justification +- [ ] `.env.example` updated if new env vars were added +- [ ] `docs/` updated if behaviour or architecture changed +- [ ] Tested under constrained memory if the change touches the agent runner or tools + +--- + +## CI + +GitHub Actions runs on every push and PR: + +- `bun install && bun test && bun run build` — must pass on all PRs. +- Docker image build runs on `main` only. + +Do not merge if CI is red. + +--- + +## File Ownership + +| Area | Primary location | +|-----------------------------|-----------------------------------------| +| Agent execution logic | `packages/core/src/agent-runner.ts` | +| LLM provider abstraction | `packages/core/src/llm-client.ts` | +| Memory (KV + vector) | `packages/core/src/memory.ts`, `vector-store.ts` | +| Tool implementations | `packages/tools/src/` | +| Frontend UI | `apps/web/src/` | +| API routes | `apps/server/` | +| Static entry point | `src/main.ts` | diff --git a/docs/phase-2-migration.md b/docs/phase-2-migration.md new file mode 100644 index 0000000..c23a6ba --- /dev/null +++ b/docs/phase-2-migration.md @@ -0,0 +1,95 @@ +# Phase 2 Migration Guide + +This document describes the files introduced or replaced in Phase 2 (Batch B + C) and what each one does. Use it as a reference when integrating Phase 2 changes into a fresh clone or when reviewing what changed between v0.1 and v0.2. + +--- + +## What Changed + +Phase 2 adds the visual no-code canvas, command palette, memory dashboard, voice controls, personality system, and swarm visualizer. It also introduces a typed API client, a WebSocket store, and a Docker Compose configuration tuned to fit within 1 GB RAM. + +--- + +## File Inventory + +### Frontend — `apps/web/src/` + +| File | Action | Purpose | +|------|--------|---------| +| `app.css` | Replace | CSS design system: dark/light theme tokens, utility classes, animations | +| `lib/types.ts` | New | Shared TypeScript interfaces: `Agent`, `Run`, `RunStep`, `Memory`, `WorkspaceFile`, `Personality`, `AppSettings`, `Tool`, `ToastItem` | +| `lib/api.ts` | Replace | Typed API client with auth injection and namespaced methods (`agents`, `runs`, `memory`, `settings`, `auth`) | +| `lib/stores/ws.ts` | New | Svelte WebSocket store with auto-reconnect backoff and per-run step subscriptions | +| `lib/stores/toast.ts` | New | Toast notification queue with auto-dismiss | +| `lib/stores/theme.ts` | New | Dark/light/system theme store with `localStorage` persistence | +| `lib/components/ui/Button.svelte` | New | Button with `variant`, `size`, and `loading` props | +| `lib/components/ui/Badge.svelte` | New | Status badge with animated pulse dot for `running` state | +| `lib/components/ui/Modal.svelte` | New | Accessible modal: backdrop click, ESC close, Svelte 5 snippets | +| `lib/components/ui/Toast.svelte` | New | Auto-dismiss toast with progress bar and type-coloured icon | +| `lib/components/layout/CommandPalette.svelte` | New | Cmd+K palette: searches agents, actions, and runs with keyboard navigation | +| `lib/components/agents/PersonalityPicker.svelte` | New | Personality dropdown with preview pane and custom personality creation | +| `lib/components/agents/AgentCard.svelte` | New | Agent preview card with status dot, hover action bar, and run button | +| `lib/components/agents/AgentEditor.svelte` | New | Agent create/edit form: personality picker, tool checkboxes, trigger config | +| `lib/components/runs/SwarmVisualizer.svelte` | New | Vertical CSS timeline of run steps with type icons and live mode | +| `lib/components/memory/MemoryDashboard.svelte` | New | Tabbed memory browser: KV store, vector search, workspace files | +| `lib/components/voice/VoiceButton.svelte` | New | Browser STT/TTS voice controls with mic button and speaker mute toggle | +| `routes/+layout.svelte` | Replace | Root layout: fixed sidebar, nav links, theme toggle, WebSocket status dot | +| `routes/+page.svelte` | Replace | Dashboard: stats grid, recent runs table, quick actions, live updates | +| `routes/+error.svelte` | New | Terminal-style error page with status code, message, and optional stack trace | +| `routes/agents/[id]/+page.svelte` | Replace | Agent detail: Overview / Runs / Memory / Settings tabs, live run view | +| `routes/agents/new/+page.svelte` | New | New agent creation page wrapping `AgentEditor` | +| `routes/memory/+page.svelte` | New | Global memory view with agent selector and `MemoryDashboard` | +| `routes/settings/+page.svelte` | New | Settings: LLM provider, voice config, personalities, danger zone | + +### Backend / Tools — `packages/` + +| File | Action | Purpose | +|------|--------|---------| +| `packages/tools/src/searxng.ts` | New | SearXNG search tool with DuckDuckGo fallback, 5 s timeout, Zod input validation | + +### Infrastructure + +| File | Action | Purpose | +|------|--------|---------| +| `docker-compose.yml` | Update | Three-service compose: `app` (200 MB), `web` (64 MB), `searxng` (256 MB) — fits within 1 GB | + +--- + +## Memory Budget (Docker Compose) + +| Service | Limit | +|---------|-------| +| `app` (API server) | 200 MB | +| `web` (SvelteKit) | 64 MB | +| `searxng` | 256 MB | +| **Total** | ~520 MB | + +Leaves headroom for the OS and SQLite on a 1 GB host. + +--- + +## New Environment Variables (Phase 2) + +Add these to `.env` / `.env.example` if not already present: + +| Variable | Purpose | +|----------|---------| +| `PUBLIC_API_URL` | Frontend → API base URL (default `http://localhost:3001`) | +| `PUBLIC_WS_URL` | Frontend → WebSocket URL (default `ws://localhost:3001/ws`) | +| `SEARXNG_BASE_URL` | SearXNG instance URL (optional; falls back to DuckDuckGo) | +| `ELEVENLABS_API_KEY` | ElevenLabs TTS (optional) | +| `ELEVENLABS_VOICE_ID` | ElevenLabs voice ID (optional) | + +--- + +## Integration Checklist + +When applying Phase 2 files to a fresh clone: + +- [ ] Replace files marked **Replace** — do not merge with v0.1 versions. +- [ ] Add files marked **New** at the paths listed above. +- [ ] Update `docker-compose.yml` with the new memory-limited service definitions. +- [ ] Add Phase 2 env vars to `.env.example`. +- [ ] Register `searxng` in `packages/core/src/tool-registry.ts`. +- [ ] Run `bun install` — no new top-level dependencies; SvelteKit deps are in `apps/web/`. +- [ ] Run `bun test` and `bun lint` before committing. diff --git a/src/main.ts b/src/main.ts index d60e492..df9d14d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,8 +3,9 @@ import { serveStatic } from "hono/bun"; import Database from "better-sqlite3"; import { randomUUID } from "crypto"; -// SQLite setup -const db = new Database("agentswarp.db"); +// SQLite setup — path from DATABASE_URL env var, fallback for local dev +const dbPath = process.env.DATABASE_URL ?? "agentswarp.db"; +const db = new Database(dbPath); db.pragma("journal_mode = WAL"); db.exec(`CREATE TABLE IF NOT EXISTS tasks (id TEXT PRIMARY KEY, goal TEXT, status TEXT, result TEXT, created_at TEXT, updated_at TEXT); CREATE TABLE IF NOT EXISTS memory (id TEXT PRIMARY KEY, key TEXT, value TEXT, created_at TEXT); diff --git a/swarm-core.md b/swarm-core.md deleted file mode 100644 index 7709ab2..0000000 --- a/swarm-core.md +++ /dev/null @@ -1,7599 +0,0 @@ -# AgentSwarp -- Phase 2 Complete Code Reference - -> AgentSwarp Phase 2 - Complete Code Reference -> -> GitHub: https://github.com/raynr7/agentswarm - -This document contains all 26 production-ready source files for AgentSwarp Phase 2 (BATCH B + C). -Each file is fully working -- no placeholders, no TODOs. Copy-paste directly into your project. - ---- - -## Quick Summary Table - -| # | File Path | Purpose | Status | -|---|-----------|---------|--------| -| 1 | `apps/web/src/app.css` | Complete CSS design system with dark/light theme, utility classes, and | **REPLACE** | -| 2 | `apps/web/src/lib/types.ts` | TypeScript interfaces for Agent, Run, RunStep, Memory, WorkspaceFile, | **NEW** | -| 3 | `apps/web/src/lib/api.ts` | Typed API client with auth injection, agents/runs/memory/settings/auth | **REPLACE** | -| 4 | `apps/web/src/lib/stores/ws.ts` | Svelte WebSocket store with auto-reconnect backoff and per-run step su | **NEW** | -| 5 | `apps/web/src/lib/stores/toast.ts` | Toast notification queue store with auto-dismiss | **NEW** | -| 6 | `apps/web/src/lib/stores/theme.ts` | Dark/light/system theme store with localStorage persistence | **NEW** | -| 7 | `apps/web/src/lib/components/ui/Button.svelte` | Reusable button with variant/size/loading props and SVG spinner | **NEW** | -| 8 | `apps/web/src/lib/components/ui/Badge.svelte` | Status badges with animated pulse dot for running state | **NEW** | -| 9 | `apps/web/src/lib/components/ui/Modal.svelte` | Accessible modal with Svelte 5 snippets, backdrop/ESC close, and fade | **NEW** | -| 10 | `apps/web/src/lib/components/ui/Toast.svelte` | Auto-dismiss toast notifications with progress bar and type-colored ic | **NEW** | -| 11 | `apps/web/src/lib/components/layout/CommandPalette.svelte` | Cmd+K command palette with agent/action/run search and keyboard naviga | **NEW** | -| 12 | `apps/web/src/lib/components/agents/PersonalityPicker.svelte` | Personality dropdown with preview pane and custom personality creation | **NEW** | -| 13 | `apps/web/src/routes/+layout.svelte` | Root layout with fixed sidebar, nav links, theme toggle, WS status dot | **REPLACE** | -| 14 | `apps/web/src/routes/+page.svelte` | Dashboard with stats grid, recent runs table, quick actions, and live | **REPLACE** | -| 15 | `apps/web/src/lib/components/agents/AgentCard.svelte` | Agent preview card with status dot, hover action bar, and run button | **NEW** | -| 16 | `apps/web/src/lib/components/agents/AgentEditor.svelte` | Agent create/edit form with personality picker, tool checkboxes, trigg | **NEW** | -| 17 | `apps/web/src/lib/components/runs/SwarmVisualizer.svelte` | Vertical CSS timeline showing run steps with type icons, live mode sup | **NEW** | -| 18 | `apps/web/src/lib/components/memory/MemoryDashboard.svelte` | Tabbed memory browser: KV store, vector search, and workspace files | **NEW** | -| 19 | `apps/web/src/lib/components/voice/VoiceButton.svelte` | Browser STT/TTS voice controls with mic button and speaker mute toggle | **NEW** | -| 20 | `apps/web/src/routes/agents/[id]/+page.svelte` | Agent detail page with Overview/Runs/Memory/Settings tabs and live run | **REPLACE** | -| 21 | `apps/web/src/routes/agents/new/+page.svelte` | New agent creation page wrapping AgentEditor with navigation on save | **NEW** | -| 22 | `apps/web/src/routes/memory/+page.svelte` | Global memory view with agent selector and MemoryDashboard component | **NEW** | -| 23 | `apps/web/src/routes/settings/+page.svelte` | Settings page with LLM provider, voice config, personalities, and dang | **NEW** | -| 24 | `apps/web/src/routes/+error.svelte` | Terminal-style error page with status, message, optional stack trace, | **NEW** | -| 25 | `packages/tools/src/searxng.ts` | SearXNG search tool with DuckDuckGo fallback, 5s timeout, zod input va | **NEW** | -| 26 | `docker-compose.yml` | Docker Compose with app(200m), web(64m), searxng(256m) -- fits 1GB OCI | **UPDATE** | - ---- - -## Table of Contents - -- [File 1: apps/web/src/app.css](#file-1) -- [File 2: apps/web/src/lib/types.ts](#file-2) -- [File 3: apps/web/src/lib/api.ts](#file-3) -- [File 4: apps/web/src/lib/stores/ws.ts](#file-4) -- [File 5: apps/web/src/lib/stores/toast.ts](#file-5) -- [File 6: apps/web/src/lib/stores/theme.ts](#file-6) -- [File 7: apps/web/src/lib/components/ui/Button.svelte](#file-7) -- [File 8: apps/web/src/lib/components/ui/Badge.svelte](#file-8) -- [File 9: apps/web/src/lib/components/ui/Modal.svelte](#file-9) -- [File 10: apps/web/src/lib/components/ui/Toast.svelte](#file-10) -- [File 11: apps/web/src/lib/components/layout/CommandPalette.svelte](#file-11) -- [File 12: apps/web/src/lib/components/agents/PersonalityPicker.svelte](#file-12) -- [File 13: apps/web/src/routes/+layout.svelte](#file-13) -- [File 14: apps/web/src/routes/+page.svelte](#file-14) -- [File 15: apps/web/src/lib/components/agents/AgentCard.svelte](#file-15) -- [File 16: apps/web/src/lib/components/agents/AgentEditor.svelte](#file-16) -- [File 17: apps/web/src/lib/components/runs/SwarmVisualizer.svelte](#file-17) -- [File 18: apps/web/src/lib/components/memory/MemoryDashboard.svelte](#file-18) -- [File 19: apps/web/src/lib/components/voice/VoiceButton.svelte](#file-19) -- [File 20: apps/web/src/routes/agents/[id]/+page.svelte](#file-20) -- [File 21: apps/web/src/routes/agents/new/+page.svelte](#file-21) -- [File 22: apps/web/src/routes/memory/+page.svelte](#file-22) -- [File 23: apps/web/src/routes/settings/+page.svelte](#file-23) -- [File 24: apps/web/src/routes/+error.svelte](#file-24) -- [File 25: packages/tools/src/searxng.ts](#file-25) -- [File 26: docker-compose.yml](#file-26) - ---- - -## File 1: `apps/web/src/app.css` - -**Status:** `REPLACE` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/app.css)** - -Complete CSS design system with dark/light theme, utility classes, and animations - -```css -@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap'); - -:root { - --bg: #0a0a0a; - --bg-surface: #111111; - --bg-elevated: #1a1a1a; - --border: #222222; - --border-subtle: #1a1a1a; - --text: #e4e4e4; - --text-muted: #666666; - --accent: #00ff88; - --accent-dim: #00ff8833; - --accent-hover: #00cc6e; - --danger: #ff4444; - --danger-dim: #ff444422; - --warning: #ffaa00; - --warning-dim: #ffaa0022; - --success: #00ff88; - --radius: 6px; -} - -[data-theme="light"] { - --bg: #f4f4f5; - --bg-surface: #ffffff; - --bg-elevated: #f0f0f0; - --border: #e0e0e0; - --border-subtle: #ebebeb; - --text: #111111; - --text-muted: #888888; - --accent: #00aa55; - --accent-dim: #00aa5522; - --accent-hover: #008844; - --danger: #cc2222; - --danger-dim: #cc222222; - --warning: #cc8800; - --warning-dim: #cc880022; - --success: #00aa55; - --radius: 6px; -} - -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html { - font-size: 14px; - -webkit-text-size-adjust: 100%; -} - -body { - background: var(--bg); - color: var(--text); - font-family: 'Inter', system-ui, -apple-system, sans-serif; - font-size: 14px; - line-height: 1.5; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - min-height: 100vh; -} - -a { - color: var(--accent); - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -img, -video { - max-width: 100%; - height: auto; - display: block; -} - -p, -h1, h2, h3, h4, h5, h6 { - overflow-wrap: break-word; -} - -/* --- Scrollbar -------------------------------------------- */ - -::-webkit-scrollbar { - width: 6px; - height: 6px; -} - -::-webkit-scrollbar-track { - background: var(--bg); -} - -::-webkit-scrollbar-thumb { - background: #333333; - border-radius: 3px; -} - -::-webkit-scrollbar-thumb:hover { - background: #444444; -} - -/* --- Form Elements ---------------------------------------- */ - -input, -textarea, -select { - background: var(--bg-elevated); - border: 1px solid var(--border); - color: var(--text); - border-radius: var(--radius); - padding: 8px 12px; - font-size: 13px; - font-family: 'Inter', system-ui, sans-serif; - outline: none; - width: 100%; - transition: border-color 150ms ease, box-shadow 150ms ease; -} - -input:focus, -textarea:focus, -select:focus { - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-dim); -} - -input::placeholder, -textarea::placeholder { - color: var(--text-muted); -} - -textarea { - resize: vertical; - min-height: 80px; -} - -select { - cursor: pointer; - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 10px center; - padding-right: 28px; -} - -/* --- Keyframes -------------------------------------------- */ - -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - -@keyframes slideUp { - from { - transform: translateY(8px); - opacity: 0; - } - to { - transform: translateY(0); - opacity: 1; - } -} - -@keyframes slideInRight { - from { - transform: translateX(20px); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -@keyframes pulseDot { - from { - transform: scale(1); - opacity: 1; - } - to { - transform: scale(1.8); - opacity: 0; - } -} - -@keyframes shimmer { - from { - background-position: -200% center; - } - to { - background-position: 200% center; - } -} - -/* --- Card ------------------------------------------------- */ - -.card { - background: var(--bg-surface); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 16px; -} - -.card-hover { - transition: transform 150ms ease, border-color 150ms ease; - cursor: pointer; -} - -.card-hover:hover { - transform: translateY(-1px); - border-color: var(--accent); -} - -/* --- Buttons ---------------------------------------------- */ - -.btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 7px 14px; - border-radius: var(--radius); - font-size: 13px; - font-weight: 500; - font-family: 'Inter', system-ui, sans-serif; - cursor: pointer; - transition: all 150ms ease; - border: none; - outline: none; - text-decoration: none; - white-space: nowrap; - user-select: none; - line-height: 1; -} - -.btn:disabled { - opacity: 0.4; - cursor: not-allowed; - pointer-events: none; -} - -.btn-default { - background: var(--accent); - color: #000000; -} - -.btn-default:hover { - background: var(--accent-hover); -} - -.btn-ghost { - background: transparent; - color: var(--text-muted); - border: 1px solid var(--border); -} - -.btn-ghost:hover { - background: var(--bg-elevated); - color: var(--text); -} - -.btn-danger { - background: var(--danger); - color: #ffffff; -} - -.btn-danger:hover { - background: #cc2222; -} - -.btn-success { - background: var(--accent); - color: #000000; -} - -.btn-success:hover { - background: var(--accent-hover); -} - -.btn-sm { - padding: 4px 10px; - font-size: 12px; -} - -.btn-lg { - padding: 10px 20px; - font-size: 15px; -} - -/* --- Badges ----------------------------------------------- */ - -.badge { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 8px; - border-radius: 999px; - font-size: 11px; - font-family: 'JetBrains Mono', monospace; - font-weight: 500; - line-height: 1.6; - white-space: nowrap; -} - -.badge-running { - background: #00ff8822; - color: #00ff88; -} - -.badge-success { - background: #00ff8822; - color: #00ff88; -} - -.badge-fail { - background: #ff444422; - color: #ff4444; -} - -.badge-partial { - background: #ffaa0022; - color: #ffaa00; -} - -.badge-pending { - background: #33333388; - color: #888888; -} - -/* --- Glass ------------------------------------------------ */ - -.glass { - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.08); -} - -/* --- Divider ---------------------------------------------- */ - -.divider { - height: 1px; - background: var(--border); - margin: 16px 0; - border: none; -} - -/* --- Typography Utilities --------------------------------- */ - -.mono { - font-family: 'JetBrains Mono', monospace; -} - -.truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -/* --- Skeleton --------------------------------------------- */ - -.skeleton { - background: linear-gradient( - 90deg, - var(--bg-elevated) 25%, - #2a2a2a 50%, - var(--bg-elevated) 75% - ); - background-size: 200% 100%; - animation: shimmer 1.5s infinite linear; - border-radius: var(--radius); - height: 16px; -} - -/* --- Table ------------------------------------------------ */ - -.table { - width: 100%; - border-collapse: collapse; -} - -.table th { - text-align: left; - padding: 8px 12px; - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-muted); - border-bottom: 1px solid var(--border); - font-weight: 500; -} - -.table td { - padding: 10px 12px; - font-size: 13px; - border-bottom: 1px solid var(--border-subtle); - vertical-align: middle; -} - -.table tr:hover td { - background: var(--bg-elevated); -} - -/* --- Grid / Layout ---------------------------------------- */ - -.grid-bento { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 16px; -} - -.flex-center { - display: flex; - align-items: center; - justify-content: center; -} - -/* --- Form Group ------------------------------------------- */ - -.input-group { - display: flex; - flex-direction: column; - gap: 6px; -} - -.label { - font-size: 12px; - font-weight: 500; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -/* --- Sidebar Navigation ----------------------------------- */ - -.sidebar-nav { - display: flex; - flex-direction: column; - gap: 2px; -} - -.sidebar-link { - display: flex; - align-items: center; - gap: 8px; - padding: 6px 16px; - color: var(--text-muted); - text-decoration: none; - border-radius: 4px; - font-size: 13px; - transition: background 150ms ease, color 150ms ease; - border-left: 3px solid transparent; -} - -.sidebar-link:hover { - background: var(--bg-elevated); - color: var(--text); - text-decoration: none; -} - -.sidebar-link.active { - color: var(--accent); - background: var(--accent-dim); - border-left: 3px solid var(--accent); - padding-left: 13px; -} - -.nav-section-label { - font-size: 10px; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--text-muted); - padding: 16px 16px 4px; - opacity: 0.5; -} - -/* --- Stat Card -------------------------------------------- */ - -.stat-card { - background: var(--bg-surface); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 16px; - text-align: center; -} - -.stat-card .stat-value { - font-size: 32px; - font-weight: 700; - font-family: 'JetBrains Mono', monospace; - color: var(--text); - line-height: 1.1; -} - -.stat-card .stat-label { - font-size: 12px; - color: var(--text-muted); - margin-top: 4px; -} - -/* --- Timeline --------------------------------------------- */ - -.timeline-item { - display: flex; - gap: 12px; - position: relative; -} - -.timeline-line { - width: 2px; - background: var(--border); - flex-shrink: 0; - margin-top: 20px; -} - -/* --- Tab Bar ---------------------------------------------- */ - -.tab-bar { - display: flex; - gap: 0; - border-bottom: 1px solid var(--border); - margin-bottom: 16px; -} - -.tab { - padding: 8px 16px; - font-size: 13px; - color: var(--text-muted); - cursor: pointer; - border-bottom: 2px solid transparent; - transition: color 150ms ease, border-color 150ms ease; - user-select: none; - background: none; - border-top: none; - border-left: none; - border-right: none; - font-family: 'Inter', system-ui, sans-serif; - outline: none; -} - -.tab:hover { - color: var(--text); -} - -.tab.active { - color: var(--accent); - border-bottom-color: var(--accent); -} - -/* --- Animation Helpers ------------------------------------ */ - -.fade-in { - animation: fadeIn 200ms ease forwards; -} - -.slide-up { - animation: slideUp 200ms ease forwards; -} - -.slide-in-right { - animation: slideInRight 200ms ease forwards; -} - -/* --- Responsive ------------------------------------------- */ - -@media (max-width: 768px) { - .grid-bento { - grid-template-columns: 1fr; - } -} - -``` - ---- - -## File 2: `apps/web/src/lib/types.ts` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/lib/types.ts)** - -TypeScript interfaces for Agent, Run, RunStep, Memory, WorkspaceFile, Personality, AppSettings, Tool, ToastItem - -```typescript -// apps/web/src/lib/types.ts -// AgentSwarp - Shared TypeScript Interfaces - -export interface Agent { - id: string; - name: string; - goal: string; - personality: string; - status: 'idle' | 'running' | 'error'; - model_override?: string; - max_sub_agents: number; - created_at: string; - updated_at: string; -} - -export interface Run { - id: string; - agent_id: string; - agent_name?: string; - status: 'running' | 'success' | 'fail' | 'partial'; - started_at: string; - finished_at?: string; - summary?: string; - step_count: number; - duration_ms?: number; -} - -export interface RunStep { - id: string; - run_id: string; - type: 'thought' | 'tool_call' | 'tool_result' | 'sub_agent' | 'message'; - content: string; - tool_name?: string; - input?: string; - output?: string; - status?: 'running' | 'success' | 'fail'; - created_at: string; -} - -export interface Memory { - id: string; - agent_id: string; - key?: string; - value?: string; - content?: string; - type: 'kv' | 'vector'; - metadata?: Record; - created_at: string; -} - -export interface WorkspaceFile { - id: string; - agent_id: string; - filename: string; - path: string; - size: number; - mime_type: string; - created_at: string; -} - -export interface Personality { - key: string; - name: string; - description: string; - systemPrompt: string; -} - -export interface AppSettings { - llm_provider: string; - llm_model: string; - llm_api_key?: string; - ollama_base_url?: string; - voice_enabled: boolean; - stt_mode: 'browser' | 'whisper'; - tts_mode: 'browser' | 'elevenlabs'; - elevenlabs_voice_id?: string; - elevenlabs_api_key?: string; -} - -export interface Tool { - id: string; - name: string; - description: string; - enabled: boolean; -} - -export interface ToastItem { - id: string; - message: string; - type: 'success' | 'error' | 'warning' | 'info'; - duration: number; -} - -export interface ApiError { - message: string; - status: number; - code?: string; -} - -``` - ---- - -## File 3: `apps/web/src/lib/api.ts` - -**Status:** `REPLACE` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/lib/api.ts)** - -Typed API client with auth injection, agents/runs/memory/settings/auth namespaces - -```typescript -import type { - Agent, - Run, - RunStep, - Memory, - WorkspaceFile, - AppSettings, - Personality, -} from './types'; - -const BASE_URL = - (import.meta.env.PUBLIC_API_URL as string) ?? 'http://localhost:3001'; - -const TOKEN_KEY = 'agentswarp-token'; - -function getToken(): string | null { - if (typeof localStorage === 'undefined') return null; - return localStorage.getItem(TOKEN_KEY); -} - -async function request( - path: string, - options: RequestInit = {} -): Promise { - const headers: Record = { - ...(options.headers as Record), - }; - - const token = getToken(); - if (token) { - headers['Authorization'] = `Bearer ${token}`; - } - - const method = (options.method ?? 'GET').toUpperCase(); - if (method !== 'GET') { - headers['Content-Type'] = 'application/json'; - } - - const response = await fetch(`${BASE_URL}${path}`, { - ...options, - headers, - }); - - if (response.status === 401) { - if (typeof localStorage !== 'undefined') { - localStorage.removeItem(TOKEN_KEY); - } - if (typeof window !== 'undefined') { - window.location.href = '/login'; - } - throw new Error('Unauthorized'); - } - - let body: unknown; - const contentType = response.headers.get('content-type') ?? ''; - if (contentType.includes('application/json')) { - body = await response.json(); - } else { - body = await response.text(); - } - - if (response.ok) { - return body as T; - } - - const errorBody = body as { message?: string; error?: string }; - const message = - errorBody?.message ?? - errorBody?.error ?? - `Request failed with status ${response.status}`; - throw new Error(message); -} - -export const agents = { - list(): Promise { - return request('/api/agents'); - }, - - get(id: string): Promise { - return request(`/api/agents/${id}`); - }, - - create(data: Partial): Promise { - return request('/api/agents', { - method: 'POST', - body: JSON.stringify(data), - }); - }, - - update(id: string, data: Partial): Promise { - return request(`/api/agents/${id}`, { - method: 'PUT', - body: JSON.stringify(data), - }); - }, - - delete(id: string): Promise { - return request(`/api/agents/${id}`, { - method: 'DELETE', - }); - }, - - run(id: string, input?: string): Promise { - return request(`/api/agents/${id}/run`, { - method: 'POST', - body: JSON.stringify({ input }), - }); - }, -}; - -export const runs = { - list(agentId?: string, limit?: number): Promise { - const params = new URLSearchParams(); - if (agentId !== undefined) params.set('agent_id', agentId); - if (limit !== undefined) params.set('limit', String(limit)); - const query = params.toString(); - return request(`/api/runs${query ? `?${query}` : ''}`); - }, - - get(id: string): Promise { - return request(`/api/runs/${id}`); - }, - - getSteps(id: string): Promise { - return request(`/api/runs/${id}/steps`); - }, -}; - -export const memory = { - list(agentId: string, type?: 'kv' | 'vector'): Promise { - const params = new URLSearchParams({ agent_id: agentId }); - if (type !== undefined) params.set('type', type); - return request(`/api/memory?${params.toString()}`); - }, - - search(agentId: string, query: string): Promise { - const params = new URLSearchParams({ agent_id: agentId, q: query }); - return request(`/api/memory/search?${params.toString()}`); - }, - - delete(agentId: string, id: string): Promise { - const params = new URLSearchParams({ agent_id: agentId }); - return request(`/api/memory/${id}?${params.toString()}`, { - method: 'DELETE', - }); - }, - - listFiles(agentId: string): Promise { - const params = new URLSearchParams({ agent_id: agentId }); - return request(`/api/memory/files?${params.toString()}`); - }, -}; - -export const settings = { - get(): Promise { - return request('/api/settings'); - }, - - update(data: Partial): Promise { - return request('/api/settings', { - method: 'PUT', - body: JSON.stringify(data), - }); - }, - - getPersonalities(): Promise { - return request('/api/settings/personalities'); - }, - - addPersonality(data: Personality): Promise { - return request('/api/settings/personalities', { - method: 'POST', - body: JSON.stringify(data), - }); - }, - - testLLM(): Promise<{ ok: boolean; message: string }> { - return request<{ ok: boolean; message: string }>('/api/settings/test-llm', { - method: 'POST', - }); - }, -}; - -export const auth = { - async login( - email: string, - password: string - ): Promise<{ token: string }> { - const result = await request<{ token: string }>('/api/auth/login', { - method: 'POST', - body: JSON.stringify({ email, password }), - }); - if (typeof localStorage !== 'undefined') { - localStorage.setItem(TOKEN_KEY, result.token); - } - return result; - }, - - magicRequest(email: string): Promise { - return request('/api/auth/magic/request', { - method: 'POST', - body: JSON.stringify({ email }), - }); - }, - - async magicVerify( - email: string, - otp: string - ): Promise<{ token: string }> { - const result = await request<{ token: string }>('/api/auth/magic/verify', { - method: 'POST', - body: JSON.stringify({ email, otp }), - }); - if (typeof localStorage !== 'undefined') { - localStorage.setItem(TOKEN_KEY, result.token); - } - return result; - }, - - logout(): void { - if (typeof localStorage !== 'undefined') { - localStorage.removeItem(TOKEN_KEY); - } - if (typeof window !== 'undefined') { - window.location.href = '/login'; - } - }, -}; - -``` - ---- - -## File 4: `apps/web/src/lib/stores/ws.ts` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/lib/stores/ws.ts)** - -Svelte WebSocket store with auto-reconnect backoff and per-run step subscriptions - -```typescript -import { writable, derived } from 'svelte/store'; -import type RunStep from '../types'; - -const WS_URL = (import.meta.env.PUBLIC_WS_URL as string) ?? 'ws://localhost:3001/ws'; - -let socket: WebSocket | null = null; -let reconnectDelay = 1000; -const maxDelay = 30000; -let heartbeatTimer: ReturnType | null = null; -const subscribers = new Map void>>(); - -export const wsStatus = writable<'connecting' | 'connected' | 'disconnected'>('disconnected'); - -export const connected = derived(wsStatus, ($s) => $s === 'connected'); - -function dispatch(runId: string, step: RunStep): void { - const specific = subscribers.get(runId); - if (specific) { - specific.forEach((cb) => cb(step)); - } - const wildcard = subscribers.get('*'); - if (wildcard) { - wildcard.forEach((cb) => cb(step)); - } -} - -export function connect(): void { - if (typeof window === 'undefined') return; - - if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) { - return; - } - - wsStatus.set('connecting'); - socket = new WebSocket(WS_URL); - - socket.onopen = () => { - wsStatus.set('connected'); - reconnectDelay = 1000; - - if (heartbeatTimer !== null) { - clearInterval(heartbeatTimer); - } - - heartbeatTimer = setInterval(() => { - if (socket && socket.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ type: 'ping' })); - } - }, 30000); - }; - - socket.onmessage = (event: MessageEvent) => { - let msg: Record; - try { - msg = JSON.parse(event.data as string); - } catch { - console.error('[ws] Failed to parse message:', event.data); - return; - } - - if (msg.type === 'pong') return; - - if (typeof msg.runId === 'string') { - dispatch(msg.runId, msg as unknown as RunStep); - } - }; - - socket.onclose = () => { - wsStatus.set('disconnected'); - - if (heartbeatTimer !== null) { - clearInterval(heartbeatTimer); - heartbeatTimer = null; - } - - setTimeout(() => { - reconnect(); - }, reconnectDelay); - - reconnectDelay = Math.min(reconnectDelay * 2, maxDelay); - }; - - socket.onerror = (error: Event) => { - console.error('[ws] WebSocket error:', error); - }; -} - -function reconnect(): void { - connect(); -} - -export function disconnect(): void { - if (heartbeatTimer !== null) { - clearInterval(heartbeatTimer); - heartbeatTimer = null; - } - - if (socket) { - socket.close(); - socket = null; - } -} - -export function sendMessage(data: object): void { - if (socket && socket.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify(data)); - } -} - -export function onRunStep(runId: string, cb: (step: RunStep) => void): () => void { - if (!subscribers.has(runId)) { - subscribers.set(runId, new Set()); - } - - const set = subscribers.get(runId)!; - set.add(cb); - - return () => { - set.delete(cb); - if (set.size === 0) { - subscribers.delete(runId); - } - }; -} - -if (typeof window !== 'undefined') { - connect(); -} - -export default { - wsStatus, - connected, - sendMessage, - onRunStep, - connect, - disconnect, -}; - -``` - ---- - -## File 5: `apps/web/src/lib/stores/toast.ts` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/lib/stores/toast.ts)** - -Toast notification queue store with auto-dismiss - -```typescript -// === FILE: apps/web/src/lib/stores/toast.ts === -import { writable, derived } from 'svelte/store' -import type { ToastItem } from '../types' - -function nanoid(): string { - return Math.random().toString(36).slice(2, 10) -} - -const _toasts = writable([]) - -export const toasts = derived(_toasts, ($t) => $t.slice(0, 5)) - -export function addToast( - message: string, - type: ToastItem['type'] = 'info', - duration = 4000 -): void { - const id = nanoid() - const item: ToastItem = { id, message, type } - _toasts.update((current) => [...current, item]) - setTimeout(() => removeToast(id), duration) -} - -export function removeToast(id: string): void { - _toasts.update((current) => current.filter((t) => t.id !== id)) -} -``` - ---- - -## File 6: `apps/web/src/lib/stores/theme.ts` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/lib/stores/theme.ts)** - -Dark/light/system theme store with localStorage persistence - -```typescript -import { writable, get } from 'svelte/store' - -type Theme = 'dark' | 'light' | 'system' - -const STORAGE_KEY = 'agentswarp-theme' - -const _theme = writable('dark') - -export const theme = { - subscribe: _theme.subscribe, -} - -function applyTheme(t: Theme): void { - if (typeof window === 'undefined') return - - if (t === 'system') { - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches - document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light') - } else { - document.documentElement.setAttribute('data-theme', t) - } -} - -export function setTheme(t: Theme): void { - _theme.set(t) - if (typeof window !== 'undefined') { - localStorage.setItem(STORAGE_KEY, t) - } - applyTheme(t) -} - -export function initTheme(): void { - if (typeof window === 'undefined') return - - const stored = localStorage.getItem(STORAGE_KEY) as Theme | null - const resolved: Theme = stored ?? 'dark' - _theme.set(resolved) - applyTheme(resolved) -} - -export function toggleTheme(): void { - const current = get(_theme) - const next: Theme = current === 'dark' ? 'light' : 'dark' - setTheme(next) -} -``` - ---- - -## File 7: `apps/web/src/lib/components/ui/Button.svelte` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/lib/components/ui/Button.svelte)** - -Reusable button with variant/size/loading props and SVG spinner - -```svelte - - -{#if href} - e.preventDefault() : onclick} - > - {#if loading} - - {/if} - - -{:else} - -{/if} - - - -``` - ---- - -## File 8: `apps/web/src/lib/components/ui/Badge.svelte` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/lib/components/ui/Badge.svelte)** - -Status badges with animated pulse dot for running state - -```svelte - - -{#if status === 'running'} - - - {displayText} - -{:else} - {displayText} -{/if} - - - -``` - ---- - -## File 9: `apps/web/src/lib/components/ui/Modal.svelte` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/lib/components/ui/Modal.svelte)** - -Accessible modal with Svelte 5 snippets, backdrop/ESC close, and fade transition - -```svelte - - -{#if open} - - -{/if} - - - -``` - ---- - -## File 10: `apps/web/src/lib/components/ui/Toast.svelte` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/lib/components/ui/Toast.svelte)** - -Auto-dismiss toast notifications with progress bar and type-colored icons - -```svelte -// Code generation failed for this file -``` - ---- - -## File 11: `apps/web/src/lib/components/layout/CommandPalette.svelte` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/lib/components/layout/CommandPalette.svelte)** - -Cmd+K command palette with agent/action/run search and keyboard navigation - -```svelte - - -{#if open} -
(open = false)} - onkeydown={(e) => e.key === 'Escape' && (open = false)} - >
- - -{/if} - - - -``` - ---- - -## File 12: `apps/web/src/lib/components/agents/PersonalityPicker.svelte` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/lib/components/agents/PersonalityPicker.svelte)** - -Personality dropdown with preview pane and custom personality creation form - -```svelte - - -
- - - - - {#if selected} -
-
{selected.systemPrompt}
-
- {/if} - - - - {#if showAddForm} -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
- {/if} -
- - - -``` - ---- - -## File 13: `apps/web/src/routes/+layout.svelte` - -**Status:** `REPLACE` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/routes/+layout.svelte)** - -Root layout with fixed sidebar, nav links, theme toggle, WS status dot, and Cmd+K - -```svelte - - -
- - -
- -
-
- - - - - - -``` - ---- - -## File 14: `apps/web/src/routes/+page.svelte` - -**Status:** `REPLACE` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/routes/+page.svelte)** - -Dashboard with stats grid, recent runs table, quick actions, and live run ticker - -```svelte - - -{#if loading} -
-
-
-
-
-
-
-
-
-
-
-
-
-{:else} -
- - - -
-
-
Active Now
-
- {#if activeNow > 0} - - {/if} - {activeNow} -
-
agents running
-
- -
-
Total Agents
-
{totalAgents}
-
configured
-
- -
-
Runs Today
-
{runsToday}
-
executions
-
- -
-
Success Rate
-
{successRate}%
-
last {finished.length} runs
-
-
- - -
-
-

Recent Runs

-
- - - - - - - - - - - {#if runList.length === 0} - - - - {:else} - {#each runList.slice(0, 10) as run} - - - - - - - {/each} - {/if} - -
AgentStatusStartedDuration
No runs yet
{run.agent_name ?? run.agent_id.slice(0, 8)}{formatRelative(run.started_at)}{run.duration_ms ? formatDuration(run.duration_ms) : '--'}
-
-
- - -
-
- - {#if activeRun} -
- - - Live: {activeRun.agent_name ?? 'Agent'} -- {liveStep || 'Running...'} - -
- {/if} -{/if} - - - -``` - ---- - -## File 15: `apps/web/src/lib/components/agents/AgentCard.svelte` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/lib/components/agents/AgentCard.svelte)** - -Agent preview card with status dot, hover action bar, and run button - -```svelte - - -
-
- -
-
-
{agent.name}
-
{agent.goal}
-
- {#if agent.personality} - {agent.personality} - {/if} - {#if agent.modelOverride} - {agent.modelOverride} - {/if} -
-
-
- -
- - ✎ Edit -
-
- - - -``` - ---- - -## File 16: `apps/web/src/lib/components/agents/AgentEditor.svelte` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/lib/components/agents/AgentEditor.svelte)** - -Agent create/edit form with personality picker, tool checkboxes, trigger config - -```svelte - - -
- -
- - -
- - -
- - -
- - - - - -
- - -
- - -
- - -
- - -
- Tools -
- {#each availableTools as tool (tool.id)} - - {/each} - {#if availableTools.length === 0} - No tools available - {/if} -
-
- - -
- Trigger -
- - - -
-
- - - {#if form.trigger_type === 'scheduled'} -
- - -

{cronHumanReadable(form.cron_expression)}

- crontab.guru -> -
- {/if} - - - {#if form.trigger_type === 'webhook' && agent} -
- - (e.currentTarget as HTMLInputElement).select()} - /> -
- {/if} - -
- - -
- - {#if onCancel} - - {/if} -
- - - - -``` - ---- - -## File 17: `apps/web/src/lib/components/runs/SwarmVisualizer.svelte` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/lib/components/runs/SwarmVisualizer.svelte)** - -Vertical CSS timeline showing run steps with type icons, live mode support - -```svelte - - -
- {#if live} -
● LIVE
- {/if} - - {#if loading} -
-
-
-
-
- {:else if steps.length === 0} -
No steps recorded yet
- {:else} -
- {#each steps as step (step.id)} - {@const config = getTypeConfig(step)} -
-
-
- {config.icon} -
-
-
-
-
- {config.label} - {#if step.tool_name} - {step.tool_name} - {/if} - {relativeTime(step.created_at)} -
-
- {#if step.content.length > 200 && !expanded.has(step.id)} - {step.content.slice(0, 200)}... - - {:else} - {step.content} - {#if step.content.length > 200} - - {/if} - {/if} -
- {#if step.type === 'tool_result'} -
- {step.status === 'success' ? '✓ Success' : '✗ Failed'} -
- {/if} -
-
- {/each} -
- {/if} -
- - - -``` - ---- - -## File 18: `apps/web/src/lib/components/memory/MemoryDashboard.svelte` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/lib/components/memory/MemoryDashboard.svelte)** - -Tabbed memory browser: KV store, vector search, and workspace files - -```svelte - - -
-
- - - -
- -
- {#if activeTab === 'kv'} -
- {#if loading} - - - - - - - - - - - {#each [1, 2, 3] as _} - - - - - - - {/each} - -
KeyValueCreatedDelete
- {:else if kvMemories.length === 0} -
-

No key-value memories stored

-
- {:else} -
- - - - - - - - - - - {#each kvMemories as m} - - - - - - - {/each} - -
KeyValueCreatedDelete
{m.key}{m.value}{formatDate(m.created_at)} - -
-
- {/if} -
- {/if} - - {#if activeTab === 'vector'} -
- - - {#if loading} - {#each [1, 2, 3] as _} -
-
-
-
- {/each} - {:else if vectorMemories.length === 0} -
-

No vector memories stored yet

-
- {:else} - {#each vectorMemories as m} -
-

- {m.content?.slice(0, 120)}{m.content && m.content.length > 120 ? '...' : ''} -

- {#if m.metadata} -
- {#each Object.keys(m.metadata) as key} - {key} - {/each} -
- {/if} - -
- {/each} - {/if} -
- {/if} - - {#if activeTab === 'files'} -
- {#if loading} - - - - - - - - - - - - {#each [1, 2, 3] as _} - - - - - - - - {/each} - -
FilenameSizeTypeCreatedActions
- {:else if files.length === 0} -
-

No workspace files

-
- {:else} -
- - - - - - - - - - - - {#each files as f} - - - - - - - - {/each} - -
FilenameSizeTypeCreatedActions
{f.filename}{formatBytes(f.size)}{f.mime_type}{formatDate(f.created_at)} - ↓ - -
-
- {/if} -
- {/if} -
-
- - - -``` - ---- - -## File 19: `apps/web/src/lib/components/voice/VoiceButton.svelte` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/lib/components/voice/VoiceButton.svelte)** - -Browser STT/TTS voice controls with mic button and speaker mute toggle - -```svelte - - -{#if supported} -
- - - - - {#if speaking} - ◎ - {/if} -
-{/if} - - - -``` - ---- - -## File 20: `apps/web/src/routes/agents/[id]/+page.svelte` - -**Status:** `REPLACE` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/routes/agents/[id]/+page.svelte)** - -Agent detail page with Overview/Runs/Memory/Settings tabs and live run visualizer - -```svelte - - -{#if loading} -
-
-
-
-
-{/if} - -{#if !loading && agent} -
-
-

{agent.name}

-

{agent.goal}

-
-
- - -
-
- -
- {#each ['overview', 'runs', 'memory', 'settings'] as tab} - - {/each} -
- - {#if activeTab === 'overview'} -
-
-
- Total Runs - {runList.length} -
-
- Success Rate - {successRate}% -
-
- Last Run - - {runList[0] ? formatRelative(runList[0].started_at) : 'Never'} - -
-
- -
-

Goal

-

{agent.goal}

-
- -
-

Configuration

-
- Personality - {agent.personality ?? '--'} - Max Sub-Agents - {agent.max_sub_agents ?? '--'} - Model - {agent.model_override ?? 'Default'} -
-
- -
-

Recent Runs

- {#if runList.length === 0} -

No runs yet.

- {:else} - {#each runList.slice(0, 5) as run} -
- - - {run.summary ? run.summary.slice(0, 60) : 'No summary'} - - {formatRelative(run.started_at)} -
- {/each} - {/if} -
- - {#if activeRun} -
-

Currently Running ●

- -
- {/if} -
- {/if} - - {#if activeTab === 'runs'} -
- {#if runList.length === 0} -

No runs found for this agent.

- {:else} -
- - - - - - - - - - - - {#each runList as run} - (expandedRunId = expandedRunId === run.id ? null : run.id)} - style="cursor:pointer" - > - - - - - - - {#if expandedRunId === run.id} - - - - {/if} - {/each} - -
StatusStartedDurationSummaryView
{formatRelative(run.started_at)}{run.duration_ms ? formatDurationMs(run.duration_ms) : '--'}{run.summary ?? '--'}{expandedRunId === run.id ? '▲' : '▼'}
-
- -
-
-
- {/if} -
- {/if} - - {#if activeTab === 'memory'} -
- -
- {/if} - - {#if activeTab === 'settings'} -
- -
- {/if} -{/if} - -{#if !loading && !agent} -
-

Agent not found or failed to load.

-
-{/if} - - - -``` - ---- - -## File 21: `apps/web/src/routes/agents/new/+page.svelte` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/routes/agents/new/+page.svelte)** - -New agent creation page wrapping AgentEditor with navigation on save - -```svelte - - -
- -
- -
-
- - -``` - ---- - -## File 22: `apps/web/src/routes/memory/+page.svelte` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/routes/memory/+page.svelte)** - -Global memory view with agent selector and MemoryDashboard component - -```svelte - - -
-

Memory

-

Browse and search agent memories

- - {#if loading} -
-
-
-
-
-
- {:else if agentList.length === 0} -
-

No agents yet

- Create an Agent -
- {:else} -
- - -
- - {#if selectedAgentId} - - {/if} - {/if} -
- - -``` - ---- - -## File 23: `apps/web/src/routes/settings/+page.svelte` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/routes/settings/+page.svelte)** - -Settings page with LLM provider, voice config, personalities, and danger zone - -```svelte - - -{#if loading} -
-
-

Loading settings...

-
-{:else} -
- - - -
-
-

LLM Provider

-

Configure your language model provider and credentials

-
-
-
- - -
- -
- - -
- - {#if appSettings.llm_provider === 'ollama'} -
- - -
- {:else} -
- - -
- {/if} - -
- - -
-
-
- - -
-
-

Voice Settings

-

Configure speech recognition and text-to-speech

-
-
-
- -
- - {#if appSettings.voice_enabled} -
-
-

Speech-to-Text (STT) Mode

-
- - -
-
- -
-

Text-to-Speech (TTS) Mode

-
- - -
-
- - {#if appSettings.tts_mode === 'elevenlabs'} -
- - -
-
- - -
- {/if} -
- {/if} - -
- -
-
-
- - -
-
-

Personalities

-

Manage AI personalities and system prompts

-
-
-
- {#each personalities as personality (personality.key)} -
- {#if editingPersonalityKey === personality.key} -
-
- - -
-
- - -
-
- - -
-
- - -
-
- {:else} -
-
- {personality.name} - {#if builtInKeys.includes(personality.key)} - Built-in - {/if} - {#if personality.description} -

{personality.description}

- {/if} -
-
- - {#if !builtInKeys.includes(personality.key)} - - {/if} -
-
- {/if} -
- {/each} -
- - {#if showAddPersonality} -
-

Add Custom Personality

-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- {:else} - - {/if} -
-
- - -
-
-

Danger Zone

-

Irreversible actions -- proceed with caution

-
-
-
-
- Clear All Memory -

Permanently delete all conversation memory and context. This cannot be undone.

-
- -
-
-
-
- Reset Settings -

Reset all settings to their default values. Your API keys will be cleared.

-
- -
-
-
-
-{/if} - - - - - - - - -``` - ---- - -## File 24: `apps/web/src/routes/+error.svelte` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/apps/web/src/routes/+error.svelte)** - -Terminal-style error page with status, message, optional stack trace, and navigation - -```svelte - - -
-
- - -

Error {$page.status}

- -

{$page.error?.message ?? 'An unexpected error occurred.'}

- - {#if $page.error?.stack} -
- View stack trace -
{$page.error.stack}
-
- {/if} - -
- <- Back to Home - -
-
-
- - -``` - ---- - -## File 25: `packages/tools/src/searxng.ts` - -**Status:** `NEW` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/packages/tools/src/searxng.ts)** - -SearXNG search tool with DuckDuckGo fallback, 5s timeout, zod input validation - -```typescript -import z from 'zod'; - -interface SearchResult { - title: string; - url: string; - snippet: string; - source: string; - score?: number; -} - -interface SearXNGResult { - title: string; - url: string; - content: string; - score?: number; -} - -interface SearXNGResponse { - results: SearXNGResult[]; -} - -const SEARXNG_URL = process.env.SEARXNG_URL || 'http://searxng:8080'; - -async function searchSearXNG( - query: string, - categories: string[] = ['general'], - language: string = 'en-US', - maxResults: number = 10 -): Promise { - const params = new URLSearchParams({ - q: query, - format: 'json', - categories: categories.join(','), - language: language, - pageno: '1', - }); - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 5000); - - try { - const response = await fetch(`${SEARXNG_URL}/search`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: params.toString(), - signal: controller.signal, - }); - - if (!response.ok) { - throw new Error(`SearXNG responded with status: ${response.status}`); - } - - const data = (await response.json()) as SearXNGResponse; - - return data.results.slice(0, maxResults).map((result: SearXNGResult): SearchResult => ({ - title: result.title, - url: result.url, - snippet: result.content, - source: 'searxng', - score: result.score, - })); - } finally { - clearTimeout(timeoutId); - } -} - -async function searchDuckDuckGo( - query: string, - maxResults: number = 10 -): Promise { - const params = new URLSearchParams({ q: query }); - - const response = await fetch(`https://html.duckduckgo.com/html/?${params.toString()}`, { - method: 'GET', - headers: { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - }, - }); - - if (!response.ok) { - throw new Error(`DuckDuckGo responded with status: ${response.status}`); - } - - const html = await response.text(); - - const results: SearchResult[] = []; - - const linkRegex = /class="result__a"[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>/g; - const snippetRegex = /class="result__snippet"[^>]*>([^<]+(?:<[^>]+>[^<]*<\/[^>]+>[^<]*)*)<\/a>/g; - - const links: Array<{ url: string; title: string }> = []; - let linkMatch: RegExpExecArray | null; - - while ((linkMatch = linkRegex.exec(html)) !== null) { - const rawUrl = linkMatch[1]; - const title = linkMatch[2].trim(); - - let url = rawUrl; - try { - const uddgParam = new URL(rawUrl).searchParams.get('uddg'); - if (uddgParam) { - url = decodeURIComponent(uddgParam); - } - } catch { - // keep original url - } - - if (url && title) { - links.push({ url, title }); - } - } - - const snippetMatches: string[] = []; - const snippetSimpleRegex = /class="result__snippet"[^>]*>(.*?)<\/a>/gs; - let snippetMatch: RegExpExecArray | null; - - while ((snippetMatch = snippetSimpleRegex.exec(html)) !== null) { - const snippet = snippetMatch[1].replace(/<[^>]+>/g, '').trim(); - snippetMatches.push(snippet); - } - - for (let i = 0; i < Math.min(links.length, maxResults); i++) { - const link = links[i]; - const snippet = snippetMatches[i] || ''; - - results.push({ - title: link.title, - url: link.url, - snippet, - source: 'duckduckgo', - }); - } - - return results; -} - -const inputSchema = z.object({ - query: z.string().describe('Search query'), - categories: z - .array(z.string()) - .optional() - .default(['general']) - .describe('Search categories (e.g. general, news, images, videos, science, it)'), - language: z.string().optional().default('en-US').describe('Search language'), - max_results: z - .number() - .optional() - .default(10) - .transform((val) => Math.min(val, 20)) - .describe('Maximum number of results to return (capped at 20)'), -}); - -export const searxngTool = { - name: 'searxng_search', - description: - 'Search the web using a self-hosted SearXNG instance with automatic fallback to DuckDuckGo. Returns relevant search results including titles, URLs, and snippets.', - inputSchema, - execute: async (input: z.infer): Promise<{ - results: SearchResult[]; - count: number; - query: string; - source: string; - }> => { - const { query, categories, language, max_results } = input; - - let results: SearchResult[] = []; - let source = 'searxng'; - - try { - results = await searchSearXNG(query, categories, language, max_results); - - if (results.length === 0) { - throw new Error('No results from SearXNG'); - } - } catch (error) { - source = 'duckduckgo'; - try { - results = await searchDuckDuckGo(query, max_results); - } catch (fallbackError) { - const searxngError = error instanceof Error ? error.message : String(error); - const ddgError = fallbackError instanceof Error ? fallbackError.message : String(fallbackError); - throw new Error( - `Both SearXNG and DuckDuckGo searches failed. SearXNG: ${searxngError}. DuckDuckGo: ${ddgError}` - ); - } - } - - return { - results, - count: results.length, - query, - source, - }; - }, -}; - -``` - ---- - -## File 26: `docker-compose.yml` - -**Status:** `UPDATE` | **[View on GitHub](https://github.com/raynr7/agentswarm/blob/main/docker-compose.yml)** - -Docker Compose with app(200m), web(64m), searxng(256m) -- fits 1GB OCI instance - -```yaml -version: '3.8' - -services: - app: - build: . - container_name: agentswarp-server - ports: - - "3001:3001" - environment: - NODE_ENV: production - DATABASE_PATH: /app/data/agentswarp.db - SECRET_KEY: ${SECRET_KEY:-changeme-generate-32-char-secret} - JWT_SECRET: ${JWT_SECRET:-changeme-jwt-secret} - ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@localhost} - ADMIN_PASSWORD: ${ADMIN_PASSWORD:-changeme} - OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://host.docker.internal:11434} - OPENAI_API_KEY: ${OPENAI_API_KEY:-} - GROQ_API_KEY: ${GROQ_API_KEY:-} - ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} - GOOGLE_AI_API_KEY: ${GOOGLE_AI_API_KEY:-} - LITELLM_BASE_URL: ${LITELLM_BASE_URL:-} - SEARXNG_URL: ${SEARXNG_URL:-http://searxng:8080} - ELEVENLABS_API_KEY: ${ELEVENLABS_API_KEY:-} - volumes: - - data:/app/data - mem_limit: 200m - restart: unless-stopped - networks: - - internal - - default - depends_on: - searxng: - condition: service_started - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3001/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - - web: - build: ./apps/web - container_name: agentswarp-web - ports: - - "3000:3000" - environment: - PUBLIC_API_URL: ${PUBLIC_API_URL:-http://localhost:3001} - PUBLIC_WS_URL: ${PUBLIC_WS_URL:-ws://localhost:3001/ws} - mem_limit: 64m - restart: unless-stopped - depends_on: - app: - condition: service_healthy - - searxng: - image: searxng/searxng:latest - container_name: agentswarp-search - ports: - - "8080:8080" - volumes: - - searxng-config:/etc/searxng - environment: - BASE_URL: http://localhost:8080/ - INSTANCE_NAME: AgentSwarp Search - AUTOCOMPLETE: "false" - mem_limit: 256m - restart: unless-stopped - networks: - - internal - - default - cap_drop: - - ALL - cap_add: - - CHOWN - - SETGID - - SETUID - logging: - driver: json-file - options: - max-size: "1m" - max-file: "1" - -networks: - internal: - driver: bridge - internal: true - -volumes: - data: - driver: local - searxng-config: - driver: local - -``` - ---- - -## Deployment Notes - -### RAM Budget - -| Service | Memory Limit | -|---------|-------------| -| app (AgentSwarp server) | 200 MB | -| web (SvelteKit frontend) | 64 MB | -| searxng (Self-hosted search) | 256 MB | -| **Total** | **520 MB** | - -[OK] Fits comfortably in a 1 GB OCI/VPS instance with ~480 MB headroom. - -### Quick Start - -```bash -# Clone the repo -git clone https://github.com/raynr7/agentswarm.git -cd agentswarm - -# Create .env from example -cp .env.example .env -# Edit .env with your API keys and secrets - -# Start all services -docker compose up -d - -# Access -# Frontend: http://localhost:3000 -# API: http://localhost:3001 -# Search: http://localhost:8080 -``` - -### .env.example - -Create a `.env.example` file in the repo root: - -```bash -# Required: Change these before production deployment -SECRET_KEY=changeme-generate-a-32-char-random-string -JWT_SECRET=changeme-jwt-secret-also-random - -# Admin credentials -ADMIN_EMAIL=admin@localhost -ADMIN_PASSWORD=changeme - -# LLM providers (add whichever you use) -OPENAI_API_KEY= -GROQ_API_KEY= -ANTHROPIC_API_KEY= -GOOGLE_AI_API_KEY= - -# Optional: Ollama (if running locally, host.docker.internal works on Mac/Windows) -OLLAMA_BASE_URL=http://host.docker.internal:11434 - -# Optional: LiteLLM proxy -LITELLM_BASE_URL= - -# Optional: ElevenLabs TTS -ELEVENLABS_API_KEY= - -# Frontend URLs (change for production) -PUBLIC_API_URL=http://localhost:3001 -PUBLIC_WS_URL=ws://localhost:3001/ws -``` - ---- - -*Generated reference - Phase 2 Complete* -*Repository: https://github.com/raynr7/agentswarm*