commit 9c019e3d4125300e4f2ca90f1685b91a28288f50 Author: Adam Jeniski Date: Sun Jan 18 22:07:39 2026 -0500 plan diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..3cb28d1 --- /dev/null +++ b/plan.md @@ -0,0 +1,302 @@ +# Spiceflow - AI Session Orchestration PWA + +## Overview +A Progressive Web App for monitoring and interacting with running Claude Code and OpenCode sessions from mobile devices. "The spice must flow." + +## Architecture + +``` +┌─────────────────┐ ┌─────────────────────────┐ ┌─────────────────┐ +│ Claude Code │◀───▶│ Spiceflow Server │◀───▶│ PWA Client │ +│ (CLI) │ │ (Clojure) │ │ (Svelte) │ +└─────────────────┘ │ │ └─────────────────┘ + │ ┌─────────────────┐ │ +┌─────────────────┐ │ │ SQLite + DB │ │ +│ OpenCode │◀───▶│ │ Abstraction │ │ +│ (CLI) │ │ │ WebSocket/SSE │ │ +└─────────────────┘ │ └─────────────────┘ │ + └─────────────────────────┘ +``` + +## CLI Analysis + +### Claude Code +- **Session storage**: `~/.claude/projects/{encoded-path}/{session-id}.jsonl` +- **Resume session**: `claude --resume ` or `claude --session-id ` +- **Programmatic I/O**: `--output-format stream-json` + `--input-format stream-json` + `--print` +- **JSONL format**: + ```json + { + "type": "user", + "sessionId": "uuid", + "parentUuid": "uuid|null", + "message": {"role": "user", "content": "..."}, + "uuid": "message-uuid", + "timestamp": "ISO-8601" + } + ``` +- **Key flags**: `--permission-mode`, `--model`, `--continue` + +### OpenCode +- **List sessions**: `opencode session list` +- **Export session**: `opencode export ` (JSON) +- **Continue session**: `opencode --session ` or `--continue` +- **Headless mode**: `opencode serve` (starts server), `opencode run ` +- **JSON format**: + ```json + { + "info": {"id": "ses_xxx", "title": "...", "time": {...}}, + "messages": [{"info": {...}, "parts": [{type, text, ...}]}] + } + ``` + +## Tech Stack +- **Frontend**: SvelteKit + TypeScript + Vite +- **PWA**: vite-plugin-pwa (Workbox under the hood) +- **Styling**: Tailwind CSS (mobile-first) +- **Backend**: Clojure + Ring/Jetty + next.jdbc +- **Database**: SQLite (via generic interface for future swapping) +- **Real-time**: WebSocket (via Ring adapter) or SSE +- **Build**: deps.edn (Clojure), pnpm (frontend) + +## Supported Agentic Runtimes + +The UI includes a **runtime selector dropdown** for choosing which agentic CLI to use: + +| Runtime | Status | Notes | +|---------|--------|-------| +| **Claude Code** | Default | Anthropic's official CLI | +| OpenCode | Supported | Open-source alternative | +| *(Future)* | Planned | Aider, Cursor CLI, Continue, etc. | + +The adapter system is designed to be extensible—adding a new runtime requires implementing the `AgentAdapter` protocol. + +## Mobile UI Design + +The PWA is **mobile-first**. Key design decisions: + +``` +┌─────────────────────────────┐ +│ ☰ Spiceflow [Claude ▾] │ ← Header + runtime dropdown +├─────────────────────────────┤ +│ │ +│ ▶ User: Fix the login... │ ← Collapsed (tap to expand) +│ ▼ Assistant: I'll fix... │ ← Expanded message +│ [full content here] │ +│ ... │ +│ ▶ User: Now add tests │ ← Collapsed +│ ▶ Assistant: Added 3... │ ← Collapsed +│ │ +├─────────────────────────────┤ +│ [Type message...] [▶] │ ← Small fixed input bar +└─────────────────────────────┘ +``` + +- **Collapsible messages**: Each message shows a preview (first line or summary). Tap to expand full content. Keeps history skimmable. +- **Fixed input bar**: Small, anchored to bottom. Expands only when focused. +- **Touch-friendly**: Large tap targets, swipe gestures for navigation. +- **Session list**: Cards showing title, runtime badge, last activity. Pull-to-refresh. + +## Database Abstraction + +Generic interface to allow mocking in tests or swapping providers: + +```clojure +;; src/spiceflow/db/protocol.clj +(defprotocol DataStore + (get-sessions [this]) + (get-session [this id]) + (save-session [this session]) + (update-session [this id data]) + (delete-session [this id]) + (get-messages [this session-id]) + (save-message [this message])) + +;; Implementations: +;; - SQLiteStore (production) +;; - AtomStore (testing/mocking) +;; - Future: PostgresStore, etc. +``` + +SQLite schema: +```sql +CREATE TABLE sessions ( + id TEXT PRIMARY KEY, + provider TEXT NOT NULL, -- 'claude' | 'opencode' + external_id TEXT, -- original session ID from CLI + title TEXT, + working_dir TEXT, + status TEXT DEFAULT 'idle', -- 'idle' | 'running' | 'completed' + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE messages ( + id TEXT PRIMARY KEY, + session_id TEXT REFERENCES sessions(id), + role TEXT NOT NULL, -- 'user' | 'assistant' | 'system' + content TEXT, + metadata TEXT, -- JSON blob for tool calls, etc. + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +``` + +## Project Structure + +``` +spiceflow/ +├── server/ # Clojure backend +│ ├── deps.edn +│ ├── src/spiceflow/ +│ │ ├── core.clj # Entry point +│ │ ├── config.clj # Configuration +│ │ ├── db/ +│ │ │ ├── protocol.clj # DataStore protocol +│ │ │ ├── sqlite.clj # SQLite implementation +│ │ │ └── memory.clj # In-memory impl for tests +│ │ ├── adapters/ +│ │ │ ├── protocol.clj # AgentAdapter protocol +│ │ │ ├── claude.clj # Claude Code adapter +│ │ │ └── opencode.clj # OpenCode adapter +│ │ ├── api/ +│ │ │ ├── routes.clj # REST endpoints +│ │ │ └── websocket.clj # WebSocket handlers +│ │ └── session/ +│ │ └── manager.clj # Session lifecycle +│ ├── test/spiceflow/ +│ │ ├── db_test.clj +│ │ └── adapters_test.clj +│ └── resources/ +│ └── migrations/ # SQL migrations +│ +├── client/ # SvelteKit PWA +│ ├── package.json +│ ├── svelte.config.js +│ ├── vite.config.ts +│ ├── src/ +│ │ ├── routes/ +│ │ │ ├── +layout.svelte +│ │ │ ├── +page.svelte # Session list +│ │ │ └── session/ +│ │ │ └── [id]/ +│ │ │ └── +page.svelte +│ │ ├── lib/ +│ │ │ ├── stores/ +│ │ │ │ ├── runtime.ts # Selected runtime (persisted) +│ │ │ │ └── sessions.ts +│ │ │ ├── components/ +│ │ │ │ ├── RuntimeSelector.svelte # Dropdown for agentic runtime +│ │ │ │ ├── SessionCard.svelte # Session list card +│ │ │ │ ├── MessageList.svelte # Scrollable message container +│ │ │ │ ├── CollapsibleMessage.svelte # Expandable message item +│ │ │ │ └── InputBar.svelte # Fixed bottom input +│ │ │ └── api.ts # Server API client +│ │ └── app.html +│ ├── static/ +│ │ └── manifest.json +│ └── tests/ +│ +└── README.md +``` + +## Core Flow: Continuing a Session + +1. **User opens PWA** → sees list of tracked sessions from SQLite +2. **User selects session** → sees message history, status +3. **User types message** → POST to `/api/sessions/:id/send` +4. **Server receives message**: + - Saves message to SQLite + - Spawns CLI process: `claude --resume --print --output-format stream-json --input-format stream-json` + - Pipes user message to stdin + - Streams stdout back via WebSocket +5. **Client receives streamed response** → renders in real-time +6. **Process completes** → server saves assistant message to SQLite + +## Implementation Phases + +### Phase 1: Project Scaffolding +1. Initialize Clojure project with deps.edn +2. Initialize SvelteKit project with TypeScript +3. Set up Tailwind CSS +4. Create basic directory structure + +### Phase 2: Database Layer +1. Define DataStore protocol +2. Implement SQLite store with next.jdbc +3. Implement in-memory store for tests +4. Write migrations +5. Write tests for both implementations + +### Phase 3: Server Core +1. Ring server setup with Jetty +2. REST API: GET/POST /sessions, GET /sessions/:id +3. WebSocket setup for real-time streaming +4. Configuration management + +### Phase 4: Agent Adapters +1. Define AgentAdapter protocol (discover, spawn, send, stream) +2. Claude Code adapter: + - Discover sessions from `~/.claude/projects/` + - Spawn with `--resume --print --output-format stream-json --input-format stream-json` + - Parse JSONL output +3. OpenCode adapter: + - Discover via `opencode session list` + - Spawn with `opencode run --session ` + - Parse JSON output + +### Phase 5: Client Core +1. SvelteKit routing setup +2. Runtime selector dropdown (Claude Code default, OpenCode, extensible for future runtimes) +3. Session list page filtered by selected runtime +4. Session detail page with message history +5. Real-time message streaming via WebSocket +6. Message input component + +### Phase 6: PWA Setup +1. Configure vite-plugin-pwa +2. Web app manifest (icons, theme color, display) +3. Service worker for offline caching +4. Install prompt handling + +### Phase 7: Interaction Features +1. Send message to session +2. Display tool calls and outputs +3. Session status indicators +4. Error handling and reconnection + +## Key Files to Create + +| File | Purpose | +|------|---------| +| `server/deps.edn` | Clojure dependencies | +| `server/src/spiceflow/db/protocol.clj` | DataStore protocol | +| `server/src/spiceflow/db/sqlite.clj` | SQLite implementation | +| `server/src/spiceflow/adapters/claude.clj` | Claude CLI adapter | +| `server/src/spiceflow/api/routes.clj` | REST API | +| `client/src/routes/+page.svelte` | Session list | +| `client/src/lib/components/RuntimeSelector.svelte` | Runtime dropdown (Claude default) | +| `client/src/lib/stores/sessions.ts` | Session state | +| `client/static/manifest.json` | PWA manifest | + +## Verification + +1. **Database test**: Run `clj -M:test` → all DataStore tests pass +2. **Server test**: `clj -M:run`, hit `GET /api/sessions` → returns `[]` +3. **Adapter test**: Claude adapter discovers existing sessions from ~/.claude +4. **Client test**: `pnpm dev`, open in browser → see session list +5. **PWA test**: Lighthouse audit → PWA criteria met +6. **E2E test**: Send message via PWA → see response streamed back + +## API Endpoints + +``` +GET /api/runtimes List available runtimes (claude=default, opencode, etc.) +GET /api/sessions List all tracked sessions (?runtime=claude filter) +POST /api/sessions Import/track a new session +GET /api/sessions/:id Get session details + messages +POST /api/sessions/:id/send Send message to session +WS /api/ws WebSocket for real-time updates + +GET /api/discover/claude Discover Claude sessions +GET /api/discover/opencode Discover OpenCode sessions +```