Skip to content

v0.1.0: honest dashboard, remote MCP daemon, migration, settings, one-line install#5

Merged
enowdev merged 49 commits into
mainfrom
hardening/rerank-clamp-sse-cors
Jul 16, 2026
Merged

v0.1.0: honest dashboard, remote MCP daemon, migration, settings, one-line install#5
enowdev merged 49 commits into
mainfrom
hardening/rerank-clamp-sse-cors

Conversation

@enowdev

@enowdev enowdev commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Prepares enowx-rag for its first tagged release (v0.1.0). 47 commits; highlights:

Install & distribution

  • One-line install + prebuilt binaries (4 methods): install.sh (curl | sh),
    Homebrew tap, npm wrapper, and go install. GoReleaser cross-compiles CGO-free
    binaries for macOS/Linux (amd64/arm64) and Windows (amd64); web/dist is committed
    so go install embeds the real dashboard. Release workflow runs on tag v*.
  • enowx-rag version / --version, stamped via ldflags with a build-info fallback.

Features

  • MCP over HTTP (remote daemon): --serve also exposes /mcp (Streamable HTTP,
    stateless), gated by the same RAG_ADMIN_TOKEN bearer — run enowx-rag on a VPS.
  • Settings page: manage API keys + admin token from the dashboard (masked view,
    gated reveal, per-request token from env or config).
  • Migration page/engine: re-embed a project into a new model/dimension or move
    between vector stores; cloud import (Qdrant Cloud verified; others experimental).
  • 11 MCP tools (was 6): list/inspect/manage RAG memory; hybrid+rerank in search.
  • Agent-driven setup: /api/docs/setup, idempotent probe, AGENTS.md merge.
  • Comprehensive Docs page; OpenAI-compatible embedder; real backend-sourced metrics
    persisted to SQLite (removed all mock/hardcoded data).

Test hygiene (release prerequisite)

  • Isolated httpapi tests from host ~/.enowx-rag/config.yaml and env vars so the
    suite is deterministic in CI. Verified green uncached even with a host config
    admin_token present and a hostile RAG_ADMIN_TOKEN in the environment.

Security

  • Setup endpoints require localhost or admin token and no longer echo API keys.
  • Cloud connectors beyond Qdrant labelled experimental (mock-tested only).

Follow-up (needs repo secrets, not blocking binaries/install.sh/go install)

  • HOMEBREW_TAP_GITHUB_TOKEN (+ enowdev/homebrew-tap repo) for the brew step.
  • NPM_TOKEN for the npm publish step.

🤖 Generated with Claude Code

enowdev and others added 30 commits July 11, 2026 14:17
Add Dim field to VoyageEmbeddingClient struct and update
NewVoyageEmbeddingClient to accept dim int parameter (3rd arg).
VectorSize() now returns c.Dim (defaulting to 1024 when 0) instead
of hardcoded 1024. voyageRequest struct gets OutputDimension field
sent as output_dimension in API requests. main.go buildProvider()
passes cfg.VectorDim as the 3rd argument.

Includes unit tests with httptest.Server verifying:
- Dim field stored correctly (dim=512, dim=0 defaults to 1024)
- VectorSize returns configured dimension
- output_dimension sent in API request body
- EmbedQuery also sends output_dimension
- Auth header and input_type correctness

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…oviders

Move QueryEmbedder interface from qdrant.go to provider.go so it is
shared across all providers. Update pgvector and chroma SemanticSearch
to check for QueryEmbedder via type assertion and use EmbedQuery
(input_type=query) when available, falling back to Embed() when not.
Qdrant already had this logic and continues to work unchanged.

Add per-provider unit tests with mock embedders implementing and not
implementing QueryEmbedder, verifying correct call dispatch and
fallback behavior.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…mbed_model/embed_dim injection

Add content_hash (SHA-256 first 8 bytes as 16 hex chars) and chunk_version='v2'
to indexer document metadata. Implement incremental sync skip: chunks whose
content_hash matches an existing point are not re-embedded. Add ModelNamer
interface and ModelName() to VoyageEmbeddingClient. Update all provider Index
methods (pgvector, qdrant, chroma) to inject embed_model and embed_dim into
stored metadata. Extend PointInfo with ContentHash and DocID fields, and update
ListPoints in all providers to return these fields. Add Skipped count to
SyncResult. All 34 tests pass including hash format, version string, skip
behavior, and per-provider metadata injection.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…CP refactor

