Skip to content
fmicalizziPublic

About

The context mesh for autonomous agents — unified observability, telemetry, and inter-agent coordination https://fmicalizzi.github.io/moolmesh/

Resources

Contributing

Stars

4 stars

Watchers

0 watching

Forks

Repository files navigation

MoolMesh CLI

MoolMesh

The context mesh for autonomous agents.

Unified observability, telemetry, and inter-agent coordination — running entirely on your machine.

License: MIT Python 3.11+ Tests: 2523 passing Zero Dependencies PyPI Español


Why MoolMesh?

Modern software development isn't human-to-keyboard anymore. It's an ecosystem of AI agents working in parallel — each with its own logs, token counters, and reasoning traces, all locked in separate silos.

When Claude Code gets stuck in a loop, your other agents don't know. When you spend tokens across several providers, you can't see which git commit justified it. When your team uses different AI tools on the same repo, nobody has the full picture.

MoolMesh congregates what is scattered. It auto-discovers sessions from every major AI coding agent, normalizes them into a single queryable database, and exposes that state to both humans (via a dashboard) and machines (via MCP).

Read our Philosophy to understand the dual axiom behind MoolMesh: Human-First & Agent-First.


What You Get

Four views in a single browser tab:

View What it shows
AI Sessions Live event feed from all agents — messages, tool calls, token usage, models
Analytics Token consumption by provider, hourly activity, top tools, top projects
Project Pulse PR kanban, issues list, milestones, GitHub Projects v2 board
Code Timeline Commit feed, author stats, hot files, daily/weekly digest narratives

Plus a MCP server that lets other AI agents query your session data programmatically — enabling agent-to-agent supervision and orchestration.


Quick Start

Install

Requires Python 3.11 or later. MoolMesh uses match/case, tomllib, and other features that don't exist in earlier versions. Python 3.10 reached end-of-life in October 2026.

# Recommended — isolated install, global command
pipx install moolmesh

# Or with pip (inside a virtual environment)
pip install moolmesh

Note: On modern Linux (Ubuntu 22.04+, Debian 12+, Fedora 38+), pip install outside a virtual environment is blocked by PEP 668. Use pipx instead, or create a venv first:

python3 -m venv ~/.venvs/moolmesh && source ~/.venvs/moolmesh/bin/activate
pip install moolmesh

Windows

# Recommended — handles PATH and dependencies automatically
uv tool install moolmesh

# Alternative
pipx install moolmesh

# Or with pip (may need to add Scripts to PATH)
pip install moolmesh

If mool is not found after installing with pip, add the Scripts directory to your PATH:

pip show -f moolmesh | findstr Scripts
$env:PATH += ";C:\Users\YourUser\AppData\Local\...\Scripts"

Start

# Foreground (blocks the terminal)
mool dashboard

# Background daemon (recommended — keeps running after you close the terminal)
mool daemon start
# → open http://localhost:5200

The daemon runs on all platforms (macOS, Linux, Windows). Use mool daemon stop to stop it, mool daemon status to check (it exits 1 when a watcher is degraded).

mool daemon stop returns only once the process has exited; after 30 s it reports "still shutting down (PID N)" and exits 1. mool daemon start/restart wait for a daemon that is still shutting down, and for one that is still starting (for example, while a migration backs up a database: a one-time starting (migrating databases?) … notice past 10 s, up to 180 s), so a stop followed by a start never leaves it down.

That's it. MoolMesh auto-discovers your AI sessions immediately. No configuration needed.

Running from source:

git clone https://github.com/fmicalizzi/moolmesh.git
cd moolmesh

# macOS / Linux
python3 -m venv .venv && source .venv/bin/activate

# Windows (PowerShell)
python -m venv .venv; .venv\Scripts\Activate.ps1

pip install -e ".[dev]"
mool dashboard

Run at login

mool daemon start does not survive a reboot. To have the daemon come back on its own, register it with your OS supervisor (opt-in, nothing is installed by default):

mool daemon install            # register + start now (localhost:5200)
mool daemon install --host 0.0.0.0 --port 5200
mool daemon install --dry-run  # print the file and commands, change nothing
mool daemon uninstall          # remove exactly what install created
OS Mechanism File
macOS LaunchAgent, label dev.moolmesh.daemon (launchctl bootstrap gui/<uid>; falls back to load -w) ~/Library/LaunchAgents/dev.moolmesh.daemon.plist
Linux systemd --user unit (Restart=on-failure, logs in journalctl --user -u moolmesh.service) ~/.config/systemd/user/moolmesh.service
Windows Task Scheduler task MoolMesh at logon, with pythonw.exe (no console) record in ~/.moolmesh/autostart-task.json
  • The supervised process always runs python -m hub.cli daemon start --foreground with the interpreter you installed from, and the PATH of the install moment (launchd and systemd start with a minimal one; without gh on it the GitHub sync would lose its token). Reinstalled or moved MoolMesh? Run mool daemon install again — mool doctor flags a registration whose interpreter no longer exists.
  • With the autostart installed, mool daemon start|stop|restart go through the supervisor (systemctl --user …, launchctl kickstart/kill, schtasks /Run), so there is never a second daemon next to the supervised one.
  • install never overwrites a file it did not create (e.g. a hand-written moolmesh.service): it exits 1 and shows the diff. --force replaces it after saving a .bak-<date> copy (for launchd the copy goes to ~/.moolmesh/).
  • Linux: without linger a user unit only starts after you log in. install checks loginctl show-user "$USER" -p Linger and, if it is off, tells you to run loginctl enable-linger "$USER" (it never runs it for you).

Stale data. MCP tools that read the portfolio (get_portfolio_grouped, get_portfolio_production) and get_server_info carry a freshness block — daemon_running, last_git_sync_at, last_github_sync_at, repos_stale, last_ingest_at, data_age_seconds, stale — plus a top-level stale: true when the daemon is down or the last git / GitHub API sync is older than [daemon] stale_after_minutes (default 60) in ~/.moolmesh/config.toml. The dashboard shows a banner on every page in that case, and mool doctor prints a freshness line. daemon.log is rotated to daemon.log.1 past 10 MB.

