@morningljn/mnemo 0.1.2 → 0.1.4
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.
- package/README.md +44 -15
- package/README_zh.md +1 -1
- package/dist/cache.d.ts +23 -0
- package/dist/cache.js +44 -0
- package/dist/cache.js.map +1 -0
- package/dist/init.js +16 -8
- package/dist/init.js.map +1 -1
- package/dist/metrics.d.ts +31 -0
- package/dist/metrics.js +57 -0
- package/dist/metrics.js.map +1 -0
- package/dist/refine.d.ts +14 -0
- package/dist/refine.js +115 -0
- package/dist/refine.js.map +1 -0
- package/dist/resources.d.ts +27 -0
- package/dist/resources.js +56 -0
- package/dist/resources.js.map +1 -0
- package/dist/retriever.d.ts +14 -2
- package/dist/retriever.js +126 -36
- package/dist/retriever.js.map +1 -1
- package/dist/server.js +40 -16
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/docs/superpowers/plans/2026-05-15-mnemo-mcp.md +1154 -0
- package/docs/superpowers/plans/2026-05-16-mnemo-query-cache.md +613 -0
- package/docs/superpowers/plans/2026-05-16-retrieval-and-injection-optimization.md +770 -0
- package/openspec/changes/archive/2026-05-15-mnemo-mcp/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-05-15-mnemo-mcp/design.md +83 -0
- package/openspec/changes/archive/2026-05-15-mnemo-mcp/proposal.md +32 -0
- package/openspec/changes/archive/2026-05-15-mnemo-mcp/specs/fact-retrieval/spec.md +75 -0
- package/openspec/changes/archive/2026-05-15-mnemo-mcp/specs/fact-store/spec.md +83 -0
- package/openspec/changes/archive/2026-05-15-mnemo-mcp/specs/mcp-server/spec.md +34 -0
- package/openspec/changes/archive/2026-05-15-mnemo-mcp/specs/security/spec.md +37 -0
- package/openspec/changes/archive/2026-05-15-mnemo-mcp/tasks.md +44 -0
- package/openspec/changes/archive/2026-05-16-mnemo-query-cache/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-05-16-mnemo-query-cache/design.md +96 -0
- package/openspec/changes/archive/2026-05-16-mnemo-query-cache/proposal.md +29 -0
- package/openspec/changes/archive/2026-05-16-mnemo-query-cache/specs/batch-operations/spec.md +42 -0
- package/openspec/changes/archive/2026-05-16-mnemo-query-cache/specs/perf-metrics/spec.md +55 -0
- package/openspec/changes/archive/2026-05-16-mnemo-query-cache/specs/query-cache/spec.md +65 -0
- package/openspec/changes/archive/2026-05-16-mnemo-query-cache/tasks.md +45 -0
- package/openspec/changes/retrieval-and-injection-optimization/.openspec.yaml +2 -0
- package/openspec/changes/retrieval-and-injection-optimization/design.md +117 -0
- package/openspec/changes/retrieval-and-injection-optimization/proposal.md +30 -0
- package/openspec/changes/retrieval-and-injection-optimization/specs/adaptive-scoring/spec.md +43 -0
- package/openspec/changes/retrieval-and-injection-optimization/specs/injection-protocol/spec.md +48 -0
- package/openspec/changes/retrieval-and-injection-optimization/specs/mcp-resources/spec.md +39 -0
- package/openspec/changes/retrieval-and-injection-optimization/specs/query-refinement/spec.md +39 -0
- package/openspec/changes/retrieval-and-injection-optimization/tasks.md +33 -0
- package/openspec/config.yaml +20 -0
- package/package.json +1 -1
- package/src/cache.ts +65 -0
- package/src/init.ts +17 -9
- package/src/metrics.ts +81 -0
- package/src/refine.ts +127 -0
- package/src/resources.ts +78 -0
- package/src/retriever.ts +141 -34
- package/src/server.ts +42 -17
- package/src/types.ts +2 -2
- package/tests/refine.test.ts +52 -0
- package/tests/resource.test.ts +62 -0
package/src/server.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { homedir } from 'node:os'
|
|
|
7
7
|
import { z } from 'zod/v4'
|
|
8
8
|
import { MemoryStore } from './store.js'
|
|
9
9
|
import { FactRetriever } from './retriever.js'
|
|
10
|
+
import { ResourceManager } from './resources.js'
|
|
10
11
|
import { fullSecurityScan } from './security.js'
|
|
11
12
|
import type { FactStoreArgs, FactFeedbackArgs, FactCategory } from './types.js'
|
|
12
13
|
|
|
@@ -27,11 +28,11 @@ const FACT_STORE_DESCRIPTION = `结构化事实记忆系统(SQLite+FTS5 索引
|
|
|
27
28
|
|
|
28
29
|
const factStoreSchema = {
|
|
29
30
|
action: z.enum(['add', 'search', 'probe', 'related', 'reason', 'contradict', 'update', 'remove', 'list']),
|
|
30
|
-
content: z.string().optional().describe("事实内容('add'
|
|
31
|
+
content: z.union([z.string(), z.array(z.string())]).optional().describe("事实内容('add' 必需,支持批量)"),
|
|
31
32
|
query: z.string().optional().describe("搜索查询('search' 必需)"),
|
|
32
33
|
entity: z.string().optional().describe("实体名('probe'/'related' 使用)"),
|
|
33
34
|
entities: z.array(z.string()).optional().describe("实体列表('reason' 使用)"),
|
|
34
|
-
fact_id: z.number().optional().describe("事实 ID('update'/'remove'
|
|
35
|
+
fact_id: z.union([z.number(), z.array(z.number())]).optional().describe("事实 ID('update'/'remove' 使用,支持批量)"),
|
|
35
36
|
category: z.enum(['identity', 'coding_style', 'tool_pref', 'workflow', 'general']).optional(),
|
|
36
37
|
tags: z.string().optional().describe('逗号分隔标签'),
|
|
37
38
|
trust_delta: z.number().optional().describe("'update' 的信任调整值"),
|
|
@@ -64,6 +65,10 @@ store.auditContradictions()
|
|
|
64
65
|
// -- MCP Server --
|
|
65
66
|
const server = new McpServer({ name: 'mnemo-mcp', version: '0.1.0' })
|
|
66
67
|
|
|
68
|
+
// -- MCP Resources: 会话预热注入 --
|
|
69
|
+
const resourceManager = new ResourceManager(store)
|
|
70
|
+
resourceManager.registerResources(server)
|
|
71
|
+
|
|
67
72
|
server.tool(
|
|
68
73
|
'fact_store',
|
|
69
74
|
FACT_STORE_DESCRIPTION,
|
|
@@ -76,20 +81,34 @@ server.tool(
|
|
|
76
81
|
switch (a.action) {
|
|
77
82
|
case 'add': {
|
|
78
83
|
if (!a.content) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Missing required argument: content' }) }] }
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
const contents = Array.isArray(a.content) ? a.content : [a.content]
|
|
85
|
+
const results: Array<{ fact_id: number; status: string; reason?: string; category?: string; contradicted_demoted?: number; warnings?: string[] }> = []
|
|
86
|
+
|
|
87
|
+
for (const content of contents) {
|
|
88
|
+
if (!content || !content.trim()) {
|
|
89
|
+
results.push({ fact_id: -1, status: 'error', reason: 'empty content' })
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
const similar = store.findSimilarFact(content, category) ?? store.findSimilarFact(content)
|
|
93
|
+
let warnings: string[] | undefined
|
|
94
|
+
const scan = fullSecurityScan(content)
|
|
95
|
+
if (scan.warnings.length > 0 || scan.hasPii) warnings = [...scan.warnings]
|
|
96
|
+
|
|
97
|
+
if (similar) {
|
|
98
|
+
store.updateFact(similar.factId, { content, tags: a.tags, trustDelta: 0.05 })
|
|
99
|
+
const demoted = store.demoteContradictingFacts(similar.factId, content, category)
|
|
100
|
+
results.push({ fact_id: similar.factId, status: 'updated', reason: 'similar_fact_merged', ...(demoted > 0 ? { contradicted_demoted: demoted } : {}), ...(warnings ? { warnings } : {}) })
|
|
101
|
+
} else {
|
|
102
|
+
const factId = store.addFact(content, category, a.tags ?? '')
|
|
103
|
+
const demoted = store.demoteContradictingFacts(factId, content, category)
|
|
104
|
+
results.push({ fact_id: factId, status: 'added', category, ...(demoted > 0 ? { contradicted_demoted: demoted } : {}), ...(warnings ? { warnings } : {}) })
|
|
105
|
+
}
|
|
88
106
|
}
|
|
89
107
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
108
|
+
retriever.getCache().clear()
|
|
109
|
+
resourceManager.invalidate()
|
|
110
|
+
const response = Array.isArray(a.content) ? results : results[0]
|
|
111
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(response) }] }
|
|
93
112
|
}
|
|
94
113
|
|
|
95
114
|
case 'search': {
|
|
@@ -124,14 +143,20 @@ server.tool(
|
|
|
124
143
|
|
|
125
144
|
case 'update': {
|
|
126
145
|
if (!a.fact_id) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Missing required argument: fact_id' }) }] }
|
|
127
|
-
const updated = store.updateFact(a.fact_id, { content: a.content, tags: a.tags, category, trustDelta: a.trust_delta })
|
|
146
|
+
const updated = store.updateFact(a.fact_id as number, { content: a.content as string | undefined, tags: a.tags, category, trustDelta: a.trust_delta })
|
|
147
|
+
retriever.getCache().clear()
|
|
148
|
+
resourceManager.invalidate()
|
|
128
149
|
return { content: [{ type: 'text' as const, text: JSON.stringify({ updated }) }] }
|
|
129
150
|
}
|
|
130
151
|
|
|
131
152
|
case 'remove': {
|
|
132
153
|
if (!a.fact_id) return { content: [{ type: 'text' as const, text: JSON.stringify({ error: 'Missing required argument: fact_id' }) }] }
|
|
133
|
-
const
|
|
134
|
-
|
|
154
|
+
const ids = Array.isArray(a.fact_id) ? a.fact_id : [a.fact_id]
|
|
155
|
+
const results = ids.map(id => ({ fact_id: id, removed: store.removeFact(id) }))
|
|
156
|
+
retriever.getCache().clear()
|
|
157
|
+
resourceManager.invalidate()
|
|
158
|
+
const response = Array.isArray(a.fact_id) ? results : results[0]
|
|
159
|
+
return { content: [{ type: 'text' as const, text: JSON.stringify(response) }] }
|
|
135
160
|
}
|
|
136
161
|
|
|
137
162
|
case 'list': {
|
package/src/types.ts
CHANGED
|
@@ -54,11 +54,11 @@ export interface RetrieverOptions {
|
|
|
54
54
|
/** fact_store 工具调用参数 */
|
|
55
55
|
export interface FactStoreArgs {
|
|
56
56
|
action: 'add' | 'search' | 'probe' | 'related' | 'reason' | 'contradict' | 'update' | 'remove' | 'list'
|
|
57
|
-
content?: string
|
|
57
|
+
content?: string | string[]
|
|
58
58
|
query?: string
|
|
59
59
|
entity?: string
|
|
60
60
|
entities?: string[]
|
|
61
|
-
fact_id?: number
|
|
61
|
+
fact_id?: number | number[]
|
|
62
62
|
category?: string
|
|
63
63
|
tags?: string
|
|
64
64
|
trust_delta?: number
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { refineQuery } from '../src/refine.js'
|
|
3
|
+
|
|
4
|
+
describe('refineQuery', () => {
|
|
5
|
+
it('filters action words from Chinese query', () => {
|
|
6
|
+
const result = refineQuery('帮我用 TypeScript 重构 auth 模块')
|
|
7
|
+
expect(result).not.toBeNull()
|
|
8
|
+
expect(result!.query).toContain('TypeScript')
|
|
9
|
+
expect(result!.query).toContain('auth')
|
|
10
|
+
expect(result!.query).not.toContain('帮我')
|
|
11
|
+
expect(result!.query).not.toContain('重构')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('returns null for pure operation commands', () => {
|
|
15
|
+
expect(refineQuery('运行测试')).toBeNull()
|
|
16
|
+
expect(refineQuery('git status')).toBeNull()
|
|
17
|
+
expect(refineQuery('创建文件')).toBeNull()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('extracts quoted Chinese entities', () => {
|
|
21
|
+
const result = refineQuery('我喜欢「深色主题」')
|
|
22
|
+
expect(result).not.toBeNull()
|
|
23
|
+
expect(result!.entityTokens).toContain('深色主题')
|
|
24
|
+
expect(result!.query).toContain('深色主题')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('extracts book title entities', () => {
|
|
28
|
+
const result = refineQuery('读了《设计模式》这本书')
|
|
29
|
+
expect(result).not.toBeNull()
|
|
30
|
+
expect(result!.entityTokens).toContain('设计模式')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('extracts capitalized English phrases', () => {
|
|
34
|
+
const result = refineQuery('使用 Visual Studio Code 编辑器')
|
|
35
|
+
expect(result).not.toBeNull()
|
|
36
|
+
expect(result!.entityTokens).toContain('Visual Studio Code')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('returns null for empty string', () => {
|
|
40
|
+
expect(refineQuery('')).toBeNull()
|
|
41
|
+
expect(refineQuery(' ')).toBeNull()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('preserves meaningful Chinese tokens', () => {
|
|
45
|
+
const result = refineQuery('用户偏好深色主题')
|
|
46
|
+
expect(result).not.toBeNull()
|
|
47
|
+
expect(result!.query).toContain('用户')
|
|
48
|
+
expect(result!.query).toContain('偏好')
|
|
49
|
+
expect(result!.query).toContain('深色')
|
|
50
|
+
expect(result!.query).toContain('主题')
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { MemoryStore } from '../src/store.js'
|
|
3
|
+
import { ResourceManager } from '../src/resources.js'
|
|
4
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { tmpdir } from 'node:os'
|
|
7
|
+
|
|
8
|
+
let store: MemoryStore
|
|
9
|
+
let manager: ResourceManager
|
|
10
|
+
let tmpDir: string
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'mnemo-test-'))
|
|
14
|
+
store = new MemoryStore(join(tmpDir, 'test.db'))
|
|
15
|
+
manager = new ResourceManager(store)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
store.close()
|
|
20
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('ResourceManager', () => {
|
|
24
|
+
it('returns empty array for empty category', () => {
|
|
25
|
+
const facts = manager.getFacts('identity')
|
|
26
|
+
expect(facts).toEqual([])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('returns facts ordered by trust score', () => {
|
|
30
|
+
store.addFact('用户偏好深色主题', 'tool_pref')
|
|
31
|
+
store.addFact('用户喜欢 VS Code', 'tool_pref')
|
|
32
|
+
const facts = manager.getFacts('tool_pref')
|
|
33
|
+
expect(facts.length).toBe(2)
|
|
34
|
+
// listFacts returns trust_score DESC, first fact added gets default trust
|
|
35
|
+
expect(facts.length).toBeGreaterThan(0)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('caches results', () => {
|
|
39
|
+
store.addFact('测试事实', 'general')
|
|
40
|
+
manager.getFacts('general')
|
|
41
|
+
expect(manager.cacheSize()).toBe(1)
|
|
42
|
+
// Second call should hit cache
|
|
43
|
+
const facts2 = manager.getFacts('general')
|
|
44
|
+
expect(facts2.length).toBe(1)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('invalidates cache on write', () => {
|
|
48
|
+
store.addFact('测试事实', 'general')
|
|
49
|
+
manager.getFacts('general')
|
|
50
|
+
expect(manager.cacheSize()).toBe(1)
|
|
51
|
+
manager.invalidate()
|
|
52
|
+
expect(manager.cacheSize()).toBe(0)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('limits to top 10 facts', () => {
|
|
56
|
+
for (let i = 0; i < 15; i++) {
|
|
57
|
+
store.addFact(`事实 ${i}`, 'general')
|
|
58
|
+
}
|
|
59
|
+
const facts = manager.getFacts('general')
|
|
60
|
+
expect(facts.length).toBe(10)
|
|
61
|
+
})
|
|
62
|
+
})
|