memos-engine 0.1.0__tar.gz → 0.2.0__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.
Files changed (78) hide show
  1. memos_engine-0.2.0/.github/release.yml +10 -0
  2. {memos_engine-0.1.0 → memos_engine-0.2.0}/.github/workflows/release.yml +4 -0
  3. {memos_engine-0.1.0 → memos_engine-0.2.0}/.gitignore +3 -0
  4. memos_engine-0.2.0/AGENTS.md +263 -0
  5. memos_engine-0.2.0/CHANGELOG.md +19 -0
  6. {memos_engine-0.1.0 → memos_engine-0.2.0}/PKG-INFO +15 -4
  7. {memos_engine-0.1.0 → memos_engine-0.2.0}/README.md +13 -3
  8. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/cli/main.py +2 -0
  9. memos_engine-0.2.0/memos/indexer/python.py +184 -0
  10. {memos_engine-0.1.0 → memos_engine-0.2.0}/pyproject.toml +2 -1
  11. memos_engine-0.2.0/tests/fixtures/python_mini/src/main.py +5 -0
  12. memos_engine-0.2.0/tests/fixtures/python_mini/src/types.py +11 -0
  13. memos_engine-0.2.0/tests/fixtures/python_mini/src/utils.py +9 -0
  14. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/test_cli_index.py +73 -0
  15. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/test_cli_query.py +35 -0
  16. memos_engine-0.2.0/tests/test_python_indexer.py +200 -0
  17. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/test_resolver.py +66 -0
  18. {memos_engine-0.1.0 → memos_engine-0.2.0}/uv.lock +20 -2
  19. memos_engine-0.1.0/AGENTS.md +0 -150
  20. memos_engine-0.1.0/CHANGELOG.md +0 -11
  21. {memos_engine-0.1.0 → memos_engine-0.2.0}/.github/workflows/ci.yml +0 -0
  22. {memos_engine-0.1.0 → memos_engine-0.2.0}/.python-version +0 -0
  23. {memos_engine-0.1.0 → memos_engine-0.2.0}/AGENTS_EXAMPLE.md +0 -0
  24. {memos_engine-0.1.0 → memos_engine-0.2.0}/LICENSE +0 -0
  25. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/__init__.py +0 -0
  26. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/api/__init__.py +0 -0
  27. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/api/main.py +0 -0
  28. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/api/schemas.py +0 -0
  29. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/cli/__init__.py +0 -0
  30. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/core/__init__.py +0 -0
  31. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/core/db.py +0 -0
  32. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/core/migrations/0001_init.sql +0 -0
  33. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/core/migrations/0002_vec.sql +0 -0
  34. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/core/migrations/0003_prompt_version.sql +0 -0
  35. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/core/migrations/0004_memory_fts.sql +0 -0
  36. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/core/migrations/__init__.py +0 -0
  37. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/core/models.py +0 -0
  38. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/core/schema.sql +0 -0
  39. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/indexer/__init__.py +0 -0
  40. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/indexer/base.py +0 -0
  41. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/indexer/diff.py +0 -0
  42. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/indexer/go.py +0 -0
  43. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/indexer/typescript.py +0 -0
  44. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/mcp/__init__.py +0 -0
  45. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/mcp/server.py +0 -0
  46. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/query/__init__.py +0 -0
  47. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/query/core.py +0 -0
  48. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/search/__init__.py +0 -0
  49. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/search/base.py +0 -0
  50. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/search/embeddings.py +0 -0
  51. {memos_engine-0.1.0 → memos_engine-0.2.0}/memos/search/sqlite_vec_store.py +0 -0
  52. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/__init__.py +0 -0
  53. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/conftest.py +0 -0
  54. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/fixtures/__init__.py +0 -0
  55. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/fixtures/go_mini/__init__.py +0 -0
  56. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/fixtures/go_mini/src/main.go +0 -0
  57. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/fixtures/go_mini/src/types.go +0 -0
  58. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/fixtures/go_mini/src/utils.go +0 -0
  59. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/fixtures/import_cycle/src/a.ts +0 -0
  60. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/fixtures/import_cycle/src/b.ts +0 -0
  61. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/fixtures/typescript_mini/src/index.ts +0 -0
  62. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/fixtures/typescript_mini/src/types.ts +0 -0
  63. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/fixtures/typescript_mini/src/utils.ts +0 -0
  64. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/test_api.py +0 -0
  65. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/test_crud.py +0 -0
  66. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/test_dependency_graph.py +0 -0
  67. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/test_go_indexer.py +0 -0
  68. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/test_hygiene.py +0 -0
  69. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/test_impact.py +0 -0
  70. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/test_llm_summary.py +0 -0
  71. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/test_mcp.py +0 -0
  72. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/test_memory_hygiene.py +0 -0
  73. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/test_migrations.py +0 -0
  74. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/test_query.py +0 -0
  75. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/test_reindex.py +0 -0
  76. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/test_schema.py +0 -0
  77. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/test_semantic_search.py +0 -0
  78. {memos_engine-0.1.0 → memos_engine-0.2.0}/tests/test_typescript_indexer.py +0 -0
