@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.
Files changed (60) hide show
  1. package/README.md +44 -15
  2. package/README_zh.md +1 -1
  3. package/dist/cache.d.ts +23 -0
  4. package/dist/cache.js +44 -0
  5. package/dist/cache.js.map +1 -0
  6. package/dist/init.js +16 -8
  7. package/dist/init.js.map +1 -1
  8. package/dist/metrics.d.ts +31 -0
  9. package/dist/metrics.js +57 -0
  10. package/dist/metrics.js.map +1 -0
  11. package/dist/refine.d.ts +14 -0
  12. package/dist/refine.js +115 -0
  13. package/dist/refine.js.map +1 -0
  14. package/dist/resources.d.ts +27 -0
  15. package/dist/resources.js +56 -0
  16. package/dist/resources.js.map +1 -0
  17. package/dist/retriever.d.ts +14 -2
  18. package/dist/retriever.js +126 -36
  19. package/dist/retriever.js.map +1 -1
  20. package/dist/server.js +40 -16
  21. package/dist/server.js.map +1 -1
  22. package/dist/types.d.ts +2 -2
  23. package/docs/superpowers/plans/2026-05-15-mnemo-mcp.md +1154 -0
  24. package/docs/superpowers/plans/2026-05-16-mnemo-query-cache.md +613 -0
  25. package/docs/superpowers/plans/2026-05-16-retrieval-and-injection-optimization.md +770 -0
  26. package/openspec/changes/archive/2026-05-15-mnemo-mcp/.openspec.yaml +2 -0
  27. package/openspec/changes/archive/2026-05-15-mnemo-mcp/design.md +83 -0
  28. package/openspec/changes/archive/2026-05-15-mnemo-mcp/proposal.md +32 -0
  29. package/openspec/changes/archive/2026-05-15-mnemo-mcp/specs/fact-retrieval/spec.md +75 -0
  30. package/openspec/changes/archive/2026-05-15-mnemo-mcp/specs/fact-store/spec.md +83 -0
  31. package/openspec/changes/archive/2026-05-15-mnemo-mcp/specs/mcp-server/spec.md +34 -0
  32. package/openspec/changes/archive/2026-05-15-mnemo-mcp/specs/security/spec.md +37 -0
  33. package/openspec/changes/archive/2026-05-15-mnemo-mcp/tasks.md +44 -0
  34. package/openspec/changes/archive/2026-05-16-mnemo-query-cache/.openspec.yaml +2 -0
  35. package/openspec/changes/archive/2026-05-16-mnemo-query-cache/design.md +96 -0
  36. package/openspec/changes/archive/2026-05-16-mnemo-query-cache/proposal.md +29 -0
  37. package/openspec/changes/archive/2026-05-16-mnemo-query-cache/specs/batch-operations/spec.md +42 -0
  38. package/openspec/changes/archive/2026-05-16-mnemo-query-cache/specs/perf-metrics/spec.md +55 -0
  39. package/openspec/changes/archive/2026-05-16-mnemo-query-cache/specs/query-cache/spec.md +65 -0
  40. package/openspec/changes/archive/2026-05-16-mnemo-query-cache/tasks.md +45 -0
  41. package/openspec/changes/retrieval-and-injection-optimization/.openspec.yaml +2 -0
  42. package/openspec/changes/retrieval-and-injection-optimization/design.md +117 -0
  43. package/openspec/changes/retrieval-and-injection-optimization/proposal.md +30 -0
  44. package/openspec/changes/retrieval-and-injection-optimization/specs/adaptive-scoring/spec.md +43 -0
  45. package/openspec/changes/retrieval-and-injection-optimization/specs/injection-protocol/spec.md +48 -0
  46. package/openspec/changes/retrieval-and-injection-optimization/specs/mcp-resources/spec.md +39 -0
  47. package/openspec/changes/retrieval-and-injection-optimization/specs/query-refinement/spec.md +39 -0
  48. package/openspec/changes/retrieval-and-injection-optimization/tasks.md +33 -0
  49. package/openspec/config.yaml +20 -0
  50. package/package.json +1 -1
  51. package/src/cache.ts +65 -0
  52. package/src/init.ts +17 -9
  53. package/src/metrics.ts +81 -0
  54. package/src/refine.ts +127 -0
  55. package/src/resources.ts +78 -0
  56. package/src/retriever.ts +141 -34
  57. package/src/server.ts +42 -17
  58. package/src/types.ts +2 -2
  59. package/tests/refine.test.ts +52 -0
  60. 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 similar = store.findSimilarFact(a.content, category) ?? store.findSimilarFact(a.content)
80
- let warnings: string[] | undefined
81
- const scan = fullSecurityScan(a.content)
82
- if (scan.warnings.length > 0 || scan.hasPii) warnings = [...scan.warnings]
83
-
84
- if (similar) {
85
- store.updateFact(similar.factId, { content: a.content, tags: a.tags, trustDelta: 0.05 })
86
- const demoted = store.demoteContradictingFacts(similar.factId, a.content, category)
87
- return { content: [{ type: 'text' as const, text: JSON.stringify({ fact_id: similar.factId, status: 'updated', reason: 'similar_fact_merged', ...(demoted > 0 ? { contradicted_demoted: demoted } : {}), ...(warnings ? { warnings } : {}) }) }] }
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
- const factId = store.addFact(a.content, category, a.tags ?? '')
91
- const demoted = store.demoteContradictingFacts(factId, a.content, category)
92
- return { content: [{ type: 'text' as const, text: JSON.stringify({ fact_id: factId, status: 'added', category, ...(demoted > 0 ? { contradicted_demoted: demoted } : {}), ...(warnings ? { warnings } : {}) }) }] }
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 removed = store.removeFact(a.fact_id)
134
- return { content: [{ type: 'text' as const, text: JSON.stringify({ removed }) }] }
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
+ })