@gamaze/hicortex 0.7.1 → 0.10.0
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.
- package/README.md +57 -39
- package/dist/claude-md.d.ts +9 -21
- package/dist/claude-md.js +9 -241
- package/dist/cli.d.ts +3 -2
- package/dist/cli.js +29 -11
- package/dist/consolidate.js +0 -7
- package/dist/db.js +24 -0
- package/dist/embedder.d.ts +11 -0
- package/dist/embedder.js +27 -0
- package/dist/extensions.d.ts +41 -88
- package/dist/extensions.js +36 -61
- package/dist/features.d.ts +21 -25
- package/dist/features.js +47 -83
- package/dist/hermes-transcript-reader.d.ts +27 -0
- package/dist/hermes-transcript-reader.js +134 -0
- package/dist/index.d.ts +16 -4
- package/dist/index.js +252 -344
- package/dist/init.d.ts +41 -1
- package/dist/init.js +545 -190
- package/dist/lesson-selection.d.ts +62 -0
- package/dist/lesson-selection.js +159 -0
- package/dist/lessons-context.d.ts +17 -0
- package/dist/lessons-context.js +96 -0
- package/dist/llm.d.ts +42 -29
- package/dist/llm.js +89 -270
- package/dist/mcp-server.d.ts +0 -1
- package/dist/mcp-server.js +404 -86
- package/dist/nightly.d.ts +9 -6
- package/dist/nightly.js +197 -357
- package/dist/oc-transcript-reader.d.ts +20 -0
- package/dist/oc-transcript-reader.js +61 -0
- package/dist/pi-transcript-reader.d.ts +1 -0
- package/dist/status.js +22 -2
- package/dist/storage.d.ts +7 -1
- package/dist/storage.js +28 -7
- package/dist/transcript-reader.d.ts +19 -0
- package/dist/transcript-reader.js +17 -3
- package/dist/types.d.ts +10 -0
- package/dist/uninstall.js +31 -1
- package/hermes-plugin/hicortex/README.md +77 -0
- package/hermes-plugin/hicortex/__init__.py +17 -0
- package/hermes-plugin/hicortex/client.py +162 -0
- package/hermes-plugin/hicortex/config.py +105 -0
- package/hermes-plugin/hicortex/plugin.yaml +12 -0
- package/hermes-plugin/hicortex/provider.py +432 -0
- package/openclaw.plugin.json +17 -44
- package/package.json +7 -5
- package/dist/pro-loader.d.ts +0 -33
- package/dist/pro-loader.js +0 -187
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Config schema + load/save for the Hicortex Hermes plugin.
|
|
2
|
+
|
|
3
|
+
Config lives at ``$HERMES_HOME/plugins/hicortex/config.json``. Environment
|
|
4
|
+
variables (``HICORTEX_URL``, ``HICORTEX_AUTH_TOKEN``) override the file, so the
|
|
5
|
+
plugin also works with env-only setup.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
# Declarative config schema — drives `hermes memory setup` (see MemoryProvider
|
|
15
|
+
# .get_config_schema). Field shape per the Hermes MemoryProvider contract:
|
|
16
|
+
# key, label, description, default, required, secret, env_var, choices, url.
|
|
17
|
+
CONFIG_SCHEMA: list[dict[str, Any]] = [
|
|
18
|
+
{
|
|
19
|
+
"key": "hicortex_url",
|
|
20
|
+
"label": "Hicortex server URL",
|
|
21
|
+
"description": (
|
|
22
|
+
"URL of the Hicortex memory server. On the server host use "
|
|
23
|
+
"http://localhost:8787; on other machines use the server's "
|
|
24
|
+
"Tailscale hostname, e.g. http://memory-server:8787."
|
|
25
|
+
),
|
|
26
|
+
"default": "http://localhost:8787",
|
|
27
|
+
"required": True,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"key": "hicortex_auth_token",
|
|
31
|
+
"label": "Auth token",
|
|
32
|
+
"description": (
|
|
33
|
+
"Bearer token for the server. Omit (leave blank) when targeting "
|
|
34
|
+
"localhost — the server bypasses auth there. Default token: "
|
|
35
|
+
"hctx-default-token."
|
|
36
|
+
),
|
|
37
|
+
"secret": True,
|
|
38
|
+
"env_var": "HICORTEX_AUTH_TOKEN",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"key": "default_project",
|
|
42
|
+
"label": "Default project",
|
|
43
|
+
"description": "Optional project name to scope recall and capture.",
|
|
44
|
+
"required": False,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"key": "recall_limit",
|
|
48
|
+
"label": "Recall limit",
|
|
49
|
+
"description": "Max memories returned per recall (default 5).",
|
|
50
|
+
"default": "5",
|
|
51
|
+
"required": False,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"key": "privacy_filter",
|
|
55
|
+
"label": "Privacy filter",
|
|
56
|
+
"description": "Comma-separated privacy levels to include (e.g. WORK,PERSONAL).",
|
|
57
|
+
"default": "WORK,PERSONAL",
|
|
58
|
+
"required": False,
|
|
59
|
+
},
|
|
60
|
+
# NOTE: recall-only plugin — no capture config. Capture is handled by the
|
|
61
|
+
# nightly server-side reader of each agent's session store.
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _config_path(hermes_home: Optional[str] = None) -> str:
|
|
66
|
+
home = hermes_home or os.environ.get("HERMES_HOME") or os.path.expanduser("~/.hermes")
|
|
67
|
+
return os.path.join(home, "plugins", "hicortex", "config.json")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def load_config() -> Dict[str, Any]:
|
|
71
|
+
"""Load merged config: file <- env overrides <- defaults."""
|
|
72
|
+
path = _config_path()
|
|
73
|
+
cfg: Dict[str, Any] = {}
|
|
74
|
+
if os.path.exists(path):
|
|
75
|
+
try:
|
|
76
|
+
with open(path, encoding="utf-8") as f:
|
|
77
|
+
cfg = json.load(f) or {}
|
|
78
|
+
except Exception:
|
|
79
|
+
cfg = {}
|
|
80
|
+
|
|
81
|
+
# Env overrides
|
|
82
|
+
if os.environ.get("HICORTEX_URL"):
|
|
83
|
+
cfg["hicortex_url"] = os.environ["HICORTEX_URL"]
|
|
84
|
+
if os.environ.get("HICORTEX_AUTH_TOKEN"):
|
|
85
|
+
cfg["hicortex_auth_token"] = os.environ["HICORTEX_AUTH_TOKEN"]
|
|
86
|
+
|
|
87
|
+
# Defaults
|
|
88
|
+
cfg.setdefault("hicortex_url", "http://localhost:8787")
|
|
89
|
+
cfg.setdefault("recall_limit", 5)
|
|
90
|
+
cfg.setdefault("privacy_filter", "WORK,PERSONAL")
|
|
91
|
+
return cfg
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def save_config(values: Dict[str, Any], hermes_home: str) -> None:
|
|
95
|
+
"""Write non-secret config values to the plugin's config file.
|
|
96
|
+
|
|
97
|
+
Called by `hermes memory setup` after collecting user inputs. Secret fields
|
|
98
|
+
(hicortex_auth_token) are routed to the env store by Hermes, not written here.
|
|
99
|
+
"""
|
|
100
|
+
path = _config_path(hermes_home)
|
|
101
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
102
|
+
# Don't persist secrets to the JSON file — Hermes stores them separately.
|
|
103
|
+
safe = {k: v for k, v in values.items() if k != "hicortex_auth_token"}
|
|
104
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
105
|
+
json.dump(safe, f, indent=2)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
name: hicortex
|
|
2
|
+
version: 0.4.0
|
|
3
|
+
description: "Self-learning memory for Hermes agents — every session is distilled into lessons overnight, and your agent wakes up wiser. Injects fresh lessons each turn and exposes the full 8-tool memory surface (search, context, ingest, lessons, index, graph, update, delete) via a shared Hicortex server. Stdlib-only."
|
|
4
|
+
pip_dependencies: []
|
|
5
|
+
hooks: []
|
|
6
|
+
requires_env:
|
|
7
|
+
- name: HICORTEX_URL
|
|
8
|
+
description: "Hicortex server URL. Leave empty for a local server (http://127.0.0.1:8787)."
|
|
9
|
+
secret: false
|
|
10
|
+
- name: HICORTEX_AUTH_TOKEN
|
|
11
|
+
description: "Auth token from the Hicortex server — run `hicortex status` on the server to see it. Leave empty when the server runs on this machine (localhost bypasses auth)."
|
|
12
|
+
secret: true
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""Hicortex MemoryProvider for Hermes — recall-only.
|
|
2
|
+
|
|
3
|
+
Recall: prefetch() -> GET /search (relevant memories before each turn)
|
|
4
|
+
queue_prefetch() -> GET /search (background recall for the next turn)
|
|
5
|
+
tools -> hicortex_search / hicortex_recall_recent
|
|
6
|
+
system_prompt_block -> lessons + memory index injected into the prompt
|
|
7
|
+
|
|
8
|
+
Capture is NOT the plugin's job. A nightly reader on the Hicortex server
|
|
9
|
+
distills each agent's own session store (Hermes: ~/.hermes/profiles/<agent>/
|
|
10
|
+
state.db) centrally — see specs/2026-07-01-memory-capture-architecture.md. This
|
|
11
|
+
plugin has no local LLM, no spool, no timer, and no capture path.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import threading
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
from agent.memory_provider import MemoryProvider
|
|
23
|
+
|
|
24
|
+
from .client import HicortexClient
|
|
25
|
+
from .config import CONFIG_SCHEMA, load_config
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
_INJECT_CONTENT_CAP = 500
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class HicortexProvider(MemoryProvider):
|
|
33
|
+
"""Hicortex long-term memory backend for Hermes (recall-only)."""
|
|
34
|
+
|
|
35
|
+
def __init__(self):
|
|
36
|
+
self._client: Optional[HicortexClient] = None
|
|
37
|
+
self._project: Optional[str] = None
|
|
38
|
+
self._recall_limit: int = 5
|
|
39
|
+
self._privacy: Optional[str] = "WORK,PERSONAL"
|
|
40
|
+
self._prefetch_cache: Dict[str, str] = {}
|
|
41
|
+
self._bg_threads: List[threading.Thread] = []
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def name(self) -> str:
|
|
45
|
+
return "hicortex"
|
|
46
|
+
|
|
47
|
+
# ------------------------------------------------------------------ config
|
|
48
|
+
def _build_client(self) -> Optional[HicortexClient]:
|
|
49
|
+
cfg = load_config()
|
|
50
|
+
url = cfg.get("hicortex_url")
|
|
51
|
+
if not url:
|
|
52
|
+
return None
|
|
53
|
+
token = cfg.get("hicortex_auth_token")
|
|
54
|
+
return HicortexClient(url, auth_token=token or None)
|
|
55
|
+
|
|
56
|
+
def _client_or_none(self) -> Optional[HicortexClient]:
|
|
57
|
+
if self._client is None:
|
|
58
|
+
try:
|
|
59
|
+
self._client = self._build_client()
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.warning("hicortex: failed to build client: %s", e)
|
|
62
|
+
return self._client
|
|
63
|
+
|
|
64
|
+
def is_available(self) -> bool:
|
|
65
|
+
"""Configured and ready — NO network call (per MemoryProvider contract).
|
|
66
|
+
|
|
67
|
+
``is_available`` runs at agent init to decide whether to activate this
|
|
68
|
+
provider. Pinging the server here would mean a slow or momentarily-down
|
|
69
|
+
server silently disables memory for the whole session. Per the contract
|
|
70
|
+
("should not make network calls — just check config and installed deps")
|
|
71
|
+
we only verify a server URL is configured; per-request failures are
|
|
72
|
+
handled at use time.
|
|
73
|
+
"""
|
|
74
|
+
return self._build_client() is not None
|
|
75
|
+
|
|
76
|
+
def initialize(self, session_id: str, **kwargs) -> None:
|
|
77
|
+
cfg = load_config()
|
|
78
|
+
self._project = cfg.get("default_project") or None
|
|
79
|
+
try:
|
|
80
|
+
self._recall_limit = int(cfg.get("recall_limit", 5))
|
|
81
|
+
except (TypeError, ValueError):
|
|
82
|
+
self._recall_limit = 5
|
|
83
|
+
self._privacy = cfg.get("privacy_filter", "WORK,PERSONAL")
|
|
84
|
+
try:
|
|
85
|
+
self._client = self._build_client()
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.warning("hicortex: init client build failed: %s", e)
|
|
88
|
+
|
|
89
|
+
# ------------------------------------------------------------------- recall
|
|
90
|
+
def _format_hits(self, hits: list[dict]) -> str:
|
|
91
|
+
if not hits:
|
|
92
|
+
return ""
|
|
93
|
+
lines = [
|
|
94
|
+
"Relevant prior context from your long-term memory "
|
|
95
|
+
"(verify before relying on these — each shows date and project):"
|
|
96
|
+
]
|
|
97
|
+
for h in hits[: self._recall_limit]:
|
|
98
|
+
date = (h.get("created_at") or "")[:10]
|
|
99
|
+
proj = h.get("project") or "global"
|
|
100
|
+
content = (h.get("content") or "").strip().replace("\n", " ")
|
|
101
|
+
if len(content) > _INJECT_CONTENT_CAP:
|
|
102
|
+
content = content[:_INJECT_CONTENT_CAP] + "…"
|
|
103
|
+
lines.append(f"- [{date}, {proj}] {content}")
|
|
104
|
+
return "\n".join(lines)
|
|
105
|
+
|
|
106
|
+
def prefetch(self, query: str, *, session_id: str = "") -> str:
|
|
107
|
+
key = hashlib.sha1(query.encode("utf-8")).hexdigest()
|
|
108
|
+
cached = self._prefetch_cache.pop(key, None)
|
|
109
|
+
if cached is not None:
|
|
110
|
+
return cached
|
|
111
|
+
client = self._client_or_none()
|
|
112
|
+
if client is None:
|
|
113
|
+
return ""
|
|
114
|
+
try:
|
|
115
|
+
hits = client.search(
|
|
116
|
+
query, limit=self._recall_limit, project=self._project, privacy=self._privacy
|
|
117
|
+
)
|
|
118
|
+
return self._format_hits(hits)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.debug("hicortex prefetch failed: %s", e)
|
|
121
|
+
return ""
|
|
122
|
+
|
|
123
|
+
def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
|
|
124
|
+
client = self._client_or_none()
|
|
125
|
+
if client is None:
|
|
126
|
+
return
|
|
127
|
+
key = hashlib.sha1(query.encode("utf-8")).hexdigest()
|
|
128
|
+
|
|
129
|
+
def _bg() -> None:
|
|
130
|
+
try:
|
|
131
|
+
hits = client.search(
|
|
132
|
+
query, limit=self._recall_limit, project=self._project, privacy=self._privacy
|
|
133
|
+
)
|
|
134
|
+
self._prefetch_cache[key] = self._format_hits(hits)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.debug("hicortex queue_prefetch failed: %s", e)
|
|
137
|
+
|
|
138
|
+
self._spawn(_bg)
|
|
139
|
+
|
|
140
|
+
# ------------------------------------------------------ system prompt/tools
|
|
141
|
+
def system_prompt_block(self) -> str:
|
|
142
|
+
client = self._client_or_none()
|
|
143
|
+
if client is None:
|
|
144
|
+
return ""
|
|
145
|
+
try:
|
|
146
|
+
data = client.lessons()
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.debug("hicortex lessons fetch failed: %s", e)
|
|
149
|
+
return ""
|
|
150
|
+
lessons = (data.get("lessons") or [])[:8]
|
|
151
|
+
idx = data.get("index") or {}
|
|
152
|
+
lines = [
|
|
153
|
+
"## Hicortex long-term memory",
|
|
154
|
+
"You have shared long-term memory across sessions. Use `hicortex_search` "
|
|
155
|
+
"for specific recall and `hicortex_recall_recent` for recent context.",
|
|
156
|
+
]
|
|
157
|
+
if lessons:
|
|
158
|
+
lines.append("Lessons:")
|
|
159
|
+
for l in lessons:
|
|
160
|
+
c = (l.get("content") or "").strip().replace("\n", " ")
|
|
161
|
+
lines.append(f"- {c[:200]}")
|
|
162
|
+
if idx.get("total"):
|
|
163
|
+
lines.append(
|
|
164
|
+
f"({idx.get('total')} memories, {idx.get('lessonCount')} lessons "
|
|
165
|
+
f"across {idx.get('sourceCount')} agents)"
|
|
166
|
+
)
|
|
167
|
+
return "\n".join(lines)
|
|
168
|
+
|
|
169
|
+
def get_tool_schemas(self) -> List[Dict[str, Any]]:
|
|
170
|
+
return [
|
|
171
|
+
{
|
|
172
|
+
"name": "hicortex_search",
|
|
173
|
+
"description": (
|
|
174
|
+
"Search long-term memory using semantic similarity. Returns the most "
|
|
175
|
+
"relevant memories from past sessions."
|
|
176
|
+
),
|
|
177
|
+
"parameters": {
|
|
178
|
+
"type": "object",
|
|
179
|
+
"properties": {
|
|
180
|
+
"query": {"type": "string", "description": "Search query text"},
|
|
181
|
+
"limit": {
|
|
182
|
+
"type": "number",
|
|
183
|
+
"description": "Max results (default 5)",
|
|
184
|
+
},
|
|
185
|
+
"project": {"type": "string", "description": "Filter by project name"},
|
|
186
|
+
},
|
|
187
|
+
"required": ["query"],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "hicortex_recall_recent",
|
|
192
|
+
"description": "Recall recent context memories, optionally filtered by project.",
|
|
193
|
+
"parameters": {
|
|
194
|
+
"type": "object",
|
|
195
|
+
"properties": {
|
|
196
|
+
"project": {"type": "string"},
|
|
197
|
+
"limit": {
|
|
198
|
+
"type": "number",
|
|
199
|
+
"description": "Max results (default 10)",
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"name": "hicortex_context",
|
|
206
|
+
"description": (
|
|
207
|
+
"Get recent context memories, optionally filtered by project. "
|
|
208
|
+
"Useful to recall what happened recently."
|
|
209
|
+
),
|
|
210
|
+
"parameters": {
|
|
211
|
+
"type": "object",
|
|
212
|
+
"properties": {
|
|
213
|
+
"project": {"type": "string", "description": "Filter by project name"},
|
|
214
|
+
"limit": {"type": "number", "description": "Max results (default 10)"},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"name": "hicortex_ingest",
|
|
220
|
+
"description": (
|
|
221
|
+
"Store a new memory in long-term storage. "
|
|
222
|
+
"Use for important facts, decisions, or lessons."
|
|
223
|
+
),
|
|
224
|
+
"parameters": {
|
|
225
|
+
"type": "object",
|
|
226
|
+
"properties": {
|
|
227
|
+
"content": {"type": "string", "description": "Memory content to store"},
|
|
228
|
+
"project": {"type": "string", "description": "Project this memory belongs to"},
|
|
229
|
+
"memory_type": {
|
|
230
|
+
"type": "string",
|
|
231
|
+
"enum": ["episode", "lesson", "fact", "decision"],
|
|
232
|
+
"description": "Type of memory (default: episode)",
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
"required": ["content"],
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"name": "hicortex_lessons",
|
|
240
|
+
"description": (
|
|
241
|
+
"Get actionable lessons learned from past sessions. "
|
|
242
|
+
"Auto-generated insights about mistakes to avoid."
|
|
243
|
+
),
|
|
244
|
+
"parameters": {
|
|
245
|
+
"type": "object",
|
|
246
|
+
"properties": {
|
|
247
|
+
"project": {"type": "string", "description": "Filter by project name"},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"name": "hicortex_index",
|
|
253
|
+
"description": (
|
|
254
|
+
"Get the knowledge domain index — shows what topics and projects "
|
|
255
|
+
"are stored in memory, grouped by domain."
|
|
256
|
+
),
|
|
257
|
+
"parameters": {
|
|
258
|
+
"type": "object",
|
|
259
|
+
"properties": {},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"name": "hicortex_graph",
|
|
264
|
+
"description": (
|
|
265
|
+
"Query the memory knowledge graph — find connected memories, "
|
|
266
|
+
"hub nodes, or paths between memories."
|
|
267
|
+
),
|
|
268
|
+
"parameters": {
|
|
269
|
+
"type": "object",
|
|
270
|
+
"properties": {
|
|
271
|
+
"operation": {
|
|
272
|
+
"type": "string",
|
|
273
|
+
"enum": ["neighbors", "hubs", "path"],
|
|
274
|
+
"description": "Graph operation to perform",
|
|
275
|
+
},
|
|
276
|
+
"id": {"type": "string", "description": "Memory ID (required for neighbors and path operations)"},
|
|
277
|
+
"target_id": {"type": "string", "description": "Target memory ID (required for path operation)"},
|
|
278
|
+
"limit": {"type": "number", "description": "Max results (default 10)"},
|
|
279
|
+
"domain": {"type": "string", "description": "Filter hubs by domain"},
|
|
280
|
+
"relationship": {
|
|
281
|
+
"type": "string",
|
|
282
|
+
"description": "Filter neighbors by relationship type (e.g., CONTRADICTS, SUPERSEDES, derives)",
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
"required": ["operation"],
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
"name": "hicortex_update",
|
|
290
|
+
"description": (
|
|
291
|
+
"Update an existing memory. Use after searching to fix incorrect information. "
|
|
292
|
+
"If content changes, the embedding is re-computed."
|
|
293
|
+
),
|
|
294
|
+
"parameters": {
|
|
295
|
+
"type": "object",
|
|
296
|
+
"properties": {
|
|
297
|
+
"id": {"type": "string", "description": "Memory ID (from search results, first 8 chars or full UUID)"},
|
|
298
|
+
"content": {"type": "string", "description": "New content text"},
|
|
299
|
+
"project": {"type": "string", "description": "New project name"},
|
|
300
|
+
"memory_type": {
|
|
301
|
+
"type": "string",
|
|
302
|
+
"enum": ["episode", "lesson", "fact", "decision"],
|
|
303
|
+
"description": "New memory type",
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
"required": ["id"],
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"name": "hicortex_delete",
|
|
311
|
+
"description": (
|
|
312
|
+
"Permanently delete a memory and its links. "
|
|
313
|
+
"Use when a memory is incorrect and should be removed entirely."
|
|
314
|
+
),
|
|
315
|
+
"parameters": {
|
|
316
|
+
"type": "object",
|
|
317
|
+
"properties": {
|
|
318
|
+
"id": {"type": "string", "description": "Memory ID (from search results, first 8 chars or full UUID)"},
|
|
319
|
+
},
|
|
320
|
+
"required": ["id"],
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs) -> str:
|
|
326
|
+
client = self._client_or_none()
|
|
327
|
+
if client is None:
|
|
328
|
+
return json.dumps({"error": "hicortex server not configured"})
|
|
329
|
+
try:
|
|
330
|
+
if tool_name == "hicortex_search":
|
|
331
|
+
hits = client.search(
|
|
332
|
+
args.get("query", ""),
|
|
333
|
+
limit=int(args.get("limit", 5)),
|
|
334
|
+
project=args.get("project") or self._project,
|
|
335
|
+
)
|
|
336
|
+
return json.dumps(hits)
|
|
337
|
+
|
|
338
|
+
elif tool_name in ("hicortex_recall_recent", "hicortex_context"):
|
|
339
|
+
hits = client.context(
|
|
340
|
+
project=args.get("project") or self._project,
|
|
341
|
+
limit=int(args.get("limit", 10)),
|
|
342
|
+
)
|
|
343
|
+
return json.dumps(hits)
|
|
344
|
+
|
|
345
|
+
elif tool_name == "hicortex_ingest":
|
|
346
|
+
content = args.get("content", "")
|
|
347
|
+
if not content:
|
|
348
|
+
return json.dumps({"error": "content is required"})
|
|
349
|
+
status, resp = client.ingest(
|
|
350
|
+
content=content,
|
|
351
|
+
source_agent="hermes/manual",
|
|
352
|
+
project=args.get("project") or self._project,
|
|
353
|
+
memory_type=args.get("memory_type", "episode"),
|
|
354
|
+
)
|
|
355
|
+
if status not in (200, 201):
|
|
356
|
+
return json.dumps({"error": resp.get("error", f"HTTP {status}")})
|
|
357
|
+
id_val = resp.get("id") or ""
|
|
358
|
+
return json.dumps({"id": id_val, "message": f"Memory stored (id: {id_val[:8]})"})
|
|
359
|
+
|
|
360
|
+
elif tool_name == "hicortex_lessons":
|
|
361
|
+
data = client.lessons()
|
|
362
|
+
lessons = (data.get("lessons") or [])
|
|
363
|
+
if not lessons:
|
|
364
|
+
return json.dumps({"message": "No lessons found."})
|
|
365
|
+
return json.dumps([{"content": l.get("content", "")[:500]} for l in lessons])
|
|
366
|
+
|
|
367
|
+
elif tool_name == "hicortex_index":
|
|
368
|
+
return json.dumps(client.index())
|
|
369
|
+
|
|
370
|
+
elif tool_name == "hicortex_graph":
|
|
371
|
+
op = args.get("operation", "")
|
|
372
|
+
result = client.graph(
|
|
373
|
+
op=op,
|
|
374
|
+
id=args.get("id"),
|
|
375
|
+
target_id=args.get("target_id"),
|
|
376
|
+
limit=args.get("limit"),
|
|
377
|
+
domain=args.get("domain"),
|
|
378
|
+
relationship=args.get("relationship"),
|
|
379
|
+
)
|
|
380
|
+
return json.dumps(result)
|
|
381
|
+
|
|
382
|
+
elif tool_name == "hicortex_update":
|
|
383
|
+
id_val = args.get("id", "")
|
|
384
|
+
if not id_val:
|
|
385
|
+
return json.dumps({"error": "id is required"})
|
|
386
|
+
status, resp = client.update(
|
|
387
|
+
id=id_val,
|
|
388
|
+
content=args.get("content"),
|
|
389
|
+
project=args.get("project"),
|
|
390
|
+
memory_type=args.get("memory_type"),
|
|
391
|
+
)
|
|
392
|
+
if status == 404:
|
|
393
|
+
return json.dumps({"error": f"Memory not found: {id_val}"})
|
|
394
|
+
if status not in (200, 201):
|
|
395
|
+
return json.dumps({"error": resp.get("error", f"HTTP {status}")})
|
|
396
|
+
return json.dumps({"updated": True, "id": resp.get("id", id_val)})
|
|
397
|
+
|
|
398
|
+
elif tool_name == "hicortex_delete":
|
|
399
|
+
id_val = args.get("id", "")
|
|
400
|
+
if not id_val:
|
|
401
|
+
return json.dumps({"error": "id is required"})
|
|
402
|
+
status, resp = client.delete(id=id_val)
|
|
403
|
+
if status == 404:
|
|
404
|
+
return json.dumps({"error": f"Memory not found: {id_val}"})
|
|
405
|
+
if status not in (200, 201):
|
|
406
|
+
return json.dumps({"error": resp.get("error", f"HTTP {status}")})
|
|
407
|
+
return json.dumps({"deleted": True, "id": resp.get("id", id_val)})
|
|
408
|
+
|
|
409
|
+
else:
|
|
410
|
+
return json.dumps({"error": f"unknown tool: {tool_name}"})
|
|
411
|
+
|
|
412
|
+
except Exception as e:
|
|
413
|
+
return json.dumps({"error": str(e)})
|
|
414
|
+
|
|
415
|
+
# ---------------------------------------------------------------- lifecycle
|
|
416
|
+
def _spawn(self, fn) -> None:
|
|
417
|
+
self._bg_threads = [t for t in self._bg_threads if t.is_alive()]
|
|
418
|
+
t = threading.Thread(target=fn, daemon=True)
|
|
419
|
+
t.start()
|
|
420
|
+
self._bg_threads.append(t)
|
|
421
|
+
|
|
422
|
+
def shutdown(self) -> None:
|
|
423
|
+
for t in self._bg_threads:
|
|
424
|
+
t.join(timeout=2.0)
|
|
425
|
+
|
|
426
|
+
def get_config_schema(self) -> List[Dict[str, Any]]:
|
|
427
|
+
return CONFIG_SCHEMA
|
|
428
|
+
|
|
429
|
+
def save_config(self, values: Dict[str, Any], hermes_home: str) -> None:
|
|
430
|
+
from .config import save_config as _save
|
|
431
|
+
|
|
432
|
+
_save(values, hermes_home)
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,69 +2,42 @@
|
|
|
2
2
|
"id": "hicortex",
|
|
3
3
|
"name": "Hicortex — Long-term Memory That Learns",
|
|
4
4
|
"description": "Your agents remember past decisions, avoid repeated mistakes, and get smarter every day. Nightly reflection generates actionable lessons that automatically update agent behavior.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.10.0",
|
|
6
6
|
"kind": "lifecycle",
|
|
7
7
|
"skills": ["./skills/hicortex-memory", "./skills/hicortex-learn", "./skills/hicortex-activate"],
|
|
8
8
|
"configSchema": {
|
|
9
9
|
"type": "object",
|
|
10
10
|
"properties": {
|
|
11
|
-
"
|
|
12
|
-
"type": "string",
|
|
13
|
-
"description": "Hicortex license key (hctx-...). Leave empty for free tier (250 memory cap)."
|
|
14
|
-
},
|
|
15
|
-
"llmBaseUrl": {
|
|
16
|
-
"type": "string",
|
|
17
|
-
"description": "LLM API base URL. Auto-detected from OpenClaw config or env vars if omitted."
|
|
18
|
-
},
|
|
19
|
-
"llmApiKey": {
|
|
20
|
-
"type": "string",
|
|
21
|
-
"description": "LLM API key. Auto-detected from OpenClaw config or env vars if omitted."
|
|
22
|
-
},
|
|
23
|
-
"llmModel": {
|
|
11
|
+
"serverUrl": {
|
|
24
12
|
"type": "string",
|
|
25
|
-
"default": "
|
|
26
|
-
"description": "
|
|
13
|
+
"default": "http://127.0.0.1:8787",
|
|
14
|
+
"description": "Hicortex server URL. Defaults to localhost (co-located server). For multi-machine setups, point this at the remote server (e.g. http://bedrock:8787 or a Tailscale HTTPS URL)."
|
|
27
15
|
},
|
|
28
|
-
"
|
|
16
|
+
"authToken": {
|
|
29
17
|
"type": "string",
|
|
30
|
-
"
|
|
31
|
-
"description": "Model for nightly reflection (cloud recommended)"
|
|
18
|
+
"description": "Bearer token for the Hicortex server. Localhost bypasses auth automatically. Required for remote servers — use the authToken from ~/.hicortex/config.json on the server."
|
|
32
19
|
},
|
|
33
|
-
"
|
|
34
|
-
"type": "number",
|
|
35
|
-
"default": 2,
|
|
36
|
-
"description": "Hour (0-23, local time) to run nightly consolidation"
|
|
37
|
-
},
|
|
38
|
-
"dbPath": {
|
|
20
|
+
"licenseKey": {
|
|
39
21
|
"type": "string",
|
|
40
|
-
"description": "
|
|
22
|
+
"description": "Hicortex license key (hctx-...). Leave empty for free tier (250 memory cap)."
|
|
41
23
|
}
|
|
42
24
|
},
|
|
43
25
|
"required": []
|
|
44
26
|
},
|
|
45
27
|
"uiHints": {
|
|
28
|
+
"serverUrl": {
|
|
29
|
+
"label": "Server URL",
|
|
30
|
+
"placeholder": "http://127.0.0.1:8787"
|
|
31
|
+
},
|
|
32
|
+
"authToken": {
|
|
33
|
+
"label": "Auth Token",
|
|
34
|
+
"sensitive": true,
|
|
35
|
+
"placeholder": "Required for remote servers"
|
|
36
|
+
},
|
|
46
37
|
"licenseKey": {
|
|
47
38
|
"label": "License Key",
|
|
48
39
|
"placeholder": "hctx-... (optional, free tier without key)",
|
|
49
40
|
"sensitive": true
|
|
50
|
-
},
|
|
51
|
-
"llmBaseUrl": {
|
|
52
|
-
"label": "LLM API URL",
|
|
53
|
-
"placeholder": "Auto-detected if empty"
|
|
54
|
-
},
|
|
55
|
-
"llmApiKey": {
|
|
56
|
-
"label": "LLM API Key",
|
|
57
|
-
"sensitive": true,
|
|
58
|
-
"placeholder": "Auto-detected if empty"
|
|
59
|
-
},
|
|
60
|
-
"llmModel": {
|
|
61
|
-
"label": "Scoring Model"
|
|
62
|
-
},
|
|
63
|
-
"reflectModel": {
|
|
64
|
-
"label": "Reflection Model"
|
|
65
|
-
},
|
|
66
|
-
"consolidateHour": {
|
|
67
|
-
"label": "Consolidation Hour"
|
|
68
41
|
}
|
|
69
42
|
}
|
|
70
43
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamaze/hicortex",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Self-learning memory for AI agents \u2014 experience captured automatically, distilled into lessons overnight, shared across your whole fleet. Works with Hermes, OpenClaw, Claude Code, and Pi.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"hicortex": "dist/cli.js"
|
|
@@ -26,14 +26,16 @@
|
|
|
26
26
|
"files": [
|
|
27
27
|
"dist/",
|
|
28
28
|
"skills/",
|
|
29
|
+
"hermes-plugin/",
|
|
29
30
|
"openclaw.plugin.json",
|
|
30
31
|
"README.md"
|
|
31
32
|
],
|
|
32
33
|
"scripts": {
|
|
33
|
-
"build": "tsc",
|
|
34
|
+
"build": "rm -rf dist && tsc",
|
|
34
35
|
"test": "vitest run",
|
|
35
36
|
"test:watch": "vitest",
|
|
36
|
-
"
|
|
37
|
+
"prepack": "npm run build && rm -rf ./hermes-plugin && mkdir -p ./hermes-plugin && cp -r ../../hermes-plugin/hicortex ./hermes-plugin/ && find ./hermes-plugin -name __pycache__ -type d -exec rm -rf {} + 2>/dev/null || true",
|
|
38
|
+
"prepublishOnly": "npm run build && rm -rf ./hermes-plugin && mkdir -p ./hermes-plugin && cp -r ../../hermes-plugin/hicortex ./hermes-plugin/ && find ./hermes-plugin -name __pycache__ -type d -exec rm -rf {} + 2>/dev/null || true"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
39
41
|
"@types/better-sqlite3": "^7.6.0",
|
|
@@ -45,7 +47,7 @@
|
|
|
45
47
|
"engines": {
|
|
46
48
|
"node": ">=18"
|
|
47
49
|
},
|
|
48
|
-
"license": "
|
|
50
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
49
51
|
"homepage": "https://hicortex.gamaze.com",
|
|
50
52
|
"repository": {
|
|
51
53
|
"type": "git",
|