claude-memory-layer 1.0.0

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 (127) hide show
  1. package/.claude-plugin/commands/memory-forget.md +42 -0
  2. package/.claude-plugin/commands/memory-history.md +34 -0
  3. package/.claude-plugin/commands/memory-import.md +56 -0
  4. package/.claude-plugin/commands/memory-list.md +37 -0
  5. package/.claude-plugin/commands/memory-search.md +36 -0
  6. package/.claude-plugin/commands/memory-stats.md +34 -0
  7. package/.claude-plugin/hooks.json +59 -0
  8. package/.claude-plugin/plugin.json +24 -0
  9. package/.history/package_20260201112328.json +45 -0
  10. package/.history/package_20260201113602.json +45 -0
  11. package/.history/package_20260201113713.json +45 -0
  12. package/.history/package_20260201114110.json +45 -0
  13. package/Memo.txt +558 -0
  14. package/README.md +520 -0
  15. package/context.md +636 -0
  16. package/dist/.claude-plugin/commands/memory-forget.md +42 -0
  17. package/dist/.claude-plugin/commands/memory-history.md +34 -0
  18. package/dist/.claude-plugin/commands/memory-import.md +56 -0
  19. package/dist/.claude-plugin/commands/memory-list.md +37 -0
  20. package/dist/.claude-plugin/commands/memory-search.md +36 -0
  21. package/dist/.claude-plugin/commands/memory-stats.md +34 -0
  22. package/dist/.claude-plugin/hooks.json +59 -0
  23. package/dist/.claude-plugin/plugin.json +24 -0
  24. package/dist/cli/index.js +3539 -0
  25. package/dist/cli/index.js.map +7 -0
  26. package/dist/core/index.js +4408 -0
  27. package/dist/core/index.js.map +7 -0
  28. package/dist/hooks/session-end.js +2971 -0
  29. package/dist/hooks/session-end.js.map +7 -0
  30. package/dist/hooks/session-start.js +2969 -0
  31. package/dist/hooks/session-start.js.map +7 -0
  32. package/dist/hooks/stop.js +3123 -0
  33. package/dist/hooks/stop.js.map +7 -0
  34. package/dist/hooks/user-prompt-submit.js +2960 -0
  35. package/dist/hooks/user-prompt-submit.js.map +7 -0
  36. package/dist/services/memory-service.js +2931 -0
  37. package/dist/services/memory-service.js.map +7 -0
  38. package/package.json +45 -0
  39. package/plan.md +1642 -0
  40. package/scripts/build.ts +102 -0
  41. package/spec.md +624 -0
  42. package/specs/citations-system/context.md +243 -0
  43. package/specs/citations-system/plan.md +495 -0
  44. package/specs/citations-system/spec.md +371 -0
  45. package/specs/endless-mode/context.md +305 -0
  46. package/specs/endless-mode/plan.md +620 -0
  47. package/specs/endless-mode/spec.md +455 -0
  48. package/specs/entity-edge-model/context.md +401 -0
  49. package/specs/entity-edge-model/plan.md +459 -0
  50. package/specs/entity-edge-model/spec.md +391 -0
  51. package/specs/evidence-aligner-v2/context.md +401 -0
  52. package/specs/evidence-aligner-v2/plan.md +303 -0
  53. package/specs/evidence-aligner-v2/spec.md +312 -0
  54. package/specs/mcp-desktop-integration/context.md +278 -0
  55. package/specs/mcp-desktop-integration/plan.md +550 -0
  56. package/specs/mcp-desktop-integration/spec.md +494 -0
  57. package/specs/post-tool-use-hook/context.md +319 -0
  58. package/specs/post-tool-use-hook/plan.md +469 -0
  59. package/specs/post-tool-use-hook/spec.md +364 -0
  60. package/specs/private-tags/context.md +288 -0
  61. package/specs/private-tags/plan.md +412 -0
  62. package/specs/private-tags/spec.md +345 -0
  63. package/specs/progressive-disclosure/context.md +346 -0
  64. package/specs/progressive-disclosure/plan.md +663 -0
  65. package/specs/progressive-disclosure/spec.md +415 -0
  66. package/specs/task-entity-system/context.md +297 -0
  67. package/specs/task-entity-system/plan.md +301 -0
  68. package/specs/task-entity-system/spec.md +314 -0
  69. package/specs/vector-outbox-v2/context.md +470 -0
  70. package/specs/vector-outbox-v2/plan.md +562 -0
  71. package/specs/vector-outbox-v2/spec.md +466 -0
  72. package/specs/web-viewer-ui/context.md +384 -0
  73. package/specs/web-viewer-ui/plan.md +797 -0
  74. package/specs/web-viewer-ui/spec.md +516 -0
  75. package/src/cli/index.ts +570 -0
  76. package/src/core/canonical-key.ts +186 -0
  77. package/src/core/citation-generator.ts +63 -0
  78. package/src/core/consolidated-store.ts +279 -0
  79. package/src/core/consolidation-worker.ts +384 -0
  80. package/src/core/context-formatter.ts +276 -0
  81. package/src/core/continuity-manager.ts +336 -0
  82. package/src/core/edge-repo.ts +324 -0
  83. package/src/core/embedder.ts +124 -0
  84. package/src/core/entity-repo.ts +342 -0
  85. package/src/core/event-store.ts +672 -0
  86. package/src/core/evidence-aligner.ts +635 -0
  87. package/src/core/graduation.ts +365 -0
  88. package/src/core/index.ts +32 -0
  89. package/src/core/matcher.ts +210 -0
  90. package/src/core/metadata-extractor.ts +203 -0
  91. package/src/core/privacy/filter.ts +179 -0
  92. package/src/core/privacy/index.ts +20 -0
  93. package/src/core/privacy/tag-parser.ts +145 -0
  94. package/src/core/progressive-retriever.ts +415 -0
  95. package/src/core/retriever.ts +235 -0
  96. package/src/core/task/blocker-resolver.ts +325 -0
  97. package/src/core/task/index.ts +9 -0
  98. package/src/core/task/task-matcher.ts +238 -0
  99. package/src/core/task/task-projector.ts +345 -0
  100. package/src/core/task/task-resolver.ts +414 -0
  101. package/src/core/types.ts +841 -0
  102. package/src/core/vector-outbox.ts +295 -0
  103. package/src/core/vector-store.ts +182 -0
  104. package/src/core/vector-worker.ts +488 -0
  105. package/src/core/working-set-store.ts +244 -0
  106. package/src/hooks/post-tool-use.ts +127 -0
  107. package/src/hooks/session-end.ts +78 -0
  108. package/src/hooks/session-start.ts +57 -0
  109. package/src/hooks/stop.ts +78 -0
  110. package/src/hooks/user-prompt-submit.ts +54 -0
  111. package/src/mcp/handlers.ts +212 -0
  112. package/src/mcp/index.ts +47 -0
  113. package/src/mcp/tools.ts +78 -0
  114. package/src/server/api/citations.ts +101 -0
  115. package/src/server/api/events.ts +101 -0
  116. package/src/server/api/index.ts +18 -0
  117. package/src/server/api/search.ts +98 -0
  118. package/src/server/api/sessions.ts +111 -0
  119. package/src/server/api/stats.ts +97 -0
  120. package/src/server/index.ts +91 -0
  121. package/src/services/memory-service.ts +626 -0
  122. package/src/services/session-history-importer.ts +367 -0
  123. package/tests/canonical-key.test.ts +101 -0
  124. package/tests/evidence-aligner.test.ts +152 -0
  125. package/tests/matcher.test.ts +112 -0
  126. package/tsconfig.json +24 -0
  127. package/vitest.config.ts +15 -0
