droste-memory 1.0.0a0__py3-none-any.whl
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.
- core/__init__.py +6 -0
- core/droste_cli.py +409 -0
- core/droste_engine.py +1067 -0
- core/droste_ingester.py +2742 -0
- core/droste_watcher.py +147 -0
- core/embedding_projector.py +279 -0
- core/treesitter_extract.py +391 -0
- droste_memory-1.0.0a0.dist-info/METADATA +21 -0
- droste_memory-1.0.0a0.dist-info/RECORD +13 -0
- droste_memory-1.0.0a0.dist-info/WHEEL +5 -0
- droste_memory-1.0.0a0.dist-info/entry_points.txt +2 -0
- droste_memory-1.0.0a0.dist-info/licenses/LICENSE +21 -0
- droste_memory-1.0.0a0.dist-info/top_level.txt +1 -0
core/__init__.py
ADDED
core/droste_cli.py
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"""Command line interface for Droste-Memory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
from urllib import error, request
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
17
|
+
if str(ROOT) not in sys.path:
|
|
18
|
+
sys.path.insert(0, str(ROOT))
|
|
19
|
+
LOCAL_PACKAGES = ROOT / ".python-packages"
|
|
20
|
+
if LOCAL_PACKAGES.exists():
|
|
21
|
+
sys.path.insert(0, str(LOCAL_PACKAGES))
|
|
22
|
+
|
|
23
|
+
from core.droste_engine import DEFAULT_DB_PATH, DrosteConceptEngine
|
|
24
|
+
from core.droste_ingester import DrosteProjectIngester, droste_zoom_query
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
VERSION = "v1.0-Alpha-Sharded"
|
|
28
|
+
VISUALIZER_CAMERA_URL = "http://127.0.0.1:5000/api/camera"
|
|
29
|
+
|
|
30
|
+
RESET = "\033[0m"
|
|
31
|
+
GREEN = "\033[92m"
|
|
32
|
+
CYAN = "\033[96m"
|
|
33
|
+
WHITE = "\033[97m"
|
|
34
|
+
DIM = "\033[2m"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
RADIAL_ART = r"""
|
|
38
|
+
.-----------------------.
|
|
39
|
+
.----' | '----.
|
|
40
|
+
.---' .-----+-----. '---.
|
|
41
|
+
.' .----' | '----. '.
|
|
42
|
+
/ .---' .---+---. '---. \
|
|
43
|
+
/ .-' .-' | '-. '-. \
|
|
44
|
+
| .' .-' .---+---. '-. '. |
|
|
45
|
+
| / .' .' | '. '. \ |
|
|
46
|
+
| | | | .--+--. | | | |
|
|
47
|
+
| --+--------+-----+---+ @ +---+-----+--------+-- |
|
|
48
|
+
| | | | '--+--' | | | |
|
|
49
|
+
| \ '. '. | .' .' / |
|
|
50
|
+
| '. '-. '---+---' .-' .' |
|
|
51
|
+
\ '-. '-. | .-' .-' /
|
|
52
|
+
\ '---. '---+---' .---' /
|
|
53
|
+
'. '----. | .----' .'
|
|
54
|
+
'---. '-----+-----' .---'
|
|
55
|
+
'----. | .----'
|
|
56
|
+
'-----------------------'
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _wants_color(enabled: bool | None = None) -> bool:
|
|
61
|
+
if enabled is not None:
|
|
62
|
+
return enabled
|
|
63
|
+
if os.environ.get("NO_COLOR"):
|
|
64
|
+
return False
|
|
65
|
+
return sys.stdout.isatty()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _paint(text: str, color: str, enabled: bool) -> str:
|
|
69
|
+
return f"{color}{text}{RESET}" if enabled else text
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def print_splash(color: bool | None = None) -> None:
|
|
73
|
+
enabled = _wants_color(color)
|
|
74
|
+
art_lines = RADIAL_ART.strip("\n").splitlines()
|
|
75
|
+
for index, line in enumerate(art_lines):
|
|
76
|
+
print(_paint(line, CYAN if index % 2 == 0 else GREEN, enabled))
|
|
77
|
+
print()
|
|
78
|
+
print(_paint("DROSTE-MEMORY // RIGID FRACTAL RADIAL LAYOUT", WHITE, enabled))
|
|
79
|
+
print(_paint(f"Local Graph Engine {VERSION}", WHITE, enabled))
|
|
80
|
+
print()
|
|
81
|
+
print(_paint("Commands", CYAN, enabled))
|
|
82
|
+
print(" droste index <path> [--reset]")
|
|
83
|
+
print(" droste status")
|
|
84
|
+
print(" droste zoom <symbol_name>")
|
|
85
|
+
print(" droste context [query] --budget 1500")
|
|
86
|
+
print()
|
|
87
|
+
print(_paint("Fast path: droste context hub_core --budget 1000 | clip", DIM, enabled))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def print_compact_header(color: bool | None = None) -> None:
|
|
91
|
+
enabled = _wants_color(color)
|
|
92
|
+
print(_paint(f"DROSTE-MEMORY {VERSION}", WHITE, enabled))
|
|
93
|
+
print(_paint("Rigid Fractal Radial Layout", CYAN, enabled))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _human_bytes(size: int) -> str:
|
|
97
|
+
units = ("B", "KB", "MB", "GB", "TB")
|
|
98
|
+
value = float(size)
|
|
99
|
+
for unit in units:
|
|
100
|
+
if value < 1024 or unit == units[-1]:
|
|
101
|
+
if unit == "B":
|
|
102
|
+
return f"{int(value)} {unit}"
|
|
103
|
+
return f"{value:.2f} {unit}"
|
|
104
|
+
value /= 1024
|
|
105
|
+
return f"{size} B"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _load_meta(db_path: Path) -> dict[str, Any]:
|
|
109
|
+
if not db_path.exists():
|
|
110
|
+
return {"storage": "missing", "error": None}
|
|
111
|
+
try:
|
|
112
|
+
return json.loads(db_path.read_text(encoding="utf-8"))
|
|
113
|
+
except json.JSONDecodeError as exc:
|
|
114
|
+
return {"storage": "corrupt", "error": str(exc)}
|
|
115
|
+
except OSError as exc:
|
|
116
|
+
return {"storage": "unreadable", "error": str(exc)}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _storage_label(db_path: Path) -> str:
|
|
120
|
+
meta = _load_meta(db_path)
|
|
121
|
+
storage = meta.get("storage")
|
|
122
|
+
if storage == "sharded":
|
|
123
|
+
return "Sharded"
|
|
124
|
+
if storage in {"missing", "corrupt", "unreadable"}:
|
|
125
|
+
return str(storage).title()
|
|
126
|
+
if isinstance(meta.get("nodes"), list) and meta.get("nodes"):
|
|
127
|
+
return "Legacy inline (migrates on next graph save)"
|
|
128
|
+
return "Inline empty"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _shard_stats(db_path: Path) -> dict[str, Any]:
|
|
132
|
+
shard_dir = db_path.parent / ".droste" / "nodes"
|
|
133
|
+
if not shard_dir.exists():
|
|
134
|
+
return {"path": str(shard_dir), "count": 0, "bytes": 0}
|
|
135
|
+
files = [path for path in shard_dir.glob("*.json") if path.is_file()]
|
|
136
|
+
return {
|
|
137
|
+
"path": str(shard_dir),
|
|
138
|
+
"count": len(files),
|
|
139
|
+
"bytes": sum(path.stat().st_size for path in files),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _engine(db_path: Path | None = None) -> DrosteConceptEngine:
|
|
144
|
+
return DrosteConceptEngine(db_path=db_path or DEFAULT_DB_PATH)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def command_status(args: argparse.Namespace) -> int:
|
|
148
|
+
engine = _engine(args.db)
|
|
149
|
+
migration = engine.ensure_sharded_storage()
|
|
150
|
+
nodes = engine.all_nodes()
|
|
151
|
+
links = engine.all_links()
|
|
152
|
+
symbol_count = sum(1 for node in nodes if node.node_type == "symbol")
|
|
153
|
+
syntax_link_count = sum(1 for link in links if link.type == "syntax_dependency")
|
|
154
|
+
shard_stats = _shard_stats(engine.db_path)
|
|
155
|
+
|
|
156
|
+
payload = {
|
|
157
|
+
"storage": _storage_label(engine.db_path),
|
|
158
|
+
"database": str(engine.db_path),
|
|
159
|
+
"node_count": len(nodes),
|
|
160
|
+
"symbol_count": symbol_count,
|
|
161
|
+
"link_count": len(links),
|
|
162
|
+
"syntax_link_count": syntax_link_count,
|
|
163
|
+
"shard_dir": shard_stats["path"],
|
|
164
|
+
"shard_count": shard_stats["count"],
|
|
165
|
+
"shard_bytes": shard_stats["bytes"],
|
|
166
|
+
"migration": migration,
|
|
167
|
+
}
|
|
168
|
+
if args.json:
|
|
169
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
170
|
+
return 0
|
|
171
|
+
|
|
172
|
+
print_compact_header()
|
|
173
|
+
print("status")
|
|
174
|
+
print(f" migration: {migration['status']}")
|
|
175
|
+
print(f" storage: {payload['storage']}")
|
|
176
|
+
print(f" database: {payload['database']}")
|
|
177
|
+
print(f" live nodes: {payload['node_count']}")
|
|
178
|
+
print(f" symbols: {payload['symbol_count']}")
|
|
179
|
+
print(f" links: {payload['link_count']}")
|
|
180
|
+
print(f" syntax links: {payload['syntax_link_count']}")
|
|
181
|
+
print(f" shard dir: {payload['shard_dir']}")
|
|
182
|
+
print(f" shard files: {payload['shard_count']}")
|
|
183
|
+
print(f" shard cache: {_human_bytes(int(payload['shard_bytes']))}")
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _post_visualizer_camera(camera: dict[str, Any], url: str) -> tuple[bool, str]:
|
|
188
|
+
payload = {
|
|
189
|
+
"x": camera.get("x", 0.0),
|
|
190
|
+
"y": camera.get("y", 0.0),
|
|
191
|
+
"zoom_level": camera.get("zoom", 1.0),
|
|
192
|
+
}
|
|
193
|
+
body = json.dumps(payload).encode("utf-8")
|
|
194
|
+
req = request.Request(
|
|
195
|
+
url,
|
|
196
|
+
data=body,
|
|
197
|
+
headers={"Content-Type": "application/json"},
|
|
198
|
+
method="POST",
|
|
199
|
+
)
|
|
200
|
+
try:
|
|
201
|
+
with request.urlopen(req, timeout=1.5) as response:
|
|
202
|
+
return True, f"HTTP {response.status}"
|
|
203
|
+
except (OSError, error.URLError, error.HTTPError) as exc:
|
|
204
|
+
return False, str(exc)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _open_editor(source_path: str | None, line_start: int | None) -> tuple[bool, str]:
|
|
208
|
+
if not source_path:
|
|
209
|
+
return False, "no source_path on focused node"
|
|
210
|
+
line = max(1, int(line_start or 1))
|
|
211
|
+
target = f"{source_path}:{line}"
|
|
212
|
+
code = shutil.which("code")
|
|
213
|
+
if code:
|
|
214
|
+
subprocess.Popen([code, "--goto", target])
|
|
215
|
+
return True, f"code --goto {target}"
|
|
216
|
+
return False, target
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def command_zoom(args: argparse.Namespace) -> int:
|
|
220
|
+
engine = _engine(args.db)
|
|
221
|
+
result = droste_zoom_query(args.symbol_name, engine=engine)
|
|
222
|
+
if result.get("status") != "focused":
|
|
223
|
+
print(f"not found: {args.symbol_name}", file=sys.stderr)
|
|
224
|
+
return 2
|
|
225
|
+
|
|
226
|
+
node = result["focused_node"]
|
|
227
|
+
camera = result["camera"]
|
|
228
|
+
print(f"focused: {node['title']}")
|
|
229
|
+
print(f" node: {node['id']}")
|
|
230
|
+
print(f" type: {node['node_type']}")
|
|
231
|
+
print(f" source: {node.get('source_path') or '(none)'}")
|
|
232
|
+
print(f" line: {node.get('line_start') or '(none)'}")
|
|
233
|
+
print(f" camera: x={camera['x']:.4f} y={camera['y']:.4f} zoom={camera['zoom']:.2f}")
|
|
234
|
+
|
|
235
|
+
if not args.no_visualizer:
|
|
236
|
+
ok, message = _post_visualizer_camera(camera, args.visualizer_url)
|
|
237
|
+
status = "sent" if ok else "not sent"
|
|
238
|
+
print(f" visualizer: {status} ({message})")
|
|
239
|
+
|
|
240
|
+
if not args.no_open:
|
|
241
|
+
opened, message = _open_editor(node.get("source_path"), node.get("line_start"))
|
|
242
|
+
status = "opened" if opened else "fallback"
|
|
243
|
+
print(f" editor: {status} ({message})")
|
|
244
|
+
return 0
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def command_context(args: argparse.Namespace) -> int:
|
|
248
|
+
engine = _engine(args.db)
|
|
249
|
+
ingester = DrosteProjectIngester(engine)
|
|
250
|
+
result = ingester.get_context(args.query, budget=args.budget)
|
|
251
|
+
if args.json:
|
|
252
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
253
|
+
return 0
|
|
254
|
+
print(result.get("compiled_context", ""))
|
|
255
|
+
return 0
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def command_index(args: argparse.Namespace) -> int:
|
|
259
|
+
from core import treesitter_extract
|
|
260
|
+
|
|
261
|
+
path = Path(args.path).expanduser()
|
|
262
|
+
if not path.is_dir():
|
|
263
|
+
print(f"not a directory: {path}", file=sys.stderr)
|
|
264
|
+
return 2
|
|
265
|
+
|
|
266
|
+
engine = _engine(args.db)
|
|
267
|
+
ingester = DrosteProjectIngester(engine)
|
|
268
|
+
result = ingester.index_project(
|
|
269
|
+
str(path),
|
|
270
|
+
reset=args.reset,
|
|
271
|
+
max_files=args.max_files,
|
|
272
|
+
max_symbols=args.max_symbols,
|
|
273
|
+
)
|
|
274
|
+
if args.json:
|
|
275
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
276
|
+
return 0
|
|
277
|
+
|
|
278
|
+
stats = result["stats"]
|
|
279
|
+
ts_on = result.get("treesitter_available", treesitter_extract.available())
|
|
280
|
+
print_compact_header()
|
|
281
|
+
print(f"indexed {path}")
|
|
282
|
+
print(f" files: {stats['file_count']}")
|
|
283
|
+
print(f" symbols: {stats['symbol_count']}")
|
|
284
|
+
print(f" links: {stats['link_count']}")
|
|
285
|
+
print(f" syntax edges: {result.get('syntax_dependency_links', 0)}")
|
|
286
|
+
print(f" reused files: {result.get('reused_files', 0)}")
|
|
287
|
+
print(f" embeddings: {engine.projector.backend}")
|
|
288
|
+
if ts_on:
|
|
289
|
+
print(" tree-sitter: on (polyglot call-graph active)")
|
|
290
|
+
else:
|
|
291
|
+
print(" tree-sitter: OFF — non-Python files degrade to symbols "
|
|
292
|
+
"without causal edges")
|
|
293
|
+
print(" fix: pip install tree-sitter-language-pack",
|
|
294
|
+
file=sys.stderr)
|
|
295
|
+
return 0
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def command_view(args: argparse.Namespace) -> int:
|
|
299
|
+
"""Export the live graph for the chosen root and open the fractal cockpit —
|
|
300
|
+
the one-command 'wow': index, then `droste view`."""
|
|
301
|
+
import http.server
|
|
302
|
+
import importlib.util
|
|
303
|
+
import socketserver
|
|
304
|
+
import threading
|
|
305
|
+
import webbrowser
|
|
306
|
+
|
|
307
|
+
vis_dir = ROOT / "visualizer"
|
|
308
|
+
spec = importlib.util.spec_from_file_location(
|
|
309
|
+
"droste_export_graph", str(vis_dir / "export_graph.py"))
|
|
310
|
+
exporter = importlib.util.module_from_spec(spec)
|
|
311
|
+
spec.loader.exec_module(exporter)
|
|
312
|
+
try:
|
|
313
|
+
counts = exporter.export(args.root, vis_dir / "graph.json")
|
|
314
|
+
except SystemExit as exc:
|
|
315
|
+
print(exc, file=sys.stderr)
|
|
316
|
+
return 2
|
|
317
|
+
|
|
318
|
+
engine = _engine(args.db)
|
|
319
|
+
nodes = engine.all_nodes()
|
|
320
|
+
links = engine.all_links()
|
|
321
|
+
(vis_dir / "status.json").write_text(json.dumps({
|
|
322
|
+
"node_count": len(nodes),
|
|
323
|
+
"symbol_count": sum(1 for n in nodes if n.node_type == "symbol"),
|
|
324
|
+
"link_count": len(links),
|
|
325
|
+
"syntax_link_count": sum(1 for l in links if l.type == "syntax_dependency"),
|
|
326
|
+
"counts": counts,
|
|
327
|
+
}, ensure_ascii=False), encoding="utf-8")
|
|
328
|
+
|
|
329
|
+
def handler(*a, **k):
|
|
330
|
+
return http.server.SimpleHTTPRequestHandler(*a, directory=str(vis_dir), **k)
|
|
331
|
+
|
|
332
|
+
socketserver.TCPServer.allow_reuse_address = True
|
|
333
|
+
httpd = socketserver.TCPServer(("127.0.0.1", args.port), handler)
|
|
334
|
+
url = f"http://127.0.0.1:{args.port}/cockpit.html"
|
|
335
|
+
print_compact_header()
|
|
336
|
+
print(f"view -> {url}")
|
|
337
|
+
print(f" project: {counts['symbol']} symbols / {counts['edge']} causal edges "
|
|
338
|
+
f"across {counts['file']} files")
|
|
339
|
+
print(" Ctrl+C to stop")
|
|
340
|
+
if not args.no_open:
|
|
341
|
+
threading.Timer(0.6, lambda: webbrowser.open(url)).start()
|
|
342
|
+
try:
|
|
343
|
+
httpd.serve_forever()
|
|
344
|
+
except KeyboardInterrupt:
|
|
345
|
+
print("\nstopped")
|
|
346
|
+
finally:
|
|
347
|
+
httpd.server_close()
|
|
348
|
+
return 0
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
352
|
+
parser = argparse.ArgumentParser(
|
|
353
|
+
prog="droste",
|
|
354
|
+
description="Droste-Memory cyberpunk graph CLI.",
|
|
355
|
+
)
|
|
356
|
+
parser.add_argument("--db", type=Path, default=DEFAULT_DB_PATH, help=argparse.SUPPRESS)
|
|
357
|
+
parser.add_argument("--version", action="version", version=f"droste {VERSION}")
|
|
358
|
+
sub = parser.add_subparsers(dest="command")
|
|
359
|
+
|
|
360
|
+
index = sub.add_parser("index", help="Index a project into the local graph.")
|
|
361
|
+
index.add_argument("path", help="Project root directory to index.")
|
|
362
|
+
index.add_argument("--reset", action="store_true",
|
|
363
|
+
help="Wipe this root's prior nodes before indexing.")
|
|
364
|
+
index.add_argument("--max-files", type=int, default=2000)
|
|
365
|
+
index.add_argument("--max-symbols", type=int, default=20000)
|
|
366
|
+
index.add_argument("--json", action="store_true", help="Emit the full index result JSON.")
|
|
367
|
+
index.set_defaults(func=command_index)
|
|
368
|
+
|
|
369
|
+
status = sub.add_parser("status", help="Show local graph health.")
|
|
370
|
+
status.add_argument("--json", action="store_true", help="Emit machine-readable JSON.")
|
|
371
|
+
status.set_defaults(func=command_status)
|
|
372
|
+
|
|
373
|
+
zoom = sub.add_parser("zoom", help="Focus a symbol in editor and visualizer.")
|
|
374
|
+
zoom.add_argument("symbol_name")
|
|
375
|
+
zoom.add_argument("--no-open", action="store_true", help="Do not launch the editor.")
|
|
376
|
+
zoom.add_argument(
|
|
377
|
+
"--no-visualizer",
|
|
378
|
+
action="store_true",
|
|
379
|
+
help="Do not POST camera coordinates to the local visualizer.",
|
|
380
|
+
)
|
|
381
|
+
zoom.add_argument("--visualizer-url", default=VISUALIZER_CAMERA_URL)
|
|
382
|
+
zoom.set_defaults(func=command_zoom)
|
|
383
|
+
|
|
384
|
+
context = sub.add_parser("context", help="Emit compressed LLM context.")
|
|
385
|
+
context.add_argument("query", nargs="?", default="project")
|
|
386
|
+
context.add_argument("--budget", type=int, default=1500)
|
|
387
|
+
context.add_argument("--json", action="store_true", help="Emit full context payload JSON.")
|
|
388
|
+
context.set_defaults(func=command_context)
|
|
389
|
+
|
|
390
|
+
view = sub.add_parser("view", help="Open the fractal visualizer on the live graph.")
|
|
391
|
+
view.add_argument("--root", default=None, help="Indexed root to view (default: most recent).")
|
|
392
|
+
view.add_argument("--port", type=int, default=7878)
|
|
393
|
+
view.add_argument("--no-open", action="store_true", help="Serve without opening a browser.")
|
|
394
|
+
view.set_defaults(func=command_view)
|
|
395
|
+
|
|
396
|
+
return parser
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def main(argv: list[str] | None = None) -> int:
|
|
400
|
+
parser = build_parser()
|
|
401
|
+
args = parser.parse_args(argv)
|
|
402
|
+
if not args.command:
|
|
403
|
+
print_splash()
|
|
404
|
+
return 0
|
|
405
|
+
return int(args.func(args))
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
if __name__ == "__main__":
|
|
409
|
+
raise SystemExit(main())
|