@gamaze/hicortex 0.7.0 → 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.
Files changed (54) hide show
  1. package/README.md +57 -39
  2. package/dist/claude-md.d.ts +9 -21
  3. package/dist/claude-md.js +9 -241
  4. package/dist/cli.d.ts +3 -2
  5. package/dist/cli.js +29 -11
  6. package/dist/consolidate.js +76 -26
  7. package/dist/db.js +24 -0
  8. package/dist/embedder.d.ts +11 -0
  9. package/dist/embedder.js +27 -0
  10. package/dist/extensions.d.ts +41 -88
  11. package/dist/extensions.js +36 -61
  12. package/dist/features.d.ts +21 -25
  13. package/dist/features.js +47 -83
  14. package/dist/graph.d.ts +1 -1
  15. package/dist/graph.js +13 -7
  16. package/dist/hermes-transcript-reader.d.ts +27 -0
  17. package/dist/hermes-transcript-reader.js +134 -0
  18. package/dist/index.d.ts +16 -4
  19. package/dist/index.js +252 -344
  20. package/dist/init.d.ts +41 -1
  21. package/dist/init.js +545 -190
  22. package/dist/lesson-selection.d.ts +62 -0
  23. package/dist/lesson-selection.js +159 -0
  24. package/dist/lessons-context.d.ts +17 -0
  25. package/dist/lessons-context.js +96 -0
  26. package/dist/llm.d.ts +42 -29
  27. package/dist/llm.js +89 -270
  28. package/dist/mcp-server.d.ts +0 -1
  29. package/dist/mcp-server.js +407 -88
  30. package/dist/nightly.d.ts +9 -6
  31. package/dist/nightly.js +197 -357
  32. package/dist/oc-transcript-reader.d.ts +20 -0
  33. package/dist/oc-transcript-reader.js +61 -0
  34. package/dist/pi-transcript-reader.d.ts +1 -0
  35. package/dist/prompts.d.ts +5 -0
  36. package/dist/prompts.js +29 -0
  37. package/dist/status.js +22 -2
  38. package/dist/storage.d.ts +7 -1
  39. package/dist/storage.js +28 -7
  40. package/dist/transcript-reader.d.ts +19 -0
  41. package/dist/transcript-reader.js +17 -3
  42. package/dist/types.d.ts +16 -0
  43. package/dist/types.js +7 -0
  44. package/dist/uninstall.js +31 -1
  45. package/hermes-plugin/hicortex/README.md +77 -0
  46. package/hermes-plugin/hicortex/__init__.py +17 -0
  47. package/hermes-plugin/hicortex/client.py +162 -0
  48. package/hermes-plugin/hicortex/config.py +105 -0
  49. package/hermes-plugin/hicortex/plugin.yaml +12 -0
  50. package/hermes-plugin/hicortex/provider.py +432 -0
  51. package/openclaw.plugin.json +17 -44
  52. package/package.json +7 -5
  53. package/dist/pro-loader.d.ts +0 -33
  54. package/dist/pro-loader.js +0 -187
