memdex-mcp 0.1.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.
- memdex_mcp-0.1.0/LICENSE +21 -0
- memdex_mcp-0.1.0/PKG-INFO +150 -0
- memdex_mcp-0.1.0/README.md +133 -0
- memdex_mcp-0.1.0/memdex/__init__.py +3 -0
- memdex_mcp-0.1.0/memdex/__main__.py +3 -0
- memdex_mcp-0.1.0/memdex/cli.py +209 -0
- memdex_mcp-0.1.0/memdex/embed.py +57 -0
- memdex_mcp-0.1.0/memdex/kensaku.py +60 -0
- memdex_mcp-0.1.0/memdex/mcp.py +88 -0
- memdex_mcp-0.1.0/memdex/project.py +54 -0
- memdex_mcp-0.1.0/memdex/scan.py +444 -0
- memdex_mcp-0.1.0/memdex/store.py +460 -0
- memdex_mcp-0.1.0/memdex/tools.py +317 -0
- memdex_mcp-0.1.0/memdex_mcp.egg-info/PKG-INFO +150 -0
- memdex_mcp-0.1.0/memdex_mcp.egg-info/SOURCES.txt +21 -0
- memdex_mcp-0.1.0/memdex_mcp.egg-info/dependency_links.txt +1 -0
- memdex_mcp-0.1.0/memdex_mcp.egg-info/entry_points.txt +2 -0
- memdex_mcp-0.1.0/memdex_mcp.egg-info/top_level.txt +1 -0
- memdex_mcp-0.1.0/pyproject.toml +29 -0
- memdex_mcp-0.1.0/setup.cfg +4 -0
- memdex_mcp-0.1.0/tests/test_concurrency.py +169 -0
- memdex_mcp-0.1.0/tests/test_memdex.py +248 -0
- memdex_mcp-0.1.0/tests/test_scan.py +619 -0
memdex_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 memdex contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: memdex-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Zero-dependency shared memory for AI coding agents: content-hash file summaries, symbol map, notes and handoffs over MCP.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/ChoYugyeong/memdex
|
|
7
|
+
Project-URL: Issues, https://github.com/ChoYugyeong/memdex/issues
|
|
8
|
+
Keywords: mcp,ai,agent,memory,claude,codex,context,sqlite
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Topic :: Software Development
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# memdex
|
|
19
|
+
|
|
20
|
+
**Shared, token-saving memory for AI coding agents.** Zero dependencies, one SQLite file, any MCP client.
|
|
21
|
+
|
|
22
|
+
[한국어 README](README.ko.md)
|
|
23
|
+
|
|
24
|
+
Every new agent session starts from nothing: it re-reads the same files, re-runs the same searches
|
|
25
|
+
and re-discovers the same pitfalls. When several agents (Claude Code, Codex, Cursor, DeepSeek, …)
|
|
26
|
+
work on one machine, they also can't see what the others learned or are doing right now.
|
|
27
|
+
|
|
28
|
+
memdex fixes that with a small MCP server:
|
|
29
|
+
|
|
30
|
+
| Feature | What it does | Borrowed idea |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| **Content-hash summary cache** | An agent reads a 3,000-token file once and saves a 100-token summary. The summary is keyed to the file's sha256, so it stays valid until the content changes, then is flagged stale automatically. Identical content in other worktrees or branches shares it. | content-addressed storage |
|
|
33
|
+
| **Incremental index** | Only files whose size or mtime moved are re-read. Re-indexing an 800-file repo takes ~0.2 s. | [Everything](https://www.voidtools.com/)'s USN-journal approach |
|
|
34
|
+
| **Outline + symbol map** | Free outlines (docstring + classes/functions with line numbers) for Python, JS/TS, Vue/Svelte, Go (receiver methods as `Type.Method`), Rust, Java, Kotlin, Scala, C#, Swift, Dart, C/C++, Ruby, PHP, Lua, GDScript, shell, PowerShell, Markdown, Elixir, Clojure, Zig, Julia, R, Haskell, Groovy, Protobuf, GraphQL, SQL. `where_is` replaces repeated grepping. | Serena / repo maps, without tree-sitter |
|
|
35
|
+
| **Notes that know when they rot** | Decisions, pitfalls, todos and handoffs, linked to the files they're about. When those files change, the note is marked "⚠ changed since". Notes are append-only (`supersedes`), safe for concurrent agents. | Basic Memory, mem0 |
|
|
36
|
+
| **Agent coordination** | `post`/`inbox` messaging and `claim`s ("I'm editing src/store, 2h") with conflict warnings. All worktrees of a repo share one project memory. | — |
|
|
37
|
+
| **Hybrid search** | SQLite FTS5 trigram (works for Korean/Japanese/Chinese, with a fallback for 2-character terms) + optional local embeddings via Ollama, merged by reciprocal rank fusion. | claude-context |
|
|
38
|
+
| **Bounded reads** | `read_snippet` returns ~256 tokens around a match or a line instead of the whole file. Pair it with `where_is`. | [Kensaku](https://github.com/yjid/kensaku)'s `read_snippet` |
|
|
39
|
+
| **Whole-machine content search** | With Kensaku installed, `search` also returns full-text, OCR and semantic hits from PDFs, Office docs and screenshots. | Kensaku |
|
|
40
|
+
|
|
41
|
+
Measured on a real 800-file TypeScript repo: a 144 KB source file → 1.6 KB `file_summary` response (outline only,
|
|
42
|
+
before any agent summary was saved).
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pipx install memdex-mcp # or: pip install memdex-mcp
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The PyPI name is `memdex-mcp` (plain `memdex` was taken); the command, package and MCP server are all `memdex`.
|
|
51
|
+
From source: `pipx install git+https://github.com/ChoYugyeong/memdex` or `pip install -e .` in a checkout.
|
|
52
|
+
|
|
53
|
+
Python 3.9+. No other dependencies — the MCP stdio protocol is implemented directly, so it won't break across MCP SDK versions.
|
|
54
|
+
|
|
55
|
+
## Connect your agents
|
|
56
|
+
|
|
57
|
+
`memdex config --agent <name>` prints these snippets.
|
|
58
|
+
|
|
59
|
+
**Claude Code**
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
claude mcp add memdex --scope user -e MEMDEX_AGENT=claude -- memdex serve
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Optional: inject the brief at every session start (`~/.claude/settings.json`):
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{ "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": "memdex hook session-start --agent claude" } ] } ] } }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Any other MCP client** (Claude Desktop, Cursor, Codex, Windsurf, DeepSeek harness, …)
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{ "mcpServers": { "memdex": { "command": "memdex", "args": ["serve"], "env": { "MEMDEX_AGENT": "codex" } } } }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Give each agent its own `MEMDEX_AGENT` name. Its full id becomes `name@checkout`, e.g. `claude@myrepo-wt`.
|
|
78
|
+
|
|
79
|
+
## Tools
|
|
80
|
+
|
|
81
|
+
| Tool | Use |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `project_brief` | Start of session: last handoff, pitfalls, decisions, todos, unread messages, other agents' claims. Refreshes the index. |
|
|
84
|
+
| `file_summary` | Before reading a file: cached summary (if the content is unchanged) + outline. |
|
|
85
|
+
| `file_summary_save` | After reading a file: store a 2–6 line summary for everyone. |
|
|
86
|
+
| `where_is` | Symbol definition lookup. |
|
|
87
|
+
| `read_snippet` | A bounded slice of a file around `query` or `line` (default ~256 tokens). |
|
|
88
|
+
| `search` | Notes, handoffs and file summaries; keyword + semantic. Plus Kensaku content hits when installed (`everywhere: true` for the whole machine). |
|
|
89
|
+
| `remember` / `note` | Save / read / archive notes (`note`, `decision`, `pitfall`, `todo`, `handoff`). |
|
|
90
|
+
| `handoff` | End of session: done / next / decisions / pitfalls. Releases your claims. |
|
|
91
|
+
| `post` / `inbox` | Messages between agents (broadcast or `to: "deepseek"`). |
|
|
92
|
+
| `claim` | Announce what you're editing; warns about overlaps. |
|
|
93
|
+
|
|
94
|
+
The server sends usage instructions in its MCP `initialize` response, so agents know the workflow without extra prompting.
|
|
95
|
+
The 12 tool schemas cost about 1,000 tokens per request.
|
|
96
|
+
|
|
97
|
+
## CLI
|
|
98
|
+
|
|
99
|
+
Everything the MCP tools do is also a command, for shell-capable agents and for humans:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
memdex brief --agent codex # start of session: handoff, notes, inbox, claims
|
|
103
|
+
memdex file src/store.py # cached summary + outline
|
|
104
|
+
memdex file src/store.py --summary "…" # save a summary
|
|
105
|
+
memdex snippet src/store.py --query "def index"
|
|
106
|
+
memdex search "locked"
|
|
107
|
+
memdex remember pitfall "WAL mode needed" --body "…" --files src/store.py
|
|
108
|
+
memdex claim src/store.py --intent "refactor"
|
|
109
|
+
memdex post "store.py is mine for an hour" --to claude
|
|
110
|
+
memdex inbox
|
|
111
|
+
memdex handoff "Done X" --next "Y" --pitfalls "Z"
|
|
112
|
+
memdex index · memdex export --out notes/ · memdex stats
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Where data lives
|
|
116
|
+
|
|
117
|
+
Everything is in `~/.memdex/memdex.db` (override with `MEMDEX_HOME`). Nothing is written into your
|
|
118
|
+
repositories, so memory never ends up in a commit. Files that look like secrets (`.env*`, `*.pem`, `*.key`,
|
|
119
|
+
`*secret*`, `*credential*`, …), binaries, lockfiles, files over 512 KB and anything git ignores are never read.
|
|
120
|
+
|
|
121
|
+
A project is identified by its git common directory, so all worktrees of a repository share notes,
|
|
122
|
+
messages and claims, while each worktree keeps its own file index.
|
|
123
|
+
|
|
124
|
+
## Kensaku integration (optional)
|
|
125
|
+
|
|
126
|
+
[Kensaku](https://github.com/yjid/kensaku) is a local, read-only desktop search engine that indexes
|
|
127
|
+
file names, the full text of PDF/DOCX/EPUB, OCR'd screenshots and a semantic tier. memdex covers a
|
|
128
|
+
project's source; Kensaku covers the rest of the machine. If `kensaku-cli` is on PATH (or
|
|
129
|
+
`MEMDEX_KENSAKU` points to it), `search` appends Kensaku's content hits, scoped to the project unless
|
|
130
|
+
`everywhere` is set. `MEMDEX_KENSAKU=off` disables it.
|
|
131
|
+
|
|
132
|
+
## Semantic search (optional)
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
ollama pull bge-m3
|
|
136
|
+
export MEMDEX_EMBED=ollama:bge-m3 # MEMDEX_OLLAMA_URL defaults to http://localhost:11434
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Notes and summaries get embedded when saved; search merges keyword and vector rankings.
|
|
140
|
+
If Ollama is unreachable, memdex silently falls back to keyword search.
|
|
141
|
+
|
|
142
|
+
## Development
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
python -m unittest discover tests
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# memdex
|
|
2
|
+
|
|
3
|
+
**Shared, token-saving memory for AI coding agents.** Zero dependencies, one SQLite file, any MCP client.
|
|
4
|
+
|
|
5
|
+
[한국어 README](README.ko.md)
|
|
6
|
+
|
|
7
|
+
Every new agent session starts from nothing: it re-reads the same files, re-runs the same searches
|
|
8
|
+
and re-discovers the same pitfalls. When several agents (Claude Code, Codex, Cursor, DeepSeek, …)
|
|
9
|
+
work on one machine, they also can't see what the others learned or are doing right now.
|
|
10
|
+
|
|
11
|
+
memdex fixes that with a small MCP server:
|
|
12
|
+
|
|
13
|
+
| Feature | What it does | Borrowed idea |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| **Content-hash summary cache** | An agent reads a 3,000-token file once and saves a 100-token summary. The summary is keyed to the file's sha256, so it stays valid until the content changes, then is flagged stale automatically. Identical content in other worktrees or branches shares it. | content-addressed storage |
|
|
16
|
+
| **Incremental index** | Only files whose size or mtime moved are re-read. Re-indexing an 800-file repo takes ~0.2 s. | [Everything](https://www.voidtools.com/)'s USN-journal approach |
|
|
17
|
+
| **Outline + symbol map** | Free outlines (docstring + classes/functions with line numbers) for Python, JS/TS, Vue/Svelte, Go (receiver methods as `Type.Method`), Rust, Java, Kotlin, Scala, C#, Swift, Dart, C/C++, Ruby, PHP, Lua, GDScript, shell, PowerShell, Markdown, Elixir, Clojure, Zig, Julia, R, Haskell, Groovy, Protobuf, GraphQL, SQL. `where_is` replaces repeated grepping. | Serena / repo maps, without tree-sitter |
|
|
18
|
+
| **Notes that know when they rot** | Decisions, pitfalls, todos and handoffs, linked to the files they're about. When those files change, the note is marked "⚠ changed since". Notes are append-only (`supersedes`), safe for concurrent agents. | Basic Memory, mem0 |
|
|
19
|
+
| **Agent coordination** | `post`/`inbox` messaging and `claim`s ("I'm editing src/store, 2h") with conflict warnings. All worktrees of a repo share one project memory. | — |
|
|
20
|
+
| **Hybrid search** | SQLite FTS5 trigram (works for Korean/Japanese/Chinese, with a fallback for 2-character terms) + optional local embeddings via Ollama, merged by reciprocal rank fusion. | claude-context |
|
|
21
|
+
| **Bounded reads** | `read_snippet` returns ~256 tokens around a match or a line instead of the whole file. Pair it with `where_is`. | [Kensaku](https://github.com/yjid/kensaku)'s `read_snippet` |
|
|
22
|
+
| **Whole-machine content search** | With Kensaku installed, `search` also returns full-text, OCR and semantic hits from PDFs, Office docs and screenshots. | Kensaku |
|
|
23
|
+
|
|
24
|
+
Measured on a real 800-file TypeScript repo: a 144 KB source file → 1.6 KB `file_summary` response (outline only,
|
|
25
|
+
before any agent summary was saved).
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pipx install memdex-mcp # or: pip install memdex-mcp
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The PyPI name is `memdex-mcp` (plain `memdex` was taken); the command, package and MCP server are all `memdex`.
|
|
34
|
+
From source: `pipx install git+https://github.com/ChoYugyeong/memdex` or `pip install -e .` in a checkout.
|
|
35
|
+
|
|
36
|
+
Python 3.9+. No other dependencies — the MCP stdio protocol is implemented directly, so it won't break across MCP SDK versions.
|
|
37
|
+
|
|
38
|
+
## Connect your agents
|
|
39
|
+
|
|
40
|
+
`memdex config --agent <name>` prints these snippets.
|
|
41
|
+
|
|
42
|
+
**Claude Code**
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
claude mcp add memdex --scope user -e MEMDEX_AGENT=claude -- memdex serve
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Optional: inject the brief at every session start (`~/.claude/settings.json`):
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{ "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": "memdex hook session-start --agent claude" } ] } ] } }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Any other MCP client** (Claude Desktop, Cursor, Codex, Windsurf, DeepSeek harness, …)
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{ "mcpServers": { "memdex": { "command": "memdex", "args": ["serve"], "env": { "MEMDEX_AGENT": "codex" } } } }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Give each agent its own `MEMDEX_AGENT` name. Its full id becomes `name@checkout`, e.g. `claude@myrepo-wt`.
|
|
61
|
+
|
|
62
|
+
## Tools
|
|
63
|
+
|
|
64
|
+
| Tool | Use |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `project_brief` | Start of session: last handoff, pitfalls, decisions, todos, unread messages, other agents' claims. Refreshes the index. |
|
|
67
|
+
| `file_summary` | Before reading a file: cached summary (if the content is unchanged) + outline. |
|
|
68
|
+
| `file_summary_save` | After reading a file: store a 2–6 line summary for everyone. |
|
|
69
|
+
| `where_is` | Symbol definition lookup. |
|
|
70
|
+
| `read_snippet` | A bounded slice of a file around `query` or `line` (default ~256 tokens). |
|
|
71
|
+
| `search` | Notes, handoffs and file summaries; keyword + semantic. Plus Kensaku content hits when installed (`everywhere: true` for the whole machine). |
|
|
72
|
+
| `remember` / `note` | Save / read / archive notes (`note`, `decision`, `pitfall`, `todo`, `handoff`). |
|
|
73
|
+
| `handoff` | End of session: done / next / decisions / pitfalls. Releases your claims. |
|
|
74
|
+
| `post` / `inbox` | Messages between agents (broadcast or `to: "deepseek"`). |
|
|
75
|
+
| `claim` | Announce what you're editing; warns about overlaps. |
|
|
76
|
+
|
|
77
|
+
The server sends usage instructions in its MCP `initialize` response, so agents know the workflow without extra prompting.
|
|
78
|
+
The 12 tool schemas cost about 1,000 tokens per request.
|
|
79
|
+
|
|
80
|
+
## CLI
|
|
81
|
+
|
|
82
|
+
Everything the MCP tools do is also a command, for shell-capable agents and for humans:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
memdex brief --agent codex # start of session: handoff, notes, inbox, claims
|
|
86
|
+
memdex file src/store.py # cached summary + outline
|
|
87
|
+
memdex file src/store.py --summary "…" # save a summary
|
|
88
|
+
memdex snippet src/store.py --query "def index"
|
|
89
|
+
memdex search "locked"
|
|
90
|
+
memdex remember pitfall "WAL mode needed" --body "…" --files src/store.py
|
|
91
|
+
memdex claim src/store.py --intent "refactor"
|
|
92
|
+
memdex post "store.py is mine for an hour" --to claude
|
|
93
|
+
memdex inbox
|
|
94
|
+
memdex handoff "Done X" --next "Y" --pitfalls "Z"
|
|
95
|
+
memdex index · memdex export --out notes/ · memdex stats
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Where data lives
|
|
99
|
+
|
|
100
|
+
Everything is in `~/.memdex/memdex.db` (override with `MEMDEX_HOME`). Nothing is written into your
|
|
101
|
+
repositories, so memory never ends up in a commit. Files that look like secrets (`.env*`, `*.pem`, `*.key`,
|
|
102
|
+
`*secret*`, `*credential*`, …), binaries, lockfiles, files over 512 KB and anything git ignores are never read.
|
|
103
|
+
|
|
104
|
+
A project is identified by its git common directory, so all worktrees of a repository share notes,
|
|
105
|
+
messages and claims, while each worktree keeps its own file index.
|
|
106
|
+
|
|
107
|
+
## Kensaku integration (optional)
|
|
108
|
+
|
|
109
|
+
[Kensaku](https://github.com/yjid/kensaku) is a local, read-only desktop search engine that indexes
|
|
110
|
+
file names, the full text of PDF/DOCX/EPUB, OCR'd screenshots and a semantic tier. memdex covers a
|
|
111
|
+
project's source; Kensaku covers the rest of the machine. If `kensaku-cli` is on PATH (or
|
|
112
|
+
`MEMDEX_KENSAKU` points to it), `search` appends Kensaku's content hits, scoped to the project unless
|
|
113
|
+
`everywhere` is set. `MEMDEX_KENSAKU=off` disables it.
|
|
114
|
+
|
|
115
|
+
## Semantic search (optional)
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
ollama pull bge-m3
|
|
119
|
+
export MEMDEX_EMBED=ollama:bge-m3 # MEMDEX_OLLAMA_URL defaults to http://localhost:11434
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Notes and summaries get embedded when saved; search merges keyword and vector rankings.
|
|
123
|
+
If Ollama is unreachable, memdex silently falls back to keyword search.
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
python -m unittest discover tests
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Command line: run the MCP server, index, search, hooks, export."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
from . import project as projects
|
|
15
|
+
from .store import Store, default_path
|
|
16
|
+
from .tools import Session, render_brief
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _session(args) -> Session:
|
|
20
|
+
s = Session(Store(args.db), agent_name=getattr(args, "agent", "") or "")
|
|
21
|
+
if getattr(args, "path", None):
|
|
22
|
+
s.cwd = args.path
|
|
23
|
+
return s
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def cmd_serve(args) -> int:
|
|
27
|
+
from .mcp import serve
|
|
28
|
+
serve(args.db, args.path)
|
|
29
|
+
return 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cmd_index(args) -> int:
|
|
33
|
+
s = _session(args)
|
|
34
|
+
t = time.time()
|
|
35
|
+
st = s.store.index(s.project)
|
|
36
|
+
print(f"{s.project.name}: {st['files']} files, {st['changed']} changed, {st['removed']} removed "
|
|
37
|
+
f"({time.time() - t:.2f}s) → {s.store.path}")
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cmd_brief(args) -> int:
|
|
42
|
+
s = _session(args)
|
|
43
|
+
print(render_brief(s.store, s.project, s.agent))
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def cmd_search(args) -> int:
|
|
48
|
+
print(_session(args).search(" ".join(args.query), limit=args.limit))
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def cmd_file(args) -> int:
|
|
53
|
+
s = _session(args)
|
|
54
|
+
print(s.file_summary_save(args.file, args.summary) if args.summary else s.file_summary(args.file))
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def cmd_snippet(args) -> int:
|
|
59
|
+
print(_session(args).read_snippet(args.file, args.query or "", args.line or 0, args.max_tokens))
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def cmd_remember(args) -> int:
|
|
64
|
+
print(_session(args).remember(args.kind, args.title, args.body or "", args.files, args.supersedes))
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def cmd_handoff(args) -> int:
|
|
69
|
+
print(_session(args).handoff(args.done, args.next or "", args.decisions or "", args.pitfalls or "", args.files))
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cmd_post(args) -> int:
|
|
74
|
+
print(_session(args).post(args.message, args.to or ""))
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def cmd_inbox(args) -> int:
|
|
79
|
+
print(_session(args).inbox(args.peek))
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def cmd_claim(args) -> int:
|
|
84
|
+
print(_session(args).claim(args.paths or None, args.intent or "", args.minutes, args.release))
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cmd_hook(args) -> int:
|
|
89
|
+
"""Hook entry points. session-start prints the brief, which Claude Code adds to context."""
|
|
90
|
+
payload = {}
|
|
91
|
+
if not sys.stdin.isatty():
|
|
92
|
+
try:
|
|
93
|
+
payload = json.loads(sys.stdin.read() or "{}")
|
|
94
|
+
except (json.JSONDecodeError, OSError):
|
|
95
|
+
payload = {}
|
|
96
|
+
args.path = args.path or payload.get("cwd") or os.getcwd()
|
|
97
|
+
s = _session(args)
|
|
98
|
+
if args.event == "session-start":
|
|
99
|
+
print(render_brief(s.store, s.project, s.agent))
|
|
100
|
+
print("(memdex MCP tools: file_summary before reading files, remember/handoff to save.)")
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def cmd_export(args) -> int:
|
|
105
|
+
"""Write the project's notes as Markdown files (one per note) for reading or backup."""
|
|
106
|
+
s = _session(args)
|
|
107
|
+
out = args.out or os.path.join(os.getcwd(), "memdex-export")
|
|
108
|
+
os.makedirs(out, exist_ok=True)
|
|
109
|
+
rows = s.store.db.execute(
|
|
110
|
+
"SELECT * FROM notes WHERE project=? AND archived=0 ORDER BY id", (s.project.key,)).fetchall()
|
|
111
|
+
for n in rows:
|
|
112
|
+
slug = re.sub(r"[^\w가-힣ぁ-んァ-ン一-龥-]+", "-", n["title"]).strip("-")[:50] or "note"
|
|
113
|
+
with open(os.path.join(out, f"{n['id']:04d}-{n['kind']}-{slug}.md"), "w", encoding="utf-8") as f:
|
|
114
|
+
f.write(f"---\nid: {n['id']}\nkind: {n['kind']}\nauthor: {n['author']}\n"
|
|
115
|
+
f"created: {time.strftime('%Y-%m-%d %H:%M', time.localtime(n['created_at']))}\n"
|
|
116
|
+
f"superseded_by: {n['superseded_by'] or ''}\nrefs: {n['refs']}\n---\n\n"
|
|
117
|
+
f"# {n['title']}\n\n{n['body']}\n")
|
|
118
|
+
print(f"exported {len(rows)} notes → {out}")
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def cmd_config(args) -> int:
|
|
123
|
+
agent = args.agent or "claude"
|
|
124
|
+
exe = "memdex"
|
|
125
|
+
print(f"""# Claude Code (user scope, all projects)
|
|
126
|
+
claude mcp add memdex --scope user -e MEMDEX_AGENT={agent} -- {exe} serve
|
|
127
|
+
|
|
128
|
+
# Any MCP client (Claude Desktop, Cursor, Codex, DeepSeek harness, ...): mcpServers entry
|
|
129
|
+
{json.dumps({"mcpServers": {"memdex": {"command": exe, "args": ["serve"], "env": {"MEMDEX_AGENT": agent}}}}, indent=2)}
|
|
130
|
+
|
|
131
|
+
# Claude Code SessionStart hook (~/.claude/settings.json): injects the brief automatically
|
|
132
|
+
{json.dumps({"hooks": {"SessionStart": [{"hooks": [{"type": "command", "command": f"{exe} hook session-start --agent {agent}"}]}]}}, indent=2)}
|
|
133
|
+
""")
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def cmd_stats(args) -> int:
|
|
138
|
+
st = Store(args.db)
|
|
139
|
+
q = lambda sql: st.db.execute(sql).fetchone()[0]
|
|
140
|
+
size = os.path.getsize(st.path) if os.path.exists(st.path) else 0
|
|
141
|
+
print(f"db: {st.path} ({size / 1024:.0f} KB)\nfiles: {q('SELECT count(*) FROM files')} "
|
|
142
|
+
f"summaries: {q('SELECT count(*) FROM summaries')} notes: {q('SELECT count(*) FROM notes')} "
|
|
143
|
+
f"messages: {q('SELECT count(*) FROM messages')} trigram: {st.trigram}")
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
148
|
+
for stream in (sys.stdout, sys.stderr):
|
|
149
|
+
try:
|
|
150
|
+
stream.reconfigure(encoding="utf-8")
|
|
151
|
+
except (AttributeError, ValueError):
|
|
152
|
+
pass
|
|
153
|
+
p = argparse.ArgumentParser(prog="memdex", description="Shared, token-saving memory for AI coding agents.")
|
|
154
|
+
p.add_argument("--version", action="version", version=f"memdex {__version__}")
|
|
155
|
+
p.add_argument("--db", default=None, help=f"database path (default {default_path()}, or $MEMDEX_HOME)")
|
|
156
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
157
|
+
|
|
158
|
+
def add(name, fn, help_):
|
|
159
|
+
sp = sub.add_parser(name, help=help_)
|
|
160
|
+
sp.set_defaults(fn=fn)
|
|
161
|
+
sp.add_argument("--path", default=None, help="project path (default: current directory)")
|
|
162
|
+
sp.add_argument("--agent", default="", help="agent name, e.g. claude, deepseek, codex")
|
|
163
|
+
return sp
|
|
164
|
+
|
|
165
|
+
add("serve", cmd_serve, "run the MCP server on stdio")
|
|
166
|
+
add("index", cmd_index, "update the file index (changed files only)")
|
|
167
|
+
add("brief", cmd_brief, "print the project brief")
|
|
168
|
+
sp = add("search", cmd_search, "search notes and file summaries")
|
|
169
|
+
sp.add_argument("query", nargs="+")
|
|
170
|
+
sp.add_argument("--limit", type=int, default=8)
|
|
171
|
+
sp = add("file", cmd_file, "show a file's cached summary and outline, or save a summary")
|
|
172
|
+
sp.add_argument("file")
|
|
173
|
+
sp.add_argument("--summary", default=None, help="save this 2-6 line summary for the file's current content")
|
|
174
|
+
sp = add("snippet", cmd_snippet, "read a bounded part of a file around a query or line")
|
|
175
|
+
sp.add_argument("file")
|
|
176
|
+
sp.add_argument("--query", default=None)
|
|
177
|
+
sp.add_argument("--line", type=int, default=0)
|
|
178
|
+
sp.add_argument("--max-tokens", type=int, default=256)
|
|
179
|
+
sp = add("remember", cmd_remember, "save a note, decision, pitfall or todo")
|
|
180
|
+
sp.add_argument("kind", choices=["note", "decision", "pitfall", "todo"])
|
|
181
|
+
sp.add_argument("title")
|
|
182
|
+
sp.add_argument("--body", default="")
|
|
183
|
+
sp.add_argument("--files", nargs="*", default=None, help="files this note is about (flagged when they change)")
|
|
184
|
+
sp.add_argument("--supersedes", type=int, default=None, help="id of the note this one replaces")
|
|
185
|
+
sp = add("handoff", cmd_handoff, "end-of-session handoff for the next session or agent")
|
|
186
|
+
sp.add_argument("done")
|
|
187
|
+
sp.add_argument("--next", default="")
|
|
188
|
+
sp.add_argument("--decisions", default="")
|
|
189
|
+
sp.add_argument("--pitfalls", default="")
|
|
190
|
+
sp.add_argument("--files", nargs="*", default=None)
|
|
191
|
+
sp = add("post", cmd_post, "message other agents on this project")
|
|
192
|
+
sp.add_argument("message")
|
|
193
|
+
sp.add_argument("--to", default="", help="agent name such as claude or deepseek; default everyone")
|
|
194
|
+
sp = add("inbox", cmd_inbox, "unread messages from other agents")
|
|
195
|
+
sp.add_argument("--peek", action="store_true", help="do not mark as read")
|
|
196
|
+
sp = add("claim", cmd_claim, "announce files you are editing; no paths lists active claims")
|
|
197
|
+
sp.add_argument("paths", nargs="*")
|
|
198
|
+
sp.add_argument("--intent", default="")
|
|
199
|
+
sp.add_argument("--minutes", type=int, default=120)
|
|
200
|
+
sp.add_argument("--release", action="store_true")
|
|
201
|
+
sp = add("hook", cmd_hook, "hook entry point for agent harnesses")
|
|
202
|
+
sp.add_argument("event", choices=["session-start"])
|
|
203
|
+
sp = add("export", cmd_export, "export notes as Markdown")
|
|
204
|
+
sp.add_argument("--out", default=None)
|
|
205
|
+
add("config", cmd_config, "print setup snippets for MCP clients and hooks")
|
|
206
|
+
add("stats", cmd_stats, "database statistics")
|
|
207
|
+
|
|
208
|
+
args = p.parse_args(argv)
|
|
209
|
+
return args.fn(args)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Optional semantic search through a local embedding model.
|
|
2
|
+
|
|
3
|
+
Off by default. Set MEMDEX_EMBED=ollama:<model> (for example ollama:bge-m3,
|
|
4
|
+
which handles Korean, Japanese and English) to turn it on. Vectors are
|
|
5
|
+
normalised float32 blobs searched by brute force, which stays fast for the
|
|
6
|
+
few thousand notes and summaries a project accumulates, with no extra library.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
import os
|
|
14
|
+
import urllib.request
|
|
15
|
+
from array import array
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
_disabled_after_error = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def config() -> Optional[tuple[str, str]]:
|
|
22
|
+
spec = os.environ.get("MEMDEX_EMBED", "").strip()
|
|
23
|
+
if not spec or _disabled_after_error:
|
|
24
|
+
return None
|
|
25
|
+
provider, _, model = spec.partition(":")
|
|
26
|
+
if provider != "ollama" or not model:
|
|
27
|
+
return None
|
|
28
|
+
return provider, model
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def embed(text: str) -> Optional[bytes]:
|
|
32
|
+
global _disabled_after_error
|
|
33
|
+
cfg = config()
|
|
34
|
+
if not cfg or not text.strip():
|
|
35
|
+
return None
|
|
36
|
+
url = os.environ.get("MEMDEX_OLLAMA_URL", "http://localhost:11434").rstrip("/") + "/api/embed"
|
|
37
|
+
req = urllib.request.Request(
|
|
38
|
+
url, data=json.dumps({"model": cfg[1], "input": text[:8000]}).encode(),
|
|
39
|
+
headers={"Content-Type": "application/json"},
|
|
40
|
+
)
|
|
41
|
+
try:
|
|
42
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
43
|
+
vec = json.loads(r.read())["embeddings"][0]
|
|
44
|
+
except Exception:
|
|
45
|
+
_disabled_after_error = True # don't stall every call on a dead server
|
|
46
|
+
return None
|
|
47
|
+
n = math.sqrt(sum(x * x for x in vec)) or 1.0
|
|
48
|
+
return array("f", (x / n for x in vec)).tobytes()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def similarity(a: bytes, b: bytes) -> float:
|
|
52
|
+
va, vb = array("f"), array("f")
|
|
53
|
+
va.frombytes(a)
|
|
54
|
+
vb.frombytes(b)
|
|
55
|
+
if len(va) != len(vb):
|
|
56
|
+
return -1.0
|
|
57
|
+
return sum(x * y for x, y in zip(va, vb))
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Optional bridge to Kensaku (https://github.com/yjid/kensaku).
|
|
2
|
+
|
|
3
|
+
Kensaku is a local desktop search engine: file names, full text of PDF/DOCX/
|
|
4
|
+
EPUB, OCR'd screenshots and a static-vector semantic tier, read-only, nothing
|
|
5
|
+
leaves the machine. memdex indexes a project's source; Kensaku indexes the whole
|
|
6
|
+
computer. When `kensaku-cli` is on PATH (or MEMDEX_KENSAKU names it), memdex
|
|
7
|
+
search also returns Kensaku's content hits, so an agent can find "the PDF about
|
|
8
|
+
the auth flow" or "the screenshot with the CUDA error" without a second tool.
|
|
9
|
+
|
|
10
|
+
Set MEMDEX_KENSAKU=off to disable.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
from typing import Any, Optional
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def command() -> Optional[str]:
|
|
23
|
+
spec = os.environ.get("MEMDEX_KENSAKU", "").strip()
|
|
24
|
+
if spec.lower() in ("off", "0", "false"):
|
|
25
|
+
return None
|
|
26
|
+
return spec or shutil.which("kensaku-cli")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _run(args: list[str], timeout: float) -> Optional[str]:
|
|
30
|
+
try:
|
|
31
|
+
r = subprocess.run(args, capture_output=True, timeout=timeout)
|
|
32
|
+
except (OSError, subprocess.SubprocessError):
|
|
33
|
+
return None
|
|
34
|
+
if r.returncode != 0: # 3 = no index yet; 2 = usage
|
|
35
|
+
return None
|
|
36
|
+
return r.stdout.decode("utf-8", "replace")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def search(query: str, root: Optional[str] = None, limit: int = 5, timeout: float = 15) -> list[dict[str, Any]]:
|
|
40
|
+
"""Ranked Kensaku hits as {path, snippet, match}. Empty when unavailable."""
|
|
41
|
+
cmd = command()
|
|
42
|
+
if not cmd or not query.strip():
|
|
43
|
+
return []
|
|
44
|
+
args = [cmd, "search", query, "--json", "--limit", str(max(1, min(limit, 50)))]
|
|
45
|
+
if root:
|
|
46
|
+
args += ["--path", root]
|
|
47
|
+
out = _run(args, timeout)
|
|
48
|
+
if not out:
|
|
49
|
+
return []
|
|
50
|
+
try:
|
|
51
|
+
data = json.loads(out)
|
|
52
|
+
except json.JSONDecodeError:
|
|
53
|
+
return []
|
|
54
|
+
hits = []
|
|
55
|
+
for it in (data.get("results") or [])[:limit]:
|
|
56
|
+
if not isinstance(it, dict) or not it.get("path"):
|
|
57
|
+
continue
|
|
58
|
+
snippet = " ".join(str(it.get("snippet") or "").split())[:240]
|
|
59
|
+
hits.append({"path": str(it["path"]), "snippet": snippet, "match": str(it.get("match_type") or "")})
|
|
60
|
+
return hits
|