Memento is a local-only markdown editor: it opens a workspace folder on disk and has no network, auth, or git layer. The intended use is as an editor UI over a memory layer stored in a private GitHub repo — the memory the Poke AI agent reads and writes. Today that repo can only be viewed on github.com or cloned by hand. There is no way to authenticate to a private repo, no way to keep the local copy fresh as the agent writes to it remotely, and no way to push the user's occasional edits back.
Turn Memento into a local editor over a GitHub-backed memory repo, where:
- The repo is cloned to a fixed, visible local path
(
~/Desktop/Memento/<repo-name>/) and opened as the workspace. - The local clone is plain markdown on disk so any agent (Poke, Claude Code, scripts) can read it directly without git or a token. A pointer file records the active path for programmatic discovery.
- Auto-fetch keeps the local copy fresh as the agent writes remotely (interval + window focus + manual "fetch now"). Fetch fast-forwards.
- A subtle status-bar sync control shows sync state and pushes the user's local edits on click (commit all dirty files + push; fetch-then-retry if the remote moved).
- Auth is a GitHub Personal Access Token (validated, then stored in the OS keychain).
- Git transport is embedded
git2(libgit2) — no shelling out to systemgit, no "is git installed" dependency, self-contained in the.app.
- Single logical author. The user and the Poke bot both act as the user's GitHub account. There is effectively one author.
- Agent is the primary writer; the user mostly reads + occasionally edits. Conflicts are rare. Fetch is expected to fast-forward; push is expected to succeed or need one fetch-retry.
- Therefore: no merge-conflict UI. The only divergence path that needs handling is "local can't fast-forward" → stop and surface an explicit choice, never silently clobber an unpushed edit. (A read-only diff viewer for local changes exists — see the Source Control view below — but there is no three-way conflict resolver.)
- A Source Control view (full tab, opened from the prominent top-right sync
button) lists per-file working-tree changes, shows each file's diff
(read-only CodeMirror merge of HEAD vs working tree), offers per-file discard,
and commits-all + pushes from a message box. A Stats tab renders a
commit-activity heatmap from local git history. Backend commands:
github_changed_files,github_file_diff,github_discard_file,github_commit_history(all incommands/github_sync.rs).
- Personal Access Token: the user creates a classic PAT with the
reposcope (private repo read/write) and pastes it into the connect dialog. The backend validates it withGET /userbefore storing — an invalid token is rejected and nothing is persisted. No registered GitHub OAuth app (and no compiled-in client id) is required. - Token stored in OS keychain via the
keyringcrate (servicecom.memento.github, accountoauth-token). Never written to the settings store or any plaintext file. Read on demand (crate::github) — there is no in-memory cache, so the keychain is the single source of truth for signed-in state. - IPC:
github_save_token(token) -> { login }(validates, then writes token to keychain on success),github_signed_in() -> bool,github_sign_out()(deletes token). - An HTTP client (
ureq) is used for theGET /uservalidation call. Git transport itself usesgit2, not HTTP.
- After auth:
github_list_repos()lists the account's repos (GitHub RESTGET /user/repos, private included) so the user can pick the memory repo; a manualowner/nameentry is also accepted. github_clone_repo(full_name):- Clones via
git2to~/Desktop/Memento/<repo-name>/using the keychain token as the credential (RemoteCallbacks::credentials→Cred::userpass_plaintext(token, "")). - If the destination already exists: when it's an existing clone whose
originmatchesfull_name(repo_matches_origin, comparingowner/repocase-insensitively and ignoring scheme/host/.git), reuse it (rewrite the pointer + open as workspace) instead of re-cloning; an unrelated directory at the path is rejected withAlreadyExists. - Writes the pointer file
~/.memento/state.json:{ "memoryPath": "<abs clone path>", "repo": "<owner/name>" }. - Opens the clone as the workspace (existing restore/open path).
- Clones via
- No separate sync config is persisted: the active memory repo is simply
whichever workspace is open, and the
~/.memento/state.jsonpointer records the path for external agents. The access token (keychain) is the only stored secret. Sync state is derived live fromgit2on the open workspace, so there is nothing to keep coherent across a settings file.
github_fetch():git2fetch fromorigin, then attempt fast-forward of the checked- out branch to the upstream.- If fast-forward succeeds: working tree files are updated → the existing
file watcher fires
fs:file-changed→ UI refreshes (free live update). - If the branch cannot fast-forward (local diverged — user has unpushed
edits that touch the same history): do not reset. Return a
divergedstatus; the frontend shows a banner ("Local changes diverged from remote — push or discard") and offers push / discard-local actions. Fail explicitly, never auto-clobber.
- Triggers (frontend): an immediate fetch on app open and on workspace switch
(right after the status snapshot, gated on
signedIn && isRepo), then an interval timer (~67s, off the round minute so installs don't poll in lockstep), window focus, and tab visibility. All call the samegithub_fetch; the sync store'sphaseguard coalesces overlapping triggers. The open/switch fetch is what keeps a reopened app from trusting a stale local HEAD and clobbering remote commits on the next push. github_fetchuses an explicitrefs/heads/<b>:refs/remotes/origin/<b>refspec so the remote-tracking ref stays current;github_sync_statusthen reportsahead/behindcommit counts (local vsorigin/<branch>) viagraph_ahead_behind. The sync button rendersbehindas an incoming ↓N indicator, and ↓N ↑N together when diverged.- No watcher coupling (decided during impl): git's working-tree writes
during fetch are intentionally not suppressed. Push is a manual button
(never auto-fired) so pulled writes can't start a push loop; the dirty count
comes from
git status(tree vs HEAD), not watcher events, so fetched files never inflate it; and we want a fetched change to firefs:file-changedso the open editor reloads fresh remote content. A fast-forward only runs when the tree is clean (see Fetch above), so there is no in-progress edit to lose. This is simpler and more robust than bracketing the checkout in the self-write guard.
github_push():- Stage all changes (
index.add_all), commit with an auto message (memento: sync <n> file(s) <iso-timestamp>; timestamp passed in from the frontend since the backend clock helper is fine here — orchrono), push toorigin. - One commit per push-press, bundling all dirty files (not per-file).
- If push is rejected (remote moved since last fetch): run
github_fetchfirst; if it fast-forwards, retry push once. If fetch reportsdiverged(same file touched both sides), stop and surface the divergence banner — do not force-push.
- Stage all changes (
- The sync control lives in the status bar / bottom corner. It is both
indicator and button:
synced(✓),dirty(↑N — N files to push, click to push),fetching/pushing(spinner),diverged(! — opens the banner),offline/error(subtle warning).- Dirty count derives from
git2status (working tree vs HEAD).
stores/sync-store.ts— Zustand slice holding sync state (signedIn,isRepo,phase,branch,dirtyCount,diverged,error); a module-leveluseWorkspaceStoresubscription re-seeds it on workspace change.hooks/use-sync-triggers.ts— owns the interval + focus + visibility listeners (side effects in one place per the guidelines).components/github/—connect-store+connect-dialog(onboarding state machine),sync-control(status-bar indicator/button),divergence-banner.- IPC wrappers live in
lib/tauri.tsalongside the rest. - Onboarding flow (first run, no memory configured):
- Prompt "Connect GitHub" → paste a Personal Access Token (validated).
- List private repos → user selects the memory repo.
- Clone to
~/Desktop/Memento/<repo>/→ write pointer file → open as workspace. - Thereafter: auto-open that workspace, auto-fetch running, sync control live.
- No 3-way merge, no conflict-marker editing. The only divergence handling is the explicit push-or-discard banner. This matches the single-author, agent-primary-writer model; revisit only if the user starts editing heavily in parallel with the agent.
- Discard-local (from the divergence banner) hard-resets the working tree to the remote and is the one destructive action — it is always explicit and confirmed, never automatic.
- Token has
reposcope (full private repo access). A future fine-grained / per-repo scoping pass is out of scope for v1. - Multi-repo / multiple memory vaults is out of scope for v1 (one active memory repo at a time); the pointer file is shaped to allow a list later.
- New crate dependencies:
git2,keyring, and an HTTP client (reqwest/ureq).git2pulls a C dependency (libgit2) that must be vendored/linked in the notarized macOS build.