No description.

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/sample.ts 53 lines1// The integration fixture: one hook per behavior the broker contract needs —
2// a matcher'd chain hook, a renderer with a press button, a double-`next`
3// probe, a throwing hook with a `.catch`, and a timer scheduler.
4
5export function register(on, options) {
6 // Rewrites the event input, reads a `$` op, and reshapes the result.
7 on('tool.call', { tool: /^Bash/ }, async ($, e, next) => {
8 const session = await $.session.id()
9 const beneath = await next({ ...e, note: `seen:${session}` })
10 return { ...beneath, observed: true }
11 })
12
13 // Draws a band whose Button carries a callback, and reads the pushed option.
14 on('ui.render', async ($, e, next) => {
15 const ui = $.ui.resolve(e)
16 await next(e)
17 return ui.Box({
18 flexDirection: 'column',
19 children: [
20 ui.Text({ color: 'cyan', children: `units=${String(options.units)}` }),
21 ui.Button({ label: 'Refresh', onPress: value => `pressed:${String(options.units)}${value === undefined ? '' : `:${String(value)}`}` }),
22 ],
23 })
24 })
25
26 // Calls `next` twice; the runner must send the frame once.
27 on('turn.start', async ($, e, next) => {
28 const first = await next(e)
29 const second = await next(e)
30 return { same: second === first }
31 })
32
33 // Throws; its `.catch` answers from `next.error`.
34 const ending = on('session.end', () => {
35 throw new Error('boom')
36 })
37 ending.catch(async ($, e, next) => ({ handled: next.error?.kind === 'throw' && next.error.message === 'boom' }))
38
39 // Schedules a broker timer whose callback logs through `$`.
40 on('session.start', async ($, e) => {
41 let bound = undefined
42 $.clock.after(5, () => { void bound.ui.log('tick') })
43 bound = $
44 return { cwd: e.cwd }
45 })
46
47 // Hands a later mod's serialized tree through unchanged.
48 on('turn.complete', async ($, e, next) => await next(e))
49
50 // Registers against an event this host never raises; the load log warns.
51 on('telemetry.mark', () => ({}))
52}
53