@@ -0,0 +1,162 @@
1
+ """Thin HTTP client for the Hicortex memory server.
2
+
3
+ Stdlib-only (no pip dependencies) so the plugin installs with zero friction.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import urllib.error
10
+ import urllib.parse
11
+ import urllib.request
12
+ from typing import Any, Optional
13
+
14
+
15
+ class HicortexClient:
16
+ """Stateless HTTP client for the Hicortex REST surface."""
17
+
18
+ def __init__(
19
+ self,
20
+ base_url: str,
21
+ auth_token: Optional[str] = None,
22
+ timeout: float = 5.0,
23
+ ):
24
+ self.base_url = base_url.rstrip("/")
25
+ # Omit the token when targeting localhost — the server bypasses auth there.
26
+ # Match the server's bypass list exactly (mcp-server.ts): IPv4, IPv6,
27
+ # and IPv4-mapped-IPv6 (which Node reports for v4 clients on a 0.0.0.0 bind).
28
+ host = urllib.parse.urlparse(self.base_url).hostname or ""
29
+ self.auth_token = (
30
+ None
31
+ if host in ("127.0.0.1", "localhost", "::1", "::ffff:127.0.0.1")
32
+ else auth_token
33
+ )
34
+ self.timeout = timeout
35
+
36
+ def _headers(self) -> dict[str, str]:
37
+ h = {"Content-Type": "application/json", "Accept": "application/json"}
38
+ if self.auth_token:
39
+ h["Authorization"] = f"Bearer {self.auth_token}"
40
+ return h
41
+
42
+ def _get(self, path: str, params: Optional[dict[str, Any]] = None) -> Any:
43
+ url = f"{self.base_url}{path}"
44
+ if params:
45
+ qs = urllib.parse.urlencode(
46
+ {k: v for k, v in params.items() if v is not None}
47
+ )
48
+ url = f"{url}?{qs}"
49
+ req = urllib.request.Request(url, headers=self._headers(), method="GET")
50
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
51
+ return json.loads(resp.read().decode("utf-8"))
52
+
53
+ def _post(self, path: str, body: dict[str, Any]) -> tuple[int, Any]:
54
+ """POST JSON body; returns (status_code, parsed_response)."""
55
+ url = f"{self.base_url}{path}"
56
+ data = json.dumps(body).encode("utf-8")
57
+ req = urllib.request.Request(url, data=data, headers=self._headers(), method="POST")
58
+ try:
59
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
60
+ return resp.status, json.loads(resp.read().decode("utf-8"))
61
+ except urllib.error.HTTPError as e:
62
+ body_bytes = e.read()
63
+ try:
64
+ parsed = json.loads(body_bytes.decode("utf-8"))
65
+ except Exception:
66
+ parsed = {"error": body_bytes.decode("utf-8", errors="replace")}
67
+ return e.code, parsed
68
+
69
+ # -- endpoints ------------------------------------------------------------
70
+
71
+ def health(self) -> dict[str, Any]:
72
+ return self._get("/health")
73
+
74
+ def search(
75
+ self,
76
+ query: str,
77
+ limit: int = 5,
78
+ project: Optional[str] = None,
79
+ privacy: Optional[str] = None,
80
+ ) -> list[dict]:
81
+ return self._get(
82
+ "/search",
83
+ {"query": query, "limit": limit, "project": project, "privacy": privacy},
84
+ ).get("results", [])
85
+
86
+ def context(
87
+ self,
88
+ project: Optional[str] = None,
89
+ limit: int = 10,
90
+ privacy: Optional[str] = None,
91
+ ) -> list[dict]:
92
+ return self._get(
93
+ "/context", {"project": project, "limit": limit, "privacy": privacy}
94
+ ).get("results", [])
95
+
96
+ def lessons(self) -> dict[str, Any]:
97
+ return self._get("/lessons")
98
+
99
+ def index(self) -> dict[str, Any]:
100
+ return self._get("/index")
101
+
102
+ def graph(
103
+ self,
104
+ op: str,
105
+ id: Optional[str] = None,
106
+ target_id: Optional[str] = None,
107
+ limit: Optional[int] = None,
108
+ domain: Optional[str] = None,
109
+ relationship: Optional[str] = None,
110
+ ) -> dict[str, Any]:
111
+ return self._get(
112
+ "/graph",
113
+ {
114
+ "op": op,
115
+ "id": id,
116
+ "target_id": target_id,
117
+ "limit": limit,
118
+ "domain": domain,
119
+ "relationship": relationship,
120
+ },
121
+ )
122
+
123
+ def ingest(
124
+ self,
125
+ content: str,
126
+ source_agent: Optional[str] = None,
127
+ project: Optional[str] = None,
128
+ memory_type: str = "episode",
129
+ privacy: str = "WORK",
130
+ ) -> tuple[int, dict[str, Any]]:
131
+ return self._post(
132
+ "/ingest",
133
+ {
134
+ "content": content,
135
+ "source_agent": source_agent or "hermes/manual",
136
+ "project": project,
137
+ "memory_type": memory_type,
138
+ "privacy": privacy,
139
+ },
140
+ )
141
+
142
+ def update(
143
+ self,
144
+ id: str,
145
+ content: Optional[str] = None,
146
+ project: Optional[str] = None,
147
+ memory_type: Optional[str] = None,
148
+ privacy: Optional[str] = None,
149
+ ) -> tuple[int, dict[str, Any]]:
150
+ body: dict[str, Any] = {"id": id}
151
+ if content is not None:
152
+ body["content"] = content
153
+ if project is not None:
154
+ body["project"] = project
155
+ if memory_type is not None:
156
+ body["memory_type"] = memory_type
157
+ if privacy is not None:
158
+ body["privacy"] = privacy
159
+ return self._post("/update", body)
160
+
161
+ def delete(self, id: str) -> tuple[int, dict[str, Any]]:
162
+ return self._post("/delete", {"id": id})
@@ -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