Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b8067ce
chore: ignore local AI agent tooling files
Yangtze-Seventh Jun 24, 2026
b443770
fix(tokenizer): silence jieba SyntaxWarning on Python >= 3.12 (#304)
Yangtze-Seventh Jun 24, 2026
b46726e
feat(demo): cloud-backed interactive demo TUI (#305)
Yangtze-Seventh Jun 24, 2026
94d7112
feat(demo): local user, synced trace stages, Query<->Answer bar, reli…
Yangtze-Seventh Jun 25, 2026
bbd58bf
feat(demo): real-time signal rail, capabilities box, recall scoring, …
Yangtze-Seventh Jun 25, 2026
b300c23
fix(demo): compact bottom, distinct capabilities box, real recall user
Yangtze-Seventh Jun 25, 2026
09b3470
feat(demo): first-class slash commands (/help /replay /clear /quit)
Yangtze-Seventh Jun 25, 2026
5c342ea
feat(demo): /live command and live slash-command panel
Yangtze-Seventh Jun 25, 2026
ee2fb93
feat(demo): show real SOTA benchmark numbers in the strengths box
Yangtze-Seventh Jun 25, 2026
8a5b4f0
feat(demo): per-round token-saving estimate in recall lock
Yangtze-Seventh Jun 25, 2026
76c001f
feat(demo): fill strengths box with token-efficiency + single benchmark
Yangtze-Seventh Jun 25, 2026
d2e3ff4
Merge remote-tracking branch 'origin/main' into feat/demo-cloud-inter…
Yangtze-Seventh Jun 25, 2026
1fb40f5
docs(readme): sync demo section with slash commands and quit keys
Yangtze-Seventh Jun 25, 2026
f8089ba
feat(demo): connect to EverOS Cloud (api.evermind.ai) instead of self…
Yangtze-Seventh Jun 29, 2026
d396045
feat(demo): prepare Netlify relay deployment
Yangtze-Seventh Aug 6, 2026
40713ba
fix(demo): limit Netlify usage by daily rounds
Yangtze-Seventh Aug 6, 2026
af23dc8
fix(demo): remove inactive help command
Yangtze-Seventh Aug 6, 2026
c9bcb77
fix(demo): support Chinese IME input
Yangtze-Seventh Aug 7, 2026
f7d8f8b
fix(demo): use public relay domain
Yangtze-Seventh Aug 7, 2026
7554eab
feat(demo): morph memory sphere into thinking ring
Yangtze-Seventh Aug 7, 2026
a76674c
fix(demo): match thinking wave orb reference
Yangtze-Seventh Aug 7, 2026
a8e6b3b
revert(demo): restore original memory sphere
Yangtze-Seventh Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@ demo/memcell_outputs/

# macOS
.DS_Store

# Office temporary lock files
~$*

# AI agent tooling (local agent skills/config, not part of the project)
.agents/
AGENTS.md
*.duckdb


Expand Down
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,20 @@ Run this before configuring API keys or starting the server:
everos demo
```

The command asks for one memory and one recall question, then opens a
full-screen terminal UI. This is an educational visualizer: it is hardcoded,
local to the CLI, and does not connect to the EverOS server. Its job is to make
the memory lifecycle visible: conversation -> memory sphere -> recall -> source
proof -> confetti. See [docs/everos-demo.md](docs/everos-demo.md) for the demo
scope and TUI source layout.
The full-screen terminal UI has an input box: type a memory, then a recall
question, directly in the UI. Each round runs the real memory pipeline through
the hosted demo relay (the shared key stays server-side, so you need no local
keys), and the panels follow your own input:
conversation -> memory sphere ->
recall -> source proof -> confetti. See
[docs/everos-demo.md](docs/everos-demo.md) for the demo scope and TUI source
layout.

The sphere moves through ingest, extraction, indexing, recall, source reveal,
and a confetti burst after the first memory lands. Press `r` to replay and `q`
to quit.
and a confetti burst after the first memory lands. Type `/` to see the
commands (`/replay`, `/live`, `/quit`); `ctrl+c` exits anytime. After a few
rounds the demo points you at configuring your own keys (`everos init`, then
`everos demo --live`).

<p align="center">
<img src="https://gist.githubusercontent.com/cyfyifanchen/afa2cf40bf138a3ec96d917e8f2791a2/raw/d4ce82a6ddd7b3ebaf221e4825af993aeca5a7ce/everos-demo-tui-animation.svg" alt="Animated EverOS demo preview showing the memory sphere moving through recall and confetti states" width="720">
Expand Down
12 changes: 7 additions & 5 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,17 @@ uv pip install everos
everos demo
```

这个命令会询问一条记忆和一个召回问题,然后打开一个全屏 terminal UI。
这是一个 educational visualizer:它是 hardcoded 的,只在 CLI 本地运行,
不会连接 EverOS server。它的作用是把 memory lifecycle 变成可感知的过程:
全屏 terminal UI 里带有一个输入框:你直接在 UI 里输入一条记忆,再输入一个
召回问题。每一轮都会通过托管的 Demo 中转服务跑真实的
memory pipeline(共享 key 仅保存在服务端,本地无需 API key),各面板跟着
你自己的输入更新:
conversation -> memory sphere -> recall -> source proof -> confetti。Demo
范围和 TUI 代码结构见 [docs/everos-demo.md](docs/everos-demo.md)。

Sphere 会经历 ingest、extraction、indexing、recall、source reveal,
并在第一条记忆落地后进入 confetti successful moment。按 `r` 可以 replay,
按 `q` 可以退出。
并在第一条记忆落地后进入 confetti successful moment。输入 `/` 可以查看可用命令
(`/replay`、`/live`、`/quit`);`ctrl+c` 随时退出。几轮之后,demo 会引导你去
配置自己的 key(`everos init`,然后 `everos demo --live`)。

<p align="center">
<img src="https://gist.githubusercontent.com/cyfyifanchen/afa2cf40bf138a3ec96d917e8f2791a2/raw/d4ce82a6ddd7b3ebaf221e4825af993aeca5a7ce/everos-demo-tui-animation.svg" alt="Animated EverOS demo preview showing the memory sphere moving through recall and confetti states" width="720">
Expand Down
52 changes: 52 additions & 0 deletions deploy/netlify_relay/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# EverOS demo relay on Netlify

This project deploys the public `everos demo` relay as a Netlify Function. The
client sends no credentials. The function accepts only the demo API surface,
injects the platform key stored in Netlify, applies distributed per-IP quotas
with Upstash Redis, and forwards the request to EverOS Cloud.

## Required Netlify environment variables

Configure these under **Project configuration -> Environment variables**. Never
put their values in this repository.

| Variable | Required | Default |
| --- | --- | --- |
| `EVEROS_CLOUD_API_KEY` | yes | none |
| `UPSTASH_REDIS_REST_URL` | yes | none |
| `UPSTASH_REDIS_REST_TOKEN` | yes | none |
| `EVEROS_CLOUD_UPSTREAM` | no | `https://api.evermind.ai` |
| `RELAY_RATE_PER_MIN` | no | `30` |
| `RELAY_DAILY_ROUNDS` | no | `3` |
| `RELAY_UPSTREAM_TIMEOUT_MS` | no | `20000` |
| `RELAY_MAX_BODY_BYTES` | no | `1000000` |

The per-minute limit counts every relay request. The daily limit counts only
`POST /api/v1/memories`, which starts a demo round; task polling, flush, and
search requests do not spend additional rounds. The quota service fails closed:
demo API requests return `503` when Redis is not configured or unavailable.
`/healthz` remains available for diagnostics and reports only whether secrets
are configured, never their values.

## Deploy from GitHub

1. In Netlify, choose **Add new project -> Import an existing project -> GitHub**.
2. Select the EverOS fork and the `feat/demo-cloud-interactive` branch.
3. Set **Base directory** to `deploy/netlify_relay`.
4. Leave the build command empty. `netlify.toml` supplies the publish and
functions directories.
5. Add the required environment variables and deploy.
6. Verify `https://<site>.netlify.app/healthz` reports both configuration flags
as `true`.
7. Test the client with
`EVEROS_CLOUD_DEMO_URL=https://<site>.netlify.app everos demo`.
8. After validation, add `everosdemo.com` as the custom domain.

## Local checks

```bash
cd deploy/netlify_relay
npm run check
```

The test suite is credential-free and mocks both Redis and EverOS Cloud.
13 changes: 13 additions & 0 deletions deploy/netlify_relay/netlify.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[build]
publish = "public"
functions = "netlify/functions"

[build.environment]
NODE_VERSION = "22"

[[headers]]
for = "/*"
[headers.values]
X-Content-Type-Options = "nosniff"
Referrer-Policy = "no-referrer"
X-Frame-Options = "DENY"
202 changes: 202 additions & 0 deletions deploy/netlify_relay/netlify/functions/relay.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
const DEFAULT_UPSTREAM = "https://api.evermind.ai";
const DEFAULT_RATE_PER_MINUTE = 30;
const DEFAULT_DAILY_ROUNDS = 3;
const DEFAULT_TIMEOUT_MS = 20_000;
const DEFAULT_MAX_BODY_BYTES = 1_000_000;

const ALLOWED_EXACT = new Set([
"POST /api/v1/memories",
"POST /api/v1/memories/flush",
"POST /api/v1/memories/search",
]);
const TASK_PATH = /^\/api\/v1\/tasks\/[^/]+$/;

function readPositiveInteger(name, fallback) {
const value = Number.parseInt(process.env[name] ?? "", 10);
return Number.isFinite(value) && value > 0 ? value : fallback;
}

function jsonResponse(body, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json; charset=utf-8" },
});
}