@@ -0,0 +1,10 @@
1
+ changelog:
2
+ categories:
3
+ - title: Features
4
+ labels: ["feat"]
5
+ - title: Bug Fixes
6
+ labels: ["fix"]
7
+ - title: Documentation
8
+ labels: ["docs"]
9
+ - title: Other Changes
10
+ labels: ["*"]
@@ -19,3 +19,7 @@ jobs:
19
19
  - run: uv publish
20
20
  env:
21
21
  UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
22
+ - uses: softprops/action-gh-release@v2
23
+ with:
24
+ files: dist/*
25
+ generate_release_notes: true
@@ -3,6 +3,9 @@ __pycache__/
3
3
  *.py[codz]
4
4
  *$py.class
5
5
 
6
+ # memos local index database
7
+ .memos/
8
+
6
9
  MEMORY_OS_ARCHITECTURE.md
7
10
 
8
11
  # C extensions
@@ -0,0 +1,263 @@
1
+ # AGENTS.md
2
+
3
+ > Этот репозиторий проиндексирован `memos` — используй MCP-инструменты
4
+ > memos для навигации по коду. Это быстрее grep/cat и точнее.
5
+
6
+ ## Code navigation policy (memos)
7
+
8
+ Этот проект проиндексирован `memos` — структурным индексом кода (symbols,
9
+ call edges, imports) с episodic memory. Инструмент доступен через MCP-тулы.
10
+ **Прежде чем читать файл целиком или делать grep по коду — используй memos.**
11
+ Это быстрее, точнее и не тратит контекст на нерелевантные строки.
12
+
13
+ ### Обязательный порядок действий
14
+
15
+ 1. **В начале сессии**, если ещё не открыт проект — вызови `open_project`
16
+ с абсолютным путём к корню репозитория. Это одноразовое действие на
17
+ сессию, если проект уже открыт — не вызывай повторно.
18
+
19
+ 2. **Чтобы найти определение функции/класса/переменной** — используй
20
+ `find_symbol_tool(name, kind=None, file_path=None)`.
21
+ Не используй `grep`/`cat` для этого — memos даёт точный результат
22
+ (файл, строки, сигнатура, exported) без ложных совпадений по имени
23
+ в комментариях/строках.
24
+
25
+ 3. **Чтобы понять, кто вызывает функцию или что вызывает она** — используй
26
+ `find_calls_tool(symbol_name, direction="callers"|"callees")`.
27
+ Это заменяет ручной grep по имени функции во всём репозитории.
28
+
29
+ 4. **Перед тем как редактировать функцию** — вызови `get_context_tool(symbol_id)`.
30
+ Он возвращает символ + всех callers + callees + episodic memory заметки +
31
+ кэшированный LLM-summary (если есть). **Это заменяет повторное чтение
32
+ всего файла и всех зависимых файлов** — не открывай их вручную, если
33
+ `get_context_tool` уже дал нужную картину.
34
+
35
+ 5. **Чтобы посмотреть всё содержимое файла структурно** (какие символы,
36
+ вызовы, импорты) — используй `get_module_tool(path)` вместо чтения
37
+ файла целиком, если тебе не нужен именно raw-текст (форматирование,
38
+ комментарии, точный синтаксис для правки).
39
+
40
+ 6. **Для поиска по смыслу, а не по точному имени** ("где у нас логика
41
+ авторизации", "функция, которая парсит даты") — используй
42
+ `semantic_search_tool(query, top_k)`.
43
+
44
+ 7. **После значимого решения, вывода или "гочи"**, которое стоит помнить
45
+ в будущих сессиях (архитектурное решение, причина странного кода,
46
+ TODO с контекстом) — сохрани заметку через `memory_add_note(content,
47
+ scope_type, scope_id, kind)`. Не полагайся только на свой ответ в чате —
48
+ он не переживёт сессию, а memory_entries переживают.
49
+
50
+ 8. **Перед началом работы с заметками** — вызови `get_memories(scope_type,
51
+ scope_id)`, чтобы узнать, что уже известно про этот файл/символ/проект
52
+ из прошлых сессий, прежде чем исследовать заново.
53
+
54
+ ### Когда grep/cat/read_file всё-таки уместны
55
+
56
+ - Файл ещё не проиндексирован (новый, не .ts/.tsx/.go — расширения см. ниже)
57
+ или недавно создан и явно устарел в индексе
58
+ - Нужен именно raw-текст: точное форматирование, конкретные комментарии,
59
+ импорты в исходном виде для правки строки
60
+ - `find_symbol_tool`/`semantic_search_tool` не нашли ничего релевантного —
61
+ тогда grep как fallback, но перепроверь опечатки в имени символа сначала
62
+
63
+ ### Что НЕ делать
64
+
65
+ - Не грепай по имени функции, чтобы найти её вызовы — используй
66
+ `find_calls_tool`. Grep даёт ложные совпадения (комментарии, строки,
67
+ похожие имена в других scope) и не различает caller/callee направление.
68
+ - Не читай весь файл, чтобы понять один символ — `get_context_tool` уже
69
+ дал тебе минимально достаточный контекст.
70
+ - Не забывай `open_project`, если сессия началась с чистого MCP-сервера —
71
+ без этого все остальные тулы вернут ошибку "no project open".
72
+ - Не дублируй информацию, которая уже есть в `get_memories` — сначала
73
+ проверь память, потом исследуй заново.
74
+
75
+ ### Available tools cheat sheet
76
+
77
+ | Tool | Когда использовать |
78
+ |------|---------------------|
79
+ | `open_project(path)` | В начале сессии, один раз на проект |
80
+ | `find_symbol_tool(name, kind?, file_path?)` | Найти определение символа по имени |
81
+ | `find_calls_tool(symbol_name, direction)` | Callers или callees символа |
82
+ | `get_module_tool(path)` | Всё содержимое файла структурно |
83
+ | `get_context_tool(symbol_id)` | Полный контекст перед правкой функции |
84
+ | `rename_impact_tool(symbol_id)` | Анализ последствий переименования символа |
85
+ | `diff_impact_tool(path)` | Анализ blast radius exported-символов файла |
86
+ | `semantic_search_tool(query, top_k?)` | Поиск по смыслу, не по точному имени |
87
+ | `list_files_tool(path_filter?)` | Список индексированных файлов |
88
+ | `list_symbols_tool(file_path?, kind?)` | Список индексированных символов |
89
+ | `list_projects_tool()` | Инфо и статистика по текущему проекту |
90
+ | `memory_add_note(content, scope_type, scope_id?, kind?)` | Сохранить заметку/решение |
91
+ | `get_memories(scope_type?, scope_id?)` | Прочитать заметки из прошлых сессий |
92
+ | `find_unused_symbols_tool()` | Найти неиспользуемые private функции |
93
+ | `find_dead_imports_tool()` | Найти неразрешившиеся импорты |
94
+ | `get_dependency_graph_tool()` | Граф зависимостей между файлами |
95
+ | `find_import_cycles_tool()` | Найти циклы в импортах |
96
+ | `memory_search_tool(query, top_k?)` | Полнотекстовый поиск по memory entries |
97
+ | `memory_prune_tool(older_than_days?, kind?, apply?)` | Удаление устаревших записей (dry-run по умолчанию) |
98
+ | `reindex_file_tool(path)` | Переиндексировать один файл после правки |
99
+
100
+ Индексируются файлы с расширениями `.ts`, `.tsx`, `.go`. Индекс лежит в
101
+ `.memos/memory.db` в корне проекта и обновляется через `memos index --path .`
102
+ (инкрементально, по content hash — быстро на повторных запусках). Для
103
+ переиндексации одного файла после правки используй `reindex_file_tool(path)`
104
+ — это быстрее и дешевле полного прохода.
105
+
106
+ Если каких-то тулов из таблицы нет в твоём MCP-клиенте — значит memos ещё
107
+ не переиндексирован для этого проекта или сервер не запущен: запусти
108
+ `memos serve-mcp` и настрой подключение согласно README проекта memos.
109
+
110
+
111
+ ## Workflow
112
+
113
+ After completing each task: `ruff check .` + `pytest` green, then commit with
114
+ a short descriptive title and all details in the commit body.
115
+
116
+ ## Commands
117
+
118
+ | Command | What |
119
+ |---------|------|
120
+ | `uv run pytest` | all tests |
121
+ | `uv run pytest -v` | verbose |
122
+ | `uv run pytest tests/test_crud.py::test_symbol_crud` | single test |
123
+ | `uv run memos index --path . --full` | index project at path |
124
+ | `uv run memos query symbol <name> [--kind KIND]` | find symbols by name |
125
+ | `uv run memos query calls <name> [--direction callers\|callees]` | find callers/callees |
126
+ | `uv run memos query module <path>` | show everything for a file |
127
+ | `uv run memos serve --path . --port 8000` | start FastAPI server |
128
+ | `uv run memos serve-mcp --path .` | start MCP server (stdio) |
129
+ | `uv run pytest tests/test_mcp.py` | MCP server tests |
130
+ | `uv run pytest tests/test_llm_summary.py` | LLM summary tests |
131
+ | `uv run pytest tests/test_impact.py` | Impact analysis tests |
132
+ | `uv run pytest tests/test_hygiene.py` | Dead code / hygiene tests |
133
+ | `uv run pytest tests/test_dependency_graph.py` | Dependency graph tests |
134
+ | `uv run pytest tests/test_memory_hygiene.py` | Memory search & prune tests |
135
+ | `uv run pytest tests/test_reindex.py` | Reindex tool tests |
136
+ | `curl http://localhost:8000/symbols/{id}/context` | API: get context for symbol |
137
+ | `uv add <pkg>` | add dependency |
138
+ | `uv add --dev <pkg>` | add dev dependency |
139
+ | `uv sync` | reinstall after pyproject.toml changes |
140
+
141
+ ## Layout
142
+
143
+ ```
144
+ memos/
145
+ core/
146
+ db.py # get_connection(WAL+FK), run_migrations, CRUD (all return pydantic models), resolve_call_edges
147
+ models.py # Project, File, Symbol, CallEdge, Import, MemoryEntry
148
+ schema.sql # human-readable DDL copy
149
+ migrations/
150
+ 0001_init.sql # authoritative DDL
151
+ 0002_vec.sql
152
+ 0003_prompt_version.sql
153
+ 0004_memory_fts.sql
154
+ indexer/
155
+ base.py # LanguageIndexer ABC + ParsedSymbol/Call/Import/Result dataclasses
156
+ typescript.py # TypeScriptIndexer (tree-sitter, handles .ts + .tsx)
157
+ go.py # GoIndexer (tree-sitter, export by name case)
158
+ python.py # PythonIndexer (tree-sitter, export by _ prefix) # NEW
159
+ diff.py # compute_file_hash, should_reindex
160
+ query/
161
+ core.py # find_symbol, find_calls, get_module, find_calls_by_id, semantic_search,
162
+ # list_files, list_symbols, get_or_generate_summary, get_context,
163
+ # get_rename_impact, get_diff_impact, find_unused_symbols, find_dead_imports,
164
+ # get_dependency_graph, find_import_cycles, memory_search, memory_prune
165
+ # — pure query layer over db
166
+ api/
167
+ main.py # FastAPI app — thin adapter over query/core.py
168
+ schemas.py # pydantic response models for API
169
+ mcp/
170
+ server.py # MCP server (FastMCP, stdio) — thin adapter over query/core.py
171
+ search/
172
+ base.py # EmbeddingModel + VectorStore ABCs
173
+ embeddings.py # FastEmbedEmbedding (all-MiniLM-L6-v2, 384-dim)
174
+ sqlite_vec_store.py # SqliteVecStore (sqlite-vec vec0 table)
175
+ cli/
176
+ main.py # argparse: "memos index [--path .] [--full] [--no-embed]"
177
+ # "memos query (symbol|calls|module)"
178
+ # "memos serve [--path] [--port]"
179
+ # "memos serve-mcp [--path]"
180
+ tests/
181
+ conftest.py # fixture: in-memory sqlite with migrations applied
182
+ test_schema.py # table existence checks
183
+ test_crud.py # CRUD + cascade delete
184
+ test_migrations.py# idempotent re-run
185
+ test_typescript_indexer.py # 18 unit tests on TS parsing
186
+ test_go_indexer.py # 18 unit tests on Go parsing
187
+ test_python_indexer.py # 18 unit tests on Python parsing
188
+ test_cli_index.py # 11 integration tests: index flow (TS + Go + Python)
189
+ test_resolver.py # 8 unit tests on call-edge resolution (+ Python)
190
+ test_query.py # 12 unit tests on query/core.py
191
+ test_cli_query.py # 10 integration tests: query flow (TS + Go + Python)
192
+ test_api.py # 7 integration tests: FastAPI endpoints
193
+ test_mcp.py # 15 tests: MCP server tools
194
+ test_semantic_search.py # 8 tests: VecStore CRUD + semantic_search query
195
+ test_llm_summary.py # 11 tests: get_or_generate_summary, get_context, source_hash
196
+ test_impact.py # 8 tests: rename_impact, diff_impact
197
+ test_hygiene.py # 8 tests: unused symbols, dead imports
198
+ test_dependency_graph.py # 8 tests: dependency graph + cycle detection
199
+ test_memory_hygiene.py # 9 tests: memory search + prune
200
+ test_reindex.py # 3 tests: reindex_file_tool
201
+ test_migrations.py # 2 tests: idempotent re-run
202
+ fixtures/
203
+ typescript_mini/src/ # 3 .ts files for integration testing
204
+ go_mini/src/ # 3 .go files for integration testing
205
+ python_mini/src/ # 3 .py files for integration testing # NEW
206
+ import_cycle/src/ # 2 .ts files with circular import
207
+ ```
208
+
209
+ ## Dependencies
210
+
211
+ - **pydantic** — all CRUD returns models, not raw rows
212
+ - **tree-sitter + tree-sitter-typescript + tree-sitter-go + tree-sitter-python** — AST parsing (.ts, .tsx, .go, .py)
213
+ - **stdlib sqlite3** — connection mgmt, WAL journal, FK enforcement
214
+ - **fastapi + uvicorn** — HTTP API
215
+ - **mcp[cli]** — MCP server (FastMCP, stdio transport)
216
+ - **fastembed** — ONNX embeddings (all-MiniLM-L6-v2, 384-dim)
217
+ - **sqlite-vec** — vector search extension for sqlite
218
+ - **rich** — CLI progress bars
219
+ - **pytest** (dev)
220
+ - **httpx** (dev, for TestClient)
221
+ - **pytest-anyio** (dev, for async MCP tests)
222
+ - **hatchling** (build)
223
+
224
+ ## Architecture conventions
225
+
226
+ - `indexer/typescript.py` produces plain dataclasses (`ParseResult`), not pydantic models — conversion happens in CLI when calling CRUD
227
+ - `cli/main.py` is a thin argparse wrapper over `indexer/` + `core/db.py`; no business logic in CLI
228
+ - `query/core.py` has no knowledge of CLI, API, or MCP — all three are thin adapters over it
229
+ - `mcp/server.py` is a thin FastMCP wrapper over `query/core.py`; same pattern as `api/main.py`
230
+ - All MCP tools return JSON strings (parsable by the LLM), never raw text
231
+ - `index_file()` in cli/main.py is the unit of change: delete old → parse → insert new
232
+ - `memos index` stores DB at `{project_root}/.memos/memory.db`
233
+ - call_edges are inserted with `callee_symbol_id=NULL` (first pass); resolution is Task 5
234
+ - `callee_symbol_id` and `resolved_file_id` use `ON DELETE SET NULL` — when a callee symbol is deleted (e.g. during reindex), the FK is automatically nulled, then re-resolved in the second pass
235
+ - `export` keyword detection (TS): `export_statement` wraps declarations; the walker passes `exported=True` to children
236
+ - Go export: determined by `name[0].isupper()` — no `export_statement`
237
+ - Python export: determined by `name[0] != '_'` — convention-based, like Go
238
+ - Go methods: `method_declaration` nodes are separate from type declarations; receiver type is extracted from `receiver` field
239
+ - Go imports: both single `import "x"` and grouped `import ( "x" "y" )` forms are handled via `import_spec` / `import_spec_list`
240
+ - Python imports: `import_statement` (single/as) and `import_from_statement` (from/relative) — no resolver yet
241
+
242
+ ## What does not exist yet
243
+
244
+ - Type checking, codegen — none configured
245
+
246
+ ## Execution order (from spec §6)
247
+
248
+ 1. ✅ `core/db.py` + `models.py` + `schema.sql` + migrations + tests
249
+ 2. ✅ `indexer/base.py` + `indexer/typescript.py` + CLI `index`
250
+ 3. ✅ `indexer/go.py`
251
+ 4. ✅ `query/core.py` (find_symbol, find_calls, get_module) + CLI `query`
252
+ 5. ✅ Call-edge resolution (second pass) + cross-file tests
253
+ 6. ✅ FastAPI wrapper
254
+ 7. ✅ Semantic search (sqlite-vec + sentence-transformers)
255
+ 8. ✅ MCP server
256
+ 9. ✅ `memory_entries` write-path via `memory_add_note`
257
+ 10. ✅ LLM enrichment: `get_or_generate_summary()` with content_hash check, `get_context()` composite function
258
+ 11. ✅ Section 0: Technical debt (dedup, batch resolve, SAVEPOINT, threading.Lock)
259
+ 12. ✅ Section 1: Impact analysis (rename_impact, diff_impact, MCP, API, tests)
260
+ 13. ✅ Section 2: Dead code / hygiene (unused symbols, dead imports, MCP, API, tests)
261
+ 14. ✅ Section 3: Dependency graph (graph + cycle detection, MCP, API, tests)
262
+ 15. ✅ Section 4: Memory search & hygiene (FTS5 search, prune, MCP, API, tests)
263
+ 16. ✅ Section 5: Reindex file tool (per-file reindex via MCP, tests)
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — 2026-07-21
4
+
5
+ - Python language support (indexer, tests, fixtures)
6
+ - Self-indexed: memos-engine uses memos for its own development
7
+ - PyPI package `memos-engine` with MIT license
8
+ - CI: GitHub Actions release workflow (tag → build → publish)
9
+ - GitHub Release automation with changelog categorization
10
+
11
+ ## 0.1.0 — 2026-07-21
12
+
13
+ - Initial release
14
+ - TypeScript/Go structural index via tree-sitter
15
+ - MCP server with 20 tools
16
+ - FastAPI with 15 endpoints
17
+ - Semantic search (fastembed + sqlite-vec)
18
+ - Episodic memory (FTS5)
19
+ - Impact analysis, dead code, dependency graph, memory hygiene
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memos-engine
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Memory OS for AI coding agents — structural code index with episodic memory
5
5
  Project-URL: Repository, https://github.com/TAskMAster339/memos-engine
6
6
  Author: TAskMAster339
@@ -18,6 +18,7 @@ Requires-Dist: pydantic
18
18
  Requires-Dist: rich>=15.0.0
19
19
  Requires-Dist: sqlite-vec>=0.1.0
20
20
  Requires-Dist: tree-sitter-go>=0.25.0
21
+ Requires-Dist: tree-sitter-python>=0.23.0
21
22
  Requires-Dist: tree-sitter-typescript>=0.23.2
22
23
  Requires-Dist: tree-sitter>=0.26.0
23
24
  Requires-Dist: uvicorn[standard]>=0.34.0
@@ -27,10 +28,20 @@ Description-Content-Type: text/markdown
27
28
 
28
29
  [GitHub: TAskMAster339/memos-engine](https://github.com/TAskMAster339/memos-engine.git)
29
30
 
31
+ <div align="center">
32
+
33
+ ![Python](https://img.shields.io/badge/Python-3776AB?logo=python&logoColor=fff)
34
+ ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=fff)
35
+ ![TSX](https://img.shields.io/badge/TSX-3178C6?logo=react&logoColor=fff)
36
+ ![Go](https://img.shields.io/badge/Go-00ADD8?logo=go&logoColor=fff)
37
+
38
+ </div>
39
+
30
40
  `memos` builds a **structural index** (symbols, call edges, imports) of a
31
- TypeScript/TSX / Go codebase using tree-sitter and stores it in SQLite. It is the
32
- first layer of a larger *Memory OS* for AI coding agents — instead of
33
- grepping text, agents query **structure** (definitions, callers, callees).
41
+ TypeScript / TSX / Go / Python codebase using tree-sitter and stores it in
42
+ SQLite. It is the first layer of a larger *Memory OS* for AI coding agents
43
+ — instead of grepping text, agents query **structure** (definitions,
44
+ callers, callees).
34
45
 
35
46
  ## Integrating memos into your project
36
47
 
@@ -2,10 +2,20 @@
2
2
 
3
3
  [GitHub: TAskMAster339/memos-engine](https://github.com/TAskMAster339/memos-engine.git)
4
4
 
5
+ <div align="center">
6
+
7
+ ![Python](https://img.shields.io/badge/Python-3776AB?logo=python&logoColor=fff)
8
+ ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=fff)
9
+ ![TSX](https://img.shields.io/badge/TSX-3178C6?logo=react&logoColor=fff)
10
+ ![Go](https://img.shields.io/badge/Go-00ADD8?logo=go&logoColor=fff)
11
+
12
+ </div>
13
+
5
14
  `memos` builds a **structural index** (symbols, call edges, imports) of a
6
- TypeScript/TSX / Go codebase using tree-sitter and stores it in SQLite. It is the
7
- first layer of a larger *Memory OS* for AI coding agents — instead of
8
- grepping text, agents query **structure** (definitions, callers, callees).
15
+ TypeScript / TSX / Go / Python codebase using tree-sitter and stores it in
16
+ SQLite. It is the first layer of a larger *Memory OS* for AI coding agents
17
+ — instead of grepping text, agents query **structure** (definitions,
18
+ callers, callees).
9
19
 
10
20
  ## Integrating memos into your project
11
21
 
@@ -32,6 +32,7 @@ from memos.core.db import (
32
32
  from memos.core.models import CallEdge, File, Import, Project, Symbol
33
33
  from memos.indexer.diff import compute_file_hash, should_reindex
34
34
  from memos.indexer.go import GoIndexer
35
+ from memos.indexer.python import PythonIndexer
35
36
  from memos.indexer.typescript import TypeScriptIndexer
36
37
  from memos.query.core import (
37
38
  add_memory_entry,
@@ -46,6 +47,7 @@ EXTENSION_INDEXERS = {
46
47
  ".ts": TypeScriptIndexer(tsx=False),
47
48
  ".tsx": TypeScriptIndexer(tsx=True),
48
49
  ".go": GoIndexer(),
50
+ ".py": PythonIndexer(),
49
51
  }
50
52
 
51
53
  SKIP_DIRS = {".git", ".memos", "node_modules", "__pycache__", ".venv", "target"}
@@ -0,0 +1,184 @@
1
+ import hashlib
2
+
3
+ import tree_sitter_python
4
+ from tree_sitter import Language, Parser
5
+
6
+ from memos.indexer.base import (
7
+ LanguageIndexer,
8
+ ParsedCall,
9
+ ParsedImport,
10
+ ParsedSymbol,
11
+ ParseResult,
12
+ )
13
+
14
+ _PY_LANG = Language(tree_sitter_python.language())
15
+
16
+
17
+ class PythonIndexer(LanguageIndexer):
18
+ def __init__(self):
19
+ self.parser = Parser()
20
+ self.parser.language = _PY_LANG
21
+
22
+ def language(self) -> str:
23
+ return "python"
24
+
25
+ def parse(self, source: str, file_path: str) -> ParseResult:
26
+ source_bytes = bytes(source, "utf-8")
27
+ tree = self.parser.parse(source_bytes)
28
+ result = ParseResult()
29
+ self._walk(tree.root_node, source_bytes, result)
30
+ return result
31
+
32
+ def _walk(self, node, source, result, scope=None, class_name=None): # noqa: C901, PLR0911, PLR0912
33
+ t = node.type
34
+
35
+ if t == "function_definition":
36
+ name_node = node.child_by_field_name("name")
37
+ name = self._node_text(name_node, source) if name_node else None
38
+ if name:
39
+ kind = "method" if class_name else "function"
40
+ self._add_symbol(
41
+ node, source, result, kind, name, parent_name=class_name,
42
+ )
43
+ method_scope = f"{class_name}.{name}" if class_name else name
44
+ for child in node.children:
45
+ self._walk(
46
+ child, source, result,
47
+ scope=method_scope, class_name=class_name,
48
+ )
49
+ return
50
+
51
+ if t == "class_definition":
52
+ name_node = node.child_by_field_name("name")
53
+ name = self._node_text(name_node, source) if name_node else None
54
+ if name:
55
+ self._add_symbol(node, source, result, "class", name)
56
+ for child in node.children:
57
+ self._walk(child, source, result, scope=name, class_name=name)
58
+ return
59
+
60
+ if t == "decorated_definition":
61
+ for child in node.children:
62
+ self._walk(child, source, result, scope=scope, class_name=class_name)
63
+ return
64
+
65
+ if t == "expression_statement":
66
+ if scope is None and class_name is None:
67
+ for child in node.children:
68
+ if child.type == "assignment":
69
+ self._handle_assignment(child, source, result)
70
+ for child in node.children:
71
+ self._walk(child, source, result, scope=scope, class_name=class_name)
72
+ return
73
+
74
+ if t == "call":
75
+ self._add_call(node, source, result, scope)
76
+ for child in node.children:
77
+ self._walk(child, source, result, scope=scope, class_name=class_name)
78
+ return
79
+
80
+ if t == "import_statement":
81
+ self._add_import(node, source, result)
82
+ return
83
+
84
+ if t == "import_from_statement":
85
+ self._add_from_import(node, source, result)
86
+ return
87
+
88
+ for child in node.children:
89
+ self._walk(child, source, result, scope=scope, class_name=class_name)
90
+
91
+ def _handle_assignment(self, node, source, result):
92
+ left = node.child_by_field_name("left")
93
+ if left and left.type == "identifier":
94
+ name = self._node_text(left, source)
95
+ kind = "const" if name.isupper() else "variable"
96
+ text = source[node.start_byte : node.end_byte].decode("utf-8")
97
+ content_hash = hashlib.sha256(text.encode()).hexdigest()
98
+ exported = not name.startswith("_")
99
+ result.symbols.append(
100
+ ParsedSymbol(
101
+ name=name,
102
+ kind=kind,
103
+ signature=text,
104
+ start_line=node.start_point[0] + 1,
105
+ end_line=node.end_point[0] + 1,
106
+ exported=exported,
107
+ content_hash=content_hash,
108
+ ),
109
+ )
110
+
111
+ def _add_symbol(self, node, source, result, kind, name, parent_name=None): # noqa: PLR0913
112
+ text = source[node.start_byte : node.end_byte].decode("utf-8")
113
+ content_hash = hashlib.sha256(text.encode()).hexdigest()
114
+ exported = not name.startswith("_")
115
+
116
+ if kind in ("function", "method"):
117
+ sig = self._extract_signature(node, source, text)
118
+ else:
119
+ sig = text.split("{", 1)[0].strip() if "{" in text else text
120
+
121
+ result.symbols.append(
122
+ ParsedSymbol(
123
+ name=name,
124
+ kind=kind,
125
+ signature=sig,
126
+ start_line=node.start_point[0] + 1,
127
+ end_line=node.end_point[0] + 1,
128
+ exported=exported,
129
+ content_hash=content_hash,
130
+ parent_name=parent_name,
131
+ ),
132
+ )
133
+
134
+ def _extract_signature(self, node, source, text):
135
+ params = node.child_by_field_name("parameters")
136
+ if params is None:
137
+ return None
138
+ params_end = params.end_byte - node.start_byte
139
+ return_type = node.child_by_field_name("return_type")
140
+ if return_type:
141
+ params_end = return_type.end_byte - node.start_byte
142
+ colon_idx = text.find(":", params_end)
143
+ if colon_idx != -1:
144
+ return text[: colon_idx + 1].strip()
145
+ return text
146
+
147
+ def _add_call(self, node, source, result, caller_name):
148
+ func_node = node.child_by_field_name("function")
149
+ if func_node is None:
150
+ func_node = node.children[0] if node.children else None
151
+ if func_node is None:
152
+ return
153
+ if func_node.type in ("identifier", "attribute"):
154
+ name = self._node_text(func_node, source)
155
+ else:
156
+ return
157
+ result.calls.append(
158
+ ParsedCall(
159
+ caller_name=caller_name,
160
+ callee_name=name,
161
+ line=node.start_point[0] + 1,
162
+ ),
163
+ )
164
+
165
+ def _add_import(self, node, source, result):
166
+ for child in node.children:
167
+ if child.type == "dotted_name":
168
+ path = self._node_text(child, source)
169
+ result.imports.append(ParsedImport(imported_path=path))
170
+ elif child.type == "aliased_import":
171
+ name_node = child.child_by_field_name("name")
172
+ if name_node:
173
+ path = self._node_text(name_node, source)
174
+ result.imports.append(ParsedImport(imported_path=path))
175
+
176
+ def _add_from_import(self, node, source, result):
177
+ module_node = node.child_by_field_name("module_name")
178
+ if module_node:
179
+ path = self._node_text(module_node, source)
180
+ result.imports.append(ParsedImport(imported_path=path))
181
+
182
+ @staticmethod
183
+ def _node_text(node, source):
184
+ return source[node.start_byte : node.end_byte].decode("utf-8")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "memos-engine"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Memory OS for AI coding agents — structural code index with episodic memory"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -23,6 +23,7 @@ dependencies = [
23
23
  "sqlite-vec>=0.1.0",
24
24
  "rich>=15.0.0",
25
25
  "mcp[cli]>=1.28.1",
26
+ "tree-sitter-python>=0.23.0",
26
27
  ]
27
28
 
28
29
  [project.urls]
@@ -0,0 +1,5 @@
1
+ from .utils import greet
2
+
3
+
4
+ def main():
5
+ greet("world")
@@ -0,0 +1,11 @@
1
+ class User:
2
+ name: str
3
+ age: int
4
+
5
+ def get_name(self) -> str:
6
+ return self.name
7
+
8
+
9
+ class Printer:
10
+ def print(self, msg: str) -> None:
11
+ pass
@@ -0,0 +1,9 @@
1
+ def _helper():
2
+ return 1
3
+
4
+
5
+ def greet(name):
6
+ return f"hello {name}"
7
+
8
+
9
+ PUBLIC_CONST = 42