hipcortex 0.3.1__tar.gz → 0.3.2__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.
- {hipcortex-0.3.1 → hipcortex-0.3.2}/PKG-INFO +1 -1
- {hipcortex-0.3.1 → hipcortex-0.3.2}/hipcortex/cli.py +58 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/hipcortex/client.py +60 -0
- hipcortex-0.3.2/hipcortex/indexer.py +219 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/hipcortex.egg-info/PKG-INFO +1 -1
- {hipcortex-0.3.1 → hipcortex-0.3.2}/hipcortex.egg-info/SOURCES.txt +1 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/pyproject.toml +1 -1
- {hipcortex-0.3.1 → hipcortex-0.3.2}/README.md +0 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/hipcortex/__init__.py +0 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/hipcortex/adapters/__init__.py +0 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/hipcortex/adapters/autogen.py +0 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/hipcortex/adapters/crewai.py +0 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/hipcortex/async_client.py +0 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/hipcortex/install/SKILL.md +0 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/hipcortex/install/__init__.py +0 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/hipcortex/langchain_memory.py +0 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/hipcortex/llamaindex_storage.py +0 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/hipcortex.egg-info/dependency_links.txt +0 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/hipcortex.egg-info/entry_points.txt +0 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/hipcortex.egg-info/requires.txt +0 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/hipcortex.egg-info/top_level.txt +0 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/setup.cfg +0 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/setup.py +0 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/tests/test_async_client.py +0 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/tests/test_async_langchain.py +0 -0
- {hipcortex-0.3.1 → hipcortex-0.3.2}/tests/test_cli_install.py +0 -0
|
@@ -1096,6 +1096,55 @@ def cmd_restore(args: argparse.Namespace) -> None:
|
|
|
1096
1096
|
__import__('sys').exit(1)
|
|
1097
1097
|
|
|
1098
1098
|
|
|
1099
|
+
def cmd_index(args: argparse.Namespace) -> None:
|
|
1100
|
+
"""Index a codebase into HipCortex symbolic knowledge graph."""
|
|
1101
|
+
url = args.url or os.environ.get("HIPCORTEX_URL", DEFAULT_URL)
|
|
1102
|
+
path = args.path or "."
|
|
1103
|
+
actor = args.actor or "codebase"
|
|
1104
|
+
|
|
1105
|
+
# Check server is reachable
|
|
1106
|
+
try:
|
|
1107
|
+
with urllib.request.urlopen(f"{url}/health", timeout=5) as r:
|
|
1108
|
+
if r.status != 200:
|
|
1109
|
+
print(f"✗ Server not reachable at {url}")
|
|
1110
|
+
sys.exit(1)
|
|
1111
|
+
except Exception:
|
|
1112
|
+
print(f"✗ Server not reachable at {url}")
|
|
1113
|
+
print(f" Start it with: hipcortex start")
|
|
1114
|
+
sys.exit(1)
|
|
1115
|
+
|
|
1116
|
+
try:
|
|
1117
|
+
from .client import HipCortexClient
|
|
1118
|
+
from .indexer import CodeIndexer
|
|
1119
|
+
except ImportError:
|
|
1120
|
+
from hipcortex.client import HipCortexClient
|
|
1121
|
+
from hipcortex.indexer import CodeIndexer
|
|
1122
|
+
|
|
1123
|
+
client = HipCortexClient(base_url=url)
|
|
1124
|
+
indexer = CodeIndexer(client=client)
|
|
1125
|
+
|
|
1126
|
+
extensions = None
|
|
1127
|
+
if args.extensions:
|
|
1128
|
+
extensions = [e if e.startswith(".") else f".{e}" for e in args.extensions.split(",")]
|
|
1129
|
+
|
|
1130
|
+
print(f"Indexing {path} into HipCortex symbolic graph...")
|
|
1131
|
+
print(f" Server: {url}")
|
|
1132
|
+
print(f" Actor: {actor}")
|
|
1133
|
+
if extensions:
|
|
1134
|
+
print(f" File types: {extensions}")
|
|
1135
|
+
print()
|
|
1136
|
+
|
|
1137
|
+
stats = indexer.index(path=path, actor=actor, extensions=extensions)
|
|
1138
|
+
|
|
1139
|
+
print(f" ✓ {stats['files']} files processed")
|
|
1140
|
+
print(f" ✓ {stats['nodes']} symbol nodes created")
|
|
1141
|
+
print(f" ✓ {stats['edges']} relationships created")
|
|
1142
|
+
print()
|
|
1143
|
+
print(f"Query code graph: GET {url}/graph")
|
|
1144
|
+
print(f"Search symbols: GET {url}/graph/search?q=<name>")
|
|
1145
|
+
print(f"In Claude Code: /hipcortex recall validate_token")
|
|
1146
|
+
|
|
1147
|
+
|
|
1099
1148
|
# ─── Argument parser ──────────────────────────────────────────────────────────
|
|
1100
1149
|
|
|
1101
1150
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -1135,6 +1184,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1135
1184
|
p_restore.add_argument("file", help="Backup file path (.json)")
|
|
1136
1185
|
p_restore.add_argument("--url", help="Server URL")
|
|
1137
1186
|
|
|
1187
|
+
# index
|
|
1188
|
+
p_index = sub.add_parser("index", help="Index a codebase into the HipCortex knowledge graph")
|
|
1189
|
+
p_index.add_argument("path", nargs="?", default=".", help="Directory or file to index (default: current dir)")
|
|
1190
|
+
p_index.add_argument("--url", help="Server URL")
|
|
1191
|
+
p_index.add_argument("--actor", help="Actor label for this codebase (default: codebase)")
|
|
1192
|
+
p_index.add_argument("--extensions", help="Comma-separated file extensions (default: .py,.ts,.js)")
|
|
1193
|
+
|
|
1138
1194
|
return parser
|
|
1139
1195
|
|
|
1140
1196
|
|
|
@@ -1154,6 +1210,8 @@ def main() -> None:
|
|
|
1154
1210
|
cmd_backup(args)
|
|
1155
1211
|
elif args.command == "restore":
|
|
1156
1212
|
cmd_restore(args)
|
|
1213
|
+
elif args.command == "index":
|
|
1214
|
+
cmd_index(args)
|
|
1157
1215
|
else:
|
|
1158
1216
|
parser.print_help()
|
|
1159
1217
|
sys.exit(1)
|
|
@@ -203,6 +203,66 @@ class HipCortexClient:
|
|
|
203
203
|
resp.raise_for_status()
|
|
204
204
|
return resp.json()
|
|
205
205
|
|
|
206
|
+
def set_state(
|
|
207
|
+
self,
|
|
208
|
+
actor: str,
|
|
209
|
+
key: str,
|
|
210
|
+
value: str,
|
|
211
|
+
confidence: float = 1.0,
|
|
212
|
+
) -> Dict[str, Any]:
|
|
213
|
+
"""Store a structured state value for an actor.
|
|
214
|
+
|
|
215
|
+
Structured state is permanent (no TTL), high-confidence, pinned.
|
|
216
|
+
Use for: current goal, chosen stack, constraints, key decisions.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
actor: Scope identifier (e.g. project name)
|
|
220
|
+
key: State key (e.g. "goal", "stack", "constraint:no-redis")
|
|
221
|
+
value: State value (free text or JSON string)
|
|
222
|
+
confidence: How confident (default 1.0)
|
|
223
|
+
|
|
224
|
+
Returns: stored record info
|
|
225
|
+
"""
|
|
226
|
+
return self.add_memory(
|
|
227
|
+
actor=actor,
|
|
228
|
+
action=f"state:{key}",
|
|
229
|
+
target=value,
|
|
230
|
+
record_type="Symbolic",
|
|
231
|
+
priority="pinned",
|
|
232
|
+
metadata={"confidence": str(confidence), "source": "user-state"},
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def get_state(
|
|
236
|
+
self,
|
|
237
|
+
actor: str,
|
|
238
|
+
key: Optional[str] = None,
|
|
239
|
+
) -> Dict[str, Any]:
|
|
240
|
+
"""Retrieve structured state for an actor.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
actor: Scope identifier
|
|
244
|
+
key: Optional specific key (e.g. "goal"). If None, returns all state.
|
|
245
|
+
|
|
246
|
+
Returns: dict of {key: value} or latest value string if key specified
|
|
247
|
+
"""
|
|
248
|
+
action_filter = f"state:{key}" if key else None
|
|
249
|
+
records = self.query_memory(actor=actor, action=action_filter, limit=50)
|
|
250
|
+
|
|
251
|
+
if key:
|
|
252
|
+
# Return latest value for this specific key
|
|
253
|
+
if records:
|
|
254
|
+
return {"key": key, "value": records[-1].get("target", ""), "actor": actor}
|
|
255
|
+
return {"key": key, "value": None, "actor": actor}
|
|
256
|
+
|
|
257
|
+
# Return all state as dict
|
|
258
|
+
state: Dict[str, Any] = {}
|
|
259
|
+
for rec in records:
|
|
260
|
+
action = rec.get("action", "")
|
|
261
|
+
if action.startswith("state:"):
|
|
262
|
+
k = action[len("state:"):]
|
|
263
|
+
state[k] = rec.get("target", "")
|
|
264
|
+
return state
|
|
265
|
+
|
|
206
266
|
def consolidate(self, actor: Optional[str] = None, threshold: float = 0.8, dry_run: bool = True) -> Dict[str, Any]:
|
|
207
267
|
"""Find near-duplicate memories and optionally merge them."""
|
|
208
268
|
params: Dict[str, Any] = {"threshold": threshold, "dry_run": dry_run}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HipCortex Code Intelligence Indexer
|
|
3
|
+
Uses Python stdlib ast + optional tree-sitter for multi-language support.
|
|
4
|
+
No external deps required for Python files.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import ast
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, Dict, List, Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CodeIndexer:
|
|
14
|
+
"""Walks a codebase, extracts symbols, feeds into HipCortex symbolic graph."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, client, base_url: str = "http://localhost:3030", api_key: Optional[str] = None):
|
|
17
|
+
"""
|
|
18
|
+
Args:
|
|
19
|
+
client: HipCortexClient instance
|
|
20
|
+
base_url: HipCortex server URL (only needed if client not provided)
|
|
21
|
+
api_key: optional API key
|
|
22
|
+
"""
|
|
23
|
+
self._client = client
|
|
24
|
+
self._node_cache: Dict[str, str] = {} # qualified_name -> node_id
|
|
25
|
+
|
|
26
|
+
def index(self, path: str, actor: str = "codebase",
|
|
27
|
+
extensions: Optional[List[str]] = None,
|
|
28
|
+
exclude_dirs: Optional[List[str]] = None) -> Dict[str, int]:
|
|
29
|
+
"""
|
|
30
|
+
Index a directory or file into HipCortex symbolic graph.
|
|
31
|
+
|
|
32
|
+
Returns: {"nodes": N, "edges": N, "files": N}
|
|
33
|
+
"""
|
|
34
|
+
if extensions is None:
|
|
35
|
+
extensions = [".py", ".ts", ".js"]
|
|
36
|
+
if exclude_dirs is None:
|
|
37
|
+
exclude_dirs = ["node_modules", ".git", "__pycache__", ".venv", "venv", "dist", "build"]
|
|
38
|
+
|
|
39
|
+
stats = {"nodes": 0, "edges": 0, "files": 0}
|
|
40
|
+
root = Path(path).resolve()
|
|
41
|
+
|
|
42
|
+
if root.is_file():
|
|
43
|
+
files = [root]
|
|
44
|
+
else:
|
|
45
|
+
files = [
|
|
46
|
+
f for f in root.rglob("*")
|
|
47
|
+
if f.suffix in extensions
|
|
48
|
+
and not any(ex in f.parts for ex in exclude_dirs)
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
for f in files:
|
|
52
|
+
try:
|
|
53
|
+
n = self._index_file(f, root, actor)
|
|
54
|
+
stats["files"] += 1
|
|
55
|
+
stats["nodes"] += n.get("nodes", 0)
|
|
56
|
+
stats["edges"] += n.get("edges", 0)
|
|
57
|
+
except Exception:
|
|
58
|
+
pass # skip unparseable files
|
|
59
|
+
|
|
60
|
+
return stats
|
|
61
|
+
|
|
62
|
+
def _make_node(self, label: str, props: Dict[str, str]) -> Optional[str]:
|
|
63
|
+
"""Create a node, return its ID. Cache by label to avoid duplicates."""
|
|
64
|
+
if label in self._node_cache:
|
|
65
|
+
return self._node_cache[label]
|
|
66
|
+
try:
|
|
67
|
+
result = self._client.create_node(label=label, properties=props)
|
|
68
|
+
node_id = result.get("id")
|
|
69
|
+
if node_id:
|
|
70
|
+
self._node_cache[label] = node_id
|
|
71
|
+
return node_id
|
|
72
|
+
except Exception:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def _make_edge(self, from_id: str, to_id: str, relation: str) -> bool:
|
|
76
|
+
try:
|
|
77
|
+
self._client.create_edge(from_id=from_id, to_id=to_id, relation=relation)
|
|
78
|
+
return True
|
|
79
|
+
except Exception:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def _index_file(self, file: Path, root: Path, actor: str) -> Dict[str, int]:
|
|
83
|
+
"""Index a single file. Returns {"nodes": N, "edges": N}."""
|
|
84
|
+
rel = str(file.relative_to(root))
|
|
85
|
+
suffix = file.suffix
|
|
86
|
+
|
|
87
|
+
if suffix == ".py":
|
|
88
|
+
return self._index_python(file, rel, actor)
|
|
89
|
+
elif suffix in (".ts", ".js"):
|
|
90
|
+
return self._index_js_simple(file, rel, actor)
|
|
91
|
+
return {"nodes": 0, "edges": 0}
|
|
92
|
+
|
|
93
|
+
def _index_python(self, file: Path, rel_path: str, actor: str) -> Dict[str, int]:
|
|
94
|
+
"""Parse Python file with ast, extract classes + functions + imports."""
|
|
95
|
+
try:
|
|
96
|
+
source = file.read_text(encoding="utf-8", errors="ignore")
|
|
97
|
+
tree = ast.parse(source, filename=str(file))
|
|
98
|
+
except SyntaxError:
|
|
99
|
+
return {"nodes": 0, "edges": 0}
|
|
100
|
+
|
|
101
|
+
nodes = 0
|
|
102
|
+
edges = 0
|
|
103
|
+
|
|
104
|
+
# File node
|
|
105
|
+
file_label = f"file:{rel_path}"
|
|
106
|
+
file_id = self._make_node(file_label, {
|
|
107
|
+
"type": "file", "path": rel_path, "language": "python"
|
|
108
|
+
})
|
|
109
|
+
if file_id:
|
|
110
|
+
nodes += 1
|
|
111
|
+
|
|
112
|
+
for node in ast.walk(tree):
|
|
113
|
+
# Class definitions
|
|
114
|
+
if isinstance(node, ast.ClassDef):
|
|
115
|
+
label = f"class:{rel_path}:{node.name}"
|
|
116
|
+
props = {
|
|
117
|
+
"type": "class",
|
|
118
|
+
"name": node.name,
|
|
119
|
+
"file": rel_path,
|
|
120
|
+
"line": str(node.lineno),
|
|
121
|
+
}
|
|
122
|
+
# Base classes
|
|
123
|
+
bases = [ast.unparse(b) for b in node.bases if isinstance(b, (ast.Name, ast.Attribute))]
|
|
124
|
+
if bases:
|
|
125
|
+
props["bases"] = ", ".join(bases[:3])
|
|
126
|
+
|
|
127
|
+
class_id = self._make_node(label, props)
|
|
128
|
+
if class_id:
|
|
129
|
+
nodes += 1
|
|
130
|
+
if file_id:
|
|
131
|
+
if self._make_edge(file_id, class_id, "defines"):
|
|
132
|
+
edges += 1
|
|
133
|
+
# Inheritance edges
|
|
134
|
+
for base in bases:
|
|
135
|
+
base_label = f"class:*:{base}"
|
|
136
|
+
# try to find existing base node
|
|
137
|
+
if base_label in self._node_cache:
|
|
138
|
+
if self._make_edge(class_id, self._node_cache[base_label], "inherits"):
|
|
139
|
+
edges += 1
|
|
140
|
+
|
|
141
|
+
# Function / method definitions
|
|
142
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
143
|
+
# Get parent context by checking if this is inside a class
|
|
144
|
+
label = f"fn:{rel_path}:{node.name}"
|
|
145
|
+
|
|
146
|
+
# Build signature
|
|
147
|
+
args = []
|
|
148
|
+
for a in node.args.args:
|
|
149
|
+
if a.annotation:
|
|
150
|
+
args.append(f"{a.arg}: {ast.unparse(a.annotation)}")
|
|
151
|
+
else:
|
|
152
|
+
args.append(a.arg)
|
|
153
|
+
|
|
154
|
+
return_ann = ""
|
|
155
|
+
if node.returns:
|
|
156
|
+
return_ann = ast.unparse(node.returns)
|
|
157
|
+
|
|
158
|
+
props = {
|
|
159
|
+
"type": "function",
|
|
160
|
+
"name": node.name,
|
|
161
|
+
"file": rel_path,
|
|
162
|
+
"line": str(node.lineno),
|
|
163
|
+
"signature": f"def {node.name}({', '.join(args[:5])})" + (f" -> {return_ann}" if return_ann else ""),
|
|
164
|
+
}
|
|
165
|
+
if any(d.id == "property" if isinstance(d, ast.Name) else False for d in node.decorator_list):
|
|
166
|
+
props["is_property"] = "true"
|
|
167
|
+
|
|
168
|
+
fn_id = self._make_node(label, props)
|
|
169
|
+
if fn_id:
|
|
170
|
+
nodes += 1
|
|
171
|
+
if file_id:
|
|
172
|
+
if self._make_edge(file_id, fn_id, "defines"):
|
|
173
|
+
edges += 1
|
|
174
|
+
|
|
175
|
+
return {"nodes": nodes, "edges": edges}
|
|
176
|
+
|
|
177
|
+
def _index_js_simple(self, file: Path, rel_path: str, actor: str) -> Dict[str, int]:
|
|
178
|
+
"""Simple regex-based JS/TS indexing (no AST parser for JS in stdlib)."""
|
|
179
|
+
import re
|
|
180
|
+
try:
|
|
181
|
+
source = file.read_text(encoding="utf-8", errors="ignore")
|
|
182
|
+
except Exception:
|
|
183
|
+
return {"nodes": 0, "edges": 0}
|
|
184
|
+
|
|
185
|
+
nodes = 0
|
|
186
|
+
edges = 0
|
|
187
|
+
|
|
188
|
+
file_label = f"file:{rel_path}"
|
|
189
|
+
file_id = self._make_node(file_label, {
|
|
190
|
+
"type": "file", "path": rel_path,
|
|
191
|
+
"language": "typescript" if rel_path.endswith(".ts") else "javascript"
|
|
192
|
+
})
|
|
193
|
+
if file_id:
|
|
194
|
+
nodes += 1
|
|
195
|
+
|
|
196
|
+
# Extract exported functions/classes
|
|
197
|
+
patterns = [
|
|
198
|
+
(r'export\s+(?:async\s+)?function\s+(\w+)', "function"),
|
|
199
|
+
(r'export\s+class\s+(\w+)', "class"),
|
|
200
|
+
(r'export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(', "function"),
|
|
201
|
+
(r'export\s+interface\s+(\w+)', "interface"),
|
|
202
|
+
(r'export\s+type\s+(\w+)', "type"),
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
for pat, kind in patterns:
|
|
206
|
+
for m in re.finditer(pat, source):
|
|
207
|
+
name = m.group(1)
|
|
208
|
+
label = f"{kind}:{rel_path}:{name}"
|
|
209
|
+
sym_id = self._make_node(label, {
|
|
210
|
+
"type": kind, "name": name, "file": rel_path,
|
|
211
|
+
"line": str(source[:m.start()].count("\n") + 1),
|
|
212
|
+
})
|
|
213
|
+
if sym_id:
|
|
214
|
+
nodes += 1
|
|
215
|
+
if file_id:
|
|
216
|
+
if self._make_edge(file_id, sym_id, "defines"):
|
|
217
|
+
edges += 1
|
|
218
|
+
|
|
219
|
+
return {"nodes": nodes, "edges": edges}
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hipcortex"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Persistent causal memory for AI agents -- LangChain, LlamaIndex, AutoGen, CrewAI"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|