feat(cli): add EverOS demo TUI (#298)
* feat: add EverOS demo TUI * docs: add EverOS demo quick start * style(cli): elevate EverOS demo TUI * style(cli): align demo palette with poster * fix(cli): round demo sphere animation loop * fix(cli): refine EverOS demo TUI layout * fix(cli): use uniform demo sphere dots * fix(cli): refine EverOS demo sphere media * fix(docs): smooth EverOS demo animation * fix(docs): raise demo animation frame rate * docs(readme): note external demo animation hosting * fix(docs): optimize demo animation size * feat(cli): add demo confetti success state * feat(cli): make demo playable * test(cli): normalize demo help ansi output * docs(readme): streamline demo and roadmap sections * docs(readme): restore star history section * refactor(tui): move demo implementation out of cli
This commit is contained in:
parent
dbfe3483f5
commit
32cb22343d
243
README.md
243
README.md
|
|
@ -21,16 +21,12 @@
|
|||
|
||||
<br>
|
||||
|
||||
- [EverOS 1.0.0](#everos-100)
|
||||
- [Upcoming Features](#upcoming-features)
|
||||
- [Why Ever OS](#why-ever-os)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Use Cases](#use-cases)
|
||||
- [Architecture At A Glance](#architecture-at-a-glance)
|
||||
- [Storage Layout](#storage-layout)
|
||||
- [Features](#features)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Documentation](#documentation)
|
||||
- [Watch EverOS](#watch-everos)
|
||||
- [Star Us](#star-us)
|
||||
- [EverMind Ecosystems](#evermind-ecosystems)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
|
|
@ -39,19 +35,16 @@
|
|||
</details>
|
||||
|
||||
|
||||
## EverOS 1.0.0
|
||||
## Upcoming Features
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> **EverOS 1.0.0 is a major release for self-evolving memory.** It brings a
|
||||
> local-first runtime, Markdown as the source of truth, hybrid retrieval,
|
||||
> multimodal ingestion, user and agent memory scopes, and modular algorithms
|
||||
> through [EverAlgo](https://github.com/EverMind-AI/EverAlgo).
|
||||
> **Knowledge Wiki:** editable, source-backed Markdown knowledge pages built
|
||||
> from memory.
|
||||
>
|
||||
> **Coming next:** Knowledge Wiki will turn memory into editable,
|
||||
> source-backed Markdown knowledge pages. Reflection will run when the
|
||||
> system is idle or offline to connect signals, compress
|
||||
> history, and improve profiles and skills between sessions.
|
||||
> **Reflection:** idle-time and offline memory evolution that connects
|
||||
> signals, compresses history, and improves profiles and skills between
|
||||
> sessions.
|
||||
|
||||
<br>
|
||||
<div align="right">
|
||||
|
|
@ -63,11 +56,11 @@
|
|||
|
||||
## Why Ever OS
|
||||
|
||||
EverOS is the local memory operating system for agents and makers. It gives
|
||||
one portable memory layer across coding assistants, apps, devices, and
|
||||
workflows. Today it stores conversations, files, and agent trajectories as
|
||||
readable Markdown, then syncs local SQLite and LanceDB indexes for fast
|
||||
retrieval and self-evolving reuse.
|
||||
EverOS is a Python library and local-first memory runtime for agents and
|
||||
makers. It gives one portable memory layer across coding assistants, apps,
|
||||
devices, and workflows from day one. It stores conversations, files, and agent
|
||||
trajectories as readable Markdown, then syncs local SQLite and LanceDB indexes
|
||||
for fast retrieval and self-evolving reuse.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
|
|
@ -100,16 +93,6 @@ retrieval and self-evolving reuse.
|
|||
<td>✅ Search by <code>user_id</code>, <code>agent_id</code>, <code>app_id</code>, <code>project_id</code>, and <code>session_id</code></td>
|
||||
<td>❌ Usually app, namespace, tenant, thread, or graph scoped</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Knowledge Wiki</strong></td>
|
||||
<td>✅ Coming next: editable, source-backed Markdown knowledge pages built from memory</td>
|
||||
<td>❌ Usually retrieval, graph, dashboards, or generated summaries instead of editable source-backed pages</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Reflection</strong></td>
|
||||
<td>✅ Coming next: Reflection that runs when the system is idle or offline to connect signals, compress history, and improve profiles and skills between sessions</td>
|
||||
<td>❌ Usually online read/write APIs, retrieval records, or summaries rather than idle-time memory consolidation</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
|
@ -122,7 +105,8 @@ retrieval and self-evolving reuse.
|
|||
|
||||
## Quick Start
|
||||
|
||||
> Goal: start EverOS, write one memory, and search it back.
|
||||
> Goal: play with the memory visualizer first, then start EverOS, write one
|
||||
> real memory, and search it back.
|
||||
|
||||
### 0. Prerequisites
|
||||
|
||||
|
|
@ -138,7 +122,43 @@ uv pip install everos
|
|||
# or: pip install everos
|
||||
```
|
||||
|
||||
### 2. Configure
|
||||
### 2. Play With The Demo
|
||||
|
||||
Run this before configuring API keys or starting the server:
|
||||
|
||||
```bash
|
||||
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 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. The optimized animated preview is externally hosted so the repository
|
||||
stays light:
|
||||
|
||||
<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">
|
||||
</p>
|
||||
|
||||
For the looping showroom view used in README media, run:
|
||||
|
||||
```bash
|
||||
everos demo --cinematic
|
||||
```
|
||||
|
||||
If your shell is not interactive, or you want a copyable preview, use:
|
||||
|
||||
```bash
|
||||
everos demo --plain
|
||||
```
|
||||
|
||||
### 3. Configure
|
||||
|
||||
Generate a starter `.env` file, then fill the four API key slots shown in the
|
||||
generated comments. Only two distinct keys are needed with the defaults:
|
||||
|
|
@ -153,7 +173,7 @@ cp .env.example .env
|
|||
`everos init` writes `./.env` by default. Use `everos init --xdg` to
|
||||
write `${XDG_CONFIG_HOME:-~/.config}/everos/.env` instead.
|
||||
|
||||
### 3. Start EverOS
|
||||
### 4. Start EverOS
|
||||
|
||||
```bash
|
||||
everos server start
|
||||
|
|
@ -177,7 +197,7 @@ The endpoint stack is OpenAI-protocol compatible (OpenAI / OpenRouter / vLLM /
|
|||
Ollama / DeepInfra) - override `*__BASE_URL` in the generated `.env` to point
|
||||
at any of them.
|
||||
|
||||
### 4. Try Your First Memory
|
||||
### 5. Try Your First Memory
|
||||
|
||||
Add a tiny conversation:
|
||||
|
||||
|
|
@ -541,7 +561,7 @@ A context-native AI wearable that listens to everyday life and converts conversa
|
|||
|
||||
#### Legacy OpenClaw Agent Memory
|
||||
|
||||
Archived pre-1.0.0 plugin reference. New integrations should use the EverOS 1.0.0 API.
|
||||
Archived pre-1.0.0 plugin reference. New integrations should use the current EverOS API.
|
||||
|
||||
[Learn more](docs/migration-to-1.0.0.md)
|
||||
|
||||
|
|
@ -615,115 +635,15 @@ Explore stored entities and relationships in a graph interface. Frontend demo; b
|
|||
|
||||
</div>
|
||||
|
||||
## Architecture At A Glance
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────┐
|
||||
│ entrypoints/ (CLI + HTTP API) │ presentation
|
||||
├───────────────────────────────────────────────┤
|
||||
│ service/ (use cases: memorize/retrieve) │ application
|
||||
├───────────────────────────────────────────────┤
|
||||
│ memory/ (extract + search + cascade) │ domain
|
||||
├───────────────────────────────────────────────┤
|
||||
│ infra/ (markdown / sqlite / lancedb) │ infrastructure
|
||||
└───────────────────────────────────────────────┘
|
||||
↑ ↑
|
||||
component/ core/
|
||||
(LLM/Embedding) (observability/lifespan)
|
||||
```
|
||||
|
||||
DDD 5 layers, single-direction dependency. See [docs/architecture.md](docs/architecture.md).
|
||||
|
||||
<br>
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
## Storage Layout
|
||||
|
||||
```
|
||||
~/.everos/
|
||||
├── default_app/ # app_id ("default" → "default_app" on disk)
|
||||
│ └── default_project/ # project_id ("default" → "default_project")
|
||||
│ ├── users/<user_id>/
|
||||
│ │ ├── user.md # profile
|
||||
│ │ ├── episodes/ # daily-log episodes (visible)
|
||||
│ │ ├── .atomic_facts/ # nested facts (dotfile-hidden)
|
||||
│ │ └── .foresights/ # predictive memory (dotfile-hidden)
|
||||
│ └── agents/<agent_id>/
|
||||
│ ├── agent.md
|
||||
│ ├── .cases/ # one task case per entry
|
||||
│ └── skills/ # named procedural memories
|
||||
├── .index/ # derived indexes (rebuildable from md)
|
||||
│ ├── sqlite/system.db # state + queue + audit
|
||||
│ └── lancedb/*.lance/ # vector + BM25 + scalar
|
||||
└── .tmp/ # transient working files
|
||||
```
|
||||
|
||||
Open any `<app>/<project>/users/<user_id>/` folder in Obsidian — your
|
||||
agent's brain is just files. The dotfile directories (`.atomic_facts/`,
|
||||
`.foresights/`, `.cases/`) stay hidden by default so the visible folder
|
||||
is the user-facing memory surface, while extracted derivatives sit
|
||||
quietly alongside.
|
||||
|
||||
<br>
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
## Features
|
||||
|
||||
- **Hybrid retrieval**: BM25 + cosine vector ANN + scalar filters, backed by LanceDB
|
||||
- **Cascade index sync**: edit a `.md` → file watcher → entry-level diff → LanceDB sync, sub-second
|
||||
- **Multi-source extraction**: conversations / agent trajectories / file knowledge
|
||||
- **Dual-track memory**: user-track (Episodes / Profiles) + agent-track (Cases / Skills)
|
||||
- **Async-first**: full asyncio, single event loop
|
||||
- **Multi-modal**: text + small image / audio inline; large media via S3/OSS reference
|
||||
|
||||
<br>
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
everos/ # repo root
|
||||
├── src/everos/ # main package (src layout)
|
||||
│ ├── entrypoints/ # cli + api
|
||||
│ ├── service/ # use case orchestration
|
||||
│ ├── memory/ # domain: extract + search + cascade + prompt_slots
|
||||
│ ├── infra/ # storage: markdown + lancedb + sqlite
|
||||
│ ├── component/ # cross-cutting: llm / embedding / config / utils
|
||||
│ ├── core/ # runtime: observability / lifespan / context
|
||||
│ └── config/ # configuration data + Settings schema
|
||||
├── tests/ # unit / integration / golden / fixtures
|
||||
├── docs/ # design docs
|
||||
└── .claude/ # team-shared rules + skills (auto-loaded by Claude Code)
|
||||
```
|
||||
<br>
|
||||
<div align="right">
|
||||
|
||||
[](#readme-top)
|
||||
|
||||
</div>
|
||||
|
||||
## Documentation
|
||||
|
||||
- [docs/overview.md](docs/overview.md) — Project overview & vision
|
||||
- [docs/architecture.md](docs/architecture.md) — DDD layered architecture & dependency rules
|
||||
- [docs/engineering.md](docs/engineering.md) — Engineering & dev-efficiency infrastructure (CI / tooling / Claude Code)
|
||||
- [docs/everos-demo.md](docs/everos-demo.md) — Demo scope and TUI source layout
|
||||
- [docs/how-memory-works.md](docs/how-memory-works.md) — Markdown, SQLite, LanceDB, and recall flow
|
||||
- [docs/use-cases.md](docs/use-cases.md) — Full use-case gallery and integration examples
|
||||
- [docs/migration-to-1.0.0.md](docs/migration-to-1.0.0.md) — Legacy API and infrastructure migration notes
|
||||
- [docs/engineering.md](docs/engineering.md) — Engineering and CI tooling
|
||||
- [docs/migration-to-1.0.0.md](docs/migration-to-1.0.0.md) — Legacy API migration notes
|
||||
- [CHANGELOG.md](CHANGELOG.md) — Release notes
|
||||
- [CONTRIBUTING.md](CONTRIBUTING.md) — How to contribute
|
||||
- [.claude/rules/](.claude/rules/) — Detailed coding conventions (auto-loaded by Claude Code)
|
||||
|
||||
<br>
|
||||
<div align="right">
|
||||
|
|
@ -732,42 +652,11 @@ everos/ # repo root
|
|||
|
||||
</div>
|
||||
|
||||
## Star Us
|
||||
|
||||
|
||||
## Watch EverOS
|
||||
|
||||
EverOS 1.0.0 is the first release of a larger memory-system roadmap.
|
||||
Watch this repository for upcoming work on deeper idle-time and offline evolution,
|
||||
benchmark releases, and more real-world agent integrations.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
<strong>Knowledge Wiki</strong><br>
|
||||
<br>
|
||||
Turns scattered episodes, files, facts, and agent traces into source-backed
|
||||
Markdown pages for people, projects, topics, decisions, and workflows. Memory
|
||||
becomes something users can read, correct, link, version, and open in their
|
||||
existing Markdown tools.
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
<strong>Reflection</strong><br>
|
||||
<br>
|
||||
Runs when the system is idle or offline to revisit stored memory, connect weak
|
||||
signals, compress noisy history into durable patterns, and improve profiles and
|
||||
skills. The agent gets better between active sessions, not only while you prompt
|
||||
it.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
Most memory systems stop at chat history, opaque profiles, or vector recall.
|
||||
EverOS keeps memory local, Markdown-native, auditable, and self-evolving: raw
|
||||
memory stays readable, derived knowledge becomes a wiki, and Reflection turns
|
||||
repeated experience into more useful long-term behavior.
|
||||
|
||||
If EverOS is useful to your agent stack, starring the repo helps more
|
||||
builders discover it.
|
||||
If EverOS is useful to your agent stack, please star the repo. It helps more
|
||||
builders discover the project and gives the memory ecosystem a stronger signal
|
||||
to keep improving.
|
||||
|
||||
### Star History
|
||||
|
||||
|
|
@ -789,7 +678,7 @@ EverMind is an open-source ecosystem for long-term memory, self-evolving agents,
|
|||
<th colspan="2">EverMind Open-Source Ecosystem</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Core Memory Architecture</strong></td>
|
||||
<td><strong>Memory Runtime</strong></td>
|
||||
<td><a href="https://github.com/EverMind-AI/EverOS">EverOS</a> - the local memory operating system and research-backed runtime for agent and user memory.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -797,7 +686,7 @@ EverMind is an open-source ecosystem for long-term memory, self-evolving agents,
|
|||
<td><a href="https://github.com/EverMind-AI/EverAlgo">EverAlgo</a> - stateless extraction, ranking, parsing, and memory operators that power EverOS.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Alternative Architecture</strong></td>
|
||||
<td><strong>Hypergraph Memory</strong></td>
|
||||
<td><a href="https://github.com/EverMind-AI/HyperMem">HyperMem</a> - hypergraph memory for long-term conversations, with its own benchmark-backed topic -> episode -> fact retrieval method.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
@ -831,7 +720,7 @@ Together, these repositories form EverMind's research-to-runtime stack: new memo
|
|||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome across the whole repository: architecture methods, benchmark coverage, use-case examples, documentation, and bug fixes. Browse [Issues](https://github.com/EverMind-AI/EverOS/issues) to find a good entry point, then open a PR when you are ready.
|
||||
Contributions are welcome across the whole repository: memory methods, benchmark coverage, use-case examples, documentation, and bug fixes. Browse [Issues](https://github.com/EverMind-AI/EverOS/issues) to find a good entry point, then open a PR when you are ready.
|
||||
|
||||
<br>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
# EverOS Demo
|
||||
|
||||
`everos demo` is a local educational TUI. It helps new users feel the memory
|
||||
lifecycle before they configure API keys, start the server, or write real
|
||||
memory through the API.
|
||||
|
||||
## Run It
|
||||
|
||||
```bash
|
||||
everos demo
|
||||
```
|
||||
|
||||
The command asks for one memory and one recall question, then opens a
|
||||
full-screen terminal UI. The visual flow is deterministic and local to the CLI:
|
||||
conversation -> memory sphere -> recall -> source proof -> confetti.
|
||||
|
||||
For non-interactive shells or a copyable preview, use:
|
||||
|
||||
```bash
|
||||
everos demo --plain
|
||||
```
|
||||
|
||||
For the looping showroom view used by README media, use:
|
||||
|
||||
```bash
|
||||
everos demo --cinematic
|
||||
```
|
||||
|
||||
## What It Does Not Do
|
||||
|
||||
The demo does not connect to the EverOS server, call LLM providers, or write
|
||||
production memory files. It is intentionally hardcoded so users can try the
|
||||
experience before configuring the full runtime.
|
||||
|
||||
## Source Layout
|
||||
|
||||
The CLI command adapter stays under `src/everos/entrypoints/cli/commands/demo.py`
|
||||
because the public command is still `everos demo`.
|
||||
|
||||
The TUI implementation lives under `src/everos/entrypoints/tui/demo/`:
|
||||
|
||||
- `app.py` renders the Textual app.
|
||||
- `data.py` builds the deterministic demo story.
|
||||
- `widgets/sphere.py` builds the memory sphere frames.
|
||||
- `readme_media.py` renders README media.
|
||||
|
||||
To regenerate README media locally:
|
||||
|
||||
```bash
|
||||
uv run python -m everos.entrypoints.tui.demo.readme_media --out-dir /tmp/everos-demo-media
|
||||
```
|
||||
|
|
@ -47,6 +47,7 @@ dependencies = [
|
|||
|
||||
# CLI
|
||||
"typer>=0.12.0",
|
||||
"textual>=8.2.7",
|
||||
|
||||
# Tokenization (BM25 Chinese support)
|
||||
"jieba==0.42.1",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
"""``everos demo`` — first-run memory sphere demo."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
from everos.entrypoints.tui.demo.data import (
|
||||
DEFAULT_MEMORY_SEED,
|
||||
DEFAULT_QUERY,
|
||||
DemoStory,
|
||||
build_demo_story,
|
||||
default_demo_story,
|
||||
)
|
||||
from everos.entrypoints.tui.demo.widgets.sphere import (
|
||||
EVEROS_GREEN,
|
||||
EVEROS_YELLOW,
|
||||
build_dot_sphere,
|
||||
render_dot_sphere_text,
|
||||
)
|
||||
|
||||
|
||||
def register(parent: typer.Typer) -> None:
|
||||
"""Attach the ``demo`` command to the root CLI app."""
|
||||
|
||||
@parent.command("demo")
|
||||
def demo(
|
||||
plain: bool = typer.Option(
|
||||
False,
|
||||
"--plain",
|
||||
help="Print a static terminal preview instead of launching the TUI.",
|
||||
),
|
||||
cinematic: bool = typer.Option(
|
||||
False,
|
||||
"--cinematic",
|
||||
help="Skip prompts and launch the looping README-style demo.",
|
||||
),
|
||||
) -> None:
|
||||
"""Launch the EverOS first-memory Textual TUI."""
|
||||
if plain or not sys.stdout.isatty():
|
||||
_print_plain_demo()
|
||||
return
|
||||
|
||||
_run_interactive_demo(cinematic=cinematic)
|
||||
|
||||
|
||||
def _run_interactive_demo(*, cinematic: bool) -> None:
|
||||
run_demo_tui = _load_run_demo_tui()
|
||||
story = None if cinematic else _collect_playable_story()
|
||||
run_demo_tui(story=story)
|
||||
|
||||
|
||||
def _load_run_demo_tui():
|
||||
try:
|
||||
from everos.entrypoints.tui.demo.app import run_demo_tui
|
||||
except ModuleNotFoundError as exc:
|
||||
if exc.name != "textual":
|
||||
raise
|
||||
typer.secho(
|
||||
"error: Textual is required for `everos demo`; install the "
|
||||
"package with TUI dependencies or run `everos demo --plain`.",
|
||||
fg=typer.colors.RED,
|
||||
err=True,
|
||||
)
|
||||
raise typer.Exit(code=1) from exc
|
||||
|
||||
return run_demo_tui
|
||||
|
||||
|
||||
def _collect_playable_story() -> DemoStory:
|
||||
Console().print(
|
||||
f"[bold {EVEROS_YELLOW}]EverOS demo[/] "
|
||||
"Give it one memory, then ask for it back."
|
||||
)
|
||||
memory = typer.prompt(
|
||||
"Give EverOS one thing to remember",
|
||||
default=DEFAULT_MEMORY_SEED,
|
||||
)
|
||||
query = typer.prompt(
|
||||
"Ask EverOS to recall it",
|
||||
default=DEFAULT_QUERY,
|
||||
)
|
||||
return build_demo_story(memory, query)
|
||||
|
||||
|
||||
def _print_plain_demo(story: DemoStory | None = None) -> None:
|
||||
story = story or default_demo_story()
|
||||
console = Console()
|
||||
frame = build_dot_sphere(
|
||||
width=57,
|
||||
height=23,
|
||||
phase=0.18,
|
||||
state_key="remembered",
|
||||
)
|
||||
console.print(
|
||||
Panel(
|
||||
render_dot_sphere_text(frame),
|
||||
title="EverOS Memory Sphere",
|
||||
border_style=EVEROS_YELLOW,
|
||||
)
|
||||
)
|
||||
console.print(f"[bold {EVEROS_GREEN}]EverOS remembered:[/]")
|
||||
console.print(story.memory)
|
||||
console.print()
|
||||
console.print(f"[bold {EVEROS_YELLOW}]Source:[/] {story.source_filename}")
|
||||
|
|
@ -13,7 +13,7 @@ from __future__ import annotations
|
|||
|
||||
import typer
|
||||
|
||||
from .commands import cascade, init_cmd, server
|
||||
from .commands import cascade, demo, init_cmd, server
|
||||
|
||||
app = typer.Typer(
|
||||
name="everos",
|
||||
|
|
@ -28,6 +28,7 @@ app.add_typer(cascade.app, name="cascade")
|
|||
# ``init`` is a top-level leaf command (not a Typer group) — match the
|
||||
# idiomatic ``alembic init`` / ``django-admin startproject`` shape.
|
||||
init_cmd.register(app)
|
||||
demo.register(app)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
"""Terminal UI entrypoints."""
|
||||
|
|
@ -0,0 +1 @@
|
|||
"""EverOS educational demo TUI."""
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
"""Textual TUI for ``everos demo``."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from rich.text import Text
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.timer import Timer
|
||||
from textual.widgets import Footer, Static
|
||||
|
||||
from everos.entrypoints.tui.demo.data import DemoStory, default_demo_story
|
||||
from everos.entrypoints.tui.demo.widgets.sphere import (
|
||||
EVEROS_AMBER,
|
||||
EVEROS_AMBER_DIM,
|
||||
EVEROS_CYAN,
|
||||
EVEROS_GREEN,
|
||||
EVEROS_ORANGE,
|
||||
EVEROS_YELLOW,
|
||||
EVEROS_YELLOW_SOFT,
|
||||
build_dot_sphere,
|
||||
render_dot_sphere_text,
|
||||
)
|
||||
|
||||
EVEROS_BLACK = "#1D1C18"
|
||||
EVEROS_SURFACE = "#24231E"
|
||||
EVEROS_SURFACE_RAISED = "#31302B"
|
||||
EVEROS_INK = "#F5EDDC"
|
||||
EVEROS_MUTED = "#918C80"
|
||||
EVEROS_BORDER = "#5A5549"
|
||||
SPHERE_FRAME_WIDTH = 37
|
||||
SPHERE_FRAME_HEIGHT = 17
|
||||
TERMINAL_CELL_HEIGHT_RATIO = 2.0
|
||||
SIGNAL_RAIL_SOURCE_WIDTH = 18
|
||||
|
||||
|
||||
class DotSphereWidget(Static):
|
||||
"""Animated dot sphere that represents EverOS memory activity."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
DotSphereWidget {
|
||||
height: 1fr;
|
||||
content-align: center middle;
|
||||
}
|
||||
"""
|
||||
|
||||
STATES = (
|
||||
"booting",
|
||||
"ingesting",
|
||||
"extracting",
|
||||
"indexing",
|
||||
"recalling",
|
||||
"remembered",
|
||||
"source",
|
||||
"celebrating",
|
||||
)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._phase = 0.0
|
||||
self._tick = 0
|
||||
self._animation_timer: Timer | None = None
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self._animation_timer = self.set_interval(1 / 12, self._advance)
|
||||
self._advance()
|
||||
|
||||
def pause_animation(self) -> None:
|
||||
if self._animation_timer is not None:
|
||||
self._animation_timer.pause()
|
||||
|
||||
def _advance(self) -> None:
|
||||
self._phase = (self._phase + 0.025) % 1.0
|
||||
self._tick += 1
|
||||
state = self.STATES[(self._tick // 36) % len(self.STATES)]
|
||||
frame = build_dot_sphere(
|
||||
width=SPHERE_FRAME_WIDTH,
|
||||
height=SPHERE_FRAME_HEIGHT,
|
||||
phase=self._phase,
|
||||
state_key=state,
|
||||
)
|
||||
self.update(render_dot_sphere_text(frame))
|
||||
|
||||
|
||||
class EverOSDemoApp(App[None]):
|
||||
"""Fullscreen first-run demo cockpit."""
|
||||
|
||||
TITLE = "EverOS Memory Core"
|
||||
SUB_TITLE = "dot sphere demo"
|
||||
BINDINGS = [
|
||||
("q", "quit", "Quit"),
|
||||
("r", "replay", "Replay"),
|
||||
]
|
||||
|
||||
CSS = f"""
|
||||
Screen {{
|
||||
background: {EVEROS_BLACK};
|
||||
color: {EVEROS_INK};
|
||||
}}
|
||||
|
||||
#shell {{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 1 2;
|
||||
border: round {EVEROS_BORDER};
|
||||
}}
|
||||
|
||||
#command-strip {{
|
||||
height: 2;
|
||||
padding: 0 1;
|
||||
color: {EVEROS_INK};
|
||||
content-align: left middle;
|
||||
}}
|
||||
|
||||
#main {{
|
||||
height: 1fr;
|
||||
margin-top: 1;
|
||||
}}
|
||||
|
||||
#memory-field {{
|
||||
width: 1fr;
|
||||
border: round {EVEROS_AMBER};
|
||||
background: {EVEROS_SURFACE};
|
||||
padding: 0 2;
|
||||
}}
|
||||
|
||||
#field-header {{
|
||||
height: 2;
|
||||
content-align: left middle;
|
||||
}}
|
||||
|
||||
#field-answer {{
|
||||
height: 2;
|
||||
border-top: hkey {EVEROS_AMBER_DIM};
|
||||
background: {EVEROS_SURFACE_RAISED};
|
||||
padding: 0 1;
|
||||
}}
|
||||
|
||||
#signal-rail {{
|
||||
width: 48;
|
||||
height: 100%;
|
||||
margin-left: 1;
|
||||
border: round {EVEROS_AMBER};
|
||||
background: {EVEROS_SURFACE};
|
||||
padding: 1 2;
|
||||
}}
|
||||
|
||||
#provenance-strip {{
|
||||
height: 6;
|
||||
margin-top: 1;
|
||||
}}
|
||||
|
||||
#source-lock {{
|
||||
width: 1fr;
|
||||
border: round {EVEROS_CYAN};
|
||||
background: {EVEROS_SURFACE};
|
||||
padding: 0 2;
|
||||
margin-right: 1;
|
||||
}}
|
||||
|
||||
#recall-lock {{
|
||||
width: 54;
|
||||
border: round {EVEROS_GREEN};
|
||||
background: {EVEROS_SURFACE};
|
||||
padding: 0 2;
|
||||
}}
|
||||
|
||||
#payoff {{
|
||||
height: 2;
|
||||
border-top: hkey {EVEROS_YELLOW};
|
||||
background: {EVEROS_SURFACE};
|
||||
color: {EVEROS_INK};
|
||||
padding: 0 1;
|
||||
margin-top: 1;
|
||||
content-align: left middle;
|
||||
}}
|
||||
|
||||
Footer {{
|
||||
background: {EVEROS_BLACK};
|
||||
color: {EVEROS_MUTED};
|
||||
}}
|
||||
|
||||
FooterKey {{
|
||||
background: {EVEROS_BLACK};
|
||||
}}
|
||||
|
||||
FooterKey > .footer-key--key {{
|
||||
color: {EVEROS_BLACK};
|
||||
background: {EVEROS_YELLOW};
|
||||
text-style: bold;
|
||||
}}
|
||||
|
||||
FooterKey > .footer-key--description {{
|
||||
color: {EVEROS_INK};
|
||||
background: {EVEROS_BLACK};
|
||||
}}
|
||||
"""
|
||||
|
||||
def __init__(self, *, story: DemoStory | None = None) -> None:
|
||||
super().__init__()
|
||||
self._story = story or default_demo_story()
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="shell"):
|
||||
yield Static(_hero_text(), id="command-strip")
|
||||
with Horizontal(id="main"):
|
||||
memory_field = Vertical(id="memory-field")
|
||||
memory_field.border_title = "memory field"
|
||||
with memory_field:
|
||||
yield Static(_field_header_text(self._story), id="field-header")
|
||||
yield DotSphereWidget()
|
||||
yield Static(_sphere_caption(self._story), id="field-answer")
|
||||
signal_rail = Static(_signal_rail_text(self._story), id="signal-rail")
|
||||
signal_rail.border_title = "signal rail"
|
||||
yield signal_rail
|
||||
with Horizontal(id="provenance-strip"):
|
||||
source_lock = Static(_source_tree_text(self._story), id="source-lock")
|
||||
source_lock.border_title = "source lock"
|
||||
yield source_lock
|
||||
recall_lock = Static(_recall_proof_text(self._story), id="recall-lock")
|
||||
recall_lock.border_title = "recall lock"
|
||||
yield recall_lock
|
||||
yield Static(_payoff_text(self._story), id="payoff")
|
||||
yield Footer(show_command_palette=False)
|
||||
|
||||
def action_replay(self) -> None:
|
||||
widget = self.query_one(DotSphereWidget)
|
||||
widget._tick = 0
|
||||
widget._phase = 0.0
|
||||
widget._advance()
|
||||
|
||||
|
||||
def run_demo_tui(*, story: DemoStory | None = None) -> None:
|
||||
EverOSDemoApp(story=story).run()
|
||||
|
||||
|
||||
def _hero_text() -> Text:
|
||||
return Text.assemble(
|
||||
(" everos demo ", f"bold black on {EVEROS_YELLOW}"),
|
||||
(" memory core ", f"bold {EVEROS_YELLOW}"),
|
||||
("online", EVEROS_MUTED),
|
||||
)
|
||||
|
||||
|
||||
def _field_header_text(story: DemoStory | None = None) -> Text:
|
||||
story = story or default_demo_story()
|
||||
return Text.assemble(
|
||||
(f"user={story.owner}", f"bold {EVEROS_INK}"),
|
||||
(" scope=local-first", f"bold {EVEROS_YELLOW_SOFT}"),
|
||||
(" trace ", EVEROS_MUTED),
|
||||
("conversation -> facts -> index", f"bold {EVEROS_YELLOW}"),
|
||||
(" live", f"bold {EVEROS_ORANGE}"),
|
||||
)
|
||||
|
||||
|
||||
def _sphere_caption(story: DemoStory | None = None) -> Text:
|
||||
story = story or default_demo_story()
|
||||
return Text.assemble(
|
||||
("query ", f"bold {EVEROS_CYAN}"),
|
||||
(f"{story.query} ", EVEROS_INK),
|
||||
("-> ", EVEROS_MUTED),
|
||||
("answer ", f"bold {EVEROS_GREEN}"),
|
||||
(story.answer, f"bold {EVEROS_GREEN}"),
|
||||
)
|
||||
|
||||
|
||||
def _signal_rail_text(story: DemoStory | None = None) -> Text:
|
||||
story = story or default_demo_story()
|
||||
return Text.assemble(
|
||||
("● ", f"bold {EVEROS_GREEN}"),
|
||||
("memory core ", EVEROS_INK),
|
||||
("ready\n", f"bold {EVEROS_GREEN}"),
|
||||
("● ", f"bold {EVEROS_YELLOW_SOFT}"),
|
||||
("conversation ", EVEROS_INK),
|
||||
("captured\n", f"bold {EVEROS_YELLOW_SOFT}"),
|
||||
("● ", f"bold {EVEROS_ORANGE}"),
|
||||
("episode -> facts ", EVEROS_INK),
|
||||
("live\n", f"bold {EVEROS_ORANGE}"),
|
||||
("● ", f"bold {EVEROS_CYAN}"),
|
||||
("SQLite + LanceDB ", EVEROS_INK),
|
||||
("synced\n", f"bold {EVEROS_CYAN}"),
|
||||
("● ", f"bold {EVEROS_GREEN}"),
|
||||
("memory recall ", EVEROS_INK),
|
||||
("hit\n", f"bold {EVEROS_GREEN}"),
|
||||
("\nsource route\n", EVEROS_MUTED),
|
||||
(_rail_cell(story.source_filename), EVEROS_INK),
|
||||
(" attached\n", f"bold {EVEROS_YELLOW_SOFT}"),
|
||||
(_rail_cell(story.fact_filename), EVEROS_INK),
|
||||
(" 7 nodes\n", f"bold {EVEROS_ORANGE}"),
|
||||
("lancedb orbit ", EVEROS_INK),
|
||||
("synced\n", f"bold {EVEROS_CYAN}"),
|
||||
("\nrecall proof\n", EVEROS_MUTED),
|
||||
("score ", EVEROS_INK),
|
||||
("0.628\n", f"bold {EVEROS_GREEN}"),
|
||||
("source ", EVEROS_INK),
|
||||
(f"{story.source_filename}\n", f"bold {EVEROS_CYAN}"),
|
||||
("field integrity\n", EVEROS_MUTED),
|
||||
("█████████░ 92%\n", f"bold {EVEROS_YELLOW}"),
|
||||
("latency ", EVEROS_MUTED),
|
||||
("42 ms\n", f"bold {EVEROS_GREEN}"),
|
||||
("mode ", EVEROS_MUTED),
|
||||
("local-first", f"bold {EVEROS_INK}"),
|
||||
)
|
||||
|
||||
|
||||
def _rail_cell(value: str, *, width: int = SIGNAL_RAIL_SOURCE_WIDTH) -> str:
|
||||
if len(value) > width:
|
||||
return f"{value[: width - 3]}..."
|
||||
return f"{value:<{width}}"
|
||||
|
||||
|
||||
def _source_tree_text(story: DemoStory | None = None) -> Text:
|
||||
story = story or default_demo_story()
|
||||
return Text.assemble(
|
||||
("episode ", EVEROS_MUTED),
|
||||
(f"{story.source_filename}\n", f"bold {EVEROS_YELLOW_SOFT}"),
|
||||
("facts ", EVEROS_MUTED),
|
||||
(f"{story.fact_filename}\n", f"bold {EVEROS_ORANGE}"),
|
||||
("index ", EVEROS_MUTED),
|
||||
("sqlite/system.db + lancedb/*.lance\n", EVEROS_CYAN),
|
||||
("root ", EVEROS_MUTED),
|
||||
("~/.everos/default_app/default_project", EVEROS_INK),
|
||||
)
|
||||
|
||||
|
||||
def _recall_proof_text(story: DemoStory | None = None) -> Text:
|
||||
story = story or default_demo_story()
|
||||
return Text.assemble(
|
||||
("score ", EVEROS_MUTED),
|
||||
("0.628\n", f"bold {EVEROS_GREEN}"),
|
||||
("scope ", EVEROS_MUTED),
|
||||
(f"user={story.owner} project=default\n", EVEROS_INK),
|
||||
("answer ", EVEROS_MUTED),
|
||||
(story.answer, f"bold {EVEROS_YELLOW}"),
|
||||
)
|
||||
|
||||
|
||||
def _payoff_text(story: DemoStory | None = None) -> Text:
|
||||
story = story or default_demo_story()
|
||||
return Text.assemble(
|
||||
("memory formed: ", f"bold {EVEROS_YELLOW}"),
|
||||
(
|
||||
f"EverOS recalled {story.answer} and kept the source attached.",
|
||||
f"bold {EVEROS_INK}",
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
"""Scripted story data for the educational ``everos demo`` flow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
DEFAULT_MEMORY_SEED = "I love climbing in Yosemite every spring."
|
||||
DEFAULT_QUERY = "Where do I like to climb?"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DemoStory:
|
||||
"""Small, deterministic story rendered by the demo TUI."""
|
||||
|
||||
owner: str
|
||||
memory: str
|
||||
query: str
|
||||
answer: str
|
||||
source_filename: str
|
||||
fact_filename: str
|
||||
|
||||
|
||||
def default_demo_story() -> DemoStory:
|
||||
"""Return the cinematic story used by README media and no-prompt previews."""
|
||||
|
||||
return DemoStory(
|
||||
owner="alice",
|
||||
memory=DEFAULT_MEMORY_SEED,
|
||||
query="Where does Alice like to climb?",
|
||||
answer="Yosemite every spring",
|
||||
source_filename="episode-2026-06-20.md",
|
||||
fact_filename="atomic_fact-2026-06-20.md",
|
||||
)
|
||||
|
||||
|
||||
def build_demo_story(
|
||||
memory_seed: str | None = None,
|
||||
query: str | None = None,
|
||||
) -> DemoStory:
|
||||
"""Build a playable demo story from one user memory and one recall query."""
|
||||
|
||||
memory = _clean(memory_seed, DEFAULT_MEMORY_SEED)
|
||||
recall_query = _clean(query, DEFAULT_QUERY)
|
||||
return DemoStory(
|
||||
owner="you",
|
||||
memory=memory,
|
||||
query=recall_query,
|
||||
answer=_derive_demo_answer(memory),
|
||||
source_filename="episode-demo.md",
|
||||
fact_filename="atomic_fact-demo.md",
|
||||
)
|
||||
|
||||
|
||||
def _clean(value: str | None, fallback: str) -> str:
|
||||
if value is None:
|
||||
return fallback
|
||||
stripped = value.strip()
|
||||
return stripped or fallback
|
||||
|
||||
|
||||
def _derive_demo_answer(memory: str) -> str:
|
||||
"""Keep the demo deterministic without pretending to run the server."""
|
||||
|
||||
lower_memory = memory.lower()
|
||||
if "yosemite" in lower_memory:
|
||||
if "spring" in lower_memory:
|
||||
return "Yosemite every spring"
|
||||
return "Yosemite"
|
||||
return _compact(memory)
|
||||
|
||||
|
||||
def _compact(text: str, *, limit: int = 66) -> str:
|
||||
if len(text) <= limit:
|
||||
return text
|
||||
return f"{text[: limit - 3].rstrip()}..."
|
||||
|
|
@ -0,0 +1,267 @@
|
|||
"""Render README media for the ``everos demo`` TUI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import base64
|
||||
import html
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import anyio
|
||||
from rich.console import Console
|
||||
from rich.terminal_theme import TerminalTheme
|
||||
|
||||
TERMINAL_SIZE = (150, 48)
|
||||
ANIMATION_TERMINAL_SIZE = (132, 42)
|
||||
ANIMATION_FPS = 24
|
||||
ANIMATION_FRAMES_PER_STATE = 4
|
||||
FRAME_SECONDS = 1 / ANIMATION_FPS
|
||||
EVEROS_TERMINAL_THEME = TerminalTheme(
|
||||
background=(29, 28, 24),
|
||||
foreground=(245, 237, 220),
|
||||
normal=[
|
||||
(29, 28, 24),
|
||||
(192, 149, 37),
|
||||
(216, 205, 175),
|
||||
(249, 185, 28),
|
||||
(139, 118, 63),
|
||||
(246, 194, 59),
|
||||
(245, 237, 220),
|
||||
(245, 237, 220),
|
||||
],
|
||||
bright=[
|
||||
(49, 48, 43),
|
||||
(192, 149, 37),
|
||||
(216, 205, 175),
|
||||
(249, 185, 28),
|
||||
(145, 140, 128),
|
||||
(246, 194, 59),
|
||||
(245, 237, 220),
|
||||
(245, 237, 220),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FramePlan:
|
||||
state: str
|
||||
phase: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OpacitySchedule:
|
||||
key_times: str
|
||||
values: str
|
||||
|
||||
|
||||
def build_frame_plan(
|
||||
states: Sequence[str],
|
||||
*,
|
||||
frames_per_state: int = ANIMATION_FRAMES_PER_STATE,
|
||||
) -> tuple[FramePlan, ...]:
|
||||
"""Return animation frames, closed by a duplicate of the first frame."""
|
||||
if not states:
|
||||
raise ValueError("at least one animation state is required")
|
||||
if frames_per_state < 1:
|
||||
raise ValueError("frames_per_state must be at least 1")
|
||||
|
||||
state_count = len(states)
|
||||
frame_count = state_count * frames_per_state
|
||||
frames = [
|
||||
FramePlan(
|
||||
state=states[index // frames_per_state],
|
||||
phase=index / frame_count,
|
||||
)
|
||||
for index in range(frame_count)
|
||||
]
|
||||
frames.append(FramePlan(state=states[0], phase=0.0))
|
||||
return tuple(frames)
|
||||
|
||||
|
||||
def opacity_schedule(*, index: int, total: int) -> OpacitySchedule:
|
||||
"""Build a gap-free discrete opacity schedule for one frame."""
|
||||
if total < 2:
|
||||
raise ValueError("at least two frames are required")
|
||||
if index < 0 or index >= total:
|
||||
raise ValueError("frame index is out of range")
|
||||
|
||||
start = index / total
|
||||
end = (index + 1) / total
|
||||
|
||||
if index == 0:
|
||||
return OpacitySchedule(key_times=f"0;{_time(end)};1", values="1;0;0")
|
||||
if index == total - 1:
|
||||
return OpacitySchedule(key_times=f"0;{_time(start)};1", values="0;1;1")
|
||||
return OpacitySchedule(
|
||||
key_times=f"0;{_time(start)};{_time(end)};1",
|
||||
values="0;1;0;0",
|
||||
)
|
||||
|
||||
|
||||
async def render_media(out_dir: Path) -> tuple[Path, Path]:
|
||||
"""Render the static screenshot and looping animated SVG."""
|
||||
from everos.entrypoints.tui.demo.app import DotSphereWidget
|
||||
|
||||
await anyio.Path(out_dir).mkdir(parents=True, exist_ok=True)
|
||||
screenshot = out_dir / "everos-demo-tui-screenshot.svg"
|
||||
remembered_index = DotSphereWidget.STATES.index("remembered")
|
||||
remembered = FramePlan(
|
||||
state=DotSphereWidget.STATES[remembered_index],
|
||||
phase=remembered_index / len(DotSphereWidget.STATES),
|
||||
)
|
||||
await _export_frame(screenshot, remembered, terminal_size=TERMINAL_SIZE)
|
||||
|
||||
plan = build_frame_plan(DotSphereWidget.STATES)
|
||||
frame_paths: list[Path] = []
|
||||
for index, frame in enumerate(plan):
|
||||
frame_path = out_dir / f"frame-{index:02d}-{frame.state}.svg"
|
||||
await _export_frame(
|
||||
frame_path,
|
||||
frame,
|
||||
terminal_size=ANIMATION_TERMINAL_SIZE,
|
||||
)
|
||||
frame_paths.append(frame_path)
|
||||
|
||||
animation = out_dir / "everos-demo-tui-animation.svg"
|
||||
animation.write_text(_build_animation_svg(frame_paths, plan))
|
||||
return screenshot, animation
|
||||
|
||||
|
||||
async def _export_frame(
|
||||
path: Path,
|
||||
frame: FramePlan,
|
||||
*,
|
||||
terminal_size: tuple[int, int] = TERMINAL_SIZE,
|
||||
) -> None:
|
||||
from everos.entrypoints.tui.demo.app import (
|
||||
SPHERE_FRAME_HEIGHT,
|
||||
SPHERE_FRAME_WIDTH,
|
||||
DotSphereWidget,
|
||||
EverOSDemoApp,
|
||||
)
|
||||
from everos.entrypoints.tui.demo.widgets.sphere import (
|
||||
build_dot_sphere,
|
||||
render_dot_sphere_text,
|
||||
)
|
||||
|
||||
no_color = os.environ.pop("NO_COLOR", None)
|
||||
try:
|
||||
app = EverOSDemoApp()
|
||||
async with app.run_test(size=terminal_size) as pilot:
|
||||
await pilot.pause(0.05)
|
||||
widget = app.query_one(DotSphereWidget)
|
||||
widget.pause_animation()
|
||||
sphere = build_dot_sphere(
|
||||
width=SPHERE_FRAME_WIDTH,
|
||||
height=SPHERE_FRAME_HEIGHT,
|
||||
phase=frame.phase,
|
||||
state_key=frame.state,
|
||||
)
|
||||
widget.update(render_dot_sphere_text(sphere))
|
||||
await pilot.pause(0.05)
|
||||
screenshot = _export_screenshot_svg(app)
|
||||
finally:
|
||||
if no_color is not None:
|
||||
os.environ["NO_COLOR"] = no_color
|
||||
await anyio.Path(path).write_text(normalize_svg_terminal_ids(screenshot))
|
||||
|
||||
|
||||
def _export_screenshot_svg(app) -> str:
|
||||
width, height = app.size
|
||||
console = Console(
|
||||
width=width,
|
||||
height=height,
|
||||
file=io.StringIO(),
|
||||
force_terminal=True,
|
||||
color_system="truecolor",
|
||||
record=True,
|
||||
legacy_windows=False,
|
||||
safe_box=False,
|
||||
)
|
||||
screen_render = app.screen._compositor.render_update(
|
||||
full=True,
|
||||
screen_stack=app.app._background_screens,
|
||||
simplify=False,
|
||||
)
|
||||
console.print(screen_render)
|
||||
return console.export_svg(
|
||||
title=app.title,
|
||||
theme=EVEROS_TERMINAL_THEME,
|
||||
unique_id="terminal-everos",
|
||||
)
|
||||
|
||||
|
||||
def _build_animation_svg(frame_paths: Sequence[Path], plan: Sequence[FramePlan]) -> str:
|
||||
if len(frame_paths) != len(plan):
|
||||
raise ValueError("frame paths and frame plan lengths must match")
|
||||
|
||||
view_box, width, height = _read_svg_dimensions(frame_paths[0])
|
||||
duration = len(frame_paths) * FRAME_SECONDS
|
||||
images = []
|
||||
for index, frame_path in enumerate(frame_paths):
|
||||
encoded = base64.b64encode(frame_path.read_bytes()).decode("ascii")
|
||||
schedule = opacity_schedule(index=index, total=len(frame_paths))
|
||||
state = html.escape(plan[index].state)
|
||||
images.append(
|
||||
f' <image width="{width}" height="{height}" '
|
||||
f'href="data:image/svg+xml;base64,{encoded}" opacity="0">\n'
|
||||
f" <title>EverOS demo state: {state}</title>\n"
|
||||
f' <animate attributeName="opacity" dur="{duration:.2f}s" '
|
||||
f'repeatCount="indefinite" keyTimes="{schedule.key_times}" '
|
||||
f'values="{schedule.values}" calcMode="discrete" />\n'
|
||||
" </image>"
|
||||
)
|
||||
|
||||
return (
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" '
|
||||
f'height="{height}" viewBox="{view_box}">\n'
|
||||
"<title>Animated EverOS demo terminal UI</title>\n"
|
||||
"<desc>EverOS demo cycles through memory states with a closed loop.</desc>\n"
|
||||
+ "\n".join(images)
|
||||
+ "\n</svg>\n"
|
||||
)
|
||||
|
||||
|
||||
def _read_svg_dimensions(path: Path) -> tuple[str, str, str]:
|
||||
svg_open = re.search(r"<svg\s+([^>]+)>", path.read_text())
|
||||
if svg_open is None:
|
||||
raise ValueError(f"could not find SVG root in {path}")
|
||||
view_box_match = re.search(r'viewBox="([^"]+)"', svg_open.group(0))
|
||||
if view_box_match is None:
|
||||
raise ValueError(f"could not find SVG viewBox in {path}")
|
||||
view_box = view_box_match.group(1)
|
||||
_, _, width, height = view_box.split()
|
||||
return view_box, width, height
|
||||
|
||||
|
||||
def normalize_svg_terminal_ids(svg: str) -> str:
|
||||
"""Normalize Rich's random terminal SVG prefix for deterministic media."""
|
||||
return re.sub(r"terminal-\d+", "terminal-everos", svg)
|
||||
|
||||
|
||||
def _time(value: float) -> str:
|
||||
return f"{value:.6f}".rstrip("0").rstrip(".")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--out-dir",
|
||||
type=Path,
|
||||
default=Path("/tmp/everos-demo-media"),
|
||||
help="Directory for generated README SVG media.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
screenshot, animation = asyncio.run(render_media(args.out_dir))
|
||||
print(screenshot)
|
||||
print(animation)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1 @@
|
|||
"""Widgets and rendering primitives for the EverOS demo TUI."""
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
"""Dot-sphere primitives for the EverOS demo TUI.
|
||||
|
||||
The Textual app consumes these pure rendering primitives so the animated
|
||||
surface stays testable without standing up a terminal UI.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
from rich.text import Text
|
||||
|
||||
EVEROS_YELLOW = "#F9B91C"
|
||||
EVEROS_YELLOW_SOFT = "#F6C23B"
|
||||
EVEROS_AMBER_DIM = "#4A3D20"
|
||||
EVEROS_AMBER = "#8B763F"
|
||||
EVEROS_CYAN = "#F5EDDC"
|
||||
EVEROS_GREEN = "#D8CDAF"
|
||||
EVEROS_ORANGE = "#C09525"
|
||||
BRAILLE_BASE = 0x2800
|
||||
BRAILLE_DOT_BITS = (
|
||||
(0x01, 0x02, 0x04, 0x40),
|
||||
(0x08, 0x10, 0x20, 0x80),
|
||||
)
|
||||
SPHERE_POINT_COUNT = 1300
|
||||
CONFETTI_POINT_COUNT = 150
|
||||
CONFETTI_GLYPHS = (".", "+", "*", "x")
|
||||
CONFETTI_STYLES = (
|
||||
EVEROS_YELLOW,
|
||||
EVEROS_YELLOW_SOFT,
|
||||
EVEROS_CYAN,
|
||||
EVEROS_ORANGE,
|
||||
EVEROS_AMBER,
|
||||
)
|
||||
GOLDEN_ANGLE = math.pi * (3 - math.sqrt(5))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SphereState:
|
||||
"""Visual and copy settings for a sphere animation state."""
|
||||
|
||||
key: str
|
||||
caption: str
|
||||
accent: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DotCell:
|
||||
"""One projected dot in terminal cell coordinates."""
|
||||
|
||||
x: int
|
||||
y: int
|
||||
z: float
|
||||
glyph: str
|
||||
style: str
|
||||
highlighted: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DotSphereFrame:
|
||||
"""A fully projected dot-sphere frame."""
|
||||
|
||||
width: int
|
||||
height: int
|
||||
state: SphereState
|
||||
cells: tuple[DotCell, ...]
|
||||
|
||||
@property
|
||||
def caption(self) -> str:
|
||||
return self.state.caption
|
||||
|
||||
|
||||
SPHERE_STATES: dict[str, SphereState] = {
|
||||
"booting": SphereState(
|
||||
key="booting",
|
||||
caption="forming local memory field",
|
||||
accent=EVEROS_YELLOW,
|
||||
),
|
||||
"ingesting": SphereState(
|
||||
key="ingesting",
|
||||
caption="ingesting conversation dots",
|
||||
accent=EVEROS_CYAN,
|
||||
),
|
||||
"extracting": SphereState(
|
||||
key="extracting",
|
||||
caption="extracting episode -> atomic facts",
|
||||
accent=EVEROS_ORANGE,
|
||||
),
|
||||
"indexing": SphereState(
|
||||
key="indexing",
|
||||
caption="syncing SQLite + LanceDB orbit",
|
||||
accent=EVEROS_CYAN,
|
||||
),
|
||||
"recalling": SphereState(
|
||||
key="recalling",
|
||||
caption="scanning memory sphere",
|
||||
accent=EVEROS_GREEN,
|
||||
),
|
||||
"remembered": SphereState(
|
||||
key="remembered",
|
||||
caption="remembered Yosemite preference",
|
||||
accent=EVEROS_YELLOW,
|
||||
),
|
||||
"source": SphereState(
|
||||
key="source",
|
||||
caption="revealing episode.md source",
|
||||
accent=EVEROS_YELLOW_SOFT,
|
||||
),
|
||||
"celebrating": SphereState(
|
||||
key="celebrating",
|
||||
caption="memory crystallized",
|
||||
accent=EVEROS_YELLOW,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def build_dot_sphere(
|
||||
*, width: int, height: int, phase: float, state_key: str
|
||||
) -> DotSphereFrame:
|
||||
"""Build one dot-sphere animation frame."""
|
||||
if width < 13 or height < 7:
|
||||
raise ValueError("dot sphere requires at least 13x7 cells")
|
||||
try:
|
||||
state = SPHERE_STATES[state_key]
|
||||
except KeyError as exc:
|
||||
raise ValueError(f"unknown sphere state: {state_key}") from exc
|
||||
|
||||
if state.key == "celebrating":
|
||||
return _build_confetti_burst(
|
||||
width=width,
|
||||
height=height,
|
||||
phase=phase,
|
||||
state=state,
|
||||
)
|
||||
|
||||
sub_width = width * 2
|
||||
sub_height = height * 4
|
||||
sub_center_x = (sub_width - 1) / 2
|
||||
sub_center_y = (sub_height - 1) / 2
|
||||
radius_x = max(1.0, sub_center_x - 5)
|
||||
radius_y = max(1.0, sub_center_y - 3)
|
||||
rotation = phase * math.tau
|
||||
active_target = _highlight_target(width, height)
|
||||
|
||||
masks: dict[tuple[int, int], int] = {}
|
||||
depths: dict[tuple[int, int], float] = {}
|
||||
highlighted_positions: set[tuple[int, int]] = set()
|
||||
for index in range(SPHERE_POINT_COUNT):
|
||||
y3 = 1 - 2 * ((index + 0.5) / SPHERE_POINT_COUNT)
|
||||
ring_radius = math.sqrt(max(0.0, 1.0 - y3 * y3))
|
||||
theta = index * GOLDEN_ANGLE + rotation
|
||||
x3 = ring_radius * math.cos(theta)
|
||||
z3 = ring_radius * math.sin(theta)
|
||||
sub_x = round(sub_center_x + x3 * radius_x)
|
||||
sub_y = round(sub_center_y + y3 * radius_y)
|
||||
if not (0 <= sub_x < sub_width and 0 <= sub_y < sub_height):
|
||||
continue
|
||||
_add_braille_dot(
|
||||
masks=masks,
|
||||
depths=depths,
|
||||
sub_x=sub_x,
|
||||
sub_y=sub_y,
|
||||
z=z3,
|
||||
)
|
||||
|
||||
if state.key in {"recalling", "remembered", "source"}:
|
||||
highlighted_positions.add(active_target)
|
||||
target_sub_x = active_target[0] * 2 + 1
|
||||
target_sub_y = active_target[1] * 4 + 1
|
||||
_add_braille_dot(
|
||||
masks=masks,
|
||||
depths=depths,
|
||||
sub_x=target_sub_x,
|
||||
sub_y=target_sub_y,
|
||||
z=1.0,
|
||||
)
|
||||
|
||||
cells = []
|
||||
for (x, y), mask in masks.items():
|
||||
highlighted = (x, y) in highlighted_positions
|
||||
if highlighted and state.key == "recalling":
|
||||
style = EVEROS_CYAN
|
||||
elif highlighted:
|
||||
style = EVEROS_YELLOW
|
||||
else:
|
||||
style = _style_for_depth(depths[(x, y)], state)
|
||||
cells.append(
|
||||
DotCell(
|
||||
x=x,
|
||||
y=y,
|
||||
z=depths[(x, y)],
|
||||
glyph=chr(BRAILLE_BASE + mask),
|
||||
style=style,
|
||||
highlighted=highlighted,
|
||||
)
|
||||
)
|
||||
|
||||
return DotSphereFrame(
|
||||
width=width,
|
||||
height=height,
|
||||
state=state,
|
||||
cells=tuple(sorted(cells, key=lambda cell: (cell.y, cell.x))),
|
||||
)
|
||||
|
||||
|
||||
def _build_confetti_burst(
|
||||
*, width: int, height: int, phase: float, state: SphereState
|
||||
) -> DotSphereFrame:
|
||||
center_x = (width - 1) / 2
|
||||
center_y = (height - 1) / 2
|
||||
radius_x = max(1.0, center_x - 3)
|
||||
radius_y = max(1.0, center_y - 2)
|
||||
local_phase = _state_local_phase(phase, state.key)
|
||||
bloom = 0.62 + 0.58 * math.sin(local_phase * math.pi)
|
||||
rotation = phase * math.tau * 1.4
|
||||
|
||||
cells_by_position: dict[tuple[int, int], DotCell] = {}
|
||||
for index in range(CONFETTI_POINT_COUNT):
|
||||
shell = 0.55 + 0.45 * ((index % 17) / 16)
|
||||
angle = index * GOLDEN_ANGLE + rotation
|
||||
drift = math.sin(phase * math.tau * 2 + index * 0.23)
|
||||
x = round(center_x + math.cos(angle) * radius_x * shell * bloom)
|
||||
y = round(
|
||||
center_y
|
||||
+ math.sin(angle) * radius_y * shell * bloom
|
||||
+ drift * 0.75 * local_phase
|
||||
)
|
||||
if not (0 <= x < width and 0 <= y < height):
|
||||
continue
|
||||
|
||||
z = math.cos(angle - rotation) * shell
|
||||
glyph = CONFETTI_GLYPHS[(index + int(local_phase * 10)) % len(CONFETTI_GLYPHS)]
|
||||
style = CONFETTI_STYLES[
|
||||
(index * 3 + int(local_phase * 7)) % len(CONFETTI_STYLES)
|
||||
]
|
||||
position = (x, y)
|
||||
existing = cells_by_position.get(position)
|
||||
if existing is None or z > existing.z:
|
||||
cells_by_position[position] = DotCell(
|
||||
x=x,
|
||||
y=y,
|
||||
z=z,
|
||||
glyph=glyph,
|
||||
style=style,
|
||||
)
|
||||
|
||||
return DotSphereFrame(
|
||||
width=width,
|
||||
height=height,
|
||||
state=state,
|
||||
cells=tuple(
|
||||
sorted(cells_by_position.values(), key=lambda cell: (cell.y, cell.x))
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def render_dot_sphere_lines(frame: DotSphereFrame) -> list[list[DotCell | None]]:
|
||||
"""Render cells into a sparse row grid for Rich/Textual consumers."""
|
||||
grid: list[list[DotCell | None]] = [
|
||||
[None for _ in range(frame.width)] for _ in range(frame.height)
|
||||
]
|
||||
for cell in frame.cells:
|
||||
if 0 <= cell.x < frame.width and 0 <= cell.y < frame.height:
|
||||
grid[cell.y][cell.x] = cell
|
||||
return grid
|
||||
|
||||
|
||||
def render_dot_sphere_text(frame: DotSphereFrame) -> Text:
|
||||
"""Convert a frame into styled terminal text."""
|
||||
rows = render_dot_sphere_lines(frame)
|
||||
text = Text(no_wrap=True)
|
||||
for row in rows:
|
||||
for cell in row:
|
||||
if cell is None:
|
||||
text.append(" ")
|
||||
else:
|
||||
text.append(cell.glyph, style=cell.style)
|
||||
text.append("\n")
|
||||
text.append("\n")
|
||||
text.append(frame.caption, style=f"bold {frame.state.accent}")
|
||||
return text
|
||||
|
||||
|
||||
def _add_braille_dot(
|
||||
*,
|
||||
masks: dict[tuple[int, int], int],
|
||||
depths: dict[tuple[int, int], float],
|
||||
sub_x: int,
|
||||
sub_y: int,
|
||||
z: float,
|
||||
) -> None:
|
||||
cell_x = sub_x // 2
|
||||
cell_y = sub_y // 4
|
||||
local_x = sub_x % 2
|
||||
local_y = sub_y % 4
|
||||
position = (cell_x, cell_y)
|
||||
masks[position] = masks.get(position, 0) | BRAILLE_DOT_BITS[local_x][local_y]
|
||||
depths[position] = max(z, depths.get(position, -1.0))
|
||||
|
||||
|
||||
def _style_for_depth(z: float, state: SphereState) -> str:
|
||||
if state.key == "extracting" and z > 0.38:
|
||||
return EVEROS_ORANGE
|
||||
if state.key == "indexing" and z > 0.45:
|
||||
return EVEROS_CYAN
|
||||
if state.key == "ingesting" and z > 0.5:
|
||||
return EVEROS_CYAN
|
||||
if z > 0.58:
|
||||
return EVEROS_YELLOW
|
||||
if z > 0.05:
|
||||
return EVEROS_YELLOW
|
||||
return EVEROS_AMBER
|
||||
|
||||
|
||||
def _highlight_target(width: int, height: int) -> tuple[int, int]:
|
||||
return (round((width - 1) * 0.66), round((height - 1) * 0.42))
|
||||
|
||||
|
||||
def _state_local_phase(phase: float, state_key: str) -> float:
|
||||
state_keys = tuple(SPHERE_STATES)
|
||||
return (phase * len(state_keys) - state_keys.index(state_key)) % 1.0
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
"""EverOS demo command contracts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
import typer
|
||||
from rich.panel import Panel
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from everos.entrypoints.cli.commands import demo as demo_command
|
||||
from everos.entrypoints.tui.demo.data import build_demo_story
|
||||
|
||||
|
||||
def test_demo_help_exposes_cinematic_mode() -> None:
|
||||
app = typer.Typer()
|
||||
demo_command.register(app)
|
||||
|
||||
result = CliRunner().invoke(app, ["demo", "--help"], terminal_width=120)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "--cinematic" in _strip_ansi(result.stdout)
|
||||
|
||||
|
||||
def test_collect_playable_story_prompts_for_memory_then_query(monkeypatch) -> None:
|
||||
prompts: list[tuple[str, str]] = []
|
||||
replies = iter(
|
||||
[
|
||||
"I keep my Monday design review notes in Notion.",
|
||||
"Where are my Monday review notes?",
|
||||
]
|
||||
)
|
||||
|
||||
def fake_prompt(label: str, *, default: str) -> str:
|
||||
prompts.append((label, default))
|
||||
return next(replies)
|
||||
|
||||
monkeypatch.setattr(demo_command.typer, "prompt", fake_prompt)
|
||||
|
||||
story = demo_command._collect_playable_story()
|
||||
|
||||
assert [label for label, _ in prompts] == [
|
||||
"Give EverOS one thing to remember",
|
||||
"Ask EverOS to recall it",
|
||||
]
|
||||
assert story.memory == "I keep my Monday design review notes in Notion."
|
||||
assert story.query == "Where are my Monday review notes?"
|
||||
|
||||
|
||||
def test_interactive_demo_checks_textual_before_prompt(monkeypatch) -> None:
|
||||
def fail_load_tui() -> object:
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
def fail_prompt(*_: object, **__: object) -> str:
|
||||
pytest.fail("demo prompted before checking TUI availability")
|
||||
|
||||
monkeypatch.setattr(demo_command, "_load_run_demo_tui", fail_load_tui)
|
||||
monkeypatch.setattr(demo_command.typer, "prompt", fail_prompt)
|
||||
|
||||
with pytest.raises(typer.Exit):
|
||||
demo_command._run_interactive_demo(cinematic=False)
|
||||
|
||||
|
||||
def test_plain_demo_uses_poster_gold_brand_primary(monkeypatch) -> None:
|
||||
printed: list[object] = []
|
||||
|
||||
class FakeConsole:
|
||||
def print(self, *renderables: object, **_: object) -> None:
|
||||
printed.extend(renderables)
|
||||
|
||||
monkeypatch.setattr(demo_command, "Console", FakeConsole)
|
||||
|
||||
demo_command._print_plain_demo()
|
||||
|
||||
panel = next(item for item in printed if isinstance(item, Panel))
|
||||
printed_text = "\n".join(str(item) for item in printed)
|
||||
assert panel.border_style == "#F9B91C"
|
||||
assert "#F9B91C" in printed_text
|
||||
assert "#FFE600" not in printed_text
|
||||
|
||||
|
||||
def test_plain_demo_prints_custom_story(monkeypatch) -> None:
|
||||
printed: list[object] = []
|
||||
|
||||
class FakeConsole:
|
||||
def print(self, *renderables: object, **_: object) -> None:
|
||||
printed.extend(renderables)
|
||||
|
||||
monkeypatch.setattr(demo_command, "Console", FakeConsole)
|
||||
|
||||
demo_command._print_plain_demo(
|
||||
build_demo_story(
|
||||
"I keep my Monday design review notes in Notion.",
|
||||
"Where are my Monday review notes?",
|
||||
)
|
||||
)
|
||||
|
||||
printed_text = "\n".join(str(item) for item in printed)
|
||||
assert "EverOS remembered" in printed_text
|
||||
assert "I keep my Monday design review notes in Notion." in printed_text
|
||||
assert "episode-demo.md" in printed_text
|
||||
|
||||
|
||||
def _strip_ansi(value: str) -> str:
|
||||
return re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", value)
|
||||
|
|
@ -13,6 +13,7 @@ def test_help_exits_zero() -> None:
|
|||
assert "everos" in result.stdout
|
||||
assert "server" in result.stdout
|
||||
assert "cascade" in result.stdout
|
||||
assert "demo" in result.stdout
|
||||
|
||||
|
||||
def test_no_args_shows_help_and_exits_nonzero() -> None:
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
"""TUI entrypoint tests."""
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
"""EverOS demo TUI color contracts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from everos.entrypoints.tui.demo.app import (
|
||||
SPHERE_FRAME_HEIGHT,
|
||||
SPHERE_FRAME_WIDTH,
|
||||
TERMINAL_CELL_HEIGHT_RATIO,
|
||||
DotSphereWidget,
|
||||
EverOSDemoApp,
|
||||
_field_header_text,
|
||||
_hero_text,
|
||||
_payoff_text,
|
||||
_recall_proof_text,
|
||||
_signal_rail_text,
|
||||
_source_tree_text,
|
||||
_sphere_caption,
|
||||
)
|
||||
from everos.entrypoints.tui.demo.data import build_demo_story
|
||||
from everos.entrypoints.tui.demo.widgets.sphere import SPHERE_STATES
|
||||
|
||||
|
||||
def test_demo_tui_uses_poster_derived_brand_palette() -> None:
|
||||
css = EverOSDemoApp.CSS
|
||||
|
||||
assert "#F9B91C" in css
|
||||
assert "#31302B" in css
|
||||
assert "#F5EDDC" in css
|
||||
assert "#918C80" in css
|
||||
assert "#FFE600" not in css
|
||||
assert "#55D6FF" not in css
|
||||
assert "#73F7A7" not in css
|
||||
assert any(span.style == "bold #F9B91C" for span in _hero_text().spans)
|
||||
|
||||
|
||||
def test_demo_tui_uses_elevated_instrument_layout() -> None:
|
||||
css = EverOSDemoApp.CSS
|
||||
|
||||
assert "#command-strip" in css
|
||||
assert "#memory-field" in css
|
||||
assert "#signal-rail" in css
|
||||
assert "#provenance-strip" in css
|
||||
assert "#payoff" in css
|
||||
assert "FooterKey" in css
|
||||
assert "background: #F9B91C" in css
|
||||
assert any("on #F9B91C" in span.style for span in _hero_text().spans)
|
||||
assert "border: double" not in css
|
||||
assert "border: heavy" not in css
|
||||
|
||||
|
||||
def test_demo_tui_uses_balanced_panel_proportions() -> None:
|
||||
css = EverOSDemoApp.CSS
|
||||
|
||||
command_strip = _css_block(css, "#command-strip")
|
||||
signal_rail = _css_block(css, "#signal-rail")
|
||||
payoff = _css_block(css, "#payoff")
|
||||
|
||||
assert "height: 2;" in command_strip
|
||||
assert "border-left: thick" not in command_strip
|
||||
assert "background: #31302B" not in command_strip
|
||||
assert len(_hero_text().plain.splitlines()) == 1
|
||||
assert len(_hero_text().plain) <= 56
|
||||
|
||||
assert "height: 1fr;" in DotSphereWidget.DEFAULT_CSS
|
||||
|
||||
assert "height: 100%;" in signal_rail
|
||||
assert "source route" in _signal_rail_text().plain
|
||||
assert "recall proof" in _signal_rail_text().plain
|
||||
|
||||
assert "height: 2;" in payoff
|
||||
assert "background: #24231E;" in payoff
|
||||
assert "padding: 0 1;" in payoff
|
||||
assert _payoff_text().plain.startswith("memory formed:")
|
||||
assert "bold #F9B91C" in {span.style for span in _payoff_text().spans}
|
||||
|
||||
|
||||
def test_demo_tui_sphere_renders_round_in_terminal_cells() -> None:
|
||||
visual_ratio = (SPHERE_FRAME_WIDTH - 4) / (
|
||||
SPHERE_FRAME_HEIGHT * TERMINAL_CELL_HEIGHT_RATIO
|
||||
)
|
||||
|
||||
assert visual_ratio == pytest.approx(1.0, abs=0.04)
|
||||
assert SPHERE_FRAME_WIDTH == 37
|
||||
assert SPHERE_FRAME_HEIGHT == 17
|
||||
|
||||
|
||||
def test_demo_tui_celebrates_after_source_reveal() -> None:
|
||||
assert DotSphereWidget.STATES[-2:] == ("source", "celebrating")
|
||||
assert set(DotSphereWidget.STATES).issubset(SPHERE_STATES)
|
||||
|
||||
|
||||
def test_demo_tui_renders_playable_story_copy() -> None:
|
||||
story = build_demo_story(
|
||||
"I keep my Monday design review notes in Notion.",
|
||||
"Where are my Monday review notes?",
|
||||
)
|
||||
|
||||
assert "user=you" in _field_header_text(story).plain
|
||||
assert "Where are my Monday review notes?" in _sphere_caption(story).plain
|
||||
assert story.answer in _sphere_caption(story).plain
|
||||
assert "server wake" not in _signal_rail_text(story).plain
|
||||
assert "memory core" in _signal_rail_text(story).plain
|
||||
assert story.source_filename in _source_tree_text(story).plain
|
||||
assert story.fact_filename in _source_tree_text(story).plain
|
||||
assert story.answer in _recall_proof_text(story).plain
|
||||
assert story.answer in _payoff_text(story).plain
|
||||
|
||||
|
||||
def test_demo_tui_signal_rail_keeps_source_status_columns_separate() -> None:
|
||||
rail = _signal_rail_text().plain
|
||||
|
||||
assert "mdattached" not in rail
|
||||
assert "md7 nodes" not in rail
|
||||
assert "..." in rail
|
||||
|
||||
|
||||
def _css_block(css: str, selector: str) -> str:
|
||||
start = css.index(f"{selector} {{")
|
||||
end = css.index("}", start)
|
||||
return css[start:end]
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
"""EverOS playable demo story contracts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from everos.entrypoints.tui.demo.data import (
|
||||
DEFAULT_MEMORY_SEED,
|
||||
DEFAULT_QUERY,
|
||||
build_demo_story,
|
||||
)
|
||||
|
||||
|
||||
def test_demo_story_preserves_prompted_memory_and_query() -> None:
|
||||
story = build_demo_story(
|
||||
"I keep my Monday design review notes in Notion.",
|
||||
"Where are my Monday review notes?",
|
||||
)
|
||||
|
||||
assert story.owner == "you"
|
||||
assert story.memory == "I keep my Monday design review notes in Notion."
|
||||
assert story.query == "Where are my Monday review notes?"
|
||||
assert story.answer == "I keep my Monday design review notes in Notion."
|
||||
assert story.source_filename == "episode-demo.md"
|
||||
assert story.fact_filename == "atomic_fact-demo.md"
|
||||
|
||||
|
||||
def test_demo_story_keeps_default_yosemite_success_moment() -> None:
|
||||
story = build_demo_story(DEFAULT_MEMORY_SEED, DEFAULT_QUERY)
|
||||
|
||||
assert story.answer == "Yosemite every spring"
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
"""EverOS demo README media generation contracts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
|
||||
from everos.entrypoints.tui.demo.readme_media import (
|
||||
ANIMATION_FPS,
|
||||
ANIMATION_FRAMES_PER_STATE,
|
||||
ANIMATION_TERMINAL_SIZE,
|
||||
FRAME_SECONDS,
|
||||
TERMINAL_SIZE,
|
||||
FramePlan,
|
||||
_export_frame,
|
||||
build_frame_plan,
|
||||
normalize_svg_terminal_ids,
|
||||
opacity_schedule,
|
||||
)
|
||||
|
||||
|
||||
def test_readme_animation_frame_plan_closes_on_first_frame() -> None:
|
||||
plan = build_frame_plan(("booting", "ingesting", "source"), frames_per_state=3)
|
||||
|
||||
assert plan[0] == plan[-1]
|
||||
assert len(plan) == 10
|
||||
assert [frame.state for frame in plan] == [
|
||||
"booting",
|
||||
"booting",
|
||||
"booting",
|
||||
"ingesting",
|
||||
"ingesting",
|
||||
"ingesting",
|
||||
"source",
|
||||
"source",
|
||||
"source",
|
||||
"booting",
|
||||
]
|
||||
assert [frame.phase for frame in plan] == [
|
||||
0 / 9,
|
||||
1 / 9,
|
||||
2 / 9,
|
||||
3 / 9,
|
||||
4 / 9,
|
||||
5 / 9,
|
||||
6 / 9,
|
||||
7 / 9,
|
||||
8 / 9,
|
||||
0.0,
|
||||
]
|
||||
|
||||
|
||||
def test_readme_animation_default_plan_uses_smoother_frame_rate() -> None:
|
||||
states = ("booting", "ingesting", "source")
|
||||
|
||||
plan = build_frame_plan(states)
|
||||
|
||||
assert len(plan) <= len(states) * 4 + 1
|
||||
assert ANIMATION_FRAMES_PER_STATE == 4
|
||||
assert ANIMATION_FPS >= 24
|
||||
assert FRAME_SECONDS <= 1 / 24
|
||||
|
||||
|
||||
def test_readme_animation_uses_compact_terminal_size_for_load_speed() -> None:
|
||||
assert ANIMATION_TERMINAL_SIZE[0] < TERMINAL_SIZE[0]
|
||||
assert ANIMATION_TERMINAL_SIZE[1] < TERMINAL_SIZE[1]
|
||||
assert ANIMATION_TERMINAL_SIZE[0] * ANIMATION_TERMINAL_SIZE[1] <= 5600
|
||||
|
||||
|
||||
def test_readme_animation_opacity_schedule_has_no_blank_gap() -> None:
|
||||
schedules = [opacity_schedule(index=idx, total=4) for idx in range(4)]
|
||||
|
||||
assert schedules[0].values == "1;0;0"
|
||||
assert schedules[-1].values == "0;1;1"
|
||||
assert all("0.0005" not in schedule.key_times for schedule in schedules)
|
||||
|
||||
|
||||
def test_readme_media_normalizes_rich_terminal_random_ids() -> None:
|
||||
svg = (
|
||||
"<style>.terminal-12345-matrix {}</style>"
|
||||
'<clipPath id="terminal-12345-line-0"></clipPath>'
|
||||
'<text class="terminal-12345-r1" '
|
||||
'clip-path="url(#terminal-12345-line-0)">EverOS</text>'
|
||||
)
|
||||
|
||||
normalized = normalize_svg_terminal_ids(svg)
|
||||
|
||||
assert "terminal-12345" not in normalized
|
||||
assert normalized.count("terminal-everos") == 4
|
||||
|
||||
|
||||
async def test_export_frame_freezes_animation_before_rendering_requested_state(
|
||||
tmp_path, monkeypatch
|
||||
) -> None:
|
||||
from everos.entrypoints.tui.demo.app import DotSphereWidget
|
||||
|
||||
def fast_mount(self: DotSphereWidget) -> None:
|
||||
self._animation_timer = self.set_interval(0.001, self._advance)
|
||||
self._advance()
|
||||
|
||||
monkeypatch.setattr(DotSphereWidget, "on_mount", fast_mount)
|
||||
path = tmp_path / "frame.svg"
|
||||
|
||||
await _export_frame(path, FramePlan(state="booting", phase=0.0))
|
||||
|
||||
svg = html.unescape(path.read_text()).replace("\xa0", " ")
|
||||
assert "forming local memory field" in svg
|
||||
assert "ingesting conversation dots" not in svg
|
||||
|
||||
|
||||
async def test_export_frame_preserves_poster_palette(tmp_path) -> None:
|
||||
path = tmp_path / "frame.svg"
|
||||
|
||||
await _export_frame(path, FramePlan(state="remembered", phase=0.5))
|
||||
|
||||
svg = path.read_text().lower()
|
||||
assert "#f9b91c" in svg
|
||||
assert "#f5eddc" in svg
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
"""EverOS demo dot-sphere rendering contracts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from everos.entrypoints.tui.demo.widgets.sphere import (
|
||||
SPHERE_STATES,
|
||||
DotSphereFrame,
|
||||
build_dot_sphere,
|
||||
)
|
||||
|
||||
|
||||
def test_dot_sphere_forms_round_bounded_cloud() -> None:
|
||||
frame = build_dot_sphere(width=41, height=19, phase=0.0, state_key="extracting")
|
||||
|
||||
assert frame.width == 41
|
||||
assert frame.height == 19
|
||||
assert frame.caption == "extracting episode -> atomic facts"
|
||||
assert len(frame.cells) >= 90
|
||||
|
||||
center_x = (frame.width - 1) / 2
|
||||
center_y = (frame.height - 1) / 2
|
||||
radius_x = center_x
|
||||
radius_y = center_y
|
||||
for cell in frame.cells:
|
||||
normalized = ((cell.x - center_x) / radius_x) ** 2 + (
|
||||
(cell.y - center_y) / radius_y
|
||||
) ** 2
|
||||
assert normalized <= 1.08
|
||||
|
||||
row_counts: dict[int, int] = {}
|
||||
for cell in frame.cells:
|
||||
row_counts[cell.y] = row_counts.get(cell.y, 0) + 1
|
||||
assert row_counts[frame.height // 2] > row_counts[min(row_counts)]
|
||||
assert row_counts[frame.height // 2] > row_counts[max(row_counts)]
|
||||
|
||||
|
||||
def test_dot_sphere_keeps_terminal_poles_visually_round() -> None:
|
||||
frame = build_dot_sphere(width=37, height=17, phase=0.0, state_key="booting")
|
||||
row_spans = _row_spans(frame)
|
||||
|
||||
assert row_spans[0] <= 8
|
||||
assert row_spans[1] <= 20
|
||||
assert row_spans[2] <= 26
|
||||
assert row_spans[frame.height // 2] >= 31
|
||||
assert row_spans[frame.height - 2] <= 20
|
||||
assert row_spans[frame.height - 1] <= 8
|
||||
|
||||
|
||||
def test_dot_sphere_uses_braille_fine_dot_cells() -> None:
|
||||
for state_key in SPHERE_STATES:
|
||||
if state_key == "celebrating":
|
||||
continue
|
||||
frame = build_dot_sphere(
|
||||
width=37,
|
||||
height=17,
|
||||
phase=0.25,
|
||||
state_key=state_key,
|
||||
)
|
||||
|
||||
assert all(_is_braille_cell(cell.glyph) for cell in frame.cells)
|
||||
assert not any(cell.glyph in {"·", "•", "●", "◆"} for cell in frame.cells)
|
||||
assert not any(cell.style.startswith("bold ") for cell in frame.cells)
|
||||
|
||||
|
||||
def test_dot_sphere_packs_multiple_subdots_per_terminal_cell() -> None:
|
||||
frame = build_dot_sphere(width=37, height=17, phase=0.0, state_key="booting")
|
||||
|
||||
subdot_count = sum(_braille_subdot_count(cell.glyph) for cell in frame.cells)
|
||||
|
||||
assert subdot_count > len(frame.cells) * 1.8
|
||||
assert subdot_count > frame.width * frame.height * 0.9
|
||||
assert any(_braille_subdot_count(cell.glyph) >= 4 for cell in frame.cells)
|
||||
|
||||
|
||||
def test_dot_sphere_avoids_flat_sides_in_terminal_frame() -> None:
|
||||
frame = build_dot_sphere(width=37, height=17, phase=0.0, state_key="booting")
|
||||
row_spans = _row_spans(frame)
|
||||
|
||||
assert max(row_spans.values()) <= frame.width - 4
|
||||
assert row_spans[frame.height // 2] >= frame.width - 4
|
||||
for y in range(frame.height // 2):
|
||||
assert abs(row_spans[y] - row_spans[frame.height - 1 - y]) <= 4
|
||||
|
||||
|
||||
def test_dot_sphere_remembered_state_has_highlighted_node() -> None:
|
||||
frame = build_dot_sphere(width=41, height=19, phase=0.25, state_key="remembered")
|
||||
|
||||
highlighted = [cell for cell in frame.cells if cell.highlighted]
|
||||
assert len(highlighted) == 1
|
||||
assert _is_braille_cell(highlighted[0].glyph)
|
||||
assert highlighted[0].style == "#F9B91C"
|
||||
assert frame.caption == "remembered Yosemite preference"
|
||||
|
||||
|
||||
def test_dot_sphere_celebrating_state_bursts_into_confetti() -> None:
|
||||
frame = build_dot_sphere(width=41, height=19, phase=0.93, state_key="celebrating")
|
||||
|
||||
assert frame.caption == "memory crystallized"
|
||||
confetti = [cell for cell in frame.cells if cell.glyph in {"*", "+", ".", "x"}]
|
||||
assert len(confetti) >= 70
|
||||
assert all(not _is_braille_cell(cell.glyph) for cell in confetti)
|
||||
assert not any(cell.style.startswith("bold ") for cell in confetti)
|
||||
|
||||
center_x = (frame.width - 1) / 2
|
||||
center_y = (frame.height - 1) / 2
|
||||
radius_x = max(1.0, center_x - 3)
|
||||
radius_y = max(1.0, center_y - 2)
|
||||
distances = [
|
||||
((cell.x - center_x) / radius_x) ** 2 + ((cell.y - center_y) / radius_y) ** 2
|
||||
for cell in confetti
|
||||
]
|
||||
assert max(distances) > 1.10
|
||||
assert sum(distance > 0.72 for distance in distances) > len(distances) * 0.4
|
||||
|
||||
styles = {cell.style for cell in confetti}
|
||||
assert "#F9B91C" in styles
|
||||
assert "#F6C23B" in styles
|
||||
|
||||
|
||||
def test_dot_sphere_front_light_uses_poster_gold_primary() -> None:
|
||||
frame = build_dot_sphere(width=41, height=19, phase=0.0, state_key="booting")
|
||||
|
||||
front_styles = {cell.style for cell in frame.cells if cell.z > 0.05}
|
||||
assert "#F9B91C" in front_styles
|
||||
assert "bold #F9B91C" not in front_styles
|
||||
assert "#FFE600" not in front_styles
|
||||
|
||||
|
||||
def _row_spans(frame: DotSphereFrame) -> dict[int, int]:
|
||||
spans: dict[int, int] = {}
|
||||
for y in range(frame.height):
|
||||
xs = [cell.x for cell in frame.cells if cell.y == y]
|
||||
spans[y] = max(xs) - min(xs) + 1 if xs else 0
|
||||
return spans
|
||||
|
||||
|
||||
def _is_braille_cell(glyph: str) -> bool:
|
||||
return len(glyph) == 1 and 0x2800 < ord(glyph) <= 0x28FF
|
||||
|
||||
|
||||
def _braille_subdot_count(glyph: str) -> int:
|
||||
assert _is_braille_cell(glyph)
|
||||
return (ord(glyph) - 0x2800).bit_count()
|
||||
57
uv.lock
57
uv.lock
|
|
@ -570,6 +570,7 @@ dependencies = [
|
|||
{ name = "pyyaml" },
|
||||
{ name = "sqlmodel" },
|
||||
{ name = "structlog" },
|
||||
{ name = "textual" },
|
||||
{ name = "typer" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
{ name = "watchdog" },
|
||||
|
|
@ -619,6 +620,7 @@ requires-dist = [
|
|||
{ name = "pyyaml", specifier = ">=6.0" },
|
||||
{ name = "sqlmodel", specifier = ">=0.0.22" },
|
||||
{ name = "structlog", specifier = ">=24.0.0" },
|
||||
{ name = "textual", specifier = ">=8.2.7" },
|
||||
{ name = "typer", specifier = ">=0.12.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.24.0" },
|
||||
{ name = "watchdog", specifier = ">=4.0.0" },
|
||||
|
|
@ -1083,6 +1085,18 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/88/d0/7e44e8143ac2dae8979ba882cc33d4af7b8da4741fb0361497e69b4a4379/lancedb-0.30.2-cp39-abi3-win_amd64.whl", hash = "sha256:531da53002c1c6fda829afccc8ced3056ef58eb036f09ddb2b94a06877ecc66c", size = 50940681, upload-time = "2026-03-31T23:25:52.35Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linkify-it-py"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "uc-micro-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mako"
|
||||
version = "1.3.12"
|
||||
|
|
@ -1107,6 +1121,11 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
linkify = [
|
||||
{ name = "linkify-it-py" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.3"
|
||||
|
|
@ -1182,6 +1201,18 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdit-py-plugins"
|
||||
version = "0.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
|
|
@ -2122,6 +2153,23 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl", hash = "sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f", size = 72510, upload-time = "2025-10-27T08:28:21.535Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textual"
|
||||
version = "8.2.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py", extra = ["linkify"] },
|
||||
{ name = "mdit-py-plugins" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "pygments" },
|
||||
{ name = "rich" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9b/7a/c519db0aba5024f86e71e9631810bfdd6866ed2c8695bd7fa34b90e7ef59/textual-8.2.7.tar.gz", hash = "sha256:658f568ff81e30ed43890c3e07520390e5cf1b4763822006e060656b0a88f105", size = 1859249, upload-time = "2026-05-19T10:52:49.531Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/f5/c1e18bc0707300a0e90204343abbf7d7acd6fb7ebe03a6d4893b99a234b8/textual-8.2.7-py3-none-any.whl", hash = "sha256:4caaa13a90bc4cf9c6c862c067ccd34fe84e9c161710a2a907a8026313b6bd73", size = 731129, upload-time = "2026-05-19T10:52:51.773Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiktoken"
|
||||
version = "0.12.0"
|
||||
|
|
@ -2259,6 +2307,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uc-micro-py"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.7.0"
|
||||
|
|
|
|||
Loading…
Reference in New Issue