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/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