memos-engine 0.1.0__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.
- memos/__init__.py +0 -0
- memos/api/__init__.py +0 -0
- memos/api/main.py +267 -0
- memos/api/schemas.py +118 -0
- memos/cli/__init__.py +0 -0
- memos/cli/main.py +430 -0
- memos/core/__init__.py +0 -0
- memos/core/db.py +293 -0
- memos/core/migrations/0001_init.sql +66 -0
- memos/core/migrations/0002_vec.sql +5 -0
- memos/core/migrations/0003_prompt_version.sql +3 -0
- memos/core/migrations/0004_memory_fts.sql +24 -0
- memos/core/migrations/__init__.py +0 -0
- memos/core/models.py +65 -0
- memos/core/schema.sql +73 -0
- memos/indexer/__init__.py +0 -0
- memos/indexer/base.py +41 -0
- memos/indexer/diff.py +24 -0
- memos/indexer/go.py +209 -0
- memos/indexer/typescript.py +201 -0
- memos/mcp/__init__.py +0 -0
- memos/mcp/server.py +734 -0
- memos/query/__init__.py +0 -0
- memos/query/core.py +689 -0
- memos/search/__init__.py +0 -0
- memos/search/base.py +36 -0
- memos/search/embeddings.py +30 -0
- memos/search/sqlite_vec_store.py +57 -0
- memos_engine-0.1.0.dist-info/METADATA +296 -0
- memos_engine-0.1.0.dist-info/RECORD +33 -0
- memos_engine-0.1.0.dist-info/WHEEL +4 -0
- memos_engine-0.1.0.dist-info/entry_points.txt +2 -0
- memos_engine-0.1.0.dist-info/licenses/LICENSE +21 -0
memos/cli/main.py
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import uvicorn
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.progress import (
|
|
11
|
+
BarColumn,
|
|
12
|
+
Progress,
|
|
13
|
+
SpinnerColumn,
|
|
14
|
+
TextColumn,
|
|
15
|
+
TimeElapsedColumn,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from memos.core.db import (
|
|
19
|
+
delete_file,
|
|
20
|
+
get_connection,
|
|
21
|
+
get_file_by_path,
|
|
22
|
+
get_project_by_root,
|
|
23
|
+
insert_call_edge,
|
|
24
|
+
insert_file,
|
|
25
|
+
insert_import,
|
|
26
|
+
insert_project,
|
|
27
|
+
insert_symbol,
|
|
28
|
+
remove_vec_for_file,
|
|
29
|
+
resolve_call_edges,
|
|
30
|
+
run_migrations,
|
|
31
|
+
)
|
|
32
|
+
from memos.core.models import CallEdge, File, Import, Project, Symbol
|
|
33
|
+
from memos.indexer.diff import compute_file_hash, should_reindex
|
|
34
|
+
from memos.indexer.go import GoIndexer
|
|
35
|
+
from memos.indexer.typescript import TypeScriptIndexer
|
|
36
|
+
from memos.query.core import (
|
|
37
|
+
add_memory_entry,
|
|
38
|
+
find_calls,
|
|
39
|
+
find_symbol,
|
|
40
|
+
get_memory_entries,
|
|
41
|
+
get_module,
|
|
42
|
+
)
|
|
43
|
+
from memos.search.sqlite_vec_store import SqliteVecStore
|
|
44
|
+
|
|
45
|
+
EXTENSION_INDEXERS = {
|
|
46
|
+
".ts": TypeScriptIndexer(tsx=False),
|
|
47
|
+
".tsx": TypeScriptIndexer(tsx=True),
|
|
48
|
+
".go": GoIndexer(),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
SKIP_DIRS = {".git", ".memos", "node_modules", "__pycache__", ".venv", "target"}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_or_create_project(conn, root_path: str) -> Project:
|
|
55
|
+
existing = get_project_by_root(conn, root_path)
|
|
56
|
+
if existing is not None:
|
|
57
|
+
return existing
|
|
58
|
+
project = Project(
|
|
59
|
+
root_path=root_path,
|
|
60
|
+
name=Path(root_path).name,
|
|
61
|
+
created_at=datetime.now(UTC).isoformat(),
|
|
62
|
+
)
|
|
63
|
+
return insert_project(conn, project)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def find_files(root: str) -> list[tuple[str, str]]:
|
|
67
|
+
files = []
|
|
68
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
69
|
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
|
70
|
+
for fn in filenames:
|
|
71
|
+
ext = Path(fn).suffix.lower()
|
|
72
|
+
if ext in EXTENSION_INDEXERS:
|
|
73
|
+
full = str(Path(dirpath) / fn)
|
|
74
|
+
rel = os.path.relpath(full, root)
|
|
75
|
+
files.append((full, rel))
|
|
76
|
+
return sorted(files)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def index_file(conn, project, full_path, rel_path, indexer, full, *, embed=True): # noqa: PLR0913, PLR0912, C901
|
|
80
|
+
current_hash = compute_file_hash(full_path)
|
|
81
|
+
existing = get_file_by_path(conn, project.id, rel_path)
|
|
82
|
+
|
|
83
|
+
if not should_reindex(existing, current_hash, full=full):
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
conn.execute("SAVEPOINT index_file")
|
|
87
|
+
try:
|
|
88
|
+
if existing is not None:
|
|
89
|
+
remove_vec_for_file(conn, existing.id)
|
|
90
|
+
delete_file(conn, existing.id)
|
|
91
|
+
|
|
92
|
+
with Path(full_path).open(encoding="utf-8", errors="replace") as f:
|
|
93
|
+
source = f.read()
|
|
94
|
+
|
|
95
|
+
parse_result = indexer.parse(source, rel_path)
|
|
96
|
+
|
|
97
|
+
file = File(
|
|
98
|
+
project_id=project.id,
|
|
99
|
+
path=rel_path,
|
|
100
|
+
language=indexer.language(),
|
|
101
|
+
content_hash=current_hash,
|
|
102
|
+
mtime=Path(full_path).stat().st_mtime,
|
|
103
|
+
)
|
|
104
|
+
file = insert_file(conn, file)
|
|
105
|
+
|
|
106
|
+
name_to_id = {}
|
|
107
|
+
embed_ids: list[int] = []
|
|
108
|
+
embed_texts: list[str] = []
|
|
109
|
+
for ps in parse_result.symbols:
|
|
110
|
+
sym = Symbol(
|
|
111
|
+
file_id=file.id,
|
|
112
|
+
name=ps.name,
|
|
113
|
+
kind=ps.kind,
|
|
114
|
+
signature=ps.signature,
|
|
115
|
+
start_line=ps.start_line,
|
|
116
|
+
end_line=ps.end_line,
|
|
117
|
+
exported=ps.exported,
|
|
118
|
+
content_hash=ps.content_hash,
|
|
119
|
+
)
|
|
120
|
+
sym = insert_symbol(conn, sym)
|
|
121
|
+
name_to_id[ps.name] = sym.id
|
|
122
|
+
|
|
123
|
+
if embed:
|
|
124
|
+
embed_ids.append(sym.id)
|
|
125
|
+
text = f"{ps.name} {ps.kind}"
|
|
126
|
+
if ps.signature:
|
|
127
|
+
text += f" {ps.signature}"
|
|
128
|
+
embed_texts.append(text)
|
|
129
|
+
|
|
130
|
+
if ps.parent_name:
|
|
131
|
+
parent_id = name_to_id.get(ps.parent_name)
|
|
132
|
+
if parent_id:
|
|
133
|
+
conn.execute(
|
|
134
|
+
"UPDATE symbols SET parent_symbol_id = ? WHERE id = ?",
|
|
135
|
+
(parent_id, sym.id),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if embed and embed_ids:
|
|
139
|
+
from memos.search.embeddings import FastEmbedEmbedding # noqa: PLC0415
|
|
140
|
+
|
|
141
|
+
embedder = FastEmbedEmbedding()
|
|
142
|
+
vecs = embedder.embed(embed_texts)
|
|
143
|
+
store = SqliteVecStore(conn)
|
|
144
|
+
store.add_batch(embed_ids, vecs)
|
|
145
|
+
|
|
146
|
+
for pc in parse_result.calls:
|
|
147
|
+
caller_id = name_to_id.get(pc.caller_name) if pc.caller_name else None
|
|
148
|
+
if caller_id is None:
|
|
149
|
+
continue
|
|
150
|
+
edge = CallEdge(
|
|
151
|
+
caller_symbol_id=caller_id,
|
|
152
|
+
callee_name=pc.callee_name,
|
|
153
|
+
line=pc.line,
|
|
154
|
+
)
|
|
155
|
+
insert_call_edge(conn, edge)
|
|
156
|
+
|
|
157
|
+
for pi in parse_result.imports:
|
|
158
|
+
imp = Import(
|
|
159
|
+
file_id=file.id,
|
|
160
|
+
imported_path=pi.imported_path,
|
|
161
|
+
)
|
|
162
|
+
insert_import(conn, imp)
|
|
163
|
+
|
|
164
|
+
return True
|
|
165
|
+
except Exception:
|
|
166
|
+
conn.execute("ROLLBACK TO SAVEPOINT index_file")
|
|
167
|
+
raise
|
|
168
|
+
finally:
|
|
169
|
+
conn.execute("RELEASE SAVEPOINT index_file")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def cmd_index(args):
|
|
173
|
+
root = str(Path(args.path).resolve())
|
|
174
|
+
|
|
175
|
+
memos_dir = Path(root) / ".memos"
|
|
176
|
+
memos_dir.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
db_path = str(memos_dir / "memory.db")
|
|
178
|
+
|
|
179
|
+
conn = get_connection(db_path)
|
|
180
|
+
run_migrations(conn)
|
|
181
|
+
|
|
182
|
+
project = get_or_create_project(conn, root)
|
|
183
|
+
files = find_files(root)
|
|
184
|
+
|
|
185
|
+
console = Console()
|
|
186
|
+
indexed = 0
|
|
187
|
+
errors = 0
|
|
188
|
+
embed_label = "[bright_black]no-embed[/]" if args.no_embed else ""
|
|
189
|
+
|
|
190
|
+
progress = Progress(
|
|
191
|
+
SpinnerColumn(spinner_name="dots"),
|
|
192
|
+
TextColumn("[bold]{task.description}"),
|
|
193
|
+
BarColumn(),
|
|
194
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
195
|
+
TimeElapsedColumn(),
|
|
196
|
+
TextColumn("[bright_black]{task.fields[info]}"),
|
|
197
|
+
console=console,
|
|
198
|
+
transient=False,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
with progress:
|
|
202
|
+
task = progress.add_task(
|
|
203
|
+
f"[cyan]Indexing[/] {embed_label}",
|
|
204
|
+
total=len(files),
|
|
205
|
+
info="",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
for full_path, rel_path in files:
|
|
209
|
+
ext = Path(full_path).suffix.lower()
|
|
210
|
+
indexer = EXTENSION_INDEXERS.get(ext)
|
|
211
|
+
if indexer is None:
|
|
212
|
+
progress.update(task, advance=1, info=f"[dim]skipped {rel_path}")
|
|
213
|
+
continue
|
|
214
|
+
try:
|
|
215
|
+
progress.update(task, info=f"[yellow]{rel_path}")
|
|
216
|
+
if index_file(
|
|
217
|
+
conn,
|
|
218
|
+
project,
|
|
219
|
+
full_path,
|
|
220
|
+
rel_path,
|
|
221
|
+
indexer,
|
|
222
|
+
args.full,
|
|
223
|
+
embed=not args.no_embed,
|
|
224
|
+
):
|
|
225
|
+
indexed += 1
|
|
226
|
+
progress.update(task, info=f"[green]{rel_path}")
|
|
227
|
+
else:
|
|
228
|
+
progress.update(task, info=f"[dim]{rel_path} unchanged")
|
|
229
|
+
except Exception as e:
|
|
230
|
+
errors += 1
|
|
231
|
+
progress.update(task, info=f"[red]{rel_path} error: {e}")
|
|
232
|
+
progress.update(task, advance=1)
|
|
233
|
+
|
|
234
|
+
if args.full:
|
|
235
|
+
conn.execute(
|
|
236
|
+
"UPDATE files SET mtime = ? WHERE project_id = ?",
|
|
237
|
+
(datetime.now(UTC).timestamp(), project.id),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
conn.commit()
|
|
241
|
+
|
|
242
|
+
total = conn.execute(
|
|
243
|
+
"SELECT COUNT(*) FROM call_edges ce "
|
|
244
|
+
"JOIN symbols s ON s.id = ce.caller_symbol_id "
|
|
245
|
+
"JOIN files f ON f.id = s.file_id "
|
|
246
|
+
"WHERE f.project_id = ?",
|
|
247
|
+
(project.id,),
|
|
248
|
+
).fetchone()[0]
|
|
249
|
+
resolved = resolve_call_edges(conn, project.id)
|
|
250
|
+
conn.commit()
|
|
251
|
+
conn.close()
|
|
252
|
+
|
|
253
|
+
summary = f"Indexed [green]{indexed}[/] of {len(files)} files"
|
|
254
|
+
if errors:
|
|
255
|
+
summary += f", [red]{errors} error(s)[/]"
|
|
256
|
+
if total:
|
|
257
|
+
summary += f" | resolved [cyan]{resolved}[/] of {total} call edges"
|
|
258
|
+
console.print(summary)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _open_db(args):
|
|
262
|
+
root = str(Path(args.path).resolve())
|
|
263
|
+
db_path = str(Path(root) / ".memos" / "memory.db")
|
|
264
|
+
if not Path(db_path).exists():
|
|
265
|
+
print(
|
|
266
|
+
"error: no .memos/memory.db found — run 'memos index' first",
|
|
267
|
+
file=sys.stderr,
|
|
268
|
+
)
|
|
269
|
+
sys.exit(1)
|
|
270
|
+
conn = get_connection(db_path)
|
|
271
|
+
run_migrations(conn)
|
|
272
|
+
project = get_project_by_root(conn, root)
|
|
273
|
+
if project is None:
|
|
274
|
+
print(f"error: no project found for {root}", file=sys.stderr)
|
|
275
|
+
sys.exit(1)
|
|
276
|
+
return conn, project
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def cmd_query_symbol(args):
|
|
280
|
+
conn, _project = _open_db(args)
|
|
281
|
+
results = find_symbol(conn, args.name, kind=args.kind)
|
|
282
|
+
print(json.dumps(results, indent=2, default=str))
|
|
283
|
+
conn.close()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def cmd_query_calls(args):
|
|
287
|
+
conn, _project = _open_db(args)
|
|
288
|
+
results = find_calls(conn, args.name, direction=args.direction)
|
|
289
|
+
print(json.dumps(results, indent=2, default=str))
|
|
290
|
+
conn.close()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def cmd_query_module(args):
|
|
294
|
+
conn, project = _open_db(args)
|
|
295
|
+
results = get_module(conn, args.module_path, project.id)
|
|
296
|
+
print(json.dumps(results, indent=2, default=str))
|
|
297
|
+
conn.close()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def cmd_serve(args):
|
|
301
|
+
os.environ["MEMOS_PROJECT_PATH"] = str(Path(args.path).resolve())
|
|
302
|
+
uvicorn.run("memos.api.main:app", host=args.host, port=args.port)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def cmd_serve_mcp(args):
|
|
306
|
+
from memos.mcp.server import mcp # noqa: PLC0415
|
|
307
|
+
|
|
308
|
+
mcp.run(transport="stdio")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def cmd_memory_add(args):
|
|
312
|
+
conn, project = _open_db(args)
|
|
313
|
+
result = add_memory_entry(
|
|
314
|
+
conn,
|
|
315
|
+
project.id,
|
|
316
|
+
args.content,
|
|
317
|
+
scope_type=args.scope_type,
|
|
318
|
+
scope_id=args.scope_id,
|
|
319
|
+
kind=args.kind,
|
|
320
|
+
source=args.source,
|
|
321
|
+
)
|
|
322
|
+
conn.commit()
|
|
323
|
+
conn.close()
|
|
324
|
+
print(json.dumps(result, indent=2, default=str))
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def cmd_memory_list(args):
|
|
328
|
+
conn, project = _open_db(args)
|
|
329
|
+
results = get_memory_entries(
|
|
330
|
+
conn,
|
|
331
|
+
project.id,
|
|
332
|
+
scope_type=args.scope_type,
|
|
333
|
+
scope_id=args.scope_id,
|
|
334
|
+
)
|
|
335
|
+
conn.close()
|
|
336
|
+
print(json.dumps(results, indent=2, default=str))
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def main():
|
|
340
|
+
parser = argparse.ArgumentParser(prog="memos")
|
|
341
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
342
|
+
|
|
343
|
+
p_index = sub.add_parser("index", help="Index project files")
|
|
344
|
+
p_index.add_argument("--path", default=".", help="Project root path")
|
|
345
|
+
p_index.add_argument("--full", action="store_true", help="Force full reindex")
|
|
346
|
+
p_index.add_argument(
|
|
347
|
+
"--no-embed",
|
|
348
|
+
action="store_true",
|
|
349
|
+
help="Skip embedding generation",
|
|
350
|
+
)
|
|
351
|
+
p_index.set_defaults(func=cmd_index)
|
|
352
|
+
|
|
353
|
+
p_query = sub.add_parser("query", help="Query indexed data")
|
|
354
|
+
qsub = p_query.add_subparsers(dest="query_command", required=True)
|
|
355
|
+
|
|
356
|
+
p_sym = qsub.add_parser("symbol", help="Find symbols by name")
|
|
357
|
+
p_sym.add_argument("name", help="Symbol name to search")
|
|
358
|
+
p_sym.add_argument(
|
|
359
|
+
"--kind",
|
|
360
|
+
help="Filter by symbol kind (function, class, const, etc.)",
|
|
361
|
+
)
|
|
362
|
+
p_sym.add_argument("--path", default=".", help="Project root path")
|
|
363
|
+
p_sym.set_defaults(func=cmd_query_symbol)
|
|
364
|
+
|
|
365
|
+
p_calls = qsub.add_parser("calls", help="Find callers or callees of a symbol")
|
|
366
|
+
p_calls.add_argument("name", help="Symbol name")
|
|
367
|
+
p_calls.add_argument(
|
|
368
|
+
"--direction",
|
|
369
|
+
default="callers",
|
|
370
|
+
choices=["callers", "callees"],
|
|
371
|
+
help="Direction: callers (who calls this) or callees (what this calls)",
|
|
372
|
+
)
|
|
373
|
+
p_calls.add_argument("--path", default=".", help="Project root path")
|
|
374
|
+
p_calls.set_defaults(func=cmd_query_calls)
|
|
375
|
+
|
|
376
|
+
p_mod = qsub.add_parser(
|
|
377
|
+
"module",
|
|
378
|
+
help="Show everything for a file (symbols, calls, imports)",
|
|
379
|
+
)
|
|
380
|
+
p_mod.add_argument("module_path", metavar="path", help="Relative file path")
|
|
381
|
+
p_mod.add_argument("--path", default=".", help="Project root path")
|
|
382
|
+
p_mod.set_defaults(func=cmd_query_module)
|
|
383
|
+
|
|
384
|
+
p_memory = sub.add_parser("memory", help="Manage memory entries")
|
|
385
|
+
msub = p_memory.add_subparsers(dest="memory_command", required=True)
|
|
386
|
+
|
|
387
|
+
p_mem_add = msub.add_parser("add", help="Add a memory entry")
|
|
388
|
+
p_mem_add.add_argument("content", help="Memory content text")
|
|
389
|
+
p_mem_add.add_argument(
|
|
390
|
+
"--scope-type",
|
|
391
|
+
default="project",
|
|
392
|
+
choices=["project", "file", "symbol"],
|
|
393
|
+
help="Scope type",
|
|
394
|
+
)
|
|
395
|
+
p_mem_add.add_argument("--scope-id", type=int, help="Scope ID (file or symbol id)")
|
|
396
|
+
p_mem_add.add_argument(
|
|
397
|
+
"--kind", default="note", help="Kind (note, summary, decision)"
|
|
398
|
+
)
|
|
399
|
+
p_mem_add.add_argument("--source", default="agent", help="Source of the memory")
|
|
400
|
+
p_mem_add.add_argument("--path", default=".", help="Project root path")
|
|
401
|
+
p_mem_add.set_defaults(func=cmd_memory_add)
|
|
402
|
+
|
|
403
|
+
p_mem_list = msub.add_parser("list", help="List memory entries")
|
|
404
|
+
p_mem_list.add_argument(
|
|
405
|
+
"--scope-type",
|
|
406
|
+
choices=["project", "file", "symbol"],
|
|
407
|
+
help="Filter by scope type",
|
|
408
|
+
)
|
|
409
|
+
p_mem_list.add_argument("--scope-id", type=int, help="Filter by scope ID")
|
|
410
|
+
p_mem_list.add_argument("--path", default=".", help="Project root path")
|
|
411
|
+
p_mem_list.set_defaults(func=cmd_memory_list)
|
|
412
|
+
|
|
413
|
+
p_serve = sub.add_parser("serve", help="Start the HTTP API server")
|
|
414
|
+
p_serve.add_argument("--path", default=".", help="Project root path")
|
|
415
|
+
p_serve.add_argument("--host", default="0.0.0.0", help="Bind host")
|
|
416
|
+
p_serve.add_argument("--port", type=int, default=8000, help="Bind port")
|
|
417
|
+
p_serve.set_defaults(func=cmd_serve)
|
|
418
|
+
|
|
419
|
+
p_serve_mcp = sub.add_parser(
|
|
420
|
+
"serve-mcp",
|
|
421
|
+
help=("Start MCP server (stdio). Use open_project tool to select a project."),
|
|
422
|
+
)
|
|
423
|
+
p_serve_mcp.set_defaults(func=cmd_serve_mcp)
|
|
424
|
+
|
|
425
|
+
args = parser.parse_args()
|
|
426
|
+
args.func(args)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
if __name__ == "__main__":
|
|
430
|
+
main()
|
memos/core/__init__.py
ADDED
|
File without changes
|