7 Commits

Author SHA1 Message Date
1328b3cc94 Merge pull request 'ci: Gitea Actions — CI build/test + cross-platform release on tags' (#22) from feat/gitea-actions into main
All checks were successful
CI / build-and-test (push) Successful in -30m49s
Reviewed-on: #22
2026-04-04 14:37:08 +00:00
SGC
87a62c0d6c ci: add Gitea Actions — CI on push/PR, release on version tags
Some checks failed
CI / build-and-test (pull_request) Failing after -31m8s
CI / build-and-test (push) Successful in -28m7s
- .gitea/workflows/ci.yml: build + test on every push and PR
- .gitea/workflows/release.yml: cross-platform release binaries on v*.*.* tags
  - Builds amcs-server and amcs-cli for linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64
  - Embeds version, commit, and build date via ldflags
  - Creates Gitea Release with all binaries and checksums.txt attached
2026-04-04 16:28:19 +02:00
3e09dc0ac6 Merge pull request 'feat: amcs-cli — MCP tool CLI and stdio bridge' (#21) from feat/amcs-cli into main 2026-04-04 14:25:04 +00:00
SGC
b59e02aebe feat: add amcs-cli — MCP tool CLI and stdio bridge
- cmd/amcs-cli: new CLI tool for human/AI MCP tool access
- amcs-cli tools: list all tools from remote MCP server
- amcs-cli call <tool> --arg k=v: invoke a tool, print JSON/YAML result
- amcs-cli stdio: stdio→HTTP MCP bridge for AI clients
- Config: ~/.config/amcs/config.yaml, AMCS_URL/AMCS_TOKEN env vars, --server/--token flags
- Token never logged in errors
- Makefile: add build-cli target

Closes #18
2026-04-04 16:14:25 +02:00
4713110e32 Merge pull request 'feat: DBML schema files + relspecgo migration generation' (#20) from feat/dbml-schema-relspecgo into main
Reviewed-on: #20
Reviewed-by: Warky <hein.puth@gmail.com>
2026-04-04 13:37:26 +00:00
SGC
6c6b49b45c fix: add cross-file Ref declarations for relspecgo merge
- Add explicit Ref: blocks at bottom of each DBML file for cross-file FK relationships
- files.dbml: stored_files → thoughts, projects
- skills.dbml: project_skills/project_guardrails → projects
- meta.dbml: chat_histories → projects
- Update Makefile to use the relspec merge workflow where applicable
- Regenerate 020_generated_schema.sql with proper cross-file constraints

Addresses review comment on PR #20
2026-04-04 15:13:50 +02:00
SGC
59c43188e5 feat: add DBML schema files and relspecgo migration generation
- Add schema/*.dbml covering all existing tables (001-019)
- Wire relspecgo via make generate-migrations target
- Add make check-schema-drift for CI drift detection
- Add schema/README.md documenting the DBML-first workflow

Closes #19
2026-04-04 14:53:33 +02:00
40 changed files with 5089 additions and 1923 deletions

41
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,41 @@
name: CI
on:
push:
branches: ['**']
tags-ignore: ['v*']
pull_request:
branches: ['**']
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.26'
- name: Cache Go modules
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Download dependencies
run: go mod download
- name: Run tests
run: go test ./...
- name: Build amcs-server
run: go build -o /dev/null ./cmd/amcs-server
- name: Build amcs-cli
run: go build -o /dev/null ./cmd/amcs-cli

View File

@@ -0,0 +1,102 @@
name: Release
on:
push:
tags:
- 'v*.*.*'
env:
GITEA_SERVER: https://git.warky.dev
GITEA_REPO: wdevs/amcs
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.26'
- name: Cache Go modules
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Download dependencies
run: go mod download
- name: Set build vars
id: vars
run: |
echo "VERSION=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
echo "COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT
- name: Build release binaries
run: |
VERSION="${{ steps.vars.outputs.VERSION }}"
COMMIT="${{ steps.vars.outputs.COMMIT }}"
BUILD_DATE="${{ steps.vars.outputs.BUILD_DATE }}"
LDFLAGS="-s -w -X git.warky.dev/wdevs/amcs/internal/buildinfo.Version=${VERSION} -X git.warky.dev/wdevs/amcs/internal/buildinfo.TagName=${VERSION} -X git.warky.dev/wdevs/amcs/internal/buildinfo.Commit=${COMMIT} -X git.warky.dev/wdevs/amcs/internal/buildinfo.BuildDate=${BUILD_DATE}"
mkdir -p dist
for BINARY in amcs-server amcs-cli; do
CMD="./cmd/${BINARY}"
for PLATFORM in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do
OS="${PLATFORM%/*}"
ARCH="${PLATFORM#*/}"
EXT=""
[ "$OS" = "windows" ] && EXT=".exe"
OUTPUT="dist/${BINARY}-${OS}-${ARCH}${EXT}"
echo "Building ${OUTPUT}..."
GOOS=$OS GOARCH=$ARCH go build -ldflags "${LDFLAGS}" -o "${OUTPUT}" "${CMD}"
done
done
cd dist && sha256sum * > checksums.txt && cd ..
- name: Create Gitea Release
id: create_release
run: |
VERSION="${{ steps.vars.outputs.VERSION }}"
BODY=$(python3 <<'PY'
import json, subprocess, os
version = os.environ['VERSION']
commit = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'], text=True).strip()
body = f"## {version}\n\nBuilt from commit {commit}.\n\nSee `checksums.txt` to verify downloads."
print(json.dumps({
'tag_name': version,
'name': version,
'body': body,
'draft': False,
'prerelease': False,
}))
PY
)
RESPONSE=$(curl -fsS -X POST "${{ env.GITEA_SERVER }}/api/v1/repos/${{ env.GITEA_REPO }}/releases" \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/json" \
-d "$BODY")
RELEASE_ID=$(printf '%s' "$RESPONSE" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
echo "RELEASE_ID=${RELEASE_ID}" >> $GITHUB_OUTPUT
- name: Upload release assets
run: |
RELEASE_ID="${{ steps.create_release.outputs.RELEASE_ID }}"
for f in dist/*; do
name=$(basename "$f")
echo "Uploading ${name}..."
curl -fsS -X POST \
"${{ env.GITEA_SERVER }}/api/v1/repos/${{ env.GITEA_REPO }}/releases/${RELEASE_ID}/assets?name=${name}" \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"${f}"
done

3
.gitignore vendored
View File

@@ -31,6 +31,3 @@ cmd/amcs-server/__debug_*
bin/ bin/
.cache/ .cache/
OB1/ OB1/
ui/node_modules/
ui/.svelte-kit/
internal/app/ui/dist/

View File

@@ -1,14 +1,3 @@
FROM node:22-bookworm AS ui-builder
RUN npm install -g pnpm
WORKDIR /src/ui
COPY ui/package.json ui/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY ui/ ./
RUN pnpm run build
FROM golang:1.26.1-bookworm AS builder FROM golang:1.26.1-bookworm AS builder
WORKDIR /src WORKDIR /src
@@ -17,7 +6,6 @@ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
COPY --from=ui-builder /src/internal/app/ui/dist ./internal/app/ui/dist
RUN set -eu; \ RUN set -eu; \
VERSION_TAG="$(git describe --tags --exact-match 2>/dev/null || echo dev)"; \ VERSION_TAG="$(git describe --tags --exact-match 2>/dev/null || echo dev)"; \

View File

@@ -3,7 +3,6 @@ GO_CACHE_DIR := $(CURDIR)/.cache/go-build
SERVER_BIN := $(BIN_DIR)/amcs-server SERVER_BIN := $(BIN_DIR)/amcs-server
CMD_SERVER := ./cmd/amcs-server CMD_SERVER := ./cmd/amcs-server
BUILDINFO_PKG := git.warky.dev/wdevs/amcs/internal/buildinfo BUILDINFO_PKG := git.warky.dev/wdevs/amcs/internal/buildinfo
UI_DIR := $(CURDIR)/ui
PATCH_INCREMENT ?= 1 PATCH_INCREMENT ?= 1
VERSION_TAG ?= $(shell git describe --tags --exact-match 2>/dev/null || echo dev) VERSION_TAG ?= $(shell git describe --tags --exact-match 2>/dev/null || echo dev)
COMMIT_SHA ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) COMMIT_SHA ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
@@ -12,34 +11,21 @@ RELSPEC ?= $(shell command -v relspec 2>/dev/null || echo $(HOME)/go/bin/relspec
SCHEMA_FILES := $(sort $(wildcard schema/*.dbml)) SCHEMA_FILES := $(sort $(wildcard schema/*.dbml))
MERGE_TARGET_TMP := $(CURDIR)/.cache/schema.merge-target.dbml MERGE_TARGET_TMP := $(CURDIR)/.cache/schema.merge-target.dbml
GENERATED_SCHEMA_MIGRATION := migrations/020_generated_schema.sql GENERATED_SCHEMA_MIGRATION := migrations/020_generated_schema.sql
PNPM ?= pnpm
LDFLAGS := -s -w \ LDFLAGS := -s -w \
-X $(BUILDINFO_PKG).Version=$(VERSION_TAG) \ -X $(BUILDINFO_PKG).Version=$(VERSION_TAG) \
-X $(BUILDINFO_PKG).TagName=$(VERSION_TAG) \ -X $(BUILDINFO_PKG).TagName=$(VERSION_TAG) \
-X $(BUILDINFO_PKG).Commit=$(COMMIT_SHA) \ -X $(BUILDINFO_PKG).Commit=$(COMMIT_SHA) \
-X $(BUILDINFO_PKG).BuildDate=$(BUILD_DATE) -X $(BUILDINFO_PKG).BuildDate=$(BUILD_DATE)
.PHONY: all build clean migrate release-version test generate-migrations check-schema-drift build-cli ui-install ui-build ui-dev ui-check .PHONY: all build clean migrate release-version test generate-migrations check-schema-drift
all: build all: build
build: ui-build build:
@mkdir -p $(BIN_DIR) @mkdir -p $(BIN_DIR)
go build -ldflags "$(LDFLAGS)" -o $(SERVER_BIN) $(CMD_SERVER) go build -ldflags "$(LDFLAGS)" -o $(SERVER_BIN) $(CMD_SERVER)
ui-install: test:
cd $(UI_DIR) && $(PNPM) install --frozen-lockfile
ui-build: ui-install
cd $(UI_DIR) && $(PNPM) run build
ui-dev: ui-install
cd $(UI_DIR) && $(PNPM) run dev
ui-check: ui-install
cd $(UI_DIR) && $(PNPM) run check
test: ui-check
@mkdir -p $(GO_CACHE_DIR) @mkdir -p $(GO_CACHE_DIR)
GOCACHE=$(GO_CACHE_DIR) go test ./... GOCACHE=$(GO_CACHE_DIR) go test ./...
@@ -93,6 +79,7 @@ check-schema-drift:
fi; \ fi; \
rm -f $$tmpfile rm -f $$tmpfile
.PHONY: build-cli
build-cli: build-cli:
@mkdir -p $(BIN_DIR) @mkdir -p $(BIN_DIR)
go build -o $(BIN_DIR)/amcs-cli ./cmd/amcs-cli go build -o $(BIN_DIR)/amcs-cli ./cmd/amcs-cli

View File

@@ -531,37 +531,7 @@ Run the SQL migrations against a local database with:
`DATABASE_URL=postgres://... make migrate` `DATABASE_URL=postgres://... make migrate`
### Backend + embedded UI build LLM integration instructions are served at `/llm`.
The web UI now lives in the top-level `ui/` module and is embedded into the Go binary at build time with `go:embed`.
**Use `pnpm` for all UI work in this repo.**
- `make build` — runs the real UI build first, then compiles the Go server
- `make test` — runs `svelte-check` for the frontend and `go test ./...` for the backend
- `make ui-install` — installs frontend dependencies with `pnpm install --frozen-lockfile`
- `make ui-build` — builds only the frontend bundle
- `make ui-dev` — starts the Vite dev server with hot reload on `http://localhost:5173`
- `make ui-check` — runs the frontend type and Svelte checks
### Local UI workflow
For the normal production-style local flow:
1. Start the backend: `./scripts/run-local.sh configs/dev.yaml`
2. Open `http://localhost:8080`
For frontend iteration with hot reload and no Go rebuilds:
1. Start the backend once: `go run ./cmd/amcs-server --config configs/dev.yaml`
2. In another shell start the UI dev server: `make ui-dev`
3. Open `http://localhost:5173`
The Vite dev server proxies backend routes such as `/api/status`, `/llm`, `/healthz`, `/readyz`, `/files`, `/mcp`, and the OAuth endpoints back to the Go server on `http://127.0.0.1:8080` by default. Override that target with `AMCS_UI_BACKEND` if needed.
The root page (`/`) is now the Svelte frontend. It preserves the existing landing-page content and status information by fetching data from `GET /api/status`.
LLM integration instructions are still served at `/llm`.
## Containers ## Containers

98
cmd/amcs-cli/cmd/call.go Normal file
View File

@@ -0,0 +1,98 @@
package cmd
import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
var argFlags []string
var callCmd = &cobra.Command{
Use: "call <tool>",
Short: "Call a remote AMCS tool",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
toolName := args[0]
toolArgs, err := parseArgs(argFlags)
if err != nil {
return err
}
session, err := connectRemote(cmd.Context())
if err != nil {
return err
}
defer func() { _ = session.Close() }()
res, err := session.CallTool(cmd.Context(), &mcp.CallToolParams{Name: toolName, Arguments: toolArgs})
if err != nil {
return fmt.Errorf("call tool %q: %w", toolName, err)
}
return printOutput(res)
},
}
func init() {
callCmd.Flags().StringArrayVar(&argFlags, "arg", nil, "Tool argument in key=value format (repeatable)")
rootCmd.AddCommand(callCmd)
}
func parseArgs(items []string) (map[string]any, error) {
result := make(map[string]any, len(items))
for _, item := range items {
key, value, ok := strings.Cut(item, "=")
if !ok || strings.TrimSpace(key) == "" {
return nil, fmt.Errorf("invalid --arg %q: want key=value", item)
}
result[key] = parseScalar(value)
}
return result, nil
}
func parseScalar(s string) any {
if s == "true" || s == "false" {
b, _ := strconv.ParseBool(s)
return b
}
if i, err := strconv.ParseInt(s, 10, 64); err == nil {
return i
}
if f, err := strconv.ParseFloat(s, 64); err == nil && strings.ContainsAny(s, ".eE") {
return f
}
var v any
if err := json.Unmarshal([]byte(s), &v); err == nil {
switch v.(type) {
case map[string]any, []any, float64, bool, nil:
return v
}
}
return s
}
func printOutput(v any) error {
switch outputFlag {
case "yaml":
data, err := yaml.Marshal(v)
if err != nil {
return fmt.Errorf("marshal yaml: %w", err)
}
_, err = os.Stdout.Write(data)
return err
default:
data, err := json.MarshalIndent(v, "", " ")
if err != nil {
return fmt.Errorf("marshal json: %w", err)
}
data = append(data, '\n')
_, err = os.Stdout.Write(data)
return err
}
}

View File

@@ -0,0 +1,60 @@
package cmd
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
type Config struct {
Server string `yaml:"server"`
Token string `yaml:"token"`
}
func defaultConfigPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home dir: %w", err)
}
return filepath.Join(home, ".config", "amcs", "config.yaml"), nil
}
func resolveConfigPath(path string) (string, error) {
if strings.TrimSpace(path) != "" {
return path, nil
}
return defaultConfigPath()
}
func loadConfigFile(path string) (Config, error) {
var cfg Config
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return cfg, nil
}
return cfg, fmt.Errorf("read config: %w", err)
}
if err := yaml.Unmarshal(data, &cfg); err != nil {
return cfg, fmt.Errorf("parse config: %w", err)
}
return cfg, nil
}
func saveConfigFile(path string, cfg Config) error {
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return fmt.Errorf("create config dir: %w", err)
}
data, err := yaml.Marshal(cfg)
if err != nil {
return fmt.Errorf("marshal config: %w", err)
}
if err := os.WriteFile(path, data, 0o600); err != nil {
return fmt.Errorf("write config: %w", err)
}
return nil
}

134
cmd/amcs-cli/cmd/root.go Normal file
View File

@@ -0,0 +1,134 @@
package cmd
import (
"context"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/spf13/cobra"
)
var (
cfgFile string
serverFlag string
tokenFlag string
outputFlag string
cfg Config
)
var rootCmd = &cobra.Command{
Use: "amcs-cli",
Short: "CLI for connecting to a remote AMCS MCP server",
SilenceUsage: true,
SilenceErrors: true,
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
return loadConfig()
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func init() {
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "Path to config file")
rootCmd.PersistentFlags().StringVar(&serverFlag, "server", "", "AMCS server URL")
rootCmd.PersistentFlags().StringVar(&tokenFlag, "token", "", "AMCS bearer token")
rootCmd.PersistentFlags().StringVar(&outputFlag, "output", "json", "Output format: json or yaml")
}
func loadConfig() error {
path, err := resolveConfigPath(cfgFile)
if err != nil {
return err
}
loaded, err := loadConfigFile(path)
if err != nil {
return err
}
cfg = loaded
if v := strings.TrimSpace(os.Getenv("AMCS_URL")); v != "" {
cfg.Server = v
}
if v := strings.TrimSpace(os.Getenv("AMCS_TOKEN")); v != "" {
cfg.Token = v
}
if v := strings.TrimSpace(serverFlag); v != "" {
cfg.Server = v
}
if v := strings.TrimSpace(tokenFlag); v != "" {
cfg.Token = v
}
outputFlag = strings.ToLower(strings.TrimSpace(outputFlag))
if outputFlag != "json" && outputFlag != "yaml" {
return fmt.Errorf("invalid --output %q: must be json or yaml", outputFlag)
}
return nil
}
func requireServer() error {
if strings.TrimSpace(cfg.Server) == "" {
return fmt.Errorf("server URL is required; set --server, AMCS_URL, or config server")
}
return nil
}
func endpointURL() string {
base := strings.TrimRight(strings.TrimSpace(cfg.Server), "/")
if strings.HasSuffix(base, "/mcp") {
return base
}
return base + "/mcp"
}
func newHTTPClient() *http.Client {
return &http.Client{
Timeout: 0,
Transport: &bearerTransport{
base: http.DefaultTransport,
token: cfg.Token,
},
}
}
type bearerTransport struct {
base http.RoundTripper
token string
}
func (t *bearerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
base := t.base
if base == nil {
base = http.DefaultTransport
}
clone := req.Clone(req.Context())
if strings.TrimSpace(t.token) != "" {
clone.Header.Set("Authorization", "Bearer "+t.token)
}
return base.RoundTrip(clone)
}
func connectRemote(ctx context.Context) (*mcp.ClientSession, error) {
if err := requireServer(); err != nil {
return nil, err
}
client := mcp.NewClient(&mcp.Implementation{Name: "amcs-cli", Version: "0.0.1"}, nil)
transport := &mcp.StreamableClientTransport{
Endpoint: endpointURL(),
HTTPClient: newHTTPClient(),
}
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
session, err := client.Connect(ctx, transport, nil)
if err != nil {
return nil, fmt.Errorf("connect to AMCS server: %w", err)
}
return session, nil
}

62
cmd/amcs-cli/cmd/stdio.go Normal file
View File

@@ -0,0 +1,62 @@
package cmd
import (
"context"
"fmt"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/spf13/cobra"
)
var stdioCmd = &cobra.Command{
Use: "stdio",
Short: "Run a stdio MCP bridge backed by a remote AMCS server",
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
remote, err := connectRemote(ctx)
if err != nil {
return err
}
defer func() { _ = remote.Close() }()
tools, err := remote.ListTools(ctx, nil)
if err != nil {
return fmt.Errorf("load remote tools: %w", err)
}
server := mcp.NewServer(&mcp.Implementation{
Name: "amcs-cli",
Title: "AMCS CLI Bridge",
Version: "0.0.1",
}, nil)
for _, tool := range tools.Tools {
remoteTool := tool
server.AddTool(&mcp.Tool{
Name: remoteTool.Name,
Description: remoteTool.Description,
InputSchema: remoteTool.InputSchema,
OutputSchema: remoteTool.OutputSchema,
Annotations: remoteTool.Annotations,
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return remote.CallTool(ctx, &mcp.CallToolParams{
Name: req.Params.Name,
Arguments: req.Params.Arguments,
})
})
}
session, err := server.Connect(ctx, &mcp.StdioTransport{}, nil)
if err != nil {
return fmt.Errorf("start stdio bridge: %w", err)
}
defer func() { _ = session.Close() }()
<-ctx.Done()
return nil
},
}
func init() {
rootCmd.AddCommand(stdioCmd)
}

38
cmd/amcs-cli/cmd/tools.go Normal file
View File

@@ -0,0 +1,38 @@
package cmd
import (
"fmt"
"os"
"strings"
"text/tabwriter"
"github.com/spf13/cobra"
)
var toolsCmd = &cobra.Command{
Use: "tools",
Short: "List tools available on the remote AMCS server",
RunE: func(cmd *cobra.Command, _ []string) error {
session, err := connectRemote(cmd.Context())
if err != nil {
return err
}
defer func() { _ = session.Close() }()
res, err := session.ListTools(cmd.Context(), nil)
if err != nil {
return fmt.Errorf("list tools: %w", err)
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "NAME\tDESCRIPTION")
for _, tool := range res.Tools {
fmt.Fprintf(w, "%s\t%s\n", tool.Name, strings.TrimSpace(tool.Description))
}
return w.Flush()
},
}
func init() {
rootCmd.AddCommand(toolsCmd)
}

7
cmd/amcs-cli/main.go Normal file
View File

@@ -0,0 +1,7 @@
package main
import "git.warky.dev/wdevs/amcs/cmd/amcs-cli/cmd"
func main() {
cmd.Execute()
}

3
go.mod
View File

@@ -8,11 +8,13 @@ require (
github.com/jackc/pgx/v5 v5.9.1 github.com/jackc/pgx/v5 v5.9.1
github.com/modelcontextprotocol/go-sdk v1.4.1 github.com/modelcontextprotocol/go-sdk v1.4.1
github.com/pgvector/pgvector-go v0.3.0 github.com/pgvector/pgvector-go v0.3.0
github.com/spf13/cobra v1.10.2
golang.org/x/sync v0.17.0 golang.org/x/sync v0.17.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
@@ -20,6 +22,7 @@ require (
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/segmentio/asm v1.1.3 // indirect github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // indirect github.com/segmentio/encoding v0.5.4 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect

9
go.sum
View File

@@ -1,5 +1,6 @@
entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ= entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ=
entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM= entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -16,6 +17,8 @@ github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbc
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -44,10 +47,15 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -73,6 +81,7 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=

View File

@@ -212,7 +212,6 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st
mux.HandleFunc("/images/project.jpg", serveHomeImage) mux.HandleFunc("/images/project.jpg", serveHomeImage)
mux.HandleFunc("/images/icon.png", serveIcon) mux.HandleFunc("/images/icon.png", serveIcon)
mux.HandleFunc("/llm", serveLLMInstructions) mux.HandleFunc("/llm", serveLLMInstructions)
mux.HandleFunc("/api/status", statusAPIHandler(info, accessTracker, oauthEnabled))
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)

View File

@@ -1,11 +1,9 @@
package app package app
import ( import (
"bytes" "fmt"
"encoding/json" "html"
"io/fs"
"net/http" "net/http"
"path"
"strings" "strings"
"time" "time"
@@ -15,33 +13,131 @@ import (
const connectedWindow = 10 * time.Minute const connectedWindow = 10 * time.Minute
type statusAPIResponse struct { type statusPageData struct {
Title string `json:"title"` Version string
Description string `json:"description"` BuildDate string
Version string `json:"version"` Commit string
BuildDate string `json:"build_date"` ConnectedCount int
Commit string `json:"commit"` TotalKnown int
ConnectedCount int `json:"connected_count"` Entries []auth.AccessSnapshot
TotalKnown int `json:"total_known"` OAuthEnabled bool
ConnectedWindow string `json:"connected_window"`
Entries []auth.AccessSnapshot `json:"entries"`
OAuthEnabled bool `json:"oauth_enabled"`
} }
func statusSnapshot(info buildinfo.Info, tracker *auth.AccessTracker, oauthEnabled bool, now time.Time) statusAPIResponse { func renderHomePage(info buildinfo.Info, tracker *auth.AccessTracker, oauthEnabled bool, now time.Time) string {
entries := tracker.Snapshot() entries := tracker.Snapshot()
return statusAPIResponse{ data := statusPageData{
Title: "Avelon Memory Crystal Server (AMCS)", Version: fallback(info.Version, "dev"),
Description: "AMCS is a memory server that captures, links, and retrieves structured project thoughts for AI assistants using semantic search, summaries, and MCP tools.", BuildDate: fallback(info.BuildDate, "unknown"),
Version: fallback(info.Version, "dev"), Commit: fallback(info.Commit, "unknown"),
BuildDate: fallback(info.BuildDate, "unknown"), ConnectedCount: tracker.ConnectedCount(now, connectedWindow),
Commit: fallback(info.Commit, "unknown"), TotalKnown: len(entries),
ConnectedCount: tracker.ConnectedCount(now, connectedWindow), Entries: entries,
TotalKnown: len(entries), OAuthEnabled: oauthEnabled,
ConnectedWindow: "last 10 minutes",
Entries: entries,
OAuthEnabled: oauthEnabled,
} }
var b strings.Builder
b.WriteString(`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AMCS</title>
<style>
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f5f7fb; color: #172033; }
main { max-width: 980px; margin: 48px auto; background: #fff; border-radius: 12px; box-shadow: 0 10px 28px rgba(23, 32, 51, 0.12); overflow: hidden; }
.content { padding: 28px; }
h1, h2 { margin: 0 0 12px 0; }
p { margin: 0; line-height: 1.5; color: #334155; }
.actions { margin-top: 18px; display: flex; flex-wrap: wrap; gap: 10px; }
.link { display: inline-block; padding: 10px 14px; border-radius: 8px; background: #172033; color: #fff; text-decoration: none; font-weight: 600; }
.link:hover { background: #0f172a; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-top: 24px; }
.card { background: #eef2ff; border-radius: 10px; padding: 16px; }
.label { display: block; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.04em; color: #475569; }
.value { display: block; margin-top: 6px; font-size: 1.4rem; font-weight: 700; color: #0f172a; }
.meta { margin-top: 28px; color: #475569; font-size: 0.95rem; }
table { width: 100%; border-collapse: collapse; margin-top: 16px; }
th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid #e2e8f0; vertical-align: top; }
th { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.04em; color: #475569; }
.empty { margin-top: 16px; color: #64748b; }
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
img { display: block; width: 100%; height: auto; }
</style>
</head>
<body>
<main>
<img src="/images/project.jpg" alt="Avelon Memory Crystal project image">
<div class="content">
<h1>Avelon Memory Crystal Server (AMCS)</h1>
<p>AMCS is a memory server that captures, links, and retrieves structured project thoughts for AI assistants using semantic search, summaries, and MCP tools.</p>
<div class="actions">
<a class="link" href="/llm">LLM Instructions</a>
<a class="link" href="/healthz">Health Check</a>
<a class="link" href="/readyz">Readiness Check</a>`)
if data.OAuthEnabled {
b.WriteString(`
<a class="link" href="/oauth-authorization-server">OAuth Authorization Server</a>`)
}
b.WriteString(`
</div>
<div class="stats">
<div class="card">
<span class="label">Connected users</span>
<span class="value">` + fmt.Sprintf("%d", data.ConnectedCount) + `</span>
</div>
<div class="card">
<span class="label">Known principals</span>
<span class="value">` + fmt.Sprintf("%d", data.TotalKnown) + `</span>
</div>
<div class="card">
<span class="label">Version</span>
<span class="value">` + html.EscapeString(data.Version) + `</span>
</div>
</div>
<div class="meta">
<strong>Build date:</strong> ` + html.EscapeString(data.BuildDate) + ` &nbsp;•&nbsp;
<strong>Commit:</strong> <code>` + html.EscapeString(data.Commit) + `</code> &nbsp;•&nbsp;
<strong>Connected window:</strong> last 10 minutes
</div>
<h2 style="margin-top: 28px;">Recent access</h2>`)
if len(data.Entries) == 0 {
b.WriteString(`
<p class="empty">No authenticated access recorded yet.</p>`)
} else {
b.WriteString(`
<table>
<thead>
<tr>
<th>Principal</th>
<th>Last accessed</th>
<th>Last path</th>
<th>Requests</th>
</tr>
</thead>
<tbody>`)
for _, entry := range data.Entries {
b.WriteString(`
<tr>
<td><code>` + html.EscapeString(entry.KeyID) + `</code></td>
<td>` + html.EscapeString(entry.LastAccessedAt.UTC().Format(time.RFC3339)) + `</td>
<td>` + html.EscapeString(entry.LastPath) + `</td>
<td>` + fmt.Sprintf("%d", entry.RequestCount) + `</td>
</tr>`)
}
b.WriteString(`
</tbody>
</table>`)
}
b.WriteString(`
</div>
</main>
</body>
</html>`)
return b.String()
} }
func fallback(value, defaultValue string) string { func fallback(value, defaultValue string) string {
@@ -51,90 +147,25 @@ func fallback(value, defaultValue string) string {
return value return value
} }
func statusAPIHandler(info buildinfo.Info, tracker *auth.AccessTracker, oauthEnabled bool) http.HandlerFunc { func homeHandler(info buildinfo.Info, tracker *auth.AccessTracker, oauthEnabled bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/status" { if r.URL.Path != "/" {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
if r.Method != http.MethodGet && r.Method != http.MethodHead { if r.Method != http.MethodGet && r.Method != http.MethodHead {
w.Header().Set("Allow", "GET, HEAD") w.Header().Set("Allow", "GET, HEAD")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return return
} }
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
if r.Method == http.MethodHead { if r.Method == http.MethodHead {
return return
} }
_ = json.NewEncoder(w).Encode(statusSnapshot(info, tracker, oauthEnabled, time.Now())) _, _ = w.Write([]byte(renderHomePage(info, tracker, oauthEnabled, time.Now())))
} }
} }
func homeHandler(_ buildinfo.Info, _ *auth.AccessTracker, _ bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
w.Header().Set("Allow", "GET, HEAD")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
requestPath := strings.TrimPrefix(path.Clean(r.URL.Path), "/")
if requestPath == "." {
requestPath = ""
}
if requestPath != "" {
if serveUIAsset(w, r, requestPath) {
return
}
http.NotFound(w, r)
return
}
serveUIIndex(w, r)
}
}
func serveUIAsset(w http.ResponseWriter, r *http.Request, name string) bool {
if uiDistFS == nil {
return false
}
if strings.Contains(name, "..") {
return false
}
file, err := uiDistFS.Open(name)
if err != nil {
return false
}
defer file.Close()
info, err := file.Stat()
if err != nil || info.IsDir() {
return false
}
data, err := fs.ReadFile(uiDistFS, name)
if err != nil {
return false
}
http.ServeContent(w, r, info.Name(), info.ModTime(), bytes.NewReader(data))
return true
}
func serveUIIndex(w http.ResponseWriter, r *http.Request) {
if indexHTML == nil {
http.Error(w, "ui assets not built", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
if r.Method == http.MethodHead {
return
}
_, _ = w.Write(indexHTML)
}

View File

@@ -1,7 +1,6 @@
package app package app
import ( import (
"encoding/json"
"io" "io"
"log/slog" "log/slog"
"net/http" "net/http"
@@ -15,62 +14,29 @@ import (
"git.warky.dev/wdevs/amcs/internal/config" "git.warky.dev/wdevs/amcs/internal/config"
) )
func TestStatusSnapshotHidesOAuthLinkWhenDisabled(t *testing.T) { func TestRenderHomePageHidesOAuthLinkWhenDisabled(t *testing.T) {
tracker := auth.NewAccessTracker() tracker := auth.NewAccessTracker()
snapshot := statusSnapshot(buildinfo.Info{Version: "v1.2.3", BuildDate: "2026-04-04", Commit: "abc123"}, tracker, false, time.Date(2026, 4, 4, 12, 0, 0, 0, time.UTC)) page := renderHomePage(buildinfo.Info{Version: "v1.2.3", BuildDate: "2026-04-04", Commit: "abc123"}, tracker, false, time.Date(2026, 4, 4, 12, 0, 0, 0, time.UTC))
if snapshot.OAuthEnabled { if strings.Contains(page, "/oauth-authorization-server") {
t.Fatal("OAuthEnabled = true, want false") t.Fatal("page unexpectedly contains OAuth link")
} }
if snapshot.ConnectedCount != 0 { if !strings.Contains(page, "Connected users") {
t.Fatalf("ConnectedCount = %d, want 0", snapshot.ConnectedCount) t.Fatal("page missing Connected users stat")
}
if snapshot.Title == "" {
t.Fatal("Title = empty, want non-empty")
} }
} }
func TestStatusSnapshotShowsTrackedAccess(t *testing.T) { func TestRenderHomePageShowsTrackedAccess(t *testing.T) {
tracker := auth.NewAccessTracker() tracker := auth.NewAccessTracker()
now := time.Date(2026, 4, 4, 12, 0, 0, 0, time.UTC) now := time.Date(2026, 4, 4, 12, 0, 0, 0, time.UTC)
tracker.Record("client-a", "/files", "127.0.0.1:1234", "tester", now) tracker.Record("client-a", "/files", "127.0.0.1:1234", "tester", now)
snapshot := statusSnapshot(buildinfo.Info{Version: "v1.2.3"}, tracker, true, now) page := renderHomePage(buildinfo.Info{Version: "v1.2.3"}, tracker, true, now)
if !snapshot.OAuthEnabled { for _, needle := range []string{"client-a", "/files", "1</span>", "/oauth-authorization-server"} {
t.Fatal("OAuthEnabled = false, want true") if !strings.Contains(page, needle) {
} t.Fatalf("page missing %q", needle)
if snapshot.ConnectedCount != 1 { }
t.Fatalf("ConnectedCount = %d, want 1", snapshot.ConnectedCount)
}
if len(snapshot.Entries) != 1 {
t.Fatalf("len(Entries) = %d, want 1", len(snapshot.Entries))
}
if snapshot.Entries[0].KeyID != "client-a" || snapshot.Entries[0].LastPath != "/files" {
t.Fatalf("entry = %+v, want keyID client-a and path /files", snapshot.Entries[0])
}
}
func TestStatusAPIHandlerReturnsJSON(t *testing.T) {
handler := statusAPIHandler(buildinfo.Info{Version: "v1"}, auth.NewAccessTracker(), true)
req := httptest.NewRequest(http.MethodGet, "/api/status", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}
if got := rec.Header().Get("Content-Type"); !strings.Contains(got, "application/json") {
t.Fatalf("content-type = %q, want application/json", got)
}
var payload statusAPIResponse
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if payload.Version != "v1" {
t.Fatalf("version = %q, want %q", payload.Version, "v1")
} }
} }
@@ -89,21 +55,6 @@ func TestHomeHandlerAllowsHead(t *testing.T) {
} }
} }
func TestHomeHandlerServesIndex(t *testing.T) {
handler := homeHandler(buildinfo.Info{Version: "v1"}, auth.NewAccessTracker(), false)
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}
if !strings.Contains(rec.Body.String(), "<div id=\"app\"></div>") {
t.Fatalf("body = %q, want embedded UI index", rec.Body.String())
}
}
func TestMiddlewareRecordsAuthenticatedAccess(t *testing.T) { func TestMiddlewareRecordsAuthenticatedAccess(t *testing.T) {
keyring, err := auth.NewKeyring([]config.APIKey{{ID: "client-a", Value: "secret"}}) keyring, err := auth.NewKeyring([]config.APIKey{{ID: "client-a", Value: "secret"}})
if err != nil { if err != nil {

View File

@@ -1,22 +0,0 @@
package app
import (
"embed"
"io/fs"
)
var (
//go:embed ui/dist
uiFiles embed.FS
uiDistFS fs.FS
indexHTML []byte
)
func init() {
dist, err := fs.Sub(uiFiles, "ui/dist")
if err != nil {
return
}
uiDistFS = dist
indexHTML, _ = fs.ReadFile(uiDistFS, "index.html")
}

File diff suppressed because it is too large Load Diff

35
schema/README.md Normal file
View File

@@ -0,0 +1,35 @@
# Schema workflow
The `schema/*.dbml` files are the database schema source of truth.
## Generate SQL migrations
Run:
```bash
make generate-migrations
```
This uses `relspec` to convert the DBML files into PostgreSQL SQL and writes the generated schema migration to:
- `migrations/020_generated_schema.sql`
## Check schema drift
Run:
```bash
make check-schema-drift
```
This regenerates the SQL from `schema/*.dbml` and compares it with `migrations/020_generated_schema.sql`.
If the generated output differs, the command fails so CI can catch schema drift.
## Workflow
1. Update the DBML files in `schema/`
2. Run `make generate-migrations`
3. Review the generated SQL
4. Commit both the DBML changes and the generated migration
Existing handwritten migrations stay in place. Going forward, update the DBML first and regenerate the SQL from there.

44
schema/calendar.dbml Normal file
View File

@@ -0,0 +1,44 @@
Table family_members {
id uuid [pk, default: `gen_random_uuid()`]
name text [not null]
relationship text
birth_date date
notes text
created_at timestamptz [not null, default: `now()`]
}
Table activities {
id uuid [pk, default: `gen_random_uuid()`]
family_member_id uuid [ref: > family_members.id]
title text [not null]
activity_type text
day_of_week text
start_time time
end_time time
start_date date
end_date date
location text
notes text
created_at timestamptz [not null, default: `now()`]
indexes {
day_of_week
family_member_id
(start_date, end_date)
}
}
Table important_dates {
id uuid [pk, default: `gen_random_uuid()`]
family_member_id uuid [ref: > family_members.id]
title text [not null]
date_value date [not null]
recurring_yearly boolean [not null, default: false]
reminder_days_before int [not null, default: 7]
notes text
created_at timestamptz [not null, default: `now()`]
indexes {
date_value
}
}

48
schema/core.dbml Normal file
View File

@@ -0,0 +1,48 @@
Table thoughts {
id bigserial [pk]
guid uuid [unique, not null, default: `gen_random_uuid()`]
content text [not null]
metadata jsonb [default: `'{}'::jsonb`]
created_at timestamptz [default: `now()`]
updated_at timestamptz [default: `now()`]
project_id uuid [ref: > projects.guid]
archived_at timestamptz
}
Table projects {
id bigserial [pk]
guid uuid [unique, not null, default: `gen_random_uuid()`]
name text [unique, not null]
description text
created_at timestamptz [default: `now()`]
last_active_at timestamptz [default: `now()`]
}
Table thought_links {
from_id bigint [not null, ref: > thoughts.id]
to_id bigint [not null, ref: > thoughts.id]
relation text [not null]
created_at timestamptz [default: `now()`]
indexes {
(from_id, to_id, relation) [pk]
from_id
to_id
}
}
Table embeddings {
id bigserial [pk]
guid uuid [unique, not null, default: `gen_random_uuid()`]
thought_id uuid [not null, ref: > thoughts.guid]
model text [not null]
dim int [not null]
embedding vector [not null]
created_at timestamptz [default: `now()`]
updated_at timestamptz [default: `now()`]
indexes {
(thought_id, model) [unique]
thought_id
}
}

53
schema/crm.dbml Normal file
View File

@@ -0,0 +1,53 @@
Table professional_contacts {
id uuid [pk, default: `gen_random_uuid()`]
name text [not null]
company text
title text
email text
phone text
linkedin_url text
how_we_met text
tags "text[]" [not null, default: `'{}'`]
notes text
last_contacted timestamptz
follow_up_date date
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
indexes {
last_contacted
follow_up_date
}
}
Table contact_interactions {
id uuid [pk, default: `gen_random_uuid()`]
contact_id uuid [not null, ref: > professional_contacts.id]
interaction_type text [not null]
occurred_at timestamptz [not null, default: `now()`]
summary text [not null]
follow_up_needed boolean [not null, default: false]
follow_up_notes text
created_at timestamptz [not null, default: `now()`]
indexes {
(contact_id, occurred_at)
}
}
Table opportunities {
id uuid [pk, default: `gen_random_uuid()`]
contact_id uuid [ref: > professional_contacts.id]
title text [not null]
description text
stage text [not null, default: 'identified']
value "decimal(12,2)"
expected_close_date date
notes text
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
indexes {
stage
}
}

25
schema/files.dbml Normal file
View File

@@ -0,0 +1,25 @@
Table stored_files {
id bigserial [pk]
guid uuid [unique, not null, default: `gen_random_uuid()`]
thought_id uuid [ref: > thoughts.guid]
project_id uuid [ref: > projects.guid]
name text [not null]
media_type text [not null]
kind text [not null, default: 'file']
encoding text [not null, default: 'base64']
size_bytes bigint [not null]
sha256 text [not null]
content bytea [not null]
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
indexes {
thought_id
project_id
sha256
}
}
// Cross-file refs (for relspecgo merge)
Ref: stored_files.thought_id > thoughts.guid [delete: set null]
Ref: stored_files.project_id > projects.guid [delete: set null]

31
schema/household.dbml Normal file
View File

@@ -0,0 +1,31 @@
Table household_items {
id uuid [pk, default: `gen_random_uuid()`]
name text [not null]
category text
location text
details jsonb [not null, default: `'{}'`]
notes text
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
indexes {
category
}
}
Table household_vendors {
id uuid [pk, default: `gen_random_uuid()`]
name text [not null]
service_type text
phone text
email text
website text
notes text
rating int
last_used date
created_at timestamptz [not null, default: `now()`]
indexes {
service_type
}
}

30
schema/maintenance.dbml Normal file
View File

@@ -0,0 +1,30 @@
Table maintenance_tasks {
id uuid [pk, default: `gen_random_uuid()`]
name text [not null]
category text
frequency_days int
last_completed timestamptz
next_due timestamptz
priority text [not null, default: 'medium']
notes text
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
indexes {
next_due
}
}
Table maintenance_logs {
id uuid [pk, default: `gen_random_uuid()`]
task_id uuid [not null, ref: > maintenance_tasks.id]
completed_at timestamptz [not null, default: `now()`]
performed_by text
cost "decimal(10,2)"
notes text
next_action text
indexes {
(task_id, completed_at)
}
}

49
schema/meals.dbml Normal file
View File

@@ -0,0 +1,49 @@
Table recipes {
id uuid [pk, default: `gen_random_uuid()`]
name text [not null]
cuisine text
prep_time_minutes int
cook_time_minutes int
servings int
ingredients jsonb [not null, default: `'[]'`]
instructions jsonb [not null, default: `'[]'`]
tags "text[]" [not null, default: `'{}'`]
rating int
notes text
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
indexes {
cuisine
tags
}
}
Table meal_plans {
id uuid [pk, default: `gen_random_uuid()`]
week_start date [not null]
day_of_week text [not null]
meal_type text [not null]
recipe_id uuid [ref: > recipes.id]
custom_meal text
servings int
notes text
created_at timestamptz [not null, default: `now()`]
indexes {
week_start
}
}
Table shopping_lists {
id uuid [pk, default: `gen_random_uuid()`]
week_start date [unique, not null]
items jsonb [not null, default: `'[]'`]
notes text
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
indexes {
week_start
}
}

32
schema/meta.dbml Normal file
View File

@@ -0,0 +1,32 @@
Table chat_histories {
id uuid [pk, default: `gen_random_uuid()`]
session_id text [not null]
title text
channel text
agent_id text
project_id uuid [ref: > projects.guid]
messages jsonb [not null, default: `'[]'`]
summary text
metadata jsonb [not null, default: `'{}'`]
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
indexes {
session_id
project_id
channel
agent_id
created_at
}
}
Table tool_annotations {
id bigserial [pk]
tool_name text [unique, not null]
notes text [not null, default: '']
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
}
// Cross-file refs (for relspecgo merge)
Ref: chat_histories.project_id > projects.guid [delete: set null]

46
schema/skills.dbml Normal file
View File

@@ -0,0 +1,46 @@
Table agent_skills {
id uuid [pk, default: `gen_random_uuid()`]
name text [unique, not null]
description text [not null, default: '']
content text [not null]
tags "text[]" [not null, default: `'{}'`]
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
}
Table agent_guardrails {
id uuid [pk, default: `gen_random_uuid()`]
name text [unique, not null]
description text [not null, default: '']
content text [not null]
severity text [not null, default: 'medium']
tags "text[]" [not null, default: `'{}'`]
created_at timestamptz [not null, default: `now()`]
updated_at timestamptz [not null, default: `now()`]
}
Table project_skills {
project_id uuid [not null, ref: > projects.guid]
skill_id uuid [not null, ref: > agent_skills.id]
created_at timestamptz [not null, default: `now()`]
indexes {
(project_id, skill_id) [pk]
project_id
}
}
Table project_guardrails {
project_id uuid [not null, ref: > projects.guid]
guardrail_id uuid [not null, ref: > agent_guardrails.id]
created_at timestamptz [not null, default: `now()`]
indexes {
(project_id, guardrail_id) [pk]
project_id
}
}
// Cross-file refs (for relspecgo merge)
Ref: project_skills.project_id > projects.guid [delete: cascade]
Ref: project_guardrails.project_id > projects.guid [delete: cascade]

View File

@@ -2,11 +2,4 @@
set -euo pipefail set -euo pipefail
CONFIG_PATH="${1:-configs/dev.yaml}" go run ./cmd/amcs-server --config "${1:-configs/dev.yaml}"
if [[ ! -f internal/app/ui/dist/index.html ]]; then
echo "UI build not found; building frontend first..."
make ui-build
fi
go run ./cmd/amcs-server --config "$CONFIG_PATH"

View File

@@ -1,16 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AMCS</title>
<meta
name="description"
content="AMCS is a memory server that captures, links, and retrieves structured project thoughts for AI assistants using semantic search, summaries, and MCP tools."
/>
</head>
<body class="bg-slate-950">
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,23 +0,0 @@
{
"name": "amcs-ui",
"private": true,
"version": "0.0.0",
"packageManager": "pnpm@10.33.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"check": "svelte-check --tsconfig ./tsconfig.json",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/vite": "^4.1.4",
"@types/node": "^24.5.2",
"svelte": "^5.28.2",
"svelte-check": "^4.1.6",
"tailwindcss": "^4.1.4",
"typescript": "^5.8.3",
"vite": "^6.3.2"
}
}

1291
ui/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,257 +0,0 @@
<script lang="ts">
import { onMount } from "svelte";
type AccessEntry = {
key_id: string;
last_accessed_at: string;
last_path: string;
request_count: number;
};
type StatusResponse = {
title: string;
description: string;
version: string;
build_date: string;
commit: string;
connected_count: number;
total_known: number;
connected_window: string;
oauth_enabled: boolean;
entries: AccessEntry[];
};
let data: StatusResponse | null = null;
let loading = true;
let error = "";
const quickLinks = [
{ href: "/llm", label: "LLM Instructions" },
{ href: "/healthz", label: "Health Check" },
{ href: "/readyz", label: "Readiness Check" },
];
async function loadStatus() {
loading = true;
error = "";
try {
const response = await fetch("/api/status");
if (!response.ok) {
throw new Error(`Status request failed with ${response.status}`);
}
data = (await response.json()) as StatusResponse;
} catch (err) {
error = err instanceof Error ? err.message : "Failed to load status";
} finally {
loading = false;
}
}
function formatDate(value: string) {
return new Date(value).toLocaleString();
}
onMount(loadStatus);
</script>
<svelte:head>
<title>AMCS</title>
</svelte:head>
<div class="min-h-screen bg-slate-950 text-slate-100">
<main
class="mx-auto flex min-h-screen max-w-7xl flex-col px-4 py-6 sm:px-6 lg:px-8"
>
<section
class="overflow-hidden rounded-3xl border border-white/10 bg-slate-900 shadow-2xl shadow-slate-950/40"
>
<img
src="/images/project.jpg"
alt="Avelon Memory Crystal"
class="h-64 w-full object-cover object-center sm:h-80"
/>
<div class="grid gap-8 p-6 sm:p-8 lg:grid-cols-[1.6fr_1fr] lg:p-10">
<div class="space-y-6">
<div class="space-y-4">
<div
class="inline-flex items-center gap-2 rounded-full border border-cyan-400/20 bg-cyan-400/10 px-3 py-1 text-sm font-medium text-cyan-200"
>
<span class="h-2 w-2 rounded-full bg-emerald-400"></span>
Avalon Memory Crystal Server
</div>
<div>
<h1
class="text-3xl font-semibold tracking-tight text-white sm:text-4xl"
>
Avelon Memory Crystal Server (AMCS)
</h1>
<p
class="mt-3 max-w-3xl text-base leading-7 text-slate-300 sm:text-lg"
>
{data?.description ??
"AMCS is a memory server that captures, links, and retrieves structured project thoughts for AI assistants using semantic search, summaries, and MCP tools."}
</p>
</div>
</div>
<div class="flex flex-wrap gap-3">
{#each quickLinks as link}
<a
class="inline-flex items-center justify-center rounded-xl border border-cyan-300/20 bg-cyan-400/10 px-4 py-2 text-sm font-semibold text-cyan-100 transition hover:border-cyan-300/40 hover:bg-cyan-400/20"
href={link.href}>{link.label}</a
>
{/each}
{#if data?.oauth_enabled}
<a
class="inline-flex items-center justify-center rounded-xl border border-violet-300/20 bg-violet-400/10 px-4 py-2 text-sm font-semibold text-violet-100 transition hover:border-violet-300/40 hover:bg-violet-400/20"
href="/oauth-authorization-server">OAuth Authorization Server</a
>
{/if}
</div>
<div class="grid gap-4 sm:grid-cols-3">
<div class="rounded-2xl border border-white/10 bg-white/5 p-5">
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">
Connected users
</p>
<p class="mt-2 text-3xl font-semibold text-white">
{data?.connected_count ?? "—"}
</p>
</div>
<div class="rounded-2xl border border-white/10 bg-white/5 p-5">
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">
Known principals
</p>
<p class="mt-2 text-3xl font-semibold text-white">
{data?.total_known ?? "—"}
</p>
</div>
<div class="rounded-2xl border border-white/10 bg-white/5 p-5">
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">
Version
</p>
<p class="mt-2 break-all text-2xl font-semibold text-white">
{data?.version ?? "—"}
</p>
</div>
</div>
</div>
<aside
class="space-y-4 rounded-2xl border border-white/10 bg-slate-950/50 p-5"
>
<div>
<h2 class="text-lg font-semibold text-white">Build details</h2>
<p class="mt-1 text-sm text-slate-400">The same status info.</p>
</div>
<dl class="space-y-3 text-sm text-slate-300">
<div>
<dt class="text-slate-500">Build date</dt>
<dd class="mt-1 font-medium text-white">
{data?.build_date ?? "unknown"}
</dd>
</div>
<div>
<dt class="text-slate-500">Commit</dt>
<dd
class="mt-1 break-all rounded-lg bg-white/5 px-3 py-2 font-mono text-xs text-cyan-100"
>
{data?.commit ?? "unknown"}
</dd>
</div>
<div>
<dt class="text-slate-500">Connected window</dt>
<dd class="mt-1 font-medium text-white">
{data?.connected_window ?? "last 10 minutes"}
</dd>
</div>
</dl>
</aside>
</div>
</section>
<section
class="mt-6 rounded-3xl border border-white/10 bg-slate-900/80 p-6 shadow-xl shadow-slate-950/20 sm:p-8"
>
<div
class="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between"
>
<div>
<h2 class="text-2xl font-semibold text-white">Recent access</h2>
<p class="mt-1 text-sm text-slate-400">
Authenticated principals AMCS has seen recently.
</p>
</div>
<button
class="inline-flex items-center justify-center rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-sm font-medium text-slate-200 transition hover:bg-white/10"
on:click={loadStatus}
>
Refresh
</button>
</div>
{#if loading}
<div
class="mt-6 rounded-2xl border border-dashed border-white/10 bg-slate-950/40 px-4 py-10 text-center text-slate-400"
>
Loading status…
</div>
{:else if error}
<div
class="mt-6 rounded-2xl border border-rose-400/30 bg-rose-400/10 px-4 py-6 text-sm text-rose-100"
>
<p class="font-semibold">Couldnt load the status snapshot.</p>
<p class="mt-1 text-rose-100/80">{error}</p>
</div>
{:else if data && data.entries.length === 0}
<div
class="mt-6 rounded-2xl border border-dashed border-white/10 bg-slate-950/40 px-4 py-10 text-center text-slate-400"
>
No authenticated access recorded yet.
</div>
{:else if data}
<div class="mt-6 overflow-hidden rounded-2xl border border-white/10">
<div class="overflow-x-auto">
<table
class="min-w-full divide-y divide-white/10 text-left text-sm text-slate-300"
>
<thead
class="bg-white/5 text-xs uppercase tracking-[0.2em] text-slate-500"
>
<tr>
<th class="px-4 py-3 font-medium">Principal</th>
<th class="px-4 py-3 font-medium">Last accessed</th>
<th class="px-4 py-3 font-medium">Last path</th>
<th class="px-4 py-3 font-medium">Requests</th>
</tr>
</thead>
<tbody class="divide-y divide-white/5 bg-slate-950/30">
{#each data.entries as entry}
<tr class="hover:bg-white/[0.03]">
<td class="px-4 py-3 align-top"
><code
class="rounded bg-white/5 px-2 py-1 font-mono text-xs text-cyan-100"
>{entry.key_id}</code
></td
>
<td class="px-4 py-3 align-top text-slate-200"
>{formatDate(entry.last_accessed_at)}</td
>
<td class="px-4 py-3 align-top"
><code class="text-slate-100">{entry.last_path}</code></td
>
<td class="px-4 py-3 align-top font-semibold text-white"
>{entry.request_count}</td
>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
</section>
</main>
</div>

View File

@@ -1,16 +0,0 @@
@import 'tailwindcss';
:root {
color-scheme: dark;
font-family: Inter, system-ui, sans-serif;
}
html,
body,
#app {
min-height: 100%;
}
body {
margin: 0;
}

View File

@@ -1,9 +0,0 @@
import './app.css';
import App from './App.svelte';
import { mount } from 'svelte';
const app = mount(App, {
target: document.getElementById('app')!
});
export default app;

View File

@@ -1,5 +0,0 @@
export default {
compilerOptions: {
dev: process.env.NODE_ENV !== 'production'
}
};

View File

@@ -1,15 +0,0 @@
{
"extends": "./tsconfig.node.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Bundler",
"target": "ES2022",
"lib": ["ESNext", "DOM"],
"verbatimModuleSyntax": true,
"strict": true,
"allowJs": true,
"checkJs": false,
"types": ["svelte", "node"]
},
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte", "vite.config.ts"]
}

View File

@@ -1,8 +0,0 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"allowSyntheticDefaultImports": true
}
}

View File

@@ -1,31 +0,0 @@
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import tailwindcss from '@tailwindcss/vite';
const backendTarget = process.env.AMCS_UI_BACKEND ?? 'http://127.0.0.1:8080';
export default defineConfig({
plugins: [svelte(), tailwindcss()],
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': backendTarget,
'/healthz': backendTarget,
'/readyz': backendTarget,
'/llm': backendTarget,
'/images': backendTarget,
'/favicon.ico': backendTarget,
'/mcp': backendTarget,
'/files': backendTarget,
'/oauth-authorization-server': backendTarget,
'/authorize': backendTarget,
'/oauth': backendTarget,
'/.well-known': backendTarget
}
},
build: {
outDir: '../internal/app/ui/dist',
emptyOutDir: true
}
});