leviathan-hivemind-mcp 0.2.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.
- leviathan_hivemind_mcp-0.2.0/LICENSE +21 -0
- leviathan_hivemind_mcp-0.2.0/PKG-INFO +151 -0
- leviathan_hivemind_mcp-0.2.0/README.md +113 -0
- leviathan_hivemind_mcp-0.2.0/hivemind_mcp/__init__.py +8 -0
- leviathan_hivemind_mcp-0.2.0/hivemind_mcp/_vault.py +243 -0
- leviathan_hivemind_mcp-0.2.0/hivemind_mcp/env_bridge.py +84 -0
- leviathan_hivemind_mcp-0.2.0/hivemind_mcp/license.py +150 -0
- leviathan_hivemind_mcp-0.2.0/hivemind_mcp/memory.py +210 -0
- leviathan_hivemind_mcp-0.2.0/hivemind_mcp/providers.py +269 -0
- leviathan_hivemind_mcp-0.2.0/hivemind_mcp/replication.py +183 -0
- leviathan_hivemind_mcp-0.2.0/hivemind_mcp/rotator.py +107 -0
- leviathan_hivemind_mcp-0.2.0/hivemind_mcp/server.py +150 -0
- leviathan_hivemind_mcp-0.2.0/leviathan_hivemind_mcp.egg-info/PKG-INFO +151 -0
- leviathan_hivemind_mcp-0.2.0/leviathan_hivemind_mcp.egg-info/SOURCES.txt +18 -0
- leviathan_hivemind_mcp-0.2.0/leviathan_hivemind_mcp.egg-info/dependency_links.txt +1 -0
- leviathan_hivemind_mcp-0.2.0/leviathan_hivemind_mcp.egg-info/entry_points.txt +2 -0
- leviathan_hivemind_mcp-0.2.0/leviathan_hivemind_mcp.egg-info/requires.txt +3 -0
- leviathan_hivemind_mcp-0.2.0/leviathan_hivemind_mcp.egg-info/top_level.txt +1 -0
- leviathan_hivemind_mcp-0.2.0/pyproject.toml +35 -0
- leviathan_hivemind_mcp-0.2.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chaz Leland Hamm (HaChazal) — Metanoia Unlimited LLC
|
|
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,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: leviathan-hivemind-mcp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: HiveMind MCP — persistent swarm memory through Obsidian, powered by your preferred LLM providers, gated by GitHub Sponsors subscription.
|
|
5
|
+
Author-email: "Chaz Leland Hamm (HaChazal)" <HaChazal@metanoiaunlimited.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Chaz Leland Hamm (HaChazal) — Metanoia Unlimited LLC
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Repository, https://github.com/chazam41892-gif/hivemind-mcp
|
|
29
|
+
Project-URL: Documentation, https://github.com/chazam41892-gif/hivemind-mcp#readme
|
|
30
|
+
Project-URL: Issue Tracker, https://github.com/chazam41892-gif/hivemind-mcp/issues
|
|
31
|
+
Requires-Python: >=3.9
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
License-File: LICENSE
|
|
34
|
+
Requires-Dist: mcp>=1.0.0
|
|
35
|
+
Requires-Dist: httpx>=0.27.0
|
|
36
|
+
Requires-Dist: pydantic>=2.0.0
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# HiveMind MCP
|
|
40
|
+
|
|
41
|
+
**Persistent swarm memory through Obsidian — powered by your preferred LLM providers, gated by a GitHub Sponsors subscription.**
|
|
42
|
+
|
|
43
|
+
> **Created by HaChazal (Chaz Leland Hamm) — Metanoia Unlimited LLC.**
|
|
44
|
+
> This is the memory layer of the LeviathanTalon swarm. Every agent, every
|
|
45
|
+
> conversation, every decision lives in your Obsidian vault and is recalled
|
|
46
|
+
> semantically — across any provider you already pay for.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Why this exists
|
|
51
|
+
|
|
52
|
+
AI agents forget. HiveMind MCP makes them *remember* — permanently, locally,
|
|
53
|
+
in your own Obsidian vault, with semantic recall across all your preferred
|
|
54
|
+
LLM providers (OpenAI, Anthropic, Gemini, Grok, DeepSeek, Mistral, Cohere,
|
|
55
|
+
Cerebras, Together, OpenRouter, Braintrust) routed through the Leviathan
|
|
56
|
+
Key Rotator so you never burn a single key dry.
|
|
57
|
+
|
|
58
|
+
## What it does
|
|
59
|
+
|
|
60
|
+
- **Persistent memory** — reads/writes `HiveMind.md` and the full vault
|
|
61
|
+
(journals, entities, decisions) through Obsidian.
|
|
62
|
+
- **Semantic recall** — embeddings + cosine similarity, not just keyword grep.
|
|
63
|
+
Ask "what did we decide about the video pipeline?" and get the actual
|
|
64
|
+
decision, even if the words don't match.
|
|
65
|
+
- **Provider-agnostic** — one interface, your preferred providers, key-rotated
|
|
66
|
+
and budget-capped.
|
|
67
|
+
- **Subscription-gated** — a GitHub Sponsors subscription unlocks the paid
|
|
68
|
+
provider tier. Free tier works fully local (Ollama) with no API costs.
|
|
69
|
+
|
|
70
|
+
## Credit
|
|
71
|
+
|
|
72
|
+
This project is the work of **HaChazal (Chaz Leland Hamm)**, Principal
|
|
73
|
+
Operator of the LeviathanTalon swarm, founder of **Metanoia Unlimited LLC**.
|
|
74
|
+
The HiveMind vault architecture, the key rotator, the swarm memory protocol,
|
|
75
|
+
and the Obsidian integration are all his design. If you use this, star the
|
|
76
|
+
repo and follow HaChazal — the swarm runs on community.
|
|
77
|
+
|
|
78
|
+
## Install
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install leviathan-hivemind-mcp
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The distribution is **`leviathan-hivemind-mcp`** on PyPI; the command stays **`hivemind-mcp`**. Zero-config launch from anywhere (auto-downloads on first use):
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uvx --from leviathan-hivemind-mcp hivemind-mcp
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Or clone and run from source:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
git clone https://github.com/chazam41892-gif/leviathan-hivemind-mcp
|
|
94
|
+
cd leviathan-hivemind-mcp
|
|
95
|
+
pip install -e .
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Configure
|
|
99
|
+
|
|
100
|
+
Set your vault and provider keys — or drop them into the unified
|
|
101
|
+
`~/.leviathan/HiveMind/.obsidian/HiveMind.env`, which every HiveMind MCP
|
|
102
|
+
server loads automatically (front-end ↔ back-end):
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
export HIVEMIND_VAULT=~/.leviathan/HiveMind # your Obsidian vault
|
|
106
|
+
export HIVEMIND_OWNER=1 # owner mode: skip subscription gate
|
|
107
|
+
export LEVIATHAN_KEY_OPENAI_1=sk-... # key rotator format
|
|
108
|
+
export LEVIATHAN_KEY_ANTHROPIC_1=sk-ant-...
|
|
109
|
+
export LEVIATHAN_BUDGET_OPENAI=10.0 # optional budget caps
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Run
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
hivemind-mcp
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Register it in your MCP client (Claude Desktop, Cursor, OpenCode, etc.):
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"mcpServers": {
|
|
123
|
+
"hivemind": {
|
|
124
|
+
"command": "hivemind-mcp",
|
|
125
|
+
"args": []
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Tools
|
|
132
|
+
|
|
133
|
+
| Tool | Description |
|
|
134
|
+
|------|-------------|
|
|
135
|
+
| `read_shared_memory` | Read `HiveMind.md` shared swarm state |
|
|
136
|
+
| `read_soul` | Read `SOUL.md` — swarm identity + doctrine |
|
|
137
|
+
| `append_to_journal` | Append a memory to an agent's journal (append-only) |
|
|
138
|
+
| `recall` | Keyword or semantic recall across the vault |
|
|
139
|
+
| `semantic_recall` | Embedding-based recall (needs a provider or local model) |
|
|
140
|
+
| `read_entity` / `upsert_entity` | Read / layer canonical entities |
|
|
141
|
+
| `read_agent_identity` | Read an agent's identity.md |
|
|
142
|
+
| `read_openai_hivemind` | Read the OpenAI/Codex shared operating memory |
|
|
143
|
+
| `record_openai_highlight` | Write a resumable project capsule → 4 replicas |
|
|
144
|
+
| `chat` | Ask any preferred provider (gated by subscription) |
|
|
145
|
+
| `list_providers` | Show configured providers + key health |
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT — but the *credit* is HaChazal's. Star the repo. Follow the swarm.
|
|
150
|
+
|
|
151
|
+
**Built by HaChazal — Metanoia Unlimited. The swarm remembers.**
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# HiveMind MCP
|
|
2
|
+
|
|
3
|
+
**Persistent swarm memory through Obsidian — powered by your preferred LLM providers, gated by a GitHub Sponsors subscription.**
|
|
4
|
+
|
|
5
|
+
> **Created by HaChazal (Chaz Leland Hamm) — Metanoia Unlimited LLC.**
|
|
6
|
+
> This is the memory layer of the LeviathanTalon swarm. Every agent, every
|
|
7
|
+
> conversation, every decision lives in your Obsidian vault and is recalled
|
|
8
|
+
> semantically — across any provider you already pay for.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why this exists
|
|
13
|
+
|
|
14
|
+
AI agents forget. HiveMind MCP makes them *remember* — permanently, locally,
|
|
15
|
+
in your own Obsidian vault, with semantic recall across all your preferred
|
|
16
|
+
LLM providers (OpenAI, Anthropic, Gemini, Grok, DeepSeek, Mistral, Cohere,
|
|
17
|
+
Cerebras, Together, OpenRouter, Braintrust) routed through the Leviathan
|
|
18
|
+
Key Rotator so you never burn a single key dry.
|
|
19
|
+
|
|
20
|
+
## What it does
|
|
21
|
+
|
|
22
|
+
- **Persistent memory** — reads/writes `HiveMind.md` and the full vault
|
|
23
|
+
(journals, entities, decisions) through Obsidian.
|
|
24
|
+
- **Semantic recall** — embeddings + cosine similarity, not just keyword grep.
|
|
25
|
+
Ask "what did we decide about the video pipeline?" and get the actual
|
|
26
|
+
decision, even if the words don't match.
|
|
27
|
+
- **Provider-agnostic** — one interface, your preferred providers, key-rotated
|
|
28
|
+
and budget-capped.
|
|
29
|
+
- **Subscription-gated** — a GitHub Sponsors subscription unlocks the paid
|
|
30
|
+
provider tier. Free tier works fully local (Ollama) with no API costs.
|
|
31
|
+
|
|
32
|
+
## Credit
|
|
33
|
+
|
|
34
|
+
This project is the work of **HaChazal (Chaz Leland Hamm)**, Principal
|
|
35
|
+
Operator of the LeviathanTalon swarm, founder of **Metanoia Unlimited LLC**.
|
|
36
|
+
The HiveMind vault architecture, the key rotator, the swarm memory protocol,
|
|
37
|
+
and the Obsidian integration are all his design. If you use this, star the
|
|
38
|
+
repo and follow HaChazal — the swarm runs on community.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install leviathan-hivemind-mcp
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The distribution is **`leviathan-hivemind-mcp`** on PyPI; the command stays **`hivemind-mcp`**. Zero-config launch from anywhere (auto-downloads on first use):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uvx --from leviathan-hivemind-mcp hivemind-mcp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or clone and run from source:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git clone https://github.com/chazam41892-gif/leviathan-hivemind-mcp
|
|
56
|
+
cd leviathan-hivemind-mcp
|
|
57
|
+
pip install -e .
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Configure
|
|
61
|
+
|
|
62
|
+
Set your vault and provider keys — or drop them into the unified
|
|
63
|
+
`~/.leviathan/HiveMind/.obsidian/HiveMind.env`, which every HiveMind MCP
|
|
64
|
+
server loads automatically (front-end ↔ back-end):
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
export HIVEMIND_VAULT=~/.leviathan/HiveMind # your Obsidian vault
|
|
68
|
+
export HIVEMIND_OWNER=1 # owner mode: skip subscription gate
|
|
69
|
+
export LEVIATHAN_KEY_OPENAI_1=sk-... # key rotator format
|
|
70
|
+
export LEVIATHAN_KEY_ANTHROPIC_1=sk-ant-...
|
|
71
|
+
export LEVIATHAN_BUDGET_OPENAI=10.0 # optional budget caps
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Run
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
hivemind-mcp
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Register it in your MCP client (Claude Desktop, Cursor, OpenCode, etc.):
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"mcpServers": {
|
|
85
|
+
"hivemind": {
|
|
86
|
+
"command": "hivemind-mcp",
|
|
87
|
+
"args": []
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Tools
|
|
94
|
+
|
|
95
|
+
| Tool | Description |
|
|
96
|
+
|------|-------------|
|
|
97
|
+
| `read_shared_memory` | Read `HiveMind.md` shared swarm state |
|
|
98
|
+
| `read_soul` | Read `SOUL.md` — swarm identity + doctrine |
|
|
99
|
+
| `append_to_journal` | Append a memory to an agent's journal (append-only) |
|
|
100
|
+
| `recall` | Keyword or semantic recall across the vault |
|
|
101
|
+
| `semantic_recall` | Embedding-based recall (needs a provider or local model) |
|
|
102
|
+
| `read_entity` / `upsert_entity` | Read / layer canonical entities |
|
|
103
|
+
| `read_agent_identity` | Read an agent's identity.md |
|
|
104
|
+
| `read_openai_hivemind` | Read the OpenAI/Codex shared operating memory |
|
|
105
|
+
| `record_openai_highlight` | Write a resumable project capsule → 4 replicas |
|
|
106
|
+
| `chat` | Ask any preferred provider (gated by subscription) |
|
|
107
|
+
| `list_providers` | Show configured providers + key health |
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT — but the *credit* is HaChazal's. Star the repo. Follow the swarm.
|
|
112
|
+
|
|
113
|
+
**Built by HaChazal — Metanoia Unlimited. The swarm remembers.**
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Vault — the markdown read/write engine for the HiveMind.
|
|
2
|
+
|
|
3
|
+
Operates directly on the vault's markdown files per the contract in
|
|
4
|
+
shared/conventions.md. Stdlib-only. Preservation Doctrine is enforced
|
|
5
|
+
structurally: journals are opened in append mode, entities are layered
|
|
6
|
+
(never rewritten), and nothing is ever truncated.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import random
|
|
12
|
+
import re
|
|
13
|
+
import time
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
# Allowed journal entry kinds (from .brain/config.yaml entry_kinds).
|
|
19
|
+
ENTRY_KINDS = (
|
|
20
|
+
"observation", "decision", "action", "result",
|
|
21
|
+
"question", "learning", "handoff",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# type -> entities subdir (the canonical entity folders that exist in the vault).
|
|
25
|
+
ENTITY_DIRS = {
|
|
26
|
+
"person": "people", "people": "people",
|
|
27
|
+
"project": "projects", "projects": "projects",
|
|
28
|
+
"token": "tokens", "tokens": "tokens",
|
|
29
|
+
"account": "accounts", "accounts": "accounts",
|
|
30
|
+
"client": "clients", "clients": "clients",
|
|
31
|
+
"concept": "concepts", "concepts": "concepts",
|
|
32
|
+
"media": "media", "video": "media", "image": "media",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _now() -> str:
|
|
39
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _ulid() -> str:
|
|
43
|
+
"""Sortable ULID-ish id: 10-char time + 16-char randomness, Crockford b32.
|
|
44
|
+
Good enough for vault ids (monotonic-ish, collision-resistant); no deps."""
|
|
45
|
+
t = int(time.time() * 1000)
|
|
46
|
+
ts = ""
|
|
47
|
+
for _ in range(10):
|
|
48
|
+
ts = _CROCKFORD[t & 31] + ts
|
|
49
|
+
t >>= 5
|
|
50
|
+
rnd = "".join(random.choice(_CROCKFORD) for _ in range(16))
|
|
51
|
+
return ts + rnd
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _vault_root(root: Optional[str] = None) -> Path:
|
|
55
|
+
if root:
|
|
56
|
+
return Path(root).expanduser()
|
|
57
|
+
env = os.environ.get("HIVEMIND_VAULT")
|
|
58
|
+
if env:
|
|
59
|
+
return Path(env).expanduser()
|
|
60
|
+
return Path.home() / ".leviathan" / "HiveMind"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _as_list(x) -> list:
|
|
64
|
+
if x is None:
|
|
65
|
+
return []
|
|
66
|
+
if isinstance(x, (list, tuple)):
|
|
67
|
+
return [str(i) for i in x]
|
|
68
|
+
return [str(x)]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Vault:
|
|
72
|
+
def __init__(self, root: Optional[str] = None):
|
|
73
|
+
self.root = _vault_root(root)
|
|
74
|
+
if not self.root.exists():
|
|
75
|
+
raise FileNotFoundError(f"HiveMind vault not found at {self.root}")
|
|
76
|
+
|
|
77
|
+
# ── paths ──────────────────────────────────────────────────────────
|
|
78
|
+
def _journal(self, agent: str) -> Path:
|
|
79
|
+
return self.root / "agents" / agent / "journal.md"
|
|
80
|
+
|
|
81
|
+
# ── 1. append_to_journal (APPEND-ONLY) ─────────────────────────────
|
|
82
|
+
def append_to_journal(self, agent: str, kind: str, body: str,
|
|
83
|
+
tags=None, links=None, session: Optional[str] = None) -> dict:
|
|
84
|
+
if kind not in ENTRY_KINDS:
|
|
85
|
+
raise ValueError(f"kind {kind!r} not in {ENTRY_KINDS}")
|
|
86
|
+
agent = str(agent).strip()
|
|
87
|
+
if not agent:
|
|
88
|
+
raise ValueError("agent is required")
|
|
89
|
+
tags = _as_list(tags)
|
|
90
|
+
links = _as_list(links)
|
|
91
|
+
ts = _now()
|
|
92
|
+
|
|
93
|
+
jp = self._journal(agent)
|
|
94
|
+
jp.parent.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
if not jp.exists(): # seed with frontmatter on first write
|
|
96
|
+
jp.write_text(
|
|
97
|
+
f"---\nid: {_ulid()}\ntype: journal\nagent: {agent}\n"
|
|
98
|
+
f"created: {ts}\nupdated: {ts}\ntags: [journal]\n---\n\n"
|
|
99
|
+
f"# {agent} — journal (append-only)\n",
|
|
100
|
+
encoding="utf-8",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
links_md = ", ".join(f"[[{l}]]" for l in links)
|
|
104
|
+
block = (
|
|
105
|
+
f"\n## {ts} — {kind}\n"
|
|
106
|
+
f"- session: {session or ''}\n"
|
|
107
|
+
f"- tags: [{', '.join(tags)}]\n"
|
|
108
|
+
f"- links: {links_md}\n\n"
|
|
109
|
+
f"{body.rstrip()}\n"
|
|
110
|
+
)
|
|
111
|
+
with jp.open("a", encoding="utf-8") as f: # append mode = Preservation
|
|
112
|
+
f.write(block)
|
|
113
|
+
return {"agent": agent, "kind": kind, "ts": ts, "tags": tags,
|
|
114
|
+
"links": links, "session": session, "body": body, "file": str(jp)}
|
|
115
|
+
|
|
116
|
+
# ── 2. recall ───────────────────────────────────────────────────────
|
|
117
|
+
def recall(self, query: Optional[str] = None, agent: Optional[str] = None,
|
|
118
|
+
kind: Optional[str] = None, tags=None, links=None,
|
|
119
|
+
since: Optional[str] = None, mode: str = "keyword",
|
|
120
|
+
limit: int = 50, fast: bool = False) -> list:
|
|
121
|
+
# Optional FTS accelerator (as-of-last-reindex). Falls back to the
|
|
122
|
+
# always-correct scan below if the index is missing/unavailable.
|
|
123
|
+
if fast and query and not tags and not links and not since:
|
|
124
|
+
try:
|
|
125
|
+
from .index import Index
|
|
126
|
+
rows = Index(self.root).search(query, agent=agent, kind=kind, limit=limit)
|
|
127
|
+
if rows is not None:
|
|
128
|
+
return rows
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
want_tags = set(_as_list(tags))
|
|
132
|
+
want_links = set(_as_list(links))
|
|
133
|
+
q = (query or "").lower().strip()
|
|
134
|
+
results = []
|
|
135
|
+
|
|
136
|
+
agents_dir = self.root / "agents"
|
|
137
|
+
agent_dirs = ([agents_dir / agent] if agent else
|
|
138
|
+
[d for d in agents_dir.glob("*") if d.is_dir()])
|
|
139
|
+
for ad in agent_dirs:
|
|
140
|
+
jp = ad / "journal.md"
|
|
141
|
+
if not jp.exists():
|
|
142
|
+
continue
|
|
143
|
+
who = ad.name
|
|
144
|
+
for e in self._parse_journal(jp.read_text(encoding="utf-8", errors="replace")):
|
|
145
|
+
e["agent"] = who
|
|
146
|
+
e["file"] = str(jp)
|
|
147
|
+
if kind and e["kind"] != kind:
|
|
148
|
+
continue
|
|
149
|
+
if since and e["ts"] < since:
|
|
150
|
+
continue
|
|
151
|
+
if want_tags and not want_tags & set(e["tags"]):
|
|
152
|
+
continue
|
|
153
|
+
if want_links and not want_links & set(e["links"]):
|
|
154
|
+
continue
|
|
155
|
+
if q and q not in e["body"].lower() and not any(q in t.lower() for t in e["tags"]):
|
|
156
|
+
continue
|
|
157
|
+
results.append(e)
|
|
158
|
+
|
|
159
|
+
results.sort(key=lambda e: e["ts"], reverse=True)
|
|
160
|
+
return results[:limit]
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def _parse_journal(text: str) -> list:
|
|
164
|
+
"""Split a journal.md into entry dicts on the `## <ts> — <kind>` headers."""
|
|
165
|
+
out = []
|
|
166
|
+
header = re.compile(r"^##\s+(\S+)\s+—\s+(\w+)\s*$", re.M)
|
|
167
|
+
marks = list(header.finditer(text))
|
|
168
|
+
for i, m in enumerate(marks):
|
|
169
|
+
ts, kind = m.group(1), m.group(2)
|
|
170
|
+
seg = text[m.end(): marks[i + 1].start() if i + 1 < len(marks) else len(text)]
|
|
171
|
+
tags, links, session = [], [], ""
|
|
172
|
+
body_lines = []
|
|
173
|
+
for line in seg.splitlines():
|
|
174
|
+
ls = line.strip()
|
|
175
|
+
if ls.startswith("- session:"):
|
|
176
|
+
session = ls[len("- session:"):].strip()
|
|
177
|
+
elif ls.startswith("- tags:"):
|
|
178
|
+
inner = ls[ls.find("[") + 1: ls.rfind("]")] if "[" in ls else ""
|
|
179
|
+
tags = [t.strip() for t in inner.split(",") if t.strip()]
|
|
180
|
+
elif ls.startswith("- links:"):
|
|
181
|
+
links = re.findall(r"\[\[([^\]|]+)", ls)
|
|
182
|
+
else:
|
|
183
|
+
body_lines.append(line)
|
|
184
|
+
out.append({"ts": ts, "kind": kind, "tags": tags, "links": links,
|
|
185
|
+
"session": session, "body": "\n".join(body_lines).strip()})
|
|
186
|
+
return out
|
|
187
|
+
|
|
188
|
+
# ── 3. read_entity ──────────────────────────────────────────────────
|
|
189
|
+
def read_entity(self, name: str) -> Optional[dict]:
|
|
190
|
+
ent = self.root / "entities"
|
|
191
|
+
for f in ent.rglob(f"{name}.md"): # first match wins
|
|
192
|
+
return {"name": name, "file": str(f), "text": f.read_text(encoding="utf-8", errors="replace")}
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
# ── 4. upsert_entity (CREATE or LAYER — never overwrite) ────────────
|
|
196
|
+
def upsert_entity(self, name: str, body: str, type: str = "concept",
|
|
197
|
+
merge: bool = True) -> dict:
|
|
198
|
+
existing = self.read_entity(name)
|
|
199
|
+
ts = _now()
|
|
200
|
+
if existing:
|
|
201
|
+
if not merge:
|
|
202
|
+
raise FileExistsError(f"entity {name!r} exists and merge=False")
|
|
203
|
+
with Path(existing["file"]).open("a", encoding="utf-8") as f: # LAYER
|
|
204
|
+
f.write(f"\n\n## update {ts}\n\n{body.rstrip()}\n")
|
|
205
|
+
return {**existing, "layered": True, "ts": ts}
|
|
206
|
+
sub = ENTITY_DIRS.get(type.lower(), "concepts")
|
|
207
|
+
d = self.root / "entities" / sub
|
|
208
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
209
|
+
fp = d / f"{name}.md"
|
|
210
|
+
fp.write_text(
|
|
211
|
+
f"---\nid: {_ulid()}\ntype: entity\ncreated: {ts}\nupdated: {ts}\n"
|
|
212
|
+
f"tags: [{type}]\n---\n\n# {name}\n\n{body.rstrip()}\n",
|
|
213
|
+
encoding="utf-8",
|
|
214
|
+
)
|
|
215
|
+
return {"name": name, "file": str(fp), "created": True, "ts": ts}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ── module-level convenience wrappers (the tool surface) ────────────────
|
|
219
|
+
_default: Optional[Vault] = None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _v() -> Vault:
|
|
223
|
+
global _default
|
|
224
|
+
if _default is None:
|
|
225
|
+
_default = Vault()
|
|
226
|
+
return _default
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def append_to_journal(agent, kind, body, tags=None, links=None, session=None):
|
|
230
|
+
return _v().append_to_journal(agent, kind, body, tags, links, session)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def recall(query=None, agent=None, kind=None, tags=None, links=None,
|
|
234
|
+
since=None, mode="keyword", limit=50):
|
|
235
|
+
return _v().recall(query, agent, kind, tags, links, since, mode, limit)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def read_entity(name):
|
|
239
|
+
return _v().read_entity(name)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def upsert_entity(name, body, type="concept", merge=True):
|
|
243
|
+
return _v().upsert_entity(name, body, type, merge)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""HiveMind.env bridge — the front-end ↔ back-end credential layer.
|
|
2
|
+
|
|
3
|
+
Loads the operator's consolidated HiveMind.env (default:
|
|
4
|
+
``~/.leviathan/HiveMind/.obsidian/HiveMind.env``, override with
|
|
5
|
+
``HIVEMIND_ENV_FILE``) into ``os.environ`` for any AI client that spawns
|
|
6
|
+
this MCP server. Existing process environment ALWAYS wins — this only
|
|
7
|
+
fills gaps, so an app that already exports a key is never overridden.
|
|
8
|
+
|
|
9
|
+
Mapping rules:
|
|
10
|
+
* ``KEY="value"`` / ``KEY=value`` / tab-indented lines are parsed; quotes
|
|
11
|
+
and trailing ``\\r`` are stripped; ``#`` comments and blanks skipped.
|
|
12
|
+
* Plural pool → singular export: ``FOO_API_KEYS="k1, k2"`` also exports
|
|
13
|
+
``FOO_API_KEY`` = first key (matching the provider adapters' env
|
|
14
|
+
fallback names) when the singular is not already set.
|
|
15
|
+
* Alias: ``XAI_API_KEYS``/``XAI_API_KEY`` also fill ``GROK_API_KEY``.
|
|
16
|
+
|
|
17
|
+
Stdlib-only; never raises; returns the number of new names exported.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
DEFAULT_ENV_FILE = Path.home() / ".leviathan" / "HiveMind" / ".obsidian" / "HiveMind.env"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _parse_env_file(path: Path) -> dict[str, str]:
|
|
30
|
+
pairs: dict[str, str] = {}
|
|
31
|
+
try:
|
|
32
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
33
|
+
except Exception:
|
|
34
|
+
return pairs
|
|
35
|
+
for raw in text.splitlines():
|
|
36
|
+
line = raw.strip().lstrip("\ufeff")
|
|
37
|
+
if not line or line.startswith("#"):
|
|
38
|
+
continue
|
|
39
|
+
if "=" not in line:
|
|
40
|
+
continue
|
|
41
|
+
key, _, value = line.partition("=")
|
|
42
|
+
key = key.strip()
|
|
43
|
+
value = value.strip().strip("\r")
|
|
44
|
+
if not key or not value:
|
|
45
|
+
continue
|
|
46
|
+
if value[:1] in ('"', "'") and value[-1:] == value[:1]:
|
|
47
|
+
value = value[1:-1]
|
|
48
|
+
if not value:
|
|
49
|
+
continue
|
|
50
|
+
pairs.setdefault(key, value)
|
|
51
|
+
return pairs
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_env_file(path: Optional[Path] = None) -> int:
|
|
55
|
+
"""Export HiveMind.env into os.environ (fill-gaps only). Returns count."""
|
|
56
|
+
env_file = Path(path) if path else Path(os.environ.get("HIVEMIND_ENV_FILE", DEFAULT_ENV_FILE))
|
|
57
|
+
pairs = _parse_env_file(env_file)
|
|
58
|
+
if not pairs:
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
exported = 0
|
|
62
|
+
for key, value in pairs.items():
|
|
63
|
+
if key not in os.environ:
|
|
64
|
+
os.environ[key] = value
|
|
65
|
+
exported += 1
|
|
66
|
+
|
|
67
|
+
# Plural pool → singular provider-key export.
|
|
68
|
+
for key, value in pairs.items():
|
|
69
|
+
if key.endswith("_API_KEYS"):
|
|
70
|
+
singular = key[:-1] # FOO_API_KEYS -> FOO_API_KEY
|
|
71
|
+
if not os.environ.get(singular):
|
|
72
|
+
first = value.split(",")[0].strip()
|
|
73
|
+
if first:
|
|
74
|
+
os.environ[singular] = first
|
|
75
|
+
exported += 1
|
|
76
|
+
|
|
77
|
+
# xAI → Grok alias (the provider registry calls it "grok").
|
|
78
|
+
for src, dst in (("XAI_API_KEY", "GROK_API_KEY"),
|
|
79
|
+
("XAI_API_KEYS", "GROK_API_KEYS")):
|
|
80
|
+
if os.environ.get(src) and not os.environ.get(dst):
|
|
81
|
+
os.environ[dst] = os.environ[src]
|
|
82
|
+
exported += 1
|
|
83
|
+
|
|
84
|
+
return exported
|