claude-memory-layer 1.0.11 → 1.0.13
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/AGENTS.md +60 -0
- package/README.md +166 -2
- package/bootstrap-kb/decisions/decisions.md +244 -0
- package/bootstrap-kb/glossary/glossary.md +46 -0
- package/bootstrap-kb/modules/.claude-plugin.md +22 -0
- package/bootstrap-kb/modules/agents.md.md +15 -0
- package/bootstrap-kb/modules/claude.md.md +15 -0
- package/bootstrap-kb/modules/context.md.md +15 -0
- package/bootstrap-kb/modules/docs.md +18 -0
- package/bootstrap-kb/modules/handoff.md.md +15 -0
- package/bootstrap-kb/modules/package-lock.json.md +15 -0
- package/bootstrap-kb/modules/package.json.md +15 -0
- package/bootstrap-kb/modules/plan.md.md +15 -0
- package/bootstrap-kb/modules/readme.md.md +15 -0
- package/bootstrap-kb/modules/scripts.md +26 -0
- package/bootstrap-kb/modules/spec.md.md +15 -0
- package/bootstrap-kb/modules/specs.md +20 -0
- package/bootstrap-kb/modules/src.md +51 -0
- package/bootstrap-kb/modules/tests.md +42 -0
- package/bootstrap-kb/modules/tsconfig.json.md +15 -0
- package/bootstrap-kb/modules/vitest.config.ts.md +15 -0
- package/bootstrap-kb/overview/overview.md +40 -0
- package/bootstrap-kb/sources/manifest.json +950 -0
- package/bootstrap-kb/sources/manifest.md +227 -0
- package/bootstrap-kb/timeline/timeline.md +57 -0
- package/d.sh +3 -0
- package/deploy.sh +3 -0
- package/dist/cli/index.js +2389 -286
- package/dist/cli/index.js.map +4 -4
- package/dist/core/index.js +1017 -132
- package/dist/core/index.js.map +4 -4
- package/dist/hooks/post-tool-use.js +1347 -202
- package/dist/hooks/post-tool-use.js.map +4 -4
- package/dist/hooks/session-end.js +1339 -194
- package/dist/hooks/session-end.js.map +4 -4
- package/dist/hooks/session-start.js +1343 -198
- package/dist/hooks/session-start.js.map +4 -4
- package/dist/hooks/stop.js +1351 -206
- package/dist/hooks/stop.js.map +4 -4
- package/dist/hooks/user-prompt-submit.js +1347 -202
- package/dist/hooks/user-prompt-submit.js.map +4 -4
- package/dist/server/api/index.js +1436 -211
- package/dist/server/api/index.js.map +4 -4
- package/dist/server/index.js +1445 -220
- package/dist/server/index.js.map +4 -4
- package/dist/services/memory-service.js +1345 -199
- package/dist/services/memory-service.js.map +4 -4
- package/dist/ui/app.js +69 -2
- package/dist/ui/index.html +8 -0
- package/docs/MCP_MEMORY_SERVICE_COMPARATIVE_REVIEW.md +271 -0
- package/docs/MEMU_ADOPTION.md +40 -0
- package/memory/.claude-plugin/commands/2026-02-25.md +263 -0
- package/memory/_index.md +405 -0
- package/memory/default/uncategorized/2026-02-25.md +4839 -0
- package/memory/specs/20260207-dashboard-upgrade/2026-02-25.md +142 -0
- package/memory/specs/citations-system/2026-02-25.md +1121 -0
- package/memory/specs/endless-mode/2026-02-25.md +1392 -0
- package/memory/specs/entity-edge-model/2026-02-25.md +1263 -0
- package/memory/specs/evidence-aligner-v2/2026-02-25.md +1028 -0
- package/memory/specs/mcp-desktop-integration/2026-02-25.md +1334 -0
- package/memory/specs/post-tool-use-hook/2026-02-25.md +1164 -0
- package/memory/specs/private-tags/2026-02-25.md +1057 -0
- package/memory/specs/progressive-disclosure/2026-02-25.md +1436 -0
- package/memory/specs/task-entity-system/2026-02-25.md +924 -0
- package/memory/specs/vector-outbox-v2/2026-02-25.md +1510 -0
- package/memory/specs/web-viewer-ui/2026-02-25.md +1709 -0
- package/package.json +2 -1
- package/scripts/build.ts +6 -0
- package/scripts/bump-patch-version.sh +18 -0
- package/src/cli/index.ts +281 -2
- package/src/core/consolidated-store.ts +63 -1
- package/src/core/consolidation-worker.ts +115 -6
- package/src/core/event-store.ts +14 -0
- package/src/core/index.ts +1 -0
- package/src/core/ingest-interceptor.ts +80 -0
- package/src/core/markdown-mirror.ts +70 -0
- package/src/core/md-mirror.ts +92 -0
- package/src/core/mongo-sync-config.ts +165 -0
- package/src/core/mongo-sync-worker.ts +381 -0
- package/src/core/retriever.ts +540 -150
- package/src/core/sqlite-event-store.ts +350 -1
- package/src/core/tag-taxonomy.ts +51 -0
- package/src/core/types.ts +28 -0
- package/src/server/api/health.ts +53 -0
- package/src/server/api/index.ts +3 -1
- package/src/server/api/stats.ts +46 -1
- package/src/services/bootstrap-organizer.ts +443 -0
- package/src/services/codex-session-history-importer.ts +474 -0
- package/src/services/memory-service.ts +373 -68
- package/src/services/session-history-importer.ts +53 -25
- package/src/ui/app.js +69 -2
- package/src/ui/index.html +8 -0
- package/tests/bootstrap-organizer.test.ts +111 -0
- package/tests/consolidation-worker.test.ts +75 -0
- package/tests/ingest-interceptor.test.ts +38 -0
- package/tests/markdown-mirror.test.ts +85 -0
- package/tests/md-mirror.test.ts +50 -0
- package/tests/retriever-fallback-chain.test.ts +223 -0
- package/tests/retriever-strategy-scope.test.ts +97 -0
- package/tests/retriever.memu-adoption.test.ts +122 -0
- package/tests/sqlite-event-store-replication.test.ts +92 -0
|
@@ -0,0 +1,1709 @@
|
|
|
1
|
+
|
|
2
|
+
## 2026-02-25T12:31:26.464Z | 259d5c09-2d91-48f7-87a1-e94a0851ba2b
|
|
3
|
+
- type: session_summary
|
|
4
|
+
- session: import:organized
|
|
5
|
+
# Web Viewer UI Context
|
|
6
|
+
|
|
7
|
+
> **Version**: 1.0.0
|
|
8
|
+
> **Created**: 2026-02-01
|
|
9
|
+
|
|
10
|
+
## 1. 배경
|
|
11
|
+
|
|
12
|
+
### 1.1 claude-mem의 접근 방식
|
|
13
|
+
|
|
14
|
+
claude-mem은 Web Viewer를 핵심 기능으로 제공:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
http://localhost:37777
|
|
18
|
+
├── 실시간 메모리 스트림
|
|
19
|
+
├── 세션별 탐색
|
|
20
|
+
├── 검색 인터페이스
|
|
21
|
+
├── 설정 관리
|
|
22
|
+
└── Beta 기능 토글
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**주요 특징**:
|
|
26
|
+
- Bun 기반 HTTP 서버
|
|
27
|
+
- 10개의 검색 엔드포인트
|
|
28
|
+
- 실시간 observation 스트림
|
|
29
|
+
- Settings 페이지에서 버전 전환
|
|
30
|
+
|
|
31
|
+
**장점**:
|
|
32
|
+
- CLI 한계 극복 (대량 데이터 시각화)
|
|
33
|
+
- 디버깅 용이
|
|
34
|
+
- 비개발자도 접근 가능
|
|
35
|
+
|
|
36
|
+
### 1.2 현재 code-memory의 상황
|
|
37
|
+
|
|
38
|
+
현재 CLI만 제공:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# 현재 지원 명령어
|
|
42
|
+
code-memory search "query"
|
|
43
|
+
code-memory history
|
|
44
|
+
code-memory stats
|
|
45
|
+
code-memory forget
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**한계점**:
|
|
49
|
+
1. 대량 결과 탐색 불편
|
|
50
|
+
2. 실시간 모니터링 불가
|
|
51
|
+
3. 시각적 통계 없음
|
|
52
|
+
4. 복잡한 필터링 어려움
|
|
53
|
+
|
|
54
|
+
### 1.3 Web UI 도입 필요성
|
|
55
|
+
|
|
56
|
+
| CLI | Web UI |
|
|
57
|
+
|-----|--------|
|
|
58
|
+
| 텍스트만 | 시각화 가능 |
|
|
59
|
+
| 동기 실행 | 실시간 업데이트 |
|
|
60
|
+
| 단일 작업 | 여러 뷰 동시 |
|
|
61
|
+
| 복잡한 필터링 | 직관적 UI |
|
|
62
|
+
|
|
63
|
+
## 2. 기술 선택 이유
|
|
64
|
+
|
|
65
|
+
### 2.1 Bun 서버
|
|
66
|
+
|
|
67
|
+
**선택 이유**:
|
|
68
|
+
- Node.js 대비 3-4배 빠른 성능
|
|
69
|
+
- 내장 WebSocket 지원
|
|
70
|
+
- TypeScript 직접 실행
|
|
71
|
+
- 번들러 내장
|
|
72
|
+
|
|
73
|
+
**대안 비교**:
|
|
74
|
+
|
|
75
|
+
| 옵션 | 장점 | 단점 |
|
|
76
|
+
|------|------|------|
|
|
77
|
+
| Express | 생태계 | 느림, 설정 복잡 |
|
|
78
|
+
| Fastify | 빠름 | 설정 복잡 |
|
|
79
|
+
| **Bun.serve** | 최고 성능, 간단 | 생태계 작음 |
|
|
80
|
+
|
|
81
|
+
### 2.2 Hono 프레임워크
|
|
82
|
+
|
|
83
|
+
**선택 이유**:
|
|
84
|
+
- 초경량 (12KB)
|
|
85
|
+
- Bun 최적화
|
|
86
|
+
- Express 유사 API
|
|
87
|
+
- 미들웨어 생태계
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// Hono 사용 예시
|
|
91
|
+
import { Hono } from 'hono';
|
|
92
|
+
|
|
93
|
+
const app = new Hono();
|
|
94
|
+
app.get('/api/sessions', (c) => c.json(sessions));
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 2.3 Preact + HTM
|
|
98
|
+
|
|
99
|
+
**선택 이유**:
|
|
100
|
+
- React 호환 API
|
|
101
|
+
- 3KB (React 45KB)
|
|
102
|
+
- JSX 없이 사용 가능
|
|
103
|
+
- 빌드 선택적
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// HTM 사용 (빌드 불필요)
|
|
107
|
+
import { html } from 'htm/preact';
|
|
108
|
+
|
|
109
|
+
function App() {
|
|
110
|
+
return html`<div class="container">Hello</div>`;
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**대안 비교**:
|
|
115
|
+
|
|
116
|
+
| 옵션 | 번들 크기 | 빌드 필요 |
|
|
117
|
+
|------|----------|----------|
|
|
118
|
+
| React | 45KB | 필수 |
|
|
119
|
+
| Vue 3 | 33KB | 권장 |
|
|
120
|
+
| Svelte | 2KB | 필수 |
|
|
121
|
+
| **Preact** | 3KB | 선택 |
|
|
122
|
+
|
|
123
|
+
### 2.4 Tailwind CSS
|
|
124
|
+
|
|
125
|
+
**선택 이유**:
|
|
126
|
+
- 빠른 개발
|
|
127
|
+
- 번들 크기 최적화 (JIT)
|
|
128
|
+
- 다크 테마 기본 지원
|
|
129
|
+
|
|
130
|
+
```html
|
|
131
|
+
<!-- CDN으로 즉시 사용 가능 -->
|
|
132
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## 3. 기존 코드와의 관계
|
|
136
|
+
|
|
137
|
+
### 3.1 MemoryService
|
|
138
|
+
|
|
139
|
+
Web 서버가 사용할 서비스 메서드:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// 현재 MemoryService
|
|
143
|
+
export class MemoryService {
|
|
144
|
+
// 이미 있는 것
|
|
145
|
+
async search(query: string): Promise<SearchResult[]>;
|
|
146
|
+
async getStats(): Promise<Stats>;
|
|
147
|
+
|
|
148
|
+
// 추가 필요
|
|
149
|
+
async getSessions(options: PageOptions): Promise<PaginatedResult<Session>>;
|
|
150
|
+
async getSessionById(id: string): Promise<Session | null>;
|
|
151
|
+
async getEventsBySession(sessionId: string): Promise<Event[]>;
|
|
152
|
+
async getEventById(id: string): Promise<Event | null>;
|
|
153
|
+
async getActivityTimeline(days: number): Promise<DailyStats[]>;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 3.2 EventStore
|
|
158
|
+
|
|
159
|
+
WebSocket 브로드캐스트를 위한 이벤트 훅:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// 현재 EventStore.append()
|
|
163
|
+
async append(event: EventInput): Promise<string> {
|
|
164
|
+
const eventId = await this.db.insert(event);
|
|
165
|
+
// WebSocket 브로드캐스트 추가 필요
|
|
166
|
+
return eventId;
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 3.3 VectorWorker
|
|
171
|
+
|
|
172
|
+
Outbox 상태 모니터링:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// VectorWorker에 상태 노출
|
|
176
|
+
export class VectorWorker {
|
|
177
|
+
getStatus(): OutboxStatus {
|
|
178
|
+
return {
|
|
179
|
+
pending: this.pendingCount,
|
|
180
|
+
processing: this.processingIds,
|
|
181
|
+
failed: this.failedIds,
|
|
182
|
+
avgTime: this.avgProcessTime
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## 4. 설계 결정 사항
|
|
189
|
+
|
|
190
|
+
### 4.1 포트 선택 (37777)
|
|
191
|
+
|
|
192
|
+
**claude-mem과 동일**: 충돌 가능성 낮은 포트
|
|
193
|
+
**설정 가능**: 환경 변수로 변경 가능
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
const PORT = process.env.MEMORY_VIEWER_PORT || 37777;
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### 4.2 localhost 전용
|
|
200
|
+
|
|
201
|
+
**보안 고려**:
|
|
202
|
+
- 외부 접근 차단
|
|
203
|
+
- 인증 불필요
|
|
204
|
+
- CORS 제한적 허용
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
Bun.serve({
|
|
208
|
+
hostname: '127.0.0.1', // localhost만
|
|
209
|
+
port: 37777
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### 4.3 자동 시작 vs 수동 시작
|
|
214
|
+
|
|
215
|
+
**자동 시작 선택**:
|
|
216
|
+
- session-start 훅에서 서버 시작
|
|
217
|
+
- 이미 실행 중이면 스킵
|
|
218
|
+
- 사용자 개입 불필요
|
|
219
|
+
|
|
220
|
+
**대안 (수동)**:
|
|
221
|
+
```bash
|
|
222
|
+
code-memory serve # 별도 명령어
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### 4.4 SSR vs CSR
|
|
226
|
+
|
|
227
|
+
**CSR (Client-Side Rendering) 선택**:
|
|
228
|
+
- 서버 복잡도 낮음
|
|
229
|
+
- 정적 파일만 서빙
|
|
230
|
+
- 실시간 업데이트 용이
|
|
231
|
+
|
|
232
|
+
**대안 (SSR)**:
|
|
233
|
+
- 초기 로딩 빠름
|
|
234
|
+
- SEO (불필요)
|
|
235
|
+
- 서버 복잡도 증가
|
|
236
|
+
|
|
237
|
+
## 5. API 설계 원칙
|
|
238
|
+
|
|
239
|
+
### 5.1 RESTful 패턴
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
GET /api/sessions # 목록 조회
|
|
243
|
+
GET /api/sessions/:id # 단일 조회
|
|
244
|
+
GET /api/events # 목록 조회 (필터링)
|
|
245
|
+
GET /api/events/:id # 단일 조회
|
|
246
|
+
POST /api/search # 검색 (body에 쿼리)
|
|
247
|
+
GET /api/stats # 통계
|
|
248
|
+
GET /api/config # 설정 조회
|
|
249
|
+
PATCH /api/config # 설정 수정
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 5.2 응답 형식
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// 성공 응답
|
|
256
|
+
{
|
|
257
|
+
data: T,
|
|
258
|
+
meta?: {
|
|
259
|
+
total: number,
|
|
260
|
+
page: number,
|
|
261
|
+
pageSize: number
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 에러 응답
|
|
266
|
+
{
|
|
267
|
+
error: {
|
|
268
|
+
code: string,
|
|
269
|
+
message: string
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### 5.3 페이지네이션
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// 쿼리 파라미터
|
|
278
|
+
GET /api/sessions?page=1&pageSize=20
|
|
279
|
+
|
|
280
|
+
// 응답
|
|
281
|
+
{
|
|
282
|
+
sessions: [...],
|
|
283
|
+
meta: {
|
|
284
|
+
total: 100,
|
|
285
|
+
page: 1,
|
|
286
|
+
pageSize: 20,
|
|
287
|
+
hasMore: true
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## 6. WebSocket 설계
|
|
293
|
+
|
|
294
|
+
### 6.1 연결 패턴
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
클라이언트 서버
|
|
298
|
+
│ │
|
|
299
|
+
│ Connect ws://... │
|
|
300
|
+
│─────────────────────────▶│
|
|
301
|
+
│ │
|
|
302
|
+
│ { type: 'subscribe', │
|
|
303
|
+
│ channels: ['events'] }│
|
|
304
|
+
│─────────────────────────▶│
|
|
305
|
+
│ │
|
|
306
|
+
│ { channel: 'events', │
|
|
307
|
+
│ data: {...} } │
|
|
308
|
+
│◀─────────────────────────│
|
|
309
|
+
│ │
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### 6.2 채널 설계
|
|
313
|
+
|
|
314
|
+
| 채널 | 용도 | 메시지 빈도 |
|
|
315
|
+
|------|------|------------|
|
|
316
|
+
| events | 새 이벤트 알림 | 높음 |
|
|
317
|
+
| outbox | 임베딩 상태 | 중간 |
|
|
318
|
+
| stats | 통계 업데이트 | 낮음 |
|
|
319
|
+
|
|
320
|
+
### 6.3 필터링
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
// 특정 세션만 구독
|
|
324
|
+
{
|
|
325
|
+
type: 'subscribe',
|
|
326
|
+
channels: ['events'],
|
|
327
|
+
filters: {
|
|
328
|
+
sessionId: 'session_123'
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## 7. 성능 고려사항
|
|
334
|
+
|
|
335
|
+
### 7.1 정적 파일 캐싱
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
app.use('/*', serveStatic({
|
|
339
|
+
root: './dist/ui',
|
|
340
|
+
maxAge: 86400 // 24시간
|
|
341
|
+
}));
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### 7.2 API 응답 압축
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
import { compress } from 'hono/compress';
|
|
348
|
+
app.use('/*', compress());
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### 7.3 WebSocket 메시지 배치
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
// 100ms 내 이벤트 모아서 전송
|
|
355
|
+
const eventBuffer: Event[] = [];
|
|
356
|
+
let flushTimeout: Timer | null = null;
|
|
357
|
+
|
|
358
|
+
function bufferEvent(event: Event) {
|
|
359
|
+
eventBuffer.push(event);
|
|
360
|
+
|
|
361
|
+
if (!flushTimeout) {
|
|
362
|
+
flushTimeout = setTimeout(() => {
|
|
363
|
+
broadcastEvents(eventBuffer);
|
|
364
|
+
eventBuffer.length = 0;
|
|
365
|
+
flushTimeout = null;
|
|
366
|
+
}, 100);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### 7.4 메모리 관리
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
// WebSocket 클라이언트 제한
|
|
375
|
+
const MAX_CLIENTS = 10;
|
|
376
|
+
|
|
377
|
+
if (clients.size >= MAX_CLIENTS) {
|
|
378
|
+
ws.close(1013, 'Too many connections');
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## 8. 참고 자료
|
|
384
|
+
|
|
385
|
+
- **claude-mem README**: Web viewer at localhost:37777
|
|
386
|
+
- **Hono Documentation**: https://hono.dev/
|
|
387
|
+
- **Preact Documentation**: https://preactjs.com/
|
|
388
|
+
- **Bun Documentation**: https://bun.sh/
|
|
389
|
+
|
|
390
|
+
## 2026-02-25T12:31:26.472Z | fd5a58b1-8adc-4b89-a282-93544fc23a52
|
|
391
|
+
- type: session_summary
|
|
392
|
+
- session: import:organized
|
|
393
|
+
# Web Viewer UI Implementation Plan
|
|
394
|
+
|
|
395
|
+
> **Version**: 1.0.0
|
|
396
|
+
> **Status**: Draft
|
|
397
|
+
> **Created**: 2026-02-01
|
|
398
|
+
|
|
399
|
+
## Phase 1: 서버 인프라 (P0)
|
|
400
|
+
|
|
401
|
+
### 1.1 HTTP 서버 설정
|
|
402
|
+
|
|
403
|
+
**파일**: `src/server/index.ts` (신규)
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
import { Hono } from 'hono';
|
|
407
|
+
import { cors } from 'hono/cors';
|
|
408
|
+
import { serveStatic } from 'hono/bun';
|
|
409
|
+
|
|
410
|
+
const app = new Hono();
|
|
411
|
+
|
|
412
|
+
// CORS (개발용)
|
|
413
|
+
app.use('/*', cors());
|
|
414
|
+
|
|
415
|
+
// Static files
|
|
416
|
+
app.use('/*', serveStatic({ root: './dist/ui' }));
|
|
417
|
+
|
|
418
|
+
// API routes
|
|
419
|
+
app.route('/api', apiRouter);
|
|
420
|
+
|
|
421
|
+
export function startServer(port: number = 37777) {
|
|
422
|
+
return Bun.serve({
|
|
423
|
+
hostname: '127.0.0.1',
|
|
424
|
+
port,
|
|
425
|
+
fetch: app.fetch
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
**작업 항목**:
|
|
431
|
+
- [ ] Hono 라우터 설정
|
|
432
|
+
- [ ] Static 파일 서빙
|
|
433
|
+
- [ ] CORS 설정
|
|
434
|
+
- [ ] 에러 핸들링 미들웨어
|
|
435
|
+
|
|
436
|
+
### 1.2 API 라우터
|
|
437
|
+
|
|
438
|
+
**파일**: `src/server/api/index.ts` (신규)
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
import { Hono } from 'hono';
|
|
442
|
+
import { sessionsRouter } from './sessions';
|
|
443
|
+
import { eventsRouter } from './events';
|
|
444
|
+
import { searchRouter } from './search';
|
|
445
|
+
import { statsRouter } from './stats';
|
|
446
|
+
import { configRouter } from './config';
|
|
447
|
+
|
|
448
|
+
export const apiRouter = new Hono()
|
|
449
|
+
.route('/sessions', sessionsRouter)
|
|
450
|
+
.route('/events', eventsRouter)
|
|
451
|
+
.route('/search', searchRouter)
|
|
452
|
+
.route('/stats', statsRouter)
|
|
453
|
+
.route('/config', configRouter);
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
**작업 항목**:
|
|
457
|
+
- [ ] API 라우터 분리 구조
|
|
458
|
+
- [ ] 공통 미들웨어 (로깅, 인증)
|
|
459
|
+
|
|
460
|
+
## Phase 2: REST API 구현 (P0)
|
|
461
|
+
|
|
462
|
+
### 2.1 Sessions API
|
|
463
|
+
|
|
464
|
+
**파일**: `src/server/api/sessions.ts` (신규)
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
import { Hono } from 'hono';
|
|
468
|
+
import { MemoryService } from '../../services/memory-service';
|
|
469
|
+
|
|
470
|
+
export const sessionsRouter = new Hono();
|
|
471
|
+
|
|
472
|
+
// GET /api/sessions
|
|
473
|
+
sessionsRouter.get('/', async (c) => {
|
|
474
|
+
const { page = 1, pageSize = 20 } = c.req.query();
|
|
475
|
+
const memoryService = await MemoryService.getInstance();
|
|
476
|
+
|
|
477
|
+
const sessions = await memoryService.getSessions({
|
|
478
|
+
page: Number(page),
|
|
479
|
+
pageSize: Number(pageSize)
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
return c.json({
|
|
483
|
+
sessions: sessions.items,
|
|
484
|
+
total: sessions.total,
|
|
485
|
+
page: Number(page),
|
|
486
|
+
pageSize: Number(pageSize)
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// GET /api/sessions/:id
|
|
491
|
+
sessionsRouter.get('/:id', async (c) => {
|
|
492
|
+
const { id } = c.req.param();
|
|
493
|
+
const memoryService = await MemoryService.getInstance();
|
|
494
|
+
|
|
495
|
+
const session = await memoryService.getSessionById(id);
|
|
496
|
+
if (!session) {
|
|
497
|
+
return c.json({ error: 'Session not found' }, 404);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const events = await memoryService.getEventsBySession(id);
|
|
501
|
+
const stats = await memoryService.getSessionStats(id);
|
|
502
|
+
|
|
503
|
+
return c.json({ session, events, stats });
|
|
504
|
+
});
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
**작업 항목**:
|
|
508
|
+
- [ ] 세션 목록 조회
|
|
509
|
+
- [ ] 세션 상세 조회
|
|
510
|
+
- [ ] 페이지네이션 구현
|
|
511
|
+
- [ ] 정렬 옵션
|
|
512
|
+
|
|
513
|
+
### 2.2 Events API
|
|
514
|
+
|
|
515
|
+
**파일**: `src/server/api/events.ts` (신규)
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
export const eventsRouter = new Hono();
|
|
519
|
+
|
|
520
|
+
// GET /api/events
|
|
521
|
+
eventsRouter.get('/', async (c) => {
|
|
522
|
+
const { sessionId, type, limit = 100, offset = 0 } = c.req.query();
|
|
523
|
+
const memoryService = await MemoryService.getInstance();
|
|
524
|
+
|
|
525
|
+
const events = await memoryService.getEvents({
|
|
526
|
+
sessionId,
|
|
527
|
+
eventType: type,
|
|
528
|
+
limit: Number(limit),
|
|
529
|
+
offset: Number(offset)
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
return c.json({
|
|
533
|
+
events: events.map(e => ({
|
|
534
|
+
eventId: e.eventId,
|
|
535
|
+
eventType: e.eventType,
|
|
536
|
+
timestamp: e.timestamp,
|
|
537
|
+
sessionId: e.sessionId,
|
|
538
|
+
preview: generatePreview(e.payload, 100)
|
|
539
|
+
})),
|
|
540
|
+
total: events.total
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// GET /api/events/:id
|
|
545
|
+
eventsRouter.get('/:id', async (c) => {
|
|
546
|
+
const { id } = c.req.param();
|
|
547
|
+
const memoryService = await MemoryService.getInstance();
|
|
548
|
+
|
|
549
|
+
const event = await memoryService.getEventById(id);
|
|
550
|
+
if (!event) {
|
|
551
|
+
return c.json({ error: 'Event not found' }, 404);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const related = await memoryService.getRelatedEvents(id);
|
|
555
|
+
|
|
556
|
+
return c.json({ event, related });
|
|
557
|
+
});
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
**작업 항목**:
|
|
561
|
+
- [ ] 이벤트 목록 조회 (필터링)
|
|
562
|
+
- [ ] 이벤트 상세 조회
|
|
563
|
+
- [ ] 미리보기 생성
|
|
564
|
+
- [ ] 관련 이벤트 조회
|
|
565
|
+
|
|
566
|
+
### 2.3 Search API
|
|
567
|
+
|
|
568
|
+
**파일**: `src/server/api/search.ts` (신규)
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
export const searchRouter = new Hono();
|
|
572
|
+
|
|
573
|
+
// POST /api/search
|
|
574
|
+
searchRouter.post('/', async (c) => {
|
|
575
|
+
const body = await c.req.json<SearchRequest>();
|
|
576
|
+
const memoryService = await MemoryService.getInstance();
|
|
577
|
+
|
|
578
|
+
const startTime = Date.now();
|
|
579
|
+
|
|
580
|
+
const results = await memoryService.search(body.query, {
|
|
581
|
+
filters: body.filters,
|
|
582
|
+
topK: body.options?.topK ?? 10,
|
|
583
|
+
minScore: body.options?.minScore ?? 0.7,
|
|
584
|
+
progressive: body.options?.progressive ?? true
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
return c.json({
|
|
588
|
+
results: results.map(r => ({
|
|
589
|
+
id: r.id,
|
|
590
|
+
score: r.score,
|
|
591
|
+
type: r.type,
|
|
592
|
+
timestamp: r.timestamp,
|
|
593
|
+
sessionId: r.sessionId,
|
|
594
|
+
preview: r.preview,
|
|
595
|
+
highlight: highlightMatches(r.content, body.query)
|
|
596
|
+
})),
|
|
597
|
+
meta: {
|
|
598
|
+
totalMatches: results.length,
|
|
599
|
+
searchTime: Date.now() - startTime,
|
|
600
|
+
mode: 'hybrid'
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
**작업 항목**:
|
|
607
|
+
- [ ] 검색 API 구현
|
|
608
|
+
- [ ] 필터링 옵션
|
|
609
|
+
- [ ] 하이라이트 기능
|
|
610
|
+
- [ ] Progressive 모드 지원
|
|
611
|
+
|
|
612
|
+
### 2.4 Stats API
|
|
613
|
+
|
|
614
|
+
**파일**: `src/server/api/stats.ts` (신규)
|
|
615
|
+
|
|
616
|
+
```typescript
|
|
617
|
+
export const statsRouter = new Hono();
|
|
618
|
+
|
|
619
|
+
// GET /api/stats
|
|
620
|
+
statsRouter.get('/', async (c) => {
|
|
621
|
+
const memoryService = await MemoryService.getInstance();
|
|
622
|
+
const stats = await memoryService.getStats();
|
|
623
|
+
|
|
624
|
+
return c.json({
|
|
625
|
+
storage: {
|
|
626
|
+
eventCount: stats.events.count,
|
|
627
|
+
vectorCount: stats.vectors.count,
|
|
628
|
+
dbSizeMB: stats.storage.duckdb / (1024 * 1024),
|
|
629
|
+
vectorSizeMB: stats.storage.lancedb / (1024 * 1024)
|
|
630
|
+
},
|
|
631
|
+
sessions: {
|
|
632
|
+
total: stats.sessions.total,
|
|
633
|
+
active: stats.sessions.active,
|
|
634
|
+
thisWeek: stats.sessions.thisWeek
|
|
635
|
+
},
|
|
636
|
+
embeddings: {
|
|
637
|
+
pending: stats.outbox.pending,
|
|
638
|
+
processed: stats.outbox.processed,
|
|
639
|
+
failed: stats.outbox.failed,
|
|
640
|
+
avgProcessTime: stats.outbox.avgTime
|
|
641
|
+
},
|
|
642
|
+
memory: {
|
|
643
|
+
heapUsed: process.memoryUsage().heapUsed,
|
|
644
|
+
heapTotal: process.memoryUsage().heapTotal
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// GET /api/stats/timeline
|
|
650
|
+
statsRouter.get('/timeline', async (c) => {
|
|
651
|
+
const { days = 7 } = c.req.query();
|
|
652
|
+
const memoryService = await MemoryService.getInstance();
|
|
653
|
+
|
|
654
|
+
const timeline = await memoryService.getActivityTimeline(Number(days));
|
|
655
|
+
|
|
656
|
+
return c.json({ daily: timeline });
|
|
657
|
+
});
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
**작업 항목**:
|
|
661
|
+
- [ ] 전체 통계 조회
|
|
662
|
+
- [ ] 타임라인 통계
|
|
663
|
+
- [ ] 메모리 사용량
|
|
664
|
+
|
|
665
|
+
## Phase 3: WebSocket 구현 (P1)
|
|
666
|
+
|
|
667
|
+
### 3.1 WebSocket 서버
|
|
668
|
+
|
|
669
|
+
**파일**: `src/server/websocket.ts` (신규)
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
import { EventEmitter } from 'events';
|
|
673
|
+
|
|
674
|
+
const eventBus = new EventEmitter();
|
|
675
|
+
|
|
676
|
+
interface WSClient {
|
|
677
|
+
ws: WebSocket;
|
|
678
|
+
subscriptions: Set<string>;
|
|
679
|
+
filters: {
|
|
680
|
+
sessionId?: string;
|
|
681
|
+
eventType?: string[];
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const clients = new Map<string, WSClient>();
|
|
686
|
+
|
|
687
|
+
export function handleWebSocket(ws: WebSocket) {
|
|
688
|
+
const clientId = crypto.randomUUID();
|
|
689
|
+
|
|
690
|
+
clients.set(clientId, {
|
|
691
|
+
ws,
|
|
692
|
+
subscriptions: new Set(),
|
|
693
|
+
filters: {}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
ws.onmessage = (event) => {
|
|
697
|
+
const msg = JSON.parse(event.data);
|
|
698
|
+
|
|
699
|
+
if (msg.type === 'subscribe') {
|
|
700
|
+
const client = clients.get(clientId);
|
|
701
|
+
msg.channels.forEach((ch: string) => client?.subscriptions.add(ch));
|
|
702
|
+
if (msg.filters) {
|
|
703
|
+
client!.filters = msg.filters;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (msg.type === 'unsubscribe') {
|
|
708
|
+
const client = clients.get(clientId);
|
|
709
|
+
msg.channels.forEach((ch: string) => client?.subscriptions.delete(ch));
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
ws.onclose = () => {
|
|
714
|
+
clients.delete(clientId);
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// 이벤트 브로드캐스트
|
|
719
|
+
export function broadcastEvent(channel: string, data: unknown) {
|
|
720
|
+
for (const client of clients.values()) {
|
|
721
|
+
if (client.subscriptions.has(channel)) {
|
|
722
|
+
// 필터 적용
|
|
723
|
+
if (channel === 'events' && client.filters.sessionId) {
|
|
724
|
+
if ((data as any).sessionId !== client.filters.sessionId) {
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
client.ws.send(JSON.stringify({ channel, data }));
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
**작업 항목**:
|
|
736
|
+
- [ ] WebSocket 연결 관리
|
|
737
|
+
- [ ] 구독/구독취소 처리
|
|
738
|
+
- [ ] 필터링 적용
|
|
739
|
+
- [ ] 브로드캐스트 함수
|
|
740
|
+
|
|
741
|
+
### 3.2 이벤트 연동
|
|
742
|
+
|
|
743
|
+
**파일**: `src/services/memory-service.ts` 수정
|
|
744
|
+
|
|
745
|
+
```typescript
|
|
746
|
+
import { broadcastEvent } from '../server/websocket';
|
|
747
|
+
|
|
748
|
+
export class MemoryService {
|
|
749
|
+
async storeEvent(event: Event): Promise<string> {
|
|
750
|
+
const eventId = await this.eventStore.append(event);
|
|
751
|
+
|
|
752
|
+
// WebSocket 브로드캐스트
|
|
753
|
+
broadcastEvent('events', {
|
|
754
|
+
type: 'new_event',
|
|
755
|
+
event: {
|
|
756
|
+
eventId,
|
|
757
|
+
eventType: event.eventType,
|
|
758
|
+
timestamp: event.timestamp,
|
|
759
|
+
sessionId: event.sessionId,
|
|
760
|
+
preview: generatePreview(event.payload, 100)
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
return eventId;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
**작업 항목**:
|
|
770
|
+
- [ ] 이벤트 저장 시 브로드캐스트
|
|
771
|
+
- [ ] Outbox 상태 브로드캐스트
|
|
772
|
+
- [ ] 통계 업데이트 브로드캐스트
|
|
773
|
+
|
|
774
|
+
## Phase 4: UI 구현 (P1)
|
|
775
|
+
|
|
776
|
+
### 4.1 HTML 템플릿
|
|
777
|
+
|
|
778
|
+
**파일**: `src/ui/index.html` (신규)
|
|
779
|
+
|
|
780
|
+
```html
|
|
781
|
+
<!DOCTYPE html>
|
|
782
|
+
<html lang="en">
|
|
783
|
+
<head>
|
|
784
|
+
<meta charset="UTF-8">
|
|
785
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
786
|
+
<title>Code Memory Dashboard</title>
|
|
787
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
788
|
+
<script type="module" src="/app.js"></script>
|
|
789
|
+
</head>
|
|
790
|
+
<body class="bg-gray-900 text-gray-100">
|
|
791
|
+
<div id="app"></div>
|
|
792
|
+
</body>
|
|
793
|
+
</html>
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
**작업 항목**:
|
|
797
|
+
- [ ] HTML 기본 템플릿
|
|
798
|
+
- [ ] Tailwind 설정
|
|
799
|
+
- [ ] 다크 테마
|
|
800
|
+
|
|
801
|
+
### 4.2 메인 앱
|
|
802
|
+
|
|
803
|
+
**파일**: `src/ui/app.ts` (신규)
|
|
804
|
+
|
|
805
|
+
```typescript
|
|
806
|
+
import { h, render } from 'preact';
|
|
807
|
+
import { signal } from '@preact/signals';
|
|
808
|
+
import { Router, Route } from 'preact-router';
|
|
809
|
+
|
|
810
|
+
import { Dashboard } from './pages/Dashboard';
|
|
811
|
+
import { Sessions } from './pages/Sessions';
|
|
812
|
+
import { Timeline } from './pages/Timeline';
|
|
813
|
+
import { Search } from './pages/Search';
|
|
814
|
+
import { Stats } from './pages/Stats';
|
|
815
|
+
|
|
816
|
+
const currentPath = signal(window.location.pathname);
|
|
817
|
+
|
|
818
|
+
function App() {
|
|
819
|
+
return h('div', { class: 'min-h-screen' },
|
|
820
|
+
h('nav', { class: 'bg-gray-800 p-4' },
|
|
821
|
+
h('div', { class: 'flex items-center gap-4' },
|
|
822
|
+
h('span', { class: 'text-xl font-bold' }, '🧠 Code Memory'),
|
|
823
|
+
h('a', { href: '/', class: 'hover:text-blue-400' }, 'Dashboard'),
|
|
824
|
+
h('a', { href: '/sessions', class: 'hover:text-blue-400' }, 'Sessions'),
|
|
825
|
+
h('a', { href: '/timeline', class: 'hover:text-blue-400' }, 'Timeline'),
|
|
826
|
+
h('a', { href: '/search', class: 'hover:text-blue-400' }, 'Search'),
|
|
827
|
+
h('a', { href: '/stats', class: 'hover:text-blue-400' }, 'Stats')
|
|
828
|
+
)
|
|
829
|
+
),
|
|
830
|
+
h('main', { class: 'p-4' },
|
|
831
|
+
h(Router, {},
|
|
832
|
+
h(Route, { path: '/', component: Dashboard }),
|
|
833
|
+
h(Route, { path: '/sessions', component: Sessions }),
|
|
834
|
+
h(Route, { path: '/sessions/:id', component: SessionDetail }),
|
|
835
|
+
h(Route, { path: '/timeline', component: Timeline }),
|
|
836
|
+
h(Route, { path: '/search', component: Search }),
|
|
837
|
+
h(Route, { path: '/stats', component: Stats })
|
|
838
|
+
)
|
|
839
|
+
)
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
render(h(App), document.getElementById('app')!);
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
**작업 항목**:
|
|
847
|
+
- [ ] Preact 앱 설정
|
|
848
|
+
- [ ] 라우터 구성
|
|
849
|
+
- [ ] 네비게이션 바
|
|
850
|
+
|
|
851
|
+
### 4.3 API 클라이언트
|
|
852
|
+
|
|
853
|
+
**파일**: `src/ui/api.ts` (신규)
|
|
854
|
+
|
|
855
|
+
```typescript
|
|
856
|
+
const BASE_URL = '/api';
|
|
857
|
+
|
|
858
|
+
export async function fetchSessions(options?: { page?: number; pageSize?: number }) {
|
|
859
|
+
const params = new URLSearchParams();
|
|
860
|
+
if (options?.page) params.set('page', String(options.page));
|
|
861
|
+
if (options?.pageSize) params.set('pageSize', String(options.pageSize));
|
|
862
|
+
|
|
863
|
+
const res = await fetch(`${BASE_URL}/sessions?${params}`);
|
|
864
|
+
return res.json();
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
export async function fetchEvents(options?: { sessionId?: string; type?: string; limit?: number }) {
|
|
868
|
+
const params = new URLSearchParams();
|
|
869
|
+
if (options?.sessionId) params.set('sessionId', options.sessionId);
|
|
870
|
+
if (options?.type) params.set('type', options.type);
|
|
871
|
+
if (options?.limit) params.set('limit', String(options.limit));
|
|
872
|
+
|
|
873
|
+
const res = await fetch(`${BASE_URL}/events?${params}`);
|
|
874
|
+
return res.json();
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
export async function search(query: string, options?: SearchOptions) {
|
|
878
|
+
const res = await fetch(`${BASE_URL}/search`, {
|
|
879
|
+
method: 'POST',
|
|
880
|
+
headers: { 'Content-Type': 'application/json' },
|
|
881
|
+
body: JSON.stringify({ query, options })
|
|
882
|
+
});
|
|
883
|
+
return res.json();
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
export async function fetchStats() {
|
|
887
|
+
const res = await fetch(`${BASE_URL}/stats`);
|
|
888
|
+
return res.json();
|
|
889
|
+
}
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
**작업 항목**:
|
|
893
|
+
- [ ] Sessions API 클라이언트
|
|
894
|
+
- [ ] Events API 클라이언트
|
|
895
|
+
- [ ] Search API 클라이언트
|
|
896
|
+
- [ ] Stats API 클라이언트
|
|
897
|
+
|
|
898
|
+
### 4.4 WebSocket 클라이언트
|
|
899
|
+
|
|
900
|
+
**파일**: `src/ui/websocket.ts` (신규)
|
|
901
|
+
|
|
902
|
+
```typescript
|
|
903
|
+
import { signal } from '@preact/signals';
|
|
904
|
+
|
|
905
|
+
export const wsConnected = signal(false);
|
|
906
|
+
export const liveEvents = signal<Event[]>([]);
|
|
907
|
+
export const outboxStatus = signal({ pending: 0, processing: [], failed: [] });
|
|
908
|
+
|
|
909
|
+
let ws: WebSocket | null = null;
|
|
910
|
+
|
|
911
|
+
export function connectWebSocket() {
|
|
912
|
+
ws = new WebSocket(`ws://${window.location.host}/ws`);
|
|
913
|
+
|
|
914
|
+
ws.onopen = () => {
|
|
915
|
+
wsConnected.value = true;
|
|
916
|
+
ws?.send(JSON.stringify({
|
|
917
|
+
type: 'subscribe',
|
|
918
|
+
channels: ['events', 'outbox']
|
|
919
|
+
}));
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
ws.onmessage = (event) => {
|
|
923
|
+
const msg = JSON.parse(event.data);
|
|
924
|
+
|
|
925
|
+
if (msg.channel === 'events') {
|
|
926
|
+
liveEvents.value = [msg.data.event, ...liveEvents.value.slice(0, 99)];
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (msg.channel === 'outbox') {
|
|
930
|
+
outboxStatus.value = msg.data;
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
ws.onclose = () => {
|
|
935
|
+
wsConnected.value = false;
|
|
936
|
+
setTimeout(connectWebSocket, 3000); // 재연결
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
export function subscribeToSession(sessionId: string) {
|
|
941
|
+
ws?.send(JSON.stringify({
|
|
942
|
+
type: 'subscribe',
|
|
943
|
+
channels: ['events'],
|
|
944
|
+
filters: { sessionId }
|
|
945
|
+
}));
|
|
946
|
+
}
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
**작업 항목**:
|
|
950
|
+
- [ ] WebSocket 연결 관리
|
|
951
|
+
- [ ] 자동 재연결
|
|
952
|
+
- [ ] 구독 관리
|
|
953
|
+
- [ ] 실시간 상태 시그널
|
|
954
|
+
|
|
955
|
+
## Phase 5: 페이지 컴포넌트 (P1)
|
|
956
|
+
|
|
957
|
+
### 5.1 Dashboard 페이지
|
|
958
|
+
|
|
959
|
+
**파일**: `src/ui/pages/Dashboard.ts` (신규)
|
|
960
|
+
|
|
961
|
+
```typescript
|
|
962
|
+
import { h } from 'preact';
|
|
963
|
+
import { useEffect, useState } from 'preact/hooks';
|
|
964
|
+
import { fetchStats, fetchSessions } from '../api';
|
|
965
|
+
|
|
966
|
+
export function Dashboard() {
|
|
967
|
+
const [stats, setStats] = useState(null);
|
|
968
|
+
const [recentSessions, setRecentSessions] = useState([]);
|
|
969
|
+
|
|
970
|
+
useEffect(() => {
|
|
971
|
+
fetchStats().then(setStats);
|
|
972
|
+
fetchSessions({ pageSize: 5 }).then(data => setRecentSessions(data.sessions));
|
|
973
|
+
}, []);
|
|
974
|
+
|
|
975
|
+
return h('div', { class: 'space-y-6' },
|
|
976
|
+
// Stats cards
|
|
977
|
+
h('div', { class: 'grid grid-cols-3 gap-4' },
|
|
978
|
+
h(StatCard, { title: 'Events', value: stats?.storage.eventCount }),
|
|
979
|
+
h(StatCard, { title: 'Vectors', value: stats?.storage.vectorCount }),
|
|
980
|
+
h(StatCard, { title: 'Sessions', value: stats?.sessions.total })
|
|
981
|
+
),
|
|
982
|
+
// Recent sessions
|
|
983
|
+
h('div', { class: 'bg-gray-800 rounded p-4' },
|
|
984
|
+
h('h2', { class: 'text-lg font-semibold mb-4' }, 'Recent Sessions'),
|
|
985
|
+
recentSessions.map(s => h(SessionItem, { session: s }))
|
|
986
|
+
)
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
**작업 항목**:
|
|
992
|
+
- [ ] 통계 카드 컴포넌트
|
|
993
|
+
- [ ] 최근 세션 목록
|
|
994
|
+
- [ ] 실시간 업데이트
|
|
995
|
+
|
|
996
|
+
### 5.2 Timeline 페이지
|
|
997
|
+
|
|
998
|
+
**파일**: `src/ui/pages/Timeline.ts` (신규)
|
|
999
|
+
|
|
1000
|
+
```typescript
|
|
1001
|
+
import { h } from 'preact';
|
|
1002
|
+
import { useEffect } from 'preact/hooks';
|
|
1003
|
+
import { liveEvents, connectWebSocket } from '../websocket';
|
|
1004
|
+
|
|
1005
|
+
export function Timeline() {
|
|
1006
|
+
useEffect(() => {
|
|
1007
|
+
connectWebSocket();
|
|
1008
|
+
}, []);
|
|
1009
|
+
|
|
1010
|
+
return h('div', { class: 'space-y-4' },
|
|
1011
|
+
h('div', { class: 'flex items-center justify-between' },
|
|
1012
|
+
h('h1', { class: 'text-xl font-bold' }, '📅 Timeline'),
|
|
1013
|
+
h('span', { class: 'text-green-400' }, '● Live')
|
|
1014
|
+
),
|
|
1015
|
+
h('div', { class: 'space-y-2' },
|
|
1016
|
+
liveEvents.value.map(event =>
|
|
1017
|
+
h(TimelineItem, { event })
|
|
1018
|
+
)
|
|
1019
|
+
)
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function TimelineItem({ event }) {
|
|
1024
|
+
const icons = {
|
|
1025
|
+
user_prompt: '💬',
|
|
1026
|
+
assistant_response: '🤖',
|
|
1027
|
+
tool_observation: '🛠️'
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
return h('div', { class: 'flex gap-4 p-4 bg-gray-800 rounded' },
|
|
1031
|
+
h('div', { class: 'text-2xl' }, icons[event.eventType] || '📝'),
|
|
1032
|
+
h('div', { class: 'flex-1' },
|
|
1033
|
+
h('div', { class: 'text-sm text-gray-400' },
|
|
1034
|
+
new Date(event.timestamp).toLocaleTimeString()
|
|
1035
|
+
),
|
|
1036
|
+
h('div', {}, event.preview)
|
|
1037
|
+
)
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
**작업 항목**:
|
|
1043
|
+
- [ ] 실시간 타임라인
|
|
1044
|
+
- [ ] 이벤트 타입별 아이콘
|
|
1045
|
+
- [ ] 필터링 옵션
|
|
1046
|
+
- [ ] 무한 스크롤
|
|
1047
|
+
|
|
1048
|
+
### 5.3 Search 페이지
|
|
1049
|
+
|
|
1050
|
+
**파일**: `src/ui/pages/Search.ts` (신규)
|
|
1051
|
+
|
|
1052
|
+
```typescript
|
|
1053
|
+
import { h } from 'preact';
|
|
1054
|
+
import { useState } from 'preact/hooks';
|
|
1055
|
+
import { search } from '../api';
|
|
1056
|
+
|
|
1057
|
+
export function Search() {
|
|
1058
|
+
const [query, setQuery] = useState('');
|
|
1059
|
+
const [results, setResults] = useState([]);
|
|
1060
|
+
const [loading, setLoading] = useState(false);
|
|
1061
|
+
|
|
1062
|
+
async function handleSearch() {
|
|
1063
|
+
if (!query.trim()) return;
|
|
1064
|
+
setLoading(true);
|
|
1065
|
+
const data = await search(query);
|
|
1066
|
+
setResults(data.results);
|
|
1067
|
+
setLoading(false);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
return h('div', { class: 'space-y-4' },
|
|
1071
|
+
h('div', { class: 'flex gap-2' },
|
|
1072
|
+
h('input', {
|
|
1073
|
+
type: 'text',
|
|
1074
|
+
value: query,
|
|
1075
|
+
onInput: (e) => setQuery(e.target.value),
|
|
1076
|
+
onKeyDown: (e) => e.key === 'Enter' && handleSearch(),
|
|
1077
|
+
placeholder: 'Search memories...',
|
|
1078
|
+
class: 'flex-1 bg-gray-800 rounded px-4 py-2'
|
|
1079
|
+
}),
|
|
1080
|
+
h('button', {
|
|
1081
|
+
onClick: handleSearch,
|
|
1082
|
+
class: 'bg-blue-600 px-4 py-2 rounded'
|
|
1083
|
+
}, 'Search')
|
|
1084
|
+
),
|
|
1085
|
+
loading && h('div', {}, 'Searching...'),
|
|
1086
|
+
h('div', { class: 'space-y-2' },
|
|
1087
|
+
results.map(r => h(SearchResult, { result: r }))
|
|
1088
|
+
)
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
**작업 항목**:
|
|
1094
|
+
- [ ] 검색 입력
|
|
1095
|
+
- [ ] 필터 옵션
|
|
1096
|
+
- [ ] 결과 표시
|
|
1097
|
+
- [ ] 하이라이트
|
|
1098
|
+
|
|
1099
|
+
## Phase 6: 빌드 및 통합 (P0)
|
|
1100
|
+
|
|
1101
|
+
### 6.1 빌드 스크립트
|
|
1102
|
+
|
|
1103
|
+
**파일**: `package.json` 수정
|
|
1104
|
+
|
|
1105
|
+
```json
|
|
1106
|
+
{
|
|
1107
|
+
"scripts": {
|
|
1108
|
+
"build:ui": "esbuild src/ui/app.ts --bundle --outfile=dist/ui/app.js --minify",
|
|
1109
|
+
"build:server": "esbuild src/server/index.ts --bundle --platform=node --outfile=dist/server.js",
|
|
1110
|
+
"dev:ui": "esbuild src/ui/app.ts --bundle --outfile=dist/ui/app.js --watch",
|
|
1111
|
+
"start:server": "bun dist/server.js"
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
**작업 항목**:
|
|
1117
|
+
- [ ] UI 빌드 스크립트
|
|
1118
|
+
- [ ] 서버 빌드 스크립트
|
|
1119
|
+
- [ ] 개발 모드 설정
|
|
1120
|
+
|
|
1121
|
+
### 6.2 서버 자동 시작
|
|
1122
|
+
|
|
1123
|
+
**파일**: `src/hooks/session-start.ts` 수정
|
|
1124
|
+
|
|
1125
|
+
```typescript
|
|
1126
|
+
import { startServer, isServerRunning } from '../server';
|
|
1127
|
+
|
|
1128
|
+
export async function handleSessionStart(): Promise<void> {
|
|
1129
|
+
// 서버 실행 확인 및 시작
|
|
1130
|
+
if (!await isServerRunning(37777)) {
|
|
1131
|
+
startServer(37777);
|
|
1132
|
+
console.log('Memory viewer started at http://localhost:37777');
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// 기존 로직...
|
|
1136
|
+
}
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
**작업 항목**:
|
|
1140
|
+
- [ ] 세션 시작 시 서버 자동 시작
|
|
1141
|
+
- [ ] 포트 충돌 처리
|
|
1142
|
+
- [ ] 로그 출력
|
|
1143
|
+
|
|
1144
|
+
## 파일 목록
|
|
1145
|
+
|
|
1146
|
+
### 신규 파일
|
|
1147
|
+
```
|
|
1148
|
+
# Server
|
|
1149
|
+
src/server/index.ts # HTTP 서버 메인
|
|
1150
|
+
src/server/api/index.ts # API 라우터
|
|
1151
|
+
src/server/api/sessions.ts # Sessions API
|
|
1152
|
+
src/server/api/events.ts # Events API
|
|
1153
|
+
src/server/api/search.ts # Search API
|
|
1154
|
+
src/server/api/stats.ts # Stats API
|
|
1155
|
+
src/server/api/config.ts # Config API
|
|
1156
|
+
src/server/websocket.ts # WebSocket 핸들러
|
|
1157
|
+
|
|
1158
|
+
# UI
|
|
1159
|
+
src/ui/index.html # HTML 템플릿
|
|
1160
|
+
src/ui/app.ts # Preact 앱
|
|
1161
|
+
src/ui/api.ts # API 클라이언트
|
|
1162
|
+
src/ui/websocket.ts # WebSocket 클라이언트
|
|
1163
|
+
src/ui/pages/Dashboard.ts # 대시보드 페이지
|
|
1164
|
+
src/ui/pages/Sessions.ts # 세션 페이지
|
|
1165
|
+
src/ui/pages/Timeline.ts # 타임라인 페이지
|
|
1166
|
+
src/ui/pages/Search.ts # 검색 페이지
|
|
1167
|
+
src/ui/pages/Stats.ts # 통계 페이지
|
|
1168
|
+
src/ui/components/*.ts # 공통 컴포넌트
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
### 수정 파일
|
|
1172
|
+
```
|
|
1173
|
+
src/services/memory-service.ts # WebSocket 브로드캐스트 추가
|
|
1174
|
+
src/hooks/session-start.ts # 서버 자동 시작
|
|
1175
|
+
package.json # 빌드 스크립트
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
## 마일스톤
|
|
1179
|
+
|
|
1180
|
+
| 단계 | 완료 기준 |
|
|
1181
|
+
|------|----------|
|
|
1182
|
+
| M1 | HTTP 서버 + 정적 파일 서빙 |
|
|
1183
|
+
| M2 | REST API (Sessions, Events) |
|
|
1184
|
+
| M3 | REST API (Search, Stats, Config) |
|
|
1185
|
+
| M4 | WebSocket 기본 구현 |
|
|
1186
|
+
| M5 | UI 기본 레이아웃 |
|
|
1187
|
+
| M6 | Dashboard + Timeline 페이지 |
|
|
1188
|
+
| M7 | Search + Stats 페이지 |
|
|
1189
|
+
| M8 | 빌드 및 통합 테스트 |
|
|
1190
|
+
|
|
1191
|
+
## 2026-02-25T12:31:26.480Z | 5489e5b4-f3f1-4933-9208-c13ca5e1bcff
|
|
1192
|
+
- type: session_summary
|
|
1193
|
+
- session: import:organized
|
|
1194
|
+
# Web Viewer UI Specification
|
|
1195
|
+
|
|
1196
|
+
> **Version**: 1.0.0
|
|
1197
|
+
> **Status**: Draft
|
|
1198
|
+
> **Created**: 2026-02-01
|
|
1199
|
+
> **Reference**: claude-mem (thedotmack/claude-mem)
|
|
1200
|
+
|
|
1201
|
+
## 1. 개요
|
|
1202
|
+
|
|
1203
|
+
### 1.1 문제 정의
|
|
1204
|
+
|
|
1205
|
+
현재 시스템에서 메모리 상태 시각화가 어려움:
|
|
1206
|
+
|
|
1207
|
+
1. **CLI 한계**: 대량 데이터 탐색 불편
|
|
1208
|
+
2. **실시간 모니터링 없음**: 세션 진행 중 메모리 변화 확인 불가
|
|
1209
|
+
3. **디버깅 어려움**: 메모리 저장/검색 과정 추적 어려움
|
|
1210
|
+
|
|
1211
|
+
### 1.2 해결 방향
|
|
1212
|
+
|
|
1213
|
+
**Web Viewer UI**:
|
|
1214
|
+
- HTTP API 서버 (localhost:37777)
|
|
1215
|
+
- 실시간 메모리 스트림 대시보드
|
|
1216
|
+
- 세션/프로젝트별 탐색 인터페이스
|
|
1217
|
+
|
|
1218
|
+
## 2. 핵심 개념
|
|
1219
|
+
|
|
1220
|
+
### 2.1 시스템 아키텍처
|
|
1221
|
+
|
|
1222
|
+
```
|
|
1223
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
1224
|
+
│ Claude Code │
|
|
1225
|
+
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
|
1226
|
+
│ │ Hooks │ │ CLI │ │ Memory │ │ Web │ │
|
|
1227
|
+
│ │ │ │ │ │ Service │ │ Server │ │
|
|
1228
|
+
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
|
|
1229
|
+
│ │ │ │ │ │
|
|
1230
|
+
│ └────────────┴────────────┴────────────┘ │
|
|
1231
|
+
│ │ │
|
|
1232
|
+
└──────────────────────────┼───────────────────────────────────┘
|
|
1233
|
+
│
|
|
1234
|
+
▼
|
|
1235
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
1236
|
+
│ Web Server (Bun) │
|
|
1237
|
+
│ localhost:37777 │
|
|
1238
|
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
1239
|
+
│ │ REST API │ │ WebSocket │ │ Static │ │
|
|
1240
|
+
│ │ /api/* │ │ /ws │ │ / │ │
|
|
1241
|
+
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
1242
|
+
└─────────────────────────────────────────────────────────────┘
|
|
1243
|
+
│
|
|
1244
|
+
▼
|
|
1245
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
1246
|
+
│ Web Browser │
|
|
1247
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
1248
|
+
│ │ Memory Dashboard │ │
|
|
1249
|
+
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
|
|
1250
|
+
│ │ │ Sessions │ │ Timeline │ │ Search │ │ │
|
|
1251
|
+
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
|
|
1252
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
1253
|
+
└─────────────────────────────────────────────────────────────┘
|
|
1254
|
+
```
|
|
1255
|
+
|
|
1256
|
+
### 2.2 주요 기능
|
|
1257
|
+
|
|
1258
|
+
| 기능 | 설명 |
|
|
1259
|
+
|------|------|
|
|
1260
|
+
| **Session Browser** | 세션 목록, 세션별 이벤트 탐색 |
|
|
1261
|
+
| **Memory Timeline** | 시간순 메모리 스트림 (실시간) |
|
|
1262
|
+
| **Search Interface** | 벡터 검색 + 필터링 |
|
|
1263
|
+
| **Stats Dashboard** | 저장소 통계, 사용량 |
|
|
1264
|
+
| **Debug View** | Outbox 상태, 임베딩 진행률 |
|
|
1265
|
+
| **Settings** | 설정 조회/수정 |
|
|
1266
|
+
|
|
1267
|
+
### 2.3 포트 및 경로
|
|
1268
|
+
|
|
1269
|
+
```
|
|
1270
|
+
http://localhost:37777
|
|
1271
|
+
├── / # 대시보드 메인
|
|
1272
|
+
├── /sessions # 세션 목록
|
|
1273
|
+
├── /sessions/:id # 세션 상세
|
|
1274
|
+
├── /timeline # 실시간 타임라인
|
|
1275
|
+
├── /search # 검색 인터페이스
|
|
1276
|
+
├── /stats # 통계
|
|
1277
|
+
├── /settings # 설정
|
|
1278
|
+
└── /api # REST API
|
|
1279
|
+
├── /api/sessions
|
|
1280
|
+
├── /api/events
|
|
1281
|
+
├── /api/search
|
|
1282
|
+
├── /api/stats
|
|
1283
|
+
└── /api/config
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
## 3. REST API 스키마
|
|
1287
|
+
|
|
1288
|
+
### 3.1 Sessions API
|
|
1289
|
+
|
|
1290
|
+
```typescript
|
|
1291
|
+
// GET /api/sessions
|
|
1292
|
+
interface SessionsResponse {
|
|
1293
|
+
sessions: {
|
|
1294
|
+
sessionId: string;
|
|
1295
|
+
projectPath: string;
|
|
1296
|
+
startedAt: Date;
|
|
1297
|
+
endedAt?: Date;
|
|
1298
|
+
eventCount: number;
|
|
1299
|
+
status: 'active' | 'ended';
|
|
1300
|
+
}[];
|
|
1301
|
+
total: number;
|
|
1302
|
+
page: number;
|
|
1303
|
+
pageSize: number;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// GET /api/sessions/:id
|
|
1307
|
+
interface SessionDetailResponse {
|
|
1308
|
+
session: {
|
|
1309
|
+
sessionId: string;
|
|
1310
|
+
projectPath: string;
|
|
1311
|
+
startedAt: Date;
|
|
1312
|
+
endedAt?: Date;
|
|
1313
|
+
summary?: string;
|
|
1314
|
+
};
|
|
1315
|
+
events: Event[];
|
|
1316
|
+
stats: {
|
|
1317
|
+
promptCount: number;
|
|
1318
|
+
responseCount: number;
|
|
1319
|
+
toolCount: number;
|
|
1320
|
+
totalTokens: number;
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
### 3.2 Events API
|
|
1326
|
+
|
|
1327
|
+
```typescript
|
|
1328
|
+
// GET /api/events?sessionId=xxx&type=xxx&limit=100
|
|
1329
|
+
interface EventsResponse {
|
|
1330
|
+
events: {
|
|
1331
|
+
eventId: string;
|
|
1332
|
+
eventType: string;
|
|
1333
|
+
timestamp: Date;
|
|
1334
|
+
sessionId: string;
|
|
1335
|
+
preview: string; // 100자 미리보기
|
|
1336
|
+
metadata: {
|
|
1337
|
+
tokenCount?: number;
|
|
1338
|
+
hasCode?: boolean;
|
|
1339
|
+
};
|
|
1340
|
+
}[];
|
|
1341
|
+
total: number;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// GET /api/events/:id
|
|
1345
|
+
interface EventDetailResponse {
|
|
1346
|
+
event: {
|
|
1347
|
+
eventId: string;
|
|
1348
|
+
eventType: string;
|
|
1349
|
+
timestamp: Date;
|
|
1350
|
+
sessionId: string;
|
|
1351
|
+
payload: unknown; // 전체 페이로드
|
|
1352
|
+
};
|
|
1353
|
+
related: {
|
|
1354
|
+
previous?: string;
|
|
1355
|
+
next?: string;
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
```
|
|
1359
|
+
|
|
1360
|
+
### 3.3 Search API
|
|
1361
|
+
|
|
1362
|
+
```typescript
|
|
1363
|
+
// POST /api/search
|
|
1364
|
+
interface SearchRequest {
|
|
1365
|
+
query: string;
|
|
1366
|
+
filters?: {
|
|
1367
|
+
sessionId?: string;
|
|
1368
|
+
eventType?: string[];
|
|
1369
|
+
dateFrom?: Date;
|
|
1370
|
+
dateTo?: Date;
|
|
1371
|
+
};
|
|
1372
|
+
options?: {
|
|
1373
|
+
topK?: number;
|
|
1374
|
+
minScore?: number;
|
|
1375
|
+
progressive?: boolean;
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
interface SearchResponse {
|
|
1380
|
+
results: {
|
|
1381
|
+
id: string;
|
|
1382
|
+
score: number;
|
|
1383
|
+
type: string;
|
|
1384
|
+
timestamp: Date;
|
|
1385
|
+
sessionId: string;
|
|
1386
|
+
preview: string;
|
|
1387
|
+
highlight?: string; // 매칭된 부분 강조
|
|
1388
|
+
}[];
|
|
1389
|
+
meta: {
|
|
1390
|
+
totalMatches: number;
|
|
1391
|
+
searchTime: number;
|
|
1392
|
+
mode: 'vector' | 'fts' | 'hybrid';
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
```
|
|
1396
|
+
|
|
1397
|
+
### 3.4 Stats API
|
|
1398
|
+
|
|
1399
|
+
```typescript
|
|
1400
|
+
// GET /api/stats
|
|
1401
|
+
interface StatsResponse {
|
|
1402
|
+
storage: {
|
|
1403
|
+
eventCount: number;
|
|
1404
|
+
vectorCount: number;
|
|
1405
|
+
dbSizeMB: number;
|
|
1406
|
+
vectorSizeMB: number;
|
|
1407
|
+
};
|
|
1408
|
+
sessions: {
|
|
1409
|
+
total: number;
|
|
1410
|
+
active: number;
|
|
1411
|
+
thisWeek: number;
|
|
1412
|
+
};
|
|
1413
|
+
embeddings: {
|
|
1414
|
+
pending: number;
|
|
1415
|
+
processed: number;
|
|
1416
|
+
failed: number;
|
|
1417
|
+
avgProcessTime: number;
|
|
1418
|
+
};
|
|
1419
|
+
memory: {
|
|
1420
|
+
heapUsed: number;
|
|
1421
|
+
heapTotal: number;
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// GET /api/stats/timeline
|
|
1426
|
+
interface TimelineStatsResponse {
|
|
1427
|
+
daily: {
|
|
1428
|
+
date: string;
|
|
1429
|
+
eventCount: number;
|
|
1430
|
+
sessionCount: number;
|
|
1431
|
+
}[];
|
|
1432
|
+
}
|
|
1433
|
+
```
|
|
1434
|
+
|
|
1435
|
+
### 3.5 Config API
|
|
1436
|
+
|
|
1437
|
+
```typescript
|
|
1438
|
+
// GET /api/config
|
|
1439
|
+
interface ConfigResponse {
|
|
1440
|
+
config: MemoryConfig;
|
|
1441
|
+
editable: string[]; // 수정 가능한 필드 목록
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// PATCH /api/config
|
|
1445
|
+
interface ConfigUpdateRequest {
|
|
1446
|
+
updates: Partial<MemoryConfig>;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
interface ConfigUpdateResponse {
|
|
1450
|
+
success: boolean;
|
|
1451
|
+
config: MemoryConfig;
|
|
1452
|
+
restartRequired?: boolean;
|
|
1453
|
+
}
|
|
1454
|
+
```
|
|
1455
|
+
|
|
1456
|
+
## 4. WebSocket 인터페이스
|
|
1457
|
+
|
|
1458
|
+
### 4.1 실시간 이벤트 스트림
|
|
1459
|
+
|
|
1460
|
+
```typescript
|
|
1461
|
+
// 연결: ws://localhost:37777/ws
|
|
1462
|
+
|
|
1463
|
+
// 클라이언트 → 서버 메시지
|
|
1464
|
+
interface WSClientMessage {
|
|
1465
|
+
type: 'subscribe' | 'unsubscribe';
|
|
1466
|
+
channels: ('events' | 'stats' | 'outbox')[];
|
|
1467
|
+
filters?: {
|
|
1468
|
+
sessionId?: string;
|
|
1469
|
+
eventType?: string[];
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// 서버 → 클라이언트 메시지
|
|
1474
|
+
interface WSServerMessage {
|
|
1475
|
+
channel: 'events' | 'stats' | 'outbox';
|
|
1476
|
+
data: EventMessage | StatsMessage | OutboxMessage;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
interface EventMessage {
|
|
1480
|
+
type: 'new_event';
|
|
1481
|
+
event: {
|
|
1482
|
+
eventId: string;
|
|
1483
|
+
eventType: string;
|
|
1484
|
+
timestamp: Date;
|
|
1485
|
+
sessionId: string;
|
|
1486
|
+
preview: string;
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
interface OutboxMessage {
|
|
1491
|
+
type: 'outbox_update';
|
|
1492
|
+
pending: number;
|
|
1493
|
+
processing: string[];
|
|
1494
|
+
completed: string[];
|
|
1495
|
+
failed: string[];
|
|
1496
|
+
}
|
|
1497
|
+
```
|
|
1498
|
+
|
|
1499
|
+
### 4.2 연결 예시
|
|
1500
|
+
|
|
1501
|
+
```typescript
|
|
1502
|
+
// 클라이언트 코드
|
|
1503
|
+
const ws = new WebSocket('ws://localhost:37777/ws');
|
|
1504
|
+
|
|
1505
|
+
ws.onopen = () => {
|
|
1506
|
+
ws.send(JSON.stringify({
|
|
1507
|
+
type: 'subscribe',
|
|
1508
|
+
channels: ['events', 'outbox']
|
|
1509
|
+
}));
|
|
1510
|
+
};
|
|
1511
|
+
|
|
1512
|
+
ws.onmessage = (event) => {
|
|
1513
|
+
const msg = JSON.parse(event.data);
|
|
1514
|
+
if (msg.channel === 'events') {
|
|
1515
|
+
addToTimeline(msg.data.event);
|
|
1516
|
+
} else if (msg.channel === 'outbox') {
|
|
1517
|
+
updateOutboxStatus(msg.data);
|
|
1518
|
+
}
|
|
1519
|
+
};
|
|
1520
|
+
```
|
|
1521
|
+
|
|
1522
|
+
## 5. UI 컴포넌트
|
|
1523
|
+
|
|
1524
|
+
### 5.1 대시보드 레이아웃
|
|
1525
|
+
|
|
1526
|
+
```
|
|
1527
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
1528
|
+
│ 🧠 Code Memory [Search] [Settings]│
|
|
1529
|
+
├─────────────────────────────────────────────────────────────┤
|
|
1530
|
+
│ ┌─────────────┐ ┌───────────────────────────────────────┐ │
|
|
1531
|
+
│ │ │ │ │ │
|
|
1532
|
+
│ │ Sessions │ │ Main Content Area │ │
|
|
1533
|
+
│ │ ───────── │ │ │ │
|
|
1534
|
+
│ │ > session1 │ │ - Timeline View │ │
|
|
1535
|
+
│ │ session2 │ │ - Search Results │ │
|
|
1536
|
+
│ │ session3 │ │ - Session Details │ │
|
|
1537
|
+
│ │ ... │ │ - Stats Dashboard │ │
|
|
1538
|
+
│ │ │ │ │ │
|
|
1539
|
+
│ │ ───────── │ │ │ │
|
|
1540
|
+
│ │ Projects │ │ │ │
|
|
1541
|
+
│ │ > project1 │ │ │ │
|
|
1542
|
+
│ │ project2 │ │ │ │
|
|
1543
|
+
│ │ │ │ │ │
|
|
1544
|
+
│ └─────────────┘ └───────────────────────────────────────┘ │
|
|
1545
|
+
├─────────────────────────────────────────────────────────────┤
|
|
1546
|
+
│ Events: 1,234 │ Vectors: 987 │ Outbox: 5 pending │
|
|
1547
|
+
└─────────────────────────────────────────────────────────────┘
|
|
1548
|
+
```
|
|
1549
|
+
|
|
1550
|
+
### 5.2 Timeline View
|
|
1551
|
+
|
|
1552
|
+
```
|
|
1553
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
1554
|
+
│ 📅 Timeline [Filter ▼] [Live ●] │
|
|
1555
|
+
├─────────────────────────────────────────────────────────────┤
|
|
1556
|
+
│ │
|
|
1557
|
+
│ ○─── 14:35 ───────────────────────────────────────────────│
|
|
1558
|
+
│ │ │
|
|
1559
|
+
│ │ 💬 User Prompt │
|
|
1560
|
+
│ │ "DuckDB 스키마를 어떻게 설계할까요?" │
|
|
1561
|
+
│ │ │
|
|
1562
|
+
│ ○─── 14:36 ───────────────────────────────────────────────│
|
|
1563
|
+
│ │ │
|
|
1564
|
+
│ │ 🛠️ Tool: Read │
|
|
1565
|
+
│ │ /src/core/event-store.ts │
|
|
1566
|
+
│ │ │
|
|
1567
|
+
│ ○─── 14:37 ───────────────────────────────────────────────│
|
|
1568
|
+
│ │ │
|
|
1569
|
+
│ │ 🤖 Assistant Response │
|
|
1570
|
+
│ │ "DuckDB를 사용하여 이벤트 소싱 패턴을..." │
|
|
1571
|
+
│ │ [Show Full] [Copy] │
|
|
1572
|
+
│ │ │
|
|
1573
|
+
│ ○─── 14:40 ───────────────────────────────────────────────│
|
|
1574
|
+
│ │
|
|
1575
|
+
└─────────────────────────────────────────────────────────────┘
|
|
1576
|
+
```
|
|
1577
|
+
|
|
1578
|
+
### 5.3 Search View
|
|
1579
|
+
|
|
1580
|
+
```
|
|
1581
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
1582
|
+
│ 🔍 Search │
|
|
1583
|
+
├─────────────────────────────────────────────────────────────┤
|
|
1584
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
1585
|
+
│ │ Type to search memories... │ │
|
|
1586
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
1587
|
+
│ │
|
|
1588
|
+
│ Filters: [All Types ▼] [All Sessions ▼] [Date Range ▼] │
|
|
1589
|
+
│ │
|
|
1590
|
+
│ ───────────────────────────────────────────────────────── │
|
|
1591
|
+
│ │
|
|
1592
|
+
│ 📄 Result 1 (score: 0.94) │
|
|
1593
|
+
│ Session: abc123 │ 2026-01-30 14:05 │
|
|
1594
|
+
│ "DuckDB를 사용하여 이벤트 소싱 패턴을 구현하는 방법을..." │
|
|
1595
|
+
│ [View] [Timeline] [Copy ID] │
|
|
1596
|
+
│ │
|
|
1597
|
+
│ 📄 Result 2 (score: 0.87) │
|
|
1598
|
+
│ Session: def456 │ 2026-01-29 10:20 │
|
|
1599
|
+
│ "타입 시스템 리팩토링 시 고려할 점..." │
|
|
1600
|
+
│ [View] [Timeline] [Copy ID] │
|
|
1601
|
+
│ │
|
|
1602
|
+
└─────────────────────────────────────────────────────────────┘
|
|
1603
|
+
```
|
|
1604
|
+
|
|
1605
|
+
### 5.4 Stats Dashboard
|
|
1606
|
+
|
|
1607
|
+
```
|
|
1608
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
1609
|
+
│ 📊 Statistics [Refresh] [Export] │
|
|
1610
|
+
├─────────────────────────────────────────────────────────────┤
|
|
1611
|
+
│ │
|
|
1612
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
|
1613
|
+
│ │ Events │ │ Vectors │ │ Sessions │ │
|
|
1614
|
+
│ │ 1,234 │ │ 987 │ │ 45 │ │
|
|
1615
|
+
│ │ ↑12 today │ │ ↑8 today │ │ 3 active │ │
|
|
1616
|
+
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
|
1617
|
+
│ │
|
|
1618
|
+
│ Storage │
|
|
1619
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
1620
|
+
│ │ DuckDB [████████░░] 156 MB / 500 MB │ │
|
|
1621
|
+
│ │ LanceDB [███░░░░░░░] 45 MB / 500 MB │ │
|
|
1622
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
1623
|
+
│ │
|
|
1624
|
+
│ Embedding Pipeline │
|
|
1625
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
1626
|
+
│ │ Pending: 5 │ Processing: 2 │ Failed: 0 │ │
|
|
1627
|
+
│ │ Avg Time: 125ms │ │
|
|
1628
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
1629
|
+
│ │
|
|
1630
|
+
│ Activity (Last 7 Days) │
|
|
1631
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
1632
|
+
│ │ ▂▄█▆▃▂▄ │ │
|
|
1633
|
+
│ │ Mon Tue Wed Thu Fri Sat Sun │ │
|
|
1634
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
1635
|
+
│ │
|
|
1636
|
+
└─────────────────────────────────────────────────────────────┘
|
|
1637
|
+
```
|
|
1638
|
+
|
|
1639
|
+
## 6. 기술 스택
|
|
1640
|
+
|
|
1641
|
+
### 6.1 서버
|
|
1642
|
+
|
|
1643
|
+
| 컴포넌트 | 기술 | 이유 |
|
|
1644
|
+
|----------|------|------|
|
|
1645
|
+
| HTTP Server | Bun.serve | 빠른 성능, 번들 불필요 |
|
|
1646
|
+
| WebSocket | Bun WebSocket | 내장 지원 |
|
|
1647
|
+
| Router | Hono | 경량, Bun 최적화 |
|
|
1648
|
+
|
|
1649
|
+
### 6.2 클라이언트
|
|
1650
|
+
|
|
1651
|
+
| 컴포넌트 | 기술 | 이유 |
|
|
1652
|
+
|----------|------|------|
|
|
1653
|
+
| UI Framework | Preact + HTM | 번들 크기 최소 |
|
|
1654
|
+
| Styling | Tailwind CSS | 빠른 개발 |
|
|
1655
|
+
| State | Signals | 경량 반응성 |
|
|
1656
|
+
| Charts | Chart.js | 간단한 통계 시각화 |
|
|
1657
|
+
|
|
1658
|
+
### 6.3 대안
|
|
1659
|
+
|
|
1660
|
+
| 옵션 | 장점 | 단점 |
|
|
1661
|
+
|------|------|------|
|
|
1662
|
+
| React + Vite | 생태계 | 번들 크기 |
|
|
1663
|
+
| Vue 3 | 간결함 | 추가 학습 |
|
|
1664
|
+
| Svelte | 번들 최소 | 생태계 작음 |
|
|
1665
|
+
| **Preact + HTM** | 초경량, JSX 없이 | 생태계 제한 |
|
|
1666
|
+
|
|
1667
|
+
## 7. 보안
|
|
1668
|
+
|
|
1669
|
+
### 7.1 접근 제어
|
|
1670
|
+
|
|
1671
|
+
```typescript
|
|
1672
|
+
// localhost만 허용
|
|
1673
|
+
const server = Bun.serve({
|
|
1674
|
+
hostname: '127.0.0.1', // localhost만
|
|
1675
|
+
port: 37777,
|
|
1676
|
+
// ...
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
// 또는 토큰 기반
|
|
1680
|
+
const AUTH_TOKEN = process.env.MEMORY_VIEWER_TOKEN;
|
|
1681
|
+
|
|
1682
|
+
function authMiddleware(req: Request): boolean {
|
|
1683
|
+
const token = req.headers.get('Authorization');
|
|
1684
|
+
return token === `Bearer ${AUTH_TOKEN}`;
|
|
1685
|
+
}
|
|
1686
|
+
```
|
|
1687
|
+
|
|
1688
|
+
### 7.2 민감 정보 필터링
|
|
1689
|
+
|
|
1690
|
+
```typescript
|
|
1691
|
+
// API 응답에서 민감 정보 제거
|
|
1692
|
+
function sanitizeEvent(event: Event): SanitizedEvent {
|
|
1693
|
+
return {
|
|
1694
|
+
...event,
|
|
1695
|
+
payload: maskSensitiveFields(event.payload)
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
```
|
|
1699
|
+
|
|
1700
|
+
## 8. 성공 기준
|
|
1701
|
+
|
|
1702
|
+
- [ ] localhost:37777에서 대시보드 접근 가능
|
|
1703
|
+
- [ ] 세션 목록 및 상세 조회 동작
|
|
1704
|
+
- [ ] 실시간 이벤트 스트림 표시
|
|
1705
|
+
- [ ] 검색 기능 동작 (벡터 + FTS)
|
|
1706
|
+
- [ ] 통계 대시보드 표시
|
|
1707
|
+
- [ ] WebSocket 연결 안정적
|
|
1708
|
+
- [ ] 첫 로드 < 1초
|
|
1709
|
+
- [ ] API 응답 < 200ms
|