export function isAllowed(method, path) {
if (ALLOWED_EXACT.has(`${method} ${path}`)) {
return true;
}
return method === "GET" && TASK_PATH.test(path);
}

function clientIp(request, context) {
return context?.ip || request.headers.get("x-nf-client-connection-ip") || "unknown";
}

async function hashClientIp(ip) {
const bytes = new TextEncoder().encode(ip);
const digest = await crypto.subtle.digest("SHA-256", bytes);
return Array.from(new Uint8Array(digest), (byte) =>
byte.toString(16).padStart(2, "0"),
)
.join("")
.slice(0, 24);
}

async function checkQuota(ip, countRound, now = Date.now()) {
const redisUrl = (process.env.UPSTASH_REDIS_REST_URL ?? "").replace(/\/$/, "");
const redisToken = process.env.UPSTASH_REDIS_REST_TOKEN ?? "";
if (!redisUrl || !redisToken) {
throw new Error("quota store is not configured");
}

const fingerprint = await hashClientIp(ip);
const minute = Math.floor(now / 60_000);
const day = new Date(now).toISOString().slice(0, 10);
const minuteKey = `everos-demo:minute:${fingerprint}:${minute}`;
const commands = [
["INCR", minuteKey],
["EXPIRE", minuteKey, 120],
];
if (countRound) {
const dailyKey = `everos-demo:rounds:${fingerprint}:${day}`;
commands.push(["INCR", dailyKey], ["EXPIRE", dailyKey, 172_800]);
}

const response = await fetch(`${redisUrl}/pipeline`, {
method: "POST",
headers: {
authorization: `Bearer ${redisToken}`,
"content-type": "application/json",
},
body: JSON.stringify(commands),
});
if (!response.ok) {
throw new Error("quota store request failed");
}

const results = await response.json();
if (!Array.isArray(results) || results.length !== commands.length) {
throw new Error("quota store returned an invalid response");
}
if (results.some((result) => result?.error)) {
throw new Error("quota store command failed");
}

const minuteCount = Number(results[0]?.result);
const dailyCount = countRound ? Number(results[2]?.result) : 0;
if (!Number.isFinite(minuteCount) || !Number.isFinite(dailyCount)) {
throw new Error("quota store returned invalid counters");
}

return {
minuteCount,
dailyCount,
minuteLimit: readPositiveInteger(
"RELAY_RATE_PER_MIN",
DEFAULT_RATE_PER_MINUTE,
),
dailyLimit: readPositiveInteger(
"RELAY_DAILY_ROUNDS",
DEFAULT_DAILY_ROUNDS,
),
};
}

