group conversations with models and local files
0

Configure Feed

Select the types of activity you want to include in your feed.

48 1 0

Clone this repository

https://git.vm.fail/burrito.space/hezo https://git.vm.fail/did:plc:onarb3evb2dc6coe6i6cmbzq
ssh://git@knot1.tangled.sh:2222/burrito.space/hezo ssh://git@knot1.tangled.sh:2222/did:plc:onarb3evb2dc6coe6i6cmbzq

For self-hosted knots, clone URLs may differ based on your setup.


README.md

hezo#

Small-group experiment: a multi-user Claude chat app, synced over iCloud Drive using Automerge. Single Electron app, file-per-actor sync, branching-DAG conversations under the hood.

What this is (and isn't)#

It is a skeleton — enough to demonstrate the architecture end-to-end:

  • Sign in with email + Anthropic API key (key stored in macOS Keychain).
  • Create a project, which becomes a folder under your sync root.
  • Create conversations and send messages; Claude replies are billed to whoever hit send.
  • Two clients pointed at the same project folder merge each other's changes.

It is not production-ready. See "stubbed / missing" below.

Run#

Requires Node 20+ and macOS (for the iCloud default and the Keychain integration via keytar).

npm install
npm run dev

If you get an ABI mismatch from keytar, run:

npx electron-rebuild -f -w keytar

Sync architecture#

  • One project = one folder under your sync root (default ~/Library/Mobile Documents/com~apple~CloudDocs/Hezo/, picker in UI).
  • One file per actor: doc.<userId>.automerge. Your client only ever writes its own file; it reads everyone else's. This sidesteps iCloud's "conflict copy" behavior when two clients write the same file concurrently.
  • Watcher (chokidar) merges any peer file on change.
  • Blobs (attachments, future-large content) live in blobs/<sha256> and are referenced by hash from the doc. Not used yet.

Data model#

Defined in src/shared/schema.ts. Highlights:

  • Turn is immutable once written. "Editing" a turn means creating a sibling with the same parentId. So the conversation is a DAG, not a list.
  • Two users replying to the same assistant turn simultaneously each create a child branch — no CRDT conflicts.
  • Mutable metadata (title, system prompt, role) uses Automerge's default LWW.
  • Sets are { [id]: true } objects, not arrays, to avoid array-merge ordering surprises.

The UI currently flattens the DAG into "the chain to the most recent leaf" and ignores other branches; the data is there to do better.

Testing peer-to-peer locally#

You need two Electron instances writing as different users into the same project folder. Easiest path:

  1. Sign in as alice@…, create a project. Note the project folder path.
  2. Quit the app, run again with a different userData to get a separate identity:
    npm run dev -- --user-data-dir=/tmp/hezo-bob
    
    Sign in as bob@…. In the project list, change the sync root to the same parent dir Alice used, then open Alice's project.
  3. Send a message from one window; watch it appear in the other within ~1s (no iCloud involved — chokidar reacts to the same local file).

For real iCloud testing, two machines logged into the same Apple ID with the folder shared via iCloud Drive sharing.

Stubbed / missing#

  • No branching UI — the DAG exists in the data; the renderer always shows the latest leaf chain.
  • No member-invite flow — peers join a project by being given the sync folder out of band.
  • No presence indicators — no "alice is composing".
  • No file attachments / files-in-project.
  • One project open at a time.
  • No system-prompt or model editor UI — defaults baked in (claude-sonnet-4-6).
  • No conflict-copy reconciliation — if iCloud does produce doc.… 2.automerge files we don't merge those. Real fix is the file-per-actor scheme already in place (which makes this rare), plus a periodic sweeper.
  • Orphaned streaming turns — if a client closes mid-stream, the assistant turn stays at status: 'streaming' forever in the saved doc. A startup sweep should mark stale streaming turns from this actor as error.
  • Full-doc save per flush — during streaming we call Automerge.save on the whole doc every 250ms. Fine for small docs; for long-running projects switch to Automerge.saveIncremental and a separate changelog file.

File map#

src/
  shared/schema.ts        types shared between main and renderer
  main/
    index.ts              app entry, IPC handlers, project lifecycle
    sync.ts               ProjectSync: Automerge + file-per-actor
    claudeApi.ts          Anthropic SDK call + DAG → message list
    keychain.ts           keytar wrapper
  preload/index.ts        contextBridge surface (`window.mc`)
  renderer/
    index.html
    src/main.tsx
    src/App.tsx           Login → ProjectList → ProjectView → ConversationView