moolmesh 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- moolmesh-1.4.0/LICENSE +21 -0
- moolmesh-1.4.0/PKG-INFO +25 -0
- moolmesh-1.4.0/README.md +294 -0
- moolmesh-1.4.0/hub/__init__.py +4 -0
- moolmesh-1.4.0/hub/__main__.py +7 -0
- moolmesh-1.4.0/hub/adapters/__init__.py +3 -0
- moolmesh-1.4.0/hub/adapters/base.py +27 -0
- moolmesh-1.4.0/hub/adapters/claude_adapter.py +238 -0
- moolmesh-1.4.0/hub/adapters/codex_adapter.py +227 -0
- moolmesh-1.4.0/hub/adapters/opencode_adapter.py +143 -0
- moolmesh-1.4.0/hub/adapters/qwen_adapter.py +201 -0
- moolmesh-1.4.0/hub/analyzers/__init__.py +19 -0
- moolmesh-1.4.0/hub/analyzers/base.py +37 -0
- moolmesh-1.4.0/hub/analyzers/cross_provider.py +58 -0
- moolmesh-1.4.0/hub/analyzers/efficiency.py +141 -0
- moolmesh-1.4.0/hub/analyzers/file_ops.py +106 -0
- moolmesh-1.4.0/hub/analyzers/qa.py +188 -0
- moolmesh-1.4.0/hub/analyzers/session_timeline.py +73 -0
- moolmesh-1.4.0/hub/analyzers/summary.py +126 -0
- moolmesh-1.4.0/hub/analyzers/user_messages.py +71 -0
- moolmesh-1.4.0/hub/backfill.py +22 -0
- moolmesh-1.4.0/hub/batch_reporter.py +316 -0
- moolmesh-1.4.0/hub/cache/__init__.py +0 -0
- moolmesh-1.4.0/hub/cache/event_store.py +717 -0
- moolmesh-1.4.0/hub/cache/git_store.py +928 -0
- moolmesh-1.4.0/hub/cli.py +614 -0
- moolmesh-1.4.0/hub/colors.py +37 -0
- moolmesh-1.4.0/hub/config.py +275 -0
- moolmesh-1.4.0/hub/correlation/__init__.py +1 -0
- moolmesh-1.4.0/hub/correlation/linker.py +162 -0
- moolmesh-1.4.0/hub/daemon.py +132 -0
- moolmesh-1.4.0/hub/dashboard/__init__.py +0 -0
- moolmesh-1.4.0/hub/dashboard/server.py +901 -0
- moolmesh-1.4.0/hub/dashboard/static/analytics.html +480 -0
- moolmesh-1.4.0/hub/dashboard/static/dashboard.html +772 -0
- moolmesh-1.4.0/hub/dashboard/static/projects.html +503 -0
- moolmesh-1.4.0/hub/dashboard/static/timeline.html +1033 -0
- moolmesh-1.4.0/hub/digests/__init__.py +4 -0
- moolmesh-1.4.0/hub/digests/engine.py +245 -0
- moolmesh-1.4.0/hub/digests/llm.py +181 -0
- moolmesh-1.4.0/hub/digests/stats.py +166 -0
- moolmesh-1.4.0/hub/digests/template.py +284 -0
- moolmesh-1.4.0/hub/discovery.py +433 -0
- moolmesh-1.4.0/hub/git_utils.py +151 -0
- moolmesh-1.4.0/hub/harvesters/__init__.py +4 -0
- moolmesh-1.4.0/hub/harvesters/git_harvester.py +279 -0
- moolmesh-1.4.0/hub/harvesters/github_harvester.py +225 -0
- moolmesh-1.4.0/hub/integrations/__init__.py +23 -0
- moolmesh-1.4.0/hub/integrations/github_client.py +265 -0
- moolmesh-1.4.0/hub/integrations/ollama_client.py +88 -0
- moolmesh-1.4.0/hub/integrations/openai_compat_client.py +81 -0
- moolmesh-1.4.0/hub/log.py +62 -0
- moolmesh-1.4.0/hub/mcp_server.py +363 -0
- moolmesh-1.4.0/hub/models/__init__.py +17 -0
- moolmesh-1.4.0/hub/models/base.py +114 -0
- moolmesh-1.4.0/hub/models/claude.py +57 -0
- moolmesh-1.4.0/hub/models/codex.py +54 -0
- moolmesh-1.4.0/hub/models/opencode.py +40 -0
- moolmesh-1.4.0/hub/models/qwen.py +59 -0
- moolmesh-1.4.0/hub/parsers/__init__.py +3 -0
- moolmesh-1.4.0/hub/parsers/base.py +29 -0
- moolmesh-1.4.0/hub/parsers/claude_parser.py +167 -0
- moolmesh-1.4.0/hub/parsers/codex_parser.py +247 -0
- moolmesh-1.4.0/hub/parsers/opencode_parser.py +237 -0
- moolmesh-1.4.0/hub/parsers/qwen_parser.py +191 -0
- moolmesh-1.4.0/hub/renderers/__init__.py +3 -0
- moolmesh-1.4.0/hub/renderers/markdown.py +97 -0
- moolmesh-1.4.0/hub/watchers/__init__.py +3 -0
- moolmesh-1.4.0/hub/watchers/base.py +149 -0
- moolmesh-1.4.0/hub/watchers/claude_watcher.py +63 -0
- moolmesh-1.4.0/hub/watchers/codex_watcher.py +58 -0
- moolmesh-1.4.0/hub/watchers/kqueue_watcher.py +94 -0
- moolmesh-1.4.0/hub/watchers/opencode_watcher.py +57 -0
- moolmesh-1.4.0/hub/watchers/polling_watcher.py +70 -0
- moolmesh-1.4.0/hub/watchers/qwen_watcher.py +63 -0
- moolmesh-1.4.0/moolmesh.egg-info/PKG-INFO +25 -0
- moolmesh-1.4.0/moolmesh.egg-info/SOURCES.txt +122 -0
- moolmesh-1.4.0/moolmesh.egg-info/dependency_links.txt +1 -0
- moolmesh-1.4.0/moolmesh.egg-info/entry_points.txt +2 -0
- moolmesh-1.4.0/moolmesh.egg-info/requires.txt +3 -0
- moolmesh-1.4.0/moolmesh.egg-info/top_level.txt +1 -0
- moolmesh-1.4.0/pyproject.toml +45 -0
- moolmesh-1.4.0/setup.cfg +4 -0
- moolmesh-1.4.0/tests/test_analyzer_cross_provider.py +85 -0
- moolmesh-1.4.0/tests/test_analyzer_session_timeline.py +86 -0
- moolmesh-1.4.0/tests/test_analyzer_user_messages.py +102 -0
- moolmesh-1.4.0/tests/test_backfill.py +94 -0
- moolmesh-1.4.0/tests/test_batch_reporter.py +74 -0
- moolmesh-1.4.0/tests/test_claude_adapter.py +144 -0
- moolmesh-1.4.0/tests/test_claude_parser.py +151 -0
- moolmesh-1.4.0/tests/test_cli_repo_sync.py +171 -0
- moolmesh-1.4.0/tests/test_codex_adapter.py +284 -0
- moolmesh-1.4.0/tests/test_codex_parser.py +342 -0
- moolmesh-1.4.0/tests/test_config.py +517 -0
- moolmesh-1.4.0/tests/test_correlation.py +96 -0
- moolmesh-1.4.0/tests/test_digest_engine.py +221 -0
- moolmesh-1.4.0/tests/test_digest_stats.py +86 -0
- moolmesh-1.4.0/tests/test_digest_template.py +144 -0
- moolmesh-1.4.0/tests/test_discovery.py +321 -0
- moolmesh-1.4.0/tests/test_event_store.py +431 -0
- moolmesh-1.4.0/tests/test_foundation.py +201 -0
- moolmesh-1.4.0/tests/test_git_harvester.py +292 -0
- moolmesh-1.4.0/tests/test_git_store.py +612 -0
- moolmesh-1.4.0/tests/test_github_client.py +213 -0
- moolmesh-1.4.0/tests/test_github_harvester.py +214 -0
- moolmesh-1.4.0/tests/test_harvester.py +215 -0
- moolmesh-1.4.0/tests/test_llm_factory.py +22 -0
- moolmesh-1.4.0/tests/test_logging.py +73 -0
- moolmesh-1.4.0/tests/test_mcp_server.py +363 -0
- moolmesh-1.4.0/tests/test_models.py +84 -0
- moolmesh-1.4.0/tests/test_ollama_client.py +88 -0
- moolmesh-1.4.0/tests/test_openai_compat_client.py +107 -0
- moolmesh-1.4.0/tests/test_opencode_adapter.py +211 -0
- moolmesh-1.4.0/tests/test_opencode_discovery.py +143 -0
- moolmesh-1.4.0/tests/test_opencode_parser.py +312 -0
- moolmesh-1.4.0/tests/test_opencode_watcher.py +134 -0
- moolmesh-1.4.0/tests/test_polling_watcher.py +81 -0
- moolmesh-1.4.0/tests/test_qwen_adapter.py +178 -0
- moolmesh-1.4.0/tests/test_qwen_parser.py +139 -0
- moolmesh-1.4.0/tests/test_server_api.py +130 -0
- moolmesh-1.4.0/tests/test_sse_robusto.py +181 -0
- moolmesh-1.4.0/tests/test_watcher_filtering.py +115 -0
- moolmesh-1.4.0/tests/test_watcher_reactivation.py +96 -0
- moolmesh-1.4.0/tests/test_watcher_startup_gap.py +112 -0
moolmesh-1.4.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MoolMesh contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
moolmesh-1.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: moolmesh
|
|
3
|
+
Version: 1.4.0
|
|
4
|
+
Summary: The context mesh for autonomous agents — unified observability, telemetry, and inter-agent coordination
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/fmicalizzi/moolmesh
|
|
7
|
+
Project-URL: Repository, https://github.com/fmicalizzi/moolmesh
|
|
8
|
+
Project-URL: Issues, https://github.com/fmicalizzi/moolmesh/issues
|
|
9
|
+
Keywords: ai,agents,observability,telemetry,mcp,claude,codex,opencode,developer-tools
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Environment :: Web Environment
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Topic :: System :: Monitoring
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
moolmesh-1.4.0/README.md
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# MoolMesh
|
|
2
|
+
|
|
3
|
+
**The context mesh for autonomous agents.**
|
|
4
|
+
|
|
5
|
+
Unified observability, telemetry, and inter-agent coordination — running entirely on your machine.
|
|
6
|
+
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://python.org)
|
|
9
|
+
[](#development)
|
|
10
|
+
[](#)
|
|
11
|
+
[](README.es.md)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why MoolMesh?
|
|
16
|
+
|
|
17
|
+
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.
|
|
18
|
+
|
|
19
|
+
When Claude Code gets stuck in a loop, your other agents don't know. When you spend tokens across four 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.
|
|
20
|
+
|
|
21
|
+
**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).
|
|
22
|
+
|
|
23
|
+
Read our [Philosophy](PHILOSOPHY.md) to understand the dual axiom behind MoolMesh: **Human-First & Agent-First**.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## What You Get
|
|
28
|
+
|
|
29
|
+
Four views in a single browser tab:
|
|
30
|
+
|
|
31
|
+
| View | What it shows |
|
|
32
|
+
|------|---------------|
|
|
33
|
+
| **AI Sessions** | Live event feed from all agents — messages, tool calls, token usage, models |
|
|
34
|
+
| **Analytics** | Token consumption by provider, hourly activity, top tools, top projects |
|
|
35
|
+
| **Project Pulse** | PR kanban, issues list, milestones, GitHub Projects v2 board |
|
|
36
|
+
| **Code Timeline** | Commit feed, author stats, hot files, daily/weekly digest narratives |
|
|
37
|
+
|
|
38
|
+
Plus a **MCP server** that lets other AI agents query your session data programmatically — enabling agent-to-agent supervision and orchestration.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Install
|
|
46
|
+
pip install moolmesh
|
|
47
|
+
|
|
48
|
+
# Start the dashboard
|
|
49
|
+
mool dashboard
|
|
50
|
+
# → open http://localhost:5200
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
That's it. MoolMesh auto-discovers your AI sessions immediately. No configuration needed.
|
|
54
|
+
|
|
55
|
+
> **Running from source:**
|
|
56
|
+
> ```bash
|
|
57
|
+
> git clone https://github.com/fmicalizzi/moolmesh.git
|
|
58
|
+
> cd moolmesh
|
|
59
|
+
> python -m venv .venv && source .venv/bin/activate
|
|
60
|
+
> pip install -e ".[dev]"
|
|
61
|
+
> mool dashboard
|
|
62
|
+
> ```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Supported Agents
|
|
67
|
+
|
|
68
|
+
| Provider | Session source | Format |
|
|
69
|
+
|----------|---------------|--------|
|
|
70
|
+
| **Claude Code** | `~/.claude/projects/` | JSONL per session + subagent logs |
|
|
71
|
+
| **Codex (GPT-5)** | `~/.codex/sessions/` + `state_5.sqlite` | Rollout JSONL + SQLite metadata |
|
|
72
|
+
| **Qwen CLI** | `~/.qwen/projects/` | JSONL per chat |
|
|
73
|
+
| **OpenCode** | `~/.local/share/opencode/opencode.db` | SQLite (session → message → part) |
|
|
74
|
+
|
|
75
|
+
Sessions are auto-discovered on startup. No configuration, no API keys, no cloud services.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Git & GitHub Integration
|
|
80
|
+
|
|
81
|
+
Register a git repository to unlock Project Pulse and Code Timeline:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
mool repo add /path/to/your/repo
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
This ingests commit history and starts polling GitHub for issues, PRs, milestones, and Projects v2.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
mool repo list # Show registered repos
|
|
91
|
+
mool repo remove /path/to/repo # Unregister
|
|
92
|
+
mool repo sync /path/to/repo --all # Re-ingest full history
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
A GitHub token is resolved automatically: `gh auth token` → `GITHUB_TOKEN` env → `config.toml`. If you use the GitHub CLI, no extra setup is needed.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## MCP Server (Inter-Agent API)
|
|
100
|
+
|
|
101
|
+
MoolMesh exposes a read-only MCP server over stdio, allowing any MCP-compatible agent to query session data:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"mcpServers": {
|
|
106
|
+
"moolmesh": {
|
|
107
|
+
"command": "uv",
|
|
108
|
+
"args": ["run", "/path/to/moolmesh/hub/mcp_server.py"]
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Available tools:
|
|
115
|
+
|
|
116
|
+
| Tool | Description |
|
|
117
|
+
|------|-------------|
|
|
118
|
+
| `get_recent_events` | Latest N events across all providers |
|
|
119
|
+
| `get_active_sessions` | Sessions active in the last N hours |
|
|
120
|
+
| `get_token_usage` | Token consumption by provider |
|
|
121
|
+
| `get_tool_stats` | Top tools used by AI agents |
|
|
122
|
+
| `search_events` | Full-text search on event summaries |
|
|
123
|
+
| `get_project_activity` | Complete project summary with stats |
|
|
124
|
+
|
|
125
|
+
Resources: `hub://schema` (database schema), `hub://projects` (project list with stats).
|
|
126
|
+
|
|
127
|
+
The server opens SQLite in read-only mode (`?mode=ro`). It runs as a separate process (~15-20 MB RAM), independent from the dashboard.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Digest Narratives
|
|
132
|
+
|
|
133
|
+
Code Timeline generates daily and weekly digests for each registered repo:
|
|
134
|
+
|
|
135
|
+
| Level | What | When |
|
|
136
|
+
|-------|------|------|
|
|
137
|
+
| **L1** | Raw SQL stats (commits, PRs, issues, LOC) | Always available |
|
|
138
|
+
| **L2** | Structured template with bullet points | Always available |
|
|
139
|
+
| **L3** | LLM-generated narrative paragraph | When an LLM provider is configured |
|
|
140
|
+
|
|
141
|
+
L3 works with any OpenAI-compatible API. Configure in `~/.moolmesh/config.toml`:
|
|
142
|
+
|
|
143
|
+
```toml
|
|
144
|
+
[llm]
|
|
145
|
+
provider = "openrouter"
|
|
146
|
+
api_url = "https://openrouter.ai/api/v1"
|
|
147
|
+
model = "google/gemma-4-31b-it:free"
|
|
148
|
+
api_key = "sk-or-v1-..."
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Supported providers: OpenRouter, OpenAI, Together, Groq, Ollama. If the LLM is unavailable, digests fall back to L2 automatically.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Batch Reports
|
|
156
|
+
|
|
157
|
+
Generate Markdown analysis reports from the command line:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Auto report — writes to ~/.moolmesh/reports/
|
|
161
|
+
mool report auto
|
|
162
|
+
|
|
163
|
+
# Full content (no truncation)
|
|
164
|
+
mool report auto --complete
|
|
165
|
+
|
|
166
|
+
# Filter by project or provider
|
|
167
|
+
mool report --project myapp --provider claude --output ./exports
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## CLI Reference
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
mool <command> [options]
|
|
176
|
+
|
|
177
|
+
Commands:
|
|
178
|
+
dashboard Start the live monitoring dashboard
|
|
179
|
+
report Generate batch Markdown analysis reports
|
|
180
|
+
discover List all discovered AI agent projects
|
|
181
|
+
repo add PATH Register a git repo for monitoring
|
|
182
|
+
repo list List registered repos with commit counts
|
|
183
|
+
repo remove PATH Unregister a repo
|
|
184
|
+
repo sync PATH Re-ingest commit history
|
|
185
|
+
|
|
186
|
+
Dashboard options:
|
|
187
|
+
--port PORT Server port (default: 5200)
|
|
188
|
+
--host HOST Server host (default: localhost)
|
|
189
|
+
--project NAME Filter to project name
|
|
190
|
+
--providers LIST Comma-separated: claude,codex,qwen,opencode
|
|
191
|
+
|
|
192
|
+
Report options:
|
|
193
|
+
--complete Full-content mode: no truncation
|
|
194
|
+
--output DIR Output directory
|
|
195
|
+
--provider PROVIDER Filter by provider
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Architecture
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
hub/
|
|
204
|
+
parsers/ JSONL + SQLite parsers for each provider
|
|
205
|
+
adapters/ Normalize provider entries → unified events
|
|
206
|
+
watchers/ File harvesters: discover → offset → parse → store → SSE
|
|
207
|
+
harvesters/ GitHarvester (120s) + GitHubHarvester (15s/60s)
|
|
208
|
+
integrations/ GitHubClient (REST + GraphQL) + LLM clients
|
|
209
|
+
digests/ L1 Stats → L2 Template → L3 LLM narrative
|
|
210
|
+
correlation/ AI ↔ Git links: Co-Author, issue refs, timestamps
|
|
211
|
+
dashboard/ HTTP server + SSE + 4 HTML pages
|
|
212
|
+
cache/ EventStore (events.db) + GitStore (github.db)
|
|
213
|
+
mcp_server.py MCP stdio server (read-only, PEP 723 inline deps)
|
|
214
|
+
cli.py CLI entry point
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### How data flows
|
|
218
|
+
|
|
219
|
+
1. **Discovery** scans provider directories for session files
|
|
220
|
+
2. **Parsers** read JSONL or query SQLite into typed entries
|
|
221
|
+
3. **Adapters** normalize to `UnifiedEvent` with common fields
|
|
222
|
+
4. **Watchers** poll incrementally, store atomically in SQLite, push to SSE
|
|
223
|
+
5. **Dashboard** serves live feed + analytics via HTTP + Server-Sent Events
|
|
224
|
+
|
|
225
|
+
All state is persisted in SQLite. Crash-safe, exactly-once semantics via transactional offsets.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Persistence
|
|
230
|
+
|
|
231
|
+
| Database | Path | Contents |
|
|
232
|
+
|----------|------|----------|
|
|
233
|
+
| `events.db` | `~/.moolmesh/events.db` | AI session events, file offsets, SSE replay buffer |
|
|
234
|
+
| `github.db` | `~/.moolmesh/github.db` | Repos, commits, issues, PRs, milestones, digests |
|
|
235
|
+
|
|
236
|
+
Both databases are created automatically. Schema migrates on startup.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Reliability
|
|
241
|
+
|
|
242
|
+
- **Zero-gap SSE** — `id:` fields enable browser reconnection with replay from SQLite
|
|
243
|
+
- **Transactional offsets** — events and file positions update in a single transaction
|
|
244
|
+
- **Git crash safety** — exceptions caught per-repo, 60s timeout on `git fetch`
|
|
245
|
+
- **GitHub ETags** — 304 responses don't consume rate limit
|
|
246
|
+
- **Digest fallback** — LLM unavailable → L2 template, no repos → L1 stats
|
|
247
|
+
- **OpenCode WAL safety** — read-only SQLite with timeout, never blocks OpenCode writes
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Roadmap
|
|
252
|
+
|
|
253
|
+
MoolMesh started with coding agents but the vision is broader — any autonomous agent that generates observable signals belongs in the mesh.
|
|
254
|
+
|
|
255
|
+
| Status | Scope | Details |
|
|
256
|
+
|--------|-------|---------|
|
|
257
|
+
| **Shipping** | Claude Code, Codex (GPT-5), Qwen CLI, OpenCode | Full session parsing, live monitoring, MCP |
|
|
258
|
+
| **Planned** | Aider, Pi, GitHub Copilot CLI, Antigravity CLI | Adapters in progress — PRs welcome |
|
|
259
|
+
| **Future** | Hermes, Odyssey, Goose, community requests | Open an issue to propose a new provider |
|
|
260
|
+
| **Vision** | Cross-repo model usage analytics | Monitor AI token consumption and agent activity across an organization's repositories |
|
|
261
|
+
|
|
262
|
+
The long-term goal: a unified observability layer for any agent ecosystem — coding, ops, research, orchestration — so teams can see what their agents are doing, what they're spending, and whether they're stepping on each other.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Limitations
|
|
267
|
+
|
|
268
|
+
- **macOS optimal, Linux supported** — macOS uses `kqueue` for instant detection; Linux uses polling (~1s)
|
|
269
|
+
- **No authentication** — dashboard binds to localhost. Use a reverse proxy for remote access
|
|
270
|
+
- **Single-user design** — not intended for multi-user or server deployment
|
|
271
|
+
- **Python 3.11+** — uses `tomllib` from stdlib
|
|
272
|
+
- **GitHub Projects v2 only** — classic Projects (v1) not supported
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Development
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
# Run all tests
|
|
280
|
+
pytest tests/ -v
|
|
281
|
+
|
|
282
|
+
# Run with coverage
|
|
283
|
+
pytest tests/ -v --cov=hub
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
509 tests. Zero external dependencies. Python stdlib + SQLite.
|
|
287
|
+
|
|
288
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## License
|
|
293
|
+
|
|
294
|
+
[MIT](LICENSE) — Your telemetry is yours.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Abstract base adapter for converting provider entries to unified models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from hub.models.base import UnifiedEvent, UnifiedMessage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseAdapter(ABC):
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def to_unified(self, entry: Any, project: str) -> UnifiedMessage | None:
|
|
15
|
+
"""Convert a provider-specific entry to UnifiedMessage.
|
|
16
|
+
|
|
17
|
+
Returns None if the entry should be skipped.
|
|
18
|
+
"""
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def to_event(self, entry: Any, project: str) -> UnifiedEvent | None:
|
|
23
|
+
"""Convert a provider-specific entry to a lightweight UnifiedEvent for SSE.
|
|
24
|
+
|
|
25
|
+
Returns None if the entry should be skipped.
|
|
26
|
+
"""
|
|
27
|
+
...
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Adapter converting Claude entries to unified models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from hub.adapters.base import BaseAdapter
|
|
8
|
+
from hub.models.base import (
|
|
9
|
+
MessageRole,
|
|
10
|
+
Provider,
|
|
11
|
+
TokenUsage,
|
|
12
|
+
ToolCall,
|
|
13
|
+
UnifiedEvent,
|
|
14
|
+
UnifiedMessage,
|
|
15
|
+
)
|
|
16
|
+
from hub.models.claude import ClaudeContentBlock, ClaudeEntry, ClaudeUsage
|
|
17
|
+
|
|
18
|
+
# Types we skip during adaptation
|
|
19
|
+
_SKIP_TYPES = frozenset({"file-history-snapshot"})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ClaudeAdapter(BaseAdapter):
|
|
23
|
+
|
|
24
|
+
def to_unified(self, entry: ClaudeEntry, project: str) -> UnifiedMessage | None:
|
|
25
|
+
if entry.type in _SKIP_TYPES:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
role = self._map_role(entry)
|
|
29
|
+
if role is None:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
tool_calls = self._extract_tool_calls(entry.content_blocks)
|
|
33
|
+
tokens = self._map_usage(entry.usage)
|
|
34
|
+
timestamp = self._parse_timestamp(entry.timestamp)
|
|
35
|
+
|
|
36
|
+
return UnifiedMessage(
|
|
37
|
+
id=entry.uuid,
|
|
38
|
+
provider=Provider.CLAUDE,
|
|
39
|
+
session_id=entry.session_id,
|
|
40
|
+
project=project,
|
|
41
|
+
role=role,
|
|
42
|
+
text=entry.content_text,
|
|
43
|
+
tool_calls=tool_calls,
|
|
44
|
+
timestamp=timestamp,
|
|
45
|
+
model=entry.model,
|
|
46
|
+
tokens=tokens,
|
|
47
|
+
parent_id=entry.parent_uuid,
|
|
48
|
+
is_sidechain=entry.is_sidechain,
|
|
49
|
+
cwd=entry.cwd,
|
|
50
|
+
raw=entry.raw,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def to_event(self, entry: ClaudeEntry, project: str) -> UnifiedEvent | None:
|
|
54
|
+
if entry.type in _SKIP_TYPES:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
role = self._map_role(entry)
|
|
58
|
+
if role is None:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
summary = self._summarize(entry)
|
|
62
|
+
tokens = None
|
|
63
|
+
if entry.usage:
|
|
64
|
+
tokens = {
|
|
65
|
+
"input": entry.usage.input_tokens,
|
|
66
|
+
"output": entry.usage.output_tokens,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
tool_name = None
|
|
70
|
+
file_path = None
|
|
71
|
+
for block in entry.content_blocks:
|
|
72
|
+
if block.type == "tool_use" and block.tool_name:
|
|
73
|
+
tool_name = block.tool_name
|
|
74
|
+
if block.tool_input:
|
|
75
|
+
file_path = (
|
|
76
|
+
block.tool_input.get("file_path")
|
|
77
|
+
or block.tool_input.get("path")
|
|
78
|
+
or block.tool_input.get("command", "")[:80]
|
|
79
|
+
)
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
return UnifiedEvent(
|
|
83
|
+
provider=Provider.CLAUDE,
|
|
84
|
+
project=project,
|
|
85
|
+
event_type=role.value,
|
|
86
|
+
timestamp=entry.timestamp,
|
|
87
|
+
summary=summary,
|
|
88
|
+
session_id=entry.session_id,
|
|
89
|
+
tokens=tokens,
|
|
90
|
+
tool_name=tool_name,
|
|
91
|
+
file_path=str(file_path) if file_path else None,
|
|
92
|
+
model=entry.model,
|
|
93
|
+
cwd=entry.cwd or None,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def _map_role(self, entry: ClaudeEntry) -> MessageRole | None:
|
|
97
|
+
match entry.type:
|
|
98
|
+
case "user":
|
|
99
|
+
return MessageRole.USER
|
|
100
|
+
case "assistant":
|
|
101
|
+
# Check if this is primarily a tool_use or tool_result message
|
|
102
|
+
has_tool_use = any(
|
|
103
|
+
b.type == "tool_use" for b in entry.content_blocks
|
|
104
|
+
)
|
|
105
|
+
has_tool_result = any(
|
|
106
|
+
b.type == "tool_result" for b in entry.content_blocks
|
|
107
|
+
)
|
|
108
|
+
has_thinking = any(
|
|
109
|
+
b.type == "thinking" for b in entry.content_blocks
|
|
110
|
+
)
|
|
111
|
+
has_text = bool(entry.content_text.strip())
|
|
112
|
+
|
|
113
|
+
if has_tool_result and not has_text:
|
|
114
|
+
return MessageRole.TOOL_RESULT
|
|
115
|
+
if has_tool_use and not has_text:
|
|
116
|
+
return MessageRole.TOOL_USE
|
|
117
|
+
if has_thinking and not has_text and not has_tool_use:
|
|
118
|
+
return MessageRole.THINKING
|
|
119
|
+
return MessageRole.ASSISTANT
|
|
120
|
+
case "system":
|
|
121
|
+
return MessageRole.SYSTEM
|
|
122
|
+
case "summary":
|
|
123
|
+
return MessageRole.SUMMARY
|
|
124
|
+
case _:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
def _extract_tool_calls(
|
|
128
|
+
self, blocks: list[ClaudeContentBlock]
|
|
129
|
+
) -> list[ToolCall]:
|
|
130
|
+
calls: list[ToolCall] = []
|
|
131
|
+
for block in blocks:
|
|
132
|
+
if block.type != "tool_use" or not block.tool_name:
|
|
133
|
+
continue
|
|
134
|
+
input_data = block.tool_input or {}
|
|
135
|
+
fp = input_data.get("file_path") or input_data.get("path")
|
|
136
|
+
op_type = self._infer_operation_type(block.tool_name, input_data)
|
|
137
|
+
calls.append(
|
|
138
|
+
ToolCall(
|
|
139
|
+
name=block.tool_name,
|
|
140
|
+
input_data=input_data,
|
|
141
|
+
tool_id=block.tool_id,
|
|
142
|
+
file_path=fp,
|
|
143
|
+
operation_type=op_type,
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
return calls
|
|
147
|
+
|
|
148
|
+
def _map_usage(self, usage: ClaudeUsage | None) -> TokenUsage | None:
|
|
149
|
+
if usage is None:
|
|
150
|
+
return None
|
|
151
|
+
return TokenUsage(
|
|
152
|
+
input_tokens=usage.input_tokens,
|
|
153
|
+
output_tokens=usage.output_tokens,
|
|
154
|
+
cache_creation=usage.cache_creation_input_tokens,
|
|
155
|
+
cache_read=usage.cache_read_input_tokens,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def _summarize(self, entry: ClaudeEntry) -> str:
|
|
159
|
+
"""Produce a human-readable one-liner for the event feed."""
|
|
160
|
+
match entry.type:
|
|
161
|
+
case "user":
|
|
162
|
+
text = entry.content_text.strip().replace("\n", " ")
|
|
163
|
+
return text[:120] if text else "[empty user message]"
|
|
164
|
+
case "assistant":
|
|
165
|
+
# Prioritize tool_use summaries
|
|
166
|
+
for block in entry.content_blocks:
|
|
167
|
+
if block.type == "tool_use" and block.tool_name:
|
|
168
|
+
brief = self._brief_tool_args(
|
|
169
|
+
block.tool_name, block.tool_input
|
|
170
|
+
)
|
|
171
|
+
return f"{block.tool_name}: {brief}"
|
|
172
|
+
if block.type == "thinking" and block.thinking:
|
|
173
|
+
return f"[thinking] {block.thinking[:100]}"
|
|
174
|
+
text = entry.content_text.strip().replace("\n", " ")
|
|
175
|
+
return text[:120] if text else "[assistant response]"
|
|
176
|
+
case "system":
|
|
177
|
+
sub = f" ({entry.subtype})" if entry.subtype else ""
|
|
178
|
+
return f"[system{sub}]"
|
|
179
|
+
case "summary":
|
|
180
|
+
return "[context summary]"
|
|
181
|
+
case _:
|
|
182
|
+
return f"[{entry.type}]"
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def _brief_tool_args(tool_name: str, input_data: dict | None) -> str:
|
|
186
|
+
if not input_data:
|
|
187
|
+
return ""
|
|
188
|
+
match tool_name:
|
|
189
|
+
case "Bash":
|
|
190
|
+
cmd = input_data.get("command", "")
|
|
191
|
+
return cmd[:80]
|
|
192
|
+
case "Read":
|
|
193
|
+
return input_data.get("file_path", "")[:80]
|
|
194
|
+
case "Write" | "Edit" | "MultiEdit":
|
|
195
|
+
return input_data.get("file_path", "")[:80]
|
|
196
|
+
case "Glob":
|
|
197
|
+
return input_data.get("pattern", "")[:80]
|
|
198
|
+
case "Grep":
|
|
199
|
+
return input_data.get("pattern", "")[:80]
|
|
200
|
+
case "Agent":
|
|
201
|
+
return input_data.get("description", "")[:80]
|
|
202
|
+
case "WebSearch" | "WebFetch":
|
|
203
|
+
return input_data.get("query", input_data.get("url", ""))[:80]
|
|
204
|
+
case _:
|
|
205
|
+
# Generic: show first key=value
|
|
206
|
+
for k, v in input_data.items():
|
|
207
|
+
return f"{k}={str(v)[:60]}"
|
|
208
|
+
return ""
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def _infer_operation_type(tool_name: str, input_data: dict) -> str | None:
|
|
212
|
+
match tool_name:
|
|
213
|
+
case "Read" | "Glob" | "Grep":
|
|
214
|
+
return "read"
|
|
215
|
+
case "Write":
|
|
216
|
+
return "create"
|
|
217
|
+
case "Edit" | "MultiEdit":
|
|
218
|
+
return "modify"
|
|
219
|
+
case "Bash":
|
|
220
|
+
cmd = input_data.get("command", "")
|
|
221
|
+
if any(k in cmd for k in ("rm ", "rm\t", "rmdir")):
|
|
222
|
+
return "delete"
|
|
223
|
+
if any(k in cmd for k in ("mkdir", "touch", "cp ", "mv ")):
|
|
224
|
+
return "create"
|
|
225
|
+
return "exec"
|
|
226
|
+
case "Agent":
|
|
227
|
+
return "exec"
|
|
228
|
+
case _:
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
232
|
+
def _parse_timestamp(ts: str) -> datetime | None:
|
|
233
|
+
if not ts:
|
|
234
|
+
return None
|
|
235
|
+
try:
|
|
236
|
+
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
237
|
+
except (ValueError, TypeError):
|
|
238
|
+
return None
|