SLOPSHOPPER

ginno-tamagotchi

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).

newband
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · ginno-tamagotchi
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM (^x^) pet full fed 2× · grew 97k [ feed ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
(^x^) pet full fed 2× · grew 97k [ feed ]
README

Ginno

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.

License: MIT Platform-black.svg) No cloud

English · 简体中文

Ginno chat workspace


What it is

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.

  • 🧠 Multi-agent chat — switch agents mid-conversation, shared history, per-agent memory
  • 🛠️ Tools · permissions · skills · MCP · hooks — Claude Code's shape, all built in
  • 🔀 Visual Workflow Studio — a versioned DSL you compile into a LangGraph state graph, with a live run observer
  • 📚 Local knowledge base — index your Obsidian vault; answers come back with citations
  • 🎯 Long-horizon goals + TODO sync — let the agent keep working toward a standing objective
  • 🌐 Embedded browser + web search — retrieval with source attribution, not vibes

Full feature walkthrough: docs/user-guide.md.

Workflow StudioLocal knowledge base
Workflow StudioKnowledge base

Why "local-first"

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.


Install

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).


Stack

LayerChoice
ShellTauri 2 (Rust) — process management + webview only
UINext.js 14 (static export) + React 18 + Tailwind
RuntimePython + FastAPI + LangGraph (sidecar, bundled with PyInstaller)
Storageplain files (no DB); optional LanceDB for semantic-search vectors

Architecture and subsystem designs: docs/architecture.md.


Status

  • A personal project, used daily (dogfooding) and iterated fast — ~2.5 months, 220+ commits.
  • Early and unpolished; APIs and UI still move.
  • Issues, PRs, and ideas are welcome — in English or Chinese.

License

MIT © 2026 ChuanchuanSong

Source 1 files
hooks/tamagotchi.mjs 64 lines
1// 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