Storage bounds. The full text stored per event (a tool's output, a long message) is capped at [daemon] full_text_max_chars characters (default 256000; 0 = no cap): over it MoolMesh keeps the head and the tail around a [… N caracteres omitidos por MoolMesh …] marker, so a 20 MB tool output can no longer bloat events.db. The cap applies to new rows; text stored earlier is left as is.

History is history on any path. An event whose own time is older than the 12 h live window when MoolMesh first reads it is stored as history (events.historical = 1), whether it came from the live watcher, the startup catch-up or mool backfill: the first read of an OpenCode or Cursor database (months of sessions) no longer floods the live feed and SSE, and the lines a resumed Claude session replays with their original dates stay out of "recent"; its new messages are live as usual.

Backups. Before rewriting existing rows, migrations and mool backfill --reparse copy the database to ~/.moolmesh/backups/; only the newest 5 events-* and 5 workspace-* copies are kept (older ones are deleted after each new backup, never the one just made).

Local day. Digests, the workspace rollup and get_portfolio_production share one definition of "which day": the zone in [user] timezone (an IANA name such as "America/Costa_Rica"; unset = the system zone; an unknown name falls back to the system zone with a warning in the log). A PR merged at 01:00 UTC is in the previous day's digest at UTC-6. get_portfolio_production(days) returns merged_prs_window and closed_issues_window (merged / closed on a local day of the window) next to the all-time merged_prs and closed_issues.

Days worked. The production strip counts a session on every local day it had events on a project, not only on its last one: a session resumed over three days lights three days. Each day counts distinct sessions; sessions counts distinct sessions in the window (never the sum of the days) and active_days the distinct days. The days live in workspace.db (edge_active_days). The first daemon pass after the upgrade fills them from events.db, after a backup of workspace.db; changing [user] timezone re-derives them the same way. A session whose events are no longer in events.db is dated by its last event, as before.

The rollup views (get_portfolio, get_portfolio_grouped and its client tier, mool workspace portfolio, the /portfolio rows) carry the same figures next to the touches, additively: sessions_worked (distinct sessions) and days_worked (distinct local days) in the window — a client unions the sessions and days of its projects, so a session shared by two projects counts once. days_worked_basis says where they came from: edge_active_days, or rollup when the table is missing or bucketed in another zone (then days_worked is active_days and sessions_worked is null). session_touches and active_days keep their meaning. The CLI shows Nd (Nd/Mses worked).

Removed worktrees. Work done in a git worktree counts toward its repo after the worktree is removed. MoolMesh records each git root it sees on disk (known_git_roots in workspace.db; a linked worktree also records its main checkout), and a directory under a recorded root that no longer exists keeps that identity instead of showing up as a separate repo-less project. A session that touched both the checkout and a worktree counts once for the project. The first daemon pass after the upgrade, after a backup of workspace.db, keeps what was already attributed to a repo and folds back the removed worktrees it has evidence for — the repo's own .git/worktrees entry or a git worktree add a session ran from the repo — never a folder name: a deleted folder with no such evidence stays as it is. The counts go to hub.log. A git worktree add counts only from its full command: every provider whose tool call carries the command (Claude, Codex, Qwen, OpenCode, Pi, Antigravity) keeps it whole in events.db for a shell call that adds a worktree, so a worktree created and removed between two polls still folds into its repo. For history older than that, one more pass after the upgrade — again after a backup of workspace.db, and resumable — reads the commands from the agents' own session files (read only; only files that mention worktree are parsed). A command whose path uses a variable or that is piped does not count, and evidence that names two repos for one path folds nothing. No config change is needed; project_aliases stays the manual fallback for history with no evidence. The scratchpad of a session that ran inside a removed worktree folds into the worktree's repo too.

Agent scratchpads and throwaway repos. An agent's scratchpad folds into the project of the session that used it, on macOS (/private/tmp/claude-<uid>/…), Linux (/tmp/claude-<uid>/…) or any other temp root ($TMPDIR): the shape <tmp>/claude-<uid>/<encoded-cwd>/<session-uuid>/ is what counts. Other folders of the harness under <tmp>/claude-<uid>/ are tooling, never a project. A git repo without a remote that an agent created (git init) in a scratchpad folds into the session's project the same way, and one under an OS temp root (/tmp, /private/tmp, /var, /private/var, /usr) shows as a temporary folder instead of a project. A repo with a remote is keyed by that remote and stays its project wherever it was checked out. Nothing to configure; the next classification pass applies it.


Updating

pipx upgrade moolmesh --pip-args=--no-cache-dir         # pipx (recommended)
uv tool upgrade moolmesh --no-cache                      # uv tool
python -m pip install --upgrade --no-cache-dir moolmesh  # pip inside a virtual environment

mool daemon restart                                      # run the new version

--no-cache-dir / --no-cache skip the local cache: right after a release, a cached wheel (or a lagging mirror) can make the upgrade resolve the previous version. With a tool manager such as Scoop, use its own upgrade (scoop update moolmesh).

Then restart open agent sessions (or their MCP servers) so they load the new code. Every agent session keeps its own hub.mcp_server alive until it closes, so it keeps serving the old version after the upgrade and mool daemon restart; mool doctor lists the ones running old code.

What an upgrade does on its own. A migration that rewrites existing rows first copies the database to ~/.moolmesh/backups/ (the newest 5 events-* and 5 workspace-* copies are kept), and every applied migration is logged (Migration N applied: …) in daemon.log, or in hub.log when the CLI or an MCP server applied it. A first start that is busy with such a backup can take a while; daemon start/restart wait for it (see Start). Each version's upgrade notes are in CHANGELOG.md.

Coming from an older version — only what still needs an action. Each item applies if the version you are upgrading from is older than the one named:

  • Older than 1.26.2: MCP servers started by 1.26.1 or earlier cannot report that they are stale, so mool doctor does not list them. Restart every open agent session once.
  • Older than 1.27.2: mool daemon start does not survive a reboot. Register the daemon with mool daemon install (see Run at login), from the pipx / uv tool install, not from a development virtualenv.
  • Older than 1.28.0, OpenCode users: run mool backfill --reparse opencode (a simulation that writes nothing), then mool backfill --reparse opencode --yes (events.db is backed up first). It recovers the replies, reasoning and tool details earlier versions stored without text, and re-stores OpenCode events with the UTC timestamps (and fingerprints) 1.28.0 introduced.
  • Older than 1.28.1: backups now prune themselves to the newest 5 of each kind; copy aside any older backup in ~/.moolmesh/backups/ you want to keep. Empty -wal / -shm files that earlier versions left next to a provider's database are not removed; delete them by hand while the provider is closed.
  • Older than 1.29.0: PRs cached before the upgrade get their stats with mool repo backfill-pr-stats --all-registered (200 PRs per run; rerun to continue).
  • Older than 1.35.0, Antigravity desktop/IDE users: run mool backfill to bring in history older than the startup catch-up window.

Downgrading is not supported once a newer version has migrated your databases. In particular, versions before 1.30.0 read kind = "remote" and kind = "board" repos as code repos: do not downgrade with such repos registered.

Windows: a running daemon can lock files inside the pipx venv and make pipx upgrade fail or apply only partially — stop it first (mool daemon stop), upgrade, then mool daemon start.


Production Install

For system-wide access (run mool from any directory):

# Recommended — isolated venv, global binary
pipx install moolmesh

# Or with pip (requires a venv on modern Python)
pip install moolmesh

systemd service (Linux)

mool daemon install (see Run at login) writes and manages this unit for you; the manual unit below is still supported.

mool daemon start works as a foreground entry point — MoolMesh auto-detects systemd and skips the double-fork:

# ~/.config/systemd/user/moolmesh.service
[Unit]
Description=MoolMesh Dashboard
After=network.target

[Service]
Type=simple
ExecStart=%h/.local/bin/mool daemon start --port 5200
Restart=always
RestartSec=5

[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user enable --now moolmesh
systemctl --user status moolmesh

Why Restart=always: according to systemd.service(5), SIGHUP, SIGINT, SIGTERM and SIGPIPE count as a clean exit, and Restart=on-failure only revives non-zero exits or unclean signals. So with Restart=on-failure a kill -TERM — including a mool daemon stop from outside — would never be revived, whatever the exit code. Restart=always revives the dashboard after any exit; systemctl --user stop moolmesh still stops it for good, because systemd never restarts a service it stopped itself.

Restart=on-failure remains a valid choice if you only want real failures revived: mool daemon start/mool dashboard exit 1 when they cannot start (port already in use, another MoolMesh on the port), and a supervised run never silently moves to the next port. But a terminated process will stay down until you start it again.

Note: mool daemon start auto-detects systemd ($INVOCATION_ID) and stays in the foreground, so Type=simple works correctly. Outside systemd, it double-forks as usual; --foreground forces the same behavior without the environment marker. When managed by systemd, use systemctl --user stop/restart moolmesh instead of mool daemon stop/restart — MoolMesh warns, but it cannot restart the unit for you.


Supported Agents

Provider Session source Format
Claude Code ~/.claude/projects/ JSONL per session + subagent logs
Codex (GPT-5) ~/.codex/sessions/ + state_5.sqlite Rollout JSONL + SQLite metadata
Qwen CLI ~/.qwen/projects/ JSONL per chat
OpenCode ~/.local/share/opencode/opencode.db SQLite (session → message → part)
Cursor ~/Library/Application Support/Cursor/User/ (macOS) SQLite (state.vscdb key-value: composer bubbles)
Pi ~/.pi/agent/sessions/ ($PI_CODING_AGENT_DIR) JSONL session tree per session
Antigravity (CLI, desktop app / IDE) ~/.gemini/antigravity-cli/ ($ANTIGRAVITY_APP_DATA_DIR) and ~/.gemini/antigravity/ JSONL transcript per conversation + SQLite session index (read-only)

Sessions are auto-discovered on startup. No configuration, no API keys, no cloud services.


Git & GitHub Integration

Register a git repository to unlock Project Pulse and Code Timeline:

cd /path/to/your/repo
mool repo add                            # Registers the current directory
mool repo add --remote acme/widgets      # No clone: issues, PRs and PR stats only
mool repo add --owner acme --dry-run     # Every repo of an org or user, as remote

This ingests commit history and starts polling GitHub for issues, PRs, milestones, and Projects v2.

mool repo list                           # Show registered repos
mool repo remove                         # Unregister current repo
mool repo sync --all                     # Re-ingest full history
mool repo backfill-pr-stats --all-registered --limit 200   # +/-, files, closed issues of older PRs

All repo subcommands default to the current directory when no path is given.

Per-PR stats: the GitHub poll also stores, for each new or updated PR, its additions, deletions, changed files and the issues it closes (closingIssuesReferences, qualified as owner/repo#n — a PR can close issues in other repos). One GraphQL call covers up to 25 PRs, at most 50 PRs per poll; if the call fails the sync continues and the PR stays without stats. PRs cached before the upgrade (or past the per-poll cap) are filled with mool repo backfill-pr-stats [PATH | --all-registered] [--limit N] — bounded and resumable, it only asks for PRs still missing stats. The dashboard's PR pipeline (/api/github/prs) carries additions, deletions, changed_files and closes.

Versioned snapshot export: mool snapshot export --repo OWNER/REPO --since YYYY-MM-DD --until YYYY-MM-DD [--format json] [--output FILE] prints one repo's snapshot for a range of local days ([user] timezone): issues opened and closed in the period, totals at the cut, merged PRs with their stats (+/-, files, closes as owner/repo#n) and a freshness field — a stable, documented contract (docs/schema/snapshot.v1.md, schema_version 1.0) for tools that build on MoolMesh. Two exports of the same range are byte-identical (no generation time in the body); --output writes atomically. A range the repo's last GitHub sync does not cover (e.g. one that includes today) is refused with exit 3 unless --allow-incomplete, which marks it freshness.complete = false. Agents get the same payload from the MCP tool get_snapshot(repo, since, until, allow_incomplete). It reads only github.db (no new API calls) and excluded repos answer as unregistered.

Per-device effort export (opt-in): mool device init [--label TEXT] creates ~/.moolmesh/device.json (mode 600: a random secret that never leaves the machine, your label, and a hash of the machine id — keep it out of dotfile sync). Then mool effort export --org ORG --timezone IANA --since YYYY-MM-DD --until YYYY-MM-DD --repo OWNER/NAME [--repo …] | --repos-from FILE [--output FILE] writes, for the listed repos only, one row per (repo, local day, provider) with sessions and touches (docs/schema/effort.v1.md, kind moolmesh.effort). Without a repo list or without device.json it refuses; a device.json copied from another machine is refused until mool device init --reset. The device id in the file is HMAC(secret, org) — a different id per organization. Unlisted repos, exclude_repos/exclude_owners and folders without a remote add nothing (no row, no count, no total). day is the local day in --timezone (the organization's zone, not this machine's); repo_id is the numeric GitHub id, so a transferred repo keeps its series. The primary measure is days worked (distinct days); sessions/touches are not additive across repos. The header carries generated_at, data_through and stale (the daemon's own record does not cover the period); re-exporting a period replaces it. Same data and arguments give identical rows. Read-only, no GitHub API calls, no author identities. mool device show [--org ORG] prints the label, the per-org id and whether the file belongs to this machine (never the secret).

Board repos (issues-only backlogs): a repo used only as an issue backlog, with no code, is registered as a board — mool repo add --board acme/backlog (no local clone needed; a local clone path also works). In config.toml that is kind = "board" on its [[repos]] entry (default "code", today's behaviour; a hand edit is picked up when the daemon restarts). A board runs no git fetch and no commit ingestion, only the GitHub issues sync (the same calls as any repo — no new API calls). The portfolio shows it as board · N abiertas · M cerradas instead of "0 entregado" and leaves it out of the merged-PR ranking. Agents read the backlog with the MCP tool get_board_issues(repo, state, label, assignee, min_age_days, max_age_days, days, limit): issues filtered by label, assignee and age, plus the issues opened and closed on a local day of the last days days ([user] timezone) and the open backlog by label. It works on code repos too.

Remote-only repos (no local clone): a code repo you follow only through GitHub is registered with mool repo add --remote acme/widgets — no clone and no git history; its issues, PRs and per-PR stats are synced like any code repo (kind = "remote" in config.toml, keyed remote:github.com/acme/widgets; --all also backfills every issue/PR now). A board stays distinct: a board is a backlog, a remote repo is code whose merged PRs are the delivery. To follow a whole organization or personal account: mool repo add --owner acme [--include 'widget*' ...] [--dry-run] [--limit N] lists its repos (paginated, at most --limit, default 300; the listing stops early when fewer than 100 API calls are left in the hour) and registers each one as remote; --dry-run only lists. The portfolio shows a remote repo with GitHub activity in the window as sin clon — never "0 entregado" for lack of commits; its merged PRs count as delivery. Identity is the numeric GitHub id (github.db migration 8, additive repos.github_id): the daemon checks each registered repo with one GET /repos/{owner}/{repo} at most every 6 h (an ETag makes an unchanged repo a 304, which does not count against the rate limit; a repo with no id yet is retried every 10 min), and when GitHub answers with a new owner/name for the same id — a transfer or a rename — the existing row and config entry are updated, so the issues/PRs series keeps its history; repo add --remote/--owner with the new name renames instead of duplicating. repo remove, repo sync and get_board_issues accept owner/repo for a clone-less repo.

GitHub token

A token is resolved automatically in this order:

  1. gh auth token (GitHub CLI — recommended)
  2. GITHUB_TOKEN environment variable
  3. ~/.moolmesh/config.toml → [github] token = "..."

For public repos, no token is needed — commit history works without GitHub API access.

For private repos, a token with repo scope is required. The easiest way:

gh auth login                            # Follow prompts, select repo scope

If you don't have the GitHub CLI, set the env var or add it to config:

# ~/.moolmesh/config.toml
[github]
token = "ghp_xxxxxxxxxxxxxxxxxxxx"

Without a valid token, mool repo add still works — it ingests local git history, but Project Pulse (issues, PRs, milestones) won't have GitHub data.


Workspace (observe the work, not just the sessions)

A session's directory name is not the project it worked on. The Workspace axis attributes every file-touch to the project that owns the file — recovering the real multi-project (M:N) picture, and making a folder visible even before or without any agent (VISION §6).

Attribute agent work you've already captured — a read-only pass over events.db that maps each absolute file_path to its owning workspace (identity ladder git-remote → git-root → path-hash, so it survives rename/move/clone and works even without git):

mool workspace backfill                  # populate workspace.db (reads events.db read-only)
mool workspace list                      # workspaces with session / file / fs-touch counts
mool workspace session <session_id>      # which workspaces a session touched
mool workspace sessions <workspace_key>  # which sessions touched a workspace

Observe folders directly — mark a root and MoolMesh watches it for file changes, so a materials-gathering folder, a non-CLI tool (e.g. a design app whose output is only observable by path), or non-software work lights up with zero agent or git activity. Strictly opt-in: with no roots marked, nothing is observed.

mool workspace root add ~/Projects/campaign --max-depth 4 --exclude drafts
mool workspace root list                 # marked roots
mool workspace root remove ~/Projects/campaign
mool workspace touches <workspace_key>   # filesystem touches attributed to a workspace

The watcher is pure-stdlib: a bounded recursive scan with an mtime cursor (no watch-per-file), sensible default excludes (VCS internals, dependency dirs, build outputs, sync/cache folders), and a max_depth bound. All path-touches land in a separate workspace.db — the events.db hot path and SSE stream are never touched. Set hide_project_names = true under [workspace] in ~/.moolmesh/config.toml to mask folder names in the visible surface.

See the portfolio — roll every path-touch into a machine-wide portfolio that is signal-agnostic: a workspace lights up whether the activity came from a session, the filesystem, or git. On top of it, a local delivery_candidate flags likely-delivered work — surfaced as a candidate with confidence, never as a fact (quiescence plus a recorded second signal: a session close, a git commit, or a new artifact at the root; no cloud entity, no LLM).

mool workspace rollup                    # (re)build the portfolio rollup + run the detector
mool workspace portfolio                 # hot workspaces, by signal
mool workspace delivery                  # delivery candidates (with confidence + the 2nd signal)

The dashboard adds a read-on-load /portfolio view over workspace.db (the SSE stream is untouched). Agents can read all of this over MCP via the read-only workspace tools (list_workspaces, get_session_workspaces, get_workspace_sessions, get_workspace_touches, get_portfolio, get_workspace_activity, get_delivery_candidates).

Team mark + order by config (v1.26) — every key is optional and empty by default:

  • [user] emails = ["you@example.com"] — your commit emails (case-insensitive). A hot project state sustained only by commits from other people carries team_only and the UI shows the · equipo chip; local sessions and filesystem touches always count as yours. Nothing per-person is ever exposed, and with no emails configured nothing is marked.
  • [workspace] project_aliases = { "<old path or key>" = "<canonical path or key>" } — fold a moved/renamed folder (or a container-internal path like /app) into the real project: its history, state and outcome move with it, and no origin row remains. A missing destination only warns.
  • [workspace] reference_containers = ["<path>"] — the folder and its children go to the "Contenedores / referencia" section (never projects), and their activity no longer makes the parent active. Works even if the folder no longer exists.
  • [workspace] temporary_containers = ["<path>"] — direct children render in their own visible, collapsed "Temporal" section, keeping their state and history.

One client across several owners, a shared registry, confidential repos (#95) — every key is optional and empty by default:

  • [workspace] client_owner_aliases = { "acme-labs" = "acme" } — map a whole owner (an organization or a personal account) to a client. Every repo of that owner, including one created tomorrow, lands under the client with no per-project override. The client attribution ladder, first match wins: client_overrides (per project) > client_owner_aliases (per owner) > personal_orgs / client_orgs > the rest (externos / referencia, shared folder). An alias also applies to a gitless parent folder (_acme-labs) and counts toward the client in the child-repo majority. An alias outranks personal_orgs, so an account that is "yours" on one machine can still be grouped under a client.

  • [workspace] client_registry = "~/shared/clients.toml" — an optional, read-only client registry several machines share so they group the same way (a relative path is relative to ~/.moolmesh). It is merged under the local values: local wins on a conflicting key, lists are unioned, and it is never copied into config.toml. A missing or invalid file logs a warning once and the local config applies alone. Format (every key optional; personal_orgs is deliberately per machine and not read from it):

    version = 1
    client_orgs = ["acme", "globex"]
    exclude_owners = ["initech"]
    exclude_repos = ["globex/secret-plans"]
    
    [client_owner_aliases]
    "acme-labs" = "acme"
    
    [client_overrides]
    "git_remote:github.com/globex/widget-services" = "acme"
  • [workspace] exclude_owners = ["initech"], exclude_repos = ["globex/secret-plans"] — confidential repos that are never read: no git fetch/git log, no GitHub API call (issues, PRs, milestones, projects, identity checks), repo add (path, --board, --remote, --owner) refuses them (--owner on an excluded owner refuses before listing anything), repo sync/backfill-pr-stats skip them, and they disappear from the portfolio, the MCP tools (get_portfolio, get_portfolio_grouped, get_portfolio_production, get_board_issues, get_snapshot) and /api/repos. Matching is case-insensitive; exclude_repos accepts owner/repo, github.com/owner/repo or a GitHub URL. A repo registered before it was excluded keeps its stored data — the daemon just skips it (logged once per repo). To delete that data: mool repo remove <path | owner/repo> (config entry and its github.db rows). Registering a local clone by path still reads its origin URL once to learn the owner, and then stops.

Per-person breakdown, opt-in (#83) — the portfolio outcome is contributor-agnostic by default. A team coordinator can turn on a breakdown by GitHub login, read only from what github.db already caches (no new GitHub call):

[team]
per_contributor = true                                  # default false
contributor_aliases = { "alice-acme-old" = "alice-acme" } # optional; several logins → one alias are summed
bot_logins = ["ci-runner-acme"]                         # optional; added to dependabot, *[bot], app/*

With it on, get_portfolio_production (MCP) and /api/workspace/portfolio/production carry per_contributor: true and, per project, contributors ({login or alias: merged PRs, closed / open issues, all-time and in the window, and PR lines +/- when the PR stats are cached}) and contributors_bots; the daily/weekly digests add a "Por persona" block; the dashboard's production card shows "vista por persona activa" and the breakdown in each project's tooltip. A merged PR is credited to its author, an issue to its first non-bot assignee (else its author); each item to exactly one person, so the per-person sum is the total minus bots. Bots — and items with no login — count in totals, never as a person. With the option off (or absent) every output is byte-for-byte what it was before. Digests already cached keep their text until regenerated.

Full history once: mool repo sync --all-registered [--all | --days N] syncs every registered repo in one bounded, resumable pass (one repo's error never stops the rest) and prints a summary. With --all it ingests the full git history and the full issues/PRs history (the incremental poll is capped at 1000 items per repo), marking each repo history_complete; until then the portfolio labels outcome totals desde <first ingested commit> instead of "all-time". Registered repos that never lit up locally fold into a collapsed "registrados sin actividad" subgroup per client, and a GitHub remote that was never registered reads "no medido" (distinct from "— sin repo" and from a measured "0 entregado").


MCP Server (Inter-Agent API)

MoolMesh exposes a read-only MCP server over stdio, allowing any MCP-compatible agent to query session data.

The MCP server uses PEP 723 inline script metadata for its dependencies (the mcp package). This keeps MoolMesh itself zero-dependency while allowing the MCP server to run standalone.

Quick setup

mool mcp setup                  # Claude Code (global, user scope)
mool mcp setup claude-desktop   # Claude Desktop (macOS/Linux/Windows)
mool mcp setup cursor           # Cursor IDE
mool mcp setup codex            # Codex (OpenAI CLI)
mool mcp setup qwen             # Qwen CLI
mool mcp setup opencode         # OpenCode
mool mcp setup antigravity      # Antigravity (~/.gemini/config/mcp_config.json)
mool mcp setup json             # Print config JSON for any MCP client

The command auto-detects your install method (pipx / uv tool / pip / venv), finds the correct Python and server paths, and checks for the mcp dependency. If mcp is missing it shows the exact command to install it, or use --install-deps to install automatically:

mool mcp setup --install-deps

--install-deps installs mcp>=1.2.0,<2 (the bound matters: mcp 2.x is incompatible) the way your install method needs:

Installed with --install-deps runs
pipx install moolmesh pipx inject moolmesh "mcp>=1.2.0,<2"
uv tool install moolmesh uv pip install --python <tool python> "mcp>=1.2.0,<2"
pip install moolmesh (venv or system) <python> -m pip install "mcp>=1.2.0,<2"

uv tool environments ship without pip, so python -m pip does not work in them. If uv is not on your PATH, reinstall with the dependency included instead: uv tool install --with "mcp>=1.2.0,<2" moolmesh --force.

Use --dry-run to preview without changing anything: for Claude Code it prints the claude mcp remove / claude mcp add commands it would run (it never runs them), and for the other clients the config it would write.

Manual configuration

If you prefer to configure manually, point the client at the Python interpreter of the environment where MoolMesh (and mcp) is installed, and run the server as a module (requires mcp>=1.2.0,<2 in that environment, e.g. pipx inject moolmesh "mcp>=1.2.0,<2" or uv pip install --python /path/to/python "mcp>=1.2.0,<2"):

{
  "mcpServers": {
    "moolmesh": {
      "command": "/path/to/pipx/venvs/moolmesh/bin/python",
      "args": ["-m", "hub.mcp_server"]
    }
  }
}

On Windows the interpreter is ...\Scripts\python.exe. Don't use uv run /path/to/hub/mcp_server.py: it runs the script in an isolated env without the hub package and fails with No module named 'hub'.

Available tools

All tools are read-only.

Tool Description
get_server_info Running vs installed version of this MCP server, plus a freshness block — call it if results look inconsistent: a stale server must be restarted
Events & sessions
get_recent_events Latest N events across all providers (offset for paging)
get_active_sessions Sessions active in the last N hours
get_sessions Sessions with metadata (title, model, branch, cwd), filterable by provider and branch
get_session_detail One session's metadata, stats and linked_sessions
get_session_events A session's events, paginated (up to 500 per page), with text_mode none / snippet / full; Pi sessions show only the active branch
get_token_usage Token consumption by provider
get_tool_stats Top tools used by AI agents
search_events Full-text search on event summaries (newest first)
search_session_content Full-text search on the stored full text of events
get_project_activity Complete project summary with stats
Correlation
get_branch_sessions Sessions correlated with a git branch
get_session_chain Sessions linked to a session (predecessors and successors)
Workspace & portfolio
list_workspaces Known workspaces with session, file and filesystem-touch counts
get_session_workspaces Workspaces a session touched
get_workspace_sessions Sessions that touched a workspace
get_workspace_touches Filesystem touches attributed to a workspace
get_workspace_activity Per-day activity of one workspace (session / filesystem / git)
get_portfolio Machine-wide portfolio rollup (with sessions_worked / days_worked)
get_portfolio_grouped The portfolio grouped by client → project → materials, with freshness
get_portfolio_production Production strip per project: days worked, outcome (all-time and in the window), optional per-contributor breakdown, freshness
get_delivery_candidates Likely-delivered work, as candidates with confidence
GitHub
get_board_issues A repo's issues by label, assignee and age, with opened/closed counts per local day
get_snapshot The versioned snapshot of mool snapshot export (docs/schema/snapshot.v1.md)

Resources: hub://schema (database schema), hub://projects (project list with stats).

The server opens SQLite in read-only mode (?mode=ro). It runs as a separate process (~15-20 MB RAM), independent from the dashboard.

A server that is older than the installed version says so: get_server_info reports stale: true, tools that return an object add a stale_server key, and the first detection is logged once. Each server also records itself in ~/.moolmesh/run/mcp-<pid>.json (removed when it exits) so mool doctor can list the ones running old code — that is MoolMesh's own state, not provider data.


Digest Narratives

Code Timeline generates daily and weekly digests for each registered repo:

Level What When
L1 Raw SQL stats (commits, PRs, issues, LOC) Always available
L2 Structured template with bullet points Always available
L3 LLM-generated narrative paragraph When an LLM provider is configured

L3 works with any OpenAI-compatible API. Configure in ~/.moolmesh/config.toml:

[llm]
provider = "openrouter"
api_url  = "https://openrouter.ai/api/v1"
model    = "google/gemma-4-31b-it:free"
api_key  = "sk-or-v1-..."

Supported providers: OpenRouter, OpenAI, Together, Groq, Ollama. If the LLM is unavailable, digests fall back to L2 automatically.


Batch Reports

Generate Markdown analysis reports from the command line:

# Auto report — writes to ~/.moolmesh/reports/
mool report auto

# Full content (no truncation)
mool report auto --complete

# Filter by project or provider
mool report --project myapp --provider claude --output ./exports

CLI Reference

mool <command> [options]

Commands:
  dashboard              Start the live monitoring dashboard (foreground)
  daemon start           Run the dashboard as a background service (--foreground: stay attached)
  daemon stop            Stop the background service (returns once it has exited)
  daemon status          Show daemon PID, uptime, autostart, log size (exit 1 if a watcher is degraded)
  daemon restart         Restart the background service
  daemon install         Start the daemon at login (launchd / systemd --user / Task Scheduler)
  daemon uninstall       Remove the autostart created by daemon install
  status [--json]        Quick alias for daemon status
  doctor                 Run system diagnostics (freshness, autostart, stale MCP servers)
  install                Install mool command globally (~/.local/bin)
  discover [--json]      List all discovered AI agent projects
  backfill               Import session history outside the live window
  sessions               List sessions with metadata (--hours, --provider, --branch, --json)
  export SESSION_ID      Export a session transcript (--format markdown|json, --output FILE)
  link SOURCE TARGET     Link two related sessions (--type continues|references|reviews)
  chain SESSION_ID       Show the sessions linked to a session
  detect-links           Detect temporal links between sessions (--auto stores them)
  report [auto]          Generate batch Markdown analysis reports
  repo add [PATH]        Register a git repo (default: current directory; --board, --remote, --owner)
  repo list              List registered repos with commit counts
  repo remove [PATH]     Unregister a repo (default: current directory; owner/repo if no clone)
  repo sync [PATH]       Re-ingest commit history (--all, --all-registered)
  repo backfill-pr-stats Fetch +/-, files and closed issues of cached PRs
  snapshot export        Versioned JSON snapshot of a repo for a range of days
  device init|show       Opt-in device identity for the effort export
  effort export          Per-device effort (listed repos, local days) as JSON
  workspace ...          Workspace axis: backfill, list, session, sessions, touches,
                         rollup, classify, portfolio, delivery, root add|list|remove
  mcp setup [TARGET]     Configure MCP server (claude-code|claude-desktop|cursor|codex|qwen|opencode|antigravity|json)
  query events           Recent events as JSON
  query sessions         Active sessions as JSON
  query tokens           Token usage by provider as JSON
  query tools            Top tools used by agents as JSON
  query search TEXT      Search events by text as JSON
  query project NAME     Project activity summary as JSON
  query chain SESSION_ID Session chain as JSON

Global options:
  --version              Show version and exit

Dashboard / daemon options:
  --port PORT            Server port (default: 5200)
  --host HOST            Server host (default: localhost)
  --project NAME         Filter to project name
  --providers LIST       Comma-separated: claude,codex,qwen,opencode,cursor,pi,antigravity
  --log-level LEVEL      dashboard only: DEBUG|INFO|WARNING|ERROR (default: INFO)

Backfill options:
  --provider P           claude|codex|qwen|pi|antigravity|all (default: all file-based providers)
  --since YYYY-MM-DD     Only files modified on/after this local date
  --dry-run              Count what would be processed; write nothing
  --limit N              At most N files with new data per run (re-run continues)
  --reparse codex|opencode  Re-ingest with the current parser (simulates; --yes applies, after a backup)

Report options:
  --complete             Full-content mode: no truncation
  --daily                Only day-level reports (auto mode)
  --output DIR           Output directory
  --provider PROVIDER    Filter by provider

Every subcommand has its own --help.

Session transcripts: mool export SESSION_ID writes Markdown (default) or JSON (--format json) to stdout or --output FILE, with every event of the session. For Pi sessions, which are trees (/tree goes back and branches), it shows only the branch the user is on; abandoned branches stay in the database and in every other view. Other providers' sessions are shown in stored order.

Agent-friendly CLI (mool query)

For agents that don't have MCP support, mool query exposes the same data as the MCP server via stdout JSON:

# Get the last 10 events
mool query events -n 10

# Active sessions in the last 2 hours
mool query sessions --hours 2

# Token consumption by provider since a date
mool query tokens --since 2026-06-01

# Top tools used in a project
mool query tools --project moolmesh -n 5

# Search for events mentioning "daemon"
mool query search "daemon" --provider claude

# Full project activity summary
mool query project moolmesh

All output is valid JSON — pipe to jq, parse with any language, or use from agent subprocess calls. Also: mool status --json and mool discover --json for machine-parseable output.

Before the first daemon start there is no ~/.moolmesh/events.db yet. mool query … and mool sessions --json then print a JSON error object on stdout and exit 1 (the same no_data exit as mool snapshot export and mool effort export), without creating the database:

{"error": "no_data", "message": "...", "hint": "mool daemon start"}

A database that exists but cannot be read answers the same way with "error": "db_unreadable". Plain mool sessions prints the hint on stderr and exits 1. (mool status --json keeps exit 0 with "running": false: "not running" is a valid status, while a query with no data is not an answer.) The mcp SDK is optional: without it mool query and mool sessions work and print nothing on stderr; only starting the MCP server reports the missing SDK.

Health endpoint

When the dashboard is running, GET /health returns:

{"status": "healthy", "pid": 4242, "version": "1.36.1", "uptime_seconds": 3600, "events_count": 45231, "watchers": {"...": "..."}}

status is degraded when a running watcher is dead or stalled. GET /api/freshness returns the same freshness block as the MCP tools.


Architecture

hub/
  parsers/         JSONL + SQLite parsers for each provider
  adapters/        Normalize provider entries → unified events
  watchers/        File harvesters: discover → offset → parse → store → SSE
  harvesters/      GitHarvester (120s) + GitHubHarvester (15s/60s)
  integrations/    GitHubClient (REST + GraphQL) + LLM clients
  digests/         L1 Stats → L2 Template → L3 LLM narrative
  correlation/     AI ↔ Git links: Co-Author, issue refs, timestamps
  dashboard/       HTTP server + SSE + 4 HTML pages
  cache/           EventStore (events.db) + GitStore (github.db)
  mcp_server.py    MCP stdio server (read-only, PEP 723 inline deps)
  cli.py           CLI entry point

How data flows

  1. Discovery scans provider directories for session files
  2. Parsers read JSONL or query SQLite into typed entries
  3. Adapters normalize to UnifiedEvent with common fields
  4. Watchers poll incrementally, store atomically in SQLite, push to SSE
  5. Dashboard serves live feed + analytics via HTTP + Server-Sent Events

All state is persisted in SQLite. Crash-safe, exactly-once semantics via transactional offsets.


Persistence

Database Path Contents
events.db ~/.moolmesh/events.db AI session events, file offsets, SSE replay buffer
github.db ~/.moolmesh/github.db Repos, commits, issues, PRs (and their stats), milestones, digests
workspace.db ~/.moolmesh/workspace.db Workspace attribution, filesystem touches, portfolio rollup, days worked

The databases are created automatically and migrate on startup (versioned migrations; a migration that rewrites rows backs up first to ~/.moolmesh/backups/, newest 5 of each kind kept). Other files under ~/.moolmesh/: config.toml (configuration), daemon.log (rotated to daemon.log.1 past 10 MB) and hub.log (CLI and MCP servers), run/ (the MCP server registry mool doctor reads), reports/ (mool report auto) and, only after mool device init, device.json (mode 600).


Reliability

  • Zero-gap SSE — id: fields enable browser reconnection with replay from SQLite
  • Transactional offsets — events and file positions update in a single transaction
  • Git crash safety — exceptions caught per-repo, 60s timeout on git fetch
  • GitHub ETags — 304 responses don't consume rate limit
  • Digest fallback — LLM unavailable → L2 template, no repos → L1 stats
  • OpenCode WAL safety — read-only SQLite with timeout, never blocks OpenCode writes

Roadmap

MoolMesh started with coding-agent sessions, but the vision is to observe the work, not just the sessions — attributing every file-touch to the project that owns it, and eventually seeing a folder directly, before or without an agent (VISION §6).

Status Version Scope
Shipped v1.6 Cross-session linking, session metadata, full text export, full-text search, git branch correlation
Shipped v1.7 Cursor provider (5th) — Claude, Codex, Qwen, OpenCode, Cursor; universal mool mcp setup <client>
Shipped v1.8 Windows daemon, GitHub client hardening (retry/backoff, pagination), MCP pagination & ordering
Shipped v1.9 Observe-base hygiene: honest session lifecycle, tool_result classification, timestamp honesty (#16/#17/#18)
Shipped v1.10 Workspace axis — Phase A: path → workspace resolver, M:N attribution over a separate workspace.db (#20)
Shipped v1.11 Workspace axis — Phase B: filesystem watcher — observe marked folders directly, no agent/git required; opt-in roots + mtime cursor into workspace.db (#21)
Shipped v1.12 Workspace axis — Phase C: signal-agnostic portfolio rollup (session + filesystem + git) + delivery_candidate (candidate with confidence); read-on-load /portfolio dashboard view (#22)
Shipped v1.13 Portfolio intelligence — Stage 1: collapse agent-harness folders onto their real project (session-cwd + validated decode) + hierarchical grouping (subdirs/materials nested, config dotfolders de-prioritized); additive workspace_classification, grouped /portfolio view (#24)
Shipped v1.14 Portfolio intelligence — Stage 2: production-over-time — per-project contribution strip (sessions/day by honest ingestion created_at), colored by agent, ordered by recency, 4d/week/month toggle; effort = sessions + active days (not duration); image/video deliverable count; inline SVG (zero-dep); leaf-project caret fix (#24)
Shipped v1.15 Project Intelligence — Unit 1: outcome layer: merged-PR / closed-issue / open-issue per canonical project in the production view (authoritative delivery from github.db, read-only, contributor-agnostic) + explicit [workspace] filesystem_monitoring opt-in flag & dashboard indicator (gates only folder monitoring, not the portfolio) (#27)
Shipped v1.16 Project Intelligence — Unit 2: derived project state: per canonical project a single state chip — 🟢 activo · 🟡 enfriándose · 🔵 entregado · 🟠 estancado · ⚪ pausado — fusing local activity (session/fs/git over honest clocks, _parse_ts) with the GitHub outcome layer; outcome_measurable distinguishes gitless (not measurable) from repo-with-0-PRs (measurable); surfaced with its auditable basis, never a bare flag; absorbs the cold-projects view (#25) (#28)
Shipped v1.17 Project Intelligence — Unit 3: client/org hierarchy: a third tier client → project → materials over the portfolio; client = git-remote org (case/underscore-insensitive), ladder override → git-org → parent-folder; known owner orgs = client nodes (client_orgs, auto-seeded), the owner's own org shown loose (personal_orgs), unknown orgs in an externos/referencia drawer, SHARED-WORKSPACE as a shared node; effort + outcome + state rolled up to the client (contributor-agnostic); starts invisible until an owner identity is configured; closes epic #26 (absorbs client attribution #24 Stage 3) (#29)
Shipped v1.18 Portfolio UX redesign — Part A (layout): the /portfolio view reclaims the wasted horizontal space — production strip fills its column (replaces the left-anchored inline SVG), dense single-line rows (~2–3× more projects/screen), clean names (github.com/ dropped, 1 line + ellipsis + tooltip), outcome as a first-class tri-state column (merged-PR / real-0 / no-repo-not-measurable), column headers, derived state as a leading scan-anchor dot; presentation-only (resolver/state/outcome #27/#28/#29, SSE and masking untouched), zero new deps (#35)
Shipped v1.19 Portfolio UX redesign — Part B (charts + KPIs): an executive-summary space over the list — a KPI tile row (proyectos activos, PR mergeados all-time, entregados, estancados, clientes), a state-distribution bar (status palette + icon + label, never color-alone) plus a recency histogram (state.age_days, real clocks, buckets hoy/1–3d/4–7d/8–30d/>30d) in one card, and top-by-activity / top-by-delivery bars (single hue + direct labels); tri-state honesty in the delivery chart (real-0 ≠ not-measurable), palette validated to avoid the failing categorical trio (ΔE 14.2); presentation-only (zero-dep inline SVG; resolver/state/outcome #27/#28/#29, SSE and masking untouched) (#37)
Shipped v1.20 Live portfolio — scheduled attribution: the daemon attributes new sessions to workspaces incrementally (events.id cursor in workspace.db, advanced only after a successful commit) and refreshes rollup/candidates/classification on new attributions or hourly, so /portfolio no longer goes stale until a manual mool workspace backfill; toggle [workspace] auto_attribution (default on, independent of filesystem_monitoring); freshness (last_attribution_at) exposed in the monitoring API; disk I/O moved outside the store lock, classification written atomically; events.db still read-only (#39)
Shipped v1.21 Codex in the portfolio + event-time dating: the Codex parser reads the current custom_tool_call format (exec JS / apply_patch, regex-only extraction, workdir-resolved, untruncated); a cwd fallback attributes any provider's sessions that touched no file to their working directory (via='cwd', replaced once a file edge appears); session activity is dated by the earliest event (event_ts, mixed formats → UTC day) instead of attribution time, with a one-time automatic re-derive of the history; session counts reconciled from edges (fs/git untouched) (#40, #42)
Shipped v1.22 Portfolio freshness + test isolation: /portfolio shows when it was last updated ("actualizado hace X") and warns when it is stale, failing, turned off or waiting for a daemon restart (dot + text, no names/paths, lightweight 60 s re-fetch); the test suite can no longer open or migrate the real ~/.moolmesh (home override before import + guard; real-data tests opt-in and read-only); folder watcher stopped cleanly on shutdown (#43, #44)
Shipped v1.23 Full history ingestion: a real, resumable mool backfill imports all Claude / Codex / Qwen sessions outside the daemon's 12 h live window (idempotent, batched, --dry-run/--since/--provider); the daemon catches up after downtime without flooding SSE; --reparse codex --yes re-ingests older Codex sessions with the current parser (backup first, per-session transaction); cloud-only placeholder files are skipped without triggering downloads; an events.historical flag keeps imported history out of the live "recent" feed (#45)
Shipped v1.24 Honest portfolio (audit #36): each project dated by its own attribution edges (per-edge activity clock) instead of the latest activity of any session that touched it; totals include nested sub-folders/materials like state does; shell-tool commands no longer become folders; install-day OpenCode rows dated by event time; temp/system folders and app bundles move to "unclassified"; automatic one-time re-derive (#56, #57, #58, #61)
Shipped v1.25 Pi provider + provider template: MoolMesh observes the Pi coding agent (tree-aware JSONL parser, file attribution, tokens and cost, daemon/backfill/catch-up/CLI/MCP/dashboard); docs/PROVIDERS.md + a provider contract test make new providers cheaper; shell commands never stored as file paths; container-internal paths (/app…) are "unclassified"; README "Updating" with --no-cache-dir (#66, #64, #63)
Shipped v1.26 Team & order: "· equipo" mark when only other people's commits keep a project hot ([user] emails, nothing per person exposed); registered-but-inactive repos folded and "no medido"; local-config ordering with project_aliases (incl. container paths), reference_containers, temporary_containers; majority-org client rung; mool repo sync --all-registered --all with "desde " labels while history is partial (#36, #60, #64, #59)
Shipped v1.27 Antigravity CLI provider (7th); OpenCode no longer loses streamed replies (mool backfill --reparse opencode); mool daemon install (launchd / systemd --user / Task Scheduler) and a stale-data flag measured from the data, with daemon.log rotation (1.27.2) (#74, #75, #78, #76)
Shipped v1.28 One local day ([user] timezone, DST-aware) for digests, the workspace rollup and the portfolio, plus the delivery of a time window; OpenCode timestamps in UTC; observe robustness: streaming reads, a per-event full-text cap, self-pruning backups, old data stored as history on any path, provider entry ids (1.28.1) (#81, #79, #52, #54, #62, #67)
Shipped v1.29 Per-PR stats (+/-, changed files, closed issues as owner/repo#n) and mool repo backfill-pr-stats; daemon stop waits for the process to exit, shutdown under a second (1.29.1) (#80, #96)
Shipped v1.30 Board repos (issues-only backlogs, MCP get_board_issues), remote-only repos (repo add --remote, --owner) with identity by GitHub id, owner aliases, a shared client registry and excluded repos (#93, #94, #95)
Shipped v1.31 Versioned snapshot export (mool snapshot export, MCP get_snapshot, docs/schema/snapshot.v1.md); outage days re-dated; every module logs under hub.* and daemon start waits out a slow start (1.31.1–1.31.2) (#82, #101, #107, #108, #111)
Shipped v1.32 Days worked: the production strip counts a session on every local day it worked a project (#114)
Shipped v1.33 Active branch of tree sessions in mool export and the MCP session events; mool mcp setup antigravity; .git reads never trigger cloud downloads; orphan workspaces pruned; Codex attribution fixes (#67, #77, #52, #46, #47, #48, #49)
Shipped v1.34 Days worked next to touches in the rollup views; Qwen active branch; parallel tool results visible again (1.34.1); an outage catch-up counts on its own day and mool export is complete (1.34.2); migrations logged in hub.log (1.34.3) (#122, #123, #127, #131, #132, #130, #136)
Shipped v1.35 Per-contributor breakdown, opt-in ([team]) in the portfolio and digests; Antigravity desktop app / IDE read too (#83, #77)
Shipped v1.36 Device identity and per-device effort export, opt-in and deny-by-default (mool device, mool effort export, docs/schema/effort.v1.md); the snapshot always names its zone (1.36.1) (#140, #146)
Next — mool-ops engine, a companion package in its own repo that reads MoolMesh only through mool snapshot export and mool effort export (#84). The container split (#30) was closed as won't-do for now. Org-scale team visibility stays deferred to v2.x; core offers only the opt-in [team] breakdown.
Future v2.x Workspace Phase D (cross-machine, opt-in); autonomous agents; org-scale observability

See ROADMAP.md for detailed plans, open questions, and design principles.


Limitations

  • macOS optimal, Linux supported — macOS uses kqueue for instant detection; Linux uses polling (~1s)
  • No authentication — dashboard binds to localhost. Use a reverse proxy for remote access
  • Single-user design — not intended for multi-user or server deployment
  • Python 3.11+ — uses tomllib from stdlib
  • GitHub Projects v2 only — classic Projects (v1) not supported
  • Cursor caveats — Cursor stores no per-message timestamps locally (MoolMesh approximates them from composer metadata) and its on-disk schema is reverse-engineered, so a Cursor update may temporarily reduce ingestion until the parser is adjusted
  • Antigravity caveats — the CLI (agy) and the desktop app / IDE data dir (~/.gemini/antigravity) are both read, a conversation present in both is ingested once (from the CLI dir); model, tokens, cost and git branch are not read (the transcript does not carry them reliably), and the on-disk layout is not a documented API and the CLI ships several releases a week, so an update may temporarily reduce ingestion until the parser is adjusted (details in docs/PROVIDERS.md)

Development

# Run all tests
pytest tests/ -v

# Run with coverage
pytest tests/ -v --cov=hub

2523 tests. Zero external dependencies. Python stdlib + SQLite.

See CONTRIBUTING.md for guidelines.


License

MIT — Your telemetry is yours.

About

The context mesh for autonomous agents — unified observability, telemetry, and inter-agent coordination https://fmicalizzi.github.io/moolmesh/

Resources

Contributing

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages