2 Commits

Author SHA1 Message Date
f6a86e3933 .
Some checks failed
CI / build-and-test (push) Failing after -32m5s
2026-04-27 00:04:11 +02:00
a4193b295a fix(ui): update AMCS references and add status handling
* Corrected "Advanced Module Control System" to "Avalon Memory Control Service" in documentation and UI components.
* Added status handling to the LoginInfoPanel and LoginPage components.
* Implemented new endpoints for robots.txt and llms.txt.
2026-04-27 00:04:08 +02:00
11 changed files with 428 additions and 35 deletions

View File

@@ -1,10 +1,10 @@
# AMCS Directory # AMCS Directory
This is the AMCS (Advanced Module Control System) directory. This is the AMCS (Avalon Memory Control Service) directory.
## Purpose ## Purpose
The AMCS directory is used to store configuration and code for the Advanced Module Control System, which handles... The AMCS directory is used to store configuration and code for the Avalon Memory Control Service, which handles...
## Structure ## Structure

View File

@@ -243,7 +243,11 @@ 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("/llms.txt", serveLLMSTXT)
mux.HandleFunc("/.well-known/llms.txt", serveLLMSTXT)
mux.HandleFunc("/robots.txt", serveRobotsTXT)
mux.HandleFunc("/api/status", statusAPIHandler(info, accessTracker, oauthEnabled)) mux.HandleFunc("/api/status", statusAPIHandler(info, accessTracker, oauthEnabled))
mux.HandleFunc("/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,7 +1,9 @@
package app package app
import ( import (
"fmt"
"net/http" "net/http"
"strings"
amcsllm "git.warky.dev/wdevs/amcs/llm" amcsllm "git.warky.dev/wdevs/amcs/llm"
) )
@@ -20,3 +22,74 @@ func serveLLMInstructions(w http.ResponseWriter, r *http.Request) {
} }
_, _ = w.Write(amcsllm.MemoryInstructions) _, _ = w.Write(amcsllm.MemoryInstructions)
} }
func serveRobotsTXT(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/robots.txt" {
http.NotFound(w, r)
return
}
if r.Method != http.MethodGet && r.Method != http.MethodHead {
w.Header().Set("Allow", "GET, HEAD")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Cache-Control", "public, max-age=300")
w.WriteHeader(http.StatusOK)
if r.Method == http.MethodHead {
return
}
body := fmt.Sprintf("User-agent: *\nAllow: /\n\n# LLM-friendly docs\nLLM: %s/llm\nLLMS: %s/llms.txt\n", requestBaseURL(r), requestBaseURL(r))
_, _ = w.Write([]byte(body))
}
func serveLLMSTXT(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/llms.txt" && r.URL.Path != "/.well-known/llms.txt" {
http.NotFound(w, r)
return
}
if r.Method != http.MethodGet && r.Method != http.MethodHead {
w.Header().Set("Allow", "GET, HEAD")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Cache-Control", "public, max-age=300")
w.WriteHeader(http.StatusOK)
if r.Method == http.MethodHead {
return
}
base := requestBaseURL(r)
body := fmt.Sprintf(
"# AMCS\n\n> A memory server for AI assistants (MCP tools, semantic retrieval, and structured project memory).\n\n## Endpoints\n- %s/llm\n- %s/status\n- %s/mcp\n- %s/.well-known/oauth-authorization-server\n",
base,
base,
base,
base,
)
_, _ = w.Write([]byte(body))
}
func requestBaseURL(r *http.Request) string {
scheme := "http"
if r != nil && r.TLS != nil {
scheme = "https"
}
if r != nil {
if proto := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto")); proto != "" {
scheme = proto
}
}
host := "localhost"
if r != nil {
if v := strings.TrimSpace(r.Host); v != "" {
host = v
}
}
return scheme + "://" + host
}

View File

