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.
Files changed (124) hide show
  1. moolmesh-1.4.0/LICENSE +21 -0
  2. moolmesh-1.4.0/PKG-INFO +25 -0
  3. moolmesh-1.4.0/README.md +294 -0
  4. moolmesh-1.4.0/hub/__init__.py +4 -0
  5. moolmesh-1.4.0/hub/__main__.py +7 -0
  6. moolmesh-1.4.0/hub/adapters/__init__.py +3 -0
  7. moolmesh-1.4.0/hub/adapters/base.py +27 -0
  8. moolmesh-1.4.0/hub/adapters/claude_adapter.py +238 -0
  9. moolmesh-1.4.0/hub/adapters/codex_adapter.py +227 -0
  10. moolmesh-1.4.0/hub/adapters/opencode_adapter.py +143 -0
  11. moolmesh-1.4.0/hub/adapters/qwen_adapter.py +201 -0
  12. moolmesh-1.4.0/hub/analyzers/__init__.py +19 -0
  13. moolmesh-1.4.0/hub/analyzers/base.py +37 -0
  14. moolmesh-1.4.0/hub/analyzers/cross_provider.py +58 -0
  15. moolmesh-1.4.0/hub/analyzers/efficiency.py +141 -0
  16. moolmesh-1.4.0/hub/analyzers/file_ops.py +106 -0
  17. moolmesh-1.4.0/hub/analyzers/qa.py +188 -0
  18. moolmesh-1.4.0/hub/analyzers/session_timeline.py +73 -0
  19. moolmesh-1.4.0/hub/analyzers/summary.py +126 -0
  20. moolmesh-1.4.0/hub/analyzers/user_messages.py +71 -0
  21. moolmesh-1.4.0/hub/backfill.py +22 -0
  22. moolmesh-1.4.0/hub/batch_reporter.py +316 -0
  23. moolmesh-1.4.0/hub/cache/__init__.py +0 -0
  24. moolmesh-1.4.0/hub/cache/event_store.py +717 -0
  25. moolmesh-1.4.0/hub/cache/git_store.py +928 -0
  26. moolmesh-1.4.0/hub/cli.py +614 -0
  27. moolmesh-1.4.0/hub/colors.py +37 -0
  28. moolmesh-1.4.0/hub/config.py +275 -0
  29. moolmesh-1.4.0/hub/correlation/__init__.py +1 -0
  30. moolmesh-1.4.0/hub/correlation/linker.py +162 -0
  31. moolmesh-1.4.0/hub/daemon.py +132 -0
  32. moolmesh-1.4.0/hub/dashboard/__init__.py +0 -0
  33. moolmesh-1.4.0/hub/dashboard/server.py +901 -0
  34. moolmesh-1.4.0/hub/dashboard/static/analytics.html +480 -0
  35. moolmesh-1.4.0/hub/dashboard/static/dashboard.html +772 -0
  36. moolmesh-1.4.0/hub/dashboard/static/projects.html +503 -0
  37. moolmesh-1.4.0/hub/dashboard/static/timeline.html +1033 -0
  38. moolmesh-1.4.0/hub/digests/__init__.py +4 -0
  39. moolmesh-1.4.0/hub/digests/engine.py +245 -0
  40. moolmesh-1.4.0/hub/digests/llm.py +181 -0
  41. moolmesh-1.4.0/hub/digests/stats.py +166 -0
  42. moolmesh-1.4.0/hub/digests/template.py +284 -0
  43. moolmesh-1.4.0/hub/discovery.py +433 -0
  44. moolmesh-1.4.0/hub/git_utils.py +151 -0
  45. moolmesh-1.4.0/hub/harvesters/__init__.py +4 -0
  46. moolmesh-1.4.0/hub/harvesters/git_harvester.py +279 -0
  47. moolmesh-1.4.0/hub/harvesters/github_harvester.py +225 -0
  48. moolmesh-1.4.0/hub/integrations/__init__.py +23 -0
  49. moolmesh-1.4.0/hub/integrations/github_client.py +265 -0
  50. moolmesh-1.4.0/hub/integrations/ollama_client.py +88 -0
  51. moolmesh-1.4.0/hub/integrations/openai_compat_client.py +81 -0
  52. moolmesh-1.4.0/hub/log.py +62 -0
  53. moolmesh-1.4.0/hub/mcp_server.py +363 -0
  54. moolmesh-1.4.0/hub/models/__init__.py +17 -0
  55. moolmesh-1.4.0/hub/models/base.py +114 -0
  56. moolmesh-1.4.0/hub/models/claude.py +57 -0
  57. moolmesh-1.4.0/hub/models/codex.py +54 -0
  58. moolmesh-1.4.0/hub/models/opencode.py +40 -0
  59. moolmesh-1.4.0/hub/models/qwen.py +59 -0
  60. moolmesh-1.4.0/hub/parsers/__init__.py +3 -0
  61. moolmesh-1.4.0/hub/parsers/base.py +29 -0
  62. moolmesh-1.4.0/hub/parsers/claude_parser.py +167 -0
  63. moolmesh-1.4.0/hub/parsers/codex_parser.py +247 -0
  64. moolmesh-1.4.0/hub/parsers/opencode_parser.py +237 -0
  65. moolmesh-1.4.0/hub/parsers/qwen_parser.py +191 -0
  66. moolmesh-1.4.0/hub/renderers/__init__.py +3 -0
  67. moolmesh-1.4.0/hub/renderers/markdown.py +97 -0
  68. moolmesh-1.4.0/hub/watchers/__init__.py +3 -0
  69. moolmesh-1.4.0/hub/watchers/base.py +149 -0
  70. moolmesh-1.4.0/hub/watchers/claude_watcher.py +63 -0
  71. moolmesh-1.4.0/hub/watchers/codex_watcher.py +58 -0
  72. moolmesh-1.4.0/hub/watchers/kqueue_watcher.py +94 -0
  73. moolmesh-1.4.0/hub/watchers/opencode_watcher.py +57 -0
  74. moolmesh-1.4.0/hub/watchers/polling_watcher.py +70 -0
  75. moolmesh-1.4.0/hub/watchers/qwen_watcher.py +63 -0
  76. moolmesh-1.4.0/moolmesh.egg-info/PKG-INFO +25 -0
  77. moolmesh-1.4.0/moolmesh.egg-info/SOURCES.txt +122 -0
  78. moolmesh-1.4.0/moolmesh.egg-info/dependency_links.txt +1 -0
  79. moolmesh-1.4.0/moolmesh.egg-info/entry_points.txt +2 -0
  80. moolmesh-1.4.0/moolmesh.egg-info/requires.txt +3 -0
  81. moolmesh-1.4.0/moolmesh.egg-info/top_level.txt +1 -0
  82. moolmesh-1.4.0/pyproject.toml +45 -0
  83. moolmesh-1.4.0/setup.cfg +4 -0
  84. moolmesh-1.4.0/tests/test_analyzer_cross_provider.py +85 -0
  85. moolmesh-1.4.0/tests/test_analyzer_session_timeline.py +86 -0
  86. moolmesh-1.4.0/tests/test_analyzer_user_messages.py +102 -0
  87. moolmesh-1.4.0/tests/test_backfill.py +94 -0
  88. moolmesh-1.4.0/tests/test_batch_reporter.py +74 -0
  89. moolmesh-1.4.0/tests/test_claude_adapter.py +144 -0
  90. moolmesh-1.4.0/tests/test_claude_parser.py +151 -0
  91. moolmesh-1.4.0/tests/test_cli_repo_sync.py +171 -0
  92. moolmesh-1.4.0/tests/test_codex_adapter.py +284 -0
  93. moolmesh-1.4.0/tests/test_codex_parser.py +342 -0
  94. moolmesh-1.4.0/tests/test_config.py +517 -0
  95. moolmesh-1.4.0/tests/test_correlation.py +96 -0
  96. moolmesh-1.4.0/tests/test_digest_engine.py +221 -0
  97. moolmesh-1.4.0/tests/test_digest_stats.py +86 -0
  98. moolmesh-1.4.0/tests/test_digest_template.py +144 -0
  99. moolmesh-1.4.0/tests/test_discovery.py +321 -0
  100. moolmesh-1.4.0/tests/test_event_store.py +431 -0
  101. moolmesh-1.4.0/tests/test_foundation.py +201 -0
  102. moolmesh-1.4.0/tests/test_git_harvester.py +292 -0
  103. moolmesh-1.4.0/tests/test_git_store.py +612 -0
  104. moolmesh-1.4.0/tests/test_github_client.py +213 -0
  105. moolmesh-1.4.0/tests/test_github_harvester.py +214 -0
  106. moolmesh-1.4.0/tests/test_harvester.py +215 -0
  107. moolmesh-1.4.0/tests/test_llm_factory.py +22 -0
  108. moolmesh-1.4.0/tests/test_logging.py +73 -0
  109. moolmesh-1.4.0/tests/test_mcp_server.py +363 -0
  110. moolmesh-1.4.0/tests/test_models.py +84 -0
  111. moolmesh-1.4.0/tests/test_ollama_client.py +88 -0
  112. moolmesh-1.4.0/tests/test_openai_compat_client.py +107 -0
  113. moolmesh-1.4.0/tests/test_opencode_adapter.py +211 -0
  114. moolmesh-1.4.0/tests/test_opencode_discovery.py +143 -0
  115. moolmesh-1.4.0/tests/test_opencode_parser.py +312 -0
  116. moolmesh-1.4.0/tests/test_opencode_watcher.py +134 -0
  117. moolmesh-1.4.0/tests/test_polling_watcher.py +81 -0
  118. moolmesh-1.4.0/tests/test_qwen_adapter.py +178 -0
  119. moolmesh-1.4.0/tests/test_qwen_parser.py +139 -0
  120. moolmesh-1.4.0/tests/test_server_api.py +130 -0
  121. moolmesh-1.4.0/tests/test_sse_robusto.py +181 -0
  122. moolmesh-1.4.0/tests/test_watcher_filtering.py +115 -0
  123. moolmesh-1.4.0/tests/test_watcher_reactivation.py +96 -0
  124. 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.
@@ -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
@@ -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: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
+ [![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-blue.svg)](https://python.org)
9
+ [![Tests: 509 passing](https://img.shields.io/badge/Tests-509%20passing-green.svg)](#development)
10
+ [![Zero Dependencies](https://img.shields.io/badge/Dependencies-Zero-brightgreen.svg)](#)
11
+ [![Español](https://img.shields.io/badge/Docs-Espa%C3%B1ol-orange.svg)](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,4 @@
1
+ """MoolMesh — the context mesh for AI coding agents."""
2
+
3
+ __version__ = "1.4.0"
4
+ USER_AGENT = f"moolmesh/{__version__}"
@@ -0,0 +1,7 @@
1
+ """Allow `python -m hub` as a fallback entry point."""
2
+
3
+ from hub.cli import main
4
+
5
+
6
+ if __name__ == "__main__":
7
+ main()
@@ -0,0 +1,3 @@
1
+ from .base import BaseAdapter
2
+
3
+ __all__ = ["BaseAdapter"]
@@ -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