A band pet that grows on context usage; feed it with a press. Ginno e2e sample (conceived after keras9496/claude-tamagotchi, an Electron app rather than a mod).

A local-first personal AI agent that runs on your own machine.
The shape of Claude Code, all-file storage (no DB · no account · no cloud), and a visual Workflow Studio.

Ginno turns the capabilities an AI agent should have into a complete desktop app, instead of scattering them across a terminal and a dozen browser tabs.
Full feature walkthrough:
docs/user-guide.md.
| Workflow Studio | Local knowledge base |
|---|---|
![]() | ![]() |
One design rule: your data should not leave your machine.
| Ginno | |
|---|---|
| Database | ❌ none — all state is JSON / JSONL / Markdown under ~/.ginno/ |
| Account | ❌ none — open it and go |
| Cloud sync | ❌ none — everything stays local |
| Portable | ✅ copy the folder to move it; readable, hand-editable, git-able |
You bring your own model key (OpenAI / Anthropic / DeepSeek, or any compatible endpoint). Ginno is just a client — it never takes custody of your key or your data.
Option A — download (macOS, Apple Silicon): grab the latest .dmg from Releases. It is not code-signed yet, so on first launch use right-click → Open to get past Gatekeeper.
Option B — build from source:
git clone https://github.com/robscc/ginno
cd ginno
pnpm install # web + desktop shell
cd packages/runtime && uv sync # Python runtime
cd ../..
pnpm dev # web + runtime + desktop shell, all at once
Requires: Node ≥ 20 · pnpm ≥ 9 · Python ≥ 3.11 (via uv) · Rust (Tauri).
Package a .app: make app → apps/desktop/target/release/bundle/dmg/Ginno_*.dmg (see docs/p3-packaging-notes.md).
| Layer | Choice |
|---|---|
| Shell | Tauri 2 (Rust) — process management + webview only |
| UI | Next.js 14 (static export) + React 18 + Tailwind |
| Runtime | Python + FastAPI + LangGraph (sidecar, bundled with PyInstaller) |
| Storage | plain files (no DB); optional LanceDB for semantic-search vectors |
Architecture and subsystem designs:
docs/architecture.md.
MIT © 2026 ChuanchuanSong
hooks/tamagotchi.mjs 64 lines1// ginno-tamagotchi: a band pet fed by context usage (Ginno e2e sample).
2// Exercises the P0 surface end to end: state slots + subscription redraw,
3// the session.usage backend op, and the Button press round trip
4// (ui.press → runner onPress → state.set → band redraw).
5// Conceived after keras9496/claude-tamagotchi (an Electron desktop pet, not
6// a mod — this is the band-renderable reading of the same idea).
7
8const PET = { plugin: "ginno-tamagotchi", key: "pet" };
9
10// hunger 0..3: fed recently → well fed; every turn without growth hungrier
11const FACES = [
12 ["(^x^)", "green", "full"],
13 ["(^ ^)", "yellow", "content"],
14 ["(o o)", "cyan", "peckish"],
15 ["(T T)", "red", "starving"],
16];
17
18export function register(on) {
19 on("session.start", async ($, e, next) => {
20 const result = await next(e);
21 await feed($, 0);
22 return result;
23 });
24
25 on("turn.complete", async ($, e, next) => {
26 const result = await next(e);
27 if (!e.agentId) {
28 await feed($, e.usage?.context?.tokens ?? 0); // main-loop turns only
29 }
30 return result;
31 });
32
33 on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
34 if (e.props.hasSurvey) return next(e);
35 const { value: pet = { fed: 0, hunger: 2, grew: 0 } } = await $.state.get(PET);
36 const hunger = Math.min(3, Math.max(0, pet.hunger ?? 2));
37 const [face, color, word] = FACES[hunger];
38 const { Box, Text, Button } = $.ui.resolve(e);
39 return Box({
40 flexDirection: "row",
41 paddingX: 1,
42 gap: 1,
43 children: [
44 Text({ color, bold: true, children: face }),
45 Text({ children: `pet ${word}` }),
46 Text({ dimColor: true, children: `fed ${pet.fed ?? 0}× · grew ${pet.grew ?? 0}k` }),
47 Button({ label: "feed", onPress: () => feed($, 1) }),
48 ],
49 });
50 });
51}
52
53// One feeding: tokens grew → well fed and hunger resets; a press feeds a
54// snack (hunger -1). Every state write redraws the band through the
55// subscription the render pass registered.
56async function feed($, grewTokens) {
57 const { context } = await $.session.usage();
58 const tokens = context?.tokens ?? 0;
59 const grew = grewTokens > 0 ? Math.round(grewTokens / 1000) : Math.round(tokens / 1000);
60 const { value: pet = { fed: 0, hunger: 2 } } = await $.state.get(PET);
61 const hunger = grew > 0 ? 0 : Math.max(0, (pet.hunger ?? 2) - 1);
62 await $.state.set(PET, { fed: (pet.fed ?? 0) + 1, hunger, grew });
63}
64