@@ -3,6 +3,7 @@ package app
import ( import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings"
"testing" "testing"
amcsllm "git.warky.dev/wdevs/amcs/llm" amcsllm "git.warky.dev/wdevs/amcs/llm"
@@ -29,3 +30,70 @@ func TestServeLLMInstructions(t *testing.T) {
t.Fatalf("body = %q, want embedded instructions", body) t.Fatalf("body = %q, want embedded instructions", body)
} }
} }
func TestServeRobotsTXT(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/robots.txt", nil)
req.Host = "amcs.example.com"
req.Header.Set("X-Forwarded-Proto", "https")
rec := httptest.NewRecorder()
serveRobotsTXT(rec, req)
res := rec.Result()
defer func() {
_ = res.Body.Close()
}()
if res.StatusCode != http.StatusOK {
t.Fatalf("status = %d, want %d", res.StatusCode, http.StatusOK)
}
if got := res.Header.Get("Content-Type"); got != "text/plain; charset=utf-8" {
t.Fatalf("content-type = %q, want %q", got, "text/plain; charset=utf-8")
}
body := rec.Body.String()
if !strings.Contains(body, "LLM: https://amcs.example.com/llm") {
t.Fatalf("body = %q, want LLM link", body)
}
if !strings.Contains(body, "LLMS: https://amcs.example.com/llms.txt") {
t.Fatalf("body = %q, want LLMS link", body)
}
}
func TestServeLLMSTXT(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/llms.txt", nil)
req.Host = "amcs.example.com"
req.Header.Set("X-Forwarded-Proto", "https")
rec := httptest.NewRecorder()
serveLLMSTXT(rec, req)
res := rec.Result()
defer func() {
_ = res.Body.Close()
}()
if res.StatusCode != http.StatusOK {
t.Fatalf("status = %d, want %d", res.StatusCode, http.StatusOK)
}
if got := res.Header.Get("Content-Type"); got != "text/plain; charset=utf-8" {
t.Fatalf("content-type = %q, want %q", got, "text/plain; charset=utf-8")
}
body := rec.Body.String()
if !strings.Contains(body, "https://amcs.example.com/llm") {
t.Fatalf("body = %q, want /llm link", body)
}
if !strings.Contains(body, "https://amcs.example.com/.well-known/oauth-authorization-server") {
t.Fatalf("body = %q, want oauth discovery link", body)
}
}
func TestServeLLMSTXTWellKnownPath(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/.well-known/llms.txt", nil)
rec := httptest.NewRecorder()
serveLLMSTXT(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}
}

View File

@@ -55,7 +55,7 @@ func fallback(value, defaultValue string) string {
func statusAPIHandler(info buildinfo.Info, tracker *auth.AccessTracker, oauthEnabled bool) http.HandlerFunc { func statusAPIHandler(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 != "/api/status" && r.URL.Path != "/status" {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }

View File

@@ -86,6 +86,26 @@ func TestStatusAPIHandlerReturnsJSON(t *testing.T) {
} }
} }
func TestStatusAPIHandlerSupportsStatusPath(t *testing.T) {
handler := statusAPIHandler(buildinfo.Info{Version: "v1"}, auth.NewAccessTracker(), true)
req := httptest.NewRequest(http.MethodGet, "/status", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}
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")
}
}
func TestHomeHandlerAllowsHead(t *testing.T) { func TestHomeHandlerAllowsHead(t *testing.T) {
handler := homeHandler(buildinfo.Info{Version: "v1"}, auth.NewAccessTracker(), false) handler := homeHandler(buildinfo.Info{Version: "v1"}, auth.NewAccessTracker(), false)
req := httptest.NewRequest(http.MethodHead, "/", nil) req := httptest.NewRequest(http.MethodHead, "/", nil)

View File

@@ -1,6 +1,6 @@
# AMCS Memory Instructions # AMCS Memory Instructions
AMCS (Avalon Memory Crystal Server) is an MCP server for capturing and retrieving thoughts, memory, and project context. It is backed by Postgres with pgvector for semantic search. AMCS (Avalon Memory Control Service) is an MCP server for capturing and retrieving thoughts, memory, and project context. It is backed by Postgres with pgvector for semantic search.
`amcs-cli` is a pre-built CLI that connects to the AMCS MCP server so agents do not need to implement their own HTTP MCP client. Download it from https://git.warky.dev/wdevs/amcs/releases `amcs-cli` is a pre-built CLI that connects to the AMCS MCP server so agents do not need to implement their own HTTP MCP client. Download it from https://git.warky.dev/wdevs/amcs/releases

View File

@@ -153,9 +153,7 @@
await GlobalStateStore.getState().fetchData(); await GlobalStateStore.getState().fetchData();
if (GlobalStateStore.getState().isLoggedIn()) { await loadStatus();
await loadStatus();
}
}); });
</script> </script>
@@ -171,6 +169,9 @@
{authBusy} {authBusy}
{authError} {authError}
{authMessage} {authMessage}
statusData={data}
statusLoading={loading}
statusError={error}
onlogin={handleCredentialLogin} onlogin={handleCredentialLogin}
/> />
{:else} {:else}

