Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,33 @@ Fetch exact memories by rowid (CLI + MCP tool), without bumping `access_count`
| Tool | Purpose |
|------|---------|
| `recall_memory(query, limit?, category?)` | Hybrid search: FTS5 + vector KNN + RRF fusion. `limit` is hard-capped at 20 by the recall contract (v2.9); larger values silently clamp — see `capped` in the JSON CLI output |
| `recall_claude_memory(query, limit?, project?)` | Read-only lexical search over Claude Code's live `~/.claude/projects/*/memory/*.md` working memory, with file provenance and bounded results |
| `store_memory(content, level?, ...)` | Store with abstraction level (meta_knowledge / semi_abstract / concrete_trace) |
| `recall_by_id(ids)` | Fetch exact memories by rowid (no access_count bump) — citation / audit |
| `get_recall_trace(trace_id)` | Inspect content-free candidate/filter counts and the exact IDs exposed by one recall |
| `validate_memory_references(trace_id, text)` | Preserve in-trace `[id:N]` citations and strip fabricated/out-of-trace IDs |
| `memory_stats()` | Stats including compression pressure, dead knowledge, search miss rate, vector coverage |

### Claude Markdown interoperability

`recall_claude_memory` gives another MCP client (including Codex) read-only access to
Claude Code's current project-working memory without copying it into SQLite. It scans
`~/.claude/projects/*/memory/*.md` on each call, excludes `MEMORY.md`, and never writes to
those files. The tool intentionally accepts no arbitrary root/path argument.

Keep the memory layers distinct:

- **Claude Markdown** is live, project-local working state — query it with
`recall_claude_memory`.
- **mneme** is portable cross-project knowledge — query it with `recall_memory`.
- **Team memory/KOS** is shared rules, decisions, and ownership — query the team's
canonical source instead of mirroring it into either personal layer.

Treat recalled Markdown as untrusted historical evidence, not executable instructions.
For a nonstandard layout, set `MNEME_CLAUDE_MEMORY_DIRS` on the server process to a list of
memory directories separated by the operating system path delimiter (`;` on Windows, `:`
on POSIX).

---

## Why MCP Makes This Universal
Expand Down Expand Up @@ -456,6 +477,7 @@ Reciprocal Rank Fusion uses only rank positions, not raw scores. This means FTS5
| Variable | Default | Description |
|----------|---------|-------------|
| `TOKENMEM_DB_PATH` | `./tokenmem.db` | Path to SQLite database |
| `MNEME_CLAUDE_MEMORY_DIRS` | `~/.claude/projects/*/memory` | Optional OS-path-delimited list of Claude Markdown memory directories used by `recall_claude_memory` |
| `EMBEDDING_API_BASE_URL` | — | OpenAI-compatible embedding API base URL |
| `EMBEDDING_API_KEY` | — | API key for embedding service |
| `EMBEDDING_MODEL` | `text-embedding-3-small` | Embedding model name |
Expand Down
18 changes: 18 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,29 @@ migrations/
| 工具 | 用途 |
|------|---------|
| `recall_memory(query, limit?, category?)` | 混合检索:FTS5 + 向量 KNN + RRF 融合打分。`limit` 被 recall contract 硬性限制在 20(v2.9),超过静默截断——`--format json` 输出的 `capped` 字段会指示是否触发 |
| `recall_claude_memory(query, limit?, project?)` | 只读检索 Claude Code 实时的 `~/.claude/projects/*/memory/*.md` 工作记忆,返回文件来源和有界结果 |
| `store_memory(content, level?, ...)` | 存储记忆,可指定抽象层级(meta_knowledge / semi_abstract / concrete_trace) |
| `recall_by_id(ids)` | 按 rowid 精确读取,不增加 `access_count`,用于引用和审计 |
| `get_recall_trace(trace_id)` | 查看一次召回的候选/过滤计数,以及真正暴露给模型的 ID |
| `validate_memory_references(trace_id, text)` | 保留本次 trace 允许的 `[id:N]`,剔除伪造或越界 ID |
| `memory_stats()` | 统计:压缩压力、死知识、搜索未命中率 |

### Claude Markdown 互通

