chore: import upstream snapshot with attribution
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
.next
|
||||
out
|
||||
.env*.local
|
||||
.vercel
|
||||
*.tsbuildinfo
|
||||
.DS_Store
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
@@ -0,0 +1,63 @@
|
||||
# agentmemory website
|
||||
|
||||
Next.js 15 App Router landing page for agentmemory. Lamborghini-inspired
|
||||
black + gold design system. Deploys to Vercel with zero config.
|
||||
|
||||
## Stack
|
||||
|
||||
- Next.js 15.1 (App Router, React 19, TypeScript 5.7)
|
||||
- `next/font` for Archivo + JetBrains Mono
|
||||
- CSS Modules + one `globals.css`
|
||||
- No Tailwind, no bundler config, no client-side routing
|
||||
|
||||
## Local dev
|
||||
|
||||
```bash
|
||||
cd website
|
||||
npm install
|
||||
npm run dev
|
||||
# open http://localhost:3000
|
||||
```
|
||||
|
||||
## Deploy (Vercel)
|
||||
|
||||
Two options:
|
||||
|
||||
1. Import the repo on vercel.com and set **Root Directory** to `website/`. That's it.
|
||||
2. Or `npx vercel` from the `website/` directory.
|
||||
|
||||
No env vars required. Node 20 LTS or newer.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
website/
|
||||
app/
|
||||
layout.tsx — <html> + fonts + metadata + viewport
|
||||
page.tsx — composes the landing sections in order
|
||||
globals.css — design tokens, buttons, section-head utilities
|
||||
components/
|
||||
Nav.tsx — hexagonal bull mark + menu
|
||||
Hero.tsx — title + lede + CTAs
|
||||
MemoryGraph.tsx — client canvas animation + hexagonal pause + scroll rail
|
||||
Stats.tsx — counter-up on intersect
|
||||
Primitives.tsx — three cards with 3D mouse tilt
|
||||
LiveTerminal.tsx — typewriter replay of memory.recall + consolidate
|
||||
Compare.tsx — agentmemory vs Mem0/Letta/Cognee table
|
||||
Agents.tsx — supported-agents grid
|
||||
Install.tsx — click-to-copy npm + console commands
|
||||
Footer.tsx — source / changelog / license links
|
||||
ScrollProgress.tsx — thin gold progress bar at the top of the viewport
|
||||
next.config.ts
|
||||
tsconfig.json
|
||||
package.json
|
||||
```
|
||||
|
||||
Each interactive component is a `"use client"` island. Everything else
|
||||
renders on the server.
|
||||
|
||||
## Design source
|
||||
|
||||
`/DESIGN.md` at the repo root (generated by
|
||||
`npx getdesign@latest add lamborghini`). Colors, type, spacing rules live
|
||||
there. Every new component should reference it first.
|
||||
@@ -0,0 +1,160 @@
|
||||
:root {
|
||||
--abyss: #000000;
|
||||
--iron: #181818;
|
||||
--charcoal: #202020;
|
||||
--ash: #7d7d7d;
|
||||
--steel: #969696;
|
||||
--mist: #e6e6e6;
|
||||
--white: #ffffff;
|
||||
--gold: #ffc000;
|
||||
--gold-deep: #917300;
|
||||
--gold-soft: #ffce3e;
|
||||
--cyan: #29abe2;
|
||||
--blue: #3860be;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--abyss);
|
||||
color: var(--white);
|
||||
font-family: var(--font-archivo), "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: color 160ms ease;
|
||||
}
|
||||
a:hover {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--gold);
|
||||
color: var(--abyss);
|
||||
}
|
||||
|
||||
/* Reveal animation */
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(32px);
|
||||
transition: opacity 600ms ease,
|
||||
transform 600ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
.reveal.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.001ms !important;
|
||||
transition-duration: 0.001ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Buttons shared */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 18px 28px;
|
||||
font-size: 14.4px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2px;
|
||||
text-transform: uppercase;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0;
|
||||
transition:
|
||||
background 200ms ease,
|
||||
color 200ms ease,
|
||||
border-color 200ms ease,
|
||||
opacity 200ms ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn--accent {
|
||||
background: var(--gold);
|
||||
color: var(--abyss);
|
||||
padding: 22px 32px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.btn--accent:hover {
|
||||
background: var(--gold-deep);
|
||||
color: var(--white);
|
||||
}
|
||||
.btn--ghost {
|
||||
background: transparent;
|
||||
color: var(--white);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.btn--ghost:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: var(--white);
|
||||
color: var(--white);
|
||||
opacity: 1;
|
||||
}
|
||||
.btn--small {
|
||||
padding: 10px 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Shared section head */
|
||||
.section-head {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto 56px;
|
||||
padding: 0 40px;
|
||||
}
|
||||
.section-eyebrow {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.96px;
|
||||
color: var(--gold);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 16px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--gold);
|
||||
}
|
||||
.section-title {
|
||||
font-size: clamp(32px, 5vw, 72px);
|
||||
font-weight: 900;
|
||||
line-height: 0.98;
|
||||
letter-spacing: -0.01em;
|
||||
margin: 0 0 24px;
|
||||
text-transform: uppercase;
|
||||
text-wrap: balance;
|
||||
}
|
||||
.section-lede {
|
||||
max-width: 680px;
|
||||
font-size: 16px;
|
||||
letter-spacing: 0.12px;
|
||||
color: var(--steel);
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.section-head {
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { Archivo, JetBrains_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const archivo = Archivo({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "700", "900"],
|
||||
variable: "--font-archivo",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const jetbrains = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500"],
|
||||
variable: "--font-mono",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://agentmemory.dev"),
|
||||
title: "AGENTMEMORY — PERSISTENT MEMORY FOR AI CODING AGENTS",
|
||||
description:
|
||||
"The memory layer your coding agent should have had from day one. 95.2% retrieval R@5. 92% fewer tokens. 0 external databases. Works with every agent.",
|
||||
icons: {
|
||||
icon: [{ url: "/icon.svg", type: "image/svg+xml" }],
|
||||
apple: "/icon.svg",
|
||||
},
|
||||
openGraph: {
|
||||
title: "agentmemory",
|
||||
description:
|
||||
"Persistent memory for AI coding agents. Runs locally. Zero external databases.",
|
||||
type: "website",
|
||||
url: "/",
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "agentmemory",
|
||||
description:
|
||||
"Persistent memory for AI coding agents. Runs locally. Zero external databases.",
|
||||
},
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: "#000000",
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={`${archivo.variable} ${jetbrains.variable}`}>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
|
||||
export const alt = "agentmemory — persistent memory for AI coding agents";
|
||||
export const size = { width: 1200, height: 630 };
|
||||
export const contentType = "image/png";
|
||||
|
||||
export default function Image() {
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
padding: "72px 80px",
|
||||
background:
|
||||
"radial-gradient(ellipse at 20% 0%, #1a1407 0%, #0a0a0a 55%, #000 100%)",
|
||||
color: "#fff",
|
||||
fontFamily: "system-ui, -apple-system, sans-serif",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 16,
|
||||
fontSize: 20,
|
||||
letterSpacing: 3,
|
||||
fontWeight: 700,
|
||||
color: "#f3b840",
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
background: "#f3b840",
|
||||
boxShadow: "0 0 24px #f3b840",
|
||||
}}
|
||||
/>
|
||||
<span>AGENTMEMORY</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 28 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
fontSize: 128,
|
||||
fontWeight: 900,
|
||||
lineHeight: 0.95,
|
||||
letterSpacing: -3,
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
<span>AGENT</span>
|
||||
<span style={{ color: "#f3b840" }}>MEMORY</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
fontSize: 32,
|
||||
color: "#b8b8b8",
|
||||
letterSpacing: 0.5,
|
||||
maxWidth: 900,
|
||||
lineHeight: 1.35,
|
||||
}}
|
||||
>
|
||||
Persistent memory for AI coding agents. Runs locally. Zero external
|
||||
databases.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 48,
|
||||
fontSize: 22,
|
||||
color: "#888",
|
||||
letterSpacing: 2,
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 700,
|
||||
borderTop: "1px solid #222",
|
||||
paddingTop: 28,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: 10 }}>
|
||||
<span style={{ color: "#f3b840" }}>95.2%</span>
|
||||
<span>RETRIEVAL R@5</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 10 }}>
|
||||
<span style={{ color: "#f3b840" }}>92%</span>
|
||||
<span>FEWER TOKENS</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: 10 }}>
|
||||
<span style={{ color: "#f3b840" }}>0</span>
|
||||
<span>EXTERNAL DBs</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{ ...size },
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ScrollProgress } from "@/components/ScrollProgress";
|
||||
import { Nav } from "@/components/Nav";
|
||||
import { Hero } from "@/components/Hero";
|
||||
import { Stats } from "@/components/Stats";
|
||||
import { Primitives } from "@/components/Primitives";
|
||||
import { Features } from "@/components/Features";
|
||||
import { CommandCenter } from "@/components/CommandCenter";
|
||||
import { LiveTerminal } from "@/components/LiveTerminal";
|
||||
import { Compare } from "@/components/Compare";
|
||||
import { Testimonials } from "@/components/Testimonials";
|
||||
import { Agents } from "@/components/Agents";
|
||||
import { Install } from "@/components/Install";
|
||||
import { Footer } from "@/components/Footer";
|
||||
import { getProjectMeta } from "@/lib/meta";
|
||||
|
||||
export default function Page() {
|
||||
const meta = getProjectMeta();
|
||||
return (
|
||||
<>
|
||||
<ScrollProgress />
|
||||
<Nav />
|
||||
<main id="top">
|
||||
<Hero />
|
||||
<Stats
|
||||
mcpTools={meta.mcpTools}
|
||||
hooks={meta.hooks}
|
||||
testsPassing={meta.testsPassing}
|
||||
/>
|
||||
<Primitives />
|
||||
<Features
|
||||
hooks={meta.hooks}
|
||||
mcpTools={meta.mcpTools}
|
||||
restEndpoints={meta.restEndpoints}
|
||||
/>
|
||||
<CommandCenter />
|
||||
<LiveTerminal mcpTools={meta.mcpTools} hooks={meta.hooks} />
|
||||
<Compare />
|
||||
<Testimonials />
|
||||
<Agents />
|
||||
<Install />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const alt = "agentmemory — persistent memory for AI coding agents";
|
||||
export const size = { width: 1200, height: 630 };
|
||||
export const contentType = "image/png";
|
||||
|
||||
export { default } from "./opengraph-image";
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 <agent>` 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
“
|
||||
</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>
|
||||
“
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export function formatCompact(n: number): string {
|
||||
if (n >= 10_000) return `${(n / 1000).toFixed(1).replace(/\.0$/, "")}K`;
|
||||
if (n >= 1_000) return `${(n / 1000).toFixed(1)}K`;
|
||||
return n.toString();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"version": "0.9.26",
|
||||
"mcpTools": 53,
|
||||
"hooks": 12,
|
||||
"restEndpoints": 126,
|
||||
"testsPassing": 1394,
|
||||
"generatedAt": "2026-06-03T10:14:01.074Z"
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import "server-only";
|
||||
|
||||
export interface RepoStats {
|
||||
stars: number;
|
||||
forks: number;
|
||||
issues: number;
|
||||
}
|
||||
|
||||
const REPO = "rohitg00/agentmemory";
|
||||
|
||||
export async function fetchRepoStats(): Promise<RepoStats> {
|
||||
const fallback: RepoStats = { stars: 0, forks: 0, issues: 0 };
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
accept: "application/vnd.github+json",
|
||||
"user-agent": "agentmemory-website",
|
||||
};
|
||||
if (process.env.GITHUB_TOKEN) {
|
||||
headers.authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||
}
|
||||
const res = await fetch(`https://api.github.com/repos/${REPO}`, {
|
||||
headers,
|
||||
next: { revalidate: 3600 },
|
||||
});
|
||||
if (!res.ok) return fallback;
|
||||
const data = (await res.json()) as {
|
||||
stargazers_count?: number;
|
||||
forks_count?: number;
|
||||
open_issues_count?: number;
|
||||
};
|
||||
return {
|
||||
stars: data.stargazers_count ?? 0,
|
||||
forks: data.forks_count ?? 0,
|
||||
issues: data.open_issues_count ?? 0,
|
||||
};
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import "server-only";
|
||||
import generated from "./generated-meta.json" with { type: "json" };
|
||||
|
||||
export interface ProjectMeta {
|
||||
version: string;
|
||||
mcpTools: number;
|
||||
hooks: number;
|
||||
restEndpoints: number;
|
||||
testsPassing: number;
|
||||
}
|
||||
|
||||
// Values are baked at build time by scripts/gen-meta.mjs (see package.json
|
||||
// prebuild). Runtime file lookups via import.meta.url break after Next.js
|
||||
// moves server components into .next/server/ — `../..` from there stays
|
||||
// inside the build cache, not at the repo root, and version silently falls
|
||||
// back to "0.0.0". Static JSON import sidesteps that entirely.
|
||||
export function getProjectMeta(): ProjectMeta {
|
||||
return {
|
||||
version: generated.version,
|
||||
mcpTools: generated.mcpTools,
|
||||
hooks: generated.hooks,
|
||||
restEndpoints: generated.restEndpoints,
|
||||
testsPassing: generated.testsPassing,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { NextConfig } from "next";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const config: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
poweredByHeader: false,
|
||||
turbopack: {
|
||||
root: here,
|
||||
},
|
||||
images: {
|
||||
dangerouslyAllowSVG: true,
|
||||
contentDispositionType: "attachment",
|
||||
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
|
||||
remotePatterns: [
|
||||
{ protocol: "https", hostname: "github.com" },
|
||||
{ protocol: "https", hostname: "avatars.githubusercontent.com" },
|
||||
{ protocol: "https", hostname: "raw.githubusercontent.com" },
|
||||
{ protocol: "https", hostname: "matthiasroder.com" },
|
||||
{ protocol: "https", hostname: "aaif.io" },
|
||||
{ protocol: "https", hostname: "trendshift.io" },
|
||||
{ protocol: "https", hostname: "api.producthunt.com" },
|
||||
{ protocol: "https", hostname: "svgl.app" },
|
||||
{ protocol: "https", hostname: "www.factory.ai" },
|
||||
{ protocol: "https", hostname: "kiro.dev" },
|
||||
{ protocol: "https", hostname: "continue.dev" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "agentmemory-website",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"gen-meta": "node scripts/gen-meta.mjs",
|
||||
"predev": "node scripts/gen-meta.mjs",
|
||||
"prebuild": "node scripts/gen-meta.mjs",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^16.2.7",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7"
|
||||
},
|
||||
"overrides": {
|
||||
"postcss": "^8.5.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.9.1",
|
||||
"@types/react": "^19.2.16",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 7.8 MiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,35 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#FF6B35"/>
|
||||
<stop offset="100%" stop-color="#FF8F5E"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<rect width="64" height="64" rx="14" fill="#1A1A1A"/>
|
||||
|
||||
<!-- 4 memory tiers -->
|
||||
<rect x="14" y="40" width="36" height="5" rx="2.5" fill="#333" opacity="0.5"/>
|
||||
<rect x="14" y="33" width="36" height="5" rx="2.5" fill="#444" opacity="0.6"/>
|
||||
<rect x="14" y="26" width="36" height="5" rx="2.5" fill="#555" opacity="0.7"/>
|
||||
<rect x="14" y="19" width="36" height="5" rx="2.5" fill="url(#g)"/>
|
||||
|
||||
<!-- Active nodes on hot layer -->
|
||||
<circle cx="22" cy="21.5" r="1.8" fill="#fff"/>
|
||||
<circle cx="32" cy="21.5" r="1.8" fill="#fff"/>
|
||||
<circle cx="42" cy="21.5" r="1.8" fill="#fff"/>
|
||||
|
||||
<!-- Retrieval lines converging up -->
|
||||
<line x1="22" y1="19" x2="32" y2="12" stroke="#FF6B35" stroke-width="1" opacity="0.7"/>
|
||||
<line x1="32" y1="19" x2="32" y2="12" stroke="#FF6B35" stroke-width="1" opacity="0.5"/>
|
||||
<line x1="42" y1="19" x2="32" y2="12" stroke="#FF6B35" stroke-width="1" opacity="0.7"/>
|
||||
|
||||
<!-- Retrieval point -->
|
||||
<circle cx="32" cy="11" r="3" fill="url(#g)"/>
|
||||
<circle cx="32" cy="11" r="5" fill="none" stroke="#FF6B35" stroke-width="0.8" opacity="0.3"/>
|
||||
|
||||
<!-- Fading dots on lower tiers -->
|
||||
<circle cx="25" cy="28.5" r="1" fill="#888" opacity="0.4"/>
|
||||
<circle cx="39" cy="28.5" r="1" fill="#888" opacity="0.4"/>
|
||||
<circle cx="32" cy="35.5" r="0.8" fill="#666" opacity="0.3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,59 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="glow" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#FF6B35"/>
|
||||
<stop offset="100%" stop-color="#FF8F5E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="fade" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#2A2A2A"/>
|
||||
<stop offset="100%" stop-color="#1A1A1A"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background circle -->
|
||||
<circle cx="60" cy="60" r="56" fill="url(#fade)" stroke="#333" stroke-width="1.5"/>
|
||||
|
||||
<!-- Memory layers (stacked rounded rects suggesting tiers) -->
|
||||
<rect x="30" y="68" width="60" height="8" rx="4" fill="#333" opacity="0.6"/>
|
||||
<rect x="30" y="56" width="60" height="8" rx="4" fill="#444" opacity="0.7"/>
|
||||
<rect x="30" y="44" width="60" height="8" rx="4" fill="#555" opacity="0.8"/>
|
||||
|
||||
<!-- Active/hot memory layer -->
|
||||
<rect x="30" y="32" width="60" height="8" rx="4" fill="url(#glow)"/>
|
||||
|
||||
<!-- Neural connection dots -->
|
||||
<circle cx="38" cy="36" r="2.5" fill="#fff" opacity="0.9"/>
|
||||
<circle cx="52" cy="36" r="2.5" fill="#fff" opacity="0.9"/>
|
||||
<circle cx="68" cy="36" r="2.5" fill="#fff" opacity="0.9"/>
|
||||
<circle cx="82" cy="36" r="2.5" fill="#fff" opacity="0.9"/>
|
||||
|
||||
<!-- Connection lines from hot layer upward (recall/retrieval) -->
|
||||
<line x1="38" y1="33" x2="48" y2="22" stroke="#FF6B35" stroke-width="1.2" opacity="0.7"/>
|
||||
<line x1="52" y1="33" x2="55" y2="20" stroke="#FF6B35" stroke-width="1.2" opacity="0.5"/>
|
||||
<line x1="68" y1="33" x2="65" y2="20" stroke="#FF6B35" stroke-width="1.2" opacity="0.5"/>
|
||||
<line x1="82" y1="33" x2="72" y2="22" stroke="#FF6B35" stroke-width="1.2" opacity="0.7"/>
|
||||
|
||||
<!-- Retrieval spark/node at top -->
|
||||
<circle cx="60" cy="18" r="4" fill="url(#glow)"/>
|
||||
<circle cx="60" cy="18" r="6" fill="none" stroke="#FF6B35" stroke-width="1" opacity="0.4"/>
|
||||
<circle cx="60" cy="18" r="9" fill="none" stroke="#FF6B35" stroke-width="0.5" opacity="0.2"/>
|
||||
|
||||
<!-- Connecting arcs to spark -->
|
||||
<line x1="48" y1="22" x2="60" y2="18" stroke="#FF6B35" stroke-width="1" opacity="0.6"/>
|
||||
<line x1="72" y1="22" x2="60" y2="18" stroke="#FF6B35" stroke-width="1" opacity="0.6"/>
|
||||
<line x1="55" y1="20" x2="60" y2="18" stroke="#FF6B35" stroke-width="0.8" opacity="0.4"/>
|
||||
<line x1="65" y1="20" x2="60" y2="18" stroke="#FF6B35" stroke-width="0.8" opacity="0.4"/>
|
||||
|
||||
<!-- Decay dots on lower layers (fading memories) -->
|
||||
<circle cx="42" cy="48" r="1.5" fill="#888" opacity="0.5"/>
|
||||
<circle cx="58" cy="48" r="1.5" fill="#888" opacity="0.5"/>
|
||||
<circle cx="74" cy="48" r="1.5" fill="#888" opacity="0.4"/>
|
||||
<circle cx="45" cy="60" r="1.2" fill="#666" opacity="0.3"/>
|
||||
<circle cx="65" cy="60" r="1.2" fill="#666" opacity="0.3"/>
|
||||
<circle cx="50" cy="72" r="1" fill="#555" opacity="0.2"/>
|
||||
<circle cx="70" cy="72" r="1" fill="#555" opacity="0.2"/>
|
||||
|
||||
<!-- Bottom text area indicator -->
|
||||
<text x="60" y="92" text-anchor="middle" font-family="Inter, system-ui, sans-serif" font-size="7.5" font-weight="800" fill="#FF6B35" letter-spacing="0.15em">AGENT</text>
|
||||
<text x="60" y="101" text-anchor="middle" font-family="Inter, system-ui, sans-serif" font-size="7.5" font-weight="400" fill="#999" letter-spacing="0.15em">MEMORY</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 469 KiB |
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build-time meta generator.
|
||||
*
|
||||
* Runs before `next build` (see package.json prebuild). Walks the real repo
|
||||
* (one level up from website/) and writes website/lib/generated-meta.json with
|
||||
* the version, MCP tool count, hook count, REST endpoint count, and test count.
|
||||
*
|
||||
* Reason this exists: meta.ts used to read package.json at runtime via
|
||||
* import.meta.url, but after Next.js compiles server components the URL
|
||||
* resolves into .next/server/ — ../.. stays inside the build cache, not at the
|
||||
* repo root, and the version silently falls back to "0.0.0". By resolving
|
||||
* files at build time from a known working directory (where this script
|
||||
* actually runs), we avoid the runtime path-guessing entirely.
|
||||
*/
|
||||
import { readFileSync, readdirSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const websiteDir = join(here, "..");
|
||||
const repoRoot = join(websiteDir, "..");
|
||||
|
||||
function readFileSafe(path) {
|
||||
try {
|
||||
return readFileSync(path, "utf-8");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function safeReadJson(path) {
|
||||
const txt = readFileSafe(path);
|
||||
if (!txt) return null;
|
||||
try {
|
||||
return JSON.parse(txt);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function safeCountMatches(path, pattern) {
|
||||
const txt = readFileSafe(path);
|
||||
if (!txt) return 0;
|
||||
const m = txt.match(pattern);
|
||||
return m ? m.length : 0;
|
||||
}
|
||||
|
||||
function countHookTypes(typesPath) {
|
||||
const txt = readFileSafe(typesPath);
|
||||
if (!txt) return 0;
|
||||
const union = txt.match(/export type HookType[\s\S]*?;/);
|
||||
if (!union) return 0;
|
||||
const body = union[0].replace(/export type HookType\s*=/, "").replace(/;$/, "");
|
||||
return body
|
||||
.split("|")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => /^["'`]/.test(s)).length;
|
||||
}
|
||||
|
||||
function countTestCases(testDir) {
|
||||
let total = 0;
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(testDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const full = join(testDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
total += countTestCases(full);
|
||||
continue;
|
||||
}
|
||||
if (!/\.test\.[jt]sx?$/.test(entry.name)) continue;
|
||||
const txt = readFileSafe(full);
|
||||
if (!txt) continue;
|
||||
const m = txt.match(/(?:^|\s)(?:it|test)(?:\.\w+)?\s*\(/g);
|
||||
if (m) total += m.length;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
const pkg = safeReadJson(join(repoRoot, "package.json"));
|
||||
const version = pkg?.version;
|
||||
if (!version) {
|
||||
throw new Error(
|
||||
`gen-meta: could not read version from ${join(repoRoot, "package.json")}. ` +
|
||||
`Check Vercel Root Directory — the full repo must be checked out, not just website/.`,
|
||||
);
|
||||
}
|
||||
|
||||
const restEndpoints = safeCountMatches(
|
||||
join(repoRoot, "src", "triggers", "api.ts"),
|
||||
/config:\s*\{\s*api_path:\s*"/g,
|
||||
);
|
||||
const mcpTools = safeCountMatches(
|
||||
join(repoRoot, "src", "mcp", "tools-registry.ts"),
|
||||
/name:\s*"memory_/g,
|
||||
);
|
||||
const hooks = countHookTypes(join(repoRoot, "src", "types.ts"));
|
||||
const testsPassing = countTestCases(join(repoRoot, "test"));
|
||||
|
||||
const meta = {
|
||||
version,
|
||||
mcpTools: mcpTools || 45,
|
||||
hooks: hooks || 12,
|
||||
restEndpoints: restEndpoints || 107,
|
||||
testsPassing: testsPassing || 794,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const outDir = join(websiteDir, "lib");
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const outPath = join(outDir, "generated-meta.json");
|
||||
writeFileSync(outPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
|
||||
|
||||
console.log(
|
||||
`[gen-meta] wrote ${outPath}: v${meta.version}, ${meta.mcpTools} tools, ${meta.hooks} hooks, ${meta.restEndpoints} endpoints, ${meta.testsPassing} tests`,
|
||||
);
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/docs",
|
||||
"destination": "https://agentmemory.mintlify.dev/docs"
|
||||
},
|
||||
{
|
||||
"source": "/docs/:match*",
|
||||
"destination": "https://agentmemory.mintlify.dev/docs/:match*"
|
||||
}
|
||||
]
|
||||
}
|
||||