Create pkg/core/service.go with Service struct wrapping provider + reranker + indexer.
Add SearchOpts (K, Recall, Hybrid, Rerank) with defaults K=5, Recall=40.
Implement Search, IndexProject, ListProjects, CreateProject, DeleteProject,
ListPoints, DeletePoints, IndexDocuments, RetrieveContext methods.
Add EventBus for SSE events (Subscribe/Unsubscribe/Publish).
Add Reranker interface and RerankHit struct to provider.go.
Refactor main.go MCP tool handlers to be thin wrappers over core.Service.
26 unit tests with mock provider and mock reranker, all passing.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Create pkg/config/config.go with Config struct (YAML tags), Path() returning
~/.enowx-rag/config.yaml, Load() reading YAML (error if file missing), Save()
writing YAML with chmod 0600 and mkdir. Implement config priority: env var >
config file > default via Resolve(). Write unit tests for load, save (with
permission check), round-trip, env var override priority, and defaults.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…o indexer (VAL-FND-015)

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…earch (VAL-RAG-001..007,017)

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…L-RAG-008..016)

Add content_tsv tsvector GENERATED ALWAYS AS column and GIN index to
project_memory table in ensureTable(). Implement SemanticSearchHybrid with
Reciprocal Rank Fusion (k=60): FULL OUTER JOIN between dense (cosine) and
lexical (ts_rank) rankings, score = COALESCE(1.0/(60+d.rank),0) +
COALESCE(1.0/(60+l.rank),0). Add HybridSearcher optional interface to
provider.go. Wire into core.Service.Search via retrieveCandidates helper
that uses hybrid path when opts.Hybrid=true and provider implements
HybridSearcher, falling back to dense-only otherwise. Use EmbedQuery for
query embedding in hybrid path. ALTER TABLE migration handles existing
tables without content_tsv. Integration tests with real PostgreSQL verify
tsvector column, GIN index, RRF scoring, EmbedQuery usage, hybrid vs dense
result differences, metadata completeness, and hybrid+rerank end-to-end.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…A serving

- Add --serve and --addr flags to main.go, split into runStdio()/runHTTP()
- Integrate pkg/config into main.go (config.Resolve for env > file > default)
- Create pkg/httpapi/ with chi router, REST handlers (GET /api/projects,
  GET /api/projects/{id}, GET /api/projects/{id}/points, POST /api/projects/{id}/reindex,
  DELETE /api/projects/{id}, POST /api/search, GET /api/stats, GET /api/events SSE),
  SPA fallback handler, and JSON error handling
- Handle 404 for unknown /api/ routes (JSON, not SPA HTML)
- Implement ListProjectIDs in pgvector provider for core.Service.ListProjects
- Fix config.Save() to call os.Chmod(path, 0600) after os.WriteFile
- Replace joinStrings in pkg/core/service.go with strings.Join
- Add JSON tags to rag.Result and rag.PointInfo structs
- Add chunk_version to pgvector ListPoints SELECT
- Write handler tests with httptest (17 test cases)

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
The indexer generates string IDs like 'dir/file.go#chunk0' but the
pgvector table used UUID PRIMARY KEY, causing syntax errors on insert.
Changed ensureTable() DDL to use TEXT PRIMARY KEY, removed ::uuid cast
in test query and id::text cast in ListPoints. Dropped and recreated
existing project_memory tables. Added tests verifying string IDs work
for Index, ListPointIDs, ListPoints, DeletePoints, and SemanticSearch.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
POST /api/setup/test: tests vector store + embedder connectivity,
returns per-component ok/message/latency_ms. Supports pgvector (TCP
dial), qdrant (/healthz), chroma (/api/v1/heartbeat), voyage
embeddings API, and TEI /embed endpoint.

POST /api/setup/apply: saves config to ~/.enowx-rag/config.yaml with
chmod 0600 via config.Save().

GET /api/setup/status: returns {configured: bool} based on whether
config file exists.

Includes 19 handler tests covering status (before/after apply),
apply (0600 permissions, valid YAML, overwrite, missing fields,
invalid JSON), test (qdrant success/fail, pgvector success/fail,
latency, unsupported store, missing fields), and parsePGDSN unit tests.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…vent cross-project data collision

Doc IDs are based on source_dir + filename (e.g. 'config/config.go#chunk0'),
so they collide across projects when using a single-column PK on id alone.
Changed PRIMARY KEY to (project_id, id), updated ON CONFLICT clause, and
added automatic migration for tables with the old single-column PK schema.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…L-API-015)

Add ProjectExists check in the search handler before executing SemanticSearch.
pgvector silently returns 0 rows for non-existent projects, so the handler
was returning HTTP 200 with empty results instead of HTTP 404. The new
ProjectExists method on core.Service checks project existence via
ListProjectIDs (fast path) or ListPoints (fallback). Search errors on
existing projects now correctly return 500 instead of 404.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Scaffold Vite + React + TypeScript + Tailwind + lucide-react SPA.
Create true-black flat design tokens (light/dark via data-theme attr).
Build app shell: sidebar (brand, nav, project list), topbar (breadcrumb,
search, theme toggle), content area. Build Overview page with bento grid:
KPI row (4 cards), retrieval playground (query, Run, toggles, chips,
scored results with highlights), index status, retrieval breakdown,
chunk distribution, recent files, activity log. Create API client
(lib/api.ts) and SSE hook (lib/sse.ts). No gradients, no shadows,
hairline borders, violet accent, Inter/JetBrains Mono fonts.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…..033,035,036)

