diff --git a/docs/superpowers/plans/2026-03-14-api-polyglot-merge.md b/docs/superpowers/plans/2026-03-14-api-polyglot-merge.md deleted file mode 100644 index 1948590..0000000 --- a/docs/superpowers/plans/2026-03-14-api-polyglot-merge.md +++ /dev/null @@ -1,788 +0,0 @@ -# core/api Polyglot Merge — Implementation Plan - -**Date:** 2026-03-14 -**Design:** [2026-03-14-api-polyglot-merge-design.md](../specs/2026-03-14-api-polyglot-merge-design.md) -**Pattern:** Follows core/mcp merge (2026-03-09) -**Co-author:** Co-Authored-By: Virgil - -## Source Inventory - -### Go side (`core/go-api`) - -Module: `forge.lthn.ai/core/go-api` (Go 1.26) - -Root-level `.go` files (50 files): -- Core: `api.go`, `group.go`, `options.go`, `response.go`, `middleware.go` -- Features: `authentik.go`, `bridge.go`, `brotli.go`, `cache.go`, `codegen.go`, `export.go`, `graphql.go`, `i18n.go`, `openapi.go`, `sse.go`, `swagger.go`, `tracing.go`, `websocket.go` -- Tests: `*_test.go` (26 files including `race_test.go`, `norace_test.go`) -- Subdirs: `cmd/api/` (CLI commands), `docs/` (4 markdown files), `.core/` (build.yaml, release.yaml) - -Key dependencies: `forge.lthn.ai/core/cli`, `github.com/gin-gonic/gin`, OpenTelemetry, Casbin, OIDC - -### PHP side (`core/php-api`) - -Package: `lthn/php-api` (PHP 8.2+, requires `lthn/php`) - -Three namespace roots: -- `Core\Api\` (`src/Api/`) — Boot, Controllers, Middleware, Models, Services, Documentation, RateLimit, Tests -- `Core\Front\Api\` (`src/Front/Api/`) — API versioning frontage, auto-discovered provider -- `Core\Website\Api\` (`src/Website/Api/`) — Documentation UI, Blade views, web routes - -### Consumer repos (Go) - -| Repo | Dependency | Go source imports | Notes | -|------|-----------|-------------------|-------| -| `core/go-ml` | direct | `api/routes.go`, `api/routes_test.go` | Aliased as `goapi` | -| `core/mcp` | direct | `pkg/mcp/bridge.go`, `pkg/mcp/bridge_test.go` | Aliased as `api` | -| `core/go-ai` | direct (go.mod only) | None | go.mod + go.sum + docs reference | -| `core/ide` | indirect | None | Transitive via go-ai or mcp | -| `core/agent` | indirect (v0.0.3) | None | Transitive | - -### Consumer repos (PHP) - -| Repo / App | Dependency | Notes | -|------------|-----------|-------| -| `host.uk.com` (Laravel app) | `core/php-api: *` | composer.json + repositories block | -| `core/php-template` | `lthn/php-api: dev-main` | Starter template | - ---- - -## Task 1 — Clone core/api and copy Go files from go-api - -Clone the empty target repo and copy all Go source files at root level (Option B from the design spec — no `pkg/api/` nesting). - -```bash -cd ~/Code/core -git clone ssh://git@forge.lthn.ai:2223/core/api.git -cd api - -# Copy all Go source files (root level) -cp ~/Code/core/go-api/*.go . - -# Copy cmd/ directory -cp -r ~/Code/core/go-api/cmd . - -# Copy docs/ -cp -r ~/Code/core/go-api/docs . - -# Copy .core/ build config -mkdir -p .core -cp ~/Code/core/go-api/.core/build.yaml .core/build.yaml -cp ~/Code/core/go-api/.core/release.yaml .core/release.yaml - -# Copy go.sum as starting point -cp ~/Code/core/go-api/go.sum . -``` - -Update `.core/build.yaml`: - -```yaml -version: 1 - -project: - name: core-api - description: REST API framework (Go + PHP) - binary: "" - -build: - cgo: false - flags: - - -trimpath - ldflags: - - -s - - -w - -targets: - - os: linux - arch: amd64 - - os: linux - arch: arm64 - - os: darwin - arch: arm64 - - os: windows - arch: amd64 -``` - -Update `.core/release.yaml`: - -```yaml -version: 1 - -project: - name: core-api - repository: core/api - -publishers: [] - -changelog: - include: - - feat - - fix - - perf - - refactor - exclude: - - chore - - docs - - style - - test - - ci -``` - -### Verification - -```bash -ls *.go | wc -l # Should be 50 files -ls cmd/api/ # Should have cmd.go, cmd_sdk.go, cmd_spec.go, cmd_test.go -ls docs/ # architecture.md, development.md, history.md, index.md -``` - ---- - -## Task 2 — Copy PHP files from php-api into src/php/ - -```bash -cd ~/Code/core/api - -# Create PHP directory structure -mkdir -p src/php/src -mkdir -p src/php/tests - -# Copy PHP source namespaces -cp -r ~/Code/core/php-api/src/Api src/php/src/Api -cp -r ~/Code/core/php-api/src/Front src/php/src/Front -cp -r ~/Code/core/php-api/src/Website src/php/src/Website - -# Copy PHP test infrastructure -cp -r ~/Code/core/php-api/tests/Feature src/php/tests/Feature 2>/dev/null || true -cp -r ~/Code/core/php-api/tests/Unit src/php/tests/Unit 2>/dev/null || true -cp ~/Code/core/php-api/tests/TestCase.php src/php/tests/TestCase.php 2>/dev/null || true - -# Copy phpunit.xml (adjust paths for new location) -cp ~/Code/core/php-api/phpunit.xml src/php/phpunit.xml -``` - -Edit `src/php/phpunit.xml` — update the `` paths: - -```xml - - - - - tests/Unit - - - tests/Feature - - - - - src - - - - - - - - - - - - - - - - -``` - -### Verification - -```bash -ls src/php/src/ # Api, Front, Website -ls src/php/src/Api/ # Boot.php, Controllers/, Middleware/, Models/, etc. -ls src/php/src/Front/Api/ # Boot.php, ApiVersionService.php, Middleware/, etc. -ls src/php/src/Website/Api/ # Boot.php, Controllers/, Views/, etc. -``` - ---- - -## Task 3 — Create go.mod, composer.json, and .gitattributes - -### go.mod - -Create `go.mod` with the new module path. Start from the existing go.mod and change the module line: - -```bash -cd ~/Code/core/api -sed 's|module forge.lthn.ai/core/go-api|module forge.lthn.ai/core/api|' \ - ~/Code/core/go-api/go.mod > go.mod -``` - -The first line should now read: - -``` -module forge.lthn.ai/core/api -``` - -Then tidy: - -```bash -go mod tidy -``` - -### composer.json - -Create `composer.json` following the core/mcp pattern: - -```json -{ - "name": "lthn/api", - "description": "REST API module — Laravel API layer + standalone Go binary", - "keywords": ["api", "rest", "laravel", "openapi"], - "license": "EUPL-1.2", - "require": { - "php": "^8.2", - "lthn/php": "*", - "symfony/yaml": "^7.0" - }, - "autoload": { - "psr-4": { - "Core\\Api\\": "src/php/src/Api/", - "Core\\Front\\Api\\": "src/php/src/Front/Api/", - "Core\\Website\\Api\\": "src/php/src/Website/Api/" - } - }, - "autoload-dev": { - "psr-4": { - "Core\\Api\\Tests\\": "src/php/tests/" - } - }, - "extra": { - "laravel": { - "providers": [ - "Core\\Front\\Api\\Boot" - ] - } - }, - "config": { - "sort-packages": true - }, - "minimum-stability": "dev", - "prefer-stable": true, - "replace": { - "core/php-api": "self.version", - "lthn/php-api": "self.version" - } -} -``` - -### .gitattributes - -Create `.gitattributes` matching the core/mcp pattern — exclude Go files from Composer installs: - -``` -*.go export-ignore -go.mod export-ignore -go.sum export-ignore -cmd/ export-ignore -pkg/ export-ignore -.core/ export-ignore -src/php/tests/ export-ignore -``` - -### .gitignore - -``` -# Binaries -core-api -*.exe - -# IDE -.idea/ -.vscode/ - -# Go -vendor/ - -# PHP -/vendor/ -node_modules/ -``` - -### Verification - -```bash -head -1 go.mod # module forge.lthn.ai/core/api -cat composer.json | python3 -m json.tool # Valid JSON -cat .gitattributes # 7 export-ignore lines -``` - ---- - -## Task 4 — Create CLAUDE.md for the polyglot repo - -Create `CLAUDE.md` at the repo root: - -```markdown -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -Core API is the REST framework for the Lethean ecosystem, providing both a **Go HTTP engine** (Gin-based, with OpenAPI generation, WebSocket/SSE, ToolBridge) and a **PHP Laravel package** (rate limiting, webhooks, API key management, OpenAPI documentation). Both halves serve the same purpose in their respective stacks. - -Module: `forge.lthn.ai/core/api` | Package: `lthn/api` | Licence: EUPL-1.2 - -## Build and Test Commands - -### Go - -```bash -core build # Build binary (if cmd/ has main) -go build ./... # Build library - -core go test # Run all Go tests -core go test --run TestName # Run a single test -core go cov # Coverage report -core go cov --open # Open HTML coverage in browser -core go qa # Format + vet + lint + test -core go qa full # Also race detector, vuln scan, security audit -core go fmt # gofmt -core go lint # golangci-lint -core go vet # go vet -``` - -### PHP (from repo root) - -```bash -composer test # Run all PHP tests (Pest) -composer test -- --filter=ApiKey # Single test -composer lint # Laravel Pint (PSR-12) -./vendor/bin/pint --dirty # Format changed files -``` - -Tests live in `src/php/src/Api/Tests/Feature/` (in-source) and `src/php/tests/` (standalone). - -## Architecture - -### Go Engine (root-level .go files) - -`Engine` is the central type, configured via functional `Option` functions passed to `New()`: - -```go -engine, _ := api.New(api.WithAddr(":8080"), api.WithCORS("*"), api.WithSwagger(...)) -engine.Register(myRouteGroup) -engine.Serve(ctx) -``` - -**Extension interfaces** (`group.go`): -- `RouteGroup` — minimum: `Name()`, `BasePath()`, `RegisterRoutes(*gin.RouterGroup)` -- `StreamGroup` — optional: `Channels() []string` for WebSocket -- `DescribableGroup` — extends RouteGroup with `Describe() []RouteDescription` for OpenAPI - -**ToolBridge** (`bridge.go`): Converts `ToolDescriptor` structs into `POST /{tool_name}` REST endpoints with auto-generated OpenAPI paths. - -**Authentication** (`authentik.go`): Authentik OIDC integration + static bearer token. Permissive middleware with `RequireAuth()` / `RequireGroup()` guards. - -**OpenAPI** (`openapi.go`, `export.go`, `codegen.go`): `SpecBuilder.Build()` generates OpenAPI 3.1 JSON. `SDKGenerator` wraps openapi-generator-cli for 11 languages. - -**CLI** (`cmd/api/`): Registers `core api spec` and `core api sdk` commands. - -### PHP Package (`src/php/`) - -Three namespace roots: - -| Namespace | Path | Role | -|-----------|------|------| -| `Core\Front\Api` | `src/php/src/Front/Api/` | API frontage — middleware, versioning, auto-discovered provider | -| `Core\Api` | `src/php/src/Api/` | Backend — auth, scopes, models, webhooks, OpenAPI docs | -| `Core\Website\Api` | `src/php/src/Website/Api/` | Documentation UI — controllers, Blade views, web routes | - -Boot chain: `Front\Api\Boot` (auto-discovered) fires `ApiRoutesRegistering` → `Api\Boot` registers middleware and routes. - -Key services: `WebhookService`, `RateLimitService`, `IpRestrictionService`, `OpenApiBuilder`, `ApiKeyService`. - -## Conventions - -- **UK English** in all user-facing strings and docs (colour, organisation, unauthorised) -- **SPDX headers** in Go files: `// SPDX-License-Identifier: EUPL-1.2` -- **`declare(strict_types=1);`** in every PHP file -- **Full type hints** on all PHP parameters and return types -- **Pest syntax** for PHP tests (not PHPUnit) -- **Flux Pro** components in Livewire views; **Font Awesome** icons -- **Conventional commits**: `type(scope): description` -- **Co-Author**: `Co-Authored-By: Virgil ` -- Go test names use `_Good` / `_Bad` / `_Ugly` suffixes - -## Key Dependencies - -| Go module | Role | -|-----------|------| -| `forge.lthn.ai/core/cli` | CLI command registration | -| `github.com/gin-gonic/gin` | HTTP router | -| `github.com/casbin/casbin/v2` | Authorisation policies | -| `github.com/coreos/go-oidc/v3` | OIDC / Authentik | -| `go.opentelemetry.io/otel` | OpenTelemetry tracing | - -PHP: `lthn/php` (Core framework), Laravel 12, `symfony/yaml`. - -Go workspace: this module is part of `~/Code/go.work`. Requires Go 1.26+, PHP 8.2+. -``` - -### Verification - -```bash -wc -l CLAUDE.md # Should be ~100 lines -``` - ---- - -## Task 5 — Update consumer repos (Go import paths) - -Change `forge.lthn.ai/core/go-api` to `forge.lthn.ai/core/api` in all consumers. - -### 5a — core/go-ml (2 source files + go.mod) - -```bash -cd ~/Code/core/go-ml - -# Update Go source imports -sed -i '' 's|forge.lthn.ai/core/go-api|forge.lthn.ai/core/api|g' \ - api/routes.go api/routes_test.go - -# Update go.mod -sed -i '' 's|forge.lthn.ai/core/go-api|forge.lthn.ai/core/api|g' go.mod - -go mod tidy -``` - -Verify the import alias in `api/routes.go` reads: -```go -goapi "forge.lthn.ai/core/api" -``` - -### 5b — core/mcp (2 source files + go.mod) - -```bash -cd ~/Code/core/mcp - -# Update Go source imports -sed -i '' 's|forge.lthn.ai/core/go-api|forge.lthn.ai/core/api|g' \ - pkg/mcp/bridge.go pkg/mcp/bridge_test.go - -# Update go.mod -sed -i '' 's|forge.lthn.ai/core/go-api|forge.lthn.ai/core/api|g' go.mod - -go mod tidy -``` - -Verify the import alias in `pkg/mcp/bridge.go` reads: -```go -api "forge.lthn.ai/core/api" -``` - -### 5c — core/go-ai (go.mod only — no Go source imports) - -```bash -cd ~/Code/core/go-ai - -sed -i '' 's|forge.lthn.ai/core/go-api|forge.lthn.ai/core/api|g' go.mod - -go mod tidy -``` - -Also update the docs reference: -```bash -sed -i '' 's|forge.lthn.ai/core/go-api|forge.lthn.ai/core/api|g' docs/index.md -``` - -### 5d — core/ide (indirect only — go.mod) - -```bash -cd ~/Code/core/ide - -sed -i '' 's|forge.lthn.ai/core/go-api|forge.lthn.ai/core/api|g' go.mod - -go mod tidy -``` - -### 5e — core/agent (indirect only — go.mod) - -```bash -cd ~/Code/core/agent - -sed -i '' 's|forge.lthn.ai/core/go-api|forge.lthn.ai/core/api|g' go.mod - -go mod tidy -``` - -### 5f — Update CLAUDE.md references in core/mcp - -In `~/Code/core/mcp/CLAUDE.md`, update the dependency table: - -``` -| `forge.lthn.ai/core/go-api` | REST framework + `ToolBridge` | -``` -becomes: -``` -| `forge.lthn.ai/core/api` | REST framework + `ToolBridge` | -``` - -### 5g — PHP consumers - -**host.uk.com Laravel app** (`~/Code/lab/host.uk.com/composer.json`): - -Replace the `core/php-api` requirement and repository entry: -- Change `"core/php-api": "*"` to `"lthn/api": "dev-main"` -- Change repository URL from `ssh://git@forge.lthn.ai:2223/core/php-api.git` to `ssh://git@forge.lthn.ai:2223/core/api.git` - -The `replace` block in the new `composer.json` (`"core/php-api": "self.version"`, `"lthn/php-api": "self.version"`) ensures backward compatibility. - -**core/php-template** (`~/Code/core/php-template/composer.json`): - -```bash -cd ~/Code/core/php-template -sed -i '' 's|"lthn/php-api": "dev-main"|"lthn/api": "dev-main"|g' composer.json -``` - -### Verification - -```bash -# Confirm no stale references remain -grep -r 'forge.lthn.ai/core/go-api' ~/Code/core/go-ml/ --include='*.go' --include='go.mod' -grep -r 'forge.lthn.ai/core/go-api' ~/Code/core/mcp/ --include='*.go' --include='go.mod' -grep -r 'forge.lthn.ai/core/go-api' ~/Code/core/go-ai/ --include='*.go' --include='go.mod' -grep -r 'forge.lthn.ai/core/go-api' ~/Code/core/ide/ --include='*.go' --include='go.mod' -grep -r 'forge.lthn.ai/core/go-api' ~/Code/core/agent/ --include='*.go' --include='go.mod' -# All should return nothing -``` - ---- - -## Task 6 — Update go.work - -Edit `~/Code/go.work`: - -```bash -cd ~/Code - -# Remove go-api entry, add api entry -sed -i '' 's|./core/go-api|./core/api|' go.work - -# Sync workspace -go work sync -``` - -### Verification - -```bash -grep 'core/api' ~/Code/go.work # Should show ./core/api -grep 'core/go-api' ~/Code/go.work # Should return nothing -``` - ---- - -## Task 7 — Build verification across all affected repos - -Run in sequence — each depends on the workspace being consistent: - -```bash -cd ~/Code - -# 1. Verify core/api itself builds and tests pass -cd ~/Code/core/api -go build ./... -go test ./... -go vet ./... - -# 2. Verify core/go-ml -cd ~/Code/core/go-ml -go build ./... -go test ./... - -# 3. Verify core/mcp -cd ~/Code/core/mcp -go build ./... -go test ./... - -# 4. Verify core/go-ai -cd ~/Code/core/go-ai -go build ./... - -# 5. Verify core/ide -cd ~/Code/core/ide -go build ./... - -# 6. Verify core/agent -cd ~/Code/core/agent -go build ./... -``` - -If any fail, fix import paths and re-run `go mod tidy` in the affected repo. - -### Verification - -All six builds should succeed with zero errors. Test failures unrelated to the import path change can be ignored (pre-existing). - ---- - -## Task 8 — Archive go-api and php-api on Forge - -Archive both source repos on Forge. This marks them read-only but keeps them accessible for existing consumers that haven't updated. - -```bash -# Archive via Forgejo API (requires GITEA_TOKEN or gh-equivalent) -# Option A: Forgejo web UI -# 1. Navigate to https://forge.lthn.ai/core/go-api/settings -# 2. Click "Archive this repository" -# 3. Repeat for https://forge.lthn.ai/core/php-api/settings - -# Option B: Forgejo API -curl -X PATCH \ - -H "Authorization: token ${FORGE_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{"archived": true}' \ - "https://forge.lthn.ai/api/v1/repos/core/go-api" - -curl -X PATCH \ - -H "Authorization: token ${FORGE_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{"archived": true}' \ - "https://forge.lthn.ai/api/v1/repos/core/php-api" -``` - -Update repo descriptions to point to the merged repo: - -```bash -curl -X PATCH \ - -H "Authorization: token ${FORGE_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{"description": "ARCHIVED — merged into core/api"}' \ - "https://forge.lthn.ai/api/v1/repos/core/go-api" - -curl -X PATCH \ - -H "Authorization: token ${FORGE_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{"description": "ARCHIVED — merged into core/api"}' \ - "https://forge.lthn.ai/api/v1/repos/core/php-api" -``` - -### Local cleanup - -```bash -# Optionally remove the old local clones (or keep for reference) -# rm -rf ~/Code/core/go-api ~/Code/core/php-api -``` - ---- - -## Commit Strategy - -Initial commit in core/api: - -```bash -cd ~/Code/core/api -git add -A -git commit -m "$(cat <<'EOF' -feat(api): merge go-api + php-api into polyglot repo - -Go source at root level (Option B), PHP under src/php/. -Module path: forge.lthn.ai/core/api -Package name: lthn/api - -Co-Authored-By: Virgil -EOF -)" -git push origin main -``` - -Consumer updates (one commit per repo): - -```bash -# core/go-ml -cd ~/Code/core/go-ml -git add -A -git commit -m "$(cat <<'EOF' -refactor(api): update import path from go-api to core/api - -Part of the polyglot merge — forge.lthn.ai/core/go-api is now -forge.lthn.ai/core/api. - -Co-Authored-By: Virgil -EOF -)" - -# core/mcp -cd ~/Code/core/mcp -git add -A -git commit -m "$(cat <<'EOF' -refactor(api): update import path from go-api to core/api - -Part of the polyglot merge — forge.lthn.ai/core/go-api is now -forge.lthn.ai/core/api. - -Co-Authored-By: Virgil -EOF -)" - -# core/go-ai -cd ~/Code/core/go-ai -git add -A -git commit -m "$(cat <<'EOF' -refactor(api): update import path from go-api to core/api - -Part of the polyglot merge — forge.lthn.ai/core/go-api is now -forge.lthn.ai/core/api. - -Co-Authored-By: Virgil -EOF -)" - -# core/ide -cd ~/Code/core/ide -git add -A -git commit -m "$(cat <<'EOF' -refactor(api): update import path from go-api to core/api - -Part of the polyglot merge — forge.lthn.ai/core/go-api is now -forge.lthn.ai/core/api. - -Co-Authored-By: Virgil -EOF -)" - -# core/agent -cd ~/Code/core/agent -git add -A -git commit -m "$(cat <<'EOF' -refactor(api): update import path from go-api to core/api - -Part of the polyglot merge — forge.lthn.ai/core/go-api is now -forge.lthn.ai/core/api. - -Co-Authored-By: Virgil -EOF -)" -``` - ---- - -## Summary - -| # | Task | Files touched | Risk | -|---|------|--------------|------| -| 1 | Clone core/api, copy Go files | 50 .go + cmd/ + docs/ + .core/ | Low | -| 2 | Copy PHP files into src/php/ | ~80 PHP files | Low | -| 3 | Create go.mod, composer.json, .gitattributes | 4 new files | Low | -| 4 | Create CLAUDE.md | 1 new file | Low | -| 5 | Update 5 Go consumers + 2 PHP consumers | ~12 files across 7 repos | Medium | -| 6 | Update go.work | 1 file | Low | -| 7 | Build verification | 0 files (validation only) | N/A | -| 8 | Archive go-api + php-api | 0 local files (Forge API) | Low | - -Estimated effort: 30–45 minutes of execution time. diff --git a/docs/superpowers/plans/2026-03-14-ide-modernisation.md b/docs/superpowers/plans/2026-03-14-ide-modernisation.md deleted file mode 100644 index 16c2b0d..0000000 --- a/docs/superpowers/plans/2026-03-14-ide-modernisation.md +++ /dev/null @@ -1,452 +0,0 @@ -# core/ide Modernisation — Implementation Plan - -**Date:** 2026-03-14 -**Spec:** [2026-03-14-ide-modernisation-design.md](../specs/2026-03-14-ide-modernisation-design.md) - ---- - -## Task 1 — Delete the 7 obsolete files - -Remove all hand-rolled services that are now provided by ecosystem packages. - -```bash -cd /Users/snider/Code/core/ide - -rm mcp_bridge.go # Replaced by core/mcp Service + transports -rm webview_svc.go # Replaced by core/gui/pkg/display + webview -rm brain_mcp.go # Replaced by core/mcp/pkg/mcp/brain -rm claude_bridge.go # Replaced by core/mcp/pkg/mcp/ide Bridge -rm headless_mcp.go # Not needed — core/mcp runs in-process -rm headless.go # config gui.enabled flag; jobrunner belongs in core/agent -rm greetservice.go # Scaffold placeholder -``` - -**Verification:** `ls *.go` should show only `main.go`. - ---- - -## Task 2 — Rewrite main.go - -Replace the current main.go with a thin Wails shell that wires `core.Core` with -ecosystem services. The full file contents: - -```go -package main - -import ( - "context" - "embed" - "io/fs" - "log" - "os" - "os/signal" - "runtime" - "syscall" - - "forge.lthn.ai/core/config" - "forge.lthn.ai/core/go-ws" - "forge.lthn.ai/core/go/pkg/core" - guiMCP "forge.lthn.ai/core/gui/pkg/mcp" - "forge.lthn.ai/core/gui/pkg/display" - "forge.lthn.ai/core/ide/icons" - "forge.lthn.ai/core/mcp/pkg/mcp" - "forge.lthn.ai/core/mcp/pkg/mcp/brain" - "forge.lthn.ai/core/mcp/pkg/mcp/ide" - "github.com/wailsapp/wails/v3/pkg/application" -) - -//go:embed all:frontend/dist/wails-angular-template/browser -var assets embed.FS - -func main() { - // ── Flags ────────────────────────────────────────────────── - mcpOnly := false - for _, arg := range os.Args[1:] { - if arg == "--mcp" { - mcpOnly = true - } - } - - // ── Configuration ────────────────────────────────────────── - cfg, _ := config.New() - - cwd, err := os.Getwd() - if err != nil { - log.Fatalf("failed to get working directory: %v", err) - } - - // ── Shared resources (built before Core) ─────────────────── - hub := ws.NewHub() - - bridgeCfg := ide.DefaultConfig() - bridgeCfg.WorkspaceRoot = cwd - if url := os.Getenv("CORE_API_URL"); url != "" { - bridgeCfg.LaravelWSURL = url - } - if token := os.Getenv("CORE_API_TOKEN"); token != "" { - bridgeCfg.Token = token - } - bridge := ide.NewBridge(hub, bridgeCfg) - - // ── Core framework ───────────────────────────────────────── - c, err := core.New( - core.WithName("ws", func(c *core.Core) (any, error) { - return hub, nil - }), - core.WithService(display.Register(nil)), // nil platform until Wails starts - core.WithName("mcp", func(c *core.Core) (any, error) { - return mcp.New( - mcp.WithWorkspaceRoot(cwd), - mcp.WithWSHub(hub), - mcp.WithSubsystem(brain.New(bridge)), - mcp.WithSubsystem(guiMCP.New(c)), - ) - }), - ) - if err != nil { - log.Fatalf("failed to create core: %v", err) - } - - // Retrieve the MCP service for transport control - mcpSvc, err := core.ServiceFor[*mcp.Service](c, "mcp") - if err != nil { - log.Fatalf("failed to get MCP service: %v", err) - } - - // ── Mode selection ───────────────────────────────────────── - if mcpOnly { - // stdio mode — Claude Code connects via --mcp flag - ctx, cancel := signal.NotifyContext(context.Background(), - syscall.SIGINT, syscall.SIGTERM) - defer cancel() - - // Start Core lifecycle manually - if err := c.ServiceStartup(ctx, nil); err != nil { - log.Fatalf("core startup failed: %v", err) - } - bridge.Start(ctx) - - if err := mcpSvc.ServeStdio(ctx); err != nil { - log.Printf("MCP stdio error: %v", err) - } - - _ = mcpSvc.Shutdown(ctx) - _ = c.ServiceShutdown(ctx) - return - } - - if !guiEnabled(cfg) { - // No GUI — run Core with MCP transport in background - ctx, cancel := signal.NotifyContext(context.Background(), - syscall.SIGINT, syscall.SIGTERM) - defer cancel() - - if err := c.ServiceStartup(ctx, nil); err != nil { - log.Fatalf("core startup failed: %v", err) - } - bridge.Start(ctx) - - go func() { - if err := mcpSvc.Run(ctx); err != nil { - log.Printf("MCP error: %v", err) - } - }() - - <-ctx.Done() - shutdownCtx := context.Background() - _ = mcpSvc.Shutdown(shutdownCtx) - _ = c.ServiceShutdown(shutdownCtx) - return - } - - // ── GUI mode ─────────────────────────────────────────────── - staticAssets, err := fs.Sub(assets, "frontend/dist/wails-angular-template/browser") - if err != nil { - log.Fatal(err) - } - - app := application.New(application.Options{ - Name: "Core IDE", - Description: "Host UK Core IDE - Development Environment", - Services: []application.Service{ - application.NewService(c), - }, - Assets: application.AssetOptions{ - Handler: application.AssetFileServerFS(staticAssets), - }, - Mac: application.MacOptions{ - ActivationPolicy: application.ActivationPolicyAccessory, - }, - OnShutdown: func() { - ctx := context.Background() - _ = mcpSvc.Shutdown(ctx) - bridge.Shutdown() - }, - }) - - // System tray - systray := app.SystemTray.New() - systray.SetTooltip("Core IDE") - - if runtime.GOOS == "darwin" { - systray.SetTemplateIcon(icons.AppTray) - } else { - systray.SetDarkModeIcon(icons.AppTray) - systray.SetIcon(icons.AppTray) - } - - // Tray panel window - trayWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{ - Name: "tray-panel", - Title: "Core IDE", - Width: 380, - Height: 480, - URL: "/tray", - Hidden: true, - Frameless: true, - BackgroundColour: application.NewRGB(26, 27, 38), - }) - systray.AttachWindow(trayWindow).WindowOffset(5) - - // Tray menu - trayMenu := app.Menu.New() - trayMenu.Add("Quit").OnClick(func(ctx *application.Context) { - app.Quit() - }) - systray.SetMenu(trayMenu) - - // Start MCP transport alongside Wails - go func() { - ctx := context.Background() - bridge.Start(ctx) - if err := mcpSvc.Run(ctx); err != nil { - log.Printf("MCP error: %v", err) - } - }() - - log.Println("Starting Core IDE...") - - if err := app.Run(); err != nil { - log.Fatal(err) - } -} - -// guiEnabled checks whether the GUI should start. -// Returns false if config says gui.enabled: false, or if no display is available. -func guiEnabled(cfg *config.Config) bool { - if cfg != nil { - var guiCfg struct { - Enabled *bool `mapstructure:"enabled"` - } - if err := cfg.Get("gui", &guiCfg); err == nil && guiCfg.Enabled != nil { - return *guiCfg.Enabled - } - } - // Fall back to display detection - if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { - return true - } - return os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != "" -} -``` - -### Key changes from the old main.go - -1. **Three modes**: `--mcp` (stdio for Claude Code), no-GUI (headless with MCP_ADDR), GUI (Wails systray). -2. **`core.Core` is the Wails service** — its `ServiceStartup`/`ServiceShutdown` drive all sub-service lifecycles. -3. **Ecosystem wiring**: `display.Register(nil)` for GUI tools, `brain.New(bridge)` for OpenBrain, `guiMCP.New(c)` for 74 GUI MCP tools, `mcp.WithWSHub(hub)` for WS streaming. -4. **No hand-rolled HTTP server** — `mcp.Service` owns the MCP protocol (stdio/TCP/Unix). The WS hub is injected via option. -5. **IDE bridge** uses `ide.DefaultConfig()` from `core/mcp/pkg/mcp/ide` with env var overrides. - ---- - -## Task 3 — Update go.mod - -### 3a. Replace go.mod with updated dependencies - -``` -module forge.lthn.ai/core/ide - -go 1.26.0 - -require ( - forge.lthn.ai/core/go v0.2.2 - forge.lthn.ai/core/config v0.1.2 - forge.lthn.ai/core/go-ws v0.1.3 - forge.lthn.ai/core/gui v0.1.0 - forge.lthn.ai/core/mcp v0.1.0 - github.com/wailsapp/wails/v3 v3.0.0-alpha.74 -) -``` - -**Removed** (no longer directly imported): -- `forge.lthn.ai/core/agent` — brain tools now via `core/mcp/pkg/mcp/brain` -- `forge.lthn.ai/core/go-process` — daemon/PID logic was in headless.go -- `forge.lthn.ai/core/go-scm` — Forgejo client was in headless.go -- `github.com/gorilla/websocket` — replaced by `core/mcp/pkg/mcp/ide` (uses it internally) - -**Added**: -- `forge.lthn.ai/core/gui` — display service + 74 MCP tools -- `forge.lthn.ai/core/mcp` — MCP server, brain subsystem, IDE bridge - -### 3b. Sync workspace and tidy - -```bash -cd /Users/snider/Code/core/ide -go mod tidy -cd /Users/snider/Code -go work sync -``` - -The `go mod tidy` will pull indirect dependencies and populate the `require` -block. The `go work sync` aligns versions across the workspace. - ---- - -## Task 4 — Update CLAUDE.md - -Replace the existing CLAUDE.md with content reflecting the new architecture: - -```markdown -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Build & Development Commands - -\`\`\`bash -# Development (hot-reload GUI + Go rebuild) -wails3 dev - -# Production build (preferred) -core build - -# Frontend-only development -cd frontend && npm install && npm run dev - -# Go tests -core go test # All tests -core go test --run TestName # Single test -core go cov # Coverage report -core go cov --open # Coverage in browser - -# Quality assurance -core go qa # Format + vet + lint + test -core go qa full # + race detector, vuln scan, security audit -core go fmt # Format only -core go lint # Lint only - -# Frontend tests -cd frontend && npm run test -\`\`\` - -## Architecture - -**Thin Wails shell** wiring ecosystem packages via `core.Core` dependency injection. Three operating modes: - -### GUI Mode (default) -`main()` → Wails 3 application with embedded Angular frontend, system tray (macOS: accessory app, no Dock icon). Core framework manages all services: -- **display** (`core/gui`) — window management, webview automation, 74 MCP tools across 14 categories -- **MCP** (`core/mcp`) — Model Context Protocol server with file ops, brain subsystem, GUI subsystem -- **IDE bridge** (`core/mcp/pkg/mcp/ide`) — WebSocket bridge to Laravel core-agentic backend -- **WS hub** (`core/go-ws`) — WebSocket hub for Angular frontend communication - -### MCP Mode (`--mcp`) -`core-ide --mcp` → stdio MCP server for Claude Code integration. No GUI, no HTTP. Configure in `.claude/.mcp.json`: -\`\`\`json -{ - "mcpServers": { - "core-ide": { - "type": "stdio", - "command": "core-ide", - "args": ["--mcp"] - } - } -} -\`\`\` - -### Headless Mode (no display or `gui.enabled: false`) -Core framework runs all services without Wails. MCP transport determined by `MCP_ADDR` env var (TCP if set, stdio otherwise). - -### Frontend -Angular 20+ app embedded via `//go:embed`. Two routes: `/tray` (system tray panel, 380x480 frameless) and `/ide` (full IDE layout). - -## Configuration - -\`\`\`yaml -# .core/config.yaml -gui: - enabled: true # false = no Wails, Core still runs -mcp: - transport: stdio # stdio | tcp | unix - tcp: - port: 9877 -brain: - api_url: http://localhost:8000 - api_token: "" # or CORE_API_TOKEN env var -\`\`\` - -## Environment Variables - -| Variable | Default | Purpose | -|----------|---------|---------| -| `CORE_API_URL` | `http://localhost:8000` | Laravel backend WebSocket URL | -| `CORE_API_TOKEN` | (empty) | Bearer token for Laravel backend auth | -| `MCP_ADDR` | (empty) | TCP address for MCP server (headless mode) | - -## Workspace Dependencies - -This module uses a Go workspace (`~/Code/go.work`) with `replace` directives for sibling modules: -- `../go` → `forge.lthn.ai/core/go` -- `../gui` → `forge.lthn.ai/core/gui` -- `../mcp` → `forge.lthn.ai/core/mcp` -- `../config` → `forge.lthn.ai/core/config` -- `../go-ws` → `forge.lthn.ai/core/go-ws` - -## Conventions - -- **UK English** in documentation and user-facing strings (colour, organisation, centre). -- **Conventional commits**: `type(scope): description` with co-author line `Co-Authored-By: Virgil `. -- **Licence**: EUPL-1.2. -- All Go code is in `package main` (single-package application). -- Services are registered via `core.WithService` or `core.WithName` factory functions. -- MCP subsystems implement `mcp.Subsystem` interface from `core/mcp`. -``` - ---- - -## Task 5 — Build verification - -```bash -cd /Users/snider/Code/core/ide -go build ./... -``` - -If the build fails, the likely causes are: - -1. **Missing workspace entries** — ensure `go.work` includes `./core/mcp` and `./core/gui`: - ```bash - cd /Users/snider/Code - go work use ./core/mcp ./core/gui - go work sync - ``` - -2. **Stale go.sum** — clear and regenerate: - ```bash - cd /Users/snider/Code/core/ide - rm go.sum - go mod tidy - ``` - -3. **Version mismatches** — check that indirect dependency versions align: - ```bash - go mod graph | grep -i conflict - ``` - -Once `go build ./...` succeeds, run the quality check: - -```bash -core go qa -``` - -This confirms formatting, vetting, linting, and tests all pass. diff --git a/docs/superpowers/plans/2026-03-14-runtime-provider-loading.md b/docs/superpowers/plans/2026-03-14-runtime-provider-loading.md deleted file mode 100644 index 955431e..0000000 --- a/docs/superpowers/plans/2026-03-14-runtime-provider-loading.md +++ /dev/null @@ -1,105 +0,0 @@ -# Runtime Provider Loading — Implementation Plan - -**Date:** 2026-03-14 -**Spec:** ../specs/2026-03-14-runtime-provider-loading-design.md -**Status:** Complete - -## Task 1: ProxyProvider in core/api (`pkg/provider/proxy.go`) - -**File:** `/Users/snider/Code/core/api/pkg/provider/proxy.go` - -Replace the Phase 3 stub with a working ProxyProvider that: - -- Takes a `ProxyConfig` struct: Name, BasePath, Upstream URL, ElementSpec, SpecFile path -- Implements `Provider` (Name, BasePath, RegisterRoutes) -- Implements `Renderable` (Element) -- `RegisterRoutes` creates a catch-all `/*path` handler using `net/http/httputil.ReverseProxy` -- Strips the base path before proxying so the upstream sees clean paths -- Upstream is always `127.0.0.1` (local process) - -**Test file:** `pkg/provider/proxy_test.go` -- Proxy routes requests to a test upstream server -- Health check passthrough -- Element() returns configured ElementSpec -- Name/BasePath return configured values - -## Task 2: Manifest Extensions in go-scm (`manifest/manifest.go`) - -**File:** `/Users/snider/Code/core/go-scm/manifest/manifest.go` - -Extend the Manifest struct with provider-specific fields: - -- `Namespace` — API route prefix (e.g. `/api/v1/cool-widget`) -- `Port` — listen port (0 = auto-assign) -- `Binary` — path to binary relative to provider dir -- `Args` — additional CLI args -- `Element` — UI element spec (tag + source) -- `Spec` — path to OpenAPI spec file - -These fields are optional — existing manifests without them remain valid. - -**Test:** Add parse test with provider fields. - -## Task 3: Provider Discovery in go-scm (`marketplace/discovery.go`) - -**File:** `/Users/snider/Code/core/go-scm/marketplace/discovery.go` - -- `DiscoverProviders(dir string)` — scans `dir/*/manifest.yaml` (using `os` directly, not Medium, since this is filesystem discovery) -- Returns `[]DiscoveredProvider` with Dir + Manifest -- Skips directories without manifests (logs warning, continues) - -**File:** `/Users/snider/Code/core/go-scm/marketplace/registry_file.go` - -- `ProviderRegistry` — read/write `registry.yaml` tracking installed providers -- `LoadRegistry(path)`, `SaveRegistry(path)`, `Add()`, `Remove()`, `Get()`, `List()` - -**Test file:** `marketplace/discovery_test.go` -- Discover finds providers in temp dirs with manifests -- Discover skips dirs without manifests -- Registry CRUD operations - -## Task 4: RuntimeManager in core/ide (`runtime.go`) - -**File:** `/Users/snider/Code/core/ide/runtime.go` - -- `RuntimeManager` struct holding discovered providers, engine reference, process registry -- `NewRuntimeManager(engine, processRegistry)` constructor -- `StartAll(ctx)` — discover providers in `~/.core/providers/`, start each binary via `os/exec.Cmd`, wait for health, register ProxyProvider -- `StopAll()` — SIGTERM each provider process, clean up -- `List()` — return running providers -- Free port allocation via `net.Listen(":0")` -- Health check polling with timeout - -## Task 5: Wire into core/ide main.go - -**File:** `/Users/snider/Code/core/ide/main.go` - -- After creating the api.Engine (`engine, _ := api.New(...)`), create a RuntimeManager -- In each mode (mcpOnly, headless, GUI), call `rm.StartAll(ctx)` before serving -- In shutdown paths, call `rm.StopAll()` -- Import `forge.lthn.ai/core/go-scm/marketplace` (add dependency) - -## Task 6: Build Verification - -- `cd core/api && go build ./...` -- `cd go-scm && go build ./...` -- `cd ide && go build ./...` (may need go.mod updates) - -## Task 7: Tests - -- `cd core/api && go test ./pkg/provider/...` -- `cd go-scm && go test ./marketplace/... ./manifest/...` - -## Repos Affected - -| Repo | Changes | -|------|---------| -| core/api | `pkg/provider/proxy.go` (implement), `pkg/provider/proxy_test.go` (new) | -| go-scm | `manifest/manifest.go` (extend), `manifest/manifest_test.go` (extend), `marketplace/discovery.go` (new), `marketplace/registry_file.go` (new), `marketplace/discovery_test.go` (new) | -| core/ide | `runtime.go` (new), `main.go` (wire), `go.mod` (add go-scm dep) | - -## Commit Strategy - -1. core/api — `feat(provider): implement ProxyProvider reverse proxy` -2. go-scm — `feat(marketplace): add provider discovery and registry` -3. core/ide — `feat(runtime): add RuntimeManager for provider loading` diff --git a/docs/superpowers/plans/2026-03-14-service-provider-framework.md b/docs/superpowers/plans/2026-03-14-service-provider-framework.md deleted file mode 100644 index ee5f8ec..0000000 --- a/docs/superpowers/plans/2026-03-14-service-provider-framework.md +++ /dev/null @@ -1,1685 +0,0 @@ -# Service Provider Framework — Implementation Plan (Phase 1) - -**Date:** 2026-03-14 -**Spec:** [2026-03-14-service-provider-framework-design.md](../specs/2026-03-14-service-provider-framework-design.md) - ---- - -## Task 1 — Create `pkg/provider/` in core/go-api - -Create the provider interfaces and Registry type. The Provider interface extends -the existing `api.RouteGroup` — providers ARE route groups and register through -`api.Engine.Register()`, not a parallel router. This gives them middleware, CORS, -Swagger, and OpenAPI for free. - -### 1a. Create `pkg/provider/provider.go` - -```go -// SPDX-Licence-Identifier: EUPL-1.2 - -// Package provider defines the Service Provider Framework interfaces. -// -// A Provider extends api.RouteGroup with a provider identity. Providers -// register through the existing api.Engine.Register() method, inheriting -// middleware, CORS, Swagger, and OpenAPI generation automatically. -// -// Optional interfaces (Streamable, Describable, Renderable) declare -// additional capabilities that consumers (GUI, MCP, WS hub) can discover -// via type assertion. -package provider - -import ( - "forge.lthn.ai/core/go-api" -) - -// Provider extends RouteGroup with a provider identity. -// Every Provider is a RouteGroup and registers through api.Engine.Register(). -type Provider interface { - api.RouteGroup // Name(), BasePath(), RegisterRoutes(*gin.RouterGroup) -} - -// Streamable providers emit real-time events via WebSocket. -// The hub is injected at construction time. Channels() declares the -// event prefixes this provider will emit (e.g. "brain.*", "process.*"). -type Streamable interface { - Provider - Channels() []string -} - -// Describable providers expose structured route descriptions for OpenAPI. -// This extends the existing DescribableGroup interface from go-api. -type Describable interface { - Provider - api.DescribableGroup // Describe() []RouteDescription -} - -// Renderable providers declare a custom element for GUI display. -type Renderable interface { - Provider - Element() ElementSpec -} - -// ElementSpec describes a web component for GUI rendering. -type ElementSpec struct { - // Tag is the custom element tag name, e.g. "core-brain-panel". - Tag string `json:"tag"` - - // Source is the URL or embedded path to the JS bundle. - Source string `json:"source"` -} -``` - -### 1b. Create `pkg/provider/registry.go` - -```go -// SPDX-Licence-Identifier: EUPL-1.2 - -package provider - -import ( - "iter" - "slices" - "sync" - - "forge.lthn.ai/core/go-api" -) - -// Registry collects providers and mounts them on an api.Engine. -// It is a convenience wrapper — providers could be registered directly -// via engine.Register(), but the Registry enables discovery by consumers -// (GUI, MCP) that need to query provider capabilities. -type Registry struct { - mu sync.RWMutex - providers []Provider -} - -// NewRegistry creates an empty provider registry. -func NewRegistry() *Registry { - return &Registry{} -} - -// Add registers a provider. Providers are mounted in the order they are added. -func (r *Registry) Add(p Provider) { - r.mu.Lock() - defer r.mu.Unlock() - r.providers = append(r.providers, p) -} - -// MountAll registers every provider with the given api.Engine. -// Each provider is passed to engine.Register(), which mounts it as a -// RouteGroup at its BasePath with all configured middleware. -func (r *Registry) MountAll(engine *api.Engine) { - r.mu.RLock() - defer r.mu.RUnlock() - for _, p := range r.providers { - engine.Register(p) - } -} - -// List returns a copy of all registered providers. -func (r *Registry) List() []Provider { - r.mu.RLock() - defer r.mu.RUnlock() - return slices.Clone(r.providers) -} - -// Iter returns an iterator over all registered providers. -func (r *Registry) Iter() iter.Seq[Provider] { - r.mu.RLock() - defer r.mu.RUnlock() - return slices.Values(slices.Clone(r.providers)) -} - -// Len returns the number of registered providers. -func (r *Registry) Len() int { - r.mu.RLock() - defer r.mu.RUnlock() - return len(r.providers) -} - -// Get returns a provider by name, or nil if not found. -func (r *Registry) Get(name string) Provider { - r.mu.RLock() - defer r.mu.RUnlock() - for _, p := range r.providers { - if p.Name() == name { - return p - } - } - return nil -} - -// Streamable returns all providers that implement the Streamable interface. -func (r *Registry) Streamable() []Streamable { - r.mu.RLock() - defer r.mu.RUnlock() - var result []Streamable - for _, p := range r.providers { - if s, ok := p.(Streamable); ok { - result = append(result, s) - } - } - return result -} - -// Describable returns all providers that implement the Describable interface. -func (r *Registry) Describable() []Describable { - r.mu.RLock() - defer r.mu.RUnlock() - var result []Describable - for _, p := range r.providers { - if d, ok := p.(Describable); ok { - result = append(result, d) - } - } - return result -} - -// Renderable returns all providers that implement the Renderable interface. -func (r *Registry) Renderable() []Renderable { - r.mu.RLock() - defer r.mu.RUnlock() - var result []Renderable - for _, p := range r.providers { - if rv, ok := p.(Renderable); ok { - result = append(result, rv) - } - } - return result -} - -// ProviderInfo is a serialisable summary of a registered provider. -type ProviderInfo struct { - Name string `json:"name"` - BasePath string `json:"basePath"` - Channels []string `json:"channels,omitempty"` - Element *ElementSpec `json:"element,omitempty"` -} - -// Info returns a summary of all registered providers. -func (r *Registry) Info() []ProviderInfo { - r.mu.RLock() - defer r.mu.RUnlock() - - infos := make([]ProviderInfo, 0, len(r.providers)) - for _, p := range r.providers { - info := ProviderInfo{ - Name: p.Name(), - BasePath: p.BasePath(), - } - if s, ok := p.(Streamable); ok { - info.Channels = s.Channels() - } - if rv, ok := p.(Renderable); ok { - elem := rv.Element() - info.Element = &elem - } - infos = append(infos, info) - } - return infos -} -``` - -### 1c. Create `pkg/provider/proxy.go` (Phase 3 stub) - -```go -// SPDX-Licence-Identifier: EUPL-1.2 - -package provider - -// ProxyProvider will wrap polyglot (PHP/TS) providers that publish an OpenAPI -// spec and run their own HTTP handler. The Go API layer reverse-proxies to -// their endpoint. -// -// This is a Phase 3 feature. The type is declared here as a forward reference -// so the package structure is established. -// -// See the design spec § Polyglot Providers for the full ProxyProvider contract. -``` - -**Files created:** - -| File | Purpose | -|------|---------| -| `/Users/snider/Code/core/go-api/pkg/provider/provider.go` | Provider, Streamable, Describable, Renderable interfaces + ElementSpec | -| `/Users/snider/Code/core/go-api/pkg/provider/registry.go` | Registry: Add, MountAll, List, Get, capability queries, Info | -| `/Users/snider/Code/core/go-api/pkg/provider/proxy.go` | Phase 3 stub (doc comment only) | - -**Why `pkg/provider/` in go-api?** The Provider interface embeds `api.RouteGroup` -and `api.DescribableGroup`, and the Registry calls `api.Engine.Register()`. These -are inseparable from the go-api package. A separate module would create a circular -dependency. The `pkg/` subdirectory keeps the provider types in their own package -namespace (`provider.Provider`) without polluting the `api` package. - ---- - -## Task 2 — Create go-process provider - -Create `pkg/api/provider.go` in go-process. This wraps the existing daemon -`Registry` as a `provider.Provider` with REST endpoints for list/start/stop/status -and WS events for daemon state changes. - -The go-process `Registry` (file-based daemon tracker at `~/.core/daemons/`) already -has `List()`, `Get()`, `Register()`, and `Unregister()`. The provider exposes these -over REST and emits WS events when daemon state changes. - -Note: go-process currently depends only on `testify`. This task adds dependencies -on `go-api` (for `api.RouteGroup`, `api.RouteDescription`, Gin, and the provider -package) and `go-ws` (for the hub). These are acceptable — go-process already -imports `core/go` (the Core framework) and the process provider is an optional -sub-package. - -### 2a. Create `/Users/snider/Code/core/go-process/pkg/api/provider.go` - -```go -// SPDX-Licence-Identifier: EUPL-1.2 - -// Package api provides a service provider that wraps go-process daemon -// management as REST endpoints with WebSocket event streaming. -package api - -import ( - "net/http" - "os" - "strconv" - "syscall" - - "forge.lthn.ai/core/go-api" - "forge.lthn.ai/core/go-api/pkg/provider" - process "forge.lthn.ai/core/go-process" - "forge.lthn.ai/core/go-ws" - "github.com/gin-gonic/gin" -) - -// ProcessProvider wraps the go-process daemon Registry as a service provider. -// It implements provider.Provider, provider.Streamable, and provider.Describable. -type ProcessProvider struct { - registry *process.Registry - hub *ws.Hub -} - -// compile-time interface checks -var ( - _ provider.Provider = (*ProcessProvider)(nil) - _ provider.Streamable = (*ProcessProvider)(nil) - _ provider.Describable = (*ProcessProvider)(nil) -) - -// NewProvider creates a process provider backed by the given daemon registry. -// The WS hub is used to emit daemon state change events. Pass nil for hub -// if WebSocket streaming is not needed. -func NewProvider(registry *process.Registry, hub *ws.Hub) *ProcessProvider { - if registry == nil { - registry = process.DefaultRegistry() - } - return &ProcessProvider{ - registry: registry, - hub: hub, - } -} - -// Name implements api.RouteGroup. -func (p *ProcessProvider) Name() string { return "process" } - -// BasePath implements api.RouteGroup. -func (p *ProcessProvider) BasePath() string { return "/api/process" } - -// Channels implements provider.Streamable. -func (p *ProcessProvider) Channels() []string { - return []string{ - "process.daemon.started", - "process.daemon.stopped", - "process.daemon.health", - } -} - -// RegisterRoutes implements api.RouteGroup. -func (p *ProcessProvider) RegisterRoutes(rg *gin.RouterGroup) { - rg.GET("/daemons", p.listDaemons) - rg.GET("/daemons/:code/:daemon", p.getDaemon) - rg.POST("/daemons/:code/:daemon/stop", p.stopDaemon) - rg.GET("/daemons/:code/:daemon/health", p.healthCheck) -} - -// Describe implements api.DescribableGroup. -func (p *ProcessProvider) Describe() []api.RouteDescription { - return []api.RouteDescription{ - { - Method: "GET", - Path: "/daemons", - Summary: "List running daemons", - Description: "Returns all alive daemon entries from the registry, pruning any with dead PIDs.", - Tags: []string{"process"}, - Response: map[string]any{ - "type": "array", - "items": map[string]any{ - "type": "object", - "properties": map[string]any{ - "code": map[string]any{"type": "string"}, - "daemon": map[string]any{"type": "string"}, - "pid": map[string]any{"type": "integer"}, - "health": map[string]any{"type": "string"}, - "project": map[string]any{"type": "string"}, - "binary": map[string]any{"type": "string"}, - "started": map[string]any{"type": "string", "format": "date-time"}, - }, - }, - }, - }, - { - Method: "GET", - Path: "/daemons/:code/:daemon", - Summary: "Get daemon status", - Description: "Returns a single daemon entry if its process is alive.", - Tags: []string{"process"}, - Response: map[string]any{ - "type": "object", - "properties": map[string]any{ - "code": map[string]any{"type": "string"}, - "daemon": map[string]any{"type": "string"}, - "pid": map[string]any{"type": "integer"}, - "health": map[string]any{"type": "string"}, - "started": map[string]any{"type": "string", "format": "date-time"}, - }, - }, - }, - { - Method: "POST", - Path: "/daemons/:code/:daemon/stop", - Summary: "Stop a daemon", - Description: "Sends SIGTERM to the daemon process and removes it from the registry.", - Tags: []string{"process"}, - Response: map[string]any{ - "type": "object", - "properties": map[string]any{ - "stopped": map[string]any{"type": "boolean"}, - }, - }, - }, - { - Method: "GET", - Path: "/daemons/:code/:daemon/health", - Summary: "Check daemon health", - Description: "Probes the daemon's health endpoint and returns the result.", - Tags: []string{"process"}, - Response: map[string]any{ - "type": "object", - "properties": map[string]any{ - "healthy": map[string]any{"type": "boolean"}, - "address": map[string]any{"type": "string"}, - }, - }, - }, - } -} - -// -- Handlers ----------------------------------------------------------------- - -func (p *ProcessProvider) listDaemons(c *gin.Context) { - entries, err := p.registry.List() - if err != nil { - c.JSON(http.StatusInternalServerError, api.Fail("list_failed", err.Error())) - return - } - if entries == nil { - entries = []process.DaemonEntry{} - } - c.JSON(http.StatusOK, api.OK(entries)) -} - -func (p *ProcessProvider) getDaemon(c *gin.Context) { - code := c.Param("code") - daemon := c.Param("daemon") - - entry, ok := p.registry.Get(code, daemon) - if !ok { - c.JSON(http.StatusNotFound, api.Fail("not_found", "daemon not found or not running")) - return - } - c.JSON(http.StatusOK, api.OK(entry)) -} - -func (p *ProcessProvider) stopDaemon(c *gin.Context) { - code := c.Param("code") - daemon := c.Param("daemon") - - entry, ok := p.registry.Get(code, daemon) - if !ok { - c.JSON(http.StatusNotFound, api.Fail("not_found", "daemon not found or not running")) - return - } - - // Send SIGTERM to the process - proc, err := os.FindProcess(entry.PID) - if err != nil { - c.JSON(http.StatusInternalServerError, api.Fail("signal_failed", err.Error())) - return - } - if err := proc.Signal(syscall.SIGTERM); err != nil { - c.JSON(http.StatusInternalServerError, api.Fail("signal_failed", err.Error())) - return - } - - // Remove from registry - _ = p.registry.Unregister(code, daemon) - - // Emit WS event - p.emitEvent("process.daemon.stopped", map[string]any{ - "code": code, - "daemon": daemon, - "pid": entry.PID, - }) - - c.JSON(http.StatusOK, api.OK(map[string]any{"stopped": true})) -} - -func (p *ProcessProvider) healthCheck(c *gin.Context) { - code := c.Param("code") - daemon := c.Param("daemon") - - entry, ok := p.registry.Get(code, daemon) - if !ok { - c.JSON(http.StatusNotFound, api.Fail("not_found", "daemon not found or not running")) - return - } - - if entry.Health == "" { - c.JSON(http.StatusOK, api.OK(map[string]any{ - "healthy": false, - "address": "", - "reason": "no health endpoint configured", - })) - return - } - - healthy := process.WaitForHealth(entry.Health, 2000) - - result := map[string]any{ - "healthy": healthy, - "address": entry.Health, - } - - // Emit health event - p.emitEvent("process.daemon.health", map[string]any{ - "code": code, - "daemon": daemon, - "healthy": healthy, - }) - - statusCode := http.StatusOK - if !healthy { - statusCode = http.StatusServiceUnavailable - } - c.JSON(statusCode, api.OK(result)) -} - -// emitEvent sends a WS event if the hub is available. -func (p *ProcessProvider) emitEvent(channel string, data any) { - if p.hub == nil { - return - } - _ = p.hub.SendToChannel(channel, ws.Message{ - Type: ws.TypeEvent, - Data: data, - }) -} - -// PIDAlive checks whether a PID is still running. Exported for use by -// consumers that need to verify daemon liveness outside the REST API. -func PIDAlive(pid int) bool { - if pid <= 0 { - return false - } - proc, err := os.FindProcess(pid) - if err != nil { - return false - } - return proc.Signal(syscall.Signal(0)) == nil -} - -// intParam parses a URL param as int, returning 0 on failure. -func intParam(c *gin.Context, name string) int { - v, _ := strconv.Atoi(c.Param(name)) - return v -} -``` - -**File created:** `/Users/snider/Code/core/go-process/pkg/api/provider.go` - -**Design decisions:** - -- **No start endpoint.** Daemons are started by their own binaries (e.g. - `core-ide`, `core-php`). The registry tracks what's already running. Adding - a "start daemon" REST endpoint would require knowing the binary path and args, - which is a Phase 2 concern (the `Manageable` interface). -- **SIGTERM for stop.** Graceful shutdown via SIGTERM matches the existing - `Daemon.Run()` pattern which blocks on `<-ctx.Done()` and calls `Stop()`. -- **`pkg/api/` sub-package.** Keeps the provider optional — go-process core - package has zero dependency on Gin or go-api. Only consumers that import - `go-process/pkg/api` pull in the HTTP layer. - ---- - -## Task 3 — Create brain provider - -Create a thin wrapper making the brain `Subsystem` also a `provider.Provider` -with REST endpoints for remember, recall, forget, and list. - -The brain subsystem already proxies to Laravel via the IDE bridge. The provider -adds REST endpoints that call the same bridge methods, making brain accessible -to non-MCP consumers (GUI panels, CLI tools, other services). - -### 3a. Create `/Users/snider/Code/core/mcp/pkg/mcp/brain/provider.go` - -```go -// SPDX-Licence-Identifier: EUPL-1.2 - -package brain - -import ( - "net/http" - - "forge.lthn.ai/core/go-api" - "forge.lthn.ai/core/go-api/pkg/provider" - "forge.lthn.ai/core/go-ws" - "forge.lthn.ai/core/mcp/pkg/mcp/ide" - "github.com/gin-gonic/gin" -) - -// BrainProvider wraps the brain Subsystem as a service provider with REST -// endpoints. It delegates to the same IDE bridge that the MCP tools use. -type BrainProvider struct { - bridge *ide.Bridge - hub *ws.Hub -} - -// compile-time interface checks -var ( - _ provider.Provider = (*BrainProvider)(nil) - _ provider.Streamable = (*BrainProvider)(nil) - _ provider.Describable = (*BrainProvider)(nil) - _ provider.Renderable = (*BrainProvider)(nil) -) - -// NewProvider creates a brain provider that proxies to Laravel via the IDE bridge. -// The WS hub is used to emit brain events. Pass nil for hub if not needed. -func NewProvider(bridge *ide.Bridge, hub *ws.Hub) *BrainProvider { - return &BrainProvider{ - bridge: bridge, - hub: hub, - } -} - -// Name implements api.RouteGroup. -func (p *BrainProvider) Name() string { return "brain" } - -// BasePath implements api.RouteGroup. -func (p *BrainProvider) BasePath() string { return "/api/brain" } - -// Channels implements provider.Streamable. -func (p *BrainProvider) Channels() []string { - return []string{ - "brain.remember.complete", - "brain.recall.complete", - "brain.forget.complete", - } -} - -// Element implements provider.Renderable. -func (p *BrainProvider) Element() provider.ElementSpec { - return provider.ElementSpec{ - Tag: "core-brain-panel", - Source: "/assets/brain-panel.js", - } -} - -// RegisterRoutes implements api.RouteGroup. -func (p *BrainProvider) RegisterRoutes(rg *gin.RouterGroup) { - rg.POST("/remember", p.remember) - rg.POST("/recall", p.recall) - rg.POST("/forget", p.forget) - rg.GET("/list", p.list) - rg.GET("/status", p.status) -} - -// Describe implements api.DescribableGroup. -func (p *BrainProvider) Describe() []api.RouteDescription { - return []api.RouteDescription{ - { - Method: "POST", - Path: "/remember", - Summary: "Store a memory", - Description: "Store a memory in the shared OpenBrain knowledge store via the Laravel backend.", - Tags: []string{"brain"}, - RequestBody: map[string]any{ - "type": "object", - "properties": map[string]any{ - "content": map[string]any{"type": "string"}, - "type": map[string]any{"type": "string"}, - "tags": map[string]any{"type": "array", "items": map[string]any{"type": "string"}}, - "project": map[string]any{"type": "string"}, - "confidence": map[string]any{"type": "number"}, - }, - "required": []string{"content", "type"}, - }, - Response: map[string]any{ - "type": "object", - "properties": map[string]any{ - "success": map[string]any{"type": "boolean"}, - "memoryId": map[string]any{"type": "string"}, - "timestamp": map[string]any{"type": "string", "format": "date-time"}, - }, - }, - }, - { - Method: "POST", - Path: "/recall", - Summary: "Semantic search memories", - Description: "Semantic search across the shared OpenBrain knowledge store.", - Tags: []string{"brain"}, - RequestBody: map[string]any{ - "type": "object", - "properties": map[string]any{ - "query": map[string]any{"type": "string"}, - "top_k": map[string]any{"type": "integer"}, - "filter": map[string]any{ - "type": "object", - "properties": map[string]any{ - "project": map[string]any{"type": "string"}, - "type": map[string]any{"type": "string"}, - }, - }, - }, - "required": []string{"query"}, - }, - Response: map[string]any{ - "type": "object", - "properties": map[string]any{ - "success": map[string]any{"type": "boolean"}, - "count": map[string]any{"type": "integer"}, - "memories": map[string]any{"type": "array"}, - }, - }, - }, - { - Method: "POST", - Path: "/forget", - Summary: "Remove a memory", - Description: "Permanently delete a memory from the knowledge store.", - Tags: []string{"brain"}, - RequestBody: map[string]any{ - "type": "object", - "properties": map[string]any{ - "id": map[string]any{"type": "string"}, - "reason": map[string]any{"type": "string"}, - }, - "required": []string{"id"}, - }, - Response: map[string]any{ - "type": "object", - "properties": map[string]any{ - "success": map[string]any{"type": "boolean"}, - "forgotten": map[string]any{"type": "string"}, - }, - }, - }, - { - Method: "GET", - Path: "/list", - Summary: "List memories", - Description: "List memories with optional filtering by project, type, and agent.", - Tags: []string{"brain"}, - Response: map[string]any{ - "type": "object", - "properties": map[string]any{ - "success": map[string]any{"type": "boolean"}, - "count": map[string]any{"type": "integer"}, - "memories": map[string]any{"type": "array"}, - }, - }, - }, - { - Method: "GET", - Path: "/status", - Summary: "Brain bridge status", - Description: "Returns whether the Laravel bridge is connected.", - Tags: []string{"brain"}, - Response: map[string]any{ - "type": "object", - "properties": map[string]any{ - "connected": map[string]any{"type": "boolean"}, - }, - }, - }, - } -} - -// -- Handlers ----------------------------------------------------------------- - -func (p *BrainProvider) remember(c *gin.Context) { - if p.bridge == nil { - c.JSON(http.StatusServiceUnavailable, api.Fail("bridge_unavailable", "brain bridge not available")) - return - } - - var input RememberInput - if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(http.StatusBadRequest, api.Fail("invalid_input", err.Error())) - return - } - - err := p.bridge.Send(ide.BridgeMessage{ - Type: "brain_remember", - Data: map[string]any{ - "content": input.Content, - "type": input.Type, - "tags": input.Tags, - "project": input.Project, - "confidence": input.Confidence, - "supersedes": input.Supersedes, - "expires_in": input.ExpiresIn, - }, - }) - if err != nil { - c.JSON(http.StatusInternalServerError, api.Fail("bridge_error", err.Error())) - return - } - - p.emitEvent("brain.remember.complete", map[string]any{ - "type": input.Type, - "project": input.Project, - }) - - c.JSON(http.StatusOK, api.OK(map[string]any{"success": true})) -} - -func (p *BrainProvider) recall(c *gin.Context) { - if p.bridge == nil { - c.JSON(http.StatusServiceUnavailable, api.Fail("bridge_unavailable", "brain bridge not available")) - return - } - - var input RecallInput - if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(http.StatusBadRequest, api.Fail("invalid_input", err.Error())) - return - } - - err := p.bridge.Send(ide.BridgeMessage{ - Type: "brain_recall", - Data: map[string]any{ - "query": input.Query, - "top_k": input.TopK, - "filter": input.Filter, - }, - }) - if err != nil { - c.JSON(http.StatusInternalServerError, api.Fail("bridge_error", err.Error())) - return - } - - p.emitEvent("brain.recall.complete", map[string]any{ - "query": input.Query, - }) - - c.JSON(http.StatusOK, api.OK(RecallOutput{ - Success: true, - Memories: []Memory{}, - })) -} - -func (p *BrainProvider) forget(c *gin.Context) { - if p.bridge == nil { - c.JSON(http.StatusServiceUnavailable, api.Fail("bridge_unavailable", "brain bridge not available")) - return - } - - var input ForgetInput - if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(http.StatusBadRequest, api.Fail("invalid_input", err.Error())) - return - } - - err := p.bridge.Send(ide.BridgeMessage{ - Type: "brain_forget", - Data: map[string]any{ - "id": input.ID, - "reason": input.Reason, - }, - }) - if err != nil { - c.JSON(http.StatusInternalServerError, api.Fail("bridge_error", err.Error())) - return - } - - p.emitEvent("brain.forget.complete", map[string]any{ - "id": input.ID, - }) - - c.JSON(http.StatusOK, api.OK(map[string]any{ - "success": true, - "forgotten": input.ID, - })) -} - -func (p *BrainProvider) list(c *gin.Context) { - if p.bridge == nil { - c.JSON(http.StatusServiceUnavailable, api.Fail("bridge_unavailable", "brain bridge not available")) - return - } - - err := p.bridge.Send(ide.BridgeMessage{ - Type: "brain_list", - Data: map[string]any{ - "project": c.Query("project"), - "type": c.Query("type"), - "agent_id": c.Query("agent_id"), - "limit": c.Query("limit"), - }, - }) - if err != nil { - c.JSON(http.StatusInternalServerError, api.Fail("bridge_error", err.Error())) - return - } - - c.JSON(http.StatusOK, api.OK(ListOutput{ - Success: true, - Memories: []Memory{}, - })) -} - -func (p *BrainProvider) status(c *gin.Context) { - connected := false - if p.bridge != nil { - connected = p.bridge.Connected() - } - c.JSON(http.StatusOK, api.OK(map[string]any{ - "connected": connected, - })) -} - -// emitEvent sends a WS event if the hub is available. -func (p *BrainProvider) emitEvent(channel string, data any) { - if p.hub == nil { - return - } - _ = p.hub.SendToChannel(channel, ws.Message{ - Type: ws.TypeEvent, - Data: data, - }) -} -``` - -**File created:** `/Users/snider/Code/core/mcp/pkg/mcp/brain/provider.go` - -**Design decisions:** - -- **Reuses existing types.** The `RememberInput`, `RecallInput`, `ForgetInput`, - `ListInput`, and output types are already defined in `tools.go`. The provider - handlers use the same types for request binding. -- **Same bridge, different transport.** MCP tools call `bridge.Send()` over stdio. - The REST provider calls the same `bridge.Send()` over HTTP. Same backend, two - entry points. -- **Renderable.** Brain declares a `` custom element. The JS - bundle path (`/assets/brain-panel.js`) is a placeholder — the actual element - will be built in Phase 2 when the GUI consumer is implemented. -- **Status endpoint.** The `/status` endpoint returns bridge connection state, - useful for the GUI tray panel's status indicators. - ---- - -## Task 4 — Update core/ide main.go - -Wire the provider registry into the IDE's main.go. Create an `api.Engine`, -register both providers, and serve the API alongside MCP. - -### 4a. Update `/Users/snider/Code/core/ide/main.go` - -Add the provider registry and API engine to all three modes (MCP-only, headless, -GUI). The full updated file: - -```go -package main - -import ( - "context" - "embed" - "io/fs" - "log" - "net/http" - "os" - "os/signal" - "runtime" - "syscall" - - "forge.lthn.ai/core/go-api" - "forge.lthn.ai/core/go-api/pkg/provider" - "forge.lthn.ai/core/config" - process "forge.lthn.ai/core/go-process" - processapi "forge.lthn.ai/core/go-process/pkg/api" - "forge.lthn.ai/core/go-ws" - "forge.lthn.ai/core/go/pkg/core" - guiMCP "forge.lthn.ai/core/gui/pkg/mcp" - "forge.lthn.ai/core/gui/pkg/display" - "forge.lthn.ai/core/ide/icons" - "forge.lthn.ai/core/mcp/pkg/mcp" - "forge.lthn.ai/core/mcp/pkg/mcp/brain" - "forge.lthn.ai/core/mcp/pkg/mcp/ide" - "github.com/wailsapp/wails/v3/pkg/application" -) - -//go:embed all:frontend/dist/wails-angular-template/browser -var assets embed.FS - -func main() { - // ── Flags ────────────────────────────────────────────────── - mcpOnly := false - for _, arg := range os.Args[1:] { - if arg == "--mcp" { - mcpOnly = true - } - } - - // ── Configuration ────────────────────────────────────────── - cfg, _ := config.New() - - cwd, err := os.Getwd() - if err != nil { - log.Fatalf("failed to get working directory: %v", err) - } - - // ── Shared resources (built before Core) ─────────────────── - hub := ws.NewHub() - - bridgeCfg := ide.DefaultConfig() - bridgeCfg.WorkspaceRoot = cwd - if url := os.Getenv("CORE_API_URL"); url != "" { - bridgeCfg.LaravelWSURL = url - } - if token := os.Getenv("CORE_API_TOKEN"); token != "" { - bridgeCfg.Token = token - } - bridge := ide.NewBridge(hub, bridgeCfg) - - // ── Service Provider Registry ────────────────────────────── - reg := provider.NewRegistry() - reg.Add(processapi.NewProvider(process.DefaultRegistry(), hub)) - reg.Add(brain.NewProvider(bridge, hub)) - - // ── API Engine ───────────────────────────────────────────── - apiAddr := ":9880" - if addr := os.Getenv("CORE_API_ADDR"); addr != "" { - apiAddr = addr - } - engine, _ := api.New( - api.WithAddr(apiAddr), - api.WithCORS("*"), - api.WithWSHandler(http.Handler(hub.Handler())), - api.WithSwagger("Core IDE", "Service Provider API", "0.1.0"), - ) - reg.MountAll(engine) - - // ── Core framework ───────────────────────────────────────── - c, err := core.New( - core.WithName("ws", func(c *core.Core) (any, error) { - return hub, nil - }), - core.WithService(display.Register(nil)), // nil platform until Wails starts - core.WithName("mcp", func(c *core.Core) (any, error) { - return mcp.New( - mcp.WithWorkspaceRoot(cwd), - mcp.WithWSHub(hub), - mcp.WithSubsystem(brain.New(bridge)), - mcp.WithSubsystem(guiMCP.New(c)), - ) - }), - ) - if err != nil { - log.Fatalf("failed to create core: %v", err) - } - - // Retrieve the MCP service for transport control - mcpSvc, err := core.ServiceFor[*mcp.Service](c, "mcp") - if err != nil { - log.Fatalf("failed to get MCP service: %v", err) - } - - // ── Mode selection ───────────────────────────────────────── - if mcpOnly { - // stdio mode — Claude Code connects via --mcp flag - ctx, cancel := signal.NotifyContext(context.Background(), - syscall.SIGINT, syscall.SIGTERM) - defer cancel() - - if err := c.ServiceStartup(ctx, nil); err != nil { - log.Fatalf("core startup failed: %v", err) - } - bridge.Start(ctx) - go hub.Run(ctx) - - // Start API server in background for provider endpoints - go func() { - if err := engine.Serve(ctx); err != nil { - log.Printf("API server error: %v", err) - } - }() - - if err := mcpSvc.ServeStdio(ctx); err != nil { - log.Printf("MCP stdio error: %v", err) - } - - _ = mcpSvc.Shutdown(ctx) - _ = c.ServiceShutdown(ctx) - return - } - - if !guiEnabled(cfg) { - // No GUI — run Core with MCP transport in background - ctx, cancel := signal.NotifyContext(context.Background(), - syscall.SIGINT, syscall.SIGTERM) - defer cancel() - - if err := c.ServiceStartup(ctx, nil); err != nil { - log.Fatalf("core startup failed: %v", err) - } - bridge.Start(ctx) - go hub.Run(ctx) - - // Start API server - go func() { - log.Printf("API server listening on %s", apiAddr) - if err := engine.Serve(ctx); err != nil { - log.Printf("API server error: %v", err) - } - }() - - go func() { - if err := mcpSvc.Run(ctx); err != nil { - log.Printf("MCP error: %v", err) - } - }() - - <-ctx.Done() - shutdownCtx := context.Background() - _ = mcpSvc.Shutdown(shutdownCtx) - _ = c.ServiceShutdown(shutdownCtx) - return - } - - // ── GUI mode ─────────────────────────────────────────────── - staticAssets, err := fs.Sub(assets, "frontend/dist/wails-angular-template/browser") - if err != nil { - log.Fatal(err) - } - - app := application.New(application.Options{ - Name: "Core IDE", - Description: "Host UK Core IDE - Development Environment", - Services: []application.Service{ - application.NewService(c), - }, - Assets: application.AssetOptions{ - Handler: application.AssetFileServerFS(staticAssets), - }, - Mac: application.MacOptions{ - ActivationPolicy: application.ActivationPolicyAccessory, - }, - OnShutdown: func() { - ctx := context.Background() - _ = mcpSvc.Shutdown(ctx) - bridge.Shutdown() - }, - }) - - // System tray - systray := app.SystemTray.New() - systray.SetTooltip("Core IDE") - - if runtime.GOOS == "darwin" { - systray.SetTemplateIcon(icons.AppTray) - } else { - systray.SetDarkModeIcon(icons.AppTray) - systray.SetIcon(icons.AppTray) - } - - // Tray panel window - trayWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{ - Name: "tray-panel", - Title: "Core IDE", - Width: 380, - Height: 480, - URL: "/tray", - Hidden: true, - Frameless: true, - BackgroundColour: application.NewRGB(26, 27, 38), - }) - systray.AttachWindow(trayWindow).WindowOffset(5) - - // Tray menu - trayMenu := app.Menu.New() - trayMenu.Add("Quit").OnClick(func(ctx *application.Context) { - app.Quit() - }) - systray.SetMenu(trayMenu) - - // Start MCP transport and API server alongside Wails - go func() { - ctx := context.Background() - bridge.Start(ctx) - go hub.Run(ctx) - - // Start API server - go func() { - log.Printf("API server listening on %s", apiAddr) - if err := engine.Serve(ctx); err != nil { - log.Printf("API server error: %v", err) - } - }() - - if err := mcpSvc.Run(ctx); err != nil { - log.Printf("MCP error: %v", err) - } - }() - - log.Println("Starting Core IDE...") - - if err := app.Run(); err != nil { - log.Fatal(err) - } -} - -// guiEnabled checks whether the GUI should start. -// Returns false if config says gui.enabled: false, or if no display is available. -func guiEnabled(cfg *config.Config) bool { - if cfg != nil { - var guiCfg struct { - Enabled *bool `mapstructure:"enabled"` - } - if err := cfg.Get("gui", &guiCfg); err == nil && guiCfg.Enabled != nil { - return *guiCfg.Enabled - } - } - // Fall back to display detection - if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { - return true - } - return os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != "" -} -``` - -### 4b. Update go.mod - -Add direct dependencies for `go-api` and `go-process`: - -```bash -cd /Users/snider/Code/core/ide -# go-api and go-process need to be direct dependencies now -go get forge.lthn.ai/core/go-api -go get forge.lthn.ai/core/go-process -go mod tidy -cd /Users/snider/Code -go work sync -``` - -The `go.mod` `require` block gains: - -``` -forge.lthn.ai/core/go-api v0.1.0 -forge.lthn.ai/core/go-process v0.1.0 -``` - -Both were already indirect dependencies; they become direct since `main.go` now -imports them. - -### Key changes from the current main.go - -1. **Provider registry** — `provider.NewRegistry()` collects both providers. -2. **API engine** — `api.New()` with CORS, Swagger, and WS handler creates the - Gin router. `reg.MountAll(engine)` registers both providers as route groups. -3. **API server** — `engine.Serve(ctx)` runs in a goroutine alongside MCP in all - three modes, on port 9880 (configurable via `CORE_API_ADDR`). -4. **WS hub** — `go hub.Run(ctx)` is started explicitly. Previously this was not - called — the hub was only used for MCP bridge dispatch. -5. **Imports** — `go-api`, `go-api/pkg/provider`, `go-process`, and - `go-process/pkg/api` are new direct imports. - ---- - -## Task 5 — Tests for registry + go-process provider - -### 5a. Create `/Users/snider/Code/core/go-api/pkg/provider/registry_test.go` - -```go -// SPDX-Licence-Identifier: EUPL-1.2 - -package provider_test - -import ( - "testing" - - "forge.lthn.ai/core/go-api" - "forge.lthn.ai/core/go-api/pkg/provider" - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// -- Test helpers (minimal providers) ----------------------------------------- - -type stubProvider struct{} - -func (s *stubProvider) Name() string { return "stub" } -func (s *stubProvider) BasePath() string { return "/api/stub" } -func (s *stubProvider) RegisterRoutes(rg *gin.RouterGroup) {} - -type streamableProvider struct{ stubProvider } - -func (s *streamableProvider) Channels() []string { return []string{"stub.event"} } - -type describableProvider struct{ stubProvider } - -func (d *describableProvider) Describe() []api.RouteDescription { - return []api.RouteDescription{ - {Method: "GET", Path: "/items", Summary: "List items", Tags: []string{"stub"}}, - } -} - -type renderableProvider struct{ stubProvider } - -func (r *renderableProvider) Element() provider.ElementSpec { - return provider.ElementSpec{Tag: "core-stub-panel", Source: "/assets/stub.js"} -} - -type fullProvider struct { - streamableProvider -} - -func (f *fullProvider) Name() string { return "full" } -func (f *fullProvider) BasePath() string { return "/api/full" } -func (f *fullProvider) Describe() []api.RouteDescription { - return []api.RouteDescription{ - {Method: "GET", Path: "/status", Summary: "Status", Tags: []string{"full"}}, - } -} -func (f *fullProvider) Element() provider.ElementSpec { - return provider.ElementSpec{Tag: "core-full-panel", Source: "/assets/full.js"} -} - -// -- Tests -------------------------------------------------------------------- - -func TestRegistry_Add_Good(t *testing.T) { - reg := provider.NewRegistry() - assert.Equal(t, 0, reg.Len()) - - reg.Add(&stubProvider{}) - assert.Equal(t, 1, reg.Len()) - - reg.Add(&streamableProvider{}) - assert.Equal(t, 2, reg.Len()) -} - -func TestRegistry_Get_Good(t *testing.T) { - reg := provider.NewRegistry() - reg.Add(&stubProvider{}) - - p := reg.Get("stub") - require.NotNil(t, p) - assert.Equal(t, "stub", p.Name()) -} - -func TestRegistry_Get_Bad(t *testing.T) { - reg := provider.NewRegistry() - p := reg.Get("nonexistent") - assert.Nil(t, p) -} - -func TestRegistry_List_Good(t *testing.T) { - reg := provider.NewRegistry() - reg.Add(&stubProvider{}) - reg.Add(&streamableProvider{}) - - list := reg.List() - assert.Len(t, list, 2) -} - -func TestRegistry_MountAll_Good(t *testing.T) { - reg := provider.NewRegistry() - reg.Add(&stubProvider{}) - reg.Add(&streamableProvider{}) - - engine, err := api.New() - require.NoError(t, err) - - reg.MountAll(engine) - assert.Len(t, engine.Groups(), 2) -} - -func TestRegistry_Streamable_Good(t *testing.T) { - reg := provider.NewRegistry() - reg.Add(&stubProvider{}) // not streamable - reg.Add(&streamableProvider{}) // streamable - - s := reg.Streamable() - assert.Len(t, s, 1) - assert.Equal(t, []string{"stub.event"}, s[0].Channels()) -} - -func TestRegistry_Describable_Good(t *testing.T) { - reg := provider.NewRegistry() - reg.Add(&stubProvider{}) // not describable - reg.Add(&describableProvider{}) // describable - - d := reg.Describable() - assert.Len(t, d, 1) - assert.Len(t, d[0].Describe(), 1) -} - -func TestRegistry_Renderable_Good(t *testing.T) { - reg := provider.NewRegistry() - reg.Add(&stubProvider{}) // not renderable - reg.Add(&renderableProvider{}) // renderable - - r := reg.Renderable() - assert.Len(t, r, 1) - assert.Equal(t, "core-stub-panel", r[0].Element().Tag) -} - -func TestRegistry_Info_Good(t *testing.T) { - reg := provider.NewRegistry() - reg.Add(&fullProvider{}) - - infos := reg.Info() - require.Len(t, infos, 1) - - info := infos[0] - assert.Equal(t, "full", info.Name) - assert.Equal(t, "/api/full", info.BasePath) - assert.Equal(t, []string{"stub.event"}, info.Channels) - require.NotNil(t, info.Element) - assert.Equal(t, "core-full-panel", info.Element.Tag) -} - -func TestRegistry_Iter_Good(t *testing.T) { - reg := provider.NewRegistry() - reg.Add(&stubProvider{}) - reg.Add(&streamableProvider{}) - - count := 0 - for range reg.Iter() { - count++ - } - assert.Equal(t, 2, count) -} -``` - -### 5b. Create `/Users/snider/Code/core/go-process/pkg/api/provider_test.go` - -```go -// SPDX-Licence-Identifier: EUPL-1.2 - -package api_test - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - goapi "forge.lthn.ai/core/go-api" - processapi "forge.lthn.ai/core/go-process/pkg/api" - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func init() { - gin.SetMode(gin.TestMode) -} - -func TestProcessProvider_Name_Good(t *testing.T) { - p := processapi.NewProvider(nil, nil) - assert.Equal(t, "process", p.Name()) -} - -func TestProcessProvider_BasePath_Good(t *testing.T) { - p := processapi.NewProvider(nil, nil) - assert.Equal(t, "/api/process", p.BasePath()) -} - -func TestProcessProvider_Channels_Good(t *testing.T) { - p := processapi.NewProvider(nil, nil) - channels := p.Channels() - assert.Contains(t, channels, "process.daemon.started") - assert.Contains(t, channels, "process.daemon.stopped") - assert.Contains(t, channels, "process.daemon.health") -} - -func TestProcessProvider_Describe_Good(t *testing.T) { - p := processapi.NewProvider(nil, nil) - descs := p.Describe() - assert.GreaterOrEqual(t, len(descs), 4) - - // Verify all descriptions have required fields - for _, d := range descs { - assert.NotEmpty(t, d.Method) - assert.NotEmpty(t, d.Path) - assert.NotEmpty(t, d.Summary) - assert.NotEmpty(t, d.Tags) - } -} - -func TestProcessProvider_ListDaemons_Good(t *testing.T) { - // Use a temp directory so the registry has no daemons - dir := t.TempDir() - registry := newTestRegistry(dir) - p := processapi.NewProvider(registry, nil) - - r := setupRouter(p) - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/process/daemons", nil) - r.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - - var resp goapi.Response[[]any] - err := json.Unmarshal(w.Body.Bytes(), &resp) - require.NoError(t, err) - assert.True(t, resp.Success) -} - -func TestProcessProvider_GetDaemon_Bad(t *testing.T) { - dir := t.TempDir() - registry := newTestRegistry(dir) - p := processapi.NewProvider(registry, nil) - - r := setupRouter(p) - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/process/daemons/test/nonexistent", nil) - r.ServeHTTP(w, req) - - assert.Equal(t, http.StatusNotFound, w.Code) -} - -func TestProcessProvider_RegistersAsRouteGroup_Good(t *testing.T) { - p := processapi.NewProvider(nil, nil) - - engine, err := goapi.New() - require.NoError(t, err) - - engine.Register(p) - assert.Len(t, engine.Groups(), 1) - assert.Equal(t, "process", engine.Groups()[0].Name()) -} - -func TestProcessProvider_Channels_RegisterAsStreamGroup_Good(t *testing.T) { - p := processapi.NewProvider(nil, nil) - - engine, err := goapi.New() - require.NoError(t, err) - - engine.Register(p) - - // Engine.Channels() discovers StreamGroups - channels := engine.Channels() - assert.Contains(t, channels, "process.daemon.started") -} - -// -- Test helpers ------------------------------------------------------------- - -func setupRouter(p *processapi.ProcessProvider) *gin.Engine { - r := gin.New() - rg := r.Group(p.BasePath()) - p.RegisterRoutes(rg) - return r -} - -// newTestRegistry creates a process.Registry backed by a test directory. -// This uses the exported constructor — no internal access needed. -func newTestRegistry(dir string) *process.Registry { - return process.NewRegistry(dir) -} -``` - -Note: The test file imports `process` as the package name, which matches the -go-process module's package declaration. Add this import: - -```go -import process "forge.lthn.ai/core/go-process" -``` - -The `newTestRegistry` helper must be adjusted in the actual import block: - -```go -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - goapi "forge.lthn.ai/core/go-api" - process "forge.lthn.ai/core/go-process" - processapi "forge.lthn.ai/core/go-process/pkg/api" - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) -``` - ---- - -## Task 6 — Build verification - -### 6a. Create go.mod for go-process sub-package (if needed) - -The go-process module (`forge.lthn.ai/core/go-process`) currently has minimal -dependencies (only `testify`). The `pkg/api/` sub-package imports `go-api` and -`go-ws`, but since it is part of the same Go module, those imports are resolved -via the module's `go.mod`. Update go-process's `go.mod`: - -```bash -cd /Users/snider/Code/core/go-process -go get forge.lthn.ai/core/go-api -go get forge.lthn.ai/core/go-ws -go get github.com/gin-gonic/gin -go mod tidy -``` - -### 6b. Create go.mod for go-api sub-package (if needed) - -The `pkg/provider/` sub-package lives within the `go-api` module, so no separate -`go.mod` is needed. It imports only its parent package (`forge.lthn.ai/core/go-api`) -which is the module itself. - -### 6c. Build all affected modules - -```bash -# Build the provider package -cd /Users/snider/Code/core/go-api -go build ./... - -# Build the process provider -cd /Users/snider/Code/core/go-process -go build ./... - -# Build the brain provider (part of mcp module) -cd /Users/snider/Code/core/mcp -go build ./... - -# Build the IDE -cd /Users/snider/Code/core/ide -go build ./... - -# Sync workspace -cd /Users/snider/Code -go work sync -``` - -### 6d. Run tests - -```bash -# Provider registry tests -cd /Users/snider/Code/core/go-api -go test ./pkg/provider/... -v - -# Process provider tests -cd /Users/snider/Code/core/go-process -go test ./pkg/api/... -v - -# Existing go-api tests still pass -cd /Users/snider/Code/core/go-api -go test ./... -v - -# Existing go-process tests still pass -cd /Users/snider/Code/core/go-process -go test ./... -v -``` - -### 6e. Verify OpenAPI generation - -```bash -# Quick smoke test — start the IDE and check Swagger -cd /Users/snider/Code/core/ide -go run . & -sleep 2 -curl -s http://localhost:9880/swagger/doc.json | jq '.paths | keys' -# Should show: /api/brain/*, /api/process/*, /health -kill %1 -``` - -Expected paths in the OpenAPI spec: - -``` -/api/brain/forget -/api/brain/list -/api/brain/recall -/api/brain/remember -/api/brain/status -/api/process/daemons -/api/process/daemons/:code/:daemon -/api/process/daemons/:code/:daemon/health -/api/process/daemons/:code/:daemon/stop -/health -``` - -### Troubleshooting - -1. **Import cycle** — `pkg/provider/` imports `go-api` (parent module). This is - fine: Go allows sub-packages to import their parent package. The parent - package does NOT import `pkg/provider/`, so there is no cycle. - -2. **Workspace resolution** — If `go build` cannot find sibling modules, ensure - `~/Code/go.work` includes all relevant paths: - ```bash - cd /Users/snider/Code - go work use ./core/go-api ./core/go-process ./core/mcp ./core/ide - go work sync - ``` - -3. **Stale go.sum** — Clear and regenerate: - ```bash - cd /Users/snider/Code/core/go-process - rm go.sum && go mod tidy - ``` - -4. **gin.SetMode** — Tests should call `gin.SetMode(gin.TestMode)` in `init()` - to suppress Gin's debug logging. diff --git a/docs/superpowers/specs/2026-03-14-api-polyglot-merge-design.md b/docs/superpowers/specs/2026-03-14-api-polyglot-merge-design.md deleted file mode 100644 index d31a939..0000000 --- a/docs/superpowers/specs/2026-03-14-api-polyglot-merge-design.md +++ /dev/null @@ -1,154 +0,0 @@ -# core/api Polyglot Merge — Go + PHP API in One Repo - -**Date:** 2026-03-14 -**Status:** Approved -**Pattern:** Same as core/mcp merge (2026-03-09) - -## Problem - -The API layer is split across two repos: -- `core/go-api` — Gin REST framework, OpenAPI, ToolBridge, SDK codegen, WS/SSE -- `core/php-api` — Laravel REST module, rate limiting, webhooks, OAuth, API docs - -These serve the same purpose in their respective stacks. The provider framework -(previous spec) needs a single `core/api` that both Go and PHP packages register -into. Same problem core/mcp solved by merging. - -## Solution - -Merge both into `core/api` (`ssh://git@forge.lthn.ai:2223/core/api.git`), -following the exact pattern established by core/mcp. - -## Target Structure - -``` -core/api/ -├── go.mod # module forge.lthn.ai/core/api -├── composer.json # name: lthn/api -├── .gitattributes # export-ignore Go files for Composer -├── CLAUDE.md -├── pkg/ # Go packages -│ ├── api/ # Engine, RouteGroup, middleware (from go-api root) -│ ├── provider/ # Provider framework (new, from previous spec) -│ └── openapi/ # SpecBuilder, codegen (from go-api root) -├── cmd/ -│ └── core-api/ # Standalone binary (if needed) -├── src/ -│ └── php/ -│ └── src/ -│ ├── Api/ # Boot, Controllers, Middleware, etc. (from php-api/src/Api/) -│ ├── Front/Api/ # Frontend API routes (from php-api/src/Front/) -│ └── Website/Api/ # Website API routes (from php-api/src/Website/) -└── docs/ -``` - -## Go Module - -```go -module forge.lthn.ai/core/api -``` - -All current consumers importing `forge.lthn.ai/core/go-api` will need to update -to `forge.lthn.ai/core/api/pkg/api` (or `forge.lthn.ai/core/api` if we keep -packages at root level — see decision below). - -### Package Layout Decision - -**Option A**: Packages under `pkg/api/` — clean but changes all import paths. -**Option B**: Packages at root (like go-api currently) — no import path change -if module path stays `forge.lthn.ai/core/api`. - -**Recommendation: B** — keep Go files at repo root. The module path changes from -`forge.lthn.ai/core/go-api` to `forge.lthn.ai/core/api`. This is a one-line -change in each consumer's import. The PHP code lives under `src/php/` and -doesn't conflict. - -``` -core/api/ -├── api.go, group.go, openapi.go, ... # Go source at root (same as go-api today) -├── pkg/provider/ # New provider framework -├── cmd/core-api/ # Binary -├── src/php/ # PHP source -├── composer.json -├── go.mod -└── .gitattributes -``` - -## Composer Package - -```json -{ - "name": "lthn/api", - "autoload": { - "psr-4": { - "Core\\Api\\": "src/php/src/Api/", - "Core\\Website\\Api\\": "src/php/src/Website/Api/" - } - }, - "replace": { - "lthn/php-api": "self.version", - "core/php-api": "self.version" - } -} -``` - -Note: No `Core\Front\Api\` namespace — php-api has no `src/Front/` directory. -Add it later if a frontend API boot provider is needed. - -## .gitattributes - -Same pattern as core/mcp — exclude Go files from Composer installs: - -``` -*.go export-ignore -go.mod export-ignore -go.sum export-ignore -cmd/ export-ignore -pkg/ export-ignore -.core/ export-ignore -src/php/tests/ export-ignore -``` - -## Migration Steps - -1. **Clone core/api** (empty repo, just created) -2. **Copy Go code** from go-api → core/api root (all .go files, cmd/, docs/) -3. **Copy PHP code** from php-api/src/ → core/api/src/php/src/ -4. **Copy PHP config** — composer.json, phpunit.xml, tests -5. **Update go.mod** — change module path to `forge.lthn.ai/core/api` -6. **Create .gitattributes** — export-ignore Go files -7. **Create composer.json** — lthn/api with PSR-4 autoload -8. **Update consumers** — sed import paths in all repos that import go-api -9. **Add provider framework** — `pkg/provider/` from the previous spec -10. **Archive go-api and php-api** on forge (keep for backward compat period) - -## Consumer Updates - -Repos that import `forge.lthn.ai/core/go-api`: -- core/go-ai (ToolBridge, API routes) -- core/go-ml (API route group) -- core/mcp (bridge.go) -- core/agent (indirect, v0.0.3) -- core/ide (via provider framework) - -Each needs: `s/forge.lthn.ai\/core\/go-api/forge.lthn.ai\/core\/api/g` in -imports and go.mod. - -PHP consumers via Composer — update `lthn/php-api` → `lthn/api` in -composer.json. Namespace stays `Core\Api\` (unchanged). - -## Go Workspace - -Add `core/api` to `~/Code/go.work`, remove `core/go-api` entry. - -## Testing - -- `go test ./...` in core/api -- `composer test` in core/api (runs PHP tests via phpunit) -- Build verification across consumer repos after import path update - -## Not In Scope - -- Provider framework implementation (separate spec, separate plan) -- New API features — this is a structural merge only -- TypeScript API layer (future Phase 3) diff --git a/docs/superpowers/specs/2026-03-14-gui-app-shell-design.md b/docs/superpowers/specs/2026-03-14-gui-app-shell-design.md deleted file mode 100644 index aa52ec3..0000000 --- a/docs/superpowers/specs/2026-03-14-gui-app-shell-design.md +++ /dev/null @@ -1,226 +0,0 @@ -# GUI App Shell — Framework-Level Application Frame - -**Date:** 2026-03-14 -**Status:** Approved -**Depends on:** Service Provider Framework, Runtime Provider Loading -**Source:** Port from core-gui/cmd/lthn-desktop/frontend - -## Problem - -core/ide has a bare Angular frontend with placeholder routes. The real app -shell exists in the archived `core-gui/cmd/lthn-desktop/frontend` with -HLCRF layout, Web Awesome components, sidebar navigation, feature flags, -i18n, and custom element support. It needs to live in `core/gui/ui/` as a -framework component that any Wails app can import. - -## Solution - -Port the application frame from lthn-desktop into `core/gui/ui/` as a -reusable Angular library. Add a `ProviderDiscoveryService` that dynamically -populates navigation and loads custom elements from registered providers. - -## Architecture - -``` -core/gui/ui/ <- framework (npm package) - src/ - frame/ - application-frame.ts <- HLCRF shell (header, sidebar, content, footer) - application-frame.html <- wa-page template with slots - system-tray-frame.ts <- tray panel (380x480 frameless) - services/ - provider-discovery.ts <- fetch providers, load custom elements - websocket.ts <- WS connection with reconnect - api-config.ts <- API base URL configuration - i18n.ts <- translation service - components/ - provider-host.ts <- wrapper that hosts a custom element - provider-nav.ts <- dynamic sidebar from providers - status-bar.ts <- footer with provider status - index.ts <- public API exports - package.json - tsconfig.json - -core/ide/frontend/ <- application (imports framework) - src/ - app/ - app.routes.ts <- routes using framework components - app.config.ts <- provider registrations - main.ts - angular.json -``` - -## Provider Discovery Service - -The core service that makes everything dynamic: - -```typescript -@Injectable({ providedIn: 'root' }) -export class ProviderDiscoveryService { - private providers = signal([]); - readonly providers$ = this.providers.asReadonly(); - - constructor(private apiConfig: ApiConfigService) {} - - async discover(): Promise { - const res = await fetch(`${this.apiConfig.baseUrl}/api/v1/providers`); - const data = await res.json(); - this.providers.set(data.providers); - - // Load custom elements for Renderable providers - for (const p of data.providers) { - if (p.element?.tag && p.element?.source) { - await this.loadElement(p.element.tag, p.element.source); - } - } - } - - private async loadElement(tag: string, source: string): Promise { - if (customElements.get(tag)) return; - const script = document.createElement('script'); - script.type = 'module'; - script.src = source; - document.head.appendChild(script); - await customElements.whenDefined(tag); - } -} - -interface ProviderInfo { - name: string; - basePath: string; - status?: string; - element?: { tag: string; source: string }; - channels?: string[]; -} -``` - -## Application Frame - -Ported from lthn-desktop with these changes: - -1. **Navigation is dynamic** — populated from ProviderDiscoveryService -2. **Content area hosts custom elements** — ProviderHostComponent wraps any custom element -3. **Feature flags from config** — reads from core/config API -4. **Web Awesome** — keeps wa-page, wa-button design system -5. **Font Awesome Pro** — keeps icon system -6. **i18n** — keeps translation service -7. **HLCRF slots** — header, navigation, main, footer map to wa-page slots - -### Dynamic Navigation - -```typescript -// In application-frame.ts -async ngOnInit() { - await this.providerService.discover(); - - this.navigation = this.providerService.providers$() - .filter(p => p.element) - .map(p => ({ - name: p.name, - href: p.name.toLowerCase(), - icon: 'fa-regular fa-puzzle-piece', - element: p.element - })); -} -``` - -### Provider Host Component - -Renders any custom element by tag name using Angular's Renderer2 for safe DOM manipulation: - -```typescript -@Component({ - selector: 'provider-host', - template: '
', - schemas: [CUSTOM_ELEMENTS_SCHEMA] -}) -export class ProviderHostComponent implements OnChanges { - @Input() tag!: string; - @Input() apiUrl = ''; - @ViewChild('container') container!: ElementRef; - - constructor(private renderer: Renderer2) {} - - ngOnChanges() { - const native = this.container.nativeElement; - // Clear previous element safely - while (native.firstChild) { - this.renderer.removeChild(native, native.firstChild); - } - // Create and append custom element - const el = this.renderer.createElement(this.tag); - if (this.apiUrl) this.renderer.setAttribute(el, 'api-url', this.apiUrl); - this.renderer.appendChild(native, el); - } -} -``` - -### Routes - -```typescript -export const routes: Routes = [ - { path: 'tray', component: SystemTrayFrame }, - { - path: '', - component: ApplicationFrame, - children: [ - { path: ':provider', component: ProviderHostComponent }, - { path: '', redirectTo: 'process', pathMatch: 'full' } - ] - } -]; -``` - -## System Tray Frame - -380x480 frameless panel showing: -- Provider status cards (from discovery service) -- Quick stats from Streamable providers (via WS) -- Brain connection status -- MCP server status - -## What to Port from lthn-desktop - -| Source | Target | Changes | -|--------|--------|---------| -| frame/application.frame.ts | core/gui/ui/src/frame/ | Dynamic nav from providers | -| frame/application.frame.html | core/gui/ui/src/frame/ | Keep wa-page template | -| frame/system-tray.frame.ts | core/gui/ui/src/frame/ | Add provider status cards | -| services/translation.service.ts | core/gui/ui/src/services/ | Keep as-is | -| services/i18n.service.ts | core/gui/ui/src/services/ | Keep as-is | - -## What NOT to Port - -- blockchain/ — that is a provider, not framework -- mining/ — that is a provider (already has its own elements) -- developer/ — that is a provider -- system/setup* — future setup wizard provider -- Wails bindings (@lthn/core/*) — replaced by REST API calls - -## Dependencies - -### core/gui/ui (npm) -- @angular/core, @angular/router, @angular/common -- @awesome.me/webawesome (design system) -- @fortawesome/fontawesome-pro (icons) - -### core/ide/frontend (npm) -- core/gui/ui (local dependency) -- Angular 20+ - -## Build - -```bash -# Build framework -cd core/gui/ui && npm run build - -# Build IDE app (imports framework) -cd core/ide/frontend && npm run build -``` - -## Not In Scope - -- Setup wizard (future provider) -- Monaco editor integration (future provider) -- Blockchain dashboard (future provider) -- Theming system (future — Web Awesome handles dark mode) diff --git a/docs/superpowers/specs/2026-03-14-ide-modernisation-design.md b/docs/superpowers/specs/2026-03-14-ide-modernisation-design.md deleted file mode 100644 index ccbcf90..0000000 --- a/docs/superpowers/specs/2026-03-14-ide-modernisation-design.md +++ /dev/null @@ -1,235 +0,0 @@ -# core/ide Modernisation — Lego Rewire - -**Date:** 2026-03-14 -**Status:** Approved - -## Problem - -core/ide was built before the ecosystem packages existed. It hand-rolls an MCP bridge, webview service, brain tools, and WebSocket relay that are now properly implemented in dedicated packages. The codebase is ~1,200 lines of duplicated logic. - -## Solution - -Replace all hand-rolled services with imports from the existing ecosystem. The IDE becomes a thin Wails shell that wires up `core.Core` with registered services. - -## Architecture - -``` -┌─────────────────────────────────────────────┐ -│ Wails 3 │ -│ ┌──────────┐ ┌──────────────────────────┐ │ -│ │ Systray │ │ Angular Frontend │ │ -│ │ (lifecycle)│ │ /tray (control pane) │ │ -│ │ │ │ /ide (full IDE) │ │ -│ └────┬─────┘ └──────────┬───────────────┘ │ -│ │ │ WS │ -└───────┼───────────────────┼─────────────────┘ - │ │ -┌───────┼───────────────────┼─────────────────┐ -│ │ core.Core │ │ -│ ┌────┴─────┐ ┌─────────┴───────┐ │ -│ │ display │ │ go-ws Hub │ │ -│ │ Service │ │ (Angular comms) │ │ -│ │(core/gui)│ └─────────────────┘ │ -│ └──────────┘ │ -│ ┌──────────────────────────────────────┐ │ -│ │ core/mcp Service │ │ -│ │ ├─ brain subsystem (OpenBrain) │ │ -│ │ ├─ gui subsystem (74 tools) │ │ -│ │ ├─ file ops (go-io sandboxed) │ │ -│ │ └─ transports: stdio, TCP, Unix │ │ -│ └──────────────────────────────────────┘ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ config │ │go-process│ │ go-log │ │ -│ └──────────┘ └──────────┘ └──────────┘ │ -└─────────────────────────────────────────────┘ - │ - │ stdio (--mcp flag) - │ -┌───────┴─────────────────────────────────────┐ -│ Claude Code / other MCP clients │ -└─────────────────────────────────────────────┘ -``` - -## Application Lifecycle - -- **Systray** is the app lifecycle. Quit from tray menu = shutdown Core. -- **Tray panel** is a 380x480 frameless Angular window attached to the tray icon. Renders a control pane (MCP status, connected agents, brain stats). -- **Windows** are disposable views — closing a window does not terminate the app. -- **macOS**: Accessory app (no Dock icon). Template tray icon. -- **config** reads `.core/config.yaml`. If `gui.enabled: false` (or no display), Wails is skipped entirely. Core still runs all services — MCP server, brain, etc. - -## Service Wiring - -Services are registered as factory functions via `core.WithService`. Each factory -receives the `*core.Core` instance and returns the service. Construction order -matters — the WS hub and IDE bridge must be built before the MCP service that -depends on them. - -```go -func main() { - cfg, _ := config.New() - - // Shared resources built before Core - hub := ws.NewHub() - bridgeCfg := ide.ConfigFromEnv() - bridge := ide.NewBridge(hub, bridgeCfg) - - // Core framework — services run regardless of GUI - c, _ := core.New( - core.WithService(func(c *core.Core) (any, error) { - return hub, nil // go-ws hub for Angular + bridge - }), - core.WithService(display.Register(nil)), // core/gui — nil platform until Wails starts - core.WithService(func(c *core.Core) (any, error) { - // MCP service with subsystems - return mcp.New( - mcp.WithWorkspaceRoot(cwd), - mcp.WithSubsystem(brain.New(bridge)), - mcp.WithSubsystem(guiMCP.New(c)), - ) - }), - ) - - if guiEnabled(cfg) { - startWailsApp(c, hub, bridge) - } else { - // No GUI — start Core lifecycle manually - ctx, cancel := signal.NotifyContext(context.Background(), - syscall.SIGINT, syscall.SIGTERM) - defer cancel() - startMCP(ctx, c) // goroutine: mcpSvc.ServeStdio or ServeTCP - bridge.Start(ctx) - <-ctx.Done() - } -} -``` - -### MCP Lifecycle - -`mcp.Service` does not implement `core.Startable`/`core.Stoppable`. The MCP -transport must be started explicitly: - -- **GUI mode**: `startWailsApp` starts `mcpSvc.ServeStdio(ctx)` or - `mcpSvc.ServeTCP(ctx, addr)` in a goroutine alongside the Wails event loop. - Wails quit triggers `mcpSvc.Shutdown(ctx)`. -- **No-GUI mode**: `startMCP(ctx, c)` starts the transport in a goroutine. - Context cancellation (SIGINT/SIGTERM) triggers shutdown. - -### Version Alignment - -After updating `go.mod` to import `core/mcp`, `core/gui`, etc., run -`go work sync` to align indirect dependency versions across the workspace. - -## MCP Server - -The `core/mcp` package provides the standard MCP protocol server with three transports: - -- **stdio** — for Claude Code integration via `.claude/.mcp.json` -- **TCP** — for network MCP clients (port from config, default 9877) -- **Unix socket** — for local IPC - -Claude Code connects via stdio: - -```json -{ - "mcpServers": { - "core-ide": { - "type": "stdio", - "command": "core-ide", - "args": ["--mcp"] - } - } -} -``` - -## Brain Subsystem - -Already implemented in `core/mcp/pkg/mcp/brain`. Proxies to Laravel OpenBrain API via the IDE bridge (`core/mcp/pkg/mcp/ide`). Tools: - -- `brain_remember` — store a memory (content, type, project, agent_id, tags) -- `brain_recall` — semantic search (query, top_k, project, type, agent_id) -- `brain_forget` — remove a memory by ID -- `brain_ensure_collection` — ensure Qdrant collection exists - -Environment: `CORE_API_URL` (default `http://localhost:8000`), `CORE_API_TOKEN`. - -## GUI Subsystem - -The `core/gui/pkg/mcp.Subsystem` provides 74 MCP tools across 14 categories (webview, window, layout, screen, clipboard, dialog, notification, tray, environment, browser, contextmenu, keybinding, dock, lifecycle). These are only active when a display platform is registered. - -The `core/gui/pkg/display.Service` bridges Core IPC to the Wails platform — window management, DOM interaction, event forwarding, WS event bridging for Angular. - -## Files - -### Delete (7 files) - -| File | Replaced by | -|------|-------------| -| `mcp_bridge.go` | `core/mcp` Service + transports | -| `webview_svc.go` | `core/gui/pkg/webview` + `core/gui/pkg/display` | -| `brain_mcp.go` | `core/mcp/pkg/mcp/brain` | -| `claude_bridge.go` | `core/mcp/pkg/mcp/ide` Bridge | -| `headless_mcp.go` | Not needed — core/mcp runs in-process | -| `headless.go` | config `gui.enabled` flag. Jobrunner/poller extracted to core/agent (not in scope). | -| `greetservice.go` | Scaffold placeholder, not needed | - -### Rewrite (1 file) - -| File | Changes | -|------|---------| -| `main.go` | Thin shell: config, core.New with services, Wails systray, `--mcp` flag for stdio | - -### Keep (unchanged) - -| File/Dir | Reason | -|----------|--------| -| `frontend/` | Angular app — tray panel + IDE routes | -| `icons/` | Tray icons | -| `.core/build.yaml` | Build configuration | -| `CLAUDE.md` | Update after implementation | -| `go.mod` | Update dependencies | - -## Dependencies - -### Add - -- `forge.lthn.ai/core/go` — DI framework, core.Core -- `forge.lthn.ai/core/mcp` — MCP server, brain subsystem, IDE bridge -- `forge.lthn.ai/core/gui` — display service, 74 MCP tools -- `forge.lthn.ai/core/go-io` — sandboxed filesystem -- `forge.lthn.ai/core/go-log` — structured logging + errors -- `forge.lthn.ai/core/config` — configuration - -### Remove (indirect cleanup) - -- `forge.lthn.ai/core/agent` — no longer imported directly (brain tools via core/mcp) -- Direct `gorilla/websocket` — replaced by go-ws - -## Configuration - -`.core/config.yaml`: - -```yaml -gui: - enabled: true # false = no Wails, Core still runs -mcp: - transport: stdio # stdio | tcp | unix - tcp: - port: 9877 -brain: - api_url: http://localhost:8000 - api_token: "" # or CORE_API_TOKEN env var -``` - -## Testing - -- Core framework tests live in their respective packages (core/mcp, core/gui) -- IDE-specific tests: Wails service startup, config loading, systray lifecycle -- Integration: `core-ide --mcp` stdio round-trip with a test MCP client - -## Not In Scope - -- Angular frontend changes (existing routes work as-is) -- New MCP tool categories beyond what core/mcp and core/gui already provide -- Jobrunner/Forgejo poller (was in headless.go — separate concern, belongs in core/agent) -- CoreDeno/TypeScript runtime integration (future phase) diff --git a/docs/superpowers/specs/2026-03-14-runtime-provider-loading-design.md b/docs/superpowers/specs/2026-03-14-runtime-provider-loading-design.md deleted file mode 100644 index 236920a..0000000 --- a/docs/superpowers/specs/2026-03-14-runtime-provider-loading-design.md +++ /dev/null @@ -1,336 +0,0 @@ -# Runtime Provider Loading — Plugin Ecosystem - -**Date:** 2026-03-14 -**Status:** Approved -**Depends on:** Service Provider Framework, SCM Provider - -## Problem - -All providers are currently compiled into the binary. Users cannot install, -remove, or update providers without rebuilding core-ide. There's no plugin -ecosystem — every provider is a Go import in main.go. - -## Solution - -Runtime provider discovery using the Mining namespace pattern. Installed -providers run as managed processes with `--namespace` flags. The IDE's Gin -router proxies to them. JS bundles load dynamically in Angular. No recompile. - -## Architecture - -``` -~/.core/providers/ -├── cool-widget/ -│ ├── manifest.yaml # Name, namespace, element, permissions -│ ├── cool-widget # Binary (or path to system binary) -│ ├── openapi.json # OpenAPI spec -│ └── assets/ -│ └── core-cool-widget.js # Custom element bundle -├── data-viz/ -│ ├── manifest.yaml -│ ├── data-viz -│ └── assets/ -│ └── core-data-viz.js -└── registry.yaml # Installed providers list -``` - -### Runtime Flow - -``` - core-ide (main process) - ┌─────────────────────────────────┐ - │ Gin Router │ - │ /api/v1/scm/* → compiled in │ - │ /api/v1/brain/* → compiled in │ - │ /api/v1/cool-widget/* → proxy ──┼──→ :9901 (cool-widget binary) - │ /api/v1/data-viz/* → proxy ──┼──→ :9902 (data-viz binary) - │ │ - │ Angular Shell │ - │ → compiled │ - │ → dynamic │ - │ → dynamic │ - └─────────────────────────────────┘ -``` - -## Manifest Format - -`.core/manifest.yaml` (same as go-scm's manifest loader): - -```yaml -code: cool-widget -name: Cool Widget Dashboard -version: 1.0.0 -author: someone -licence: EUPL-1.2 - -# Provider configuration -namespace: /api/v1/cool-widget -port: 0 # 0 = auto-assign -binary: ./cool-widget # Relative to provider dir -args: [] # Additional CLI args - -# UI -element: - tag: core-cool-widget - source: ./assets/core-cool-widget.js - -# Layout -layout: HCF -slots: - H: toolbar - C: dashboard - F: status - -# OpenAPI spec -spec: ./openapi.json - -# Permissions (for TIM sandbox — future) -permissions: - network: ["api.example.com"] - filesystem: ["~/.core/providers/cool-widget/data/"] - -# Signature -sign: -``` - -## Provider Lifecycle - -### Discovery - -On startup, the IDE scans `~/.core/providers/*/manifest.yaml`: - -```go -func DiscoverProviders(dir string) ([]RuntimeProvider, error) { - entries, _ := os.ReadDir(dir) - var providers []RuntimeProvider - for _, e := range entries { - if !e.IsDir() { continue } - m, err := manifest.Load(filepath.Join(dir, e.Name())) - if err != nil { continue } - providers = append(providers, RuntimeProvider{ - Dir: filepath.Join(dir, e.Name()), - Manifest: m, - }) - } - return providers, nil -} -``` - -### Start - -For each discovered provider: - -1. Assign a free port (if `port: 0` in manifest) -2. Start the binary via go-process: `./cool-widget --namespace /api/v1/cool-widget --port 9901` -3. Wait for health check: `GET http://localhost:9901/health` -4. Register a `ProxyProvider` in the API engine that reverse-proxies to that port -5. Serve the JS bundle as a static asset at `/assets/{code}.js` - -```go -type RuntimeProvider struct { - Dir string - Manifest *manifest.Manifest - Process *process.Daemon - Port int -} - -func (rp *RuntimeProvider) Start(engine *api.Engine, hub *ws.Hub) error { - // Start binary - rp.Port = findFreePort() - rp.Process = process.NewDaemon(process.DaemonOptions{ - Command: filepath.Join(rp.Dir, rp.Manifest.Binary), - Args: append(rp.Manifest.Args, "--namespace", rp.Manifest.Namespace, "--port", strconv.Itoa(rp.Port)), - PIDFile: filepath.Join(rp.Dir, "provider.pid"), - }) - rp.Process.Start() - - // Wait for health - waitForHealth(rp.Port) - - // Register proxy provider - proxy := provider.NewProxy(provider.ProxyConfig{ - Name: rp.Manifest.Code, - BasePath: rp.Manifest.Namespace, - Upstream: fmt.Sprintf("http://127.0.0.1:%d", rp.Port), - Element: rp.Manifest.Element, - SpecFile: filepath.Join(rp.Dir, rp.Manifest.Spec), - }) - engine.Register(proxy) - - // Serve JS assets - engine.Router().Static("/assets/"+rp.Manifest.Code, filepath.Join(rp.Dir, "assets")) - - return nil -} -``` - -### Stop - -On IDE quit or provider removal: -1. Send SIGTERM to provider process -2. Remove proxy routes from Gin -3. Unload JS bundle from Angular - -### Hot Reload (Development) - -During development (`core dev` in a provider dir): -1. Watch for binary changes → restart process -2. Watch for JS changes → reload in Angular -3. Watch for manifest changes → re-register proxy - -## Install / Remove - -### Install - -```bash -core install forge.lthn.ai/someone/cool-widget -``` - -1. Clone or download the provider repo -2. Verify Ed25519 signature in manifest -3. If Go source: `go build -o cool-widget .` in the provider dir -4. Copy to `~/.core/providers/cool-widget/` -5. Update `~/.core/providers/registry.yaml` -6. If IDE is running: hot-load the provider (no restart needed) - -### Remove - -```bash -core remove cool-widget -``` - -1. Stop the provider process -2. Remove from `~/.core/providers/cool-widget/` -3. Update `~/.core/providers/registry.yaml` -4. If IDE is running: unload the proxy + UI - -### Update - -```bash -core update cool-widget -``` - -1. Pull latest from git -2. Verify new signature -3. Rebuild if source-based -4. Stop old process, start new -5. Reload JS bundle - -## Registry - -`~/.core/providers/registry.yaml`: - -```yaml -version: 1 -providers: - cool-widget: - installed: "2026-03-14T12:00:00Z" - version: 1.0.0 - source: forge.lthn.ai/someone/cool-widget - auto_start: true - data-viz: - installed: "2026-03-14T13:00:00Z" - version: 0.2.0 - source: github.com/user/data-viz - auto_start: true -``` - -## Custom Binary Build - -```bash -core build --brand "My Product" --include cool-widget,data-viz -``` - -Instead of runtime proxy, this compiles the selected providers directly -into the binary: - -1. Read each provider's Go source -2. Import as compiled providers (not proxied) -3. Embed JS bundles via `//go:embed` -4. Set binary name, icon, and metadata from brand config -5. Output: single binary with everything compiled in - -Same providers, two modes: proxied (plugin) or compiled (product). - -## Provider Binary Contract - -A provider binary must: - -1. Accept `--namespace` flag (API route prefix) -2. Accept `--port` flag (HTTP listen port) -3. Serve `GET /health` → `{"status": "ok"}` -4. Serve its API under the namespace path -5. Optionally accept `--ws-url` flag to connect to IDE's WS hub for events - -The element-template already scaffolds this pattern. - -## Swagger Aggregation - -The IDE's `SpecBuilder` aggregates OpenAPI specs from: -1. Compiled providers (via `DescribableGroup.Describe()`) -2. Runtime providers (via their `openapi.json` files) - -Merged into one spec at `/swagger/doc.json`. The Swagger UI shows all -providers' endpoints in one place. - -## Angular Dynamic Loading - -Custom elements load at runtime without Angular knowing about them at -build time: - -```typescript -// In the IDE's Angular shell -async function loadProviderElement(tag: string, scriptUrl: string) { - if (customElements.get(tag)) return; // Already loaded - - const script = document.createElement('script'); - script.type = 'module'; - script.src = scriptUrl; - document.head.appendChild(script); - - // Wait for registration - await customElements.whenDefined(tag); -} -``` - -The tray panel and IDE layout call this for each Renderable provider -discovered at startup. Angular wraps the custom element in a host component -for the HLCRF slot assignment. - -## Security - -### Signature Verification - -All manifests must be signed (Ed25519). Unsigned providers are rejected -unless `--allow-unsigned` is passed (development only). - -### Process Isolation - -Provider processes run as the current user with no special privileges. -Future: TIM containers for full sandbox (filesystem + network isolation -per the manifest's permissions declaration). - -### Network - -Providers listen on `127.0.0.1` only. No external network exposure. -The IDE's Gin router is the only entry point. - -## Implementation Location - -| Component | Package | New/Existing | -|-----------|---------|-------------| -| Provider discovery | go-scm/marketplace | Extend existing | -| Process management | go-process | Existing daemon API | -| Proxy provider | core/api/pkg/provider | New: proxy.go | -| Install/remove CLI | core/cli cmd/ | New commands | -| Runtime loader | core/ide | New: runtime.go | -| JS dynamic loading | core/ide frontend/ | New: provider-loader service | -| Registry file | go-scm/marketplace | Extend existing | - -## Not In Scope - -- TIM container sandbox (future — Phase 4 from provider framework spec) -- Provider marketplace server (git-based discovery is sufficient) -- Revenue sharing / paid providers (future — SMSG licensing) -- Angular module federation (future — current pattern is custom elements) -- Multi-language provider SDKs (future — element-template is Go-first) diff --git a/docs/superpowers/specs/2026-03-14-scm-provider-design.md b/docs/superpowers/specs/2026-03-14-scm-provider-design.md deleted file mode 100644 index 60dc2ea..0000000 --- a/docs/superpowers/specs/2026-03-14-scm-provider-design.md +++ /dev/null @@ -1,258 +0,0 @@ -# go-scm Service Provider + Custom Elements - -**Date:** 2026-03-14 -**Status:** Approved -**Depends on:** Service Provider Framework, core/api - -## Problem - -go-scm has marketplace, manifest, and registry functionality but no REST API -or UI. The IDE can't browse providers, install apps, or inspect manifests -without CLI commands. - -## Solution - -Add a service provider to go-scm with REST endpoints and a Lit custom element -bundle containing multiple composable elements. - -## Provider (Go) - -### ScmProvider - -Lives in `go-scm/pkg/api/provider.go`. Implements `Provider` + `Streamable` + -`Describable` + `Renderable`. - -```go -Name() → "scm" -BasePath() → "/api/v1/scm" -Element() → ElementSpec{Tag: "core-scm-panel", Source: "/assets/core-scm.js"} -Channels() → []string{"scm.marketplace.*", "scm.manifest.*", "scm.registry.*"} -``` - -### REST Endpoints - -#### Marketplace - -| Method | Path | Description | -|--------|------|-------------| -| GET | /marketplace | List available providers from git registry | -| GET | /marketplace/:code | Get provider details | -| POST | /marketplace/:code/install | Install provider | -| DELETE | /marketplace/:code | Remove installed provider | -| POST | /marketplace/refresh | Pull latest marketplace index (requires new FetchIndex function) | - -#### Manifest - -| Method | Path | Description | -|--------|------|-------------| -| GET | /manifest | Read .core/manifest.yaml from current directory | -| POST | /manifest/verify | Verify Ed25519 signature (body: {public_key: hex}) | -| POST | /manifest/sign | Sign manifest with private key (body: {private_key: hex}) | -| GET | /manifest/permissions | List declared permissions | - -#### Installed - -| Method | Path | Description | -|--------|------|-------------| -| GET | /installed | List installed providers | -| POST | /installed/:code/update | Apply update (pulls latest from git) | - -Note: Single-item `GET /installed/:code` and update-check require new methods -on `Installer` (`Get(code)` and `CheckUpdate(code)`). Deferred — the list -endpoint is sufficient for Phase 1. Update applies `Installer.Update()`. - -#### Registry - -| Method | Path | Description | -|--------|------|-------------| -| GET | /registry | List repos from repos.yaml | -| GET | /registry/:name/status | Git status for a repo | - -Note: Registry endpoints are read-only. Pull/push actions are handled by -`core dev` CLI commands, not the REST API. The `` element -shows status only — no write buttons. - -### WS Events - -| Event | Data | Trigger | -|-------|------|---------| -| scm.marketplace.refreshed | {count} | After marketplace pull | -| scm.marketplace.installed | {code, name, version, installed_at} | After install | -| scm.marketplace.removed | {code} | After remove | -| scm.manifest.verified | {code, valid, signer} | After signature check | -| scm.registry.changed | {name, status} | Repo status change | - -## Custom Elements (Lit) - -### Bundle Structure - -``` -go-scm/ui/ -├── src/ -│ ├── index.ts # Bundle entry — exports all elements -│ ├── scm-panel.ts # — HLCRF layout -│ ├── scm-marketplace.ts # — browse/install -│ ├── scm-manifest.ts # — view/verify -│ ├── scm-installed.ts # — manage installed -│ ├── scm-registry.ts # — repo status -│ └── shared/ -│ ├── api.ts # Fetch wrapper for /api/v1/scm/* -│ └── events.ts # WS event listener helpers -├── package.json -├── tsconfig.json -└── index.html # Demo page -``` - -### Elements - -#### `` - -Top-level element. Arranges child elements in HLCRF layout `H[LC]CF`: -- H: Title bar with refresh button -- H-L: Navigation tabs (Marketplace / Installed / Registry) -- H-C: Search input -- C: Active tab content (one of the child elements) -- F: Status bar (connection state, last refresh) - -#### `` - -Browse the git-based provider marketplace. - -| Attribute | Type | Description | -|-----------|------|-------------| -| api-url | string | API base URL (default: current origin) | -| category | string | Filter by category | - -Displays: -- Provider cards with name, description, version, author -- Install/Remove button per card -- Category filter tabs -- Search (filters client-side) - -#### `` - -View and verify a .core/manifest.yaml file. - -| Attribute | Type | Description | -|-----------|------|-------------| -| api-url | string | API base URL | -| path | string | Directory to read manifest from | - -Displays: -- Manifest fields (code, name, version, layout variant) -- HLCRF slot assignments -- Permission declarations -- Signature status badge (verified/unsigned/invalid) -- Sign button (calls POST /manifest/sign) - -#### `` - -Manage installed providers. - -| Attribute | Type | Description | -|-----------|------|-------------| -| api-url | string | API base URL | - -Displays: -- Installed provider list with version, status -- Update available indicator -- Update/Remove buttons -- Provider detail panel on click - -#### `` - -Show repos.yaml registry status. - -| Attribute | Type | Description | -|-----------|------|-------------| -| api-url | string | API base URL | - -Displays: -- Repo list from registry -- Git status per repo (clean/dirty/ahead/behind) - -### Shared - -#### `api.ts` - -```typescript -export class ScmApi { - constructor(private baseUrl: string = '') {} - - private get base() { - return `${this.baseUrl}/api/v1/scm`; - } - - marketplace() { return fetch(`${this.base}/marketplace`).then(r => r.json()); } - install(code: string){ return fetch(`${this.base}/marketplace/${code}/install`, {method:'POST'}).then(r => r.json()); } - remove(code: string) { return fetch(`${this.base}/marketplace/${code}`, {method:'DELETE'}).then(r => r.json()); } - installed() { return fetch(`${this.base}/installed`).then(r => r.json()); } - manifest() { return fetch(`${this.base}/manifest`).then(r => r.json()); } - verify(publicKey: string) { return fetch(`${this.base}/manifest/verify`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({public_key: publicKey})}).then(r => r.json()); } - sign(privateKey: string) { return fetch(`${this.base}/manifest/sign`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({private_key: privateKey})}).then(r => r.json()); } - registry() { return fetch(`${this.base}/registry`).then(r => r.json()); } -} -``` - -#### `events.ts` - -```typescript -export function connectScmEvents(wsUrl: string, handler: (event: any) => void) { - const ws = new WebSocket(wsUrl); - ws.onmessage = (e) => { - const event = JSON.parse(e.data); - if (event.type?.startsWith('scm.')) handler(event); - }; - return ws; -} -``` - -## Build - -```bash -cd go-scm/ui -npm install -npm run build # → dist/core-scm.js (single bundle, all elements) -``` - -The built JS is embedded in go-scm's Go binary via `//go:embed` and served -as a static asset by the provider. - -## Dependencies - -### Go (go-scm) -- core/api (provider interfaces, Gin) -- go-ws (WS hub for events) -- Existing: manifest, marketplace, repos packages (already in go-scm) - -### TypeScript (ui/) -- lit (Web Components) -- No other runtime deps - -## Files - -### Create in go-scm - -| File | Purpose | -|------|---------| -| `pkg/api/provider.go` | ScmProvider with all REST endpoints | -| `pkg/api/provider_test.go` | Endpoint tests | -| `pkg/api/embed.go` | `//go:embed` for UI assets | -| `ui/src/index.ts` | Bundle entry | -| `ui/src/scm-panel.ts` | Top-level HLCRF panel | -| `ui/src/scm-marketplace.ts` | Marketplace browser | -| `ui/src/scm-manifest.ts` | Manifest viewer | -| `ui/src/scm-installed.ts` | Installed provider manager | -| `ui/src/scm-registry.ts` | Registry status | -| `ui/src/shared/api.ts` | API client | -| `ui/src/shared/events.ts` | WS event helpers | -| `ui/package.json` | Lit + TypeScript deps | -| `ui/tsconfig.json` | TypeScript config | -| `ui/index.html` | Demo page | - -## Not In Scope - -- Actual STIM packaging/distribution (future — uses Borg) -- Provider sandbox enforcement (future — uses TIM/CoreDeno) -- Marketplace git server (uses existing forge) -- Angular wrappers (IDE dynamically loads custom elements) diff --git a/docs/superpowers/specs/2026-03-14-service-provider-framework-design.md b/docs/superpowers/specs/2026-03-14-service-provider-framework-design.md deleted file mode 100644 index a340493..0000000 --- a/docs/superpowers/specs/2026-03-14-service-provider-framework-design.md +++ /dev/null @@ -1,290 +0,0 @@ -# Service Provider Framework — Polyglot API + UI - -**Date:** 2026-03-14 -**Status:** Approved -**Depends on:** IDE Modernisation (2026-03-14) - -## Problem - -Each package in the ecosystem (Go, PHP, TypeScript) builds its own API endpoints, -WebSocket events, and UI components independently. There's no standard way for a -package to say "I provide these capabilities" and have them automatically -assembled into an API router, MCP server, or GUI. - -The Mining repo proves the pattern works — Gin routes + WS events + Angular -custom element = full process management UI. But it's hand-wired for one use case. - -## Vision - -Any package in any language can register as a service provider. The contract is -OpenAPI. Go packages implement a Go interface directly. PHP and TypeScript -packages publish an OpenAPI spec and run their own HTTP handler — the API layer -reverse-proxies or aggregates. The result: - -- Every provider automatically gets a REST API -- Every provider with a custom element automatically gets a GUI panel -- Every provider with tool descriptions automatically gets MCP tools -- The language the provider is written in is irrelevant - -## Architecture - -``` -.core/config.yaml - │ - ├─→ core/go-api (Service Registry) - │ ├─ Go providers: implement Provider interface directly - │ ├─ PHP providers: OpenAPI spec + reverse proxy to FrankenPHP - │ ├─ TS providers: OpenAPI spec + reverse proxy to CoreDeno - │ ├─ Assembled Gin router (all routes merged) - │ └─ WS hub (all events merged) - │ - ├─→ core/gui (Display Layer) - │ ├─ Discovers Renderable providers - │ ├─ Loads custom elements into Angular shell - │ └─ HLCRF layout from .core/ config - │ - ├─→ core/mcp (Tool Layer) - │ ├─ Discovers Describable providers - │ ├─ Registers as MCP tools - │ └─ stdio/TCP/Unix transports - │ - └─→ core/ide (Application Shell) - ├─ Wails systray + Angular frontend - ├─ Hosts go-api router - └─ Hosts core/mcp server -``` - -## The Provider Interface (Go) - -Lives in `core/go-api/pkg/provider/`. Built on top of the existing `RouteGroup` -and `DescribableGroup` interfaces — providers ARE route groups, not a parallel -system. - -```go -// Provider extends RouteGroup with a provider identity. -// Every Provider is a RouteGroup and registers through api.Engine.Register(). -type Provider interface { - api.RouteGroup // Name(), BasePath(), RegisterRoutes(*gin.RouterGroup) -} - -// Streamable providers emit real-time events via WebSocket. -// The hub is injected at construction time. Channels() declares the -// event prefixes this provider will emit (e.g. "brain.*"). -type Streamable interface { - Provider - Channels() []string // Event prefixes emitted by this provider -} - -// Describable providers expose structured route descriptions for OpenAPI. -// This extends the existing DescribableGroup interface. -type Describable interface { - Provider - api.DescribableGroup // Describe() []RouteDescription -} - -// Renderable providers declare a custom element for GUI display. -type Renderable interface { - Provider - Element() ElementSpec -} - -type ElementSpec struct { - Tag string // e.g. "core-brain-panel" - Source string // URL or embedded path to the JS bundle -} -``` - -Note: `Manageable` (Start/Stop/Status) is deferred to Phase 2. In Phase 1, -provider lifecycle is handled by `core.Core`'s existing `Startable`/`Stoppable` -interfaces — providers that need lifecycle management implement those directly -when registered as Core services. - -### Registration - -Providers register through the existing `api.Engine`, not a parallel router. -This gives them middleware, CORS, Swagger, health checks, and OpenAPI for free. - -```go -engine, _ := api.New( - api.WithCORS(), - api.WithSwagger(), - api.WithWSHub(hub), -) - -// Register providers as route groups — they get middleware, OpenAPI, etc. -engine.Register(brain.NewProvider(bridge, hub)) -engine.Register(daemon.NewProvider(registry)) -engine.Register(build.NewProvider()) - -// Providers that are Streamable have the hub injected at construction. -// They call hub.SendToChannel("brain.recall.complete", event) internally. -``` - -The `Registry` type is a convenience wrapper that collects providers and -calls `engine.Register()` for each: - -```go -reg := provider.NewRegistry() -reg.Add(brain.NewProvider(bridge, hub)) -reg.Add(build.NewProvider()) -reg.MountAll(engine) // calls engine.Register() for each -``` - -## Polyglot Providers (PHP, TypeScript) - -Non-Go providers don't implement the Go interface. They: - -1. Publish an OpenAPI spec in `.core/providers/{name}.yaml` -2. Run their own HTTP server (FrankenPHP, CoreDeno, or any process) -3. The Go API layer discovers the spec and creates a reverse proxy route group - -```yaml -# .core/providers/studio.yaml -name: studio -language: php -spec: openapi-3.json # Path to OpenAPI spec -endpoint: http://localhost:8000 # Where the PHP handler listens -element: - tag: core-studio-panel - source: /assets/studio-panel.js -events: - - studio.render.started - - studio.render.complete -``` - -The Go registry wraps this as a `ProxyProvider` — it implements `Provider` by -reverse-proxying to the endpoint, `Describable` by reading the spec file, -and `Renderable` by reading the element config. - -For real-time events, the upstream process connects to the Go WS hub as a -client (using `ws.ReconnectingClient`) or pushes events via the go-ws Redis -pub/sub backend. The `ProxyProvider` declares the expected channels from the -YAML config. The mechanism choice depends on deployment: Redis for multi-host, -direct WS for single-binary. - -### OpenAPI as Contract - -The OpenAPI spec is the single source of truth for: -- **go-api**: Route mounting and request validation -- **core/mcp**: Automatic MCP tool generation from endpoints -- **core/gui**: Form generation for Manageable providers -- **SDK codegen**: TypeScript/Python/PHP client generation (already in go-api) - -A PHP package that publishes a valid OpenAPI spec gets all four for free. - -## Discovery - -Provider discovery follows the `.core/` convention: - -1. **Static config** — `.core/config.yaml` lists enabled providers -2. **Directory scan** — `.core/providers/*.yaml` for polyglot provider specs -3. **Go registration** — `core.WithService(provider.Register(registry))` in main.go - -```yaml -# .core/config.yaml -providers: - brain: - enabled: true - studio: - enabled: true - endpoint: http://localhost:8000 - gallery: - enabled: false -``` - -## GUI Integration - -`core/gui`'s display service queries the registry for `Renderable` providers. -For each one, it: - -1. Loads the custom element JS bundle (from `ElementSpec.Source`) -2. Creates an Angular wrapper component that hosts the custom element -3. Registers it in the available panels list -4. Layout is configured via `.core/config.yaml` or defaults to auto-arrangement - -The Angular shell doesn't know about providers at build time. Custom elements -are loaded dynamically at runtime. This is the same pattern as Mining's -`` — a self-contained web component that talks to the -Gin API via fetch/WS. - -### Tray Panel - -The systray control pane shows: -- List of registered providers with status indicators -- Start/Stop controls for Manageable providers -- Quick stats for Streamable providers -- Click to open full panel in a new window - -## WS Event Protocol - -All providers share a single WS hub. Events are namespaced by provider: - -```json -{ - "type": "brain.recall.complete", - "timestamp": "2026-03-14T10:30:00Z", - "data": { "query": "...", "results": 5 } -} -``` - -Angular services filter by prefix (`brain.*`, `studio.*`, etc.). -This is identical to Mining's `WebSocketService` pattern but generalised. - -## Implementation Phases - -### Phase 1: Go Provider Framework (this spec) -- `Provider` interface (extends `RouteGroup`) + `Registry` in `core/go-api/pkg/provider/` -- Providers register through existing `api.Engine` — get middleware, OpenAPI, Swagger for free -- Streamable providers receive WS hub at construction, declare channel prefixes -- **go-process as first provider** — daemon registry, PID files, health checks → `` -- Brain as second provider -- core/ide consumes the registry -- Element template: [core-element-template](https://github.com/Snider/core-element-template) — Go CLI + Lit custom element scaffold for new providers - -### Phase 2: GUI Consumer -- core/gui discovers Renderable providers -- Dynamic custom element loading in Angular shell -- Tray panel with provider status -- HLCRF layout configuration - -### Phase 3: Polyglot Providers -- `ProxyProvider` for PHP/TS providers -- `.core/providers/*.yaml` discovery -- OpenAPI spec → MCP tool auto-generation -- PHP packages (core/php-*) expose providers via FrankenPHP -- TS packages (core/ts) expose providers via CoreDeno - -### Phase 4: SDK + Marketplace -- Auto-generate client SDKs from assembled OpenAPI spec -- Provider marketplace (git-based, same pattern as dAppServer) -- Signed provider manifests (ed25519, from `.core/view.yml` spec) - -## Files (Phase 1) - -### Create in core/go-api - -| File | Purpose | -|------|---------| -| `pkg/provider/provider.go` | Provider (extends RouteGroup), Streamable, Describable, Renderable interfaces | -| `pkg/provider/registry.go` | Registry: Add, MountAll(engine), List | -| `pkg/provider/proxy.go` | ProxyProvider for polyglot (Phase 3, stub for now) | -| `pkg/provider/registry_test.go` | Unit tests | - -### Update in core/ide - -| File | Change | -|------|--------| -| `main.go` | Create registry, register providers, mount router | - -## Dependencies - -- `core/go-api` — Gin, route groups, OpenAPI (already there) -- `core/go-ws` — WS hub (already there) -- No new external dependencies - -## Not In Scope - -- Angular component library (Phase 2) -- PHP/TS provider runtime (Phase 3) -- Provider marketplace (Phase 4) -- Authentication/authorisation per provider (future — Authentik integration)