noterai
v0.1.2
Published
Locally-run, browser-based AI agent dashboard for developers using LLM coding assistants
Maintainers
Readme
Noter
A locally-run, browser-based AI agent dashboard that monitors AI agent activity, summarizes context using a local or cloud LLM, and generates next-step task suggestions and implementation prompts.
npm package:
noterai— install withnpx noteraiornpm install -g noterai. The CLI command isnoter.
Architecture
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ OpenCode (SSE port 4096) │ │ Claude Code (MCP stdio) │
└────────────┬────────────────────┘ └────────────┬────────────────────┘
│ SSE │ MCP
└────────────────┬──────────────────────┘
│
┌─────────▼──────────┐
│ Express Backend │ port 3000
│ (packages/server) │
│ │
│ • EventStore │ 200 events, FIFO ring buffer
│ • StateManager │ summary, suggestions, notes, prompts
│ • Summarizer │ Ollama + ai-sdk generateObject
│ • Suggester │ Ollama + ai-sdk generateObject
│ • PromptBuilder │ Ollama / Anthropic / OpenAI / Gemini
│ • OllamaQueue │ serialized inference queue
│ • OllamaCache │ memoized provider instances
│ • wsServer │ ping/pong heartbeat, 1MB msg limit
│ • MCP Server │ stdio transport, send_event + get_state
└─────────┬────────┘
│ WebSocket + HTTP
┌─────────▼──────────┐
│ React Frontend │ port 5173 (dev) / served static
│ (packages/client) │
│ │
│ • 2x2 grid layout│ Editorial Brutalism design system
│ • wsClient │ auto-reconnect, exponential backoff
│ • 4 panels │ Notes, Tasks, Context, Prompts
└──────────────────┘Quick Start
From npm (no clone needed)
npx noterai # Run without installing (opens browser automatically)
npx noterai serve # Production server without auto-opening browser
npx noterai serve --open # Production server + auto-open browser
npx noterai help # Show all commands
npx noterai version # Print version
# Or install globally:
npm install -g noterai
noter # Then run directlyFrom source (development)
git clone https://github.com/oniwakaa/noter.git && cd noter
pnpm install
pnpm run devThe dashboard opens at http://localhost:5173. The backend runs on http://localhost:3000.
Note:
pnpm run devstarts both the backend server (:3000) and the Vite dev client (:5173) concurrently.
Prerequisites
- Node.js >= 20 (tested on v24.4.1)
- pnpm >= 10 (tested on v10.30.3)
- Ollama running locally with at least one model pulled (check
ollama list)- Default model in
.env.exampleisfalcon-h1r:7b-q4_K_M— update.envto a model you have installed (e.g.deepseek-v4-flash:cloudorgemma4:e4b)
- Default model in
- (Optional) OpenCode CLI for OpenCode SSE event ingestion
- (Optional) API keys if using cloud LLM providers for prompt generation (Anthropic, OpenAI, Gemini)
Environment Variables
Copy .env.example to .env and adjust values:
cp .env.example .env| Variable | Default | Description |
|----------|---------|-------------|
| PORT | 3000 | Backend HTTP/WebSocket server port |
| OLLAMA_BASE_URL | http://localhost:11434 | Ollama API endpoint |
| OLLAMA_MODEL | falcon-h1r:7b-q4_K_M | Model for summarization & suggestions. Must be a model you have pulled locally. |
| CONTEXT_OFFLOAD_THRESHOLD | 0.8 | Auto-offload model when accumulated context exceeds this % of context window (0.0–1.0) |
| VRAM_LIMIT_GB | 6 | VRAM display limit for the Ollama status memory bar |
| OPENCODE_PORT | 4096 | OpenCode SSE stream port (only when ACTIVE_SOURCE includes opencode) |
| ACTIVE_SOURCE | generic | Event source: opencode, claude-code, codex, factory-droid, pi, generic, auto, both, or all |
| NOTER_EVENT_LOG | /tmp/noter-events.jsonl | Path to JSONL event log for file-based adapters (Droid, Pi) |
| GENERIC_LOG_PATH | /tmp/noter-generic-events.jsonl | Path to JSONL event log for the generic adapter |
| LLM_PROVIDER | ollama | Prompt generation provider: ollama, anthropic, openai, gemini |
| ANTHROPIC_API_KEY | (empty) | Required when LLM_PROVIDER=anthropic |
| OPENAI_API_KEY | (empty) | Required when LLM_PROVIDER=openai |
| GEMINI_API_KEY | (empty) | Required when LLM_PROVIDER=gemini |
Scripts
Root (package.json)
| Script | Description |
|--------|-------------|
| pnpm run dev | Start both server and client concurrently |
| pnpm run dev:server | Start backend only (Express + WebSocket on port 3000) |
| pnpm run dev:client | Start frontend only (Vite dev server on port 5173) |
| pnpm run build | Build shared + server + client + bin for production |
| pnpm run typecheck | TypeScript strict mode check (both packages) |
| pnpm run test | Run full Vitest suite (server + client workspaces) |
Sub-package scripts
# Client build
cd packages/client && pnpm run build # tsc && vite build
cd packages/client && pnpm run preview # preview production build
# Server build
cd packages/server && pnpm run build # tsc
cd packages/server && pnpm run build:mcp # tsc, outputs dist/mcp-server.jsCLI (bin/noter.ts / bin/noter.js)
The package exposes a noter CLI via the bin field:
noter --help # Show all commands
noter version # Print version
noter serve # Production server (serves static frontend + API)
noter serve --open # Production server + auto-open browser
noter dev # Development mode (hot-reload)
noter build # Build client & server for production
PORT=8080 noter serve # Custom portProject Structure
noter/
├── bin/
│ ├── noter.ts # CLI source (compiled to noter.js for publish)
│ ├── noter.js # Compiled CLI entry point (gitignored, built on publish)
│ └── tsconfig.json # Bin-specific tsconfig (node16 module resolution)
├── packages/
│ ├── server/ # Express + WebSocket backend
│ │ ├── src/
│ │ │ ├── adapters/
│ │ │ │ ├── opencodeAdapter.ts # OpenCode SSE → AgentEvent
│ │ │ │ ├── claudeAdapter.ts # Claude Code MCP → AgentEvent
│ │ │ │ ├── codexAdapter.ts # OpenAI Codex CLI MCP → AgentEvent
│ │ │ │ ├── droidAdapter.ts # Factory Droid hooks → AgentEvent
│ │ │ │ ├── piAdapter.ts # Pi (pi.dev) hooks → AgentEvent
│ │ │ │ └── genericAdapter.ts # Generic JSONL/webhook → AgentEvent
│ │ │ ├── llm/
│ │ │ │ ├── ollamaClient.ts # Low-level Ollama client (60s timeout, AbortController)
│ │ │ │ ├── ollamaCache.ts # Memoized createOllama() provider instances
│ │ │ │ ├── ollamaQueue.ts # Serialized inference queues (suggester + inference)
│ │ │ │ ├── summarizer.ts # ContextSummary generation from events
│ │ │ │ ├── suggester.ts # TaskSuggestion[] generation from context + notes
│ │ │ │ └── promptBuilder.ts # GeneratedPrompt from task + context + notes
│ │ │ ├── routes/
│ │ │ │ ├── notes.ts # POST /api/notes → save + fire-and-forget suggest
│ │ │ │ ├── prompt.ts # POST /api/prompt (by taskId) + GET /:taskId
│ │ │ │ ├── summarize.ts # POST /api/summarize (mutex-guarded)
│ │ │ │ └── ollama.ts # GET /api/ollama/status + POST /api/ollama/offload
│ │ │ ├── config.ts # AppConfig, types, loadConfig(), Zod-like validation
│ │ │ ├── eventStore.ts # In-memory FIFO ring buffer (200 max)
│ │ │ ├── stateManager.ts # Shared state + task→prompt map
│ │ │ ├── wsServer.ts # WebSocketServer wrapper (heartbeat, 1MB limit)
│ │ │ ├── mcp-server.ts # MCP stdio server (send_event, get_state tools)
│ │ │ └── index.ts # createApp() factory + listenServer() + startServer()
│ │ └── package.json
│ └── client/ # React + Vite frontend
│ ├── src/
│ │ ├── components/
│ │ │ ├── NotesPanel.tsx # Debounced textarea, drag-to-delete
│ │ │ ├── SuggestionsPanel.tsx # Task cards, priority badges, conflict warnings
│ │ │ ├── ContextPanel.tsx # Summary fields, Ollama status, activity log
│ │ │ ├── PromptsPanel.tsx # Build → expand prompt, copy to clipboard
│ │ │ └── OllamaStatus.tsx # Model name, connection, VRAM bar, offload
│ │ ├── styles/
│ │ │ └── tokens.css # DESIGN.md tokens: monochrome, zero-radius, no shadows
│ │ ├── App.tsx # 2x2 grid, top nav, dark/light toggle
│ │ ├── wsClient.ts # Singleton WebSocket client (reconnect backoff)
│ │ ├── localDeletions.ts # Drag-delete tracking set
│ │ ├── types.ts # Shared TypeScript interfaces
│ │ └── test/ # Component tests (RTL + jsdom)
│ └── package.json
├── .env.example # All environment variables with comments
├── vitest.workspace.ts # Server (node) + Client (jsdom) test workspaces
├── tsconfig.base.json # Shared TypeScript strict config
├── pnpm-workspace.yaml # Monorepo workspace definition
└── README.md # This fileFrontend Panels
The UI is a fixed 2x2 viewport grid separated by 1px solid dividers (outline_variant).
| Panel | Grid Position | Description |
|-------|--------------|-------------|
| Notes | Top-left | Borderless textarea with debounced save (500ms POST to /api/notes). Supports drag-to-delete: drop a task card here to remove it. |
| Suggested Tasks | Top-right | Task cards (TaskSuggestion[]) with priority badges (high/medium/low), conflict warnings, and a "Build →" action to generate a prompt. |
| Agent Context | Bottom-left | ContextSummary fields: what's happening, files changed, current goal, blockers. Includes Ollama status (model, VRAM bar, offload button). Terminal-style activity log. |
| Suggested Prompts | Bottom-right | List of generated prompts. Click "Build →" to expand a prompt row (animated from 0fr to 1fr). Each prompt has a copy-to-clipboard button. |
Design System: High-End Editorial Brutalism — zero border-radius, no box-shadow, monochrome palette (#131313 surface), Space Grotesk for labels, Geist Mono for data/code. See DESIGN.md for full spec.
WebSocket Message Types
All messages are typed as WsMessage union. The client auto-reconnects with exponential backoff (500ms → 30s max).
type WsMessage =
| { type: 'sync'; payload: { events: AgentEvent[]; summary: ContextSummary | null; suggestions: TaskSuggestion[]; notes: HumanNotes | null } }
| { type: 'event'; payload: AgentEvent }
| { type: 'summary'; payload: ContextSummary }
| { type: 'suggestions'; payload: TaskSuggestion[] }
| { type: 'notes'; payload: HumanNotes };sync— sent to every new WebSocket connection to hydrate frontend state.event— broadcast when a new agent event is ingested.summary— broadcast when the summarizer generates a new context summary.suggestions— broadcast when new task suggestions are generated.notes— broadcast when notes are updated.
Server-side protections: ping/pong heartbeat every 30s (stale connections terminated), maximum message size 1MB.
API Endpoints
GET /health
Returns {"status":"ok"} — used by services.yaml healthcheck and CLI startup verification.
POST /api/notes
Body: { content: string, updatedAt: number } (Zod-validated)
Saves notes, returns { saved: true } immediately, then fire-and-forget generates TaskSuggestion[] via the suggester queue. Suggestions are broadcast via WebSocket.
POST /api/prompt
Body: { taskId: string } (Zod-validated)
Generates a GeneratedPrompt for the given task using the configured LLM_PROVIDER. Uses shared INFERENCE_QUEUE to serialize with summarization. Returns 404 if taskId not found, 500 if API key missing or generation fails.
Also: GET /api/prompt/:taskId — retrieve a previously generated prompt for a task.
POST /api/summarize
Body: none required.
Triggers context summarization manually. Mutex-guarded: concurrent requests return 202 without duplicate Ollama calls. Returns ContextSummary with fields: whatIsHappening, filesChanged, currentGoal, blockers, rawEventCount.
GET /api/ollama/status
Returns Ollama /api/ps response formatted with models[] and memory{} (totalSize, totalVram, usedPercent).
POST /api/ollama/offload
Offloads all loaded Ollama models (sends keep_alive: 0).
MCP Integration
Noter exposes an MCP server via stdio transport for use with Claude Code, Cursor, VS Code Copilot, Continue.dev, Windsurf, and OpenAI Codex CLI.
Tools:
send_event— Push an agent event into the pipeline. Zod-validated againstAgentEventschema. Stored ineventStore, broadcast via WebSocket, and processed by the summarizer.get_state— Returns current application state (summary, suggestions, notes) as JSON.
Configuration — add to your agent's MCP settings (e.g. ~/.claude/mcp.json, .cursor/mcp.json, or equivalent):
{
"mcpServers": {
"noter": {
"command": "node",
"args": ["packages/server/dist/mcp-server.js"]
}
}
}Per-Harness Setup
Claude Code & OpenAI Codex CLI:
Both use the same MCP protocol. Add the MCP server config above to your agent's settings, then set ACTIVE_SOURCE=claude-code (or codex, or all).
Factory Droid:
Droid writes events to /tmp/noter-events.jsonl via its hooks system. The droidAdapter.ts watches this file automatically when ACTIVE_SOURCE includes factory-droid.
Pi (pi.dev):
Pi extensions can emit events to the same JSONL file. Install a noter-bridge.ts extension in Pi that writes { type, sessionId, payload, timestamp } lines to NOTER_EVENT_LOG.
OpenCode:
Run opencode with the --events flag pointing to http://localhost:${OPENCODE_PORT}. The opencodeAdapter.ts connects via SSE.
Generic (any harness):
The generic adapter watches a JSONL file (default: /tmp/noter-generic-events.jsonl) for events from any agent harness. Zero-config — just write JSON events to the default path, or set GENERIC_LOG_PATH for a custom path. Set ACTIVE_SOURCE=generic (the default).
Ollama Integration
- Provider caching:
ollamaCache.tsmemoizescreateOllama()instances bybaseURLso the same provider config is reused across summarizer, suggester, and prompt builder. - Queue serialization:
ollamaQueue.tsprovides two queues:INFERENCE_QUEUE— shared between summarizer and prompt builder (prevents concurrent calls).SUGGESTER_QUEUE— dedicated queue for suggestion generation (runs independently).
- Timeout:
ollamaClient.tsusesAbortControllerwith a 60-second default timeout. Configurable viaOllamaClientOptions.timeout. - Context auto-offload: Tracks accumulated token usage and automatically offloads models when the context window threshold (
CONTEXT_OFFLOAD_THRESHOLD) is exceeded.
LLM Providers for Prompt Generation
| Provider | Model ID | Requires Key |
|----------|---------|-------------|
| ollama | Configured OLLAMA_MODEL | No |
| anthropic | claude-sonnet-4-20250514 | ANTHROPIC_API_KEY |
| openai | gpt-4o | OPENAI_API_KEY |
| gemini | gemini-2.5-flash | GEMINI_API_KEY |
Event Sources
| Source | Adapter | Transport | Trigger |
|--------|---------|-----------|---------|
| OpenCode | opencodeAdapter.ts | SSE (http://localhost:${OPENCODE_PORT}) | session.idle, tool.before/after, message.updated |
| Claude Code | claudeAdapter.ts | MCP stdio | send_event tool calls |
| OpenAI Codex CLI | codexAdapter.ts | MCP stdio | send_event tool calls |
| Factory Droid | droidAdapter.ts | File hooks (AGENTDESK_EVENT_LOG) | tool.before/after, session.start/end |
| Pi (pi.dev) | piAdapter.ts | File hooks (AGENTDESK_EVENT_LOG) | tool_call, tool_result, agent_start/end |
| Generic | genericAdapter.ts | JSONL file watcher + webhook | Any harness writing JSON events |
Set ACTIVE_SOURCE=all to ingest from all supported harnesses simultaneously.
Testing
# Full suite (server + client)
pnpm run test
# Server only (node environment)
pnpm vitest run --project server
# Client only (jsdom environment)
pnpm vitest run --project client
# With coverage
pnpm vitest run --coverage- Server tests: 30+ test files covering routes, adapters (5 harnesses), LLM pipeline, config validation, WebSocket server, MCP server, session management, and CLI.
- Client tests: 10+ component test files using React Testing Library + jsdom.
TypeScript
Strict mode enabled across both packages. Root tsconfig.base.json provides shared compiler options.
pnpm run typecheck # Both packagesSecurity
- No hardcoded secrets in source files.
.envis in.gitignore.- API keys are read exclusively via
loadConfig()inconfig.ts.
License
MIT