Add Content and ChunkIndex fields to PointInfo, update ListPoints in
pgvector/qdrant/chroma to return chunk previews and indices. Add DELETE
/api/projects/{id}/points/{pointId} endpoint for individual chunk deletion.

Enhance Playground page with full-page retrieval layout, SSE activity panel
showing realtime events, and loading spinner. Rewrite Chunks page with content
previews, chunk indices, source_file filter, delete buttons, and pagination.
Update api.ts with deletePoint method and extended PointInfo interface.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Static HTML mockup of 6-step onboarding wizard (Welcome, VectorStore,
Embedding, Test, AutoSetup, Done) following true-black flat design.
Matches dashboard.html style with zero gradient, #000000 dark bg,
hairline borders, violet accent, monospace for technical data.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
6-step wizard with stepper state machine: Welcome (env detection),
VectorStore (pgvector/qdrant/chroma cards, local/cloud, mono endpoint),
Embedding (voyage/tei cards, API key reveal, model selector, dim input,
re-index warning), Test (POST /api/setup/test, per-component green/red,
latency, proceed gate), AutoSetup (docker-compose generation, commands,
auto-run with disclaimer, SSE progress), Done (POST /api/setup/apply,
redirect to dashboard, config overwrite confirmation).

First-run detection: App.tsx checks GET /api/setup/status, shows wizard
when config missing. Draft state persisted in localStorage across step
navigation and page navigation. Setup page accessible via sidebar nav
after config exists. True-black flat design matching mockup.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
The wizard already persisted the full DraftConfig to localStorage but not
the step position. When the wizard unmounted and remounted, it always
restarted at step 1 (Welcome) even though form values were preserved.

Add a wizard-step localStorage key that is saved on every step change
and loaded on mount via loadStep(). Both wizard-draft and wizard-step
are cleaned up when the user finishes the wizard.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…9,020)

Add Makefile with targets: web (npm ci && npm run build), build (depends
on web, go build -o enowx-rag), dev-web (npm run dev), dev-api (go run
--serve), mcp (MCP-only build with placeholder dist), test, vet, clean,
placeholder. Update .gitignore for new binary name.

Verified: make web builds SPA to web/dist, make build produces self-contained
binary, binary works from /tmp without web/ on disk, go build works without
web/dist (placeholder), all 6 MCP tools work in stdio mode, env-only config
works without config file.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
… (VAL-CROSS-014..018)

- Update README with --serve/--addr flag docs, REST API endpoint table,
  one-command deploy instructions, comprehensive env var table (all RAG_*
  vars), and dual-mode (stdio + HTTP) documentation.
- Add docker-compose.all-in-one.yml: enowx-rag server + Qdrant with
  optional PostgreSQL/pgvector and TEI services (commented out).