View File

@@ -1,34 +1,226 @@
<script lang="ts"> <script lang="ts">
const { isOAuthCallback }: { isOAuthCallback: boolean } = $props(); import type { StatusResponse } from "../../types";
type IntelligenceCard = {
id: string;
title: string;
accentClass: string;
summary: string;
detail: string;
tools: string[];
};
const {
isOAuthCallback,
data,
loading,
error,
}: {
isOAuthCallback: boolean;
data: StatusResponse | null;
loading: boolean;
error: string;
} = $props();
const intelligenceCards: IntelligenceCard[] = [
{
id: "projects",
title: "Projects",
accentClass: "text-indigo-200",
summary:
"Named containers that scope memory and operations so retrieval stays focused on the right workstream.",
detail:
"Project context can be resolved explicitly or from active session scope depending on client behavior.",
tools: ["list_projects", "create_project", "get_project_context"],
},
{
id: "thoughts",
title: "Thoughts",
accentClass: "text-violet-200",
summary:
"Core memory records with metadata and links that power search, recall, summaries, and relationship traversal.",
detail:
"Thought capture can include metadata enrichment and link-based navigation for richer retrieval.",
tools: ["capture_thought", "search_thoughts", "related_thoughts"],
},
{
id: "skills",
title: "Skills",
accentClass: "text-cyan-200",
summary:
"Reusable agent instructions and capabilities that can be linked to a project and loaded with it.",
detail:
"Use project-linked skills to keep behavior consistent across sessions and assistants.",
tools: ["list_project_skills", "add_skill", "add_project_skill"],
},
{
id: "guardrails",
title: "Guardrails",
accentClass: "text-amber-200",
summary:
"Safety and policy constraints with severity levels that can be enforced globally or per project.",
detail:
"Guardrails provide stable operational boundaries for memory and tool usage behavior.",
tools: [
"list_project_guardrails",
"add_guardrail",
"add_project_guardrail",
],
},
{
id: "learnings",
title: "Learnings",
accentClass: "text-emerald-200",
summary:
"Curated records for durable lessons and decisions, separate from raw thoughts for cleaner review.",
detail:
"Structured fields such as status, priority, and confidence support operational follow-through.",
tools: ["add_learning", "list_learnings", "get_learning"],
},
{
id: "vector-metadata-build",
title: "Vector + Metadata Build",
accentClass: "text-fuchsia-200",
summary:
"Backfill and repair flows for embeddings and metadata so retrieval quality stays healthy over time.",
detail:
"Use dry runs for safe audits, then run updates to regenerate missing vectors or retry failed metadata.",
tools: [
"backfill_embeddings",
"reparse_thought_metadata",
"retry_failed_metadata",
],
},
];
let activeCard = $state("projects");
function setActiveCard(id: string) {
activeCard = id;
}
function handleCardKeydown(event: KeyboardEvent, id: string) {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
setActiveCard(id);
}
}
</script> </script>
<div class="rounded-3xl border border-cyan-400/20 bg-slate-900/80 p-8 shadow-2xl shadow-slate-950/40"> <div
<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"> class="rounded-3xl border border-cyan-400/20 bg-slate-900/80 p-8 shadow-2xl shadow-slate-950/40"
>
<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> <span class="h-2 w-2 rounded-full bg-emerald-400"></span>
AMCS Control Interface AMCS (Avalon Memory Control Service)
</div> </div>
<h1 class="mt-6 text-4xl font-semibold tracking-tight text-white"> <h1 class="mt-6 text-4xl font-semibold tracking-tight text-white">
{#if isOAuthCallback} {#if isOAuthCallback}
Completing login Completing login...
{:else} {:else}
Login Avalon Memory Control Service
{/if} {/if}
</h1> </h1>
<p class="mt-3 max-w-2xl text-base leading-7 text-slate-300"> <p class="mt-3 max-w-2xl text-base leading-7 text-slate-300">
Origin-style operator access for the AMCS admin interface. ResolveSpec OAuth is the front door now, AMCS is a Go MCP server for capturing project thoughts, semantic retrieval,
not the old login shortcut. summaries, and linked memory workflows with Postgres + pgvector.
</p>
<p class="mt-2 max-w-2xl text-sm leading-6 text-slate-400">
It stores durable memory for assistants, supports project scoping, and
exposes tools over MCP for capture, search, context recall, and structured
operations.
</p> </p>
<div class="mt-8 grid gap-4 sm:grid-cols-2"> <div class="mt-8 grid gap-4 sm:grid-cols-3">
<div class="rounded-2xl border border-white/10 bg-white/5 p-5"> <div class="rounded-2xl border border-white/10 bg-white/5 p-5">
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">Primary module</p> <p class="text-sm uppercase tracking-[0.2em] text-slate-400">
<p class="mt-2 text-2xl font-semibold text-white">Projects</p> Server status
<p class="mt-2 text-sm text-slate-400">Projects are the first real admin screen in this rollout.</p> </p>
{#if loading}
<p class="mt-2 text-lg font-semibold text-white">Loading…</p>
{:else if error}
<p class="mt-2 text-sm font-medium text-rose-300">Unavailable</p>
<p class="mt-2 text-xs text-rose-200/80">{error}</p>
{:else if data}
<p class="mt-2 text-2xl font-semibold text-white">{data.version}</p>
<p class="mt-2 text-sm text-slate-400">
{data.connected_count} connected in {data.connected_window}
</p>
<a
href="/status"
class="mt-3 inline-flex items-center rounded-lg border border-emerald-300/30 bg-emerald-400/10 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.12em] text-emerald-100 transition hover:border-emerald-300/50 hover:bg-emerald-400/20"
>
Status Endpoint
</a>
{:else}
<p class="mt-2 text-sm text-slate-400">No status snapshot yet.</p>
{/if}
</div> </div>
<div class="rounded-2xl border border-white/10 bg-white/5 p-5"> <div class="rounded-2xl border border-white/10 bg-white/5 p-5">
<p class="text-sm uppercase tracking-[0.2em] text-slate-400">OAuth path</p> <p class="text-sm uppercase tracking-[0.2em] text-slate-400">
<p class="mt-2 text-2xl font-semibold text-white">ResolveSpec</p> Memory stack
<p class="mt-2 text-sm text-slate-400">Client registration, authorize, callback, token exchange.</p> </p>
<p class="mt-2 text-2xl font-semibold text-white">Postgres + pgvector</p>
<p class="mt-2 text-sm text-slate-400">
Semantic search with full-text fallback when vectors are missing.
</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">
Operator docs
</p>
<a
href="/llm"
class="mt-2 inline-flex items-center rounded-lg border border-cyan-300/30 bg-cyan-400/10 px-3 py-2 text-sm font-semibold text-cyan-100 transition hover:border-cyan-300/50 hover:bg-cyan-400/20"
>
Open LLM Instructions
</a>
<p class="mt-2 text-sm text-slate-400">
Tool behavior, workflows, and MCP guidance for assistants.
</p>
</div>
</div>
<div class="mt-6 rounded-2xl border border-white/10 bg-slate-950/35 p-5">
<h2 class="text-lg font-semibold text-white">Project intelligence model</h2>
<p class="mt-2 text-sm text-slate-400">
AMCS separates reusable behavior, safety constraints, and curated
knowledge so assistants can be guided consistently across sessions.
</p>
<div class="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{#each intelligenceCards as card}
<article
class={`rounded-xl border p-4 transition ${
activeCard === card.id
? "border-cyan-300/40 bg-cyan-400/[0.08] shadow-lg shadow-cyan-950/30"
: "border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.05]"
}`}
role="button"
tabindex="0"
aria-pressed={activeCard === card.id}
onclick={() => setActiveCard(card.id)}
onkeydown={(event) => handleCardKeydown(event, card.id)}
>
<p class={`text-xs uppercase tracking-[0.16em] ${card.accentClass}`}>
{card.title}
</p>
<p class="mt-2 text-sm text-slate-300">{card.summary}</p>
{#if activeCard === card.id}
<p class="mt-2 text-xs text-slate-300/90">{card.detail}</p>
{/if}
<p class="mt-3 text-xs text-slate-400">
Tools:
{#each card.tools as tool, idx}
<code class="text-slate-200">{tool}</code>{idx <
card.tools.length - 1
? ", "
: ""}
{/each}
</p>
</article>
{/each}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import LoginInfoPanel from './LoginInfoPanel.svelte'; import LoginInfoPanel from './LoginInfoPanel.svelte';
import LoginPanel from './LoginPanel.svelte'; import LoginPanel from './LoginPanel.svelte';
import type { StatusResponse } from '../../types';
const { const {
isOAuthCallback, isOAuthCallback,
@@ -8,6 +9,9 @@
authBusy, authBusy,
authError, authError,
authMessage, authMessage,
statusData,
statusLoading,
statusError,
onlogin onlogin
}: { }: {
isOAuthCallback: boolean; isOAuthCallback: boolean;
@@ -15,20 +19,51 @@
authBusy: boolean; authBusy: boolean;
authError: string; authError: string;
authMessage: string; authMessage: string;
statusData: StatusResponse | null;
statusLoading: boolean;
statusError: string;
onlogin: (username: string, password: string) => void; onlogin: (username: string, password: string) => void;
} = $props(); } = $props();
</script> </script>
<main class="mx-auto flex min-h-screen max-w-6xl items-center px-4 py-10 sm:px-6 lg:px-8"> <main class="mx-auto min-h-screen w-full max-w-6xl px-4 py-8 sm:px-6 lg:px-8">
<section class="grid w-full gap-8 lg:grid-cols-[1.15fr_0.85fr]"> <section class="flex min-h-[calc(100vh-4rem)] flex-col gap-8">
<LoginInfoPanel {isOAuthCallback} /> <LoginInfoPanel {isOAuthCallback} data={statusData} loading={statusLoading} error={statusError} />
<LoginPanel <div class="mt-auto">
{isOAuthCallback} <LoginPanel
{callbackBusy} {isOAuthCallback}
{authBusy} {callbackBusy}
{authError} {authBusy}
{authMessage} {authError}
{onlogin} {authMessage}
/> {onlogin}
/>
</div>
<div class="mt-3 flex flex-wrap items-center justify-center gap-3 text-xs text-slate-400">
<a
href="/llms.txt"
class="inline-flex items-center rounded-md border border-white/10 bg-white/[0.03] px-2.5 py-1.5 transition hover:border-cyan-300/40 hover:text-cyan-100"
>
llms.txt
</a>
<a
href="/robots.txt"
class="inline-flex items-center rounded-md border border-white/10 bg-white/[0.03] px-2.5 py-1.5 transition hover:border-cyan-300/40 hover:text-cyan-100"
>
robots.txt
</a>
<a
href="/.well-known/oauth-authorization-server"
class="inline-flex items-center rounded-md border border-white/10 bg-white/[0.03] px-2.5 py-1.5 transition hover:border-cyan-300/40 hover:text-cyan-100"
>
OAuth Discovery
</a>
<a
href="/llm"
class="inline-flex items-center rounded-md border border-white/10 bg-white/[0.03] px-2.5 py-1.5 transition hover:border-cyan-300/40 hover:text-cyan-100"
>
LLM Docs
</a>
</div>
</section> </section>
</main> </main>

View File

@@ -28,7 +28,7 @@
{#if isOAuthCallback} {#if isOAuthCallback}
<h2 class="text-xl font-semibold text-white">Authorizing operator session</h2> <h2 class="text-xl font-semibold text-white">Authorizing operator session</h2>
<p class="mt-2 text-sm leading-6 text-slate-400"> <p class="mt-2 text-sm leading-6 text-slate-400">
Finishing the ResolveSpec handshake and exchanging the returned code for an AMCS token. Finishing the callback flow and exchanging the returned code for an AMCS token.
</p> </p>
<div class="mt-6 rounded-2xl border border-cyan-400/20 bg-cyan-400/5 px-4 py-6 text-sm text-cyan-100"> <div class="mt-6 rounded-2xl border border-cyan-400/20 bg-cyan-400/5 px-4 py-6 text-sm text-cyan-100">
@@ -42,7 +42,7 @@
</div> </div>
{:else} {:else}
<h2 class="text-xl font-semibold text-white">Operator login</h2> <h2 class="text-xl font-semibold text-white">Operator login</h2>
<p class="mt-1 text-sm text-slate-400">Authenticate with your ResolveSpec credentials.</p> <p class="mt-1 text-sm text-slate-400">Authenticate to access the AMCS admin interface.</p>
<form class="mt-6 space-y-4" onsubmit={handleSubmit}> <form class="mt-6 space-y-4" onsubmit={handleSubmit}>
<div> <div>