async function forwardRequest(request, path, context) {
const apiKey = process.env.EVEROS_CLOUD_API_KEY ?? "";
if (!apiKey) {
return jsonResponse({ error: "relay API key is not configured" }, 500);
}

let quota;
try {
const countRound = request.method === "POST" && path === "/api/v1/memories";
quota = await checkQuota(clientIp(request, context), countRound);
} catch {
return jsonResponse({ error: "relay quota service is unavailable" }, 503);
}
if (quota.minuteCount > quota.minuteLimit) {
return jsonResponse({ error: "rate limit exceeded, slow down" }, 429);
}
if (quota.dailyCount > quota.dailyLimit) {
return jsonResponse(
{ error: "daily demo round limit reached, configure your own key" },
429,
);
}

const maxBodyBytes = readPositiveInteger(
"RELAY_MAX_BODY_BYTES",
DEFAULT_MAX_BODY_BYTES,
);
const body = request.method === "GET" ? null : await request.arrayBuffer();
if (body && body.byteLength > maxBodyBytes) {
return jsonResponse({ error: "request body is too large" }, 413);
}

const upstream = (process.env.EVEROS_CLOUD_UPSTREAM ?? DEFAULT_UPSTREAM).replace(
/\/$/,
"",
);
const incomingUrl = new URL(request.url);
const upstreamUrl = `${upstream}${path}${incomingUrl.search}`;
const timeoutMs = readPositiveInteger(
"RELAY_UPSTREAM_TIMEOUT_MS",
DEFAULT_TIMEOUT_MS,
);

let response;
try {
response = await fetch(upstreamUrl, {
method: request.method,
headers: {
accept: "application/json",
authorization: `Bearer ${apiKey}`,
"content-type": "application/json",
},
body,
signal: AbortSignal.timeout(timeoutMs),
});
} catch {
return jsonResponse({ error: "upstream unreachable" }, 502);
}

const headers = new Headers();
headers.set(
"content-type",
response.headers.get("content-type") ?? "application/json",
);
const retryAfter = response.headers.get("retry-after");
if (retryAfter) {
headers.set("retry-after", retryAfter);
}
return new Response(await response.arrayBuffer(), {
status: response.status,
headers,
});
}