- Add RAG_ADMIN_TOKEN middleware: protects /api/* endpoints with
  Bearer token auth when set, no-op when unset. Uses constant-time
  comparison to prevent timing attacks.
- Update Dockerfile to multi-stage build: Node stage builds SPA, Go
  stage embeds it, runtime stage is minimal Alpine.
- Add auth_test.go with 6 test cases covering all auth scenarios.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
… on SSE events (VAL-CROSS-009..013, VAL-WIZ-028)

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…hcheck, clean mcp-server binary

- Remove binary name (./enowx-rag) from docker-compose command field since
  Dockerfile ENTRYPOINT already provides it. Including both caused Docker to
  pass ./enowx-rag as the first arg, making the container run in stdio MCP
  mode instead of HTTP serve mode.
- Add Authorization header to healthcheck using CMD-SHELL so the healthcheck
  passes when RAG_ADMIN_TOKEN is set (otherwise /api/stats returns 401).
- Add mcp-server/mcp-server to Makefile clean target (built by 'make mcp').

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Two hardening fixes surfaced by a code audit against docs/PLANNING.md:

- core.Service.Search now defensively truncates reranked results to k
  instead of trusting the rerank API to honor top_k. Adds
  TestSearchRerankClampsToK.
- SSE (/api/events) no longer sends a wildcard
  Access-Control-Allow-Origin. The header is only emitted when
  RAG_CORS_ORIGIN is set, so the event stream stays same-origin only by
  default (important when exposed publicly alongside RAG_ADMIN_TOKEN).
  Adds TestSSE_NoCORSByDefault and TestSSE_CORSWhenConfigured, plus a
  README env-var row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implementation spec (PLANNING.md), agent handoff guide (HANDOFF.md), and
the approved true-black dashboard mockup that guided the SPA build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Establishes the baseline required for the project to be adopted and
contributed to:

- LICENSE (Apache-2.0) + NOTICE — the project was previously unlicensed,
  which legally blocks reuse and contribution.
- CONTRIBUTING.md, CODE_OF_CONDUCT.md (Contributor Covenant 2.1),
  SECURITY.md (private vuln reporting + operator hardening notes).
- .github/: bug/feature issue templates, PR template, and a CI workflow
  (GitHub Actions) that runs go vet/build/test and the frontend build on
  every push and PR.
- CHANGELOG.md documenting the v0.1.0 surface.
- README: CI, license, and Go-version badges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the dashboard's mock/hardcoded data with real backend metrics and
remove dead controls, so the UI never shows a number that didn't come from
the backend. Surfaced by a UI audit after a dead "Settings" button was found.

Backend (capability-based, local-first):
- Token counter: VoyageEmbeddingClient/VoyageReranker parse usage.total_tokens
  and expose it via a new rag.TokenCounter interface; providers forward their
  embedder's count. No change to Embed/Rerank signatures. TEI reports 0 honestly.
- core.Metrics: in-memory ring-buffer latency (avg/p50/p95) + query count,
  always on for every backend. Search records latency + QueryComposition.
- GET /api/metrics returns the snapshot with honest capability flags
  (backend name, persistent=false unless the backend implements MetricsStore).
- Compress search option: deterministic near-duplicate dedup (content_hash /
  identical content), no LLM. Wired through SearchOpts + SearchRequest.
- `enowx-rag setup [--run]` CLI subcommand generates the docker-compose and
  optionally runs `docker compose up -d` in the terminal — never over HTTP,
  so there is no remote-command-execution surface on the server.
- Fix: IndexProject now ensures the collection exists first, so first-time
  reindex from the HTTP UI no longer 404s.

Frontend (honest UI):
- Overview: drop all mock results/stats/KPIs; wire chunks, file count, chunk
  distribution, recent files to real listPoints; latency/tokens/retrieval
  breakdown to /api/metrics with honest empty states; Re-index prompts for a
  directory and reports success/failure; Compress toggle now functional.
- Sidebar: remove dead "Settings" button and fabricated project list.
- Topbar: remove non-functional ⌘K search box.
- Onboarding: remove fake setTimeout auto-run simulation (pointed to
  `enowx-rag setup --run`); make Docker/Postgres checks honest.

Tests: token accumulation, metrics ring/percentiles/concurrency, compress
dedup, metrics-recording in Search, and the /api/metrics endpoint.

Deferred (pgvector-only, follow-up): durable metric persistence + real
dense/lexical retrieval composition via RRF projection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up audit found several features silently dead or unreachable
depending on backend/entry-point. Fixes:

- Qdrant & Chroma now implement ListProjectIDs, so /api/projects, /api/stats
  and the whole dashboard work on those backends — previously they returned
  empty unless the backend was pgvector. (Verified live on Qdrant: 10 real
  projects / 14,786 chunks now surface; was 0/0 before.)
- MCP tools (rag_semantic_search, rag_retrieve_context) now accept and pass
  Hybrid/Rerank/Recall/Compress. Previously they hardcoded SearchOpts{K,
  Recall:K}, so the reranker and hybrid RRF were never used from MCP clients
  (the primary interface) and the recall→rerank funnel was collapsed.
  Hybrid/Rerank default to true; recall defaults to 40.
- Security: /api/setup/apply and /api/setup/test now require localhost or a
  valid admin token (LocalOrAdminMiddleware), and /api/setup/apply no longer
  echoes the saved config (which contained the Voyage API key) back in its
  response. Removed the deprecated middleware.RealIP, which rewrote
  RemoteAddr from client X-Forwarded-For and would have let a remote caller
  spoof localhost to bypass the setup gate.
- pgvector SemanticSearchHybrid now projects in_dense/in_lexical, so the
  retrieval-composition breakdown shows real dense/lexical counts instead of
  always zero.
- Onboarding: the deployment=cloud toggle now actually skips local Docker
  setup (previously it only changed a label); StepDone hides the reranker row
  for TEI (no Voyage reranker); StepTest gate condition simplified; Playground
  compress added to the search useCallback deps (toggle took effect one run
  late).
- Removed dead joinStrings; corrected the misleading MetricsStore comment
  (no provider can implement it without an import cycle — durable persistence
  needs a separate injected persister, noted as future work).

Tests: Qdrant/Chroma ListProjectIDs, setup auth (remote rejected without
token, allowed with token, no key leak).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ListProjects (which powers /api/projects, /api/stats, and GetProject)
counted a project's chunks by calling ListPoints, which scrolls every
point WITH its full payload just to take len(). On Qdrant with 9 large
projects this made the dashboard take >10s and time out.

Add an optional ProjectCounter interface (CountPoints) implemented by all
three providers with native counts — Qdrant points/count, pgvector
COUNT(*), Chroma /count. ListProjects prefers it and only falls back to
the ListPoints scroll when a provider doesn't implement it.

Verified live on Qdrant: /api/projects and /api/stats dropped from >10s
(timeout) to ~0.6s, same data (9 projects / 14,770 chunks).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two follow-ups from the backend audit.

Durable metrics persistence:
- Add SQLiteMetricsStore (core), backed by ~/.enowx-rag/metrics.db via the
  pure-Go modernc.org/sqlite driver — no cgo, so the single-static-binary
  goal holds. It works with ANY vector-store backend (metrics live
  independently of the store), not just pgvector.
- Redesign MetricsStore as a dependency injected into Service (SetMetricsStore)
  rather than a provider method. A provider lives in package rag, which cannot
  import core without an import cycle — the old design could never be
  implemented, so Persistent was always false. Now Search persists each query
  asynchronously and MetricsSnapshot reads durable aggregates, so counts and
  latency percentiles survive restarts.
- Wired in main.go; falls back to in-memory if the DB can't be opened.
- Verified live on Qdrant: persistent=true, and query_count/latency survive a
  server restart.

Chroma honesty:
- Documented the Chroma provider as experimental (legacy /api/v1, mock-tested
  only; Chroma >= 0.6 uses /api/v2) in the README provider table, a per-backend
  feature-support matrix, and a doc comment on ChromaProvider. Qdrant and
  pgvector are the supported/tested backends.

Also documented /api/metrics, the compress search field, and the setup-auth
requirement in the README, and updated CHANGELOG.

Tests: SQLite store empty/aggregate/durability-across-reopen, and Service
persisting to an injected store with Persistent=true.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the plain CSS letter "e" brand mark and the browser-default favicon
with a proper icon: a stylized "e" monogram wired to two retrieval nodes,
symbolizing per-project RAG memory + retrieval.

Monochrome and theme-adaptive: black mark on light UI, white mark on dark UI.
- Favicon swaps via <link media="(prefers-color-scheme)"> in index.html.
- Sidebar and wizard brand marks swap via data-theme CSS (with a
  prefers-color-scheme fallback when no theme is stamped).
- Icons live in web/public and are embedded into the single binary through
  the existing dist embed.FS. Sizes: 32 (favicon), 192 (apple-touch), 512.

Generated from a flat solid-color source, background removed to transparency,
then recolored to near-black/near-white keeping antialiased edges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
enowdev and others added 19 commits July 13, 2026 15:40
…llbar

The dashboard scrolled the whole window when results were long. Lock the
shell to one screen (app/main = 100vh, overflow hidden) and let content and
the Playground result/activity panels scroll internally instead. Query box
and toolbar stay pinned above the scrolling results.

Also style the scrollbar to match the theme (thin pill using --border-strong)
so it's no longer a glaring white bar in dark mode, and fix the Playground
compress toggle being one run stale (added to the search useCallback deps).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a wizard "Install" step (6/7) that connects enowx-rag to the user's AI
tool, so onboarding ends with a working install rather than just a saved
config.yaml.

Backend (pkg/httpapi):
- mcpclients.go: registry of 8 clients (Claude Code, Claude Desktop, Cursor,
  Cline, Windsurf, Codex, Zed, Continue) with per-format serializers. Three
  format groups handled with merge-not-replace: JSON mcpServers (+ Cursor
  type:stdio, Cline flags), Zed context_servers with args, Codex TOML section,
  Continue YAML list (replace-by-name). Existing servers and top-level keys are
  preserved; the original file is backed up to .bak before writing.
- setup_install.go: mcpServerEntry() builds command (os.Executable) + env from
  the saved config. Endpoints: POST /setup/install-mcp (writes/merges, gated by
  LocalOrAdminMiddleware), GET /setup/mcp-snippet (manual paste), GET
  /setup/clients, GET /setup/skill-guide (manual skill instructions — skills
  are only auto-installed by some clients, so we show commands instead).

Frontend:
- StepInstall.tsx: pick a client, install automatically or view a copy-paste
  snippet ("Other" -> snippet only), global/project scope, plus a manual skill
  install section. Wired into the wizard; step badges updated to /7.
- api.ts: mcpClients/installMcp/mcpSnippet/skillGuide.

Verified live: snippet correct per format; install merges into an existing
Cursor config (existing server + top-level keys preserved), writes type:stdio,
and creates a .bak. Tests cover every serializer, merge/backup, and the
loopback gate on install-mcp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cker

The "Auto Setup" step was misleading: it didn't auto-anything and it listed a
docker-compose even when nothing needed to run locally (e.g. a hosted Qdrant +
the Voyage API). Rework it:

- Add localServices(cfg): a component needs Docker only if it's self-hosted at
  a localhost URL. A cloud API embedder (Voyage) and a remote vector store are
  omitted. generateDockerCompose/generateCommands now emit only those services.
- StepAutoSetup: when localServices is empty, the step says "nothing to run
  locally" and can be skipped; otherwise it names the exact services and shows
  the compose + commands to run manually.
- Rename the step "Auto Setup" -> "Local Backend" and clarify the copy.
- Remove the now-dead "Deployment" (Local/Cloud) toggle and its DeploymentMode
  field — nothing read it; the localhost-URL check is the real signal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a third embedder alongside Voyage and TEI: RAG_EMBEDDER=openai, targeting
any OpenAI-compatible /v1/embeddings API. One provider covers OpenAI, Together,
Jina, Mistral, DeepInfra, LiteLLM, a local Ollama, etc. — set base URL + model,
API key optional (empty for local/no-auth endpoints).

- pkg/rag/openai.go: OpenAIEmbeddingClient (EmbeddingClient + ModelNamer +
  TokenCounter). URL normalization (root / /v1 / full path), optional
  "dimensions", token accounting, and VectorSize auto-probe when dim is unknown.
- config + main.go + RuntimeConfig: OpenAIConfig and RAG_OPENAI_* env vars,
  wired into buildProvider and the install-mcp env map.
- setup.go: /api/setup/test supports the openai embedder (reuses the provider,
  so URL handling matches production) and reports the detected dimension.
- Wizard: third "OpenAI-compatible" card (base URL / key / model / dim) and the
  TEI card now explains it serves any local model (BGE, GTE, E5, nomic…).
- README + CHANGELOG documented.

Verified live: /api/setup/test against a mock OpenAI-compatible server connects
and auto-detects the dimension. Tests cover URL normalization, request shape
(dimensions sent/omitted), token counting, dimension probe, and no-auth mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backend for migrating a project into enowx-rag, changing embedding
dimension/model, or moving between vector stores. Raw vectors are
model-specific and non-portable, so migration re-embeds from the stored text.

- rag.Exporter interface + ExportPoints on qdrant/chroma/pgvector: return every
  point with FULL content and metadata (ListPoints truncates to 200 chars for
  previews; export must not). Preserves doc_id so identity survives migration.
  core.Service.ExportProject surfaces it.
- pkg/ragbuild: build a provider+embedder from an explicit Spec (no env), so a
  *destination* provider can differ in store/model/dimension/pgvector-table.
  main.go's buildProvider now delegates here (no duplicate logic).
- pkg/migrate.Migrator{Src,Dst}: export from source -> CreateCollection on dest
  -> Index in batches (re-embedded by the destination embedder), with throttled
  progress callbacks.
- POST /api/migrate: builds the destination from the request spec, runs the
  migration asynchronously (202), streams migration_started/progress/completed/
  failed over SSE (throttled to integer-percent changes so the bounded event
  bus isn't flooded). Loopback/admin-token gated.

Verified live on Qdrant: migsrc (17 chunks) -> migdst re-embedded via Voyage;
SSE progressed 0->100%; dest has 17 chunks and is searchable. Tests cover
export (full content, doc_id preserved) on qdrant/chroma, the Migrator
(batching, collection creation, metadata preserved, non-exporter error), and
the endpoint gate + validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "Migration" page to the dashboard (nav + route + SSE wiring) that drives
the migration engine: pick a source project, choose a destination store +
embedder (+ connection/model/dimension fields), name the destination, and run.

- Live progress bar fed by the migration_* SSE events (registered in sse.ts),
  showing documents done/total and percent.
- On success: prompt to verify, then an explicit "Delete source project" button
  (with confirm) — the source is never auto-removed.
- pgvector destination shows a hint that a NEW table name is required to change
  dimension (single fixed-dimension table).
- App.tsx/Sidebar/Topbar wired for the new 'migration' page; api.ts gains
  migrate() + MigrateRequest/MigrateResponse.

Verified live via the Vite proxy (the page's exact network path): uimig ->
uimig-voyage-4 re-embedded, 17 -> 17 chunks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…perimental)

Extend migration to pull from external cloud vector databases and re-embed into
enowx-rag. Migrator.Src is now a rag.Exporter, so any source — a local provider
or a cloud connector — flows through the same re-embed/write path.

- pkg/migrate/cloud: CloudSource + connectors.
  - Qdrant Cloud reuses the tested rag.QdrantProvider against a remote URL —
    VERIFIED live (imported a Qdrant collection → re-embedded → 17→17 chunks).
  - Pinecone (list+fetch), Weaviate (GraphQL), Chroma Cloud (/get) read text
    from metadata. EXPERIMENTAL: built from vendor docs, mock-tested only, not
    verified against live accounts — flagged in code, UI, README, CHANGELOG
    (same honesty policy as the Chroma provider).
- POST /api/migrate accepts an optional cloud_source; the source becomes the
  connector instead of the running provider.
- Migration page: source-mode toggle (existing project / import from cloud) with
  per-vendor fields and a clear experimental banner for unverified vendors.
- README "Migration" section + CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Let an AI agent set up enowx-rag for a project from one short prompt, doing only
what's missing.

- GET /api/docs/setup: markdown the agent reads — probe, then install the MCP
  server, skill, and AGENTS.md block, skipping anything already present.
- GET /api/setup/probe?client=&dir=: reports current state — whether the
  enowx-rag MCP server is in each client's config, whether the skill is
  installed, and whether the project's AGENTS.md already has the enowx-rag block.
- POST /api/setup/write-agents-md {dir, project_id}: merges an enowx-rag section
  into AGENTS.md via <!-- enowx-rag:start/end --> markers — create if missing,
  append (preserving the user's content) if unmarked, or update in place if the
  markers exist. Idempotent. Loopback/admin-token gated.
- Install step shows a copy-paste prompt that points the agent at the docs URL.

Verified live: probe correctly reported claude-code MCP + skill installed and
AGENTS.md present-without-block on this machine; write appended while preserving
user content, then re-running updated in place (single block, no duplication).
Tests cover create/append/update merge, content preservation, and probe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "Docs" page to the dashboard so the agent-setup instructions are readable
by humans, not just fetchable by agents. It shows the copy-paste agent prompt at
the top and renders the /api/docs/setup markdown below (headings, paragraphs,
lists, inline code, endpoint blocks) with a tiny inline renderer — no markdown
dependency added. Wired into nav/route/topbar as the 'docs' page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expand Docs from the single agent-setup page into a full, sectioned reference
served from the backend so both the dashboard and agents read one source.

Backend (pkg/httpapi/docs.go):
- Section registry with GET /api/docs (table of contents) and
  GET /api/docs/{section} (markdown). /api/docs/setup kept as an alias for the
  agent-setup section. Sections: Overview, Quick start, MCP tools, API
  reference, Embedders, Vector stores, Search (hybrid/rerank/compress),
  Migration, Metrics, Agent setup — each documenting the real, shipped behavior.

Frontend (Docs page):
- Left section nav + content pane; fetches the list and each section's markdown.
- Minimal inline markdown renderer extended to handle tables and code blocks (no
  markdown dependency). The agent-setup section keeps the copy-paste prompt.

Tests cover the docs list, a section, the setup alias, and 404. Verified live:
/api/docs returns all 10 sections.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose the MCP server over HTTP so agents can use enowx-rag as a centralized
remote daemon (e.g. on a VPS) instead of a local stdio process — the user's
"daemon on a VPS, accessed by API key" use case.

- Factor server construction into newMCPServer(svc), shared by stdio and HTTP.
- runHTTP mounts a StreamableHTTPHandler (stateless) at /mcp using the SDK's
  NewStreamableHTTPHandler (go-sdk v0.4.0, no new dependency). One server
  instance is shared across all sessions.
- NewRouter gains an mcpHandler param; /mcp is mounted before the SPA catch-all
  in a group gated by the existing AdminTokenMiddleware, so RAG_ADMIN_TOKEN
  secures /mcp exactly like /api (open when unset — local only).
- Docs "Remote / daemon" section + README + CHANGELOG: how to run the daemon,
  secure it, and connect a client via { url, headers: { Authorization } }.

Local stdio mode is unchanged (default, no --serve).

Verified live: with RAG_ADMIN_TOKEN set, POST /mcp without a bearer → 401; with
the bearer, the MCP `initialize` handshake returns capabilities and `tools/list`
returns all six RAG tools. Tests cover the /mcp gate (401 / pass-through / open).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After adding MCP-over-HTTP, the setup flow only produced local stdio config.
Now install-mcp and mcp-snippet can produce a remote entry pointing at a daemon.

- mcpEntry gains RemoteURL + Token; all serializers (JSON mcpServers /
  context_servers, Codex TOML, Continue YAML list) emit a { url, headers:
  { Authorization: Bearer <token> } } entry when remote, instead of command+env.
- install-mcp accepts mode/remote_url/token; mcp-snippet accepts
  ?mode=remote&remote_url=&token= (with a placeholder URL if none given).
- StepInstall UI: a Local (stdio) / Remote daemon toggle with URL + token fields;
  applies to both auto-install and the manual snippet.
- Agent-setup docs document the remote option and point to the Remote / daemon
  section.

Verified live: remote snippet (JSON + TOML) yields url + Authorization header,
and a remote install writes a { url, headers } Cursor config. Tests cover the
remote form across JSON/TOML/YAML.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose more of core.Service to agents. Adds (6 -> 11 tools):
- rag_list_projects: list projects + chunk counts (discover available memory)
- rag_project_exists: check a project has indexed data
- rag_list_points: list chunks (id, source_file, preview), optional file filter
- rag_delete_points: delete specific chunks by ID (no full re-index)
- rag_stats: projects, total chunks, embed model, latency, token usage

All are thin wrappers over existing Service methods and work over both stdio and
remote HTTP. Docs "MCP tools" section, README, and CHANGELOG updated.

Verified live via /mcp: tools/list returns all 11; rag_list_projects returns the
9 real projects with chunk counts.

Note: setup/config (apply config, install MCP) is intentionally NOT an MCP tool —
that's the HTTP API + wizard's job; an agent talking over MCP is already
installed. MCP tools stay scoped to memory operations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a Settings page (and endpoints) to view and change secrets without editing
config.yaml or env by hand.

- GET /api/setup/config: current config with secrets masked (pa-a••••viD).
- GET /api/setup/config/reveal: full secrets — gated by LocalOrAdminMiddleware
  (localhost or a valid admin token) so it isn't open on an exposed daemon.
- POST /api/setup/config: partial update; only provided keys overwrite, merged
  into ~/.enowx-rag/config.yaml (0600).
- POST /api/setup/gen-token: generate a strong random admin token, save it to
  config, and return it once to copy.
- config.AdminToken + EffectiveAdminToken(): the admin token now comes from
  RAG_ADMIN_TOKEN (precedence) OR config.yaml. AdminTokenMiddleware /
  LocalOrAdminMiddleware read it per-request, so a generated token gates
  requests immediately without a restart.
- Settings UI: masked keys with reveal, per-key update, and token generation
  with an env-precedence warning.

Verified live: config returns a masked Voyage key; reveal (localhost) returns
the full key. Tests cover masking and that a generated token then gates a remote
request (401 without it, 200 with it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Settings page listed every provider key (Voyage, OpenAI, Qdrant) even when
unused, which was confusing — e.g. an OpenAI key field with a Voyage setup.
Now it shows only the keys relevant to the active config: the embedder's key
(Voyage / OpenAI-compatible, or a "no key needed" note for TEI) and the vector
store's key (Qdrant). A hint states the active embedder + vector store and
points to the Setup wizard to change them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Auth middleware now reads config.EffectiveAdminToken(), which loads
~/.enowx-rag/config.yaml. That made httpapi tests non-deterministic: a
developer's saved admin_token (or a RAG_ADMIN_TOKEN in the environment)
would turn expected-200 responses into 401s, and host config could leak
into setup/config assertions. This is a release/CI prerequisite.

- newTestServer points HOME at an empty temp dir and clears
  RAG_ADMIN_TOKEN, but skips the HOME override when a test has already
  isolated it (HOME != realHome captured at init), so config-writing
  tests keep their own dir.
- Tests that exercise auth set RAG_ADMIN_TOKEN *after* newTestServer
  (read per-request, so it still takes effect).
- Direct NewRouter callers that need host isolation (TestSearch_
  BadProject_NoLister, TestMCPMount_OpenWhenNoToken) isolate HOME.

Verified deterministic: suite passes uncached with both a host
config admin_token present AND RAG_ADMIN_TOKEN=hostile-leak-token in
the env, and on a clean empty HOME. go vet clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The only way to install was clone + `make build` (needs Go + Node). Add
simple, toolchain-free install paths for v0.1.0.

The binary is CGO-free (pure-Go modernc.org/sqlite, no `import "C"`), so
it cross-compiles to all targets; web/dist is committed, so `go install`
embeds the real dashboard without npm. Verified builds for linux, darwin
(amd64/arm64) and windows/amd64.

- .goreleaser.yaml: cross-compile matrix, -trimpath, version via ldflags,
  tar.gz/zip archives + checksums, Homebrew tap (brews:).
- .github/workflows/release.yml: on tag v*, run GoReleaser then publish
  the npm wrapper.
- install.sh: curl|sh — detect OS/arch, download the matching release
  archive, verify checksum, install to /usr/local/bin (or ~/.local/bin).
- npm/: thin wrapper (postinstall downloads the native binary; bin shim
  execs it and forwards args/exit). No runtime deps.
- main.go: `enowx-rag version`/`--version`; version stamped via
  -X main.version, with a runtime/debug build-info fallback for
  `go install ...@vX.Y.Z`.
- README Install section; CHANGELOG; gitignore dist/ + npm/bin.

Archive naming verified identical across GoReleaser, install.sh, and the
npm wrapper. Full Go suite green; go vet clean. GoReleaser config will be
exercised by CI on the release tag (couldn't build locally: disk full).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TestSetupTest_PGVectorSuccess dialed a real local PostgreSQL
("per the mission setup"), so it passed on the dev machine but failed
in CI (connection refused, no DB there). The pgvector check only dials
TCP (setup.go: "we don't import pgx here"), so a bare net.Listen stand-in
proves reachability without any database — deterministic in CI.

Verified: full suite green with an empty HOME and no Postgres running.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a hero Overview screenshot, a "Why enowx-rag?" benefits section, and
a 6-shot dashboard preview gallery (Overview, Playground, Chunks,
Migration, Docs, Settings) to the README. Screenshots are captured from
a live instance with real data (16k+ chunks across 9 projects, real
retrieval results with scores/snippets), not mockups.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@enowdev
enowdev merged commit 7d5a436 into main Jul 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant