docs: add ecosystem overview and historical design plans
Moved from core/go during docs cleanup — these belong with the CLI that orchestrates the ecosystem, not the DI framework. - ecosystem.md: full module inventory and dependency graph - 3 active plans (authentik-traefik, core-help design/plan) - 13 completed design plans (MCP, go-api, cli-meta, go-forge, etc.) Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
37a8ae8d31
commit
aa83cf77cc
17 changed files with 10422 additions and 0 deletions
457
docs/ecosystem.md
Normal file
457
docs/ecosystem.md
Normal file
|
|
@ -0,0 +1,457 @@
|
|||
# Core Go Ecosystem
|
||||
|
||||
The Core Go ecosystem is a set of 19 standalone Go modules that form the infrastructure backbone for the host-uk platform and the Lethean network. All modules are hosted under the `forge.lthn.ai/core/` organisation. Each module has its own repository, independent versioning, and a `docs/` directory.
|
||||
|
||||
The CLI framework documented in the rest of this site (`forge.lthn.ai/core/cli`) is one node in this graph. The satellite packages listed here are separate repositories that the CLI imports or that stand alone as libraries.
|
||||
|
||||
---
|
||||
|
||||
## Module Index
|
||||
|
||||
| Package | Module Path | Managed By |
|
||||
|---------|-------------|-----------|
|
||||
| [go-inference](#go-inference) | `forge.lthn.ai/core/go-inference` | Virgil |
|
||||
| [go-mlx](#go-mlx) | `forge.lthn.ai/core/go-mlx` | Virgil |
|
||||
| [go-rocm](#go-rocm) | `forge.lthn.ai/core/go-rocm` | Charon |
|
||||
| [go-ml](#go-ml) | `forge.lthn.ai/core/go-ml` | Virgil |
|
||||
| [go-ai](#go-ai) | `forge.lthn.ai/core/go-ai` | Virgil |
|
||||
| [go-agentic](#go-agentic) | `forge.lthn.ai/core/go-agentic` | Charon |
|
||||
| [go-rag](#go-rag) | `forge.lthn.ai/core/go-rag` | Charon |
|
||||
| [go-i18n](#go-i18n) | `forge.lthn.ai/core/go-i18n` | Virgil |
|
||||
| [go-html](#go-html) | `forge.lthn.ai/core/go-html` | Charon |
|
||||
| [go-crypt](#go-crypt) | `forge.lthn.ai/core/go-crypt` | Virgil |
|
||||
| [go-scm](#go-scm) | `forge.lthn.ai/core/go-scm` | Charon |
|
||||
| [go-p2p](#go-p2p) | `forge.lthn.ai/core/go-p2p` | Charon |
|
||||
| [go-devops](#go-devops) | `forge.lthn.ai/core/go-devops` | Virgil |
|
||||
| [go-help](#go-help) | `forge.lthn.ai/core/go-help` | Charon |
|
||||
| [go-ratelimit](#go-ratelimit) | `forge.lthn.ai/core/go-ratelimit` | Charon |
|
||||
| [go-session](#go-session) | `forge.lthn.ai/core/go-session` | Charon |
|
||||
| [go-store](#go-store) | `forge.lthn.ai/core/go-store` | Charon |
|
||||
| [go-ws](#go-ws) | `forge.lthn.ai/core/go-ws` | Charon |
|
||||
| [go-webview](#go-webview) | `forge.lthn.ai/core/go-webview` | Charon |
|
||||
|
||||
---
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
The graph below shows import relationships. An arrow `A → B` means A imports B.
|
||||
|
||||
```
|
||||
go-inference (no dependencies — foundation contract)
|
||||
↑
|
||||
├── go-mlx (CGO, Apple Silicon Metal GPU)
|
||||
├── go-rocm (AMD ROCm, llama-server subprocess)
|
||||
└── go-ml (scoring engine, backends, orchestrator)
|
||||
↑
|
||||
└── go-ai (MCP hub, 49 tools)
|
||||
↑
|
||||
└── go-agentic (service lifecycle, allowances)
|
||||
|
||||
go-rag (Qdrant + Ollama, standalone)
|
||||
↑
|
||||
└── go-ai
|
||||
|
||||
go-i18n (grammar engine, standalone; Phase 2a imports go-mlx)
|
||||
|
||||
go-crypt (standalone)
|
||||
↑
|
||||
├── go-p2p (UEPS wire protocol)
|
||||
└── go-scm (AgentCI dispatch)
|
||||
|
||||
go-store (SQLite KV, standalone)
|
||||
↑
|
||||
├── go-ratelimit (sliding window limiter)
|
||||
├── go-session (transcript parser)
|
||||
└── go-agentic
|
||||
|
||||
go-ws (WebSocket hub, standalone)
|
||||
↑
|
||||
└── go-ai
|
||||
|
||||
go-webview (CDP client, standalone)
|
||||
↑
|
||||
└── go-ai
|
||||
|
||||
go-html (DOM compositor, standalone)
|
||||
|
||||
go-help (help catalogue, standalone)
|
||||
|
||||
go-devops (Ansible, build, infrastructure — imports go-scm)
|
||||
```
|
||||
|
||||
The CLI framework (`forge.lthn.ai/core/cli`) has internal equivalents of several of these packages (`pkg/rag`, `pkg/ws`, `pkg/webview`, `pkg/i18n`) that were developed in parallel. The satellite packages are the canonical standalone versions intended for use outside the CLI binary.
|
||||
|
||||
---
|
||||
|
||||
## Package Descriptions
|
||||
|
||||
### go-inference
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-inference`
|
||||
|
||||
Zero-dependency interface package that defines the common contract for all inference backends in the ecosystem:
|
||||
|
||||
- `TextModel` — the top-level model interface (`Generate`, `Stream`, `Close`)
|
||||
- `Backend` — hardware/runtime abstraction (Metal, ROCm, CPU, remote)
|
||||
- `Token` — streaming token type with metadata
|
||||
|
||||
No concrete implementations live here. Any package that needs to call inference without depending on a specific hardware library imports `go-inference` and receives an implementation at runtime.
|
||||
|
||||
---
|
||||
|
||||
### go-mlx
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-mlx`
|
||||
|
||||
Native Metal GPU inference for Apple Silicon using CGO bindings to `mlx-c` (the C API for Apple's MLX framework). Implements the `go-inference` interfaces.
|
||||
|
||||
Build requirements:
|
||||
- macOS 13+ (Ventura) on Apple Silicon
|
||||
- `mlx-c` installed (`brew install mlx`)
|
||||
- CGO enabled: `CGO_CFLAGS` and `CGO_LDFLAGS` must reference the mlx-c headers and library
|
||||
|
||||
Features:
|
||||
- Loads GGUF and MLX-format models
|
||||
- Streaming token generation directly on GPU
|
||||
- Quantised model support (Q4, Q8)
|
||||
- Phase 4 backend abstraction in progress — will allow hot-swapping backends at runtime
|
||||
|
||||
Local path: `/Users/snider/Code/go-mlx`
|
||||
|
||||
---
|
||||
|
||||
### go-rocm
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-rocm`
|
||||
|
||||
AMD ROCm GPU inference for Linux. Rather than using CGO, this package manages a `llama-server` subprocess (from llama.cpp) compiled with ROCm support and communicates over its HTTP API.
|
||||
|
||||
Features:
|
||||
- Subprocess lifecycle management (start, health-check, restart on crash)
|
||||
- OpenAI-compatible HTTP client wrapping llama-server's API
|
||||
- Implements `go-inference` interfaces
|
||||
- Targeted at the homelab RX 7800 XT running Ubuntu 24.04
|
||||
|
||||
Managed by Charon (Linux homelab).
|
||||
|
||||
---
|
||||
|
||||
### go-ml
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-ml`
|
||||
|
||||
Scoring engine, backend registry, and agent orchestration layer. The hub that connects models from `go-mlx`, `go-rocm`, and future backends into a unified interface.
|
||||
|
||||
Features:
|
||||
- Backend registry: register multiple inference backends, select by capability
|
||||
- Scoring pipeline: evaluate model outputs against rubrics
|
||||
- Agent orchestrator: coordinate multi-step inference tasks
|
||||
- ~3.5K LOC
|
||||
|
||||
---
|
||||
|
||||
### go-ai
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-ai`
|
||||
|
||||
MCP (Model Context Protocol) server hub with 49 registered tools. Acts as the primary facade for AI capabilities in the ecosystem.
|
||||
|
||||
Features:
|
||||
- 49 MCP tools covering file operations, RAG, metrics, process management, WebSocket, and CDP/webview
|
||||
- Imports `go-ml`, `go-rag`, `go-mlx`
|
||||
- Can run as stdio MCP server or TCP MCP server
|
||||
- AI usage metrics recorded to JSONL
|
||||
|
||||
Run the MCP server:
|
||||
|
||||
```bash
|
||||
# stdio (for Claude Desktop / Claude Code)
|
||||
core mcp serve
|
||||
|
||||
# TCP
|
||||
MCP_ADDR=:9000 core mcp serve
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### go-agentic
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-agentic`
|
||||
|
||||
Service lifecycle and allowance management for autonomous agents. Handles:
|
||||
|
||||
- Agent session tracking and state persistence
|
||||
- Allowance system: budget constraints on tool calls, token usage, and wall-clock time
|
||||
- Integration with `go-store` for persistence
|
||||
- REST client for the PHP `core-agentic` backend
|
||||
|
||||
Managed by Charon.
|
||||
|
||||
---
|
||||
|
||||
### go-rag
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-rag`
|
||||
|
||||
Retrieval-Augmented Generation pipeline using Qdrant for vector storage and Ollama for embeddings.
|
||||
|
||||
Features:
|
||||
- `ChunkMarkdown`: semantic splitting by H2 headers and paragraphs with overlap
|
||||
- `Ingest`: crawl a directory of Markdown files, embed, and store in Qdrant
|
||||
- `Query`: semantic search returning ranked `QueryResult` slices
|
||||
- `FormatResultsContext`: formats results as XML tags for LLM prompt injection
|
||||
- Clients: `QdrantClient` and `OllamaClient` wrapping their respective Go SDKs
|
||||
|
||||
Managed by Charon.
|
||||
|
||||
---
|
||||
|
||||
### go-i18n
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-i18n`
|
||||
|
||||
Grammar engine for natural-language generation. Goes beyond key-value lookup tables to handle pluralisation, verb conjugation, past tense, gerunds, and semantic sentence construction ("Subject verbed object").
|
||||
|
||||
Features:
|
||||
- `T(key, args...)` — main translation function
|
||||
- `S(noun, value)` — semantic subject with grammatical context
|
||||
- Language rules defined in JSON; algorithmic fallbacks for irregular verbs
|
||||
- **GrammarImprint**: a linguistic hash (reversal of the grammar engine) used as a semantic fingerprint — part of the Lethean identity verification stack
|
||||
- Phase 2a (imports `go-mlx` for language model-assisted reversal) currently blocked on `go-mlx` Phase 4
|
||||
|
||||
Local path: `/Users/snider/Code/go-i18n`
|
||||
|
||||
---
|
||||
|
||||
### go-html
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-html`
|
||||
|
||||
HLCRF DOM compositor — a programmatic HTML/DOM construction library targeting both server-side rendering and WASM (browser).
|
||||
|
||||
HLCRF stands for Header, Left, Content, Right, Footer — the region layout model used throughout the CLI's terminal UI and web rendering layer.
|
||||
|
||||
Features:
|
||||
- Composable region-based layout (mirrors the terminal `Composite` in `pkg/cli`)
|
||||
- WASM build target: runs in the browser without JavaScript
|
||||
- Used by the LEM Chat UI and web SDK generation
|
||||
|
||||
Managed by Charon.
|
||||
|
||||
---
|
||||
|
||||
### go-crypt
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-crypt`
|
||||
|
||||
Cryptographic primitives, authentication, and trust policy enforcement.
|
||||
|
||||
Features:
|
||||
- Password hashing (Argon2id with tuned parameters)
|
||||
- Symmetric encryption (ChaCha20-Poly1305, AES-GCM)
|
||||
- Key derivation (HKDF, Scrypt)
|
||||
- OpenPGP challenge-response authentication
|
||||
- Trust policies: define and evaluate access rules
|
||||
- Foundation for the UEPS (User-controlled Encryption Policy System) wire protocol in `go-p2p`
|
||||
|
||||
---
|
||||
|
||||
### go-scm
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-scm`
|
||||
|
||||
Source control management and CI integration, including the AgentCI dispatch system.
|
||||
|
||||
Features:
|
||||
- Forgejo and Gitea API clients (typed wrappers)
|
||||
- GitHub integration via the `gh` CLI
|
||||
- `AgentCI`: dispatches AI work items to agent runners over SSH using Charm stack libraries (`soft-serve`, `keygen`, `melt`, `wishlist`)
|
||||
- PR lifecycle management: create, review, merge, label
|
||||
- JSONL job journal for audit trails
|
||||
|
||||
Managed by Charon.
|
||||
|
||||
---
|
||||
|
||||
### go-p2p
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-p2p`
|
||||
|
||||
Peer-to-peer mesh networking implementing the UEPS (User-controlled Encryption Policy System) wire protocol.
|
||||
|
||||
Features:
|
||||
- UEPS: consent-gated TLV frames with Ed25519 consent tokens and an Intent-Broker
|
||||
- Peer discovery and mesh routing
|
||||
- Encrypted relay transport
|
||||
- Integration with `go-crypt` for all cryptographic operations
|
||||
|
||||
This is a core component of the Lethean Web3 network layer.
|
||||
|
||||
Managed by Charon (Linux homelab).
|
||||
|
||||
---
|
||||
|
||||
### go-devops
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-devops`
|
||||
|
||||
Infrastructure automation, build tooling, and release pipeline utilities, intended as a standalone library form of what the Core CLI provides as commands.
|
||||
|
||||
Features:
|
||||
- Ansible-lite engine (native Go SSH playbook execution)
|
||||
- LinuxKit image building and VM lifecycle
|
||||
- Multi-target binary build and release
|
||||
- Integration with `go-scm` for repository operations
|
||||
|
||||
---
|
||||
|
||||
### go-help
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-help`
|
||||
|
||||
Embedded documentation catalogue with full-text search and an optional HTTP server for serving help content.
|
||||
|
||||
Features:
|
||||
- YAML-frontmatter Markdown topic parsing
|
||||
- In-memory reverse index with title/heading/body scoring
|
||||
- Snippet extraction with keyword highlighting
|
||||
- `HTTP server` mode: serve the catalogue as a documentation site
|
||||
- Used by the `core pkg search` command and the `pkg/help` package inside the CLI
|
||||
|
||||
Managed by Charon.
|
||||
|
||||
---
|
||||
|
||||
### go-ratelimit
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-ratelimit`
|
||||
|
||||
Sliding-window rate limiter with a SQLite persistence backend.
|
||||
|
||||
Features:
|
||||
- Token bucket and sliding-window algorithms
|
||||
- SQLite backend via `go-store` for durable rate state across restarts
|
||||
- HTTP middleware helper
|
||||
- Used by `go-ai` and `go-agentic` to enforce per-agent API quotas
|
||||
|
||||
Managed by Charon.
|
||||
|
||||
---
|
||||
|
||||
### go-session
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-session`
|
||||
|
||||
Claude Code JSONL transcript parser and visualisation toolkit (standalone version of `pkg/session` inside the CLI).
|
||||
|
||||
Features:
|
||||
- `ParseTranscript(path)`: reads `.jsonl` session files and reconstructs tool use timelines
|
||||
- `ListSessions(dir)`: scans a Claude projects directory for session files
|
||||
- `Search(dir, query)`: full-text search across sessions
|
||||
- `RenderHTML(sess, path)`: single-file HTML visualisation
|
||||
- `RenderMP4(sess, path)`: terminal video replay via VHS
|
||||
|
||||
Managed by Charon.
|
||||
|
||||
---
|
||||
|
||||
### go-store
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-store`
|
||||
|
||||
SQLite-backed key-value store with reactive change notification.
|
||||
|
||||
Features:
|
||||
- `Get`, `Set`, `Delete`, `List` over typed keys
|
||||
- `Watch(key, handler)`: register a callback that fires on change
|
||||
- `OnChange(handler)`: subscribe to all changes
|
||||
- Used by `go-ratelimit`, `go-session`, and `go-agentic` for lightweight persistence
|
||||
|
||||
Managed by Charon.
|
||||
|
||||
---
|
||||
|
||||
### go-ws
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-ws`
|
||||
|
||||
WebSocket hub with channel-based subscriptions and an optional Redis pub/sub bridge for multi-instance deployments.
|
||||
|
||||
Features:
|
||||
- Hub pattern: central registry of connected clients
|
||||
- Channel routing: `SendToChannel(topic, msg)` delivers only to subscribers
|
||||
- Redis bridge: publish messages from one instance, receive on all
|
||||
- HTTP handler: `hub.Handler()` for embedding in any Go HTTP server
|
||||
- `SendProcessOutput(id, line)`: convenience method for streaming process logs
|
||||
|
||||
Managed by Charon.
|
||||
|
||||
---
|
||||
|
||||
### go-webview
|
||||
|
||||
**Module:** `forge.lthn.ai/core/go-webview`
|
||||
|
||||
Chrome DevTools Protocol (CDP) client for browser automation, testing, and AI-driven web interaction (standalone version of `pkg/webview` inside the CLI).
|
||||
|
||||
Features:
|
||||
- Navigation, click, type, screenshot
|
||||
- `Evaluate(script)`: arbitrary JavaScript execution with result capture
|
||||
- Console capture and filtering
|
||||
- Angular-aware helpers: `WaitForAngular()`, `GetNgModel(selector)`
|
||||
- `ActionSequence`: chain interactions into a single call
|
||||
- Used by `go-ai` to expose browser tools to MCP agents
|
||||
|
||||
Managed by Charon.
|
||||
|
||||
---
|
||||
|
||||
## Forge Repository Paths
|
||||
|
||||
All repositories are hosted at `forge.lthn.ai` (Forgejo). SSH access uses port 2223:
|
||||
|
||||
```
|
||||
ssh://git@forge.lthn.ai:2223/core/go-inference.git
|
||||
ssh://git@forge.lthn.ai:2223/core/go-mlx.git
|
||||
ssh://git@forge.lthn.ai:2223/core/go-rocm.git
|
||||
ssh://git@forge.lthn.ai:2223/core/go-ml.git
|
||||
ssh://git@forge.lthn.ai:2223/core/go-ai.git
|
||||
ssh://git@forge.lthn.ai:2223/core/go-agentic.git
|
||||
ssh://git@forge.lthn.ai:2223/core/go-rag.git
|
||||
ssh://git@forge.lthn.ai:2223/core/go-i18n.git
|
||||
ssh://git@forge.lthn.ai:2223/core/go-html.git
|
||||
ssh://git@forge.lthn.ai:2223/core/go-crypt.git
|
||||
ssh://git@forge.lthn.ai:2223/core/go-scm.git
|
||||
ssh://git@forge.lthn.ai:2223/core/go-p2p.git
|
||||
ssh://git@forge.lthn.ai:2223/core/go-devops.git
|
||||
ssh://git@forge.lthn.ai:2223/core/go-help.git
|
||||
ssh://git@forge.lthn.ai:2223/core/go-ratelimit.git
|
||||
ssh://git@forge.lthn.ai:2223/core/go-session.git
|
||||
ssh://git@forge.lthn.ai:2223/core/go-store.git
|
||||
ssh://git@forge.lthn.ai:2223/core/go-ws.git
|
||||
ssh://git@forge.lthn.ai:2223/core/go-webview.git
|
||||
```
|
||||
|
||||
HTTPS authentication is not available on Forge. Always use SSH remotes.
|
||||
|
||||
---
|
||||
|
||||
## Go Workspace Setup
|
||||
|
||||
The satellite packages can be used together in a Go workspace. After cloning the repositories you need:
|
||||
|
||||
```bash
|
||||
go work init
|
||||
go work use ./go-inference ./go-mlx ./go-rag ./go-ai # add as needed
|
||||
go work sync
|
||||
```
|
||||
|
||||
The CLI repository already uses a Go workspace that includes `cmd/core-gui`, `cmd/bugseti`, and `cmd/examples/*`.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [index.md](index.md) — Main documentation hub
|
||||
- [getting-started.md](getting-started.md) — CLI installation
|
||||
- [configuration.md](configuration.md) — `repos.yaml` registry format
|
||||
1163
docs/plans/2026-02-20-authentik-traefik-plan.md
Normal file
1163
docs/plans/2026-02-20-authentik-traefik-plan.md
Normal file
File diff suppressed because it is too large
Load diff
155
docs/plans/2026-02-21-core-help-design.md
Normal file
155
docs/plans/2026-02-21-core-help-design.md
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
# core.help Documentation Website — Design
|
||||
|
||||
**Date:** 2026-02-21
|
||||
**Author:** Virgil
|
||||
**Status:** Design approved
|
||||
**Domain:** https://core.help
|
||||
|
||||
## Problem
|
||||
|
||||
Documentation is scattered across 39 repos (18 Go packages, 20 PHP packages, 1 CLI). There is no unified docs site. Developers need a single entry point to find CLI commands, Go package APIs, MCP tool references, and PHP module guides.
|
||||
|
||||
## Solution
|
||||
|
||||
A Hugo + Docsy static site at core.help, built from existing markdown docs aggregated by `core docs sync`. No new content — just collect and present what already exists across the ecosystem.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Stack
|
||||
|
||||
- **Hugo** — Go-native static site generator, sub-second builds
|
||||
- **Docsy theme** — Purpose-built for technical docs (used by Kubernetes, gRPC, Knative)
|
||||
- **BunnyCDN** — Static hosting with pull zone
|
||||
- **`core docs sync --target hugo`** — Collects markdown from all repos into Hugo content tree
|
||||
|
||||
### Why Hugo + Docsy (not VitePress or mdBook)
|
||||
|
||||
- Go-native, no Node.js dependency
|
||||
- Handles multi-section navigation (CLI, Go packages, PHP modules, MCP tools)
|
||||
- Sub-second builds for ~250 markdown files
|
||||
- Docsy has built-in search, versioned nav, API reference sections
|
||||
|
||||
## Content Structure
|
||||
|
||||
```
|
||||
docs-site/
|
||||
├── hugo.toml
|
||||
├── content/
|
||||
│ ├── _index.md # Landing page
|
||||
│ ├── getting-started/ # CLI top-level guides
|
||||
│ │ ├── _index.md
|
||||
│ │ ├── installation.md
|
||||
│ │ ├── configuration.md
|
||||
│ │ ├── user-guide.md
|
||||
│ │ ├── troubleshooting.md
|
||||
│ │ └── faq.md
|
||||
│ ├── cli/ # CLI command reference (43 commands)
|
||||
│ │ ├── _index.md
|
||||
│ │ ├── dev/ # core dev commit, push, pull, etc.
|
||||
│ │ ├── ai/ # core ai commands
|
||||
│ │ ├── go/ # core go test, lint, etc.
|
||||
│ │ └── ...
|
||||
│ ├── go/ # Go ecosystem packages (18)
|
||||
│ │ ├── _index.md # Ecosystem overview
|
||||
│ │ ├── go-api/ # README + architecture/development/history
|
||||
│ │ ├── go-ai/
|
||||
│ │ ├── go-mlx/
|
||||
│ │ ├── go-i18n/
|
||||
│ │ └── ...
|
||||
│ ├── mcp/ # MCP tool reference (49 tools)
|
||||
│ │ ├── _index.md
|
||||
│ │ ├── file-operations.md
|
||||
│ │ ├── process-management.md
|
||||
│ │ ├── rag.md
|
||||
│ │ └── ...
|
||||
│ ├── php/ # PHP packages (from core-php/docs/packages/)
|
||||
│ │ ├── _index.md
|
||||
│ │ ├── admin/
|
||||
│ │ ├── tenant/
|
||||
│ │ ├── commerce/
|
||||
│ │ └── ...
|
||||
│ └── kb/ # Knowledge base (wiki pages from go-mlx, go-i18n)
|
||||
│ ├── _index.md
|
||||
│ ├── mlx/
|
||||
│ └── i18n/
|
||||
├── static/ # Logos, favicons
|
||||
├── layouts/ # Custom template overrides (minimal)
|
||||
└── go.mod # Hugo modules (Docsy as module dep)
|
||||
```
|
||||
|
||||
## Sync Pipeline
|
||||
|
||||
`core docs sync --target hugo --output site/content/` performs:
|
||||
|
||||
### Source Mapping
|
||||
|
||||
```
|
||||
cli/docs/index.md → content/getting-started/_index.md
|
||||
cli/docs/getting-started.md → content/getting-started/installation.md
|
||||
cli/docs/user-guide.md → content/getting-started/user-guide.md
|
||||
cli/docs/configuration.md → content/getting-started/configuration.md
|
||||
cli/docs/troubleshooting.md → content/getting-started/troubleshooting.md
|
||||
cli/docs/faq.md → content/getting-started/faq.md
|
||||
|
||||
core/docs/cmd/**/*.md → content/cli/**/*.md
|
||||
|
||||
go-*/README.md → content/go/{name}/_index.md
|
||||
go-*/docs/*.md → content/go/{name}/*.md
|
||||
go-*/KB/*.md → content/kb/{name-suffix}/*.md
|
||||
|
||||
core-*/docs/**/*.md → content/php/{name-suffix}/**/*.md
|
||||
```
|
||||
|
||||
### Front Matter Injection
|
||||
|
||||
If a markdown file doesn't start with `---`, prepend:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: "{derived from filename}"
|
||||
linkTitle: "{short name}"
|
||||
weight: {auto-incremented}
|
||||
---
|
||||
```
|
||||
|
||||
No other content transformations. Markdown stays as-is.
|
||||
|
||||
### Build & Deploy
|
||||
|
||||
```bash
|
||||
core docs sync --target hugo --output docs-site/content/
|
||||
cd docs-site && hugo build
|
||||
hugo deploy --target bunnycdn
|
||||
```
|
||||
|
||||
Hugo deploy config in `hugo.toml`:
|
||||
|
||||
```toml
|
||||
[deployment]
|
||||
[[deployment.targets]]
|
||||
name = "bunnycdn"
|
||||
URL = "s3://core-help?endpoint=storage.bunnycdn.com®ion=auto"
|
||||
```
|
||||
|
||||
Credentials via env vars.
|
||||
|
||||
## Registry
|
||||
|
||||
All 39 repos registered in `.core/repos.yaml` with `docs: true`. Go repos use explicit `path:` fields since they live outside the PHP `base_path`. `FindRegistry()` checks `.core/repos.yaml` alongside `repos.yaml`.
|
||||
|
||||
## Prerequisites Completed
|
||||
|
||||
- [x] `.core/repos.yaml` created with all 39 repos
|
||||
- [x] `FindRegistry()` updated to find `.core/repos.yaml`
|
||||
- [x] `Repo.Path` supports explicit YAML override
|
||||
- [x] go-api docs gap filled (architecture.md, development.md, history.md)
|
||||
- [x] All 18 Go repos have standard docs trio
|
||||
|
||||
## What Remains (Implementation Plan)
|
||||
|
||||
1. Create docs-site repo with Hugo + Docsy scaffold
|
||||
2. Extend `core docs sync` with `--target hugo` mode
|
||||
3. Write section _index.md files (landing page, section intros)
|
||||
4. Hugo config (navigation, search, theme colours)
|
||||
5. BunnyCDN deployment config
|
||||
6. CI pipeline on Forge (optional — can deploy manually initially)
|
||||
642
docs/plans/2026-02-21-core-help-plan.md
Normal file
642
docs/plans/2026-02-21-core-help-plan.md
Normal file
|
|
@ -0,0 +1,642 @@
|
|||
# core.help Hugo Documentation Site — Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Build a Hugo + Docsy documentation site at core.help that aggregates markdown from 39 repos via `core docs sync --target hugo`.
|
||||
|
||||
**Architecture:** Hugo static site with Docsy theme, populated by extending `core docs sync` with a `--target hugo` flag that maps repo docs into Hugo's `content/` tree with auto-injected front matter. Deploy to BunnyCDN.
|
||||
|
||||
**Tech Stack:** Hugo (Go SSG), Docsy theme (Hugo module), BunnyCDN, `core docs sync` CLI
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The docs sync command lives in `/Users/snider/Code/host-uk/cli/cmd/docs/`. The site will be scaffolded at `/Users/snider/Code/host-uk/docs-site/`. The registry at `/Users/snider/Code/host-uk/.core/repos.yaml` already contains all 39 repos (20 PHP + 18 Go + 1 CLI) with explicit paths for Go repos.
|
||||
|
||||
Key files:
|
||||
- `/Users/snider/Code/host-uk/cli/cmd/docs/cmd_sync.go` — sync command (modify)
|
||||
- `/Users/snider/Code/host-uk/cli/cmd/docs/cmd_scan.go` — repo scanner (modify)
|
||||
- `/Users/snider/Code/host-uk/docs-site/` — Hugo site (create)
|
||||
|
||||
## Task 1: Scaffold Hugo + Docsy site
|
||||
|
||||
**Files:**
|
||||
- Create: `/Users/snider/Code/host-uk/docs-site/hugo.toml`
|
||||
- Create: `/Users/snider/Code/host-uk/docs-site/go.mod`
|
||||
- Create: `/Users/snider/Code/host-uk/docs-site/content/_index.md`
|
||||
- Create: `/Users/snider/Code/host-uk/docs-site/content/getting-started/_index.md`
|
||||
- Create: `/Users/snider/Code/host-uk/docs-site/content/cli/_index.md`
|
||||
- Create: `/Users/snider/Code/host-uk/docs-site/content/go/_index.md`
|
||||
- Create: `/Users/snider/Code/host-uk/docs-site/content/mcp/_index.md`
|
||||
- Create: `/Users/snider/Code/host-uk/docs-site/content/php/_index.md`
|
||||
- Create: `/Users/snider/Code/host-uk/docs-site/content/kb/_index.md`
|
||||
|
||||
This is the one-time Hugo scaffolding. No tests — just files.
|
||||
|
||||
**`hugo.toml`:**
|
||||
```toml
|
||||
baseURL = "https://core.help/"
|
||||
title = "Core Documentation"
|
||||
languageCode = "en"
|
||||
defaultContentLanguage = "en"
|
||||
|
||||
enableRobotsTXT = true
|
||||
enableGitInfo = false
|
||||
|
||||
[outputs]
|
||||
home = ["HTML", "JSON"]
|
||||
section = ["HTML"]
|
||||
|
||||
[params]
|
||||
description = "Documentation for the Core CLI, Go packages, PHP modules, and MCP tools"
|
||||
copyright = "Host UK — EUPL-1.2"
|
||||
|
||||
[params.ui]
|
||||
sidebar_menu_compact = true
|
||||
breadcrumb_disable = false
|
||||
sidebar_search_disable = false
|
||||
navbar_logo = false
|
||||
|
||||
[params.ui.readingtime]
|
||||
enable = false
|
||||
|
||||
[module]
|
||||
proxy = "direct"
|
||||
|
||||
[module.hugoVersion]
|
||||
extended = true
|
||||
min = "0.120.0"
|
||||
|
||||
[[module.imports]]
|
||||
path = "github.com/google/docsy"
|
||||
disable = false
|
||||
|
||||
[markup.goldmark.renderer]
|
||||
unsafe = true
|
||||
|
||||
[menu]
|
||||
[[menu.main]]
|
||||
name = "Getting Started"
|
||||
weight = 10
|
||||
url = "/getting-started/"
|
||||
[[menu.main]]
|
||||
name = "CLI Reference"
|
||||
weight = 20
|
||||
url = "/cli/"
|
||||
[[menu.main]]
|
||||
name = "Go Packages"
|
||||
weight = 30
|
||||
url = "/go/"
|
||||
[[menu.main]]
|
||||
name = "MCP Tools"
|
||||
weight = 40
|
||||
url = "/mcp/"
|
||||
[[menu.main]]
|
||||
name = "PHP Packages"
|
||||
weight = 50
|
||||
url = "/php/"
|
||||
[[menu.main]]
|
||||
name = "Knowledge Base"
|
||||
weight = 60
|
||||
url = "/kb/"
|
||||
```
|
||||
|
||||
**`go.mod`:**
|
||||
```
|
||||
module github.com/host-uk/docs-site
|
||||
|
||||
go 1.22
|
||||
|
||||
require github.com/google/docsy v0.11.0
|
||||
```
|
||||
|
||||
Note: Run `hugo mod get` after creating these files to populate `go.sum` and download Docsy.
|
||||
|
||||
**Section `_index.md` files** — each needs Hugo front matter:
|
||||
|
||||
`content/_index.md`:
|
||||
```markdown
|
||||
---
|
||||
title: "Core Documentation"
|
||||
description: "Documentation for the Core CLI, Go packages, PHP modules, and MCP tools"
|
||||
---
|
||||
|
||||
Welcome to the Core ecosystem documentation.
|
||||
|
||||
## Sections
|
||||
|
||||
- [Getting Started](/getting-started/) — Installation, configuration, and first steps
|
||||
- [CLI Reference](/cli/) — Command reference for `core` CLI
|
||||
- [Go Packages](/go/) — Go ecosystem package documentation
|
||||
- [MCP Tools](/mcp/) — Model Context Protocol tool reference
|
||||
- [PHP Packages](/php/) — PHP module documentation
|
||||
- [Knowledge Base](/kb/) — Wiki articles and deep dives
|
||||
```
|
||||
|
||||
`content/getting-started/_index.md`:
|
||||
```markdown
|
||||
---
|
||||
title: "Getting Started"
|
||||
linkTitle: "Getting Started"
|
||||
weight: 10
|
||||
description: "Installation, configuration, and first steps with the Core CLI"
|
||||
---
|
||||
```
|
||||
|
||||
`content/cli/_index.md`:
|
||||
```markdown
|
||||
---
|
||||
title: "CLI Reference"
|
||||
linkTitle: "CLI Reference"
|
||||
weight: 20
|
||||
description: "Command reference for the core CLI tool"
|
||||
---
|
||||
```
|
||||
|
||||
`content/go/_index.md`:
|
||||
```markdown
|
||||
---
|
||||
title: "Go Packages"
|
||||
linkTitle: "Go Packages"
|
||||
weight: 30
|
||||
description: "Documentation for the Go ecosystem packages"
|
||||
---
|
||||
```
|
||||
|
||||
`content/mcp/_index.md`:
|
||||
```markdown
|
||||
---
|
||||
title: "MCP Tools"
|
||||
linkTitle: "MCP Tools"
|
||||
weight: 40
|
||||
description: "Model Context Protocol tool reference — file operations, RAG, ML inference, process management"
|
||||
---
|
||||
```
|
||||
|
||||
`content/php/_index.md`:
|
||||
```markdown
|
||||
---
|
||||
title: "PHP Packages"
|
||||
linkTitle: "PHP Packages"
|
||||
weight: 50
|
||||
description: "Documentation for the PHP module ecosystem"
|
||||
---
|
||||
```
|
||||
|
||||
`content/kb/_index.md`:
|
||||
```markdown
|
||||
---
|
||||
title: "Knowledge Base"
|
||||
linkTitle: "Knowledge Base"
|
||||
weight: 60
|
||||
description: "Wiki articles, deep dives, and reference material"
|
||||
---
|
||||
```
|
||||
|
||||
**Verify:** After creating files, run from `/Users/snider/Code/host-uk/docs-site/`:
|
||||
```bash
|
||||
hugo mod get
|
||||
hugo server
|
||||
```
|
||||
The site should start and show the landing page with Docsy theme at `localhost:1313`.
|
||||
|
||||
**Commit:**
|
||||
```bash
|
||||
cd /Users/snider/Code/host-uk/docs-site
|
||||
git init
|
||||
git add .
|
||||
git commit -m "feat: scaffold Hugo + Docsy documentation site"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Extend scanRepoDocs to collect KB/ and README
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/snider/Code/host-uk/cli/cmd/docs/cmd_scan.go`
|
||||
|
||||
Currently `scanRepoDocs` only collects files from `docs/`. For the Hugo target we also need:
|
||||
- `KB/**/*.md` files (wiki pages from go-mlx, go-i18n)
|
||||
- `README.md` content (becomes the package _index.md)
|
||||
|
||||
Add a `KBFiles []string` field to `RepoDocInfo` and scan `KB/` alongside `docs/`:
|
||||
|
||||
```go
|
||||
type RepoDocInfo struct {
|
||||
Name string
|
||||
Path string
|
||||
HasDocs bool
|
||||
Readme string
|
||||
ClaudeMd string
|
||||
Changelog string
|
||||
DocsFiles []string // All files in docs/ directory (recursive)
|
||||
KBFiles []string // All files in KB/ directory (recursive)
|
||||
}
|
||||
```
|
||||
|
||||
In `scanRepoDocs`, after the `docs/` walk, add a second walk for `KB/`:
|
||||
|
||||
```go
|
||||
// Recursively scan KB/ directory for .md files
|
||||
kbDir := filepath.Join(repo.Path, "KB")
|
||||
if _, err := io.Local.List(kbDir); err == nil {
|
||||
_ = filepath.WalkDir(kbDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if d.IsDir() || !strings.HasSuffix(d.Name(), ".md") {
|
||||
return nil
|
||||
}
|
||||
relPath, _ := filepath.Rel(kbDir, path)
|
||||
info.KBFiles = append(info.KBFiles, relPath)
|
||||
info.HasDocs = true
|
||||
return nil
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**Tests:** The existing tests should still pass. No new test file needed — this is a data-collection change.
|
||||
|
||||
**Verify:** `cd /Users/snider/Code/host-uk/cli && GOWORK=off go build ./cmd/docs/...`
|
||||
|
||||
**Commit:**
|
||||
```bash
|
||||
git add cmd/docs/cmd_scan.go
|
||||
git commit -m "feat(docs): scan KB/ directory alongside docs/"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Add `--target hugo` flag and Hugo sync logic
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/snider/Code/host-uk/cli/cmd/docs/cmd_sync.go`
|
||||
|
||||
This is the main task. Add a `--target` flag (default `"php"`) and a new `runHugoSync` function that maps repos to Hugo's content tree.
|
||||
|
||||
**Add flag variable and registration:**
|
||||
|
||||
```go
|
||||
var (
|
||||
docsSyncRegistryPath string
|
||||
docsSyncDryRun bool
|
||||
docsSyncOutputDir string
|
||||
docsSyncTarget string
|
||||
)
|
||||
|
||||
func init() {
|
||||
docsSyncCmd.Flags().StringVar(&docsSyncRegistryPath, "registry", "", i18n.T("common.flag.registry"))
|
||||
docsSyncCmd.Flags().BoolVar(&docsSyncDryRun, "dry-run", false, i18n.T("cmd.docs.sync.flag.dry_run"))
|
||||
docsSyncCmd.Flags().StringVar(&docsSyncOutputDir, "output", "", i18n.T("cmd.docs.sync.flag.output"))
|
||||
docsSyncCmd.Flags().StringVar(&docsSyncTarget, "target", "php", "Target format: php (default) or hugo")
|
||||
}
|
||||
```
|
||||
|
||||
**Update RunE to pass target:**
|
||||
```go
|
||||
RunE: func(cmd *cli.Command, args []string) error {
|
||||
return runDocsSync(docsSyncRegistryPath, docsSyncOutputDir, docsSyncDryRun, docsSyncTarget)
|
||||
},
|
||||
```
|
||||
|
||||
**Update `runDocsSync` signature and add target dispatch:**
|
||||
```go
|
||||
func runDocsSync(registryPath string, outputDir string, dryRun bool, target string) error {
|
||||
reg, basePath, err := loadRegistry(registryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch target {
|
||||
case "hugo":
|
||||
return runHugoSync(reg, basePath, outputDir, dryRun)
|
||||
default:
|
||||
return runPHPSync(reg, basePath, outputDir, dryRun)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Rename current sync body to `runPHPSync`** — extract lines 67-159 of current `runDocsSync` into `runPHPSync(reg, basePath, outputDir string, dryRun bool) error`. This is a pure extract, no logic changes.
|
||||
|
||||
**Add `hugoOutputName` mapping function:**
|
||||
```go
|
||||
// hugoOutputName maps repo name to Hugo content section and folder.
|
||||
// Returns (section, folder) where section is the top-level content dir.
|
||||
func hugoOutputName(repoName string) (string, string) {
|
||||
// CLI guides
|
||||
if repoName == "cli" {
|
||||
return "getting-started", ""
|
||||
}
|
||||
// Core CLI command docs
|
||||
if repoName == "core" {
|
||||
return "cli", ""
|
||||
}
|
||||
// Go packages
|
||||
if strings.HasPrefix(repoName, "go-") {
|
||||
return "go", repoName
|
||||
}
|
||||
// PHP packages
|
||||
if strings.HasPrefix(repoName, "core-") {
|
||||
return "php", strings.TrimPrefix(repoName, "core-")
|
||||
}
|
||||
return "go", repoName
|
||||
}
|
||||
```
|
||||
|
||||
**Add front matter injection helper:**
|
||||
```go
|
||||
// injectFrontMatter prepends Hugo front matter to markdown content if missing.
|
||||
func injectFrontMatter(content []byte, title string, weight int) []byte {
|
||||
// Already has front matter
|
||||
if bytes.HasPrefix(bytes.TrimSpace(content), []byte("---")) {
|
||||
return content
|
||||
}
|
||||
fm := fmt.Sprintf("---\ntitle: %q\nweight: %d\n---\n\n", title, weight)
|
||||
return append([]byte(fm), content...)
|
||||
}
|
||||
|
||||
// titleFromFilename derives a human-readable title from a filename.
|
||||
func titleFromFilename(filename string) string {
|
||||
name := strings.TrimSuffix(filepath.Base(filename), ".md")
|
||||
name = strings.ReplaceAll(name, "-", " ")
|
||||
name = strings.ReplaceAll(name, "_", " ")
|
||||
// Title case
|
||||
words := strings.Fields(name)
|
||||
for i, w := range words {
|
||||
if len(w) > 0 {
|
||||
words[i] = strings.ToUpper(w[:1]) + w[1:]
|
||||
}
|
||||
}
|
||||
return strings.Join(words, " ")
|
||||
}
|
||||
```
|
||||
|
||||
**Add `runHugoSync` function:**
|
||||
```go
|
||||
func runHugoSync(reg *repos.Registry, basePath string, outputDir string, dryRun bool) error {
|
||||
if outputDir == "" {
|
||||
outputDir = filepath.Join(basePath, "docs-site", "content")
|
||||
}
|
||||
|
||||
// Scan all repos
|
||||
var docsInfo []RepoDocInfo
|
||||
for _, repo := range reg.List() {
|
||||
if repo.Name == "core-template" || repo.Name == "core-claude" {
|
||||
continue
|
||||
}
|
||||
info := scanRepoDocs(repo)
|
||||
if info.HasDocs {
|
||||
docsInfo = append(docsInfo, info)
|
||||
}
|
||||
}
|
||||
|
||||
if len(docsInfo) == 0 {
|
||||
cli.Text("No documentation found")
|
||||
return nil
|
||||
}
|
||||
|
||||
cli.Print("\n Hugo sync: %d repos with docs → %s\n\n", len(docsInfo), outputDir)
|
||||
|
||||
// Show plan
|
||||
for _, info := range docsInfo {
|
||||
section, folder := hugoOutputName(info.Name)
|
||||
target := section
|
||||
if folder != "" {
|
||||
target = section + "/" + folder
|
||||
}
|
||||
fileCount := len(info.DocsFiles) + len(info.KBFiles)
|
||||
if info.Readme != "" {
|
||||
fileCount++
|
||||
}
|
||||
cli.Print(" %s → %s/ (%d files)\n", repoNameStyle.Render(info.Name), target, fileCount)
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
cli.Print("\n Dry run — no files written\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
cli.Blank()
|
||||
if !confirm("Sync to Hugo content directory?") {
|
||||
cli.Text("Aborted")
|
||||
return nil
|
||||
}
|
||||
|
||||
cli.Blank()
|
||||
var synced int
|
||||
for _, info := range docsInfo {
|
||||
section, folder := hugoOutputName(info.Name)
|
||||
|
||||
// Build destination path
|
||||
destDir := filepath.Join(outputDir, section)
|
||||
if folder != "" {
|
||||
destDir = filepath.Join(destDir, folder)
|
||||
}
|
||||
|
||||
// Copy docs/ files
|
||||
weight := 10
|
||||
docsDir := filepath.Join(info.Path, "docs")
|
||||
for _, f := range info.DocsFiles {
|
||||
src := filepath.Join(docsDir, f)
|
||||
dst := filepath.Join(destDir, f)
|
||||
if err := copyWithFrontMatter(src, dst, weight); err != nil {
|
||||
cli.Print(" %s %s: %s\n", errorStyle.Render("✗"), f, err)
|
||||
continue
|
||||
}
|
||||
weight += 10
|
||||
}
|
||||
|
||||
// Copy README.md as _index.md (if not CLI/core which use their own index)
|
||||
if info.Readme != "" && folder != "" {
|
||||
dst := filepath.Join(destDir, "_index.md")
|
||||
if err := copyWithFrontMatter(info.Readme, dst, 1); err != nil {
|
||||
cli.Print(" %s README: %s\n", errorStyle.Render("✗"), err)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy KB/ files to kb/{suffix}/
|
||||
if len(info.KBFiles) > 0 {
|
||||
// Extract suffix: go-mlx → mlx, go-i18n → i18n
|
||||
suffix := strings.TrimPrefix(info.Name, "go-")
|
||||
kbDestDir := filepath.Join(outputDir, "kb", suffix)
|
||||
kbDir := filepath.Join(info.Path, "KB")
|
||||
kbWeight := 10
|
||||
for _, f := range info.KBFiles {
|
||||
src := filepath.Join(kbDir, f)
|
||||
dst := filepath.Join(kbDestDir, f)
|
||||
if err := copyWithFrontMatter(src, dst, kbWeight); err != nil {
|
||||
cli.Print(" %s KB/%s: %s\n", errorStyle.Render("✗"), f, err)
|
||||
continue
|
||||
}
|
||||
kbWeight += 10
|
||||
}
|
||||
}
|
||||
|
||||
cli.Print(" %s %s\n", successStyle.Render("✓"), info.Name)
|
||||
synced++
|
||||
}
|
||||
|
||||
cli.Print("\n Synced %d repos to Hugo content\n", synced)
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyWithFrontMatter copies a markdown file, injecting front matter if missing.
|
||||
func copyWithFrontMatter(src, dst string, weight int) error {
|
||||
if err := io.Local.EnsureDir(filepath.Dir(dst)); err != nil {
|
||||
return err
|
||||
}
|
||||
content, err := io.Local.Read(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
title := titleFromFilename(src)
|
||||
result := injectFrontMatter([]byte(content), title, weight)
|
||||
return io.Local.Write(dst, string(result))
|
||||
}
|
||||
```
|
||||
|
||||
**Add imports** at top of file:
|
||||
```go
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/cli"
|
||||
"forge.lthn.ai/core/go/pkg/i18n"
|
||||
"forge.lthn.ai/core/go/pkg/io"
|
||||
"forge.lthn.ai/core/go/pkg/repos"
|
||||
)
|
||||
```
|
||||
|
||||
**Verify:** `cd /Users/snider/Code/host-uk/cli && GOWORK=off go build ./cmd/docs/...`
|
||||
|
||||
**Commit:**
|
||||
```bash
|
||||
git add cmd/docs/cmd_sync.go
|
||||
git commit -m "feat(docs): add --target hugo sync mode for core.help"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Test the full pipeline
|
||||
|
||||
**No code changes.** Run the pipeline end-to-end.
|
||||
|
||||
**Step 1:** Sync docs to Hugo:
|
||||
```bash
|
||||
cd /Users/snider/Code/host-uk
|
||||
core docs sync --target hugo --dry-run
|
||||
```
|
||||
Verify all 39 repos appear with correct section mappings.
|
||||
|
||||
**Step 2:** Run actual sync:
|
||||
```bash
|
||||
core docs sync --target hugo
|
||||
```
|
||||
|
||||
**Step 3:** Build and preview:
|
||||
```bash
|
||||
cd /Users/snider/Code/host-uk/docs-site
|
||||
hugo server
|
||||
```
|
||||
Open `localhost:1313` and verify:
|
||||
- Landing page renders with section links
|
||||
- Getting Started section has CLI guides
|
||||
- CLI Reference section has command docs
|
||||
- Go Packages section has 18 packages with architecture/development/history
|
||||
- PHP Packages section has PHP module docs
|
||||
- Knowledge Base has MLX and i18n wiki pages
|
||||
- Navigation works, search works
|
||||
|
||||
**Step 4:** Fix any issues found during preview.
|
||||
|
||||
**Commit docs-site content:**
|
||||
```bash
|
||||
cd /Users/snider/Code/host-uk/docs-site
|
||||
git add content/
|
||||
git commit -m "feat: sync initial content from 39 repos"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: BunnyCDN deployment config
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/snider/Code/host-uk/docs-site/hugo.toml`
|
||||
|
||||
Add deployment target:
|
||||
|
||||
```toml
|
||||
[deployment]
|
||||
[[deployment.targets]]
|
||||
name = "production"
|
||||
URL = "s3://core-help?endpoint=storage.bunnycdn.com®ion=auto"
|
||||
```
|
||||
|
||||
Add a `Taskfile.yml` for convenience:
|
||||
|
||||
**Create:** `/Users/snider/Code/host-uk/docs-site/Taskfile.yml`
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
dev:
|
||||
desc: Start Hugo dev server
|
||||
cmds:
|
||||
- hugo server --buildDrafts
|
||||
|
||||
build:
|
||||
desc: Build static site
|
||||
cmds:
|
||||
- hugo --minify
|
||||
|
||||
sync:
|
||||
desc: Sync docs from all repos
|
||||
dir: ..
|
||||
cmds:
|
||||
- core docs sync --target hugo
|
||||
|
||||
deploy:
|
||||
desc: Build and deploy to BunnyCDN
|
||||
cmds:
|
||||
- task: sync
|
||||
- task: build
|
||||
- hugo deploy --target production
|
||||
|
||||
clean:
|
||||
desc: Remove generated content (keeps _index.md files)
|
||||
cmds:
|
||||
- find content -name "*.md" ! -name "_index.md" -delete
|
||||
```
|
||||
|
||||
**Verify:** `task dev` starts the site.
|
||||
|
||||
**Commit:**
|
||||
```bash
|
||||
git add hugo.toml Taskfile.yml
|
||||
git commit -m "feat: add BunnyCDN deployment config and Taskfile"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependency Sequencing
|
||||
|
||||
```
|
||||
Task 1 (Hugo scaffold) — independent, do first
|
||||
Task 2 (scan KB/) — independent, can parallel with Task 1
|
||||
Task 3 (--target hugo) — depends on Task 2
|
||||
Task 4 (test pipeline) — depends on Tasks 1 + 3
|
||||
Task 5 (deploy config) — depends on Task 1
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
After all tasks:
|
||||
1. `core docs sync --target hugo` populates `docs-site/content/` from all repos
|
||||
2. `cd docs-site && hugo server` renders the full site
|
||||
3. Navigation has 6 sections: Getting Started, CLI, Go, MCP, PHP, KB
|
||||
4. All existing markdown renders correctly with auto-injected front matter
|
||||
5. `hugo build` produces `public/` with no errors
|
||||
851
docs/plans/completed/2026-02-05-mcp-integration-original.md
Normal file
851
docs/plans/completed/2026-02-05-mcp-integration-original.md
Normal file
|
|
@ -0,0 +1,851 @@
|
|||
# MCP Integration Implementation Plan
|
||||
|
||||
> **Status:** Completed. MCP command now lives in `go-ai/cmd/mcpcmd/`. Code examples below use the old `init()` + `RegisterCommands()` pattern — the current approach uses `cli.WithCommands()` (see cli-meta-package-design.md).
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Add `core mcp serve` command with RAG and metrics tools, then configure the agentic-flows plugin to use it.
|
||||
|
||||
**Architecture:** Create a new `mcp` command package that starts the pkg/mcp server with extended tools. RAG tools call the existing exported functions in internal/cmd/rag. Metrics tools call pkg/ai directly. The agentic-flows plugin gets a `.mcp.json` that spawns `core mcp serve`.
|
||||
|
||||
**Tech Stack:** Go 1.25, github.com/modelcontextprotocol/go-sdk/mcp, pkg/rag, pkg/ai
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add RAG tools to pkg/mcp
|
||||
|
||||
**Files:**
|
||||
- Create: `pkg/mcp/tools_rag.go`
|
||||
- Modify: `pkg/mcp/mcp.go:99-101` (registerTools)
|
||||
- Test: `pkg/mcp/tools_rag_test.go`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Create `pkg/mcp/tools_rag_test.go`:
|
||||
|
||||
```go
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
func TestRAGQueryTool_Good(t *testing.T) {
|
||||
// This test verifies the tool is registered and callable.
|
||||
// It doesn't require Qdrant/Ollama running - just checks structure.
|
||||
s, err := New(WithWorkspaceRoot(""))
|
||||
if err != nil {
|
||||
t.Fatalf("New() error: %v", err)
|
||||
}
|
||||
|
||||
// Check that rag_query tool is registered
|
||||
tools := s.Server().ListTools()
|
||||
found := false
|
||||
for _, tool := range tools {
|
||||
if tool.Name == "rag_query" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("rag_query tool not registered")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRAGQueryInput_Good(t *testing.T) {
|
||||
input := RAGQueryInput{
|
||||
Question: "how do I deploy?",
|
||||
Collection: "hostuk-docs",
|
||||
TopK: 5,
|
||||
}
|
||||
if input.Question == "" {
|
||||
t.Error("Question should not be empty")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `go test -run TestRAGQueryTool ./pkg/mcp/... -v`
|
||||
Expected: FAIL with "rag_query tool not registered"
|
||||
|
||||
**Step 3: Create tools_rag.go with types and tool registration**
|
||||
|
||||
Create `pkg/mcp/tools_rag.go`:
|
||||
|
||||
```go
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
ragcmd "forge.lthn.ai/core/cli/internal/cmd/rag"
|
||||
"forge.lthn.ai/core/cli/pkg/rag"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// RAG tool input/output types
|
||||
|
||||
// RAGQueryInput contains parameters for querying the vector database.
|
||||
type RAGQueryInput struct {
|
||||
Question string `json:"question"`
|
||||
Collection string `json:"collection,omitempty"`
|
||||
TopK int `json:"top_k,omitempty"`
|
||||
}
|
||||
|
||||
// RAGQueryOutput contains the query results.
|
||||
type RAGQueryOutput struct {
|
||||
Results []RAGResult `json:"results"`
|
||||
Context string `json:"context"`
|
||||
}
|
||||
|
||||
// RAGResult represents a single search result.
|
||||
type RAGResult struct {
|
||||
Content string `json:"content"`
|
||||
Score float32 `json:"score"`
|
||||
Source string `json:"source"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// RAGIngestInput contains parameters for ingesting documents.
|
||||
type RAGIngestInput struct {
|
||||
Path string `json:"path"`
|
||||
Collection string `json:"collection,omitempty"`
|
||||
Recreate bool `json:"recreate,omitempty"`
|
||||
}
|
||||
|
||||
// RAGIngestOutput contains the ingestion results.
|
||||
type RAGIngestOutput struct {
|
||||
Success bool `json:"success"`
|
||||
Path string `json:"path"`
|
||||
Chunks int `json:"chunks"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// RAGCollectionsInput contains parameters for listing collections.
|
||||
type RAGCollectionsInput struct {
|
||||
ShowStats bool `json:"show_stats,omitempty"`
|
||||
}
|
||||
|
||||
// RAGCollectionsOutput contains the list of collections.
|
||||
type RAGCollectionsOutput struct {
|
||||
Collections []CollectionInfo `json:"collections"`
|
||||
}
|
||||
|
||||
// CollectionInfo describes a Qdrant collection.
|
||||
type CollectionInfo struct {
|
||||
Name string `json:"name"`
|
||||
PointsCount uint64 `json:"points_count,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// registerRAGTools adds RAG tools to the MCP server.
|
||||
func (s *Service) registerRAGTools(server *mcp.Server) {
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "rag_query",
|
||||
Description: "Query the vector database for relevant documents using semantic search",
|
||||
}, s.ragQuery)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "rag_ingest",
|
||||
Description: "Ingest a file or directory into the vector database",
|
||||
}, s.ragIngest)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "rag_collections",
|
||||
Description: "List available vector database collections",
|
||||
}, s.ragCollections)
|
||||
}
|
||||
|
||||
func (s *Service) ragQuery(ctx context.Context, req *mcp.CallToolRequest, input RAGQueryInput) (*mcp.CallToolResult, RAGQueryOutput, error) {
|
||||
s.logger.Info("MCP tool execution", "tool", "rag_query", "question", input.Question)
|
||||
|
||||
collection := input.Collection
|
||||
if collection == "" {
|
||||
collection = "hostuk-docs"
|
||||
}
|
||||
topK := input.TopK
|
||||
if topK <= 0 {
|
||||
topK = 5
|
||||
}
|
||||
|
||||
results, err := ragcmd.QueryDocs(ctx, input.Question, collection, topK)
|
||||
if err != nil {
|
||||
return nil, RAGQueryOutput{}, fmt.Errorf("query failed: %w", err)
|
||||
}
|
||||
|
||||
// Convert to output format
|
||||
out := RAGQueryOutput{
|
||||
Results: make([]RAGResult, 0, len(results)),
|
||||
Context: rag.FormatResultsContext(results),
|
||||
}
|
||||
for _, r := range results {
|
||||
out.Results = append(out.Results, RAGResult{
|
||||
Content: r.Content,
|
||||
Score: r.Score,
|
||||
Source: r.Source,
|
||||
Metadata: r.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
return nil, out, nil
|
||||
}
|
||||
|
||||
func (s *Service) ragIngest(ctx context.Context, req *mcp.CallToolRequest, input RAGIngestInput) (*mcp.CallToolResult, RAGIngestOutput, error) {
|
||||
s.logger.Security("MCP tool execution", "tool", "rag_ingest", "path", input.Path)
|
||||
|
||||
collection := input.Collection
|
||||
if collection == "" {
|
||||
collection = "hostuk-docs"
|
||||
}
|
||||
|
||||
// Check if path is a file or directory
|
||||
info, err := s.medium.Stat(input.Path)
|
||||
if err != nil {
|
||||
return nil, RAGIngestOutput{}, fmt.Errorf("path not found: %w", err)
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
err = ragcmd.IngestDirectory(ctx, input.Path, collection, input.Recreate)
|
||||
if err != nil {
|
||||
return nil, RAGIngestOutput{}, fmt.Errorf("ingest directory failed: %w", err)
|
||||
}
|
||||
return nil, RAGIngestOutput{
|
||||
Success: true,
|
||||
Path: input.Path,
|
||||
Message: fmt.Sprintf("Ingested directory into collection %s", collection),
|
||||
}, nil
|
||||
}
|
||||
|
||||
chunks, err := ragcmd.IngestFile(ctx, input.Path, collection)
|
||||
if err != nil {
|
||||
return nil, RAGIngestOutput{}, fmt.Errorf("ingest file failed: %w", err)
|
||||
}
|
||||
|
||||
return nil, RAGIngestOutput{
|
||||
Success: true,
|
||||
Path: input.Path,
|
||||
Chunks: chunks,
|
||||
Message: fmt.Sprintf("Ingested %d chunks into collection %s", chunks, collection),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) ragCollections(ctx context.Context, req *mcp.CallToolRequest, input RAGCollectionsInput) (*mcp.CallToolResult, RAGCollectionsOutput, error) {
|
||||
s.logger.Info("MCP tool execution", "tool", "rag_collections")
|
||||
|
||||
client, err := rag.NewQdrantClient(rag.DefaultQdrantConfig())
|
||||
if err != nil {
|
||||
return nil, RAGCollectionsOutput{}, fmt.Errorf("connect to Qdrant: %w", err)
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
names, err := client.ListCollections(ctx)
|
||||
if err != nil {
|
||||
return nil, RAGCollectionsOutput{}, fmt.Errorf("list collections: %w", err)
|
||||
}
|
||||
|
||||
out := RAGCollectionsOutput{
|
||||
Collections: make([]CollectionInfo, 0, len(names)),
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
info := CollectionInfo{Name: name}
|
||||
if input.ShowStats {
|
||||
cinfo, err := client.CollectionInfo(ctx, name)
|
||||
if err == nil {
|
||||
info.PointsCount = cinfo.PointsCount
|
||||
info.Status = cinfo.Status.String()
|
||||
}
|
||||
}
|
||||
out.Collections = append(out.Collections, info)
|
||||
}
|
||||
|
||||
return nil, out, nil
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Update mcp.go to call registerRAGTools**
|
||||
|
||||
In `pkg/mcp/mcp.go`, modify the `registerTools` function (around line 104) to add:
|
||||
|
||||
```go
|
||||
func (s *Service) registerTools(server *mcp.Server) {
|
||||
// File operations (existing)
|
||||
// ... existing code ...
|
||||
|
||||
// RAG operations
|
||||
s.registerRAGTools(server)
|
||||
}
|
||||
```
|
||||
|
||||
**Step 5: Run test to verify it passes**
|
||||
|
||||
Run: `go test -run TestRAGQuery ./pkg/mcp/... -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add pkg/mcp/tools_rag.go pkg/mcp/tools_rag_test.go pkg/mcp/mcp.go
|
||||
git commit -m "feat(mcp): add RAG tools (query, ingest, collections)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Add metrics tools to pkg/mcp
|
||||
|
||||
**Files:**
|
||||
- Create: `pkg/mcp/tools_metrics.go`
|
||||
- Modify: `pkg/mcp/mcp.go` (registerTools)
|
||||
- Test: `pkg/mcp/tools_metrics_test.go`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
Create `pkg/mcp/tools_metrics_test.go`:
|
||||
|
||||
```go
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMetricsRecordTool_Good(t *testing.T) {
|
||||
s, err := New(WithWorkspaceRoot(""))
|
||||
if err != nil {
|
||||
t.Fatalf("New() error: %v", err)
|
||||
}
|
||||
|
||||
tools := s.Server().ListTools()
|
||||
found := false
|
||||
for _, tool := range tools {
|
||||
if tool.Name == "metrics_record" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("metrics_record tool not registered")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsQueryTool_Good(t *testing.T) {
|
||||
s, err := New(WithWorkspaceRoot(""))
|
||||
if err != nil {
|
||||
t.Fatalf("New() error: %v", err)
|
||||
}
|
||||
|
||||
tools := s.Server().ListTools()
|
||||
found := false
|
||||
for _, tool := range tools {
|
||||
if tool.Name == "metrics_query" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("metrics_query tool not registered")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `go test -run TestMetrics ./pkg/mcp/... -v`
|
||||
Expected: FAIL
|
||||
|
||||
**Step 3: Create tools_metrics.go**
|
||||
|
||||
Create `pkg/mcp/tools_metrics.go`:
|
||||
|
||||
```go
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"forge.lthn.ai/core/cli/pkg/ai"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// Metrics tool input/output types
|
||||
|
||||
// MetricsRecordInput contains parameters for recording a metric event.
|
||||
type MetricsRecordInput struct {
|
||||
Type string `json:"type"`
|
||||
AgentID string `json:"agent_id,omitempty"`
|
||||
Repo string `json:"repo,omitempty"`
|
||||
Data map[string]any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// MetricsRecordOutput contains the result of recording.
|
||||
type MetricsRecordOutput struct {
|
||||
Success bool `json:"success"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
// MetricsQueryInput contains parameters for querying metrics.
|
||||
type MetricsQueryInput struct {
|
||||
Since string `json:"since,omitempty"` // e.g., "7d", "24h"
|
||||
}
|
||||
|
||||
// MetricsQueryOutput contains the query results.
|
||||
type MetricsQueryOutput struct {
|
||||
Total int `json:"total"`
|
||||
ByType []MetricCount `json:"by_type"`
|
||||
ByRepo []MetricCount `json:"by_repo"`
|
||||
ByAgent []MetricCount `json:"by_agent"`
|
||||
Events []MetricEventBrief `json:"events,omitempty"`
|
||||
}
|
||||
|
||||
// MetricCount represents a count by key.
|
||||
type MetricCount struct {
|
||||
Key string `json:"key"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// MetricEventBrief is a simplified event for output.
|
||||
type MetricEventBrief struct {
|
||||
Type string `json:"type"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
AgentID string `json:"agent_id,omitempty"`
|
||||
Repo string `json:"repo,omitempty"`
|
||||
}
|
||||
|
||||
// registerMetricsTools adds metrics tools to the MCP server.
|
||||
func (s *Service) registerMetricsTools(server *mcp.Server) {
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "metrics_record",
|
||||
Description: "Record a metric event (AI task, security scan, job creation, etc.)",
|
||||
}, s.metricsRecord)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "metrics_query",
|
||||
Description: "Query recorded metrics with aggregation by type, repo, and agent",
|
||||
}, s.metricsQuery)
|
||||
}
|
||||
|
||||
func (s *Service) metricsRecord(ctx context.Context, req *mcp.CallToolRequest, input MetricsRecordInput) (*mcp.CallToolResult, MetricsRecordOutput, error) {
|
||||
s.logger.Info("MCP tool execution", "tool", "metrics_record", "type", input.Type)
|
||||
|
||||
if input.Type == "" {
|
||||
return nil, MetricsRecordOutput{}, fmt.Errorf("type is required")
|
||||
}
|
||||
|
||||
event := ai.Event{
|
||||
Type: input.Type,
|
||||
Timestamp: time.Now(),
|
||||
AgentID: input.AgentID,
|
||||
Repo: input.Repo,
|
||||
Data: input.Data,
|
||||
}
|
||||
|
||||
if err := ai.Record(event); err != nil {
|
||||
return nil, MetricsRecordOutput{}, fmt.Errorf("record event: %w", err)
|
||||
}
|
||||
|
||||
return nil, MetricsRecordOutput{
|
||||
Success: true,
|
||||
Timestamp: event.Timestamp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) metricsQuery(ctx context.Context, req *mcp.CallToolRequest, input MetricsQueryInput) (*mcp.CallToolResult, MetricsQueryOutput, error) {
|
||||
s.logger.Info("MCP tool execution", "tool", "metrics_query", "since", input.Since)
|
||||
|
||||
since := input.Since
|
||||
if since == "" {
|
||||
since = "7d"
|
||||
}
|
||||
|
||||
duration, err := parseDuration(since)
|
||||
if err != nil {
|
||||
return nil, MetricsQueryOutput{}, fmt.Errorf("invalid since value: %w", err)
|
||||
}
|
||||
|
||||
sinceTime := time.Now().Add(-duration)
|
||||
events, err := ai.ReadEvents(sinceTime)
|
||||
if err != nil {
|
||||
return nil, MetricsQueryOutput{}, fmt.Errorf("read events: %w", err)
|
||||
}
|
||||
|
||||
summary := ai.Summary(events)
|
||||
|
||||
out := MetricsQueryOutput{
|
||||
Total: summary["total"].(int),
|
||||
}
|
||||
|
||||
// Convert by_type
|
||||
if byType, ok := summary["by_type"].([]map[string]any); ok {
|
||||
for _, entry := range byType {
|
||||
out.ByType = append(out.ByType, MetricCount{
|
||||
Key: entry["key"].(string),
|
||||
Count: entry["count"].(int),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Convert by_repo
|
||||
if byRepo, ok := summary["by_repo"].([]map[string]any); ok {
|
||||
for _, entry := range byRepo {
|
||||
out.ByRepo = append(out.ByRepo, MetricCount{
|
||||
Key: entry["key"].(string),
|
||||
Count: entry["count"].(int),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Convert by_agent
|
||||
if byAgent, ok := summary["by_agent"].([]map[string]any); ok {
|
||||
for _, entry := range byAgent {
|
||||
out.ByAgent = append(out.ByAgent, MetricCount{
|
||||
Key: entry["key"].(string),
|
||||
Count: entry["count"].(int),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Include last 10 events for context
|
||||
limit := 10
|
||||
if len(events) < limit {
|
||||
limit = len(events)
|
||||
}
|
||||
for i := len(events) - limit; i < len(events); i++ {
|
||||
ev := events[i]
|
||||
out.Events = append(out.Events, MetricEventBrief{
|
||||
Type: ev.Type,
|
||||
Timestamp: ev.Timestamp,
|
||||
AgentID: ev.AgentID,
|
||||
Repo: ev.Repo,
|
||||
})
|
||||
}
|
||||
|
||||
return nil, out, nil
|
||||
}
|
||||
|
||||
// parseDuration parses a human-friendly duration like "7d", "24h", "30d".
|
||||
func parseDuration(s string) (time.Duration, error) {
|
||||
if len(s) < 2 {
|
||||
return 0, fmt.Errorf("invalid duration: %s", s)
|
||||
}
|
||||
|
||||
unit := s[len(s)-1]
|
||||
value := s[:len(s)-1]
|
||||
|
||||
var n int
|
||||
if _, err := fmt.Sscanf(value, "%d", &n); err != nil {
|
||||
return 0, fmt.Errorf("invalid duration: %s", s)
|
||||
}
|
||||
|
||||
if n <= 0 {
|
||||
return 0, fmt.Errorf("duration must be positive: %s", s)
|
||||
}
|
||||
|
||||
switch unit {
|
||||
case 'd':
|
||||
return time.Duration(n) * 24 * time.Hour, nil
|
||||
case 'h':
|
||||
return time.Duration(n) * time.Hour, nil
|
||||
case 'm':
|
||||
return time.Duration(n) * time.Minute, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown unit %c in duration: %s", unit, s)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Update mcp.go to call registerMetricsTools**
|
||||
|
||||
In `pkg/mcp/mcp.go`, add to `registerTools`:
|
||||
|
||||
```go
|
||||
func (s *Service) registerTools(server *mcp.Server) {
|
||||
// ... existing file operations ...
|
||||
|
||||
// RAG operations
|
||||
s.registerRAGTools(server)
|
||||
|
||||
// Metrics operations
|
||||
s.registerMetricsTools(server)
|
||||
}
|
||||
```
|
||||
|
||||
**Step 5: Run test to verify it passes**
|
||||
|
||||
Run: `go test -run TestMetrics ./pkg/mcp/... -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add pkg/mcp/tools_metrics.go pkg/mcp/tools_metrics_test.go pkg/mcp/mcp.go
|
||||
git commit -m "feat(mcp): add metrics tools (record, query)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Create `core mcp serve` command
|
||||
|
||||
**Files:**
|
||||
- Create: `internal/cmd/mcpcmd/cmd_mcp.go`
|
||||
- Modify: `internal/variants/full.go` (add import)
|
||||
- Test: Manual test via `core mcp serve`
|
||||
|
||||
**Step 1: Create the mcp command package**
|
||||
|
||||
Create `internal/cmd/mcpcmd/cmd_mcp.go`:
|
||||
|
||||
```go
|
||||
package mcpcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"forge.lthn.ai/core/cli/pkg/cli"
|
||||
"forge.lthn.ai/core/cli/pkg/i18n"
|
||||
"forge.lthn.ai/core/cli/pkg/mcp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cli.RegisterCommands(AddMCPCommands)
|
||||
}
|
||||
|
||||
var (
|
||||
mcpWorkspace string
|
||||
)
|
||||
|
||||
var mcpCmd = &cli.Command{
|
||||
Use: "mcp",
|
||||
Short: i18n.T("cmd.mcp.short"),
|
||||
Long: i18n.T("cmd.mcp.long"),
|
||||
}
|
||||
|
||||
var serveCmd = &cli.Command{
|
||||
Use: "serve",
|
||||
Short: i18n.T("cmd.mcp.serve.short"),
|
||||
Long: i18n.T("cmd.mcp.serve.long"),
|
||||
RunE: func(cmd *cli.Command, args []string) error {
|
||||
return runServe()
|
||||
},
|
||||
}
|
||||
|
||||
func AddMCPCommands(root *cli.Command) {
|
||||
initMCPFlags()
|
||||
mcpCmd.AddCommand(serveCmd)
|
||||
root.AddCommand(mcpCmd)
|
||||
}
|
||||
|
||||
func initMCPFlags() {
|
||||
serveCmd.Flags().StringVar(&mcpWorkspace, "workspace", "", i18n.T("cmd.mcp.serve.flag.workspace"))
|
||||
}
|
||||
|
||||
func runServe() error {
|
||||
opts := []mcp.Option{}
|
||||
|
||||
if mcpWorkspace != "" {
|
||||
opts = append(opts, mcp.WithWorkspaceRoot(mcpWorkspace))
|
||||
} else {
|
||||
// Default to unrestricted for MCP server
|
||||
opts = append(opts, mcp.WithWorkspaceRoot(""))
|
||||
}
|
||||
|
||||
svc, err := mcp.New(opts...)
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "create MCP service")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Handle shutdown signals
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigCh
|
||||
cancel()
|
||||
}()
|
||||
|
||||
return svc.Run(ctx)
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Add i18n strings**
|
||||
|
||||
Create or update `pkg/i18n/en.yaml` (if it exists) or add to the existing i18n mechanism:
|
||||
|
||||
```yaml
|
||||
cmd.mcp.short: "MCP (Model Context Protocol) server"
|
||||
cmd.mcp.long: "Start an MCP server for Claude Code integration with file, RAG, and metrics tools."
|
||||
cmd.mcp.serve.short: "Start the MCP server"
|
||||
cmd.mcp.serve.long: "Start the MCP server in stdio mode. Use MCP_ADDR env var for TCP mode."
|
||||
cmd.mcp.serve.flag.workspace: "Restrict file operations to this directory (empty = unrestricted)"
|
||||
```
|
||||
|
||||
**Step 3: Add import to full.go**
|
||||
|
||||
Modify `internal/variants/full.go` to add:
|
||||
|
||||
```go
|
||||
import (
|
||||
// ... existing imports ...
|
||||
_ "forge.lthn.ai/core/cli/internal/cmd/mcpcmd"
|
||||
)
|
||||
```
|
||||
|
||||
**Step 4: Build and test**
|
||||
|
||||
Run: `go build && ./core mcp serve --help`
|
||||
Expected: Help output showing the serve command
|
||||
|
||||
**Step 5: Test MCP server manually**
|
||||
|
||||
Run: `echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | ./core mcp serve`
|
||||
Expected: JSON response listing all tools including rag_query, metrics_record, etc.
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add internal/cmd/mcpcmd/cmd_mcp.go internal/variants/full.go
|
||||
git commit -m "feat: add 'core mcp serve' command"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Configure agentic-flows plugin with .mcp.json
|
||||
|
||||
**Files:**
|
||||
- Create: `/home/shared/hostuk/claude-plugins/plugins/agentic-flows/.mcp.json`
|
||||
- Modify: `/home/shared/hostuk/claude-plugins/plugins/agentic-flows/.claude-plugin/plugin.json` (optional, add mcpServers)
|
||||
|
||||
**Step 1: Create .mcp.json**
|
||||
|
||||
Create `/home/shared/hostuk/claude-plugins/plugins/agentic-flows/.mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"core-cli": {
|
||||
"command": "core",
|
||||
"args": ["mcp", "serve"],
|
||||
"env": {
|
||||
"MCP_WORKSPACE": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Verify plugin loads**
|
||||
|
||||
Restart Claude Code and run `/mcp` to verify the core-cli server appears.
|
||||
|
||||
**Step 3: Test MCP tools**
|
||||
|
||||
Test that tools are available:
|
||||
- `mcp__plugin_agentic-flows_core-cli__rag_query`
|
||||
- `mcp__plugin_agentic-flows_core-cli__rag_ingest`
|
||||
- `mcp__plugin_agentic-flows_core-cli__rag_collections`
|
||||
- `mcp__plugin_agentic-flows_core-cli__metrics_record`
|
||||
- `mcp__plugin_agentic-flows_core-cli__metrics_query`
|
||||
- `mcp__plugin_agentic-flows_core-cli__file_read`
|
||||
- etc.
|
||||
|
||||
**Step 4: Commit plugin changes**
|
||||
|
||||
```bash
|
||||
cd /home/shared/hostuk/claude-plugins
|
||||
git add plugins/agentic-flows/.mcp.json
|
||||
git commit -m "feat(agentic-flows): add MCP server configuration for core-cli"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Update documentation
|
||||
|
||||
**Files:**
|
||||
- Modify: `/home/claude/.claude/projects/-home-claude/memory/MEMORY.md`
|
||||
- Modify: `/home/claude/.claude/projects/-home-claude/memory/plugin-dev-notes.md`
|
||||
|
||||
**Step 1: Update MEMORY.md**
|
||||
|
||||
Add under "Core CLI MCP Server" section:
|
||||
|
||||
```markdown
|
||||
### Core CLI MCP Server
|
||||
- **Command:** `core mcp serve` (stdio mode) or `MCP_ADDR=:9000 core mcp serve` (TCP)
|
||||
- **Tools available:**
|
||||
- File ops: file_read, file_write, file_edit, file_delete, file_rename, file_exists, dir_list, dir_create
|
||||
- RAG: rag_query, rag_ingest, rag_collections
|
||||
- Metrics: metrics_record, metrics_query
|
||||
- Language: lang_detect, lang_list
|
||||
- **Plugin config:** `plugins/agentic-flows/.mcp.json`
|
||||
```
|
||||
|
||||
**Step 2: Update plugin-dev-notes.md**
|
||||
|
||||
Add section:
|
||||
|
||||
```markdown
|
||||
## MCP Server (core mcp serve)
|
||||
|
||||
### Available Tools
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| file_read | Read file contents |
|
||||
| file_write | Write file contents |
|
||||
| file_edit | Edit file (replace string) |
|
||||
| file_delete | Delete file |
|
||||
| file_rename | Rename/move file |
|
||||
| file_exists | Check if file exists |
|
||||
| dir_list | List directory contents |
|
||||
| dir_create | Create directory |
|
||||
| rag_query | Query vector DB |
|
||||
| rag_ingest | Ingest file/directory |
|
||||
| rag_collections | List collections |
|
||||
| metrics_record | Record event |
|
||||
| metrics_query | Query events |
|
||||
| lang_detect | Detect file language |
|
||||
| lang_list | List supported languages |
|
||||
|
||||
### Example .mcp.json
|
||||
```json
|
||||
{
|
||||
"core-cli": {
|
||||
"command": "core",
|
||||
"args": ["mcp", "serve"]
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
**Step 3: Commit documentation**
|
||||
|
||||
```bash
|
||||
git add ~/.claude/projects/-home-claude/memory/*.md
|
||||
git commit -m "docs: update memory with MCP server tools"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Task | Files | Purpose |
|
||||
|------|-------|---------|
|
||||
| 1 | `pkg/mcp/tools_rag.go` | RAG tools (query, ingest, collections) |
|
||||
| 2 | `pkg/mcp/tools_metrics.go` | Metrics tools (record, query) |
|
||||
| 3 | `internal/cmd/mcpcmd/cmd_mcp.go` | `core mcp serve` command |
|
||||
| 4 | `plugins/agentic-flows/.mcp.json` | Plugin MCP configuration |
|
||||
| 5 | Memory docs | Documentation updates |
|
||||
|
||||
## Services Required
|
||||
|
||||
- **Qdrant:** localhost:6333 (verified running)
|
||||
- **Ollama:** localhost:11434 with nomic-embed-text (verified running)
|
||||
- **InfluxDB:** localhost:8086 (optional, for future time-series metrics)
|
||||
82
docs/plans/completed/2026-02-17-lem-chat-design.md
Normal file
82
docs/plans/completed/2026-02-17-lem-chat-design.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# LEM Chat — Web Components Design
|
||||
|
||||
**Date**: 2026-02-17
|
||||
**Status**: Approved
|
||||
|
||||
## Summary
|
||||
|
||||
Standalone chat UI built with vanilla Web Components (Custom Elements + Shadow DOM). Connects to the MLX inference server's OpenAI-compatible SSE streaming endpoint. Zero framework dependencies. Single JS file output, embeddable anywhere.
|
||||
|
||||
## Components
|
||||
|
||||
| Element | Purpose |
|
||||
|---------|---------|
|
||||
| `<lem-chat>` | Container. Conversation state, SSE connection, config via attributes |
|
||||
| `<lem-messages>` | Scrollable message list with auto-scroll anchoring |
|
||||
| `<lem-message>` | Single message bubble. Streams tokens for assistant messages |
|
||||
| `<lem-input>` | Text input, Enter to send, Shift+Enter for newline |
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
User types in <lem-input>
|
||||
→ dispatches 'lem-send' CustomEvent
|
||||
→ <lem-chat> catches it
|
||||
→ adds user message to <lem-messages>
|
||||
→ POST /v1/chat/completions {stream: true, messages: [...history]}
|
||||
→ reads SSE chunks via fetch + ReadableStream
|
||||
→ appends tokens to streaming <lem-message>
|
||||
→ on [DONE], finalises message
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
```html
|
||||
<lem-chat endpoint="http://localhost:8090" model="qwen3-8b"></lem-chat>
|
||||
```
|
||||
|
||||
Attributes: `endpoint`, `model`, `system-prompt`, `max-tokens`, `temperature`
|
||||
|
||||
## Theming
|
||||
|
||||
Shadow DOM with CSS custom properties:
|
||||
|
||||
```css
|
||||
--lem-bg: #1a1a1e;
|
||||
--lem-msg-user: #2a2a3e;
|
||||
--lem-msg-assistant: #1e1e2a;
|
||||
--lem-accent: #5865f2;
|
||||
--lem-text: #e0e0e0;
|
||||
--lem-font: system-ui;
|
||||
```
|
||||
|
||||
## Markdown
|
||||
|
||||
Minimal inline parsing: fenced code blocks, inline code, bold, italic. No library.
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
lem-chat/
|
||||
├── index.html # Demo page
|
||||
├── src/
|
||||
│ ├── lem-chat.ts # Main container + SSE client
|
||||
│ ├── lem-messages.ts # Message list with scroll anchoring
|
||||
│ ├── lem-message.ts # Single message with streaming
|
||||
│ ├── lem-input.ts # Text input
|
||||
│ ├── markdown.ts # Minimal markdown → HTML
|
||||
│ └── styles.ts # CSS template literals
|
||||
├── package.json # typescript + esbuild
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
Build: `esbuild src/lem-chat.ts --bundle --outfile=dist/lem-chat.js`
|
||||
|
||||
## Not in v1
|
||||
|
||||
- Model selection UI
|
||||
- Conversation persistence
|
||||
- File/image upload
|
||||
- Syntax highlighting
|
||||
- Typing indicators
|
||||
- User avatars
|
||||
657
docs/plans/completed/2026-02-20-go-api-design-original.md
Normal file
657
docs/plans/completed/2026-02-20-go-api-design-original.md
Normal file
|
|
@ -0,0 +1,657 @@
|
|||
# go-api Design — HTTP Gateway + OpenAPI SDK Generation
|
||||
|
||||
**Date:** 2026-02-20
|
||||
**Author:** Virgil
|
||||
**Status:** Phase 1 + Phase 2 + Phase 3 Complete (176 tests in go-api)
|
||||
**Module:** `forge.lthn.ai/core/go-api`
|
||||
|
||||
## Problem
|
||||
|
||||
The Core Go ecosystem exposes 42+ tools via MCP (JSON-RPC), which is ideal for AI agents but inaccessible to regular HTTP clients, frontend applications, and third-party integrators. There is no unified HTTP gateway, no OpenAPI specification, and no generated SDKs.
|
||||
|
||||
Both external customers (Host UK products) and Lethean network peers need programmatic access to the same services. The gateway also serves web routes, static assets, and streaming endpoints — not just REST APIs.
|
||||
|
||||
## Solution
|
||||
|
||||
A `go-api` package that acts as the central HTTP gateway:
|
||||
|
||||
1. **Gin-based HTTP gateway** with extensible middleware via gin-contrib plugins
|
||||
2. **RouteGroup interface** that subsystems implement to register their own endpoints (API, web, or both)
|
||||
3. **WebSocket + SSE integration** for real-time streaming
|
||||
4. **OpenAPI 3.1 spec generation** via runtime SpecBuilder (not swaggo annotations)
|
||||
5. **SDK generation pipeline** targeting 11 languages via openapi-generator-cli
|
||||
|
||||
## Architecture
|
||||
|
||||
### Four-Protocol Access
|
||||
|
||||
Same backend services, four client protocols:
|
||||
|
||||
```
|
||||
┌─── REST (go-api) POST /v1/ml/generate → JSON
|
||||
│
|
||||
├─── GraphQL (gqlgen) mutation { mlGenerate(...) { response } }
|
||||
Client ────────────┤
|
||||
├─── WebSocket (go-ws) subscribe ml.generate → streaming
|
||||
│
|
||||
└─── MCP (go-ai) ml_generate → JSON-RPC
|
||||
```
|
||||
|
||||
### Dependency Graph
|
||||
|
||||
```
|
||||
go-api (Gin engine + middleware + OpenAPI)
|
||||
↑ imported by (each registers its own routes)
|
||||
├── go-ai/api/ → /v1/file/*, /v1/process/*, /v1/metrics/*
|
||||
├── go-ml/api/ → /v1/ml/*
|
||||
├── go-rag/api/ → /v1/rag/*
|
||||
├── go-agentic/api/ → /v1/tasks/*
|
||||
├── go-help/api/ → /v1/help/*
|
||||
└── go-ws/api/ → /ws (WebSocket upgrade)
|
||||
```
|
||||
|
||||
go-api has zero internal ecosystem dependencies. Subsystems import go-api, not the other way round.
|
||||
|
||||
### Subsystem Opt-In
|
||||
|
||||
Not every MCP tool becomes a REST endpoint. Each subsystem decides what to expose via a separate `RegisterAPI()` method, independent of MCP's `RegisterTools()`. A subsystem with 15 MCP tools might expose 5 REST endpoints.
|
||||
|
||||
## Package Structure
|
||||
|
||||
```
|
||||
forge.lthn.ai/core/go-api
|
||||
├── api.go # Engine struct, New(), Serve(), Shutdown()
|
||||
├── middleware.go # Auth, CORS, rate limiting, request logging, recovery
|
||||
├── options.go # WithAddr, WithAuth, WithCORS, WithRateLimit, etc.
|
||||
├── group.go # RouteGroup interface + registration
|
||||
├── response.go # Envelope type, error responses, pagination
|
||||
├── docs/ # Generated swagger docs (swaggo output)
|
||||
├── sdk/ # SDK generation tooling / Makefile targets
|
||||
└── go.mod # forge.lthn.ai/core/go-api
|
||||
```
|
||||
|
||||
## Core Interface
|
||||
|
||||
```go
|
||||
// RouteGroup registers API routes onto a Gin router group.
|
||||
// Subsystems implement this to expose their endpoints.
|
||||
type RouteGroup interface {
|
||||
// Name returns the route group identifier (e.g. "ml", "rag", "tasks")
|
||||
Name() string
|
||||
// BasePath returns the URL prefix (e.g. "/v1/ml")
|
||||
BasePath() string
|
||||
// RegisterRoutes adds handlers to the provided router group
|
||||
RegisterRoutes(rg *gin.RouterGroup)
|
||||
}
|
||||
|
||||
// StreamGroup optionally declares WebSocket channels a subsystem publishes to.
|
||||
type StreamGroup interface {
|
||||
Channels() []string
|
||||
}
|
||||
```
|
||||
|
||||
### Subsystem Example (go-ml)
|
||||
|
||||
```go
|
||||
// In go-ml/api/routes.go
|
||||
package api
|
||||
|
||||
type Routes struct {
|
||||
service *ml.Service
|
||||
}
|
||||
|
||||
func NewRoutes(svc *ml.Service) *Routes {
|
||||
return &Routes{service: svc}
|
||||
}
|
||||
|
||||
func (r *Routes) Name() string { return "ml" }
|
||||
func (r *Routes) BasePath() string { return "/v1/ml" }
|
||||
|
||||
func (r *Routes) RegisterRoutes(rg *gin.RouterGroup) {
|
||||
rg.POST("/generate", r.Generate)
|
||||
rg.POST("/score", r.Score)
|
||||
rg.GET("/backends", r.Backends)
|
||||
rg.GET("/status", r.Status)
|
||||
}
|
||||
|
||||
func (r *Routes) Channels() []string {
|
||||
return []string{"ml.generate", "ml.status"}
|
||||
}
|
||||
|
||||
// @Summary Generate text via ML backend
|
||||
// @Tags ml
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param input body MLGenerateInput true "Generation parameters"
|
||||
// @Success 200 {object} Response[MLGenerateOutput]
|
||||
// @Router /v1/ml/generate [post]
|
||||
func (r *Routes) Generate(c *gin.Context) {
|
||||
var input MLGenerateInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(400, api.Fail("invalid_input", err.Error()))
|
||||
return
|
||||
}
|
||||
result, err := r.service.Generate(c.Request.Context(), input.Backend, input.Prompt, ml.GenOpts{
|
||||
Temperature: input.Temperature,
|
||||
MaxTokens: input.MaxTokens,
|
||||
Model: input.Model,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(500, api.Fail("ml.generate_failed", err.Error()))
|
||||
return
|
||||
}
|
||||
c.JSON(200, api.OK(MLGenerateOutput{
|
||||
Response: result,
|
||||
Backend: input.Backend,
|
||||
Model: input.Model,
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
### Engine Wiring (in core CLI)
|
||||
|
||||
```go
|
||||
engine := api.New(
|
||||
api.WithAddr(":8080"),
|
||||
api.WithCORS("*"),
|
||||
api.WithAuth(api.BearerToken(cfg.APIKey)),
|
||||
api.WithRateLimit(100, time.Minute),
|
||||
api.WithWSHub(wsHub),
|
||||
)
|
||||
|
||||
engine.Register(mlapi.NewRoutes(mlService))
|
||||
engine.Register(ragapi.NewRoutes(ragService))
|
||||
engine.Register(agenticapi.NewRoutes(agenticService))
|
||||
|
||||
engine.Serve(ctx) // Blocks until context cancelled
|
||||
```
|
||||
|
||||
## Response Envelope
|
||||
|
||||
All endpoints return a consistent envelope:
|
||||
|
||||
```go
|
||||
type Response[T any] struct {
|
||||
Success bool `json:"success"`
|
||||
Data T `json:"data,omitempty"`
|
||||
Error *Error `json:"error,omitempty"`
|
||||
Meta *Meta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details any `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
type Meta struct {
|
||||
RequestID string `json:"request_id"`
|
||||
Duration string `json:"duration"`
|
||||
Page int `json:"page,omitempty"`
|
||||
PerPage int `json:"per_page,omitempty"`
|
||||
Total int `json:"total,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
Helper functions:
|
||||
|
||||
```go
|
||||
func OK[T any](data T) Response[T]
|
||||
func Fail(code, message string) Response[any]
|
||||
func Paginated[T any](data T, page, perPage, total int) Response[T]
|
||||
```
|
||||
|
||||
## Middleware Stack
|
||||
|
||||
```go
|
||||
api.New(
|
||||
api.WithAddr(":8080"),
|
||||
api.WithCORS(api.CORSConfig{...}), // gin-contrib/cors
|
||||
api.WithAuth(api.BearerToken("...")), // Phase 1: simple bearer token
|
||||
api.WithRateLimit(100, time.Minute), // Per-IP sliding window
|
||||
api.WithRequestID(), // X-Request-ID header generation
|
||||
api.WithRecovery(), // Panic recovery → 500 response
|
||||
api.WithLogger(slog.Default()), // Structured request logging
|
||||
)
|
||||
```
|
||||
|
||||
Auth evolution path: bearer token → API keys → Authentik (OIDC/forward auth). Middleware slot stays the same.
|
||||
|
||||
## WebSocket Integration
|
||||
|
||||
go-api wraps the existing go-ws Hub as a first-class transport:
|
||||
|
||||
```go
|
||||
// Automatic registration:
|
||||
// GET /ws → WebSocket upgrade (go-ws Hub)
|
||||
|
||||
// Client subscribes: {"type":"subscribe","channel":"ml.generate"}
|
||||
// Events arrive: {"type":"event","channel":"ml.generate","data":{...}}
|
||||
// Client unsubscribes: {"type":"unsubscribe","channel":"ml.generate"}
|
||||
```
|
||||
|
||||
Subsystems implementing `StreamGroup` declare which channels they publish to. This metadata feeds into the OpenAPI spec as documentation.
|
||||
|
||||
## OpenAPI + SDK Generation
|
||||
|
||||
### Runtime Spec Generation (SpecBuilder)
|
||||
|
||||
swaggo annotations were rejected because routes are dynamic via RouteGroup, Response[T] generics break swaggo, and MCP tools already carry JSON Schema at runtime. Instead, a `SpecBuilder` constructs the full OpenAPI 3.1 spec from registered RouteGroups at runtime.
|
||||
|
||||
```go
|
||||
// Groups that implement DescribableGroup contribute endpoint metadata
|
||||
type DescribableGroup interface {
|
||||
RouteGroup
|
||||
Describe() []RouteDescription
|
||||
}
|
||||
|
||||
// SpecBuilder assembles the spec from all groups
|
||||
builder := &api.SpecBuilder{Title: "Core API", Description: "...", Version: "1.0.0"}
|
||||
spec, _ := builder.Build(engine.Groups())
|
||||
```
|
||||
|
||||
### MCP-to-REST Bridge (ToolBridge)
|
||||
|
||||
The `ToolBridge` converts MCP tool descriptors into REST POST endpoints and implements both `RouteGroup` and `DescribableGroup`. Each tool becomes `POST /{tool_name}`. Generic types are captured at MCP registration time via closures, enabling JSON unmarshalling to the correct input type at request time.
|
||||
|
||||
```go
|
||||
bridge := api.NewToolBridge("/v1/tools")
|
||||
mcp.BridgeToAPI(mcpService, bridge) // Populates bridge from MCP tool registry
|
||||
engine.Register(bridge) // Registers REST endpoints + OpenAPI metadata
|
||||
```
|
||||
|
||||
### Swagger UI
|
||||
|
||||
```go
|
||||
// Built-in at GET /swagger/*any
|
||||
// SpecBuilder output served via gin-swagger, cached via sync.Once
|
||||
api.New(api.WithSwagger("Core API", "...", "1.0.0"))
|
||||
```
|
||||
|
||||
### SDK Generation
|
||||
|
||||
```bash
|
||||
# Via openapi-generator-cli (11 languages supported)
|
||||
core api sdk --lang go # Generate Go SDK
|
||||
core api sdk --lang typescript-fetch,python # Multiple languages
|
||||
core api sdk --lang rust --output ./sdk/ # Custom output dir
|
||||
```
|
||||
|
||||
### CLI Commands
|
||||
|
||||
```bash
|
||||
core api spec # Emit OpenAPI JSON to stdout
|
||||
core api spec --format yaml # YAML variant
|
||||
core api spec --output spec.json # Write to file
|
||||
core api sdk --lang python # Generate Python SDK
|
||||
core api sdk --lang go,rust # Multiple SDKs
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| `github.com/gin-gonic/gin` | HTTP framework |
|
||||
| `github.com/swaggo/gin-swagger` | Swagger UI middleware |
|
||||
| `github.com/gin-contrib/cors` | CORS middleware |
|
||||
| `github.com/gin-contrib/secure` | Security headers |
|
||||
| `github.com/gin-contrib/sessions` | Server-side sessions |
|
||||
| `github.com/gin-contrib/authz` | Casbin authorisation |
|
||||
| `github.com/gin-contrib/httpsign` | HTTP signature verification |
|
||||
| `github.com/gin-contrib/slog` | Structured request logging |
|
||||
| `github.com/gin-contrib/timeout` | Per-request timeouts |
|
||||
| `github.com/gin-contrib/gzip` | Gzip compression |
|
||||
| `github.com/gin-contrib/static` | Static file serving |
|
||||
| `github.com/gin-contrib/pprof` | Runtime profiling |
|
||||
| `github.com/gin-contrib/expvar` | Runtime metrics |
|
||||
| `github.com/gin-contrib/location/v2` | Reverse proxy detection |
|
||||
| `github.com/99designs/gqlgen` | GraphQL endpoint |
|
||||
| `go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin` | Distributed tracing |
|
||||
| `gopkg.in/yaml.v3` | YAML spec export |
|
||||
| `forge.lthn.ai/core/go-ws` | WebSocket Hub (existing) |
|
||||
|
||||
## Estimated Size
|
||||
|
||||
| Component | LOC |
|
||||
|-----------|-----|
|
||||
| Engine + options | ~200 |
|
||||
| Middleware | ~150 |
|
||||
| Response envelope | ~80 |
|
||||
| RouteGroup interface | ~30 |
|
||||
| WebSocket integration | ~60 |
|
||||
| Tests | ~300 |
|
||||
| **Total go-api** | **~820** |
|
||||
|
||||
Each subsystem's `api/` package adds ~100-200 LOC per route group.
|
||||
|
||||
## Phase 1 — Implemented (20 Feb 2026)
|
||||
|
||||
**Commit:** `17ae945` on Forge (`core/go-api`)
|
||||
|
||||
| Component | Status | Tests |
|
||||
|-----------|--------|-------|
|
||||
| Response envelope (OK, Fail, Paginated) | Done | 9 |
|
||||
| RouteGroup + StreamGroup interfaces | Done | 4 |
|
||||
| Engine (New, Register, Handler, Serve) | Done | 9 |
|
||||
| Bearer auth middleware | Done | 3 |
|
||||
| Request ID middleware | Done | 2 |
|
||||
| CORS middleware (gin-contrib/cors) | Done | 3 |
|
||||
| WebSocket endpoint | Done | 3 |
|
||||
| Swagger UI (gin-swagger) | Done | 2 |
|
||||
| Health endpoint | Done | 1 |
|
||||
| **Total** | **~840 LOC** | **36** |
|
||||
|
||||
**Integration proof:** go-ml/api/ registers 3 endpoints with 12 tests (`0c23858`).
|
||||
|
||||
## Phase 2 Wave 1 — Implemented (20 Feb 2026)
|
||||
|
||||
**Commits:** `6bb7195..daae6f7` on Forge (`core/go-api`)
|
||||
|
||||
| Component | Option | Dependency | Tests |
|
||||
|-----------|--------|------------|-------|
|
||||
| Authentik (forward auth + OIDC) | `WithAuthentik()` | `go-oidc/v3`, `oauth2` | 14 |
|
||||
| Security headers (HSTS, CSP, etc.) | `WithSecure()` | `gin-contrib/secure` | 8 |
|
||||
| Structured request logging | `WithSlog()` | `gin-contrib/slog` | 6 |
|
||||
| Per-request timeouts | `WithTimeout()` | `gin-contrib/timeout` | 5 |
|
||||
| Gzip compression | `WithGzip()` | `gin-contrib/gzip` | 5 |
|
||||
| Static file serving | `WithStatic()` | `gin-contrib/static` | 5 |
|
||||
| **Wave 1 Total** | | | **43** |
|
||||
|
||||
**Cumulative:** 76 tests (36 Phase 1 + 43 Wave 1 - 3 shared), all passing.
|
||||
|
||||
## Phase 2 Wave 2 — Implemented (20 Feb 2026)
|
||||
|
||||
**Commits:** `64a8b16..67dcc83` on Forge (`core/go-api`)
|
||||
|
||||
| Component | Option | Dependency | Tests | Notes |
|
||||
|-----------|--------|------------|-------|-------|
|
||||
| Brotli compression | `WithBrotli()` | `andybalholm/brotli` | 5 | Custom middleware; `gin-contrib/brotli` is empty stub |
|
||||
| Response caching | `WithCache()` | none (in-memory) | 5 | Custom middleware; `gin-contrib/cache` is per-handler, not global |
|
||||
| Server-side sessions | `WithSessions()` | `gin-contrib/sessions` | 5 | Cookie store, configurable name + secret |
|
||||
| Casbin authorisation | `WithAuthz()` | `gin-contrib/authz`, `casbin/v2` | 5 | Subject via Basic Auth; RBAC policy model |
|
||||
| **Wave 2 Total** | | | **20** | |
|
||||
|
||||
**Cumulative:** 102 passing tests (2 integration skipped), all green.
|
||||
|
||||
## Phase 2 Wave 3 — Implemented (20 Feb 2026)
|
||||
|
||||
**Commits:** `7b3f99e..d517fa2` on Forge (`core/go-api`)
|
||||
|
||||
| Component | Option | Dependency | Tests | Notes |
|
||||
|-----------|--------|------------|-------|-------|
|
||||
| HTTP signature verification | `WithHTTPSign()` | `gin-contrib/httpsign` | 5 | HMAC-SHA256; extensible via httpsign.Option |
|
||||
| Server-Sent Events | `WithSSE()` | none (custom SSEBroker) | 6 | Channel filtering, multi-client broadcast, GET /events |
|
||||
| Reverse proxy detection | `WithLocation()` | `gin-contrib/location/v2` | 5 | X-Forwarded-Host/Proto parsing |
|
||||
| Locale detection | `WithI18n()` | `golang.org/x/text/language` | 5 | Accept-Language parsing, message lookup, GetLocale/GetMessage |
|
||||
| GraphQL endpoint | `WithGraphQL()` | `99designs/gqlgen` | 5 | /graphql + optional /graphql/playground |
|
||||
| **Wave 3 Total** | | | **26** | |
|
||||
|
||||
**Cumulative:** 128 passing tests (2 integration skipped), all green.
|
||||
|
||||
## Phase 2 Wave 4 — Implemented (21 Feb 2026)
|
||||
|
||||
**Commits:** `32b3680..8ba1716` on Forge (`core/go-api`)
|
||||
|
||||
| Component | Option | Dependency | Tests | Notes |
|
||||
|-----------|--------|------------|-------|-------|
|
||||
| Runtime profiling | `WithPprof()` | `gin-contrib/pprof` | 5 | /debug/pprof/* endpoints, flag-based mount |
|
||||
| Runtime metrics | `WithExpvar()` | `gin-contrib/expvar` | 5 | /debug/vars endpoint, flag-based mount |
|
||||
| Distributed tracing | `WithTracing()` | `otelgin` + OpenTelemetry SDK | 5 | W3C traceparent propagation, span attributes |
|
||||
| **Wave 4 Total** | | | **15** | |
|
||||
|
||||
**Cumulative:** 143 passing tests (2 integration skipped), all green.
|
||||
|
||||
**Phase 2 complete.** All 4 waves implemented. Every planned plugin has a `With*()` option and tests.
|
||||
|
||||
## Phase 3 — OpenAPI Spec Generation + SDK Codegen (21 Feb 2026)
|
||||
|
||||
**Architecture:** Runtime OpenAPI generation via SpecBuilder (NOT swaggo annotations). Routes are dynamic via RouteGroup, Response[T] generics break swaggo, and MCP tools carry JSON Schema at runtime. A `ToolBridge` converts tool descriptors into RouteGroup + OpenAPI metadata. A `SpecBuilder` constructs the full OpenAPI 3.1 spec. SDK codegen wraps `openapi-generator-cli`.
|
||||
|
||||
### Wave 1: go-api (Tasks 1-5)
|
||||
|
||||
**Commits:** `465bd60..1910aec` on Forge (`core/go-api`)
|
||||
|
||||
| Component | File | Tests | Notes |
|
||||
|-----------|------|-------|-------|
|
||||
| DescribableGroup interface | `group.go` | 5 | Opt-in OpenAPI metadata for RouteGroups |
|
||||
| ToolBridge | `bridge.go` | 6 | Tool descriptors → POST endpoints + DescribableGroup |
|
||||
| SpecBuilder | `openapi.go` | 6 | OpenAPI 3.1 JSON with Response[T] envelope wrapping |
|
||||
| Swagger refactor | `swagger.go` | 5 | Replaced hardcoded empty spec with SpecBuilder |
|
||||
| Spec export | `export.go` | 5 | JSON + YAML export to file/writer |
|
||||
| SDK codegen | `codegen.go` | 5 | 11-language wrapper for openapi-generator-cli |
|
||||
| **Wave 1 Total** | | **32** | |
|
||||
|
||||
### Wave 2: go-ai MCP bridge (Tasks 6-7)
|
||||
|
||||
**Commits:** `2107eda..c37e1cf` on Forge (`core/go-ai`)
|
||||
|
||||
| Component | File | Tests | Notes |
|
||||
|-----------|------|-------|-------|
|
||||
| Tool registry | `mcp/registry.go` | 5 | Generic `addToolRecorded[In,Out]` captures types in closures |
|
||||
| BridgeToAPI | `mcp/bridge.go` | 5 | MCP tools → go-api ToolBridge, 10MB body limit, error classification |
|
||||
| **Wave 2 Total** | | **10** | |
|
||||
|
||||
### Wave 3: CLI commands (Tasks 8-9)
|
||||
|
||||
**Commit:** `d6eec4d` on Forge (`core/cli` dev branch)
|
||||
|
||||
| Component | File | Tests | Notes |
|
||||
|-----------|------|-------|-------|
|
||||
| `core api spec` | `cmd/api/cmd_spec.go` | 2 | JSON/YAML export, --output/--format flags |
|
||||
| `core api sdk` | `cmd/api/cmd_sdk.go` | 2 | --lang (required), --output, --spec, --package flags |
|
||||
| **Wave 3 Total** | | **4** | |
|
||||
|
||||
**Cumulative go-api:** 176 passing tests. **Phase 3 complete.**
|
||||
|
||||
### Known Limitations
|
||||
|
||||
- **Subsystem tools excluded from bridge:** Subsystems call `mcp.AddTool` directly, bypassing `addToolRecorded`. Only the 10 built-in MCP tools appear in the REST bridge. Future: pass `*Service` to `RegisterTools` instead of `*mcp.Server`.
|
||||
- **Flat schema only:** `structSchema` reflection handles flat structs but does not recurse into nested structs. Adequate for current tool inputs.
|
||||
- **CLI spec produces empty bridge:** `core api spec` currently generates a spec with only `/health`. Full MCP integration requires wiring the MCP service into the CLI command.
|
||||
|
||||
## Phase 2 — Gin Plugin Roadmap (Complete)
|
||||
|
||||
All plugins drop in as `With*()` options on the Engine. No architecture changes needed.
|
||||
|
||||
### Security & Auth
|
||||
|
||||
| Plugin | Option | Purpose | Priority |
|
||||
|--------|--------|---------|----------|
|
||||
| ~~**Authentik**~~ | ~~`WithAuthentik()`~~ | ~~OIDC + forward auth integration.~~ | ~~**Done**~~ |
|
||||
| ~~gin-contrib/secure~~ | ~~`WithSecure()`~~ | ~~Security headers: HSTS, X-Frame-Options, X-Content-Type-Options, CSP.~~ | ~~**Done**~~ |
|
||||
| ~~gin-contrib/sessions~~ | ~~`WithSessions()`~~ | ~~Server-side sessions (cookie store). Web session management alongside Authentik tokens.~~ | ~~**Done**~~ |
|
||||
| ~~gin-contrib/authz~~ | ~~`WithAuthz()`~~ | ~~Casbin-based authorisation. Policy-driven access control via RBAC.~~ | ~~**Done**~~ |
|
||||
| ~~gin-contrib/httpsign~~ | ~~`WithHTTPSign()`~~ | ~~HTTP signature verification. HMAC-SHA256 with extensible options.~~ | ~~**Done**~~ |
|
||||
|
||||
### Performance & Reliability
|
||||
|
||||
| Plugin | Option | Purpose | Priority |
|
||||
|--------|--------|---------|----------|
|
||||
| ~~gin-contrib/cache~~ | ~~`WithCache()`~~ | ~~Response caching (in-memory). GET response caching with TTL, lazy eviction.~~ | ~~**Done**~~ |
|
||||
| ~~gin-contrib/timeout~~ | ~~`WithTimeout()`~~ | ~~Per-request timeouts.~~ | ~~**Done**~~ |
|
||||
| ~~gin-contrib/gzip~~ | ~~`WithGzip()`~~ | ~~Gzip response compression.~~ | ~~**Done**~~ |
|
||||
| ~~gin-contrib/brotli~~ | ~~`WithBrotli()`~~ | ~~Brotli compression via `andybalholm/brotli`. Custom middleware (gin-contrib stub empty).~~ | ~~**Done**~~ |
|
||||
|
||||
### Observability
|
||||
|
||||
| Plugin | Option | Purpose | Priority |
|
||||
|--------|--------|---------|----------|
|
||||
| ~~gin-contrib/slog~~ | ~~`WithSlog()`~~ | ~~Structured request logging via slog.~~ | ~~**Done**~~ |
|
||||
| ~~gin-contrib/pprof~~ | ~~`WithPprof()`~~ | ~~Runtime profiling endpoints at /debug/pprof/. Flag-based mount.~~ | ~~**Done**~~ |
|
||||
| ~~gin-contrib/expvar~~ | ~~`WithExpvar()`~~ | ~~Go runtime metrics at /debug/vars. Flag-based mount.~~ | ~~**Done**~~ |
|
||||
| ~~otelgin~~ | ~~`WithTracing()`~~ | ~~OpenTelemetry distributed tracing. W3C traceparent propagation.~~ | ~~**Done**~~ |
|
||||
|
||||
### Content & Streaming
|
||||
|
||||
| Plugin | Option | Purpose | Priority |
|
||||
|--------|--------|---------|----------|
|
||||
| ~~gin-contrib/static~~ | ~~`WithStatic()`~~ | ~~Serve static files.~~ | ~~**Done**~~ |
|
||||
| ~~gin-contrib/sse~~ | ~~`WithSSE()`~~ | ~~Server-Sent Events. Custom SSEBroker with channel filtering, GET /events.~~ | ~~**Done**~~ |
|
||||
| ~~gin-contrib/location~~ | ~~`WithLocation()`~~ | ~~Auto-detect scheme/host from X-Forwarded-* headers.~~ | ~~**Done**~~ |
|
||||
|
||||
### Query Layer
|
||||
|
||||
| Plugin | Option | Purpose | Priority |
|
||||
|--------|--------|---------|----------|
|
||||
| ~~99designs/gqlgen~~ | ~~`WithGraphQL()`~~ | ~~GraphQL endpoint at `/graphql` + optional playground. Accepts gqlgen ExecutableSchema.~~ | ~~**Done**~~ |
|
||||
|
||||
The GraphQL schema can be generated from the same Go Input/Output structs that define the REST endpoints. gqlgen produces an `http.Handler` that mounts directly on Gin. Subsystems opt-in via:
|
||||
|
||||
```go
|
||||
// Subsystems that want GraphQL implement this alongside RouteGroup
|
||||
type ResolverGroup interface {
|
||||
// RegisterResolvers adds query/mutation resolvers to the GraphQL schema
|
||||
RegisterResolvers(schema *graphql.Schema)
|
||||
}
|
||||
```
|
||||
|
||||
This means a subsystem like go-ml exposes:
|
||||
- **REST:** `POST /v1/ml/generate` (existing)
|
||||
- **GraphQL:** `mutation { mlGenerate(prompt: "...", backend: "mlx") { response, model } }` (same handler)
|
||||
- **MCP:** `ml_generate` tool (existing)
|
||||
|
||||
Four protocols, one set of handlers.
|
||||
|
||||
### Ecosystem Integration
|
||||
|
||||
| Plugin | Option | Purpose | Priority |
|
||||
|--------|--------|---------|----------|
|
||||
| ~~gin-contrib/i18n~~ | ~~`WithI18n()`~~ | ~~Locale detection via Accept-Language. Custom middleware using `golang.org/x/text/language`.~~ | ~~**Done**~~ |
|
||||
| [gin-contrib/graceful](https://github.com/gin-contrib/graceful) | — | Already implemented in Engine.Serve(). Could swap to this for more robust lifecycle management if needed. | — |
|
||||
| [gin-contrib/requestid](https://github.com/gin-contrib/requestid) | — | Already implemented. Theirs uses UUID, ours uses hex. Could swap for standards compliance. | — |
|
||||
|
||||
### Implementation Order
|
||||
|
||||
**Wave 1 (gateway hardening):** ~~Authentik, secure, slog, timeout, gzip, static~~ **DONE** (20 Feb 2026)
|
||||
**Wave 2 (performance + auth):** ~~cache, sessions, authz, brotli~~ **DONE** (20 Feb 2026)
|
||||
**Wave 3 (network + streaming):** ~~httpsign, sse, location, i18n, gqlgen~~ **DONE** (20 Feb 2026)
|
||||
**Wave 4 (observability):** ~~pprof, expvar, tracing~~ **DONE** (21 Feb 2026)
|
||||
|
||||
Each wave adds `With*()` options + tests. No breaking changes — existing code continues to work without any new options enabled.
|
||||
|
||||
## Authentik Integration
|
||||
|
||||
[Authentik](https://goauthentik.io/) is the identity provider and edge auth proxy. It handles user registration, login, MFA, social auth, SAML, and OIDC — so go-api doesn't have to.
|
||||
|
||||
### Two Integration Modes
|
||||
|
||||
**1. Forward Auth (web traffic)**
|
||||
|
||||
Traefik sits in front of go-api. For web routes, Traefik's `forwardAuth` middleware checks with Authentik before passing the request through. Authentik handles login flows, session cookies, and consent. go-api receives pre-authenticated requests with identity headers.
|
||||
|
||||
```
|
||||
Browser → Traefik → Authentik (forward auth) → go-api
|
||||
↓
|
||||
Login page (if unauthenticated)
|
||||
```
|
||||
|
||||
go-api reads trusted headers set by Authentik:
|
||||
```
|
||||
X-Authentik-Username: alice
|
||||
X-Authentik-Groups: admins,developers
|
||||
X-Authentik-Email: alice@example.com
|
||||
X-Authentik-Uid: <uuid>
|
||||
X-Authentik-Jwt: <signed token>
|
||||
```
|
||||
|
||||
**2. OIDC Token Validation (API traffic)**
|
||||
|
||||
API clients (SDKs, CLI tools, network peers) authenticate directly with Authentik's OAuth2 token endpoint, then send the JWT to go-api. go-api validates the JWT using Authentik's OIDC discovery endpoint (`.well-known/openid-configuration`).
|
||||
|
||||
```
|
||||
SDK client → Authentik (token endpoint) → receives JWT
|
||||
SDK client → go-api (Authorization: Bearer <jwt>) → validates via OIDC
|
||||
```
|
||||
|
||||
### Implementation in go-api
|
||||
|
||||
```go
|
||||
engine := api.New(
|
||||
api.WithAuthentik(api.AuthentikConfig{
|
||||
Issuer: "https://auth.lthn.ai/application/o/core-api/",
|
||||
ClientID: "core-api",
|
||||
TrustedProxy: true, // Trust X-Authentik-* headers from Traefik
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
`WithAuthentik()` adds middleware that:
|
||||
1. Checks for `X-Authentik-Jwt` header (forward auth mode) — validates signature, extracts claims
|
||||
2. Falls back to `Authorization: Bearer <jwt>` header (direct OIDC mode) — validates via JWKS
|
||||
3. Populates `c.Set("user", AuthentikUser{...})` in the Gin context for handlers to use
|
||||
4. Skips /health, /swagger, and any public paths
|
||||
|
||||
```go
|
||||
// In any handler:
|
||||
func (r *Routes) ListItems(c *gin.Context) {
|
||||
user := api.GetUser(c) // Returns *AuthentikUser or nil
|
||||
if user == nil {
|
||||
c.JSON(401, api.Fail("unauthorised", "Authentication required"))
|
||||
return
|
||||
}
|
||||
// user.Username, user.Groups, user.Email, user.UID available
|
||||
}
|
||||
```
|
||||
|
||||
### Auth Layers
|
||||
|
||||
```
|
||||
Authentik (identity) → WHO is this? (user, groups, email)
|
||||
↓
|
||||
go-api middleware → IS their token valid? (JWT verification)
|
||||
↓
|
||||
Casbin authz (optional) → CAN they do this? (role → endpoint policies)
|
||||
↓
|
||||
Handler → DOES this (business logic)
|
||||
```
|
||||
|
||||
Phase 1 bearer auth continues to work alongside Authentik — useful for service-to-service tokens, CI/CD, and development. `WithBearerAuth` and `WithAuthentik` can coexist.
|
||||
|
||||
### Authentik Deployment
|
||||
|
||||
Authentik runs as a Docker service alongside go-api, fronted by Traefik:
|
||||
- **auth.lthn.ai** — Authentik UI + OIDC endpoints (production)
|
||||
- **auth.leth.in** — Authentik for devnet/testnet
|
||||
- Traefik routes `/outpost.goauthentik.io/` to Authentik's embedded outpost for forward auth
|
||||
|
||||
### Dependencies
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| `github.com/coreos/go-oidc/v3` | OIDC discovery + JWT validation |
|
||||
| `golang.org/x/oauth2` | OAuth2 token exchange (for server-side flows) |
|
||||
|
||||
Both are standard Go libraries with no heavy dependencies.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- gRPC gateway
|
||||
- Built-in user registration/login (Authentik handles this)
|
||||
- API versioning beyond /v1/ prefix
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Phase 1 (Done)
|
||||
|
||||
1. ~~`core api serve` starts a Gin server with registered subsystem routes~~
|
||||
2. ~~WebSocket subscriptions work alongside REST~~
|
||||
3. ~~Swagger UI accessible at `/swagger/`~~
|
||||
4. ~~All endpoints return consistent Response envelope~~
|
||||
5. ~~Bearer token auth protects all routes~~
|
||||
6. ~~First subsystem integration (go-ml/api/) proves the pattern~~
|
||||
|
||||
### Phase 2 (Done)
|
||||
|
||||
7. ~~Security headers, compression, and caching active in production~~
|
||||
8. ~~Session-based auth alongside bearer tokens~~
|
||||
9. ~~HTTP signature verification for Lethean network peers~~
|
||||
10. ~~Static file serving for docs site and SDK downloads~~
|
||||
11. ~~GraphQL endpoint at `/graphql` with playground~~
|
||||
|
||||
### Phase 3 (Done)
|
||||
|
||||
12. ~~`core api spec` emits valid OpenAPI 3.1 JSON via runtime SpecBuilder~~
|
||||
13. ~~`core api sdk` generates SDKs for 11 languages via openapi-generator-cli~~
|
||||
14. ~~MCP tools bridged to REST endpoints via ToolBridge + BridgeToAPI~~
|
||||
15. ~~OpenAPI spec includes Response[T] envelope wrapping~~
|
||||
16. ~~Spec export to file in JSON and YAML formats~~
|
||||
1503
docs/plans/completed/2026-02-20-go-api-plan-original.md
Normal file
1503
docs/plans/completed/2026-02-20-go-api-plan-original.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,128 @@
|
|||
# CLI Meta-Package Restructure — Design
|
||||
|
||||
**Goal:** Transform `core/cli` from a 35K LOC monolith into a thin assembly repo that ships variant binaries. Domain repos own their commands. `go/pkg/cli` is the only import any domain package needs for CLI concerns.
|
||||
|
||||
**Architecture:** Commands register as framework services via `cli.WithCommands()`, passed to `cli.Main()`. Command code lives in the domain repos that own the business logic. The cli repo is a thin `main.go` that wires them together.
|
||||
|
||||
**Tech Stack:** go/pkg/cli (wraps cobra + charmbracelet), Core framework lifecycle, Taskfile
|
||||
|
||||
---
|
||||
|
||||
## 1. CLI SDK — The Single Import
|
||||
|
||||
`forge.lthn.ai/core/go/pkg/cli` is the **only** import domain packages use for CLI concerns. It wraps cobra, charmbracelet, and stdlib behind a stable API. If the underlying libraries change, only `go/pkg/cli` is touched — every domain repo is insulated.
|
||||
|
||||
### Already done
|
||||
|
||||
- **Cobra:** `Command` type alias, `NewCommand()`, `NewGroup()`, `NewRun()`, flag helpers (`StringFlag`, `BoolFlag`, `IntFlag`, `StringSliceFlag`), arg validators
|
||||
- **Output:** `Success()`, `Error()`, `Warn()`, `Info()`, `Table`, `Section()`, `Label()`, `Task()`, `Hint()`
|
||||
- **Prompts:** `Confirm()`, `Question()`, `Choose()`, `ChooseMulti()` with grammar-based action variants
|
||||
- **Styles:** 17 pre-built styles, `AnsiStyle` builder, Tailwind colour constants (47 hex values)
|
||||
- **Glyphs:** `:check:`, `:cross:`, `:warn:` etc. with Unicode/Emoji/ASCII themes
|
||||
- **Layout:** HLCRF composite renderer (Header/Left/Content/Right/Footer)
|
||||
- **Errors:** `Wrap()`, `WrapVerb()`, `ExitError`, `Is()`, `As()`
|
||||
- **Logging:** `LogDebug()`, `LogInfo()`, `LogWarn()`, `LogError()`, `LogSecurity()`
|
||||
- **TUI primitives:** `Spinner`, `ProgressBar`, `InteractiveList`, `TextInput`, `Viewport`, `RunTUI`
|
||||
- **Command registration:** `WithCommands(name, fn)` — registers commands as framework services
|
||||
|
||||
### Stubbed for later (interface exists, returns simple fallback)
|
||||
|
||||
- `Form(fields []FormField) (map[string]string, error)` — multi-field form (backed by huh later)
|
||||
- `FilePicker(opts ...FilePickerOption) (string, error)` — file browser
|
||||
- `Tabs(items []TabItem) error` — tabbed content panes
|
||||
|
||||
### Rule
|
||||
|
||||
Domain packages import `forge.lthn.ai/core/go/pkg/cli` and **nothing else** for CLI concerns. No `cobra`, no `lipgloss`, no `bubbletea`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Command Registration — Framework Lifecycle
|
||||
|
||||
Commands register through the Core framework's service lifecycle, not through global state or `init()` functions.
|
||||
|
||||
### The contract
|
||||
|
||||
Each domain repo exports an `Add*Commands(root *cli.Command)` function. The CLI binary wires it in via `cli.WithCommands()`:
|
||||
|
||||
```go
|
||||
// go-ai/cmd/daemon/cmd.go
|
||||
package daemon
|
||||
|
||||
import "forge.lthn.ai/core/go/pkg/cli"
|
||||
|
||||
// AddDaemonCommand adds the 'daemon' command group to the root.
|
||||
func AddDaemonCommand(root *cli.Command) {
|
||||
daemonCmd := cli.NewGroup("daemon", "Manage the core daemon", "")
|
||||
root.AddCommand(daemonCmd)
|
||||
// subcommands...
|
||||
}
|
||||
```
|
||||
|
||||
No `init()`. No blank imports. No `cli.RegisterCommands()`.
|
||||
|
||||
### How it works
|
||||
|
||||
`cli.WithCommands(name, fn)` wraps the registration function as a framework service implementing `Startable`. During `Core.ServiceStartup()`, the service's `OnStartup()` casts `Core.App` to `*cobra.Command` and calls the registration function. Core services (i18n, log, workspace) start first since they're registered before command services.
|
||||
|
||||
```go
|
||||
// cli/main.go
|
||||
func main() {
|
||||
cli.Main(
|
||||
cli.WithCommands("config", config.AddConfigCommands),
|
||||
cli.WithCommands("doctor", doctor.AddDoctorCommands),
|
||||
// ...
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Migration status (completed)
|
||||
|
||||
| Source | Destination | Status |
|
||||
|--------|-------------|--------|
|
||||
| `cmd/dev, setup, qa, docs, gitcmd, monitor` | `go-devops/cmd/` | Done |
|
||||
| `cmd/lab` | `go-ai/cmd/` | Done |
|
||||
| `cmd/workspace` | `go-agentic/cmd/` | Done |
|
||||
| `cmd/go` | `core/go/cmd/gocmd` | Done |
|
||||
| `cmd/vanity-import, community` | `go-devops/cmd/` | Done |
|
||||
| `cmd/updater` | `go-update` | Done (own repo) |
|
||||
| `cmd/daemon, mcpcmd, security` | `go-ai/cmd/` | Done |
|
||||
| `cmd/crypt` | `go-crypt/cmd/` | Done |
|
||||
| `cmd/rag` | `go-rag/cmd/` | Done |
|
||||
| `cmd/unifi` | `go-netops/cmd/` | Done |
|
||||
| `cmd/api` | `go-api/cmd/` | Done |
|
||||
| `cmd/collect, forge, gitea` | `go-scm/cmd/` | Done |
|
||||
| `cmd/deploy, prod, vm` | `go-devops/cmd/` | Done |
|
||||
|
||||
### Stays in cli/ (meta/framework commands)
|
||||
|
||||
`config`, `doctor`, `help`, `module`, `pkgcmd`, `plugin`, `session`
|
||||
|
||||
---
|
||||
|
||||
## 3. Variant Binaries (future)
|
||||
|
||||
The cli/ repo can produce variant binaries by creating multiple `main.go` files that wire different sets of commands.
|
||||
|
||||
```
|
||||
cli/
|
||||
├── main.go # Current — meta commands only
|
||||
├── cmd/core-full/main.go # Full CLI — all ecosystem commands
|
||||
├── cmd/core-ci/main.go # CI agent dispatch + SCM
|
||||
├── cmd/core-mlx/main.go # ML inference subprocess
|
||||
└── cmd/core-ops/main.go # DevOps + infra management
|
||||
```
|
||||
|
||||
Each variant calls `cli.Main()` with its specific `cli.WithCommands()` set. No blank imports needed.
|
||||
|
||||
### Why variants matter
|
||||
|
||||
- `core-mlx` ships to the homelab as a ~10MB binary, not 50MB with devops/forge/netops
|
||||
- `core-ci` deploys to agent machines without ML or CGO dependencies
|
||||
- Adding a new variant = one new `main.go` with the right `WithCommands` calls
|
||||
|
||||
---
|
||||
|
||||
## 4. Current State
|
||||
|
||||
cli/ has 7 meta packages, one `main.go`, and zero business logic. Everything else lives in the domain repos that own it. Total cli/ LOC is ~2K.
|
||||
1724
docs/plans/completed/2026-02-21-cli-sdk-expansion-plan-original.md
Normal file
1724
docs/plans/completed/2026-02-21-cli-sdk-expansion-plan-original.md
Normal file
File diff suppressed because it is too large
Load diff
286
docs/plans/completed/2026-02-21-go-forge-design.md
Normal file
286
docs/plans/completed/2026-02-21-go-forge-design.md
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
# go-forge Design Document
|
||||
|
||||
## Overview
|
||||
|
||||
**go-forge** is a full-coverage Go client for the Forgejo API (450 endpoints, 284 paths, 229 types). It uses a generic `Resource[T, C, U]` pattern for CRUD operations (91% of endpoints) and hand-written methods for 39 unique action endpoints. Types are generated from Forgejo's `swagger.v1.json` spec.
|
||||
|
||||
**Module path:** `forge.lthn.ai/core/go-forge`
|
||||
|
||||
**Origin:** Extracted from `go-scm/forge/` (45 methods covering 10% of API), expanded to full coverage.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
forge.lthn.ai/core/go-forge
|
||||
├── client.go # HTTP client: auth, headers, rate limiting, context.Context
|
||||
├── pagination.go # Generic paginated request helper
|
||||
├── resource.go # Resource[T, C, U] generic CRUD (List/Get/Create/Update/Delete)
|
||||
├── errors.go # Typed error handling (APIError, NotFound, Forbidden, etc.)
|
||||
├── forge.go # Top-level Forge client aggregating all services
|
||||
│
|
||||
├── types/ # Generated from swagger.v1.json
|
||||
│ ├── generate.go # //go:generate directive
|
||||
│ ├── repo.go # Repository, CreateRepoOption, EditRepoOption
|
||||
│ ├── issue.go # Issue, CreateIssueOption, EditIssueOption
|
||||
│ ├── pr.go # PullRequest, CreatePullRequestOption
|
||||
│ ├── user.go # User, CreateUserOption
|
||||
│ ├── org.go # Organisation, CreateOrgOption
|
||||
│ ├── team.go # Team, CreateTeamOption
|
||||
│ ├── label.go # Label, CreateLabelOption
|
||||
│ ├── release.go # Release, CreateReleaseOption
|
||||
│ ├── branch.go # Branch, BranchProtection
|
||||
│ ├── milestone.go # Milestone, CreateMilestoneOption
|
||||
│ ├── hook.go # Hook, CreateHookOption
|
||||
│ ├── key.go # DeployKey, PublicKey, GPGKey
|
||||
│ ├── notification.go # NotificationThread, NotificationSubject
|
||||
│ ├── package.go # Package, PackageFile
|
||||
│ ├── action.go # ActionRunner, ActionSecret, ActionVariable
|
||||
│ ├── commit.go # Commit, CommitStatus, CombinedStatus
|
||||
│ ├── content.go # ContentsResponse, FileOptions
|
||||
│ ├── wiki.go # WikiPage, WikiPageMetaData
|
||||
│ ├── review.go # PullReview, PullReviewComment
|
||||
│ ├── reaction.go # Reaction
|
||||
│ ├── topic.go # TopicResponse
|
||||
│ ├── misc.go # Markdown, License, GitignoreTemplate, NodeInfo
|
||||
│ ├── admin.go # Cron, QuotaGroup, QuotaRule
|
||||
│ ├── activity.go # Activity, Feed
|
||||
│ └── common.go # Shared types: Permission, ExternalTracker, etc.
|
||||
│
|
||||
├── repos.go # RepoService: CRUD + fork, mirror, transfer, template
|
||||
├── issues.go # IssueService: CRUD + pin, deadline, reactions, stopwatch
|
||||
├── pulls.go # PullService: CRUD + merge, update, reviews, dismiss
|
||||
├── orgs.go # OrgService: CRUD + members, avatar, block, hooks
|
||||
├── users.go # UserService: CRUD + keys, followers, starred, settings
|
||||
├── teams.go # TeamService: CRUD + members, repos
|
||||
├── admin.go # AdminService: users, orgs, cron, runners, quota, unadopted
|
||||
├── branches.go # BranchService: CRUD + protection rules
|
||||
├── releases.go # ReleaseService: CRUD + assets
|
||||
├── labels.go # LabelService: repo + org + issue labels
|
||||
├── webhooks.go # WebhookService: CRUD + test hook
|
||||
├── notifications.go # NotificationService: list, mark read
|
||||
├── packages.go # PackageService: list, get, delete
|
||||
├── actions.go # ActionsService: runners, secrets, variables, workflow dispatch
|
||||
├── contents.go # ContentService: file read/write/delete via API
|
||||
├── wiki.go # WikiService: pages
|
||||
├── commits.go # CommitService: status, notes, diff
|
||||
├── misc.go # MiscService: markdown, licenses, gitignore, nodeinfo
|
||||
│
|
||||
├── config.go # URL/token resolution: env → config file → flags
|
||||
│
|
||||
├── cmd/forgegen/ # Code generator: swagger.v1.json → types/*.go
|
||||
│ ├── main.go
|
||||
│ ├── parser.go # Parse OpenAPI 2.0 definitions
|
||||
│ ├── generator.go # Render Go source files
|
||||
│ └── templates/ # Go text/template files for codegen
|
||||
│
|
||||
└── testdata/
|
||||
└── swagger.v1.json # Pinned spec for testing + generation
|
||||
```
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### 1. Generic Resource[T, C, U]
|
||||
|
||||
Three type parameters: T (resource type), C (create options), U (update options).
|
||||
|
||||
```go
|
||||
type Resource[T any, C any, U any] struct {
|
||||
client *Client
|
||||
path string // e.g. "/api/v1/repos/{owner}/{repo}/issues"
|
||||
}
|
||||
|
||||
func (r *Resource[T, C, U]) List(ctx context.Context, params Params, opts ListOptions) ([]T, error)
|
||||
func (r *Resource[T, C, U]) Get(ctx context.Context, params Params, id string) (*T, error)
|
||||
func (r *Resource[T, C, U]) Create(ctx context.Context, params Params, body *C) (*T, error)
|
||||
func (r *Resource[T, C, U]) Update(ctx context.Context, params Params, id string, body *U) (*T, error)
|
||||
func (r *Resource[T, C, U]) Delete(ctx context.Context, params Params, id string) error
|
||||
```
|
||||
|
||||
`Params` is `map[string]string` resolving path variables: `{"owner": "core", "repo": "go-forge"}`.
|
||||
|
||||
This covers 411 of 450 endpoints (91%).
|
||||
|
||||
### 2. Service Structs Embed Resource
|
||||
|
||||
```go
|
||||
type IssueService struct {
|
||||
Resource[types.Issue, types.CreateIssueOption, types.EditIssueOption]
|
||||
}
|
||||
|
||||
// CRUD comes free. Actions are hand-written:
|
||||
func (s *IssueService) Pin(ctx context.Context, owner, repo string, index int64) error
|
||||
func (s *IssueService) SetDeadline(ctx context.Context, owner, repo string, index int64, deadline *time.Time) error
|
||||
```
|
||||
|
||||
### 3. Top-Level Forge Client
|
||||
|
||||
```go
|
||||
type Forge struct {
|
||||
client *Client
|
||||
Repos *RepoService
|
||||
Issues *IssueService
|
||||
Pulls *PullService
|
||||
Orgs *OrgService
|
||||
Users *UserService
|
||||
Teams *TeamService
|
||||
Admin *AdminService
|
||||
Branches *BranchService
|
||||
Releases *ReleaseService
|
||||
Labels *LabelService
|
||||
Webhooks *WebhookService
|
||||
Notifications *NotificationService
|
||||
Packages *PackageService
|
||||
Actions *ActionsService
|
||||
Contents *ContentService
|
||||
Wiki *WikiService
|
||||
Commits *CommitService
|
||||
Misc *MiscService
|
||||
}
|
||||
|
||||
func NewForge(url, token string, opts ...Option) *Forge
|
||||
```
|
||||
|
||||
### 4. Codegen from swagger.v1.json
|
||||
|
||||
The `cmd/forgegen/` tool reads the OpenAPI 2.0 spec and generates:
|
||||
- Go struct definitions with JSON tags and doc comments
|
||||
- Enum constants
|
||||
- Type mapping (OpenAPI → Go)
|
||||
|
||||
229 type definitions → ~25 grouped Go files in `types/`.
|
||||
|
||||
Type mapping rules:
|
||||
| OpenAPI | Go |
|
||||
|---------|-----|
|
||||
| `string` | `string` |
|
||||
| `string` + `date-time` | `time.Time` |
|
||||
| `integer` + `int64` | `int64` |
|
||||
| `integer` | `int` |
|
||||
| `boolean` | `bool` |
|
||||
| `array` of T | `[]T` |
|
||||
| `$ref` | `*T` (pointer) |
|
||||
| nullable | pointer type |
|
||||
| `binary` | `[]byte` |
|
||||
|
||||
### 5. HTTP Client
|
||||
|
||||
```go
|
||||
type Client struct {
|
||||
baseURL string
|
||||
token string
|
||||
httpClient *http.Client
|
||||
userAgent string
|
||||
}
|
||||
|
||||
func New(url, token string, opts ...Option) *Client
|
||||
|
||||
func (c *Client) Get(ctx context.Context, path string, out any) error
|
||||
func (c *Client) Post(ctx context.Context, path string, body, out any) error
|
||||
func (c *Client) Patch(ctx context.Context, path string, body, out any) error
|
||||
func (c *Client) Put(ctx context.Context, path string, body, out any) error
|
||||
func (c *Client) Delete(ctx context.Context, path string) error
|
||||
```
|
||||
|
||||
Options: `WithHTTPClient`, `WithUserAgent`, `WithRateLimit`, `WithLogger`.
|
||||
|
||||
### 6. Pagination
|
||||
|
||||
Forgejo uses `page` + `limit` query params and `X-Total-Count` response header.
|
||||
|
||||
```go
|
||||
type ListOptions struct {
|
||||
Page int
|
||||
Limit int // default 50, max configurable
|
||||
}
|
||||
|
||||
type PagedResult[T any] struct {
|
||||
Items []T
|
||||
TotalCount int
|
||||
Page int
|
||||
HasMore bool
|
||||
}
|
||||
|
||||
// ListAll fetches all pages automatically.
|
||||
func (r *Resource[T, C, U]) ListAll(ctx context.Context, params Params) ([]T, error)
|
||||
```
|
||||
|
||||
### 7. Error Handling
|
||||
|
||||
```go
|
||||
type APIError struct {
|
||||
StatusCode int
|
||||
Message string
|
||||
URL string
|
||||
}
|
||||
|
||||
func IsNotFound(err error) bool
|
||||
func IsForbidden(err error) bool
|
||||
func IsConflict(err error) bool
|
||||
```
|
||||
|
||||
### 8. Config Resolution (from go-scm/forge)
|
||||
|
||||
Priority: flags → environment → config file.
|
||||
|
||||
```go
|
||||
func NewFromConfig(flagURL, flagToken string) (*Forge, error)
|
||||
func ResolveConfig(flagURL, flagToken string) (url, token string, err error)
|
||||
func SaveConfig(url, token string) error
|
||||
```
|
||||
|
||||
Env vars: `FORGE_URL`, `FORGE_TOKEN`. Config file: `~/.config/forge/config.json`.
|
||||
|
||||
## API Coverage
|
||||
|
||||
| Category | Endpoints | CRUD | Actions |
|
||||
|----------|-----------|------|---------|
|
||||
| Repository | 175 | 165 | 10 (fork, mirror, transfer, template, avatar, diffpatch) |
|
||||
| User | 74 | 70 | 4 (avatar, GPG verify) |
|
||||
| Issue | 67 | 57 | 10 (pin, deadline, reactions, stopwatch, labels) |
|
||||
| Organisation | 63 | 59 | 4 (avatar, block/unblock) |
|
||||
| Admin | 39 | 35 | 4 (cron run, rename, adopt, quota set) |
|
||||
| Miscellaneous | 12 | 7 | 5 (markdown render, markup, nodeinfo) |
|
||||
| Notification | 7 | 7 | 0 |
|
||||
| ActivityPub | 6 | 3 | 3 (inbox POST) |
|
||||
| Package | 4 | 4 | 0 |
|
||||
| Settings | 4 | 4 | 0 |
|
||||
| **Total** | **450** | **411** | **39** |
|
||||
|
||||
## Integration Points
|
||||
|
||||
### go-api
|
||||
|
||||
Services implement `DescribableGroup` from go-api Phase 3, enabling:
|
||||
- REST endpoint generation via ToolBridge
|
||||
- Auto-generated OpenAPI spec
|
||||
- Multi-language SDK codegen
|
||||
|
||||
### go-scm
|
||||
|
||||
go-scm/forge/ becomes a thin adapter importing go-forge types. Existing go-scm users are unaffected — the multi-provider abstraction layer stays.
|
||||
|
||||
### go-ai/mcp
|
||||
|
||||
The MCP subsystem can register go-forge operations as MCP tools, giving AI agents full Forgejo API access.
|
||||
|
||||
## 39 Unique Action Methods
|
||||
|
||||
These require hand-written implementation:
|
||||
|
||||
**Repository:** migrate, fork, generate (template), transfer, accept/reject transfer, mirror sync, push mirror sync, avatar, diffpatch, contents (multi-file modify)
|
||||
|
||||
**Pull Requests:** merge, update (rebase), submit review, dismiss/undismiss review
|
||||
|
||||
**Issues:** pin, set deadline, add reaction, start/stop stopwatch, add issue labels
|
||||
|
||||
**Comments:** add reaction
|
||||
|
||||
**Admin:** run cron task, adopt unadopted, rename user, set quota groups
|
||||
|
||||
**Misc:** render markdown, render raw markdown, render markup, GPG key verify
|
||||
|
||||
**ActivityPub:** inbox POST (actor, repo, user)
|
||||
|
||||
**Actions:** dispatch workflow
|
||||
|
||||
**Git:** set note on commit, test webhook
|
||||
2549
docs/plans/completed/2026-02-21-go-forge-plan.md
Normal file
2549
docs/plans/completed/2026-02-21-go-forge-plan.md
Normal file
File diff suppressed because it is too large
Load diff
30
docs/plans/completed/cli-meta-package.md
Normal file
30
docs/plans/completed/cli-meta-package.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# CLI Meta-Package Restructure — Completed
|
||||
|
||||
**Completed:** 22 Feb 2026
|
||||
|
||||
## What Was Done
|
||||
|
||||
`pkg/cli` was extracted from `core/go` into its own Go module at `forge.lthn.ai/core/cli`. This made the CLI SDK a first-class, independently versioned package rather than a subdirectory of the Go foundation repo.
|
||||
|
||||
Following the extraction, an ecosystem-wide import path migration updated all consumers from the old path to the new one:
|
||||
|
||||
- Old: `forge.lthn.ai/core/go/pkg/cli`
|
||||
- New: `forge.lthn.ai/core/cli/pkg/cli`
|
||||
|
||||
## Scope
|
||||
|
||||
- **147+ files** updated across **10 repos**
|
||||
- All repos build clean after migration
|
||||
|
||||
## Repos Migrated
|
||||
|
||||
`core/cli`, `core/go`, `go-devops`, `go-ai`, `go-agentic`, `go-crypt`, `go-rag`, `go-scm`, `go-api`, `go-update`
|
||||
|
||||
## Key Outcomes
|
||||
|
||||
- `forge.lthn.ai/core/cli/pkg/cli` is the single import for all CLI concerns across the ecosystem
|
||||
- Domain repos are insulated from cobra, lipgloss, and bubbletea — only `pkg/cli` imports them
|
||||
- Command registration uses the Core framework lifecycle via `cli.WithCommands()` — no `init()`, no global state
|
||||
- `core/cli` is a thin assembly repo (~2K LOC) with 7 meta packages; all business logic lives in domain repos
|
||||
- Variant binary pattern established: multiple `main.go` files can wire different `WithCommands` sets for targeted binaries (core-ci, core-mlx, core-ops, etc.)
|
||||
- Command migration from the old `core/cli` monolith to domain repos was completed in full (13 command groups moved)
|
||||
39
docs/plans/completed/cli-sdk-expansion.md
Normal file
39
docs/plans/completed/cli-sdk-expansion.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# CLI SDK Expansion — Completion Summary
|
||||
|
||||
**Completed:** 21 February 2026
|
||||
**Module:** `forge.lthn.ai/core/go/pkg/cli` (later migrated to `forge.lthn.ai/core/cli`)
|
||||
**Status:** Complete — all TUI primitives shipped, then extracted to core/cli
|
||||
|
||||
## What Was Built
|
||||
|
||||
Extended `pkg/cli` with charmbracelet TUI primitives so domain repos only
|
||||
import `core/cli` for all CLI concerns. Charmbracelet dependencies (bubbletea,
|
||||
bubbles, lipgloss) are encapsulated behind our own types.
|
||||
|
||||
### Components added
|
||||
|
||||
| Component | File | Purpose |
|
||||
|-----------|------|---------|
|
||||
| RunTUI | `runtui.go` | Escape hatch with `Model`/`Msg`/`Cmd`/`KeyMsg` types |
|
||||
| Spinner | `spinner.go` | Async handle with `Update()`, `Done()`, `Fail()` |
|
||||
| ProgressBar | `progressbar.go` | `Increment()`, `Set()`, `SetMessage()`, `Done()` |
|
||||
| InteractiveList | `list.go` | Keyboard navigation with terminal fallback |
|
||||
| TextInput | `textinput.go` | Placeholder, masking, validation |
|
||||
| Viewport | `viewport.go` | Scrollable content for logs, diffs, docs |
|
||||
| Form (stub) | `form.go` | Interface defined, bufio fallback |
|
||||
| FilePicker (stub) | `filepicker.go` | Interface defined, bufio fallback |
|
||||
| Tabs (stub) | `tabs.go` | Interface defined, simple fallback |
|
||||
|
||||
### Subsequent migration
|
||||
|
||||
On 22 February 2026, `pkg/cli` was extracted from `core/go` into its own
|
||||
module at `forge.lthn.ai/core/cli` and all imports were updated. The TUI
|
||||
primitives now live in the standalone CLI module.
|
||||
|
||||
### Frame upgrade (follow-on)
|
||||
|
||||
The Frame layout system was upgraded to implement `tea.Model` directly on
|
||||
22 February 2026 (in `core/cli`), adding bubbletea lifecycle, `KeyMap` for
|
||||
configurable bindings, `Navigate()`/`Back()` for panel switching, and
|
||||
lipgloss-based HLCRF rendering. This was a separate plan
|
||||
(`frame-bubbletea`) that built on the SDK expansion.
|
||||
57
docs/plans/completed/go-api.md
Normal file
57
docs/plans/completed/go-api.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# go-api — Completion Summary
|
||||
|
||||
**Completed:** 21 February 2026
|
||||
**Module:** `forge.lthn.ai/core/go-api`
|
||||
**Status:** Phases 1–3 complete, 176 tests passing
|
||||
|
||||
## What Was Built
|
||||
|
||||
### Phase 1 — Core Framework (20 Feb 2026)
|
||||
|
||||
Gin-based HTTP engine with extensible middleware via `With*()` options. Key components:
|
||||
|
||||
- `RouteGroup` / `StreamGroup` interfaces — subsystems register their own endpoints
|
||||
- `Response[T]` envelope — `OK()`, `Fail()`, `Paginated()` generics
|
||||
- `Engine` — `New()`, `Register()`, `Handler()`, `Serve()` with graceful shutdown
|
||||
- Bearer auth, request ID, and CORS middleware
|
||||
- WebSocket endpoint wrapping a `go-ws` Hub
|
||||
- Swagger UI at `/swagger/` with runtime spec serving
|
||||
- `/health` endpoint always available without auth
|
||||
- First integration proof in `go-ml/api/` (3 endpoints, 12 tests)
|
||||
|
||||
### Phase 2 — Gin Plugin Stack (20–21 Feb 2026)
|
||||
|
||||
17 middleware plugins added across four waves, all as drop-in `With*()` options:
|
||||
|
||||
| Wave | Plugins |
|
||||
|------|---------|
|
||||
| 1 — Gateway hardening | Authentik (OIDC + forward auth), secure headers, structured slog, timeouts, gzip, static files |
|
||||
| 2 — Performance + auth | Brotli compression, in-memory response cache, server-side sessions, Casbin RBAC |
|
||||
| 3 — Network + streaming | HTTP signature verification, SSE broker, reverse proxy detection, i18n locale, GraphQL |
|
||||
| 4 — Observability | pprof, expvar, OpenTelemetry distributed tracing |
|
||||
|
||||
### Phase 3 — OpenAPI + SDK Codegen (21 Feb 2026)
|
||||
|
||||
Runtime spec generation (not swaggo annotations — incompatible with dynamic RouteGroups and `Response[T]` generics):
|
||||
|
||||
- `DescribableGroup` interface — opt-in OpenAPI metadata for route groups
|
||||
- `ToolBridge` — converts MCP tool descriptors into `POST /{tool_name}` REST endpoints
|
||||
- `SpecBuilder` — assembles full OpenAPI 3.1 JSON from registered groups at runtime
|
||||
- Spec export to JSON and YAML (`core api spec`)
|
||||
- SDK codegen wrapper for openapi-generator-cli, 11 languages (`core api sdk --lang go`)
|
||||
- `go-ai` `mcp/registry.go` — generic `addToolRecorded[In,Out]` captures types in closures
|
||||
- `go-ai` `mcp/bridge.go` — `BridgeToAPI()` populates ToolBridge from MCP tool registry
|
||||
- CLI commands: `core api spec`, `core api sdk` (in `core/cli` dev branch)
|
||||
|
||||
## Key Outcomes
|
||||
|
||||
- **176 tests** across go-api (143), go-ai bridge (10), and CLI commands (4), all passing
|
||||
- Zero internal ecosystem dependencies — subsystems import go-api, not the reverse
|
||||
- Authentik (OIDC) and bearer token auth coexist; Casbin adds RBAC on top
|
||||
- Four-protocol access pattern established: REST, GraphQL, WebSocket, MCP — same handlers
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Subsystem MCP tools registered via `mcp.AddTool` directly are excluded from the REST bridge (only the 10 built-in tools appear). Fix: pass `*Service` to `RegisterTools` instead of `*mcp.Server`.
|
||||
- `structSchema` reflection handles flat structs only; nested structs are not recursed.
|
||||
- `core api spec` currently emits a spec with only `/health`; full MCP wiring into the CLI command is pending.
|
||||
37
docs/plans/completed/mcp-integration.md
Normal file
37
docs/plans/completed/mcp-integration.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# MCP Integration — Completion Summary
|
||||
|
||||
**Completed:** 2026-02-05
|
||||
**Plan:** `docs/plans/2026-02-05-mcp-integration.md`
|
||||
|
||||
## What Was Built
|
||||
|
||||
### RAG Tools (`pkg/mcp/tools_rag.go`)
|
||||
Three MCP tools added to the existing `pkg/mcp` server:
|
||||
- `rag_query` — semantic search against Qdrant vector DB
|
||||
- `rag_ingest` — ingest a file or directory into a named collection
|
||||
- `rag_collections` — list available Qdrant collections (with optional stats)
|
||||
|
||||
### Metrics Tools (`pkg/mcp/tools_metrics.go`)
|
||||
Two MCP tools for agent activity tracking:
|
||||
- `metrics_record` — write a typed event (agent_id, repo, arbitrary data) to JSONL storage
|
||||
- `metrics_query` — query events with aggregation by type, repo, and agent; supports human-friendly duration strings (7d, 24h)
|
||||
|
||||
Also added `parseDuration()` helper for "Nd"/"Nh"/"Nm" duration strings.
|
||||
|
||||
### `core mcp serve` Command (`internal/cmd/mcpcmd/cmd_mcp.go`)
|
||||
New CLI sub-command registered via `cli.WithCommands()` (not `init()`).
|
||||
- Runs `pkg/mcp` server over stdio by default
|
||||
- TCP mode via `MCP_ADDR=:9000` environment variable
|
||||
- `--workspace` flag to restrict file operations to a directory
|
||||
|
||||
Registered in the full CLI variant. i18n strings added for all user-facing text.
|
||||
|
||||
### Plugin Configuration
|
||||
`.mcp.json` created for the `agentic-flows` Claude Code plugin, pointing to `core mcp serve`. Exposes all 15 tools to Claude Code agents via the `core-cli` MCP server name.
|
||||
|
||||
## Key Outcomes
|
||||
|
||||
- `core mcp serve` is the single entry point for all MCP tooling (file ops, RAG, metrics, language detection, process management, WebSocket, webview/CDP)
|
||||
- MCP command moved to `go-ai/cmd/mcpcmd/` in final form; the plan's `internal/cmd/mcpcmd/` path reflects the pre-extraction location
|
||||
- Registration pattern updated from `init()` + `RegisterCommands()` to `cli.WithCommands()` lifecycle hooks
|
||||
- Services required at runtime: Qdrant (localhost:6333), Ollama with nomic-embed-text (localhost:11434)
|
||||
62
docs/plans/completed/qk-bone-orientation.md
Normal file
62
docs/plans/completed/qk-bone-orientation.md
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# Q/K Bone Orientation — Completion Summary
|
||||
|
||||
**Completed:** 23 February 2026
|
||||
**Repos:** go-inference, go-mlx, go-ml, LEM
|
||||
**Status:** All 7 tasks complete, 14 files changed (+917 lines), all tests passing
|
||||
|
||||
## What Was Built
|
||||
|
||||
### go-inference — AttentionSnapshot types (Task 1)
|
||||
|
||||
`AttentionSnapshot` struct and `AttentionInspector` optional interface. Backends expose attention data via type assertion — no breaking changes to `TextModel`.
|
||||
|
||||
### go-mlx — KV cache extraction (Task 2)
|
||||
|
||||
`InspectAttention` on `metalAdapter` runs a single prefill pass and extracts post-RoPE K vectors from each layer's KV cache. Tested against real Gemma3-1B (26 layers, 1 KV head via GQA, 256 head dim).
|
||||
|
||||
### go-ml — Adapter pass-through (Task 3)
|
||||
|
||||
`InspectAttention` on `InferenceAdapter` type-asserts the underlying `TextModel` to `AttentionInspector`. Returns clear error for unsupported backends.
|
||||
|
||||
### LEM — Analysis engine (Task 4)
|
||||
|
||||
Pure Go CPU math in `pkg/lem/attention.go`. Computes 5 BO metrics from raw K tensors:
|
||||
|
||||
- **Mean Coherence** — pairwise cosine similarity of K vectors within each layer
|
||||
- **Cross-Layer Alignment** — cosine similarity of mean K vectors between adjacent layers
|
||||
- **Head Entropy** — normalised Shannon entropy of K vector magnitudes across positions
|
||||
- **Phase-Lock Score** — fraction of head pairs above coherence threshold (0.7)
|
||||
- **Joint Collapse Count** — layers where cross-alignment drops below threshold (0.5)
|
||||
|
||||
Composite score: 30% coherence + 25% cross-alignment + 20% phase-lock + 15% entropy + 10% joint stability → 0-100 scale.
|
||||
|
||||
### LEM — CLI command (Task 5)
|
||||
|
||||
`lem score attention -model <path> -prompt <text> [-json]` loads a model, runs InspectAttention, and prints BO metrics.
|
||||
|
||||
### LEM — Distill integration (Task 6)
|
||||
|
||||
Opt-in attention scoring in the distill pipeline. Gated behind `scorer.attention: true` and `scorer.attention_min_score` in ai.yaml. Costs one extra prefill per probe.
|
||||
|
||||
### LEM — Feature vectors (Task 7)
|
||||
|
||||
19D full feature vector: 6D grammar + 8D heuristic + 5D attention (`mean_coherence`, `cross_alignment`, `head_entropy`, `phase_lock`, `joint_stability`). Ready for Poindexter KDTree spatial indexing.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
- **Optional interface** — `AttentionInspector` via type assertion, not added to `TextModel`
|
||||
- **Named `BOResult`** — avoids collision with `metal.AttentionResult` in go-mlx
|
||||
- **Opt-in for distill** — extra prefill per probe is expensive, off by default
|
||||
- **Pure Go analysis** — zero CGO deps in the analysis engine; GPU data extracted once via `.Floats()`
|
||||
|
||||
## Commits
|
||||
|
||||
| Repo | SHA | Message |
|
||||
|------|-----|---------|
|
||||
| go-inference | `0f7263f` | feat: add AttentionInspector optional interface |
|
||||
| go-mlx | `c2177f7` | feat: implement AttentionInspector via KV cache extraction |
|
||||
| go-ml | `45e9fed` | feat: add InspectAttention pass-through |
|
||||
| LEM | `28309b2` | feat: add Q/K Bone Orientation analysis engine |
|
||||
| LEM | `e333192` | feat: add 'lem score attention' CLI |
|
||||
| LEM | `fbc636e` | feat: integrate attention scoring into distill pipeline |
|
||||
| LEM | `b621baa` | feat: add 19D full feature vector |
|
||||
Loading…
Add table
Reference in a new issue