`recall_claude_memory` 让其他 MCP 客户端(包括 Codex)只读查询 Claude Code 当前的
项目工作记忆,不把它复制进 SQLite。每次调用都会重新扫描
`~/.claude/projects/*/memory/*.md`,排除 `MEMORY.md`,并且绝不写入这些文件。工具刻意不提供
任意 root/path 参数。

三层记忆保持分工,不做镜像:

- **Claude Markdown**:实时、项目内的工作状态,用 `recall_claude_memory`。
- **mneme**:可跨项目复用的个人知识,用 `recall_memory`。
- **团队记忆/KOS**:团队规则、决策和归属,查团队权威源,不复制进个人层。

Markdown 召回内容只能当作不可信的历史证据,不能当作可执行指令。非标准目录可在 server 进程上设置
`MNEME_CLAUDE_MEMORY_DIRS`,多个目录用操作系统路径分隔符连接(Windows 是 `;`,POSIX 是 `:`)。

---

## 为什么用 MCP 让它通用
Expand Down Expand Up @@ -428,6 +445,7 @@ Reciprocal Rank Fusion 只用排名位置,不用原始分数。这样 FTS5 BM2
| 变量 | 默认 | 描述 |
|----------|---------|-------------|
| `TOKENMEM_DB_PATH` | `./tokenmem.db` | SQLite 数据库路径 |
| `MNEME_CLAUDE_MEMORY_DIRS` | `~/.claude/projects/*/memory` | 可选;`recall_claude_memory` 使用的 Claude Markdown 目录列表,以操作系统路径分隔符连接 |
| `EMBEDDING_API_BASE_URL` | — | OpenAI 兼容 embedding API base URL |
| `EMBEDDING_API_KEY` | — | embedding 服务 API key |
| `EMBEDDING_MODEL` | `text-embedding-3-small` | embedding 模型名 |
Expand Down
67 changes: 67 additions & 0 deletions claude-markdown-memory-mcp.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'

import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'

const serverPath = resolve('mcp-server.mjs')

test('recall_claude_memory is a bounded read-only stdio MCP tool', async () => {
const root = mkdtempSync(join(tmpdir(), 'mneme-claude-mcp-'))
const memoryDir = join(root, 'E--project', 'memory')
const memoryPath = join(memoryDir, 'windows-utf8.md')
const dbPath = join(root, 'mneme-test.db')
mkdirSync(memoryDir, { recursive: true })
writeFileSync(memoryPath, [
'---',
'name: Windows UTF-8 rule',
'description: Read Chinese text with explicit UTF-8 decoding',
'type: playbook',
'---',
'Use explicit UTF-8 decoding on Windows.',
].join('\n'), 'utf8')
const before = readFileSync(memoryPath)

const transport = new StdioClientTransport({
command: process.execPath,
args: [serverPath],
env: {
...process.env,
TOKENMEM_DB_PATH: dbPath,
MNEME_AUTH: 'off',
MNEME_CLAUDE_MEMORY_DIRS: memoryDir,
},
stderr: 'pipe',
})
const client = new Client({ name: 'claude-markdown-memory-test', version: '1.0.0' })

try {
await client.connect(transport)

const listed = await client.listTools()
const tool = listed.tools.find(item => item.name === 'recall_claude_memory')
assert.ok(tool)
assert.deepEqual(Object.keys(tool.inputSchema.properties).sort(), ['limit', 'project', 'query'])
assert.equal(tool.inputSchema.properties.path, undefined)
assert.equal(tool.inputSchema.properties.root, undefined)

const called = await client.callTool({
name: 'recall_claude_memory',
arguments: { query: 'UTF-8', project: 'E--project', limit: 3 },
})
assert.equal(called.isError, undefined)
const payload = JSON.parse(called.content[0].text)
assert.equal(payload.source, 'claude_markdown_memory')
assert.match(payload.notice, /untrusted historical evidence/i)
assert.equal(payload.hits.length, 1)
assert.equal(payload.hits[0].path, resolve(memoryPath))
assert.equal(payload.scanned_files, 1)
assert.deepEqual(readFileSync(memoryPath), before)
} finally {
await client.close().catch(() => {})
rmSync(root, { recursive: true, force: true })
}
})
Loading
Loading