export default async function relay(request, context) {
const path = new URL(request.url).pathname;
if (path === "/healthz") {
return jsonResponse({
ok: true,
upstream: process.env.EVEROS_CLOUD_UPSTREAM ?? DEFAULT_UPSTREAM,
key_configured: Boolean(process.env.EVEROS_CLOUD_API_KEY),
quota_configured: Boolean(
process.env.UPSTASH_REDIS_REST_URL &&
process.env.UPSTASH_REDIS_REST_TOKEN,
),
});
}
if (!isAllowed(request.method, path)) {
return jsonResponse({ error: "endpoint not allowed" }, 403);
}
return forwardRequest(request, path, context);
}

export const config = {
path: ["/healthz", "/api/v1/*"],
};
15 changes: 15 additions & 0 deletions deploy/netlify_relay/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions deploy/netlify_relay/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "everos-demo-netlify-relay",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"check": "node --check netlify/functions/relay.mjs && node --test tests/*.test.mjs",
"test": "node --test tests/*.test.mjs"
},
"engines": {
"node": ">=22"
}
}
16 changes: 16 additions & 0 deletions deploy/netlify_relay/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<title>EverOS Demo Relay</title>
</head>
<body>
<main>
<h1>EverOS Demo Relay</h1>
<p>This service powers the public <code>everos demo</code> experience.</p>
<p><a href="/healthz">Service health</a></p>
</main>
</body>
</html>
Loading