package/plan.md ADDED
@@ -0,0 +1,1642 @@
1
+ # Implementation Plan: Claude Code Memory Plugin
2
+
3
+ ## 1. 개발 단계 개요
4
+
5
+ AXIOMMIND 파이프라인 구현 단계에 맞춰 구성:
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────────┐
9
+ │ P0: 필수 품질 (Must Have) │
10
+ │ ├── EventStore (append-only, dedupe) │
11
+ │ ├── EvidenceAligner (증거 스팬 정렬) │
12
+ │ ├── Task Entity System (canonical_key) │
13
+ │ ├── Vector Outbox (single-writer) │
14
+ │ ├── Basic Hooks (SessionStart, Stop) │
15
+ │ └── CLI 기본 명령어 │
16
+ ├─────────────────────────────────────────────────────────────────┤
17
+ │ P1: 운영 (Operations) │
18
+ │ ├── build_runs 테이블 (파이프라인 실행 추적) │
19
+ │ ├── conflict_ledger (충돌 기록) │
20
+ │ ├── decision_ledger (의사결정 기록) │
21
+ │ ├── 메트릭 스켈레톤 │
22
+ │ └── 에러 복구 메커니즘 │
23
+ ├─────────────────────────────────────────────────────────────────┤
24
+ │ P2: 검색 (Retrieval) │
25
+ │ ├── 하이브리드 검색 (Vector + FTS) │
26
+ │ ├── 골드셋 평가 시스템 │
27
+ │ ├── 인사이트 추출 (LLM 기반) │
28
+ │ └── L2→L3→L4 승격 파이프라인 │
29
+ └─────────────────────────────────────────────────────────────────┘
30
+ ```
31
+
32
+ ### 구현 Phase 매핑
33
+
34
+ | AXIOMMIND | 플러그인 Phase | 핵심 산출물 |
35
+ |-----------|---------------|-------------|
36
+ | **P0** | Phase 0-1 | EventStore, VectorWorker, 기본 Hook |
37
+ | **P0** | Phase 2-3 | Embedder, Retriever, Hook 통합 |
38
+ | **P1** | Phase 4 | CLI, 운영 도구, 메트릭 |
39
+ | **P2** | Phase 5 | 하이브리드 검색, 평가, 최적화 |
40
+
41
+ ```
42
+ Phase 0: 프로젝트 설정
43
+
44
+ Phase 1: Core Storage Layer (P0 - EventStore, Outbox)
45
+
46
+ Phase 2: Embedding & Retrieval (P0 - Vector, Matcher)
47
+
48
+ Phase 3: Hook Integration (P0 - 기본 동작)
49
+
50
+ Phase 4: Commands & CLI (P1 - 운영)
51
+
52
+ Phase 5: Testing & Polish (P2 - 검색 최적화)
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Phase 0: 프로젝트 설정
58
+
59
+ ### 0.1 디렉토리 구조 생성
60
+
61
+ ```
62
+ code-memory/
63
+ ├── .claude-plugin/
64
+ │ └── plugin.json # 플러그인 메타데이터
65
+ ├── commands/
66
+ │ ├── search.md # /code-memory:search
67
+ │ ├── history.md # /code-memory:history
68
+ │ ├── insights.md # /code-memory:insights
69
+ │ ├── forget.md # /code-memory:forget
70
+ │ └── stats.md # /code-memory:stats
71
+ ├── hooks/
72
+ │ └── hooks.json # Hook 설정
73
+ ├── src/
74
+ │ ├── cli/
75
+ │ │ ├── index.ts # CLI 엔트리포인트
76
+ │ │ ├── commands/
77
+ │ │ │ ├── session-start.ts
78
+ │ │ │ ├── session-end.ts
79
+ │ │ │ ├── search.ts
80
+ │ │ │ ├── save.ts
81
+ │ │ │ └── init.ts
82
+ │ │ └── utils.ts
83
+ │ ├── core/
84
+ │ │ ├── canonical-key.ts # 정규화된 키 생성 (AXIOMMIND)
85
+ │ │ ├── event-store.ts # DuckDB 이벤트 저장소
86
+ │ │ ├── vector-store.ts # LanceDB 벡터 저장소
87
+ │ │ ├── vector-worker.ts # Single-writer 패턴 워커 (AXIOMMIND)
88
+ │ │ ├── embedder.ts # 임베딩 생성
89
+ │ │ ├── matcher.ts # 가중치 기반 매칭 (AXIOMMIND)
90
+ │ │ ├── evidence-aligner.ts # 증거 스팬 정렬 (AXIOMMIND 원칙4)
91
+ │ │ ├── retriever.ts # 기억 검색
92
+ │ │ ├── projector.ts # 이벤트→엔티티 투영 (AXIOMMIND)
93
+ │ │ ├── graduation.ts # L0→L4 승격 파이프라인 (AXIOMMIND)
94
+ │ │ └── types.ts # 타입 정의
95
+ │ ├── hooks/
96
+ │ │ ├── session-start.ts
97
+ │ │ ├── user-prompt-submit.ts
98
+ │ │ ├── stop.ts
99
+ │ │ └── session-end.ts
100
+ │ └── index.ts
101
+ ├── tests/
102
+ │ ├── event-store.test.ts
103
+ │ ├── vector-store.test.ts
104
+ │ ├── retriever.test.ts
105
+ │ └── integration.test.ts
106
+ ├── scripts/
107
+ │ └── build.ts
108
+ ├── package.json
109
+ ├── tsconfig.json
110
+ ├── vitest.config.ts
111
+ ├── README.md
112
+ ├── spec.md
113
+ ├── plan.md
114
+ └── context.md
115
+ ```
116
+
117
+ ### 0.2 초기 설정 작업
118
+
119
+ ```bash
120
+ # 1. package.json 생성
121
+ npm init -y
122
+
123
+ # 2. TypeScript 및 핵심 의존성 설치
124
+ npm install -D typescript @types/node tsx esbuild vitest
125
+
126
+ # 3. 런타임 의존성
127
+ npm install zod commander duckdb lancedb
128
+
129
+ # 4. 임베딩 (선택)
130
+ npm install @xenova/transformers # 또는 Python sentence-transformers
131
+ ```
132
+
133
+ ### 0.3 TypeScript 설정
134
+
135
+ ```json
136
+ // tsconfig.json
137
+ {
138
+ "compilerOptions": {
139
+ "target": "ES2022",
140
+ "module": "ESNext",
141
+ "moduleResolution": "bundler",
142
+ "strict": true,
143
+ "esModuleInterop": true,
144
+ "skipLibCheck": true,
145
+ "outDir": "dist",
146
+ "rootDir": "src",
147
+ "declaration": true,
148
+ "resolveJsonModule": true
149
+ },
150
+ "include": ["src/**/*"],
151
+ "exclude": ["node_modules", "dist", "tests"]
152
+ }
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Phase 1: Core Storage Layer
158
+
159
+ ### 1.1 타입 정의 (src/core/types.ts)
160
+
161
+ ```typescript
162
+ // Idris2 영감: 완전하고 불변한 타입 정의
163
+
164
+ import { z } from 'zod';
165
+
166
+ // 이벤트 타입 스키마
167
+ export const EventTypeSchema = z.enum([
168
+ 'user_prompt',
169
+ 'agent_response',
170
+ 'session_summary'
171
+ ]);
172
+ export type EventType = z.infer<typeof EventTypeSchema>;
173
+
174
+ // 메모리 이벤트 스키마
175
+ export const MemoryEventSchema = z.object({
176
+ id: z.string().uuid(),
177
+ eventType: EventTypeSchema,
178
+ sessionId: z.string(),
179
+ timestamp: z.date(),
180
+ content: z.string(),
181
+ contentHash: z.string(),
182
+ metadata: z.record(z.unknown()).optional()
183
+ });
184
+ export type MemoryEvent = z.infer<typeof MemoryEventSchema>;
185
+
186
+ // 세션 스키마
187
+ export const SessionSchema = z.object({
188
+ id: z.string(),
189
+ startedAt: z.date(),
190
+ endedAt: z.date().optional(),
191
+ projectPath: z.string().optional(),
192
+ summary: z.string().optional(),
193
+ tags: z.array(z.string()).optional()
194
+ });
195
+ export type Session = z.infer<typeof SessionSchema>;
196
+
197
+ // 검색 결과
198
+ export const MemoryMatchSchema = z.object({
199
+ event: MemoryEventSchema,
200
+ score: z.number().min(0).max(1),
201
+ relevanceReason: z.string().optional()
202
+ });
203
+ export type MemoryMatch = z.infer<typeof MemoryMatchSchema>;
204
+
205
+ // 설정 스키마
206
+ export const ConfigSchema = z.object({
207
+ storage: z.object({
208
+ path: z.string().default('~/.claude-code/memory'),
209
+ maxSizeMB: z.number().default(500)
210
+ }),
211
+ embedding: z.object({
212
+ provider: z.enum(['local', 'openai']).default('local'),
213
+ model: z.string().default('all-MiniLM-L6-v2'),
214
+ batchSize: z.number().default(32)
215
+ }),
216
+ retrieval: z.object({
217
+ topK: z.number().default(5),
218
+ minScore: z.number().default(0.7),
219
+ maxTokens: z.number().default(2000)
220
+ })
221
+ });
222
+ export type Config = z.infer<typeof ConfigSchema>;
223
+
224
+ // AXIOMMIND: Matching 결과 타입
225
+ export const MatchConfidenceSchema = z.enum(['high', 'suggested', 'none']);
226
+ export type MatchConfidence = z.infer<typeof MatchConfidenceSchema>;
227
+
228
+ export const MatchResultSchema = z.object({
229
+ match: MemoryMatchSchema.nullable(),
230
+ confidence: MatchConfidenceSchema,
231
+ gap: z.number().optional(),
232
+ alternatives: z.array(MemoryMatchSchema).optional()
233
+ });
234
+ export type MatchResult = z.infer<typeof MatchResultSchema>;
235
+
236
+ // AXIOMMIND: Matching Thresholds
237
+ export const MATCH_THRESHOLDS = {
238
+ minCombinedScore: 0.92,
239
+ minGap: 0.03,
240
+ suggestionThreshold: 0.75
241
+ } as const;
242
+ ```
243
+
244
+ ### 1.2 Canonical Key 구현 (src/core/canonical-key.ts) - AXIOMMIND
245
+
246
+ ```typescript
247
+ /**
248
+ * AXIOMMIND canonical_key.py 포팅
249
+ * 동일한 제목은 항상 동일한 키를 생성하는 결정론적 정규화
250
+ */
251
+
252
+ import { createHash } from 'crypto';
253
+
254
+ const MAX_KEY_LENGTH = 200;
255
+
256
+ /**
257
+ * 텍스트를 정규화된 canonical key로 변환
258
+ *
259
+ * 정규화 단계:
260
+ * 1. NFKC 유니코드 정규화
261
+ * 2. 소문자 변환
262
+ * 3. 구두점 제거
263
+ * 4. 연속 공백 정리
264
+ * 5. 컨텍스트 추가 (선택)
265
+ * 6. 긴 키 truncate + MD5
266
+ */
267
+ export function makeCanonicalKey(
268
+ title: string,
269
+ context?: { project?: string; sessionId?: string }
270
+ ): string {
271
+ // Step 1: NFKC 정규화
272
+ let normalized = title.normalize('NFKC');
273
+
274
+ // Step 2: 소문자 변환
275
+ normalized = normalized.toLowerCase();
276
+
277
+ // Step 3: 구두점 제거 (유니코드 호환)
278
+ normalized = normalized.replace(/[^\p{L}\p{N}\s]/gu, '');
279
+
280
+ // Step 4: 연속 공백 정리
281
+ normalized = normalized.replace(/\s+/g, ' ').trim();
282
+
283
+ // Step 5: 컨텍스트 추가
284
+ let key = normalized;
285
+ if (context?.project) {
286
+ key = `${context.project}::${key}`;
287
+ }
288
+
289
+ // Step 6: 긴 키 처리
290
+ if (key.length > MAX_KEY_LENGTH) {
291
+ const hashSuffix = createHash('md5').update(key).digest('hex').slice(0, 8);
292
+ key = key.slice(0, MAX_KEY_LENGTH - 9) + '_' + hashSuffix;
293
+ }
294
+
295
+ return key;
296
+ }
297
+
298
+ /**
299
+ * 두 텍스트가 동일한 canonical key를 가지는지 확인
300
+ */
301
+ export function isSameCanonicalKey(a: string, b: string): boolean {
302
+ return makeCanonicalKey(a) === makeCanonicalKey(b);
303
+ }
304
+
305
+ /**
306
+ * Dedupe key 생성 (content + session으로 유일성 보장)
307
+ */
308
+ export function makeDedupeKey(content: string, sessionId: string): string {
309
+ const contentHash = createHash('sha256').update(content).digest('hex');
310
+ return `${sessionId}:${contentHash}`;
311
+ }
312
+ ```
313
+
314
+ ### 1.3 Event Store 구현 (src/core/event-store.ts)
315
+
316
+ ```typescript
317
+ // AXIOMMIND 스타일: append-only, 멱등성, 단일 진실 공급원
318
+
319
+ import { Database } from 'duckdb';
320
+ import { createHash } from 'crypto';
321
+ import { MemoryEvent, EventType } from './types';
322
+
323
+ export class EventStore {
324
+ private db: Database;
325
+
326
+ constructor(dbPath: string) {
327
+ this.db = new Database(dbPath);
328
+ this.initialize();
329
+ }
330
+
331
+ private initialize(): void {
332
+ // 이벤트 테이블 (불변, append-only)
333
+ this.db.run(`
334
+ CREATE TABLE IF NOT EXISTS events (
335
+ id VARCHAR PRIMARY KEY,
336
+ event_type VARCHAR NOT NULL,
337
+ session_id VARCHAR NOT NULL,
338
+ timestamp TIMESTAMP NOT NULL,
339
+ content TEXT NOT NULL,
340
+ metadata JSON,
341
+ content_hash VARCHAR UNIQUE
342
+ )
343
+ `);
344
+
345
+ // 중복 방지 테이블
346
+ this.db.run(`
347
+ CREATE TABLE IF NOT EXISTS event_dedup (
348
+ content_hash VARCHAR PRIMARY KEY,
349
+ event_id VARCHAR NOT NULL
350
+ )
351
+ `);
352
+
353
+ // 세션 테이블
354
+ this.db.run(`
355
+ CREATE TABLE IF NOT EXISTS sessions (
356
+ id VARCHAR PRIMARY KEY,
357
+ started_at TIMESTAMP NOT NULL,
358
+ ended_at TIMESTAMP,
359
+ project_path VARCHAR,
360
+ summary TEXT
361
+ )
362
+ `);
363
+ }
364
+
365
+ // 멱등성 보장 저장
366
+ async append(event: Omit<MemoryEvent, 'id' | 'contentHash'>): Promise<{
367
+ success: boolean;
368
+ eventId?: string;
369
+ isDuplicate?: boolean;
370
+ }> {
371
+ const contentHash = this.hashContent(event.content);
372
+
373
+ // 중복 확인
374
+ const existing = this.db.prepare(`
375
+ SELECT event_id FROM event_dedup WHERE content_hash = ?
376
+ `).get(contentHash);
377
+
378
+ if (existing) {
379
+ return { success: true, eventId: existing.event_id, isDuplicate: true };
380
+ }
381
+
382
+ const id = crypto.randomUUID();
383
+
384
+ this.db.run(`
385
+ INSERT INTO events (id, event_type, session_id, timestamp, content, metadata, content_hash)
386
+ VALUES (?, ?, ?, ?, ?, ?, ?)
387
+ `, [id, event.eventType, event.sessionId, event.timestamp, event.content,
388
+ JSON.stringify(event.metadata), contentHash]);
389
+
390
+ this.db.run(`
391
+ INSERT INTO event_dedup (content_hash, event_id) VALUES (?, ?)
392
+ `, [contentHash, id]);
393
+
394
+ return { success: true, eventId: id, isDuplicate: false };
395
+ }
396
+
397
+ private hashContent(content: string): string {
398
+ return createHash('sha256').update(content).digest('hex');
399
+ }
400
+
401
+ // 세션별 이벤트 조회
402
+ async getSessionEvents(sessionId: string): Promise<MemoryEvent[]> {
403
+ return this.db.prepare(`
404
+ SELECT * FROM events WHERE session_id = ? ORDER BY timestamp ASC
405
+ `).all(sessionId);
406
+ }
407
+
408
+ // 최근 이벤트 조회
409
+ async getRecentEvents(limit: number = 100): Promise<MemoryEvent[]> {
410
+ return this.db.prepare(`
411
+ SELECT * FROM events ORDER BY timestamp DESC LIMIT ?
412
+ `).all(limit);
413
+ }
414
+ }
415
+ ```
416
+
417
+ ### 1.3 Vector Store 구현 (src/core/vector-store.ts)
418
+
419
+ ```typescript
420
+ import * as lancedb from 'lancedb';
421
+
422
+ export class VectorStore {
423
+ private db: lancedb.Connection;
424
+ private table: lancedb.Table | null = null;
425
+
426
+ constructor(private dbPath: string) {}
427
+
428
+ async initialize(): Promise<void> {
429
+ this.db = await lancedb.connect(this.dbPath);
430
+
431
+ // 테이블이 없으면 생성 (첫 데이터 삽입 시)
432
+ try {
433
+ this.table = await this.db.openTable('conversations');
434
+ } catch {
435
+ // 테이블이 없으면 나중에 생성
436
+ this.table = null;
437
+ }
438
+ }
439
+
440
+ async upsert(data: {
441
+ id: string;
442
+ eventId: string;
443
+ sessionId: string;
444
+ eventType: string;
445
+ content: string;
446
+ vector: number[];
447
+ timestamp: string;
448
+ metadata?: Record<string, unknown>;
449
+ }): Promise<void> {
450
+ if (!this.table) {
451
+ // 첫 데이터로 테이블 생성
452
+ this.table = await this.db.createTable('conversations', [data]);
453
+ return;
454
+ }
455
+
456
+ await this.table.add([data]);
457
+ }
458
+
459
+ async search(queryVector: number[], options: {
460
+ limit?: number;
461
+ minScore?: number;
462
+ filter?: string;
463
+ } = {}): Promise<Array<{
464
+ id: string;
465
+ eventId: string;
466
+ content: string;
467
+ score: number;
468
+ }>> {
469
+ if (!this.table) {
470
+ return [];
471
+ }
472
+
473
+ const { limit = 5, minScore = 0.7 } = options;
474
+
475
+ const results = await this.table
476
+ .search(queryVector)
477
+ .limit(limit)
478
+ .execute();
479
+
480
+ return results
481
+ .filter(r => r._distance <= (1 - minScore)) // distance to score
482
+ .map(r => ({
483
+ id: r.id,
484
+ eventId: r.eventId,
485
+ content: r.content,
486
+ score: 1 - r._distance
487
+ }));
488
+ }
489
+
490
+ async delete(eventId: string): Promise<void> {
491
+ if (!this.table) return;
492
+ await this.table.delete(`eventId = "${eventId}"`);
493
+ }
494
+ }
495
+ ```
496
+
497
+ ### 1.4 Vector Worker 구현 (src/core/vector-worker.ts) - AXIOMMIND Single-Writer
498
+
499
+ ```typescript
500
+ /**
501
+ * AXIOMMIND vector_worker.py 포팅
502
+ * Single-Writer 패턴으로 LanceDB 동시성 문제 해결
503
+ *
504
+ * 원리:
505
+ * 1. 이벤트 저장 시 embedding_outbox에 작업 추가 (빠름)
506
+ * 2. 별도 워커가 outbox를 순차적으로 처리 (단일 쓰기)
507
+ * 3. 처리 완료 시 outbox에서 삭제
508
+ */
509
+
510
+ import { Database } from 'duckdb';
511
+ import { VectorStore } from './vector-store';
512
+ import { Embedder } from './embedder';
513
+
514
+ interface OutboxItem {
515
+ id: string;
516
+ eventId: string;
517
+ content: string;
518
+ status: 'pending' | 'processing' | 'done' | 'failed';
519
+ retryCount: number;
520
+ createdAt: Date;
521
+ }
522
+
523
+ export class VectorWorker {
524
+ private isRunning = false;
525
+ private pollInterval = 1000; // 1초
526
+
527
+ constructor(
528
+ private db: Database,
529
+ private vectorStore: VectorStore,
530
+ private embedder: Embedder
531
+ ) {}
532
+
533
+ /**
534
+ * Outbox에 임베딩 작업 추가 (빠른 반환)
535
+ */
536
+ async enqueue(eventId: string, content: string): Promise<string> {
537
+ const id = crypto.randomUUID();
538
+
539
+ this.db.run(`
540
+ INSERT INTO embedding_outbox (id, event_id, content, status, retry_count, created_at)
541
+ VALUES (?, ?, ?, 'pending', 0, NOW())
542
+ `, [id, eventId, content]);
543
+
544
+ return id;
545
+ }
546
+
547
+ /**
548
+ * 워커 시작 (백그라운드에서 실행)
549
+ */
550
+ async start(): Promise<void> {
551
+ if (this.isRunning) return;
552
+ this.isRunning = true;
553
+
554
+ while (this.isRunning) {
555
+ await this.processOutbox();
556
+ await this.sleep(this.pollInterval);
557
+ }
558
+ }
559
+
560
+ /**
561
+ * 워커 중지
562
+ */
563
+ stop(): void {
564
+ this.isRunning = false;
565
+ }
566
+
567
+ /**
568
+ * Outbox 처리 (핵심 로직)
569
+ */
570
+ private async processOutbox(batchSize: number = 32): Promise<number> {
571
+ // 1. Pending 항목 가져오기 + 락 획득 (atomic update)
572
+ const pending = this.db.prepare(`
573
+ UPDATE embedding_outbox
574
+ SET status = 'processing'
575
+ WHERE id IN (
576
+ SELECT id FROM embedding_outbox
577
+ WHERE status = 'pending'
578
+ ORDER BY created_at
579
+ LIMIT ?
580
+ )
581
+ RETURNING *
582
+ `).all(batchSize) as OutboxItem[];
583
+
584
+ if (pending.length === 0) {
585
+ return 0;
586
+ }
587
+
588
+ try {
589
+ // 2. 배치 임베딩 생성
590
+ const contents = pending.map(p => p.content);
591
+ const vectors = await this.embedder.embedBatch(contents);
592
+
593
+ // 3. LanceDB에 저장 (단일 쓰기 - 동시성 안전)
594
+ for (let i = 0; i < pending.length; i++) {
595
+ const item = pending[i];
596
+ await this.vectorStore.upsert({
597
+ id: crypto.randomUUID(),
598
+ eventId: item.eventId,
599
+ sessionId: '', // 필요시 조회
600
+ eventType: '',
601
+ content: item.content.slice(0, 1000), // 미리보기용
602
+ vector: vectors[i],
603
+ timestamp: new Date().toISOString()
604
+ });
605
+ }
606
+
607
+ // 4. 성공한 항목 삭제
608
+ const ids = pending.map(p => `'${p.id}'`).join(',');
609
+ this.db.run(`DELETE FROM embedding_outbox WHERE id IN (${ids})`);
610
+
611
+ return pending.length;
612
+
613
+ } catch (error) {
614
+ // 5. 실패 시 retry_count 증가
615
+ const ids = pending.map(p => `'${p.id}'`).join(',');
616
+ this.db.run(`
617
+ UPDATE embedding_outbox
618
+ SET status = CASE WHEN retry_count >= 3 THEN 'failed' ELSE 'pending' END,
619
+ retry_count = retry_count + 1,
620
+ error_message = ?
621
+ WHERE id IN (${ids})
622
+ `, [String(error)]);
623
+
624
+ return 0;
625
+ }
626
+ }
627
+
628
+ private sleep(ms: number): Promise<void> {
629
+ return new Promise(resolve => setTimeout(resolve, ms));
630
+ }
631
+ }
632
+ ```
633
+
634
+ ### 1.5 Matcher 구현 (src/core/matcher.ts) - AXIOMMIND Weighted Scoring
635
+
636
+ ```typescript
637
+ /**
638
+ * AXIOMMIND task_matcher.py 포팅
639
+ * 가중치 기반 스코어링 + 엄격한 매칭 임계값
640
+ */
641
+
642
+ import { MemoryMatch, MatchResult, MATCH_THRESHOLDS } from './types';
643
+
644
+ interface ScoringWeights {
645
+ semanticSimilarity: number;
646
+ ftsScore: number;
647
+ recencyBonus: number;
648
+ statusWeight: number;
649
+ }
650
+
651
+ const DEFAULT_WEIGHTS: ScoringWeights = {
652
+ semanticSimilarity: 0.4,
653
+ ftsScore: 0.25,
654
+ recencyBonus: 0.2,
655
+ statusWeight: 0.15
656
+ };
657
+
658
+ interface RawSearchResult {
659
+ id: string;
660
+ eventId: string;
661
+ content: string;
662
+ vectorScore: number; // 벡터 유사도 (0-1)
663
+ ftsScore?: number; // 전문 검색 점수 (0-1)
664
+ timestamp: Date;
665
+ eventType: string;
666
+ }
667
+
668
+ /**
669
+ * 가중치 결합 점수 계산
670
+ */
671
+ export function calculateWeightedScore(
672
+ result: RawSearchResult,
673
+ weights: ScoringWeights = DEFAULT_WEIGHTS
674
+ ): number {
675
+ // 최신성 가산점 (30일 이내 = 1.0, 이후 감소)
676
+ const daysSince = (Date.now() - result.timestamp.getTime()) / (1000 * 60 * 60 * 24);
677
+ const recencyScore = Math.max(0, 1 - daysSince / 30);
678
+
679
+ // 상태별 가중치 (agent_response > user_prompt)
680
+ const statusScore = result.eventType === 'agent_response' ? 1.0 : 0.8;
681
+
682
+ const score =
683
+ result.vectorScore * weights.semanticSimilarity +
684
+ (result.ftsScore ?? 0) * weights.ftsScore +
685
+ recencyScore * weights.recencyBonus +
686
+ statusScore * weights.statusWeight;
687
+
688
+ return Math.min(1, Math.max(0, score)); // 0-1 범위로 클램프
689
+ }
690
+
691
+ /**
692
+ * 엄격한 매칭: top-1이 확실히 우세할 때만 확정
693
+ *
694
+ * AXIOMMIND 규칙:
695
+ * - combined score >= 0.92 AND gap >= 0.03 → 'high' (확정)
696
+ * - 0.75 <= score < 0.92 → 'suggested' (제안)
697
+ * - score < 0.75 → 'none' (매칭 없음)
698
+ */
699
+ export function matchWithConfidence(
700
+ candidates: MemoryMatch[]
701
+ ): MatchResult {
702
+ if (candidates.length === 0) {
703
+ return { match: null, confidence: 'none' };
704
+ }
705
+
706
+ // 점수순 정렬
707
+ const sorted = [...candidates].sort((a, b) => b.score - a.score);
708
+ const top = sorted[0];
709
+
710
+ // 임계값 미달
711
+ if (top.score < MATCH_THRESHOLDS.suggestionThreshold) {
712
+ return { match: null, confidence: 'none' };
713
+ }
714
+
715
+ // 확정 매칭 체크
716
+ if (top.score >= MATCH_THRESHOLDS.minCombinedScore) {
717
+ // 단일 후보
718
+ if (sorted.length === 1) {
719
+ return { match: top, confidence: 'high' };
720
+ }
721
+
722
+ // 2위와의 gap 체크
723
+ const gap = top.score - sorted[1].score;
724
+ if (gap >= MATCH_THRESHOLDS.minGap) {
725
+ return { match: top, confidence: 'high', gap };
726
+ }
727
+ }
728
+
729
+ // 제안 모드 (확실하지 않음)
730
+ return {
731
+ match: top,
732
+ confidence: 'suggested',
733
+ gap: sorted.length > 1 ? top.score - sorted[1].score : undefined,
734
+ alternatives: sorted.slice(1, 4) // 상위 3개 대안
735
+ };
736
+ }
737
+
738
+ /**
739
+ * 검색 결과를 MemoryMatch로 변환
740
+ */
741
+ export function toMemoryMatch(
742
+ result: RawSearchResult,
743
+ weights?: ScoringWeights
744
+ ): MemoryMatch {
745
+ const score = calculateWeightedScore(result, weights);
746
+
747
+ return {
748
+ event: {
749
+ id: result.eventId,
750
+ eventType: result.eventType as any,
751
+ sessionId: '',
752
+ timestamp: result.timestamp,
753
+ content: result.content,
754
+ contentHash: ''
755
+ },
756
+ score,
757
+ relevanceReason: `Weighted score: ${(score * 100).toFixed(1)}% ` +
758
+ `(vector: ${(result.vectorScore * 100).toFixed(0)}%)`
759
+ };
760
+ }
761
+ ```
762
+
763
+ ### 1.6 Evidence Aligner 구현 (src/core/evidence-aligner.ts) - AXIOMMIND 원칙4
764
+
765
+ ```typescript
766
+ /**
767
+ * AXIOMMIND 원칙 4: 증거 범위는 파이프라인이 확정
768
+ *
769
+ * LLM이 추출한 대략적인 인용문을 원본 텍스트에서
770
+ * 정확한 (start, end) 위치로 변환
771
+ */
772
+
773
+ interface EvidenceSpan {
774
+ start: number;
775
+ end: number;
776
+ confidence: number;
777
+ matchType: 'exact' | 'fuzzy' | 'none';
778
+ originalQuote: string;
779
+ alignedText: string;
780
+ }
781
+
782
+ interface FuzzyMatch {
783
+ start: number;
784
+ end: number;
785
+ score: number;
786
+ text: string;
787
+ }
788
+
789
+ export class EvidenceAligner {
790
+ private fuzzyThreshold: number;
791
+
792
+ constructor(fuzzyThreshold: number = 0.85) {
793
+ this.fuzzyThreshold = fuzzyThreshold;
794
+ }
795
+
796
+ /**
797
+ * LLM 인용문을 원본 텍스트에서 정확한 스팬으로 변환
798
+ */
799
+ align(sourceText: string, llmQuote: string): EvidenceSpan | null {
800
+ // 1. 정확한 매칭 시도
801
+ const exactPos = sourceText.indexOf(llmQuote);
802
+ if (exactPos >= 0) {
803
+ return {
804
+ start: exactPos,
805
+ end: exactPos + llmQuote.length,
806
+ confidence: 1.0,
807
+ matchType: 'exact',
808
+ originalQuote: llmQuote,
809
+ alignedText: llmQuote
810
+ };
811
+ }
812
+
813
+ // 2. 정규화 후 매칭 (공백, 대소문자 무시)
814
+ const normalizedMatch = this.normalizedSearch(sourceText, llmQuote);
815
+ if (normalizedMatch && normalizedMatch.score >= 0.95) {
816
+ return {
817
+ start: normalizedMatch.start,
818
+ end: normalizedMatch.end,
819
+ confidence: normalizedMatch.score,
820
+ matchType: 'exact',
821
+ originalQuote: llmQuote,
822
+ alignedText: normalizedMatch.text
823
+ };
824
+ }
825
+
826
+ // 3. Fuzzy 매칭 (Levenshtein 거리)
827
+ const fuzzyMatch = this.fuzzySearch(sourceText, llmQuote);
828
+ if (fuzzyMatch && fuzzyMatch.score >= this.fuzzyThreshold) {
829
+ return {
830
+ start: fuzzyMatch.start,
831
+ end: fuzzyMatch.end,
832
+ confidence: fuzzyMatch.score,
833
+ matchType: 'fuzzy',
834
+ originalQuote: llmQuote,
835
+ alignedText: fuzzyMatch.text
836
+ };
837
+ }
838
+
839
+ // 4. 매칭 실패
840
+ return null;
841
+ }
842
+
843
+ /**
844
+ * 정규화된 검색 (공백/대소문자 무시)
845
+ */
846
+ private normalizedSearch(text: string, query: string): FuzzyMatch | null {
847
+ const normalize = (s: string) => s.toLowerCase().replace(/\s+/g, ' ').trim();
848
+ const normalizedText = normalize(text);
849
+ const normalizedQuery = normalize(query);
850
+
851
+ const pos = normalizedText.indexOf(normalizedQuery);
852
+ if (pos < 0) return null;
853
+
854
+ // 원본 텍스트에서 실제 위치 찾기
855
+ let originalStart = 0;
856
+ let normalizedPos = 0;
857
+ for (let i = 0; i < text.length && normalizedPos < pos; i++) {
858
+ if (!/\s/.test(text[i]) || (i > 0 && !/\s/.test(text[i-1]))) {
859
+ normalizedPos++;
860
+ }
861
+ originalStart = i + 1;
862
+ }
863
+
864
+ return {
865
+ start: originalStart,
866
+ end: originalStart + query.length,
867
+ score: 0.98,
868
+ text: text.slice(originalStart, originalStart + query.length)
869
+ };
870
+ }
871
+
872
+ /**
873
+ * Fuzzy 검색 (슬라이딩 윈도우 + Levenshtein)
874
+ */
875
+ private fuzzySearch(text: string, query: string): FuzzyMatch | null {
876
+ const windowSize = Math.min(query.length * 1.5, text.length);
877
+ let bestMatch: FuzzyMatch | null = null;
878
+
879
+ for (let i = 0; i <= text.length - query.length; i++) {
880
+ const window = text.slice(i, i + Math.ceil(windowSize));
881
+ const score = this.similarity(window.slice(0, query.length), query);
882
+
883
+ if (!bestMatch || score > bestMatch.score) {
884
+ bestMatch = {
885
+ start: i,
886
+ end: i + query.length,
887
+ score,
888
+ text: window.slice(0, query.length)
889
+ };
890
+ }
891
+ }
892
+
893
+ return bestMatch;
894
+ }
895
+
896
+ /**
897
+ * Levenshtein 기반 유사도 (0-1)
898
+ */
899
+ private similarity(a: string, b: string): number {
900
+ const distance = this.levenshteinDistance(a.toLowerCase(), b.toLowerCase());
901
+ const maxLen = Math.max(a.length, b.length);
902
+ return maxLen === 0 ? 1 : 1 - distance / maxLen;
903
+ }
904
+
905
+ private levenshteinDistance(a: string, b: string): number {
906
+ const matrix: number[][] = [];
907
+ for (let i = 0; i <= b.length; i++) {
908
+ matrix[i] = [i];
909
+ }
910
+ for (let j = 0; j <= a.length; j++) {
911
+ matrix[0][j] = j;
912
+ }
913
+ for (let i = 1; i <= b.length; i++) {
914
+ for (let j = 1; j <= a.length; j++) {
915
+ const cost = a[j - 1] === b[i - 1] ? 0 : 1;
916
+ matrix[i][j] = Math.min(
917
+ matrix[i - 1][j] + 1,
918
+ matrix[i][j - 1] + 1,
919
+ matrix[i - 1][j - 1] + cost
920
+ );
921
+ }
922
+ }
923
+ return matrix[b.length][a.length];
924
+ }
925
+ }
926
+ ```
927
+
928
+ ### 1.7 Graduation Pipeline 구현 (src/core/graduation.ts) - AXIOMMIND L0→L4
929
+
930
+ ```typescript
931
+ /**
932
+ * AXIOMMIND Memory Graduation Pipeline
933
+ * L0(Raw) → L1(Structured) → L2(Candidates) → L3(Verified) → L4(Active)
934
+ */
935
+
936
+ import { EventStore } from './event-store';
937
+ import { VectorStore } from './vector-store';
938
+ import { VectorWorker } from './vector-worker';
939
+ import { MemoryEvent } from './types';
940
+
941
+ type MemoryLevel = 'L0' | 'L1' | 'L2' | 'L3' | 'L4';
942
+
943
+ interface GraduationResult {
944
+ eventId: string;
945
+ fromLevel: MemoryLevel;
946
+ toLevel: MemoryLevel;
947
+ success: boolean;
948
+ reason?: string;
949
+ }
950
+
951
+ interface LevelStatus {
952
+ level: MemoryLevel;
953
+ count: number;
954
+ lastUpdated: Date | null;
955
+ }
956
+
957
+ export class GraduationPipeline {
958
+ constructor(
959
+ private eventStore: EventStore,
960
+ private vectorStore: VectorStore,
961
+ private vectorWorker: VectorWorker
962
+ ) {}
963
+
964
+ /**
965
+ * L0 → L1: 원본 이벤트에서 구조화된 데이터 추출
966
+ * (LLM 기반 추출은 별도 서비스에서 처리)
967
+ */
968
+ async promoteToL1(eventId: string): Promise<GraduationResult> {
969
+ // L1 승격은 LLM 추출 완료 후 마킹
970
+ // 여기서는 메타데이터만 업데이트
971
+ return {
972
+ eventId,
973
+ fromLevel: 'L0',
974
+ toLevel: 'L1',
975
+ success: true
976
+ };
977
+ }
978
+
979
+ /**
980
+ * L1 → L2: 타입 검증 대상으로 승격
981
+ * TypeScript 타입 체크 통과 여부 확인
982
+ */
983
+ async promoteToL2(eventId: string, structuredData: unknown): Promise<GraduationResult> {
984
+ try {
985
+ // Zod 스키마로 검증
986
+ // const validated = SomeSchema.parse(structuredData);
987
+
988
+ return {
989
+ eventId,
990
+ fromLevel: 'L1',
991
+ toLevel: 'L2',
992
+ success: true
993
+ };
994
+ } catch (error) {
995
+ return {
996
+ eventId,
997
+ fromLevel: 'L1',
998
+ toLevel: 'L2',
999
+ success: false,
1000
+ reason: `Validation failed: ${error}`
1001
+ };
1002
+ }
1003
+ }
1004
+
1005
+ /**
1006
+ * L2 → L3: 검증 완료 (모순 없음 확인)
1007
+ */
1008
+ async promoteToL3(eventId: string): Promise<GraduationResult> {
1009
+ // 기존 지식과의 모순 체크
1010
+ // 중복 체크
1011
+ // 신뢰도 계산
1012
+
1013
+ return {
1014
+ eventId,
1015
+ fromLevel: 'L2',
1016
+ toLevel: 'L3',
1017
+ success: true
1018
+ };
1019
+ }
1020
+
1021
+ /**
1022
+ * L3 → L4: 검색 가능 상태로 승격 (벡터 인덱싱)
1023
+ */
1024
+ async promoteToL4(eventId: string, content: string): Promise<GraduationResult> {
1025
+ try {
1026
+ // VectorWorker의 outbox에 추가 (비동기 인덱싱)
1027
+ await this.vectorWorker.enqueue(eventId, content);
1028
+
1029
+ return {
1030
+ eventId,
1031
+ fromLevel: 'L3',
1032
+ toLevel: 'L4',
1033
+ success: true
1034
+ };
1035
+ } catch (error) {
1036
+ return {
1037
+ eventId,
1038
+ fromLevel: 'L3',
1039
+ toLevel: 'L4',
1040
+ success: false,
1041
+ reason: `Indexing failed: ${error}`
1042
+ };
1043
+ }
1044
+ }
1045
+
1046
+ /**
1047
+ * 전체 파이프라인 실행 (L0 → L4)
1048
+ */
1049
+ async graduateFull(event: MemoryEvent): Promise<GraduationResult[]> {
1050
+ const results: GraduationResult[] = [];
1051
+
1052
+ // L0 → L1 (구조화)
1053
+ const l1Result = await this.promoteToL1(event.id);
1054
+ results.push(l1Result);
1055
+ if (!l1Result.success) return results;
1056
+
1057
+ // L1 → L2 (타입 검증)
1058
+ const l2Result = await this.promoteToL2(event.id, event);
1059
+ results.push(l2Result);
1060
+ if (!l2Result.success) return results;
1061
+
1062
+ // L2 → L3 (모순 체크)
1063
+ const l3Result = await this.promoteToL3(event.id);
1064
+ results.push(l3Result);
1065
+ if (!l3Result.success) return results;
1066
+
1067
+ // L3 → L4 (인덱싱)
1068
+ const l4Result = await this.promoteToL4(event.id, event.content);
1069
+ results.push(l4Result);
1070
+
1071
+ return results;
1072
+ }
1073
+
1074
+ /**
1075
+ * 각 레벨별 통계 조회
1076
+ */
1077
+ async getLevelStats(): Promise<LevelStatus[]> {
1078
+ // 실제 구현에서는 DB 쿼리
1079
+ return [
1080
+ { level: 'L0', count: 0, lastUpdated: null },
1081
+ { level: 'L1', count: 0, lastUpdated: null },
1082
+ { level: 'L2', count: 0, lastUpdated: null },
1083
+ { level: 'L3', count: 0, lastUpdated: null },
1084
+ { level: 'L4', count: 0, lastUpdated: null }
1085
+ ];
1086
+ }
1087
+ }
1088
+ ```
1089
+
1090
+ ---
1091
+
1092
+ ## Phase 2: Embedding & Retrieval
1093
+
1094
+ ### 2.1 Embedder 구현 (src/core/embedder.ts)
1095
+
1096
+ ```typescript
1097
+ import { pipeline, Pipeline } from '@xenova/transformers';
1098
+
1099
+ export class Embedder {
1100
+ private model: Pipeline | null = null;
1101
+ private modelName: string;
1102
+
1103
+ constructor(modelName: string = 'Xenova/all-MiniLM-L6-v2') {
1104
+ this.modelName = modelName;
1105
+ }
1106
+
1107
+ async initialize(): Promise<void> {
1108
+ this.model = await pipeline('feature-extraction', this.modelName);
1109
+ }
1110
+
1111
+ async embed(text: string): Promise<number[]> {
1112
+ if (!this.model) {
1113
+ await this.initialize();
1114
+ }
1115
+
1116
+ const result = await this.model!(text, {
1117
+ pooling: 'mean',
1118
+ normalize: true
1119
+ });
1120
+
1121
+ return Array.from(result.data);
1122
+ }
1123
+
1124
+ async embedBatch(texts: string[]): Promise<number[][]> {
1125
+ return Promise.all(texts.map(t => this.embed(t)));
1126
+ }
1127
+ }
1128
+ ```
1129
+
1130
+ ### 2.2 Retriever 구현 (src/core/retriever.ts)
1131
+
1132
+ ```typescript
1133
+ import { Embedder } from './embedder';
1134
+ import { VectorStore } from './vector-store';
1135
+ import { EventStore } from './event-store';
1136
+ import { MemoryMatch, Config } from './types';
1137
+
1138
+ export class Retriever {
1139
+ constructor(
1140
+ private embedder: Embedder,
1141
+ private vectorStore: VectorStore,
1142
+ private eventStore: EventStore,
1143
+ private config: Config
1144
+ ) {}
1145
+
1146
+ async search(query: string): Promise<MemoryMatch[]> {
1147
+ // 1. 쿼리 임베딩
1148
+ const queryVector = await this.embedder.embed(query);
1149
+
1150
+ // 2. 벡터 검색
1151
+ const vectorResults = await this.vectorStore.search(queryVector, {
1152
+ limit: this.config.retrieval.topK,
1153
+ minScore: this.config.retrieval.minScore
1154
+ });
1155
+
1156
+ // 3. 이벤트 정보 보강
1157
+ const matches: MemoryMatch[] = [];
1158
+
1159
+ for (const result of vectorResults) {
1160
+ // 원본 이벤트 조회
1161
+ const events = await this.eventStore.getSessionEvents(result.eventId);
1162
+ const event = events.find(e => e.id === result.eventId);
1163
+
1164
+ if (event) {
1165
+ matches.push({
1166
+ event,
1167
+ score: result.score,
1168
+ relevanceReason: `Semantic similarity: ${(result.score * 100).toFixed(1)}%`
1169
+ });
1170
+ }
1171
+ }
1172
+
1173
+ return matches;
1174
+ }
1175
+
1176
+ // 컨텍스트 포맷팅
1177
+ formatContext(matches: MemoryMatch[]): string {
1178
+ if (matches.length === 0) {
1179
+ return '';
1180
+ }
1181
+
1182
+ const lines = ['## Relevant Memories\n'];
1183
+
1184
+ for (const match of matches) {
1185
+ const date = new Date(match.event.timestamp).toLocaleDateString();
1186
+ lines.push(`### ${match.event.eventType} (${date})`);
1187
+ lines.push(`> ${match.event.content.slice(0, 500)}...`);
1188
+ lines.push(`_Relevance: ${(match.score * 100).toFixed(0)}%_\n`);
1189
+ }
1190
+
1191
+ return lines.join('\n');
1192
+ }
1193
+ }
1194
+ ```
1195
+
1196
+ ---
1197
+
1198
+ ## Phase 3: Hook Integration
1199
+
1200
+ ### 3.1 hooks.json 설정
1201
+
1202
+ ```json
1203
+ {
1204
+ "hooks": {
1205
+ "SessionStart": [
1206
+ {
1207
+ "type": "command",
1208
+ "command": "npx code-memory session-start",
1209
+ "timeout": 5000
1210
+ }
1211
+ ],
1212
+ "UserPromptSubmit": [
1213
+ {
1214
+ "type": "command",
1215
+ "command": "npx code-memory search --stdin",
1216
+ "timeout": 3000
1217
+ }
1218
+ ],
1219
+ "Stop": [
1220
+ {
1221
+ "type": "command",
1222
+ "command": "npx code-memory save --stdin",
1223
+ "timeout": 5000
1224
+ }
1225
+ ],
1226
+ "SessionEnd": [
1227
+ {
1228
+ "type": "command",
1229
+ "command": "npx code-memory session-end --stdin",
1230
+ "timeout": 10000
1231
+ }
1232
+ ]
1233
+ }
1234
+ }
1235
+ ```
1236
+
1237
+ ### 3.2 UserPromptSubmit Hook (src/hooks/user-prompt-submit.ts)
1238
+
1239
+ ```typescript
1240
+ import { Retriever } from '../core/retriever';
1241
+ import { loadServices } from '../core/services';
1242
+
1243
+ export async function handleUserPromptSubmit(input: {
1244
+ session_id: string;
1245
+ prompt: string;
1246
+ }): Promise<{ context?: string }> {
1247
+ const { retriever } = await loadServices();
1248
+
1249
+ // 관련 기억 검색
1250
+ const matches = await retriever.search(input.prompt);
1251
+
1252
+ if (matches.length === 0) {
1253
+ return {};
1254
+ }
1255
+
1256
+ // 컨텍스트 포맷팅
1257
+ const context = retriever.formatContext(matches);
1258
+
1259
+ return { context };
1260
+ }
1261
+
1262
+ // CLI 엔트리포인트
1263
+ if (process.stdin.isTTY === false) {
1264
+ let data = '';
1265
+ process.stdin.on('data', chunk => data += chunk);
1266
+ process.stdin.on('end', async () => {
1267
+ const input = JSON.parse(data);
1268
+ const result = await handleUserPromptSubmit(input);
1269
+ console.log(JSON.stringify(result));
1270
+ });
1271
+ }
1272
+ ```
1273
+
1274
+ ### 3.3 Stop Hook (src/hooks/stop.ts)
1275
+
1276
+ ```typescript
1277
+ import { EventStore } from '../core/event-store';
1278
+ import { VectorStore } from '../core/vector-store';
1279
+ import { Embedder } from '../core/embedder';
1280
+ import { loadServices } from '../core/services';
1281
+
1282
+ export async function handleStop(input: {
1283
+ session_id: string;
1284
+ messages: Array<{ role: string; content: string }>;
1285
+ }): Promise<void> {
1286
+ const { eventStore, vectorStore, embedder } = await loadServices();
1287
+
1288
+ // 마지막 user-assistant 쌍 저장
1289
+ const messages = input.messages.slice(-2);
1290
+
1291
+ for (const msg of messages) {
1292
+ const eventType = msg.role === 'user' ? 'user_prompt' : 'agent_response';
1293
+
1294
+ // 1. 이벤트 저장
1295
+ const result = await eventStore.append({
1296
+ eventType,
1297
+ sessionId: input.session_id,
1298
+ timestamp: new Date(),
1299
+ content: msg.content,
1300
+ metadata: {}
1301
+ });
1302
+
1303
+ // 2. 임베딩 생성 및 벡터 저장 (중복 아닌 경우)
1304
+ if (result.success && !result.isDuplicate) {
1305
+ const vector = await embedder.embed(msg.content);
1306
+
1307
+ await vectorStore.upsert({
1308
+ id: crypto.randomUUID(),
1309
+ eventId: result.eventId!,
1310
+ sessionId: input.session_id,
1311
+ eventType,
1312
+ content: msg.content.slice(0, 1000), // 미리보기용
1313
+ vector,
1314
+ timestamp: new Date().toISOString()
1315
+ });
1316
+ }
1317
+ }
1318
+ }
1319
+ ```
1320
+
1321
+ ---
1322
+
1323
+ ## Phase 4: Commands & CLI
1324
+
1325
+ ### 4.1 Search Command (commands/search.md)
1326
+
1327
+ ```markdown
1328
+ ---
1329
+ description: Search through your conversation memory
1330
+ ---
1331
+
1332
+ # Memory Search
1333
+
1334
+ Search for relevant memories based on your query.
1335
+
1336
+ ## Usage
1337
+
1338
+ The user wants to search their conversation memory for: "$ARGUMENTS"
1339
+
1340
+ Search the memory database and return the most relevant past conversations, code snippets, and insights related to the query.
1341
+
1342
+ Display the results in a clear format showing:
1343
+ 1. The date of the memory
1344
+ 2. A brief excerpt of the content
1345
+ 3. The relevance score
1346
+
1347
+ If no relevant memories are found, inform the user and suggest they can build up their memory by having more conversations.
1348
+ ```
1349
+
1350
+ ### 4.2 CLI Entry Point (src/cli/index.ts)
1351
+
1352
+ ```typescript
1353
+ import { Command } from 'commander';
1354
+ import { handleSessionStart } from './commands/session-start';
1355
+ import { handleSearch } from './commands/search';
1356
+ import { handleSave } from './commands/save';
1357
+ import { handleSessionEnd } from './commands/session-end';
1358
+ import { handleInit } from './commands/init';
1359
+
1360
+ const program = new Command();
1361
+
1362
+ program
1363
+ .name('code-memory')
1364
+ .description('Claude Code Memory Plugin CLI')
1365
+ .version('1.0.0');
1366
+
1367
+ program
1368
+ .command('init')
1369
+ .description('Initialize the memory database')
1370
+ .action(handleInit);
1371
+
1372
+ program
1373
+ .command('session-start')
1374
+ .description('Handle session start')
1375
+ .option('--session-id <id>', 'Session ID')
1376
+ .option('--cwd <path>', 'Current working directory')
1377
+ .action(handleSessionStart);
1378
+
1379
+ program
1380
+ .command('search')
1381
+ .description('Search memories')
1382
+ .option('--query <text>', 'Search query')
1383
+ .option('--stdin', 'Read from stdin')
1384
+ .option('--limit <n>', 'Max results', '5')
1385
+ .action(handleSearch);
1386
+
1387
+ program
1388
+ .command('save')
1389
+ .description('Save conversation')
1390
+ .option('--stdin', 'Read from stdin')
1391
+ .action(handleSave);
1392
+
1393
+ program
1394
+ .command('session-end')
1395
+ .description('Handle session end')
1396
+ .option('--stdin', 'Read from stdin')
1397
+ .action(handleSessionEnd);
1398
+
1399
+ program.parse();
1400
+ ```
1401
+
1402
+ ---
1403
+
1404
+ ## Phase 5: Testing & Polish
1405
+
1406
+ ### 5.1 테스트 케이스 (tests/event-store.test.ts)
1407
+
1408
+ ```typescript
1409
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
1410
+ import { EventStore } from '../src/core/event-store';
1411
+ import { unlink } from 'fs/promises';
1412
+
1413
+ describe('EventStore', () => {
1414
+ let store: EventStore;
1415
+ const testDbPath = './test-events.db';
1416
+
1417
+ beforeEach(() => {
1418
+ store = new EventStore(testDbPath);
1419
+ });
1420
+
1421
+ afterEach(async () => {
1422
+ await unlink(testDbPath).catch(() => {});
1423
+ });
1424
+
1425
+ it('should append events with deduplication', async () => {
1426
+ const event = {
1427
+ eventType: 'user_prompt' as const,
1428
+ sessionId: 'test-session',
1429
+ timestamp: new Date(),
1430
+ content: 'Hello, how are you?',
1431
+ metadata: {}
1432
+ };
1433
+
1434
+ // 첫 번째 저장
1435
+ const result1 = await store.append(event);
1436
+ expect(result1.success).toBe(true);
1437
+ expect(result1.isDuplicate).toBe(false);
1438
+
1439
+ // 중복 저장 시도
1440
+ const result2 = await store.append(event);
1441
+ expect(result2.success).toBe(true);
1442
+ expect(result2.isDuplicate).toBe(true);
1443
+ expect(result2.eventId).toBe(result1.eventId);
1444
+ });
1445
+
1446
+ it('should retrieve events by session', async () => {
1447
+ const sessionId = 'test-session';
1448
+
1449
+ await store.append({
1450
+ eventType: 'user_prompt',
1451
+ sessionId,
1452
+ timestamp: new Date(),
1453
+ content: 'First message',
1454
+ metadata: {}
1455
+ });
1456
+
1457
+ await store.append({
1458
+ eventType: 'agent_response',
1459
+ sessionId,
1460
+ timestamp: new Date(),
1461
+ content: 'First response',
1462
+ metadata: {}
1463
+ });
1464
+
1465
+ const events = await store.getSessionEvents(sessionId);
1466
+ expect(events.length).toBe(2);
1467
+ });
1468
+ });
1469
+ ```
1470
+
1471
+ ### 5.2 통합 테스트 (tests/integration.test.ts)
1472
+
1473
+ ```typescript
1474
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
1475
+ import { EventStore } from '../src/core/event-store';
1476
+ import { VectorStore } from '../src/core/vector-store';
1477
+ import { Embedder } from '../src/core/embedder';
1478
+ import { Retriever } from '../src/core/retriever';
1479
+
1480
+ describe('Integration: Memory Retrieval', () => {
1481
+ let eventStore: EventStore;
1482
+ let vectorStore: VectorStore;
1483
+ let embedder: Embedder;
1484
+ let retriever: Retriever;
1485
+
1486
+ beforeAll(async () => {
1487
+ eventStore = new EventStore('./test-integration.db');
1488
+ vectorStore = new VectorStore('./test-integration-vectors');
1489
+ embedder = new Embedder();
1490
+
1491
+ await vectorStore.initialize();
1492
+ await embedder.initialize();
1493
+
1494
+ retriever = new Retriever(eventStore, vectorStore, embedder, {
1495
+ retrieval: { topK: 5, minScore: 0.5, maxTokens: 2000 }
1496
+ });
1497
+
1498
+ // 테스트 데이터 삽입
1499
+ const testData = [
1500
+ { content: 'How to implement rate limiting in Express?', type: 'user_prompt' },
1501
+ { content: 'You can use express-rate-limit middleware...', type: 'agent_response' },
1502
+ { content: 'How to add authentication to my API?', type: 'user_prompt' },
1503
+ { content: 'Use Passport.js or JWT for authentication...', type: 'agent_response' }
1504
+ ];
1505
+
1506
+ for (const data of testData) {
1507
+ const result = await eventStore.append({
1508
+ eventType: data.type as any,
1509
+ sessionId: 'test-session',
1510
+ timestamp: new Date(),
1511
+ content: data.content,
1512
+ metadata: {}
1513
+ });
1514
+
1515
+ const vector = await embedder.embed(data.content);
1516
+ await vectorStore.upsert({
1517
+ id: crypto.randomUUID(),
1518
+ eventId: result.eventId!,
1519
+ sessionId: 'test-session',
1520
+ eventType: data.type,
1521
+ content: data.content,
1522
+ vector,
1523
+ timestamp: new Date().toISOString()
1524
+ });
1525
+ }
1526
+ });
1527
+
1528
+ it('should find relevant memories', async () => {
1529
+ const matches = await retriever.search('rate limiting');
1530
+
1531
+ expect(matches.length).toBeGreaterThan(0);
1532
+ expect(matches[0].event.content).toContain('rate limiting');
1533
+ });
1534
+
1535
+ it('should format context correctly', () => {
1536
+ const matches = [
1537
+ {
1538
+ event: {
1539
+ id: '1',
1540
+ eventType: 'user_prompt',
1541
+ sessionId: 'test',
1542
+ timestamp: new Date(),
1543
+ content: 'Test content',
1544
+ contentHash: 'hash'
1545
+ },
1546
+ score: 0.95,
1547
+ relevanceReason: 'High similarity'
1548
+ }
1549
+ ];
1550
+
1551
+ const context = retriever.formatContext(matches);
1552
+ expect(context).toContain('Relevant Memories');
1553
+ expect(context).toContain('Test content');
1554
+ });
1555
+ });
1556
+ ```
1557
+
1558
+ ### 5.3 빌드 스크립트 (scripts/build.ts)
1559
+
1560
+ ```typescript
1561
+ import * as esbuild from 'esbuild';
1562
+
1563
+ async function build() {
1564
+ // CLI 빌드
1565
+ await esbuild.build({
1566
+ entryPoints: ['src/cli/index.ts'],
1567
+ bundle: true,
1568
+ platform: 'node',
1569
+ target: 'node18',
1570
+ outfile: 'dist/cli.js',
1571
+ external: ['duckdb', 'lancedb', '@xenova/transformers']
1572
+ });
1573
+
1574
+ console.log('Build complete!');
1575
+ }
1576
+
1577
+ build().catch(console.error);
1578
+ ```
1579
+
1580
+ ---
1581
+
1582
+ ## 마일스톤 체크리스트
1583
+
1584
+ ### Phase 0: 프로젝트 설정
1585
+ - [ ] 디렉토리 구조 생성
1586
+ - [ ] package.json 초기화
1587
+ - [ ] 의존성 설치
1588
+ - [ ] TypeScript 설정
1589
+ - [ ] plugin.json 생성
1590
+
1591
+ ### Phase 1: Core Storage Layer
1592
+ - [ ] types.ts - Zod 스키마 정의
1593
+ - [ ] event-store.ts - DuckDB 연동
1594
+ - [ ] vector-store.ts - LanceDB 연동
1595
+ - [ ] 단위 테스트
1596
+
1597
+ ### Phase 2: Embedding & Retrieval
1598
+ - [ ] embedder.ts - 로컬 임베딩
1599
+ - [ ] retriever.ts - 검색 로직
1600
+ - [ ] 컨텍스트 포맷터
1601
+ - [ ] 단위 테스트
1602
+
1603
+ ### Phase 3: Hook Integration
1604
+ - [ ] hooks.json 설정
1605
+ - [ ] session-start hook
1606
+ - [ ] user-prompt-submit hook
1607
+ - [ ] stop hook
1608
+ - [ ] session-end hook
1609
+
1610
+ ### Phase 4: Commands & CLI
1611
+ - [ ] CLI 엔트리포인트
1612
+ - [ ] search 명령어
1613
+ - [ ] history 명령어
1614
+ - [ ] forget 명령어
1615
+ - [ ] stats 명령어
1616
+
1617
+ ### Phase 5: Testing & Polish
1618
+ - [ ] 통합 테스트
1619
+ - [ ] README.md
1620
+ - [ ] 에러 처리 개선
1621
+ - [ ] 성능 최적화
1622
+ - [ ] 첫 릴리스 준비
1623
+
1624
+ ---
1625
+
1626
+ ## 리스크 및 대응
1627
+
1628
+ | 리스크 | 영향 | 대응 |
1629
+ |--------|------|------|
1630
+ | 임베딩 모델 로드 시간 | 세션 시작 지연 | 모델 캐싱, 지연 로드 |
1631
+ | 대용량 대화 처리 | 메모리 부족 | 스트리밍, 배치 처리 |
1632
+ | DuckDB/LanceDB 호환성 | 설치 실패 | fallback 구현, 순수 JS 대안 |
1633
+ | Hook 타임아웃 | 기능 미작동 | 비동기 처리, 캐싱 |
1634
+
1635
+ ---
1636
+
1637
+ ## 다음 단계
1638
+
1639
+ 1. **Phase 0 완료 후**: 기본 플러그인 구조 동작 확인
1640
+ 2. **Phase 1-2 완료 후**: 기억 저장/검색 기능 데모
1641
+ 3. **Phase 3-4 완료 후**: 실제 Claude Code에서 테스트
1642
+ 4. **Phase 5 완료 후**: 커뮤니티 마켓플레이스 등록