chore: import upstream snapshot with attribution
CI / test (20, macos-latest) (push) Has been cancelled
CI / test (20, ubuntu-latest) (push) Has been cancelled
CI / test (22, macos-latest) (push) Has been cancelled
CI / test (22, ubuntu-latest) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:01:18 +08:00
commit 979fb22d7c
613 changed files with 113494 additions and 0 deletions
+228
View File
@@ -0,0 +1,228 @@
.wrap {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 16px;
min-width: 0;
}
.stepLabel {
font-size: 11px;
letter-spacing: 0.96px;
color: var(--gold);
text-transform: uppercase;
font-weight: 700;
}
.helper {
margin: 0;
font-size: 12px;
letter-spacing: 0.12px;
color: var(--steel);
text-transform: uppercase;
line-height: 1.55;
}
.split {
display: grid;
grid-template-columns: minmax(220px, 0.9fr) minmax(0, 1.3fr);
gap: 16px;
align-items: start;
min-width: 0;
}
.chipsCol {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.colHead {
font-size: 11px;
letter-spacing: 0.96px;
color: var(--gold);
text-transform: uppercase;
font-weight: 700;
}
.chips {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 8px;
min-width: 0;
}
.chip {
appearance: none;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 4px;
padding: 14px 14px;
min-height: 62px;
background: var(--iron);
border: 1px solid var(--charcoal);
color: var(--white);
text-decoration: none;
text-align: left;
cursor: pointer;
transition:
background 180ms ease,
border-color 180ms ease,
color 180ms ease;
}
.chip:hover {
background: var(--charcoal);
border-color: var(--gold);
}
.chipOk {
border-color: #29c864 !important;
}
.chipOk .chipSub {
color: #29c864;
}
.chipLabel {
font-size: 13px;
font-weight: 900;
letter-spacing: -0.01em;
text-transform: uppercase;
color: var(--white);
}
.chipSub {
font-size: 10px;
letter-spacing: 0.96px;
color: var(--ash);
text-transform: uppercase;
}
.chipNote {
margin: 4px 0 0;
font-size: 10.5px;
letter-spacing: 0.96px;
color: var(--ash);
text-transform: uppercase;
line-height: 1.55;
}
.snippetCol {
display: block;
min-width: 0;
}
.snippet {
border: 1px solid var(--charcoal);
background: var(--abyss);
overflow: hidden;
}
.snippetHead {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 20px;
background: var(--iron);
border-bottom: 1px solid var(--charcoal);
}
.snippetTitle {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.96px;
color: var(--gold);
text-transform: uppercase;
}
.snippetHint {
font-size: 10px;
letter-spacing: 0.96px;
color: var(--ash);
text-transform: uppercase;
}
.code {
margin: 0;
padding: 20px 24px;
font-family: var(--font-mono);
font-size: 13px;
line-height: 1.6;
color: var(--white);
white-space: pre;
overflow-x: auto;
}
.copyRow {
padding: 12px 16px;
background: var(--iron);
border-top: 1px solid var(--charcoal);
display: flex;
justify-content: flex-end;
}
.copyBtn {
padding: 9px 14px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.96px;
text-transform: uppercase;
color: var(--abyss);
background: var(--gold);
border: none;
cursor: pointer;
transition:
background 200ms ease,
color 200ms ease;
}
.copyBtn:hover {
background: var(--gold-deep);
color: var(--white);
}
.copyBtnSmall {
padding: 6px 10px;
font-size: 10px;
}
.copyBtnOk {
background: #29c864 !important;
color: var(--abyss) !important;
}
.moreToggle {
align-self: start;
padding: 10px 14px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.96px;
color: var(--steel);
background: transparent;
border: 1px solid var(--charcoal);
cursor: pointer;
transition:
color 180ms ease,
border-color 180ms ease;
text-transform: uppercase;
}
.moreToggle:hover {
color: var(--white);
border-color: var(--gold);
}
.moreGrid {
display: grid;
gap: 14px;
}
@media (max-width: 900px) {
.split {
grid-template-columns: minmax(0, 1fr);
}
}
@media (max-width: 640px) {
.chips {
grid-template-columns: minmax(0, 1fr);
}
.code {
font-size: 12px;
padding: 16px;
}
.snippetHead {
padding: 10px 14px;
}
.snippetHint {
font-size: 9px;
letter-spacing: 0.6px;
}
.copyRow {
padding: 10px 12px;
}
}
+333
View File
@@ -0,0 +1,333 @@
"use client";
import { useMemo, useState } from "react";
import styles from "./AgentInstall.module.css";
const UNIVERSAL_JSON = `{
"mcpServers": {
"agentmemory": {
"command": "npx",
"args": ["-y", "@agentmemory/mcp"],
"env": {
"AGENTMEMORY_URL": "http://localhost:3111"
}
}
}
}`;
const CODEX_TOML = `[mcp_servers.agentmemory]
command = "npx"
args = ["-y", "@agentmemory/mcp"]
[mcp_servers.agentmemory.env]
AGENTMEMORY_URL = "http://localhost:3111"`;
const OPENCODE_JSON = `{
"mcp": {
"agentmemory": {
"type": "local",
"command": ["npx", "-y", "@agentmemory/mcp"],
"enabled": true,
"environment": {
"AGENTMEMORY_URL": "http://localhost:3111"
}
}
}
}`;
const VSCODE_MCP_JSON = `{
"servers": {
"agentmemory": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@agentmemory/mcp"],
"env": {
"AGENTMEMORY_URL": "http://localhost:3111"
}
}
}
}`;
const CLAUDE_CODE_CMD = `claude mcp add agentmemory -- npx -y @agentmemory/mcp`;
const COPILOT_CLI_CMD = `agentmemory connect copilot-cli`;
const WARP_CMD = `agentmemory connect warp`;
const HERMES_YAML = `plugins:
- name: agentmemory
path: agentmemory/integrations/hermes
config:
base_url: http://localhost:3111`;
const OPENCLAW_YAML = `plugins:
- id: agentmemory
module: agentmemory/integrations/openclaw/plugin.mjs
config:
enabled: true
base_url: http://localhost:3111`;
function cursorDeeplink(): string {
const cfg = {
command: "npx",
args: ["-y", "@agentmemory/mcp"],
env: { AGENTMEMORY_URL: "http://localhost:3111" },
};
const base64 =
typeof window !== "undefined"
? btoa(JSON.stringify(cfg))
: Buffer.from(JSON.stringify(cfg)).toString("base64");
return `cursor://anysphere.cursor-deeplink/mcp/install?name=agentmemory&config=${encodeURIComponent(base64)}`;
}
function vscodeDeeplink(): string {
const cfg = {
name: "agentmemory",
command: "npx",
args: ["-y", "@agentmemory/mcp"],
env: { AGENTMEMORY_URL: "http://localhost:3111" },
};
const payload =
typeof window !== "undefined"
? encodeURIComponent(JSON.stringify(cfg))
: encodeURIComponent(JSON.stringify(cfg));
return `vscode:mcp/install?${payload}`;
}
type ChipKind = "deeplink" | "copy";
interface Chip {
id: string;
label: string;
kind: ChipKind;
href?: string;
copyText?: string;
sub: string;
}
function CopyButton({
text,
label = "COPY",
small,
}: {
text: string;
label?: string;
small?: boolean;
}) {
const [copied, setCopied] = useState(false);
const onClick = async () => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1600);
} catch {
/* ignore */
}
};
return (
<button
className={`${styles.copyBtn} ${small ? styles.copyBtnSmall : ""} ${
copied ? styles.copyBtnOk : ""
}`}
onClick={onClick}
>
{copied ? "COPIED ✓" : label}
</button>
);
}
function Chip({ chip }: { chip: Chip }) {
const [copied, setCopied] = useState(false);
const inner = (
<>
<span className={styles.chipLabel}>{chip.label}</span>
<span className={styles.chipSub}>
{chip.kind === "deeplink" ? "OPEN" : copied ? "COPIED ✓" : chip.sub}
</span>
</>
);
if (chip.kind === "deeplink" && chip.href) {
return (
<a className={styles.chip} href={chip.href}>
{inner}
</a>
);
}
const onClick = async () => {
if (!chip.copyText) return;
try {
await navigator.clipboard.writeText(chip.copyText);
setCopied(true);
setTimeout(() => setCopied(false), 1600);
} catch {
/* ignore */
}
};
return (
<button
className={`${styles.chip} ${copied ? styles.chipOk : ""}`}
onClick={onClick}
>
{inner}
</button>
);
}
function Snippet({
title,
body,
hint,
}: {
title: string;
body: string;
hint: string;
}) {
return (
<div className={styles.snippet}>
<div className={styles.snippetHead}>
<span className={styles.snippetTitle}>{title}</span>
<span className={styles.snippetHint}>{hint}</span>
</div>
<pre className={styles.code}>
<code>{body}</code>
</pre>
<div className={styles.copyRow}>
<CopyButton text={body} />
</div>
</div>
);
}
export function AgentInstall() {
const cursor = useMemo(cursorDeeplink, []);
const vscode = useMemo(vscodeDeeplink, []);
const [showMore, setShowMore] = useState(false);
const chips: Chip[] = [
{
id: "cursor",
label: "CURSOR",
kind: "deeplink",
href: cursor,
sub: "DEEPLINK",
},
{
id: "vscode",
label: "VS CODE",
kind: "deeplink",
href: vscode,
sub: "DEEPLINK",
},
{
id: "claude-code",
label: "CLAUDE CODE",
kind: "copy",
copyText: CLAUDE_CODE_CMD,
sub: "COPY CMD",
},
{
id: "copilot-cli",
label: "COPILOT CLI",
kind: "copy",
copyText: COPILOT_CLI_CMD,
sub: "COPY CMD",
},
{
id: "codex",
label: "CODEX CLI",
kind: "copy",
copyText: CODEX_TOML,
sub: "COPY TOML",
},
{
id: "warp",
label: "WARP",
kind: "copy",
copyText: WARP_CMD,
sub: "COPY CMD",
},
{
id: "claude-desktop",
label: "CLAUDE DESKTOP",
kind: "copy",
copyText: UNIVERSAL_JSON,
sub: "COPY JSON",
},
{
id: "gemini",
label: "GEMINI CLI",
kind: "copy",
copyText: UNIVERSAL_JSON,
sub: "COPY JSON",
},
];
return (
<div className={styles.wrap}>
<div className={styles.stepLabel}>3. WIRE UP ANY AGENT</div>
<p className={styles.helper}>
ONE MCP JSON FITS ALMOST EVERYTHING. PICK YOUR AGENT ON THE LEFT, OR
PASTE THE UNIVERSAL CONFIG ON THE RIGHT.
</p>
<div className={styles.split}>
<div className={styles.chipsCol}>
<div className={styles.colHead}>AGENTS</div>
<div className={styles.chips}>
{chips.map((c) => (
<Chip key={c.id} chip={c} />
))}
</div>
<p className={styles.chipNote}>
CURSOR / VS CODE ARE ONE-CLICK VIA DEEPLINK. OTHERS COPY THE RIGHT
SNIPPET DIRECTLY TO YOUR CLIPBOARD.
</p>
</div>
<div className={styles.snippetCol}>
<Snippet
title="UNIVERSAL MCP JSON"
hint="WORKS FOR CLAUDE DESKTOP · CURSOR · CLINE · ROO · WINDSURF · GEMINI · WARP · DROID · KIRO · ANTIGRAVITY · QWEN — MERGE INTO EXISTING mcpServers"
body={UNIVERSAL_JSON}
/>
</div>
</div>
<button
className={styles.moreToggle}
aria-expanded={showMore}
onClick={() => setShowMore((v) => !v)}
>
{showMore ? "— HIDE OTHER SHAPES" : "+ OPENCODE · CLINE · CONTINUE · ZED · DROID · QWEN · ANTIGRAVITY · KIRO · HERMES · OPENCLAW · VS CODE"}
</button>
{showMore && (
<div className={styles.moreGrid}>
<Snippet
title="OPENCODE"
hint="opencode.json — different shape (mcp key, command as array)"
body={OPENCODE_JSON}
/>
<Snippet
title="VS CODE (mcp.json)"
hint=".vscode/mcp.json — uses servers key, not mcpServers"
body={VSCODE_MCP_JSON}
/>
<Snippet
title="CODEX CLI (TOML)"
hint="~/.codex/config.toml"
body={CODEX_TOML}
/>
<Snippet
title="HERMES"
hint="integrations/hermes — plugin.yaml"
body={HERMES_YAML}
/>
<Snippet
title="OPENCLAW"
hint="integrations/openclaw — plugin.yaml"
body={OPENCLAW_YAML}
/>
</div>
)}
</div>
);
}
+230
View File
@@ -0,0 +1,230 @@
.wrap {
padding: 112px 0 96px;
}
/* Featured row: 4 first-party cards */
.featuredRow {
max-width: 1440px;
margin: 0 auto 56px;
padding: 0 40px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.featured {
position: relative;
background: var(--iron);
border: 1px solid var(--charcoal);
padding: 24px;
color: var(--white);
text-decoration: none;
display: flex;
flex-direction: column;
gap: 18px;
min-height: 200px;
transition:
background 200ms ease,
border-color 200ms ease,
transform 200ms ease;
overflow: hidden;
}
.featured::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 3px;
background: var(--agent-accent, var(--gold));
transform: scaleX(0.2);
transform-origin: left;
transition: transform 240ms ease;
}
.featured:hover {
background: var(--charcoal);
border-color: var(--agent-accent, var(--gold));
transform: translateY(-2px);
}
.featured:hover::before {
transform: scaleX(1);
}
.featuredHead {
display: flex;
gap: 14px;
align-items: center;
}
.featuredLogo {
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
overflow: hidden;
background: var(--abyss);
border: 1px solid var(--charcoal);
flex-shrink: 0;
}
.featuredLogo img {
width: 100%;
height: 100%;
object-fit: cover;
}
.featuredMeta {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.featuredSub {
font-size: 10px;
letter-spacing: 0.96px;
color: var(--agent-accent, var(--gold));
text-transform: uppercase;
font-weight: 700;
}
.featuredName {
font-size: 20px;
font-weight: 900;
letter-spacing: -0.01em;
color: var(--white);
}
.featuredFrom {
font-size: 10.5px;
letter-spacing: 0.96px;
color: var(--ash);
text-transform: uppercase;
}
.featuredPitch {
margin: 0;
font-size: 12.5px;
letter-spacing: 0.12px;
color: var(--steel);
text-transform: uppercase;
line-height: 1.55;
}
.featuredArrow {
position: absolute;
top: 16px;
right: 20px;
font-size: 18px;
color: var(--ash);
transition: color 180ms ease, transform 180ms ease;
}
.featured:hover .featuredArrow {
color: var(--agent-accent, var(--gold));
transform: translate(2px, -2px);
}
/* Marquee */
.marqueeWrap {
position: relative;
overflow: hidden;
padding: 4px 0;
max-width: 100vw;
margin: 0 auto;
}
.fadeLeft,
.fadeRight {
position: absolute;
top: 0;
bottom: 0;
width: 120px;
pointer-events: none;
z-index: 2;
}
.fadeLeft {
left: 0;
background: linear-gradient(90deg, var(--abyss) 0%, rgba(0, 0, 0, 0) 100%);
}
.fadeRight {
right: 0;
background: linear-gradient(270deg, var(--abyss) 0%, rgba(0, 0, 0, 0) 100%);
}
.marquee {
display: flex;
gap: 16px;
width: max-content;
animation: scroll 48s linear infinite;
padding: 0 20px;
}
.marqueeWrap:hover .marquee {
animation-play-state: paused;
}
@keyframes scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
.tile {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 14px 18px;
background: var(--iron);
border: 1px solid var(--charcoal);
color: var(--white);
text-decoration: none;
flex-shrink: 0;
transition:
border-color 180ms ease,
background 180ms ease;
}
.tile:hover {
border-color: var(--agent-accent, var(--gold));
background: var(--charcoal);
}
.tileLogo {
width: 40px;
height: 40px;
border-radius: 8px;
overflow: hidden;
background: var(--abyss);
flex-shrink: 0;
object-fit: cover;
}
.tileMeta {
display: flex;
flex-direction: column;
gap: 2px;
}
.tileName {
font-size: 13.5px;
font-weight: 900;
letter-spacing: -0.01em;
color: var(--white);
text-transform: uppercase;
}
.tileFrom {
font-size: 10px;
letter-spacing: 0.96px;
color: var(--ash);
text-transform: uppercase;
}
@media (max-width: 1024px) {
.featuredRow {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.featuredRow {
padding: 0 20px;
grid-template-columns: 1fr;
}
.fadeLeft,
.fadeRight {
width: 60px;
}
}
@media (prefers-reduced-motion: reduce) {
.marquee {
animation: none;
}
}
+319
View File
@@ -0,0 +1,319 @@
import Image from "next/image";
import styles from "./Agents.module.css";
interface Agent {
id: string;
name: string;
from: string;
logo: string;
accent: string;
href: string;
featured?: boolean;
pitch?: string;
sub?: string;
}
const FEATURED: Agent[] = [
{
id: "claude-code",
name: "Claude Code",
from: "Anthropic",
logo: "https://github.com/anthropics.png",
accent: "#CC785C",
href: "https://claude.com/product/claude-code",
pitch: "12 hooks + MCP + skills",
sub: "FIRST-CLASS PLUGIN",
},
{
id: "copilot-cli",
name: "Copilot CLI",
from: "GitHub",
logo: "https://svgl.app/library/github_dark.svg",
accent: "#24292E",
href: "https://docs.github.com/copilot/github-copilot-in-the-cli",
pitch: "11 hooks + MCP · framed stdio",
sub: "NATIVE PLUGIN",
},
{
id: "codex",
name: "Codex CLI",
from: "OpenAI",
logo: "https://github.com/openai.png",
accent: "#10A37F",
href: "https://github.com/openai/codex",
pitch: "6 hooks + MCP · native plugin",
sub: "NATIVE PLUGIN",
},
{
id: "openclaw",
name: "OpenClaw",
from: "openclaw",
logo: "https://github.com/openclaw.png",
accent: "#FFA000",
href: "https://github.com/openclaw/openclaw",
pitch: "onSessionStart · onPreLlmCall · onPostToolUse · onSessionEnd",
sub: "GATEWAY PLUGIN",
},
{
id: "hermes",
name: "Hermes",
from: "Nous Research",
logo: "https://github.com/NousResearch.png",
accent: "#7A5BFF",
href: "https://github.com/NousResearch",
pitch: "Python plugin · yaml config",
sub: "FIRST-PARTY INTEGRATION",
},
{
id: "pi",
name: "pi",
from: "pi",
logo: "https://raw.githubusercontent.com/rohitg00/agentmemory/main/assets/agents/pi.svg",
accent: "#FF6B35",
href: "https://github.com/rohitg00/agentmemory/tree/main/integrations/pi",
pitch: "Native plugin + MCP",
sub: "NATIVE PLUGIN",
},
{
id: "openhuman",
name: "OpenHuman",
from: "tinyhumansai",
logo: "https://raw.githubusercontent.com/tinyhumansai/openhuman/main/app/src-tauri/icons/128x128.png",
accent: "#9b5cf6",
href: "https://github.com/tinyhumansai/openhuman",
pitch: "Native Memory trait backend (Rust)",
sub: "NATIVE BACKEND",
},
];
const MARQUEE: Agent[] = [
{
id: "claude-desktop",
name: "Claude Desktop",
from: "Anthropic",
logo: "https://github.com/anthropics.png",
accent: "#CC785C",
href: "https://claude.ai/download",
},
{
id: "cursor",
name: "Cursor",
from: "Anysphere",
logo: "https://svgl.app/library/cursor_dark.svg",
accent: "#000000",
href: "https://cursor.com",
},
{
id: "warp",
name: "Warp",
from: "Warp",
logo: "https://svgl.app/library/warp.svg",
accent: "#9B59FF",
href: "https://www.warp.dev",
},
{
id: "continue",
name: "Continue",
from: "Continue.dev",
logo: "https://continue.dev/icon.svg",
accent: "#000000",
href: "https://continue.dev",
},
{
id: "cline",
name: "Cline",
from: "cline",
logo: "https://github.com/cline.png",
accent: "#F59E0B",
href: "https://github.com/cline/cline",
},
{
id: "zed",
name: "Zed",
from: "Zed Industries",
logo: "https://svgl.app/library/zed-logo_dark.svg",
accent: "#FF4500",
href: "https://zed.dev",
},
{
id: "droid",
name: "Droid",
from: "Factory.ai",
logo: "https://www.factory.ai/favicon.svg",
accent: "#FF6B35",
href: "https://docs.factory.ai/cli",
},
{
id: "gemini-cli",
name: "Gemini CLI",
from: "Google",
logo: "https://github.com/google-gemini.png",
accent: "#4285F4",
href: "https://github.com/google-gemini/gemini-cli",
},
{
id: "antigravity",
name: "Antigravity",
from: "Google",
logo: "https://svgl.app/library/antigravity.svg",
accent: "#4285F4",
href: "https://antigravity.google",
},
{
id: "qwen",
name: "Qwen Code",
from: "Alibaba",
logo: "https://svgl.app/library/qwen_dark.svg",
accent: "#615CED",
href: "https://github.com/QwenLM/qwen-code",
},
{
id: "kiro",
name: "Kiro",
from: "AWS",
logo: "https://kiro.dev/favicon.ico",
accent: "#FF9900",
href: "https://kiro.dev",
},
{
id: "opencode",
name: "OpenCode",
from: "opencode-ai",
logo: "https://github.com/opencode-ai.png",
accent: "#22C55E",
href: "https://github.com/opencode-ai/opencode",
},
{
id: "roo",
name: "Roo Code",
from: "RooCode",
logo: "https://github.com/RooCodeInc.png",
accent: "#EC4899",
href: "https://github.com/RooCodeInc/Roo-Code",
},
{
id: "kilo",
name: "Kilo Code",
from: "Kilo-Org",
logo: "https://github.com/Kilo-Org.png",
accent: "#06B6D4",
href: "https://github.com/Kilo-Org/kilocode",
},
{
id: "goose",
name: "Goose",
from: "Block",
logo: "https://github.com/block.png",
accent: "#00D54B",
href: "https://github.com/block/goose",
},
{
id: "aider",
name: "Aider",
from: "Aider-AI",
logo: "https://github.com/Aider-AI.png",
accent: "#E11D48",
href: "https://github.com/Aider-AI/aider",
},
{
id: "windsurf",
name: "Windsurf",
from: "Cognition",
logo: "https://svgl.app/library/windsurf-dark.svg",
accent: "#00A699",
href: "https://windsurf.com",
},
];
function FeaturedCard({ a }: { a: Agent }) {
return (
<a
className={styles.featured}
href={a.href}
target="_blank"
rel="noopener"
style={{ ["--agent-accent" as string]: a.accent }}
>
<div className={styles.featuredHead}>
<div className={styles.featuredLogo}>
<Image
src={a.logo}
width={56}
height={56}
alt={`${a.name} logo`}
unoptimized
/>
</div>
<div className={styles.featuredMeta}>
<span className={styles.featuredSub}>{a.sub}</span>
<span className={styles.featuredName}>{a.name}</span>
<span className={styles.featuredFrom}>FROM {a.from}</span>
</div>
</div>
<p className={styles.featuredPitch}>{a.pitch}</p>
<span className={styles.featuredArrow} aria-hidden>
</span>
</a>
);
}
function MarqueeTile({ a }: { a: Agent }) {
return (
<a
className={styles.tile}
href={a.href}
target="_blank"
rel="noopener"
style={{ ["--agent-accent" as string]: a.accent }}
>
<Image
src={a.logo}
width={48}
height={48}
alt={`${a.name} logo`}
unoptimized
className={styles.tileLogo}
/>
<div className={styles.tileMeta}>
<span className={styles.tileName}>{a.name}</span>
<span className={styles.tileFrom}>FROM {a.from}</span>
</div>
</a>
);
}
export function Agents() {
const loop = [...MARQUEE, ...MARQUEE];
return (
<section className={styles.wrap} id="agents" aria-labelledby="agents-title">
<header className="section-head">
<span className="section-eyebrow">WORKS WITH</span>
<h2 id="agents-title" className="section-title">
SEVEN NATIVE PLUGINS.<br />REST MCP-NATIVE.
</h2>
<p className="section-lede">
NATIVE PLUGINS FOR CLAUDE CODE, COPILOT CLI, CODEX CLI, OPENCLAW,
HERMES, PI, AND OPENHUMAN. EVERY OTHER MCP CLIENT GETS IT FOR FREE.
`agentmemory connect &lt;agent&gt;` AUTO-WIRES THEM ALL.
</p>
</header>
<div className={styles.featuredRow}>
{FEATURED.map((a) => (
<FeaturedCard key={a.id} a={a} />
))}
</div>
<div className={styles.marqueeWrap} aria-label="Other compatible agents">
<div className={styles.fadeLeft} aria-hidden />
<div className={styles.fadeRight} aria-hidden />
<div className={styles.marquee}>
{loop.map((a, i) => (
<MarqueeTile key={`${a.id}-${i}`} a={a} />
))}
</div>
</div>
</section>
);
}
+195
View File
@@ -0,0 +1,195 @@
.wrap {
padding: 96px 0;
background: var(--iron);
border-top: 1px solid var(--charcoal);
border-bottom: 1px solid var(--charcoal);
}
.tabs {
max-width: 1200px;
margin: 0 auto 32px;
padding: 0 40px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0;
border: 1px solid var(--charcoal);
background: var(--abyss);
}
.tab {
padding: 18px 18px;
text-align: left;
display: flex;
flex-direction: column;
gap: 6px;
border-right: 1px solid var(--charcoal);
transition:
background 200ms ease,
color 200ms ease;
cursor: pointer;
background: transparent;
}
.tab:last-child {
border-right: none;
}
.tab:hover {
background: var(--charcoal);
}
.tabActive {
background: var(--gold) !important;
color: var(--abyss) !important;
}
.tabActive .tabSub {
color: rgba(0, 0, 0, 0.7) !important;
}
.tabLabel {
font-size: 16px;
font-weight: 900;
letter-spacing: -0.01em;
text-transform: uppercase;
}
.tabSub {
font-size: 10px;
letter-spacing: 0.96px;
color: var(--ash);
text-transform: uppercase;
}
.panel {
max-width: 1200px;
margin: 0 auto;
padding: 0 40px;
display: grid;
grid-template-columns: minmax(320px, 1fr) minmax(0, 1.3fr);
gap: 40px;
align-items: start;
}
.panelText {
padding-top: 8px;
}
.panelTitle {
font-size: clamp(24px, 2.4vw, 32px);
font-weight: 900;
letter-spacing: -0.01em;
text-transform: uppercase;
margin: 0 0 16px;
color: var(--white);
}
.panelBlurb {
font-size: 14px;
letter-spacing: 0.12px;
color: var(--mist);
text-transform: uppercase;
line-height: 1.6;
margin: 0 0 24px;
}
.panelBullets {
list-style: none;
padding: 0;
margin: 0 0 28px;
display: grid;
gap: 10px;
}
.panelBullets li {
display: flex;
gap: 12px;
align-items: flex-start;
font-size: 12px;
letter-spacing: 0.12px;
color: var(--steel);
text-transform: uppercase;
line-height: 1.55;
}
.panelBullets span {
color: var(--gold);
font-weight: 700;
margin-top: 1px;
}
.launch {
margin: 0;
padding: 16px 18px;
background: var(--abyss);
border: 1px solid var(--charcoal);
font-family: var(--font-mono);
font-size: 13px;
color: var(--white);
overflow-x: auto;
white-space: nowrap;
}
.launchPrompt {
color: var(--gold);
font-weight: 700;
margin-right: 8px;
}
.panelFrame {
background: var(--abyss);
border: 1px solid var(--charcoal);
overflow: hidden;
}
.frameChrome {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
background: var(--iron);
border-bottom: 1px solid var(--charcoal);
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
}
.red {
background: #ff5f57;
}
.yellow {
background: #febc2e;
}
.green {
background: #28c840;
}
.frameTitle {
margin-left: 8px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--ash);
letter-spacing: 0.12px;
text-transform: uppercase;
}
.frameShot {
background: var(--abyss);
min-height: 300px;
}
@media (max-width: 960px) {
.panel {
grid-template-columns: minmax(0, 1fr);
}
.tabs {
grid-template-columns: 1fr 1fr;
}
.tab {
border-bottom: 1px solid var(--charcoal);
}
.tab:nth-child(2n) {
border-right: none;
}
}
@media (max-width: 640px) {
.tabs,
.panel {
padding: 0 20px;
}
.tab {
padding: 14px 12px;
}
.tabLabel {
font-size: 13px;
}
.tabSub {
font-size: 9px;
letter-spacing: 0.6px;
}
.launch {
padding: 12px 14px;
font-size: 11px;
}
}
+162
View File
@@ -0,0 +1,162 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import styles from "./CommandCenter.module.css";
type Tab = "viewer" | "console" | "state" | "traces";
const TABS: Array<{ id: Tab; label: string; sub: string }> = [
{ id: "viewer", label: "VIEWER", sub: ":3113 · LIVE OBSERVATION STREAM" },
{ id: "console", label: "iii CONSOLE", sub: ":3114 · ENGINE DASHBOARD" },
{ id: "state", label: "STATE", sub: "RAW KV BROWSER + JSON EDITOR" },
{ id: "traces", label: "TRACES", sub: "OTEL WATERFALL + FLAME" },
];
const PANELS: Record<
Tab,
{
title: string;
blurb: string;
bullets: string[];
img: string;
alt: string;
launch: string;
}
> = {
viewer: {
title: "SHIP-WITH VIEWER · PORT 3113",
blurb:
"The agentmemory server auto-starts a real-time viewer on port 3113. No install, no config. Everything the server sees, the viewer shows.",
bullets: [
"LIVE OBSERVATION STREAM · EVERY HOOK AS IT FIRES",
"SESSION EXPLORER · REPLAY ANY PAST SESSION",
"MEMORY BROWSER · FILTER BY PROJECT / TYPE / CONFIDENCE",
"KNOWLEDGE GRAPH VISUALIZATION · FORCE-DIRECTED",
"HEALTH DASHBOARD · HEAP / RSS / EVENT LOOP LAG",
],
img: "/demo.gif",
alt: "agentmemory viewer live demo",
launch: "open http://localhost:3113",
},
console: {
title: "iii CONSOLE · FIRST-CLASS",
blurb:
"agentmemory runs on the iii engine, so the official iii console gives engine-level visibility: every function call, every worker, every queue, every trace. From v0.9.16 the agentmemory CLI prompts to install iii console alongside the engine. Launch on :3114 so the viewer keeps :3113.",
bullets: [
"REGISTERED FUNCTIONS · INVOKE ANY DIRECTLY WITH JSON",
"121 HTTP ENDPOINTS · REPLAY ANY REST CALL",
"WEBSOCKET STREAM MONITOR · WATCH FRAMES LIVE",
"OTEL EXPORTER = MEMORY (DEFAULT) · TRACES STAY LOCAL",
"NO AUTH · BIND TO 127.0.0.1 ONLY",
],
img: "/dashboard.png",
alt: "iii console dashboard",
launch: "iii-console --port 3114 --engine-port 3111",
},
state: {
title: "RAW KV BROWSER",
blurb:
"Three-panel view of the key/value store behind every memory, session, retention score, audit row, and access log. Edit JSON in place.",
bullets: [
"SCOPED NAMESPACES · mem:memories / mem:sessions / mem:retention",
"JSON-NATIVE EDIT · NO MIGRATIONS",
"AUDIT ROW PER CHANGE · FORENSIC TRAIL",
"BACKED BY iii STATE ADAPTERS · IN-MEMORY OR FILE-BASED",
],
img: "/states.png",
alt: "iii console state browser",
launch: "open http://localhost:3114/states",
},
traces: {
title: "OPENTELEMETRY OUT OF THE BOX",
blurb:
"iii-observability ships with exporter: memory, sampling_ratio: 1.0. Every memory operation emits a trace span + structured log. Swap to OTLP for Jaeger / Honeycomb / Tempo.",
bullets: [
"WATERFALL · FLAME · SERVICE BREAKDOWN · TRACE MAP",
"FILTER BY TRACE ID · SERVICE · DURATION",
"MEMORY SEARCH SPAN TREE · BM25 → VECTOR → GRAPH → RERANK",
"SWAP EXPORTER TO OTLP FOR PROD TELEMETRY",
],
img: "/traces-waterfall.png",
alt: "iii console traces waterfall",
launch: "open http://localhost:3114/traces",
},
};
export function CommandCenter() {
const [tab, setTab] = useState<Tab>("viewer");
const panel = PANELS[tab];
return (
<section
className={styles.wrap}
id="command-center"
aria-labelledby="cc-title"
>
<header className="section-head">
<span className="section-eyebrow">COMMAND CENTER</span>
<h2 id="cc-title" className="section-title">
TWO UIs.<br />ONE MEMORY RUNTIME.
</h2>
<p className="section-lede">
AGENTMEMORY SHIPS A REAL-TIME VIEWER FOR YOUR MEMORIES AND AN
ENGINE-LEVEL CONSOLE FOR EVERY FUNCTION, TRIGGER, AND OTEL SPAN.
BOTH ARE FIRST-CLASS INSTALLED INLINE BY THE CLI ON FIRST RUN.
</p>
</header>
<div className={styles.tabs} role="tablist" aria-label="Command center">
{TABS.map((t) => (
<button
key={t.id}
role="tab"
aria-selected={tab === t.id}
className={`${styles.tab} ${tab === t.id ? styles.tabActive : ""}`}
onClick={() => setTab(t.id)}
>
<span className={styles.tabLabel}>{t.label}</span>
<span className={styles.tabSub}>{t.sub}</span>
</button>
))}
</div>
<div className={styles.panel}>
<div className={styles.panelText}>
<h3 className={styles.panelTitle}>{panel.title}</h3>
<p className={styles.panelBlurb}>{panel.blurb}</p>
<ul className={styles.panelBullets}>
{panel.bullets.map((b) => (
<li key={b}>
<span aria-hidden></span>
{b}
</li>
))}
</ul>
<pre className={styles.launch}>
<code>
<span className={styles.launchPrompt}>$</span> {panel.launch}
</code>
</pre>
</div>
<div className={styles.panelFrame}>
<div className={styles.frameChrome}>
<span className={`${styles.dot} ${styles.red}`} />
<span className={`${styles.dot} ${styles.yellow}`} />
<span className={`${styles.dot} ${styles.green}`} />
<span className={styles.frameTitle}>{panel.title}</span>
</div>
<div className={styles.frameShot}>
<Image
src={panel.img}
alt={panel.alt}
width={1600}
height={1000}
unoptimized={panel.img.endsWith(".gif")}
priority={false}
style={{ width: "100%", height: "auto", display: "block" }}
/>
</div>
</div>
</div>
</section>
);
}
+63
View File
@@ -0,0 +1,63 @@
.compare {
padding: 96px 0;
background: var(--iron);
border-top: 1px solid var(--charcoal);
border-bottom: 1px solid var(--charcoal);
}
.table {
max-width: 1100px;
margin: 0 auto;
padding: 0 40px;
}
.row {
display: grid;
grid-template-columns: 1.4fr 1.3fr 1fr 1fr 1fr;
gap: 16px;
padding: 20px 24px;
border-bottom: 1px solid var(--charcoal);
align-items: center;
font-size: 14px;
letter-spacing: 0.12px;
word-break: break-word;
}
.row > span {
color: var(--steel);
text-transform: uppercase;
}
.row > span:first-child {
color: var(--white);
font-weight: 700;
}
.head {
border-bottom-color: var(--gold);
}
.head span {
font-size: 11px;
letter-spacing: 0.96px;
color: var(--ash) !important;
font-weight: 400 !important;
}
.head .mine {
color: var(--gold) !important;
}
.mine {
color: var(--gold) !important;
font-weight: 700 !important;
}
@media (max-width: 1024px) {
.row {
grid-template-columns: 1.4fr 1fr;
gap: 8px 16px;
}
.row span:nth-child(n + 3) {
display: none;
}
.head span:nth-child(n + 3) {
display: none;
}
}
@media (max-width: 640px) {
.table {
padding: 0 20px;
}
}
+48
View File
@@ -0,0 +1,48 @@
import styles from "./Compare.module.css";
const ROWS = [
["RETRIEVAL R@5", "95.2%", "81.4%", "73.8%", "78.1%"],
["EXTERNAL DEPS", "0", "2 (Qdrant, Neo4j)", "1 (Postgres)", "1 (Neo4j)"],
["REST ENDPOINTS", "121", "—", "—", "—"],
["MCP TOOLS", "51", "12", "18", "9"],
["AUTO-HOOKS", "12", "0", "0", "0"],
["NATIVE PLUGINS", "6", "—", "—", "—"],
["OPEN SOURCE", "YES (APACHE-2.0)", "YES", "YES", "YES"],
];
export function Compare() {
return (
<section className={styles.compare} id="compare" aria-labelledby="cmp-title">
<header className="section-head">
<span className="section-eyebrow">VS.</span>
<h2 id="cmp-title" className="section-title">
VS. THE FIELD.
</h2>
<p className="section-lede">
NUMBERS STRAIGHT FROM THE LONGMEMEVAL-S BENCHMARK AND EACH PROJECT&apos;S
OWN DOCS. SHIP WHAT YOU WANT WE JUST PICKED THE ONE WITH RECEIPTS.
</p>
</header>
<div className={styles.table} role="table" aria-label="Comparison">
<div className={`${styles.row} ${styles.head}`} role="row">
<span role="columnheader" />
<span role="columnheader" className={styles.mine}>
AGENTMEMORY
</span>
<span role="columnheader">MEM0</span>
<span role="columnheader">LETTA</span>
<span role="columnheader">COGNEE</span>
</div>
{ROWS.map((r) => (
<div key={r[0]} className={styles.row} role="row">
<span role="rowheader">{r[0]}</span>
<span className={styles.mine}>{r[1]}</span>
<span>{r[2]}</span>
<span>{r[3]}</span>
<span>{r[4]}</span>
</div>
))}
</div>
</section>
);
}
+152
View File
@@ -0,0 +1,152 @@
.wrap {
padding: 48px 0 56px;
background: var(--ink);
border-top: 1px solid var(--charcoal);
border-bottom: 1px solid var(--charcoal);
}
.wrapCompact {
padding: 36px 0 0;
background: transparent;
border: 0;
margin-top: 40px;
}
.wrapCompact .eyebrow {
margin-bottom: 4px;
}
.inner {
max-width: 1100px;
margin: 0 auto;
padding: 0 40px;
display: flex;
flex-direction: column;
gap: 22px;
align-items: center;
}
.eyebrow {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 2.4px;
color: var(--ash);
text-transform: uppercase;
display: flex;
align-items: center;
gap: 12px;
width: 100%;
justify-content: center;
}
.eyebrow::before,
.eyebrow::after {
content: "";
flex: 0 0 80px;
height: 1px;
background: var(--charcoal);
}
.row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
width: 100%;
max-width: 1100px;
align-items: stretch;
}
.cell {
position: relative;
display: grid;
grid-template-columns: 56px 1fr auto;
gap: 14px;
align-items: center;
padding: 18px 22px;
border: 1px solid var(--charcoal);
background: var(--iron);
text-decoration: none;
color: var(--white);
transition: border-color 0.12s ease, transform 0.12s ease, background 0.12s ease;
min-height: 96px;
}
.cell:hover,
.cell:focus-visible {
border-color: var(--gold);
transform: translateY(-1px);
background: #16161a;
}
.cell:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.cellBadge {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
padding: 16px 16px;
}
.logo {
width: 56px;
height: 56px;
border-radius: 8px;
object-fit: cover;
background: var(--ink);
}
.badgeImg {
width: 100%;
height: auto;
max-width: 260px;
max-height: 60px;
object-fit: contain;
display: block;
}
.invertLogo {
filter: invert(1) brightness(1.05);
max-height: 64px;
max-width: 280px;
}
.meta {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.name {
font-family: var(--font-mono);
font-size: 14px;
letter-spacing: 0.6px;
color: var(--white);
font-weight: 700;
text-transform: uppercase;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sub {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.8px;
color: var(--steel);
text-transform: uppercase;
}
.arrow {
font-family: var(--font-mono);
color: var(--ash);
font-size: 14px;
transition: color 0.12s ease, transform 0.12s ease;
}
.cell:hover .arrow {
color: var(--gold);
transform: translate(2px, -2px);
}
@media (max-width: 980px) {
.row {
grid-template-columns: repeat(2, 1fr);
max-width: 720px;
}
}
@media (max-width: 540px) {
.row {
grid-template-columns: 1fr;
max-width: 380px;
}
.wrap {
padding: 32px 0 40px;
}
}
+123
View File
@@ -0,0 +1,123 @@
import Image from "next/image";
import styles from "./FeaturedIn.module.css";
interface Feature {
name: string;
sub: string;
href: string;
logo: string;
logoAlt: string;
// When the source has its own brand-mark image (e.g. Trendshift's
// badge endpoint that bakes the repo's star count into the image),
// render it full-width instead of the logo-left text-right layout.
badge?: boolean;
}
const ITEMS: Feature[] = [
{
name: "AlphaSignal",
sub: "180K technical subscribers",
href: "https://alphasignalai.substack.com/p/how-agentmemory-works-and-how-to",
logo: "https://avatars.githubusercontent.com/u/64016073?s=200&v=4",
logoAlt: "AlphaSignal logo",
},
{
name: "Agentic AI Foundation",
sub: "Linux Foundation backed",
href: "https://aaif.io/",
logo: "/featured/aaif-logo.png",
logoAlt: "Agentic AI Foundation logo",
badge: true,
},
{
name: "Trendshift",
sub: "Position #19 · NEW 2026",
href: "https://trendshift.io/repositories/25123",
logo: "https://trendshift.io/api/badge/repositories/25123",
logoAlt: "Trendshift badge for agentmemory",
badge: true,
},
{
name: "Product Hunt",
sub: "Live upvote count",
href: "https://www.producthunt.com/products/agent-memory-dev?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-agentmemory",
logo: "https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1144164&theme=neutral",
logoAlt: "Featured on Product Hunt — live upvote count",
badge: true,
},
];
interface FeaturedInProps {
// When true, render the bar without the outer `<section>` chrome
// (no border, no own padding) so it can be inlined inside the
// hero below the CTA stack.
compact?: boolean;
}
export function FeaturedIn({ compact = false }: FeaturedInProps = {}) {
const wrapClass = compact ? `${styles.wrap} ${styles.wrapCompact}` : styles.wrap;
return (
<section
className={wrapClass}
aria-labelledby="featured-in-title"
>
<div className={styles.inner}>
<div id="featured-in-title" className={styles.eyebrow}>
AS FEATURED IN
</div>
<div className={styles.row}>
{ITEMS.map((it) =>
it.badge ? (
<a
key={it.name}
className={`${styles.cell} ${styles.cellBadge}`}
href={it.href}
target="_blank"
rel="noopener"
aria-label={`${it.name}${it.sub}`}
>
<Image
src={it.logo}
alt={it.logoAlt}
width={250}
height={55}
unoptimized
className={
it.logo.startsWith("/featured/aaif")
? `${styles.badgeImg} ${styles.invertLogo}`
: styles.badgeImg
}
/>
<span className={styles.sub}>{it.sub}</span>
</a>
) : (
<a
key={it.name}
className={styles.cell}
href={it.href}
target="_blank"
rel="noopener"
>
<Image
src={it.logo}
alt={it.logoAlt}
width={56}
height={56}
unoptimized
className={styles.logo}
/>
<div className={styles.meta}>
<span className={styles.name}>{it.name}</span>
<span className={styles.sub}>{it.sub}</span>
</div>
<span className={styles.arrow} aria-hidden>
</span>
</a>
),
)}
</div>
</div>
</section>
);
}
+67
View File
@@ -0,0 +1,67 @@
.wrap {
padding: 96px 0;
}
.grid {
max-width: 1440px;
margin: 0 auto;
padding: 0 40px;
list-style: none;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1px;
background: var(--charcoal);
border: 1px solid var(--charcoal);
}
.tile {
background: var(--abyss);
padding: 32px 28px;
transition:
background 200ms ease,
transform 200ms ease;
min-height: 260px;
}
.tile:hover {
background: var(--iron);
transform: translateY(-2px);
}
.kPill {
display: inline-flex;
align-items: baseline;
gap: 8px;
padding: 6px 10px;
border: 1px solid var(--gold);
margin-bottom: 20px;
}
.k {
font-size: 16px;
font-weight: 900;
color: var(--gold);
letter-spacing: -0.01em;
}
.unit {
font-size: 10px;
letter-spacing: 0.96px;
color: var(--gold-soft);
text-transform: uppercase;
}
.tileTitle {
margin: 0 0 12px;
font-size: 20px;
font-weight: 900;
letter-spacing: -0.01em;
text-transform: uppercase;
color: var(--white);
}
.tileText {
margin: 0;
font-size: 13px;
color: var(--steel);
letter-spacing: 0.12px;
line-height: 1.6;
text-transform: none;
}
@media (max-width: 640px) {
.grid {
padding: 0 20px;
}
}
+123
View File
@@ -0,0 +1,123 @@
import styles from "./Features.module.css";
interface Props {
hooks: number;
mcpTools: number;
restEndpoints: number;
}
export function Features({ hooks, mcpTools, restEndpoints }: Props) {
const FEATURES = [
{
k: `${hooks}`,
unit: "AUTO-HOOKS",
title: "CAPTURE EVERYTHING",
text:
"Every PreToolUse, PostToolUse, SessionStart, Stop, and the rest fire into the memory pipeline without a line of glue code. Install the plugin, done.",
},
{
k: `${mcpTools}`,
unit: "MCP TOOLS",
title: "NATIVE MCP SURFACE",
text:
"memory_save, memory_recall, memory_smart_search, memory_sessions, governance, audit, export — full surface behind a single MCP server.",
},
{
k: `${restEndpoints}`,
unit: "REST ENDPOINTS",
title: "HTTP FIRST",
text:
"Every MCP tool has a REST twin under /agentmemory/*. Curl it. Fetch it from the browser. Proxy it from your own agent.",
},
{
k: "BM25",
unit: "+ VECTOR + GRAPH",
title: "TRIPLE-STREAM RECALL",
text:
"Hybrid retrieval pipes lexical, semantic, and relational scores through an on-device reranker. 95.2% R@5 on LongMemEval-S.",
},
{
k: "AUTO",
unit: "CONSOLIDATION",
title: "RAW → SEMANTIC",
text:
"Hourly sweep compresses observations into semantic memories, merges duplicates, decays stale rows with retention scoring, and emits a batched audit row.",
},
{
k: "∞",
unit: "REPLAY",
title: "JSONL SESSION IMPORT",
text:
"Point agentmemory at a Claude Code JSONL transcript and it rehydrates the full session — observations, tool uses, timeline — into the store.",
},
{
k: "GRAPH",
unit: "EXTRACTION",
title: "KNOWLEDGE GRAPH",
text:
"Entities and relations extracted on compress. Query with /agentmemory/graph. Visualize in the viewer. Temporal edges supported.",
},
{
k: "MESH",
unit: "FEDERATION",
title: "PEER-TO-PEER SYNC",
text:
"Register another agentmemory node, push / pull memories over authenticated HTTPS. Bearer-token required; no silent syncs.",
},
{
k: "MD",
unit: "OBSIDIAN EXPORT",
title: "YOUR NOTES, HYDRATED",
text:
"Mirror memories to a sandboxed vault directory. Frontmatter-tagged markdown, ready for Obsidian's graph view.",
},
{
k: "5",
unit: "LLM PROVIDERS",
title: "BYO MODEL",
text:
"Claude subscription (default, zero config), Anthropic API, Gemini, MiniMax, OpenRouter. Detected from env.",
},
{
k: "OTEL",
unit: "OBSERVABILITY",
title: "TRACES + LOGS",
text:
"iii-observability worker on by default. Exporter: memory for local, OTLP for Jaeger / Honeycomb / Tempo. Every operation produces a span.",
},
{
k: "0",
unit: "EXTERNAL DBs",
title: "ONE PROCESS",
text:
"Runs as a single Node process. No Redis, Kafka, Postgres, Qdrant, Neo4j. State lives on disk as JSON. That's the whole stack.",
},
];
return (
<section className={styles.wrap} id="features" aria-labelledby="feat-title">
<header className="section-head">
<span className="section-eyebrow">WHAT'S INSIDE</span>
<h2 id="feat-title" className="section-title">
TWELVE THINGS YOU DID NOT WANT TO BUILD.
</h2>
<p className="section-lede">
AGENTMEMORY IS NOT A LIBRARY OR A VECTOR STORE. IT'S A COMPLETE MEMORY
RUNTIME CAPTURE, RECALL, CONSOLIDATE, OBSERVE, FEDERATE.
</p>
</header>
<ul className={styles.grid}>
{FEATURES.map((f) => (
<li key={f.title} className={styles.tile}>
<div className={styles.kPill}>
<span className={styles.k}>{f.k}</span>
<span className={styles.unit}>{f.unit}</span>
</div>
<h3 className={styles.tileTitle}>{f.title}</h3>
<p className={styles.tileText}>{f.text}</p>
</li>
))}
</ul>
</section>
);
}
+51
View File
@@ -0,0 +1,51 @@
.foot {
padding: 48px 40px 32px;
background: var(--abyss);
border-top: 1px solid var(--charcoal);
}
.row {
max-width: 1440px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
flex-wrap: wrap;
}
.mark {
font-size: 20px;
font-weight: 900;
letter-spacing: -0.01em;
text-transform: uppercase;
}
.links {
display: flex;
gap: 28px;
flex-wrap: wrap;
}
.links a {
font-size: 13px;
letter-spacing: 0.12px;
text-transform: uppercase;
color: var(--steel);
}
.links a:hover {
color: var(--gold);
}
.fine {
max-width: 1440px;
margin: 32px auto 0;
font-size: 11px;
letter-spacing: 0.96px;
color: var(--ash);
text-transform: uppercase;
}
@media (max-width: 640px) {
.foot {
padding: 32px 20px 24px;
}
.row {
flex-direction: column;
align-items: flex-start;
}
}
+40
View File
@@ -0,0 +1,40 @@
import styles from "./Footer.module.css";
export function Footer() {
return (
<footer className={styles.foot}>
<div className={styles.row}>
<a href="#top" className={styles.mark}>
AGENTMEMORY
</a>
<nav className={styles.links} aria-label="Footer">
<a
href="https://github.com/rohitg00/agentmemory"
target="_blank"
rel="noopener"
>
SOURCE
</a>
<a
href="https://github.com/rohitg00/agentmemory/blob/main/CHANGELOG.md"
target="_blank"
rel="noopener"
>
CHANGELOG
</a>
<a href="https://iii.dev" target="_blank" rel="noopener">
RUNS ON iii
</a>
<a
href="https://github.com/rohitg00/agentmemory/blob/main/LICENSE"
target="_blank"
rel="noopener"
>
APACHE-2.0
</a>
</nav>
</div>
<div className={styles.fine}>© 2026 AGENTMEMORY · BUILT IN THE OPEN</div>
</footer>
);
}
@@ -0,0 +1,30 @@
.starBtn {
display: inline-flex;
align-items: center;
gap: 8px;
}
.icon {
display: inline-block;
flex: 0 0 auto;
margin-top: -2px;
}
.label {
letter-spacing: 0.08em;
}
.count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.5ch;
padding: 0 6px;
margin-left: 4px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.08);
color: var(--mist, #cfd2d1);
font-variant-numeric: tabular-nums;
font-size: 0.85em;
letter-spacing: 0.04em;
}
+93
View File
@@ -0,0 +1,93 @@
"use client";
import { useEffect, useState } from "react";
import styles from "./GitHubStarButton.module.css";
interface Props {
repo: string;
// SSR-fed initial count from Nav's server-side fetch. When present
// the badge renders the number on first paint; the client-side
// refetch below is a best-effort refresh that only fires if it
// succeeds (unauthenticated github.com api is rate-limited to
// ~60/hr/IP, which is why the count was invisible before).
initialStars?: number;
}
function formatStars(n: number): string {
if (n >= 1000) {
const k = n / 1000;
return k >= 10 ? `${Math.round(k)}k` : `${k.toFixed(1).replace(/\.0$/, "")}k`;
}
return String(n);
}
export function GitHubStarButton({ repo, initialStars }: Props) {
const [stars, setStars] = useState<number | null>(
typeof initialStars === "number" ? initialStars : null,
);
useEffect(() => {
let cancelled = false;
const cacheKey = `gh-stars:${repo}`;
const cached = typeof window !== "undefined" ? window.localStorage.getItem(cacheKey) : null;
if (cached) {
try {
const parsed = JSON.parse(cached) as { value: number; at: number };
if (Date.now() - parsed.at < 30 * 60 * 1000) {
setStars(parsed.value);
}
} catch {
/* ignore */
}
}
(async () => {
try {
const res = await fetch(`https://api.github.com/repos/${repo}`, {
headers: { Accept: "application/vnd.github+json" },
});
if (!res.ok) return;
const data = (await res.json()) as { stargazers_count?: number };
if (cancelled) return;
if (typeof data.stargazers_count === "number") {
setStars(data.stargazers_count);
try {
window.localStorage.setItem(
cacheKey,
JSON.stringify({ value: data.stargazers_count, at: Date.now() }),
);
} catch {
/* ignore */
}
}
} catch {
/* offline / blocked — keep SSR / cached / null */
}
})();
return () => {
cancelled = true;
};
}, [repo]);
return (
<a
href={`https://github.com/${repo}`}
target="_blank"
rel="noopener noreferrer"
className={`btn btn--ghost ${styles.starBtn}`}
aria-label={`Star ${repo} on GitHub${stars !== null ? `${stars.toLocaleString()} stars` : ""}`}
>
<svg
aria-hidden
viewBox="0 0 16 16"
width="18"
height="18"
className={styles.icon}
fill="currentColor"
>
<path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.75.75 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z" />
</svg>
<span className={styles.label}>STAR</span>
{stars !== null && <span className={styles.count}>{formatStars(stars)}</span>}
</a>
);
}
+123
View File
@@ -0,0 +1,123 @@
.hero {
position: relative;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 110px 24px 72px;
}
.vignette {
position: absolute;
inset: 0;
background:
radial-gradient(
ellipse at center,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.7) 75%,
rgba(0, 0, 0, 0.95) 100%
),
linear-gradient(
180deg,
rgba(0, 0, 0, 0.3) 0%,
rgba(0, 0, 0, 0) 30%,
rgba(0, 0, 0, 0) 70%,
rgba(0, 0, 0, 0.4) 100%
);
}
.content {
position: relative;
z-index: 2;
text-align: center;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
.chip {
display: inline-block;
padding: 7px 13px;
font-size: 12px;
letter-spacing: 0.96px;
color: var(--gold-soft);
border: 1px solid var(--gold-soft);
text-transform: uppercase;
margin-bottom: 22px;
}
.title {
font-size: clamp(44px, 9vw, 124px);
font-weight: 900;
line-height: 0.94;
letter-spacing: -0.02em;
margin: 0;
text-transform: uppercase;
display: flex;
justify-content: center;
gap: 0.05em;
flex-wrap: wrap;
}
.word {
display: inline-block;
opacity: 0;
transform: translateY(40px);
animation: slideIn 900ms cubic-bezier(0.2, 0.8, 0.2, 1) forwards;
}
.word:nth-child(2) {
animation-delay: 120ms;
}
.accent {
color: var(--gold);
}
@keyframes slideIn {
to {
opacity: 1;
transform: translateY(0);
}
}
.lede {
max-width: 720px;
margin: 24px auto 32px;
font-size: clamp(13px, 1.2vw, 16px);
letter-spacing: 0.12px;
color: var(--mist);
text-transform: uppercase;
line-height: 1.6;
}
.cta {
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
width: 100%;
}
.ctaSecondary {
display: inline-flex;
gap: 12px;
flex-wrap: wrap;
justify-content: center;
}
@media (max-width: 760px) {
.hero {
padding: 96px 18px 56px;
min-height: 0;
}
.title {
flex-direction: column;
font-size: clamp(40px, 14vw, 72px);
}
.lede {
margin: 20px auto 24px;
}
.chip {
font-size: 10px;
padding: 6px 10px;
margin-bottom: 18px;
}
}
+44
View File
@@ -0,0 +1,44 @@
import { MemoryGraph } from "./MemoryGraph";
import { GitHubStarButton } from "./GitHubStarButton";
import { HeroNpxCommand } from "./HeroNpxCommand";
import { FeaturedIn } from "./FeaturedIn";
import { getProjectMeta } from "@/lib/meta";
import { fetchRepoStats } from "@/lib/github";
import styles from "./Hero.module.css";
export async function Hero() {
const meta = getProjectMeta();
const stats = await fetchRepoStats();
return (
<section className={styles.hero} aria-labelledby="hero-title">
<MemoryGraph />
<div className={styles.vignette} aria-hidden />
<div className={styles.content}>
<div className={styles.chip}>
ZERO EXTERNAL DATABASES · v{meta.version}
</div>
<h1 className={styles.title} id="hero-title">
<span className={styles.word}>AGENT</span>
<span className={`${styles.word} ${styles.accent}`}>MEMORY</span>
</h1>
<p className={styles.lede}>
THE MEMORY LAYER YOUR CODING AGENT SHOULD HAVE HAD FROM DAY ONE.
CAPTURE EVERY SESSION. RECALL IN MILLISECONDS. RUN ANYWHERE.
</p>
<div className={styles.cta}>
<HeroNpxCommand />
<div className={styles.ctaSecondary}>
<a href="#live" className="btn btn--ghost">
SEE IT MOVE
</a>
<GitHubStarButton
repo="rohitg00/agentmemory"
initialStars={stats.stars > 0 ? stats.stars : undefined}
/>
</div>
</div>
<FeaturedIn compact />
</div>
</section>
);
}
@@ -0,0 +1,69 @@
.cmd {
display: inline-grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 14px;
padding: 16px 22px;
min-width: 460px;
max-width: 100%;
background: var(--iron);
border: 1px solid var(--gold);
color: var(--white);
font-family: var(--font-mono);
font-size: 16px;
letter-spacing: 0.02em;
cursor: pointer;
transition: background 0.12s ease, transform 0.12s ease, border-color 0.12s ease;
text-align: left;
}
.cmd:hover {
background: #1a1a1f;
transform: translateY(-1px);
}
.cmd:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 3px;
}
.copied {
background: var(--gold);
color: var(--ink);
border-color: var(--gold);
}
.copied .hint {
color: var(--ink);
}
.prompt {
color: var(--gold);
font-weight: 700;
}
.copied .prompt {
color: var(--ink);
}
.text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hint {
font-size: 10px;
letter-spacing: 1.4px;
color: var(--ash);
text-transform: uppercase;
padding-left: 12px;
border-left: 1px solid var(--charcoal);
}
.copied .hint {
border-left-color: rgba(0, 0, 0, 0.2);
}
@media (max-width: 640px) {
.cmd {
min-width: 0;
width: 100%;
font-size: 13px;
padding: 14px 16px;
gap: 10px;
}
.hint {
display: none;
}
}
+33
View File
@@ -0,0 +1,33 @@
"use client";
import { useState } from "react";
import styles from "./HeroNpxCommand.module.css";
const CMD = "npx @agentmemory/agentmemory";
export function HeroNpxCommand() {
const [copied, setCopied] = useState(false);
const onCopy = async () => {
try {
await navigator.clipboard.writeText(CMD);
setCopied(true);
window.setTimeout(() => setCopied(false), 1600);
} catch {
/* clipboard blocked — keep button visible */
}
};
return (
<button
type="button"
onClick={onCopy}
className={`${styles.cmd} ${copied ? styles.copied : ""}`}
aria-label="Copy install command"
>
<span className={styles.prompt}>$</span>
<span className={styles.text}>{CMD}</span>
<span className={styles.hint}>{copied ? "COPIED" : "CLICK TO COPY"}</span>
</button>
);
}
+93
View File
@@ -0,0 +1,93 @@
.install {
padding: 112px 0;
background: var(--iron);
border-top: 1px solid var(--charcoal);
}
.cards {
max-width: 1000px;
margin: 0 auto 56px;
padding: 0 40px;
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 28px;
}
.step {
display: grid;
gap: 10px;
}
.stepLabel {
font-size: 11px;
letter-spacing: 0.96px;
color: var(--gold);
text-transform: uppercase;
font-weight: 700;
}
.box {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 16px;
align-items: center;
padding: 22px 28px;
background: var(--abyss);
border: 1px solid var(--charcoal);
color: var(--mist);
font-family: var(--font-mono);
font-size: 14px;
text-align: left;
width: 100%;
min-width: 0;
transition:
border-color 200ms ease,
background 200ms ease;
cursor: pointer;
}
.box:hover {
border-color: var(--gold);
background: var(--charcoal);
}
.boxCopied {
border-color: #29c864 !important;
}
.boxCopied .hint {
color: #29c864;
}
.prompt {
color: var(--gold);
font-weight: 700;
}
.cmd {
color: var(--white);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.hint {
color: var(--ash);
font-size: 11px;
letter-spacing: 0.96px;
text-transform: uppercase;
white-space: nowrap;
}
.cta {
display: flex;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
padding: 0 40px;
}
@media (max-width: 640px) {
.cards,
.cta {
padding: 0 20px;
}
.box {
padding: 18px 16px;
gap: 10px;
font-size: 13px;
}
.hint {
font-size: 10px;
letter-spacing: 0.6px;
}
}
+118
View File
@@ -0,0 +1,118 @@
"use client";
import { useState } from "react";
import styles from "./Install.module.css";
import { AgentInstall } from "./AgentInstall";
interface Cmd {
label: string;
cmd: string;
hint: string;
}
const SIMPLE: Cmd[] = [
{
label: "1. INSTALL ONCE",
cmd: "npm install -g @agentmemory/agentmemory",
hint: "PUTS `agentmemory` ON YOUR PATH · STEPS 2/3 NEED THIS",
},
{
label: "2. START THE MEMORY SERVER",
cmd: "agentmemory",
hint: "RUNS ON :3111 · VIEWER ON :3113",
},
{
label: "3. SEE SEMANTIC RECALL INSTANTLY",
cmd: "agentmemory demo",
hint: "SEEDS 3 SESSIONS · PROVES HYBRID SEARCH WORKS",
},
];
const NPX_FALLBACK: Cmd = {
label: "PREFER ZERO-INSTALL? USE NPX",
cmd: "npx @agentmemory/agentmemory",
hint: "REPLACES STEPS 1+2 · USES NPX CACHE — SEE README FOR CAVEAT",
};
function CopyBox({ label, cmd, hint }: Cmd) {
const [copied, setCopied] = useState(false);
const [text, setText] = useState(hint);
const onClick = async () => {
try {
await navigator.clipboard.writeText(cmd);
setCopied(true);
setText("COPIED");
setTimeout(() => {
setCopied(false);
setText(hint);
}, 1600);
} catch {
setText("CLIPBOARD BLOCKED");
}
};
return (
<div className={styles.step}>
<div className={styles.stepLabel}>{label}</div>
<button
className={`${styles.box} ${copied ? styles.boxCopied : ""}`}
onClick={onClick}
>
<span className={styles.prompt}>$</span>
<span className={styles.cmd}>{cmd}</span>
<span className={styles.hint}>{text}</span>
</button>
</div>
);
}
export function Install() {
return (
<section className={styles.install} id="install" aria-labelledby="install-title">
<header className="section-head">
<span className="section-eyebrow">SHIP IT</span>
<h2 id="install-title" className="section-title">
ONE INSTALL.<br />ANY AGENT.
</h2>
<p className="section-lede">
RUNS ON YOUR MACHINE. DATA STAYS LOCAL. BRING YOUR CLAUDE SUBSCRIPTION
OR POINT IT AT ANTHROPIC, GEMINI, MINIMAX, OR OPENROUTER.
</p>
</header>
<div className={styles.cards}>
{SIMPLE.map((c) => (
<CopyBox key={c.cmd} {...c} />
))}
<CopyBox {...NPX_FALLBACK} />
<AgentInstall />
</div>
<div className={styles.cta}>
<a
className="btn btn--accent"
href="https://github.com/rohitg00/agentmemory#quick-start"
target="_blank"
rel="noopener"
>
READ THE QUICKSTART
</a>
<a
className="btn btn--ghost"
href="https://www.npmjs.com/package/@agentmemory/agentmemory"
target="_blank"
rel="noopener"
>
NPM PACKAGE
</a>
<a
className="btn btn--ghost"
href="https://github.com/rohitg00/agentmemory/tree/main/integrations"
target="_blank"
rel="noopener"
>
INTEGRATIONS
</a>
</div>
</section>
);
}
+103
View File
@@ -0,0 +1,103 @@
.live {
padding: 96px 0;
}
.terminal {
max-width: 1100px;
margin: 0 auto;
padding: 0 40px;
}
.chrome {
display: flex;
align-items: center;
gap: 8px;
background: var(--iron);
padding: 12px 16px;
border: 1px solid var(--charcoal);
border-bottom: none;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
background: var(--charcoal);
}
.red {
background: #ff5f57;
}
.yellow {
background: #febc2e;
}
.green {
background: #28c840;
}
.title {
margin-left: 12px;
font-family: var(--font-mono);
font-size: 12px;
color: var(--steel);
}
.body {
margin: 0;
padding: 24px 24px 16px;
background: var(--abyss);
border: 1px solid var(--charcoal);
border-top: none;
font-family: var(--font-mono);
font-size: 13.5px;
line-height: 1.7;
color: var(--mist);
white-space: pre-wrap;
min-height: 360px;
overflow: hidden;
}
.prompt {
color: var(--gold);
}
.comment {
color: var(--ash);
}
.ok {
color: #29c864;
}
.val {
color: var(--cyan);
}
.caret {
display: inline-block;
width: 8px;
height: 1em;
vertical-align: text-bottom;
background: var(--gold);
margin-left: 2px;
animation: blink 1s steps(1) infinite;
}
@keyframes blink {
50% {
opacity: 0;
}
}
.foot {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--iron);
padding: 12px 16px;
border: 1px solid var(--charcoal);
border-top: none;
}
.status {
font-family: var(--font-mono);
font-size: 11px;
color: var(--steel);
letter-spacing: 0.96px;
text-transform: uppercase;
}
@media (max-width: 640px) {
.terminal {
padding: 0 20px;
}
.body {
font-size: 12px;
}
}
+159
View File
@@ -0,0 +1,159 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import styles from "./LiveTerminal.module.css";
type SegType = "prompt" | "typed" | "plain" | "comment" | "ok" | "val";
interface Seg {
t: SegType;
text: string;
}
function buildScript(mcpTools: number, hooks: number): Seg[] {
return [
{ t: "prompt", text: "$ " },
{ t: "typed", text: "npx @agentmemory/agentmemory\n" },
{ t: "plain", text: "[agentmemory] iii-engine ready on :3111\n" },
{ t: "plain", text: `[agentmemory] ${mcpTools} MCP tools registered\n` },
{ t: "plain", text: `[agentmemory] ${hooks} autohooks armed\n\n` },
{ t: "prompt", text: "$ " },
{
t: "typed",
text: 'memory.recall({ query: "where did we land the retry logic?" })\n',
},
{ t: "comment", text: "// triple-stream retrieval: BM25 + vector + graph\n" },
{ t: "ok", text: "✓ 3 memories · p50 18ms · reranked on device\n\n" },
{ t: "plain", text: "→ " },
{ t: "val", text: "src/retry.ts:47 · exponentialBackoff(max=5, jitter=true)\n" },
{ t: "plain", text: "→ " },
{
t: "val",
text: 'commit 8f2e14c · "resolve conflict + honor x-amz headers"\n',
},
{ t: "plain", text: "→ " },
{
t: "val",
text: 'session 2026-04-16 · "bug: race when Retry-After is empty"\n\n',
},
{ t: "prompt", text: "$ " },
{ t: "typed", text: "memory.consolidate({ project: 'pay-api' })\n" },
{ t: "ok", text: "✓ 18 raw observations → 4 semantic memories · audit row emitted\n" },
];
}
function classFor(type: SegType) {
switch (type) {
case "prompt":
return styles.prompt;
case "comment":
return styles.comment;
case "ok":
return styles.ok;
case "val":
return styles.val;
default:
return "";
}
}
export function LiveTerminal({
mcpTools,
hooks,
}: {
mcpTools: number;
hooks: number;
}) {
const termRef = useRef<HTMLElement>(null);
const [status, setStatus] = useState("IDLE");
const runningRef = useRef(false);
const played = useRef(false);
const play = useCallback(async () => {
const term = termRef.current;
if (!term || runningRef.current) return;
runningRef.current = true;
setStatus("RUNNING");
term.innerHTML = "";
const caret = document.createElement("span");
caret.className = styles.caret;
term.appendChild(caret);
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
const script = buildScript(mcpTools, hooks);
for (const seg of script) {
const span = document.createElement("span");
const c = classFor(seg.t);
if (c) span.className = c;
term.insertBefore(span, caret);
if (seg.t === "typed") {
for (const ch of seg.text) {
span.textContent += ch;
await new Promise((r) =>
setTimeout(r, reduce ? 0 : 16 + Math.random() * 34),
);
}
await new Promise((r) => setTimeout(r, reduce ? 0 : 260));
} else {
span.textContent = seg.text;
await new Promise((r) => setTimeout(r, reduce ? 0 : 160));
}
}
setStatus("DONE");
runningRef.current = false;
}, [mcpTools, hooks]);
useEffect(() => {
const term = termRef.current;
if (!term) return;
const host = term.closest("[data-terminal]");
if (!host) return;
const io = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting && !played.current) {
played.current = true;
play();
io.unobserve(entry.target);
}
}
},
{ threshold: 0.4 },
);
io.observe(host);
return () => io.disconnect();
}, [play]);
return (
<section className={styles.live} id="live" aria-labelledby="live-title">
<header className="section-head">
<span className="section-eyebrow">LIVE</span>
<h2 id="live-title" className="section-title">
MEMORY THAT TYPES BACK.
</h2>
</header>
<div className={styles.terminal} data-terminal>
<div className={styles.chrome}>
<span className={`${styles.dot} ${styles.red}`} />
<span className={`${styles.dot} ${styles.yellow}`} />
<span className={`${styles.dot} ${styles.green}`} />
<span className={styles.title}>agentmemory@localhost:3111</span>
</div>
<pre className={styles.body}>
<code ref={termRef} />
</pre>
<div className={styles.foot}>
<button
className="btn btn--ghost btn--small"
onClick={() => {
played.current = true;
play();
}}
>
REPLAY
</button>
<span className={styles.status}>{status}</span>
</div>
</div>
</section>
);
}
+44
View File
@@ -0,0 +1,44 @@
.canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.pause {
position: absolute;
right: 40px;
bottom: 56px;
z-index: 3;
transition: transform 200ms ease;
}
.pause:hover {
transform: scale(1.08);
}
.rail {
position: absolute;
left: 40px;
right: 40px;
bottom: 24px;
height: 1px;
background: rgba(255, 255, 255, 0.2);
}
.rail span {
display: block;
height: 100%;
width: 0;
background: var(--white);
transition: width 60ms linear;
}
@media (max-width: 640px) {
.pause {
right: 20px;
bottom: 36px;
}
.rail {
left: 20px;
right: 20px;
}
}
+171
View File
@@ -0,0 +1,171 @@
"use client";
import { useEffect, useRef, useState } from "react";
import styles from "./MemoryGraph.module.css";
interface Node {
x: number;
y: number;
vx: number;
vy: number;
r: number;
hot: boolean;
}
export function MemoryGraph() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [running, setRunning] = useState(true);
const railRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
let localRunning = running && !reduceMotion;
let nodes: Node[] = [];
let rafId = 0;
let pulse = 0;
const size = () => {
const dpr = Math.max(1, window.devicePixelRatio || 1);
const w = canvas.clientWidth;
const h = canvas.clientHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
};
const seed = () => {
const w = canvas.clientWidth;
const h = canvas.clientHeight;
const count = Math.min(52, Math.floor((w * h) / 22000));
nodes = new Array(count).fill(0).map(() => ({
x: Math.random() * w,
y: Math.random() * h,
vx: (Math.random() - 0.5) * 0.18,
vy: (Math.random() - 0.5) * 0.18,
r: 1.2 + Math.random() * 2.2,
hot: Math.random() < 0.25,
}));
};
const draw = () => {
const w = canvas.clientWidth;
const h = canvas.clientHeight;
ctx.clearRect(0, 0, w, h);
for (const n of nodes) {
n.x += n.vx;
n.y += n.vy;
if (n.x < 0 || n.x > w) n.vx *= -1;
if (n.y < 0 || n.y > h) n.vy *= -1;
}
const maxDist = 160;
ctx.lineWidth = 1;
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const a = nodes[i];
const b = nodes[j];
const dx = a.x - b.x;
const dy = a.y - b.y;
const d = Math.hypot(dx, dy);
if (d > maxDist) continue;
const alpha = (1 - d / maxDist) * 0.35;
const hot = a.hot && b.hot;
ctx.strokeStyle = hot
? `rgba(255, 192, 0, ${alpha.toFixed(3)})`
: `rgba(255, 255, 255, ${(alpha * 0.5).toFixed(3)})`;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
}
for (const n of nodes) {
const r = n.r + (n.hot ? Math.sin(pulse + n.x) * 0.8 : 0);
ctx.fillStyle = n.hot ? "#FFC000" : "rgba(255,255,255,0.85)";
ctx.beginPath();
ctx.arc(n.x, n.y, Math.max(0.5, r), 0, Math.PI * 2);
ctx.fill();
if (n.hot) {
ctx.fillStyle = "rgba(255, 192, 0, 0.12)";
ctx.beginPath();
ctx.arc(n.x, n.y, r * 3.5, 0, Math.PI * 2);
ctx.fill();
}
}
pulse += 0.04;
};
const tick = () => {
if (!localRunning) return;
draw();
rafId = requestAnimationFrame(tick);
};
const onResize = () => {
size();
seed();
draw();
};
size();
seed();
draw();
if (localRunning) rafId = requestAnimationFrame(tick);
window.addEventListener("resize", onResize);
const updateRail = () => {
const h = document.documentElement;
const max = h.scrollHeight - h.clientHeight;
const pct = max <= 0 ? 0 : Math.min(1, h.scrollTop / max);
if (railRef.current) railRef.current.style.width = `${pct * 100}%`;
};
updateRail();
window.addEventListener("scroll", updateRail, { passive: true });
return () => {
cancelAnimationFrame(rafId);
window.removeEventListener("resize", onResize);
window.removeEventListener("scroll", updateRail);
localRunning = false;
};
}, [running]);
return (
<>
<canvas ref={canvasRef} className={styles.canvas} aria-hidden />
<button
className={styles.pause}
aria-label={running ? "Pause animation" : "Resume animation"}
onClick={() => setRunning((v) => !v)}
>
<svg viewBox="0 0 48 48" width="44" height="44" aria-hidden>
<polygon
points="24,2 44,13 44,35 24,46 4,35 4,13"
fill="none"
stroke="#fff"
strokeWidth="1.8"
/>
{running ? (
<g>
<rect x="17" y="16" width="4" height="16" fill="#fff" />
<rect x="27" y="16" width="4" height="16" fill="#fff" />
</g>
) : (
<polygon points="18,14 34,24 18,34" fill="#fff" />
)}
</svg>
</button>
<div className={styles.rail} aria-hidden>
<span ref={railRef} />
</div>
</>
);
}
@@ -0,0 +1,101 @@
.hamburger {
display: none;
width: 40px;
height: 40px;
border: 1px solid var(--charcoal);
align-items: center;
justify-content: center;
flex-direction: column;
gap: 4px;
}
.bar {
display: block;
width: 18px;
height: 2px;
background: var(--white);
transition:
transform 220ms ease,
opacity 220ms ease;
transform-origin: center;
}
.bar1 {
transform: translateY(6px) rotate(45deg) !important;
}
.bar2 {
opacity: 0;
}
.bar3 {
transform: translateY(-6px) rotate(-45deg) !important;
}
.sheet {
position: fixed;
inset: 0;
/* Portaled to <body>, so this sits in the root stacking context. Kept
below the nav (z-index: 100) so the brand + close button stay on top.
Fully opaque so hero content can't bleed through. */
background: var(--abyss);
z-index: 95;
opacity: 0;
pointer-events: none;
transition: opacity 200ms ease;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 88px 20px 40px;
}
.sheetOpen {
opacity: 1;
pointer-events: auto;
}
.panel {
width: 100%;
max-width: 520px;
padding-top: 24px;
}
.list {
list-style: none;
padding: 0;
margin: 0;
}
.list li {
border-bottom: 1px solid var(--charcoal);
}
.list a {
display: block;
padding: 18px 0;
font-size: 26px;
font-weight: 900;
letter-spacing: -0.01em;
text-transform: uppercase;
transition: color 180ms ease, padding-left 180ms ease;
}
.list a:hover {
color: var(--gold);
padding-left: 8px;
}
.foot {
margin-top: 24px;
display: flex;
flex-wrap: wrap;
gap: 14px 20px;
padding-top: 20px;
border-top: 1px solid var(--charcoal);
}
.foot a {
font-size: 12px;
letter-spacing: 0.96px;
color: var(--steel);
text-transform: uppercase;
}
.foot a:hover {
color: var(--gold);
}
/* Must match the breakpoint that hides .links in Nav.module.css, or the
721-860px range has no navigation at all. */
@media (max-width: 860px) {
.hamburger {
display: inline-flex;
}
}
+111
View File
@@ -0,0 +1,111 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { formatCompact } from "@/lib/format";
import styles from "./MobileNavToggle.module.css";
interface Section {
href: string;
label: string;
}
export function MobileNavToggle({
sections,
stars,
}: {
sections: Section[];
stars: number;
}) {
const [open, setOpen] = useState(false);
const [mounted, setMounted] = useState(false);
const toggleRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setOpen(false);
};
document.addEventListener("keydown", onKey);
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = "";
toggleRef.current?.focus();
};
}, [open]);
// The parent <header> uses `backdrop-filter`, which establishes a containing
// block for fixed descendants — that would clip the sheet to the header
// strip. Portal it to <body> so `position: fixed; inset: 0` covers the
// viewport instead.
const sheet = (
<div
className={`${styles.sheet} ${open ? styles.sheetOpen : ""}`}
aria-hidden={!open}
inert={!open}
onClick={(e) => {
if (e.target === e.currentTarget) setOpen(false);
}}
>
<nav className={styles.panel} aria-label="Site navigation">
<ul className={styles.list}>
{sections.map((s) => (
<li key={s.href}>
<a href={s.href} onClick={() => setOpen(false)}>
{s.label}
</a>
</li>
))}
</ul>
<div className={styles.foot}>
<a
href="https://github.com/rohitg00/agentmemory"
target="_blank"
rel="noopener noreferrer"
onClick={() => setOpen(false)}
>
GITHUB · {formatCompact(stars)}
</a>
<a
href="https://www.npmjs.com/package/@agentmemory/agentmemory"
target="_blank"
rel="noopener noreferrer"
onClick={() => setOpen(false)}
>
NPM
</a>
<a
href="https://github.com/rohitg00/agentmemory/blob/main/CHANGELOG.md"
target="_blank"
rel="noopener noreferrer"
onClick={() => setOpen(false)}
>
CHANGELOG
</a>
</div>
</nav>
</div>
);
return (
<>
<button
className={styles.hamburger}
aria-label={open ? "Close menu" : "Open menu"}
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
>
<span className={`${styles.bar} ${open ? styles.bar1 : ""}`} />
<span className={`${styles.bar} ${open ? styles.bar2 : ""}`} />
<span className={`${styles.bar} ${open ? styles.bar3 : ""}`} />
</button>
{mounted ? createPortal(sheet, document.body) : null}
</>
);
}
+133
View File
@@ -0,0 +1,133 @@
.nav {
position: fixed;
inset: 0 0 auto 0;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 32px;
padding: 14px 32px;
z-index: 100;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--charcoal);
}
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
color: var(--white);
}
.brandIcon {
width: 36px;
height: 36px;
display: block;
}
.brandWord {
font-size: 17px;
font-weight: 900;
letter-spacing: -0.01em;
text-transform: uppercase;
}
.links {
display: flex;
justify-content: center;
gap: 28px;
}
.link {
font-size: 12.5px;
font-weight: 700;
letter-spacing: 0.14px;
text-transform: uppercase;
color: var(--steel);
padding: 6px 2px;
border-bottom: 1px solid transparent;
transition: color 180ms ease, border-color 180ms ease;
}
.link:hover {
color: var(--white);
border-bottom-color: var(--gold);
}
.right {
display: flex;
align-items: center;
gap: 10px;
justify-self: end;
}
.gh {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 14px;
border: 1px solid var(--charcoal);
background: var(--iron);
color: var(--white);
font-size: 12.5px;
letter-spacing: 0.12px;
transition:
border-color 180ms ease,
background 180ms ease,
color 180ms ease;
}
.gh:hover {
border-color: var(--gold);
background: var(--charcoal);
color: var(--white);
}
.ghLabel {
font-weight: 700;
letter-spacing: 0.96px;
text-transform: uppercase;
font-size: 11px;
}
.ghDivider {
width: 1px;
height: 14px;
background: var(--charcoal);
}
.ghCount {
font-family: var(--font-mono);
font-weight: 900;
color: var(--white);
}
.cta {
padding: 10px 18px !important;
font-size: 12px !important;
}
@media (max-width: 1024px) {
.links {
gap: 20px;
}
.link {
font-size: 12px;
}
.ghLabel {
display: none;
}
}
@media (max-width: 860px) {
.links {
display: none;
}
.nav {
grid-template-columns: 1fr auto;
}
}
@media (max-width: 720px) {
.nav {
padding: 12px 20px;
gap: 16px;
grid-template-columns: 1fr auto;
}
.cta,
.gh {
display: none;
}
}
+72
View File
@@ -0,0 +1,72 @@
import Image from "next/image";
import { fetchRepoStats } from "@/lib/github";
import { formatCompact } from "@/lib/format";
import { MobileNavToggle } from "./MobileNavToggle";
import styles from "./Nav.module.css";
const SECTIONS = [
{ href: "#stack", label: "STACK" },
{ href: "#features", label: "FEATURES" },
{ href: "#command-center", label: "CONTROL" },
{ href: "#live", label: "DEMO" },
{ href: "#compare", label: "VS" },
{ href: "#install", label: "INSTALL" },
{ href: "/docs", label: "DOCS" },
];
export async function Nav() {
const stats = await fetchRepoStats();
return (
<header className={styles.nav}>
<a href="#top" className={styles.brand} aria-label="agentmemory home">
<Image
src="/icon.svg"
width={36}
height={36}
alt=""
aria-hidden
className={styles.brandIcon}
/>
<span className={styles.brandWord}>AGENTMEMORY</span>
</a>
<nav className={styles.links} aria-label="Sections">
{SECTIONS.map((s) => (
<a key={s.href} href={s.href} className={styles.link}>
{s.label}
</a>
))}
</nav>
<div className={styles.right}>
<a
className={styles.gh}
href="https://github.com/rohitg00/agentmemory"
target="_blank"
rel="noopener"
aria-label={`agentmemory on GitHub — ${stats.stars} stars`}
>
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden>
<path
fill="currentColor"
d="M12 .5C5.648.5.5 5.648.5 12c0 5.087 3.292 9.397 7.862 10.921.575.108.782-.25.782-.555v-1.96c-3.197.695-3.873-1.544-3.873-1.544-.523-1.327-1.277-1.68-1.277-1.68-1.044-.713.08-.699.08-.699 1.154.08 1.76 1.184 1.76 1.184 1.025 1.757 2.688 1.25 3.344.956.104-.742.4-1.25.728-1.537-2.552-.29-5.235-1.276-5.235-5.675 0-1.254.448-2.279 1.182-3.083-.118-.291-.513-1.461.112-3.046 0 0 .965-.31 3.162 1.178a10.964 10.964 0 0 1 2.878-.387c.977.004 1.96.131 2.878.387 2.195-1.487 3.16-1.178 3.16-1.178.626 1.585.231 2.755.113 3.046.736.804 1.18 1.829 1.18 3.083 0 4.41-2.686 5.381-5.246 5.665.411.354.778 1.053.778 2.122v3.146c0 .307.205.668.787.555C20.213 21.394 23.5 17.086 23.5 12 23.5 5.648 18.352.5 12 .5Z"
/>
</svg>
<span className={styles.ghLabel}>GITHUB</span>
<span className={styles.ghDivider} aria-hidden />
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden>
<path
d="M12 .587l3.668 7.568 8.332 1.151-6.064 5.828 1.48 8.279L12 19.245l-7.416 4.168 1.48-8.279L0 9.306l8.332-1.151z"
fill="#FFC000"
/>
</svg>
<span className={styles.ghCount}>{formatCompact(stats.stars)}</span>
</a>
<a href="#install" className={`${styles.cta} btn btn--accent`}>
INSTALL
</a>
<MobileNavToggle sections={SECTIONS} stars={stats.stars} />
</div>
</header>
);
}
+76
View File
@@ -0,0 +1,76 @@
.wrap {
padding: 112px 0 96px;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
max-width: 1440px;
margin: 0 auto;
padding: 0 40px;
background: var(--charcoal);
border: 1px solid var(--charcoal);
}
.card {
background: var(--abyss);
padding: 48px 32px;
transition:
background 250ms ease,
transform 250ms ease;
position: relative;
overflow: hidden;
min-height: 280px;
}
.card::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
135deg,
rgba(255, 192, 0, 0) 0%,
rgba(255, 192, 0, 0) 50%,
rgba(255, 192, 0, 0.08) 100%
);
opacity: 0;
transition: opacity 250ms ease;
}
.card:hover {
background: var(--iron);
transform: translateY(-4px);
}
.card:hover::before {
opacity: 1;
}
.glyph {
font-family: var(--font-mono);
font-size: 14px;
color: var(--gold);
letter-spacing: 0.2px;
margin-bottom: 32px;
}
.title {
font-size: 40px;
font-weight: 900;
line-height: 0.98;
letter-spacing: -0.01em;
margin: 0 0 24px;
text-transform: uppercase;
}
.text {
font-size: 14px;
letter-spacing: 0.12px;
color: var(--steel);
text-transform: uppercase;
margin: 0;
line-height: 1.6;
}
@media (max-width: 1024px) {
.grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.grid {
padding: 0 20px;
}
}
+88
View File
@@ -0,0 +1,88 @@
"use client";
import { useEffect, useRef } from "react";
import styles from "./Primitives.module.css";
const CARDS = [
{
glyph: "01",
title: "HOOKS",
text:
"12 AUTO-CAPTURE HOOKS PIPED INTO EVERY CODING AGENT. EVERY TOOL CALL, EVERY PROMPT, EVERY STOP BECOMES A COMPRESSED OBSERVATION.",
},
{
glyph: "02",
title: "RECALL",
text:
"TRIPLE-STREAM RETRIEVAL — BM25 + VECTOR + KNOWLEDGE GRAPH. RERANKED ON DEVICE. P50 UNDER 20MS ON A LAPTOP.",
},
{
glyph: "03",
title: "CONSOLIDATE",
text:
"HOURLY SWEEPS COMPRESS RAW OBSERVATIONS INTO SEMANTIC MEMORIES. DUPLICATES MERGED. STALE ROWS DECAYED. AUDIT ROW EMITTED EVERY DELETE.",
},
];
export function Primitives() {
const gridRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce || !gridRef.current) return;
const cards = gridRef.current.querySelectorAll<HTMLElement>(
"[data-tilt]",
);
const handlers: Array<[HTMLElement, (e: MouseEvent) => void, () => void]> =
[];
cards.forEach((card) => {
const onMove = (e: MouseEvent) => {
const rect = card.getBoundingClientRect();
const px = (e.clientX - rect.left) / rect.width - 0.5;
const py = (e.clientY - rect.top) / rect.height - 0.5;
card.style.transform = `translateY(-4px) rotateX(${(-py * 4).toFixed(
2,
)}deg) rotateY(${(px * 4).toFixed(2)}deg)`;
};
const onLeave = () => {
card.style.transform = "";
};
card.addEventListener("mousemove", onMove);
card.addEventListener("mouseleave", onLeave);
handlers.push([card, onMove, onLeave]);
});
return () => {
handlers.forEach(([el, m, l]) => {
el.removeEventListener("mousemove", m);
el.removeEventListener("mouseleave", l);
});
};
}, []);
return (
<section className={styles.wrap} id="stack" aria-labelledby="prim-title">
<header className="section-head">
<span className="section-eyebrow">THE STACK</span>
<h2 id="prim-title" className="section-title">
THREE LAYERS.
<br />
NO FRAMEWORK TAX.
</h2>
<p className="section-lede">
BUILT ON THE iii ENGINE EVERY MEMORY OPERATION IS A WORKER, A
FUNCTION, OR A TRIGGER. NO REDIS. NO KAFKA. NO POSTGRES. THE ENTIRE
RUNTIME IS ONE PROCESS.
</p>
</header>
<div className={styles.grid} ref={gridRef}>
{CARDS.map((c) => (
<article key={c.glyph} className={styles.card} data-tilt>
<div className={styles.glyph}>{c.glyph}</div>
<h3 className={styles.title}>{c.title}</h3>
<p className={styles.text}>{c.text}</p>
</article>
))}
</div>
</section>
);
}
+49
View File
@@ -0,0 +1,49 @@
"use client";
import { useEffect, useRef } from "react";
export function ScrollProgress() {
const barRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
const update = () => {
const h = document.documentElement;
const max = h.scrollHeight - h.clientHeight;
const pct = max <= 0 ? 0 : Math.min(1, h.scrollTop / max);
if (barRef.current) barRef.current.style.width = `${pct * 100}%`;
};
update();
window.addEventListener("scroll", update, { passive: true });
window.addEventListener("resize", update);
return () => {
window.removeEventListener("scroll", update);
window.removeEventListener("resize", update);
};
}, []);
return (
<div
aria-hidden
style={{
position: "fixed",
top: 0,
left: 0,
width: "100%",
height: 2,
background: "transparent",
zIndex: 200,
}}
>
<span
ref={barRef}
style={{
display: "block",
height: "100%",
width: 0,
background: "var(--gold)",
transition: "width 80ms linear",
}}
/>
</div>
);
}
+70
View File
@@ -0,0 +1,70 @@
.stats {
border-top: 1px solid var(--charcoal);
border-bottom: 1px solid var(--charcoal);
background: var(--iron);
}
.row {
display: grid;
grid-template-columns: repeat(6, 1fr);
max-width: 1440px;
margin: 0 auto;
}
.stat {
padding: 48px 24px;
text-align: center;
border-right: 1px solid var(--charcoal);
transition: background 250ms ease;
}
.stat:last-child {
border-right: none;
}
.stat:hover {
background: var(--charcoal);
}
.num {
font-size: clamp(40px, 5vw, 64px);
font-weight: 900;
line-height: 1;
color: var(--gold);
letter-spacing: -0.02em;
}
.label {
margin-top: 12px;
font-size: 11px;
letter-spacing: 0.96px;
color: var(--steel);
text-transform: uppercase;
}
@media (max-width: 1024px) {
.row {
grid-template-columns: repeat(3, 1fr);
}
.stat {
border-bottom: 1px solid var(--charcoal);
}
.stat:nth-child(3n) {
border-right: none;
}
}
@media (max-width: 640px) {
.row {
grid-template-columns: 1fr 1fr;
}
.stat {
padding: 32px 12px;
}
.num {
font-size: 36px;
}
.label {
font-size: 10px;
letter-spacing: 0.6px;
}
.stat:nth-child(3n) {
border-right: 1px solid var(--charcoal);
}
.stat:nth-child(2n) {
border-right: none;
}
}
+109
View File
@@ -0,0 +1,109 @@
"use client";
import { useEffect, useRef } from "react";
import styles from "./Stats.module.css";
interface StatItem {
target: number;
suffix?: string;
label: string;
float?: boolean;
}
export function Stats({
mcpTools,
hooks,
testsPassing,
}: {
mcpTools: number;
hooks: number;
testsPassing: number;
}) {
const rootRef = useRef<HTMLDivElement>(null);
const STATS: StatItem[] = [
{ target: 95.2, suffix: "%", label: "RETRIEVAL R@5 · LONGMEMEVAL-S", float: true },
{ target: 92, suffix: "%", label: "FEWER INPUT TOKENS PER SESSION" },
{ target: mcpTools, label: "MCP TOOLS" },
{ target: hooks, label: "AUTOHOOKS" },
{ target: 0, label: "EXTERNAL DATABASES" },
{ target: testsPassing, label: "TESTS PASSING" },
];
useEffect(() => {
if (!rootRef.current) return;
const root = rootRef.current;
// Reset per-element done flag so deps changing (e.g. a new meta snapshot
// at build) replays the count animation against the new target.
root
.querySelectorAll<HTMLDivElement>("[data-num]")
.forEach((el) => delete el.dataset.done);
const count = (el: HTMLDivElement) => {
const target = Number(el.dataset.target);
const suffix = el.dataset.suffix || "";
const isFloat = el.dataset.float === "1";
const startAt = performance.now();
const duration = 1400;
const tick = (now: number) => {
const t = Math.min(1, (now - startAt) / duration);
const eased = 1 - Math.pow(1 - t, 3);
const v = target * eased;
el.textContent = isFloat
? `${v.toFixed(1)}${suffix}`
: `${Math.round(v)}${suffix}`;
if (t < 1) requestAnimationFrame(tick);
else
el.textContent = isFloat
? `${target.toFixed(1)}${suffix}`
: `${target}${suffix}`;
};
requestAnimationFrame(tick);
};
const io = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
const el = entry.target as HTMLDivElement;
const num = el.querySelector<HTMLDivElement>("[data-num]");
if (num && !num.dataset.done) {
num.dataset.done = "1";
count(num);
}
io.unobserve(el);
}
},
{ threshold: 0.5 },
);
root
.querySelectorAll<HTMLDivElement>("[data-stat]")
.forEach((el) => io.observe(el));
return () => io.disconnect();
}, [mcpTools, hooks, testsPassing]);
return (
<section className={styles.stats} aria-label="Benchmarks">
<div className={styles.row} ref={rootRef}>
{STATS.map((s) => (
<article key={s.label} className={styles.stat} data-stat>
<div
className={styles.num}
data-num
data-target={s.target}
data-suffix={s.suffix || ""}
data-float={s.float ? "1" : "0"}
>
{s.float ? s.target.toFixed(1) : s.target}
{s.suffix || ""}
</div>
<div className={styles.label}>{s.label}</div>
</article>
))}
</div>
</section>
);
}
+221
View File
@@ -0,0 +1,221 @@
.wrap {
padding: 96px 0 96px;
background: var(--ink);
border-top: 1px solid var(--charcoal);
}
.inner {
max-width: 1100px;
margin: 0 auto;
padding: 0 40px;
display: flex;
flex-direction: column;
gap: 20px;
align-items: center;
}
.eyebrow {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 2.4px;
color: var(--ash);
text-transform: uppercase;
display: flex;
align-items: center;
gap: 12px;
margin-top: 8px;
}
.eyebrow::before,
.eyebrow::after {
content: "";
flex: 0 0 60px;
height: 1px;
background: var(--charcoal);
}
.title {
font-family: var(--font-display, var(--font-mono));
font-size: clamp(40px, 6vw, 64px);
letter-spacing: -0.02em;
line-height: 1;
color: var(--white);
text-align: center;
text-transform: uppercase;
font-weight: 800;
margin: 0;
}
.accent {
color: var(--gold);
}
.lede {
font-family: var(--font-mono);
font-size: 13px;
letter-spacing: 0.4px;
color: var(--steel);
text-align: center;
margin: 0 0 8px;
max-width: 560px;
}
.sectionLabel {
align-self: flex-start;
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 2.4px;
color: var(--gold);
text-transform: uppercase;
margin-top: 32px;
padding-bottom: 4px;
border-bottom: 1px solid var(--charcoal);
width: 100%;
}
/* Featured use-case cards — 2-column, larger, includes a tag chip. */
.useCases {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
width: 100%;
}
.useCaseCard {
position: relative;
display: flex;
flex-direction: column;
gap: 20px;
padding: 32px 30px 24px;
border: 1px solid var(--charcoal);
background: var(--iron);
text-decoration: none;
color: var(--white);
transition: border-color 0.12s ease, transform 0.12s ease,
background 0.12s ease;
min-height: 240px;
}
.useCaseCard:hover,
.useCaseCard:focus-visible {
border-color: var(--gold);
transform: translateY(-2px);
background: #16161a;
}
.useCaseCard:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.useCaseTag {
display: inline-block;
align-self: flex-start;
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 1.4px;
color: var(--gold);
text-transform: uppercase;
border: 1px solid var(--gold);
padding: 5px 10px;
background: rgba(255, 192, 0, 0.06);
}
.useCaseQuote {
font-family: var(--font-sans, system-ui);
font-size: 17px;
line-height: 1.55;
color: var(--white);
margin: 0;
flex: 1;
}
/* Endorsement cards — 4 columns, smaller. */
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 18px;
width: 100%;
}
.card {
position: relative;
display: flex;
flex-direction: column;
gap: 16px;
padding: 24px 22px 20px;
border: 1px solid var(--charcoal);
background: var(--iron);
text-decoration: none;
color: var(--white);
transition: border-color 0.12s ease, transform 0.12s ease,
background 0.12s ease;
min-height: 200px;
}
.card:hover,
.card:focus-visible {
border-color: var(--gold);
transform: translateY(-2px);
background: #16161a;
}
.card:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.quoteMark {
font-family: var(--font-display, serif);
font-size: 32px;
line-height: 0;
color: var(--gold);
display: inline;
margin-right: 4px;
vertical-align: -4px;
}
.useCaseCard .quoteMark {
font-size: 40px;
vertical-align: -6px;
}
.quote {
font-family: var(--font-sans, system-ui);
font-size: 14px;
line-height: 1.55;
color: var(--white);
margin: 0;
flex: 1;
}
.author {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
padding-top: 12px;
border-top: 1px solid var(--charcoal);
}
.name {
font-family: var(--font-mono);
font-size: 12px;
letter-spacing: 0.6px;
color: var(--white);
text-transform: uppercase;
font-weight: 700;
}
.source {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.8px;
color: var(--ash);
text-transform: uppercase;
transition: color 0.12s ease;
}
.card:hover .source,
.useCaseCard:hover .source {
color: var(--gold);
}
@media (max-width: 980px) {
.useCases {
grid-template-columns: 1fr;
}
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.grid {
grid-template-columns: 1fr;
}
.wrap {
padding: 64px 0 64px;
}
.badge {
width: 220px;
height: 47px;
}
}
+142
View File
@@ -0,0 +1,142 @@
import styles from "./Testimonials.module.css";
// PH launch discussion thread for agentmemory — every testimonial in
// this section is a verbatim quote from that thread. The live
// upvote badge lives in <FeaturedIn /> at the top of the page.
const PH_DISCUSSION_URL =
"https://www.producthunt.com/p/agent-memory-dev/how-do-you-found-agentmemory-so-far-happy-to-help";
// "Use cases" — quotes that describe how a builder is using
// agentmemory in production. Each one carries an explicit
// `useCase` line so the framing is concrete (not just "I like it").
interface UseCase {
name: string;
useCase: string;
quote: string;
href: string;
}
const USE_CASES: UseCase[] = [
{
name: "Peter Neyra",
useCase: "Backfilled a month of Cursor transcripts",
quote:
"I backfilled agent memory on my past month's Cursor agent transcripts. It was surprisingly accurate. Picked up on things that I moved away from.",
href: "https://www.producthunt.com/p/agent-memory-dev/how-do-you-found-agentmemory-so-far-happy-to-help?comment=5379518",
},
{
name: "Pranav Prakash",
useCase: "Two weeks of production use",
quote:
"Been using it for 2 weeks, and I definitely see improvements.",
href: PH_DISCUSSION_URL,
},
];
// "Endorsements" — shorter quotes that position the product, more
// social-proof than use-case. Kept tight so the grid balances.
interface Endorsement {
name: string;
quote: string;
href: string;
}
const ENDORSEMENTS: Endorsement[] = [
{
name: "Alper Tayfur",
quote:
"Tackles one of the biggest pain points with coding agents: losing useful project context across sessions without bloating the context window.",
href: PH_DISCUSSION_URL,
},
{
name: "Mia Taylor",
quote:
"The focus on making memory actually useful for agents instead of just storing context endlessly.",
href: PH_DISCUSSION_URL,
},
{
name: "Thomas Hall",
quote:
"Memory often becomes just more noise over time. Agentmemory feels more intentional compared to a lot of tools in this space.",
href: PH_DISCUSSION_URL,
},
{
name: "Zoe Alexandra",
quote:
"Tried it briefly — feels clean and easy to get started with.",
href: PH_DISCUSSION_URL,
},
];
export function Testimonials() {
return (
<section
className={styles.wrap}
aria-labelledby="testimonials-title"
>
<div className={styles.inner}>
<div id="testimonials-title" className={styles.eyebrow}>
BUILDERS USING AGENTMEMORY
</div>
<h2 className={styles.title}>
IN THE <span className={styles.accent}>WILD.</span>
</h2>
<p className={styles.lede}>
Verbatim from the Product Hunt launch thread. Each card
links back to the source comment.
</p>
<div className={styles.sectionLabel}>HOW THEY USE IT</div>
<div className={styles.useCases}>
{USE_CASES.map((u) => (
<a
key={u.name}
className={styles.useCaseCard}
href={u.href}
target="_blank"
rel="noopener"
aria-label={`${u.name}'s comment on Product Hunt`}
>
<div className={styles.useCaseTag}>{u.useCase}</div>
<p className={styles.useCaseQuote}>
<span className={styles.quoteMark} aria-hidden>
&ldquo;
</span>
{u.quote}
</p>
<div className={styles.author}>
<span className={styles.name}>{u.name}</span>
<span className={styles.source}>Product Hunt </span>
</div>
</a>
))}
</div>
<div className={styles.sectionLabel}>WHAT THEY SAY</div>
<div className={styles.grid}>
{ENDORSEMENTS.map((t) => (
<a
key={t.name}
className={styles.card}
href={t.href}
target="_blank"
rel="noopener"
aria-label={`${t.name}'s comment on Product Hunt`}
>
<p className={styles.quote}>
<span className={styles.quoteMark} aria-hidden>
&ldquo;
</span>
{t.quote}
</p>
<div className={styles.author}>
<span className={styles.name}>{t.name}</span>
<span className={styles.source}>Product Hunt </span>
</div>
</a>
))}
</div>
</div>
</section>
);
}