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.
@@ -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,3 @@
1
+ """memdex: shared, token-saving memory for AI coding agents."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -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