ralph-mem 0.1.0 → 0.1.3

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.
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "ralph-mem",
3
+ "description": "Ralph Loop 기반 반복 실행 및 세션 간 컨텍스트 영속성 관리",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "roboco-io"
7
+ },
8
+ "homepage": "https://github.com/roboco-io/ralph-mem",
9
+ "repository": "https://github.com/roboco-io/ralph-mem",
10
+ "license": "MIT"
11
+ }
package/README.md CHANGED
@@ -1,10 +1,17 @@
1
1
  # ralph-mem
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/ralph-mem.svg)](https://www.npmjs.com/package/ralph-mem)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.6-blue.svg)](https://www.typescriptlang.org/)
6
+ [![Bun](https://img.shields.io/badge/Bun-1.0+-black.svg)](https://bun.sh/)
7
+
3
8
  Claude Code를 위한 Ralph Loop 기반 지속적 컨텍스트 관리 플러그인
4
9
 
5
10
  ## 개요
6
11
 
7
- ralph-mem은 [Ralph Loop](https://ghuntley.com/ralph/)의 "성공할 때까지 반복" 철학과 [claude-mem](https://github.com/thedotmack/claude-mem) "지능적 컨텍스트 관리"를 결합한 Claude Code 플러그인입니다.
12
+ ralph-mem은 [Geoffrey Huntley](https://ghuntley.com/)의 [Ralph Loop](https://ghuntley.com/ralph/)와 [thedotmack](https://github.com/thedotmack)의 [claude-mem](https://github.com/thedotmack/claude-mem)에서 영감을 받아 시작된 프로젝트입니다.
13
+
14
+ Ralph Loop의 "성공할 때까지 반복" 철학과 claude-mem의 "지능적 컨텍스트 관리"를 결합하여 Claude Code를 위한 지속적 메모리 관리 플러그인을 구현했습니다.
8
15
 
9
16
  ### 해결하는 문제
10
17
 
@@ -60,6 +67,7 @@ flowchart TB
60
67
 
61
68
  - `SessionStart` - 관련 메모리 자동 주입
62
69
  - `PostToolUse` - 도구 사용 결과 기록
70
+ - `Stop` - 세션 강제 종료 시 정리 작업
63
71
  - `SessionEnd` - 세션 요약 생성 및 저장
64
72
 
65
73
  ### 3. Progressive Disclosure
@@ -79,11 +87,63 @@ flowchart TB
79
87
 
80
88
  ## 설치
81
89
 
90
+ ### npm
91
+
92
+ ```bash
93
+ npm install ralph-mem
94
+ ```
95
+
96
+ ### yarn
97
+
98
+ ```bash
99
+ yarn add ralph-mem
100
+ ```
101
+
102
+ ### pnpm
103
+
82
104
  ```bash
83
- # Claude Code 플러그인으로 설치
84
- claude plugins install ralph-mem
105
+ pnpm add ralph-mem
106
+ ```
107
+
108
+ ### bun
109
+
110
+ ```bash
111
+ bun add ralph-mem
112
+ ```
113
+
114
+ ### Claude Code 플러그인
115
+
116
+ Claude Code에서 플러그인으로 사용하려면 [roboco-io/plugins](https://github.com/roboco-io/plugins) 마켓플레이스를 통해 설치합니다:
117
+
118
+ 1. 마켓플레이스 추가
119
+ ```
120
+ /plugin marketplace add roboco-io/plugins
121
+ ```
122
+
123
+ 2. 플러그인 설치
124
+ ```
125
+ /plugin install ralph-mem@roboco-plugins
85
126
  ```
86
127
 
128
+ 또는 `/plugin` 명령으로 플러그인 매니저를 열어 UI에서 설치할 수 있습니다.
129
+
130
+ ### 플러그인 업데이트
131
+
132
+ 방법 1: CLI 명령으로 업데이트 (터미널에서 실행)
133
+ ```
134
+ claude plugin update ralph-mem@roboco-plugins
135
+ ```
136
+
137
+ 방법 2: 재설치 (세션 내에서 실행)
138
+ ```
139
+ /plugin uninstall ralph-mem
140
+ ```
141
+ ```
142
+ /plugin install ralph-mem@roboco-plugins
143
+ ```
144
+
145
+ 업데이트 후 Claude Code를 재시작하면 변경 사항이 적용됩니다.
146
+
87
147
  ## 사용법
88
148
 
89
149
  ### Ralph Loop
@@ -128,6 +188,38 @@ claude plugins install ralph-mem
128
188
  /mem-forget <observation-id>
129
189
  ```
130
190
 
191
+ ### 4. Privacy 기능
192
+
193
+ 민감한 정보를 메모리에서 제외합니다.
194
+
195
+ **`<private>` 태그:**
196
+
197
+ ```bash
198
+ # 태그로 감싼 내용은 저장되지 않습니다
199
+ My API key is <private>sk-1234567890</private>
200
+ # 저장됨: My API key is [PRIVATE]
201
+ ```
202
+
203
+ **설정 기반 제외:**
204
+
205
+ ```yaml
206
+ privacy:
207
+ exclude_patterns:
208
+ - "*.env"
209
+ - "*password*"
210
+ - "*secret*"
211
+ ```
212
+
213
+ ### 5. MCP 도구
214
+
215
+ 스킬 외에 MCP(Model Context Protocol) 도구로도 메모리에 접근할 수 있습니다.
216
+
217
+ | 도구 | 설명 |
218
+ |------|------|
219
+ | `ralph_mem_search` | Progressive Disclosure 기반 검색 |
220
+ | `ralph_mem_timeline` | 특정 관찰 주변 시간순 컨텍스트 |
221
+ | `ralph_mem_get` | 관찰 ID로 전체 상세 조회 |
222
+
131
223
  ## 설정
132
224
 
133
225
  `~/.config/ralph-mem/config.yaml`:
@@ -153,6 +245,144 @@ privacy:
153
245
  - "*secret*"
154
246
  ```
155
247
 
248
+ ## 동작 원리
249
+
250
+ ralph-mem은 크게 두 가지 모드로 동작합니다:
251
+
252
+ 1. **자동 모드 (Lifecycle Hooks)**: 사용자 개입 없이 백그라운드에서 동작
253
+ 2. **명시적 모드 (Skills/Commands)**: 사용자가 슬래시 명령어로 직접 제어
254
+
255
+ ### Lifecycle Hooks
256
+
257
+ 플러그인이 설치되면 Claude Code의 lifecycle에 자동으로 연결되어 동작합니다.
258
+
259
+ ```mermaid
260
+ sequenceDiagram
261
+ participant CC as Claude Code
262
+ participant Hook as Hook Layer
263
+ participant Core as Core Layer
264
+ participant DB as SQLite
265
+
266
+ CC->>Hook: SessionStart
267
+ Hook->>Core: 관련 메모리 검색
268
+ Core->>DB: FTS5 + Embedding 검색
269
+ DB-->>Core: 이전 컨텍스트
270
+ Core-->>Hook: 검색 결과
271
+ Hook-->>CC: 컨텍스트 자동 주입
272
+
273
+ CC->>Hook: UserPromptSubmit
274
+ Hook->>Core: 쿼리 관련 검색
275
+ Core-->>Hook: 관련 메모리 알림
276
+ Hook-->>CC: 알림 표시 (주입 X)
277
+
278
+ CC->>Hook: PostToolUse
279
+ Hook->>Core: 도구 사용 결과 기록
280
+ Core->>DB: Observation 저장
281
+
282
+ CC->>Hook: SessionEnd
283
+ Hook->>Core: 세션 요약 생성
284
+ Core->>DB: 요약 저장
285
+ ```
286
+
287
+ | Hook | 시점 | 동작 |
288
+ |------|------|------|
289
+ | `SessionStart` | 세션 시작 | 프로젝트 관련 이전 컨텍스트 자동 주입 |
290
+ | `UserPromptSubmit` | 프롬프트 제출 | 관련 메모리 알림 (토큰 절약을 위해 주입하지 않음) |
291
+ | `PostToolUse` | 도구 사용 후 | 쓰기 도구, Bash 명령 결과를 Observation으로 기록 |
292
+ | `SessionEnd` | 세션 종료 | 세션 요약 생성 및 저장 |
293
+
294
+ ### Ralph Loop 동작
295
+
296
+ `/ralph start` 명령으로 활성화되며, 성공 기준 달성까지 자동 반복합니다.
297
+
298
+ ```mermaid
299
+ flowchart LR
300
+ A[Task + Context] --> B[Claude 실행]
301
+ B --> C{성공 판단}
302
+ C -->|YES| D[완료]
303
+ C -->|NO| E[결과 추가]
304
+ E --> F{중단 조건?}
305
+ F -->|NO| A
306
+ F -->|YES| G[실패 + 롤백 안내]
307
+ ```
308
+
309
+ **성공 판단**: Claude가 테스트/빌드 출력을 분석하여 성공 여부를 판단합니다.
310
+
311
+ **Overbaking 방지**: 무한 반복을 방지하기 위한 중단 조건:
312
+
313
+ | 조건 | 기본값 | 설명 |
314
+ |------|--------|------|
315
+ | `maxIterations` | 10 | 최대 반복 횟수 |
316
+ | `maxDurationMs` | 30분 | 최대 실행 시간 |
317
+ | `noProgressThreshold` | 3회 | 진척 없음 허용 횟수 |
318
+
319
+ **스냅샷**: Loop 시작 시 변경 파일을 스냅샷으로 저장하여 실패 시 롤백 가능.
320
+
321
+ ### 검색 엔진
322
+
323
+ 2단계 검색으로 최적의 결과를 반환합니다:
324
+
325
+ 1. **FTS5 전문 검색** (기본): SQLite FTS5를 사용한 빠른 텍스트 검색
326
+ 2. **Embedding 유사도** (폴백): FTS5 결과가 부족할 때 의미 기반 검색
327
+
328
+ **Embedding 모델**: `paraphrase-multilingual-MiniLM-L12-v2`
329
+ - 로컬 실행 (API 호출 없음)
330
+ - 50+ 언어 지원 (한국어, 영어 포함)
331
+ - 384차원, ~278MB
332
+
333
+ ### 데이터 흐름
334
+
335
+ ```mermaid
336
+ flowchart TB
337
+ subgraph Input["입력"]
338
+ Tool[도구 사용 결과]
339
+ Prompt[사용자 프롬프트]
340
+ end
341
+
342
+ subgraph Process["처리"]
343
+ Privacy[Privacy 필터]
344
+ Compress[압축기]
345
+ Embed[Embedding 생성]
346
+ end
347
+
348
+ subgraph Storage["저장"]
349
+ Obs[(Observations)]
350
+ Session[(Sessions)]
351
+ FTS[(FTS5 Index)]
352
+ Vec[(Embedding)]
353
+ end
354
+
355
+ Tool --> Privacy
356
+ Privacy --> Compress
357
+ Compress --> Obs
358
+ Obs --> FTS
359
+ Obs --> Embed
360
+ Embed --> Vec
361
+
362
+ Prompt --> FTS
363
+ Prompt --> Vec
364
+ FTS --> Result[검색 결과]
365
+ Vec --> Result
366
+ ```
367
+
368
+ ### Observation 타입
369
+
370
+ 도구 사용 결과는 타입별로 분류되어 저장됩니다:
371
+
372
+ | 타입 | 설명 | 기록 대상 |
373
+ |------|------|----------|
374
+ | `tool_use` | 도구 사용 결과 | Edit, Write 등 쓰기 도구 |
375
+ | `bash` | 명령 실행 결과 | Bash 명령어 |
376
+ | `error` | 에러 발생 | 모든 에러 (높은 중요도) |
377
+ | `success` | 성공 기록 | 테스트 통과, 빌드 성공 |
378
+ | `note` | 수동 메모 | `/mem-inject`로 주입된 내용 |
379
+
380
+ **중요도 자동 산정**:
381
+ - 에러 발생: 1.0 (최고)
382
+ - 테스트 통과/실패: 0.9
383
+ - 파일 생성/수정: 0.7
384
+ - 일반 명령: 0.5
385
+
156
386
  ## 아키텍처
157
387
 
158
388
  ```mermaid
@@ -0,0 +1,17 @@
1
+ ---
2
+ description: 특정 메모리 항목 삭제
3
+ ---
4
+
5
+ # Memory Forget
6
+
7
+ 특정 메모리 항목을 삭제합니다.
8
+
9
+ ## 사용법
10
+
11
+ - `obs-id` - 특정 관찰 삭제
12
+ - `--session sess-id` - 특정 세션의 모든 관찰 삭제
13
+ - `--before 30d` - 30일 이전의 관찰 삭제
14
+ - `--type error` - 특정 타입의 관찰만 삭제
15
+ - `--dry-run` - 실제 삭제 없이 대상 확인
16
+
17
+ $ARGUMENTS
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: 수동으로 컨텍스트를 메모리에 주입
3
+ ---
4
+
5
+ # Memory Inject
6
+
7
+ 수동으로 컨텍스트를 메모리에 저장합니다.
8
+
9
+ ## 사용법
10
+
11
+ - `"context"` - 컨텍스트 저장
12
+ - `"context" --type note` - 타입 지정하여 저장
13
+ - `"context" --tags tag1,tag2` - 태그와 함께 저장
14
+
15
+ ## 관찰 타입
16
+
17
+ - `note` - 일반 메모 (기본값)
18
+ - `tool_use` - 도구 사용 기록
19
+ - `bash` - 명령어 실행 기록
20
+ - `error` - 에러 기록
21
+ - `success` - 성공 기록
22
+
23
+ $ARGUMENTS
@@ -0,0 +1,24 @@
1
+ ---
2
+ description: 저장된 메모리 검색 (Progressive disclosure로 토큰 효율적 사용)
3
+ ---
4
+
5
+ # Memory Search
6
+
7
+ 저장된 관찰 기록과 세션 정보를 검색합니다.
8
+
9
+ ## 사용법
10
+
11
+ - `"keyword"` - 키워드로 검색 (Layer 1)
12
+ - `"keyword" --layer 2` - 타임라인 컨텍스트 포함
13
+ - `--layer 3 obs-id` - 특정 관찰의 전체 상세 정보
14
+ - `"keyword" --since 7d` - 최근 7일 내 검색
15
+
16
+ ## 검색 레이어
17
+
18
+ | Layer | 내용 | 토큰 |
19
+ |-------|------|------|
20
+ | 1 | Index (ID + 점수) | 50-100/결과 |
21
+ | 2 | Timeline (시간순 컨텍스트) | 200-300/결과 |
22
+ | 3 | Full Details | 500-1000/결과 |
23
+
24
+ $ARGUMENTS
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: 메모리 상태와 통계 확인
3
+ ---
4
+
5
+ # Memory Status
6
+
7
+ 메모리 시스템의 현재 상태를 확인합니다.
8
+
9
+ ## 사용법
10
+
11
+ - (인수 없음) - 기본 상태 표시
12
+ - `--detailed` - 상세 통계 표시
13
+ - `--json` - JSON 형식으로 출력
14
+
15
+ ## 출력 정보
16
+
17
+ - 총 세션 수
18
+ - 총 관찰 수
19
+ - 타입별 관찰 분포
20
+ - 스토리지 사용량
21
+ - 최근 활동
22
+
23
+ $ARGUMENTS
@@ -0,0 +1,26 @@
1
+ ---
2
+ description: Ralph Loop 제어 - 성공할 때까지 반복 실행 (start, stop, status, config)
3
+ ---
4
+
5
+ # Ralph Loop
6
+
7
+ 성공 기준을 달성할 때까지 작업을 자동으로 반복 실행합니다.
8
+
9
+ ## 사용법
10
+
11
+ - `start "task"` - Loop 시작
12
+ - `start "task" --criteria lint_clean` - 커스텀 성공 기준으로 시작
13
+ - `stop` - Loop 중단
14
+ - `stop --rollback` - 변경사항 롤백하며 중단
15
+ - `status` - 현재 상태 확인
16
+ - `config` - 설정 확인/변경
17
+
18
+ ## 성공 기준
19
+
20
+ - `test_pass` - 테스트 통과 (기본값)
21
+ - `build_success` - 빌드 성공
22
+ - `lint_clean` - Lint 오류 없음
23
+ - `type_check` - 타입 체크 통과
24
+ - `custom` - 사용자 정의 명령
25
+
26
+ $ARGUMENTS
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createDBClient
3
- } from "./chunk-41rc1bhg.js";
3
+ } from "./chunk-jz140n5a.js";
4
4
 
5
5
  // src/core/store.ts
6
6
  function toSession(dbSession) {
@@ -69,7 +69,7 @@ var require_file_uri_to_path = __commonJS((exports, module) => {
69
69
 
70
70
  // node_modules/bindings/bindings.js
71
71
  var require_bindings = __commonJS((exports, module) => {
72
- var __filename = "/Users/dohyunjung/Workspace/roboco-io/ralph-mem/node_modules/bindings/bindings.js";
72
+ var __filename = "/home/runner/work/ralph-mem/ralph-mem/node_modules/bindings/bindings.js";
73
73
  var fs = __require("fs");
74
74
  var path = __require("path");
75
75
  var fileURLToPath = require_file_uri_to_path();
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createDBClient
3
- } from "./chunk-41rc1bhg.js";
3
+ } from "./chunk-jz140n5a.js";
4
4
 
5
5
  // src/core/search.ts
6
6
  function escapeFtsQuery(query) {
@@ -3,13 +3,28 @@ import {
3
3
  } from "../chunk-c3a91ngd.js";
4
4
  import {
5
5
  createDBClient
6
- } from "../chunk-41rc1bhg.js";
6
+ } from "../chunk-jz140n5a.js";
7
7
  import {
8
8
  ensureProjectDirs,
9
9
  getProjectDBPath
10
10
  } from "../chunk-w40c0y00.js";
11
11
  import"../chunk-ns0dgdnb.js";
12
12
 
13
+ // src/utils/privacy.ts
14
+ function stripPrivateTags(content) {
15
+ const privateTagRegex = /<private>[\s\S]*?<\/private>/gi;
16
+ return content.replace(privateTagRegex, "[PRIVATE]");
17
+ }
18
+ function isEntirelyPrivate(content) {
19
+ const trimmed = content.trim();
20
+ const fullPrivateRegex = /^<private>[\s\S]*<\/private>$/i;
21
+ if (fullPrivateRegex.test(trimmed)) {
22
+ const stripped = stripPrivateTags(trimmed);
23
+ return stripped.trim() === "[PRIVATE]";
24
+ }
25
+ return false;
26
+ }
27
+
13
28
  // src/hooks/post-tool-use.ts
14
29
  var RECORDABLE_TOOLS = new Set([
15
30
  "Edit",
@@ -97,6 +112,15 @@ async function postToolUseHook(context, options) {
97
112
  content = `Error: ${context.error}
98
113
  ${content}`;
99
114
  }
115
+ if (isEntirelyPrivate(content)) {
116
+ return {
117
+ observationId: null,
118
+ recorded: false,
119
+ type: null,
120
+ importance: 0
121
+ };
122
+ }
123
+ content = stripPrivateTags(content);
100
124
  if (shouldExclude(content, config.privacy.exclude_patterns)) {
101
125
  return {
102
126
  observationId: null,
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  createMemoryStore
3
- } from "../chunk-v8anyhk1.js";
3
+ } from "../chunk-gy7xx0jv.js";
4
4
  import {
5
5
  createDBClient
6
- } from "../chunk-41rc1bhg.js";
6
+ } from "../chunk-jz140n5a.js";
7
7
  import {
8
8
  ensureProjectDirs,
9
9
  getProjectDBPath
@@ -86,4 +86,3 @@ export {
86
86
  sessionEndHook,
87
87
  generateSummary
88
88
  };
89
- export { sessionEndHook };
@@ -4,10 +4,10 @@ import {
4
4
  import {
5
5
  createMemoryStore,
6
6
  estimateTokens
7
- } from "../chunk-v8anyhk1.js";
7
+ } from "../chunk-gy7xx0jv.js";
8
8
  import {
9
9
  createDBClient
10
- } from "../chunk-41rc1bhg.js";
10
+ } from "../chunk-jz140n5a.js";
11
11
  import {
12
12
  ensureProjectDirs,
13
13
  getProjectDBPath
@@ -92,4 +92,3 @@ export {
92
92
  formatSessionContext,
93
93
  backupDatabase
94
94
  };
95
- export { sessionStartHook };
@@ -0,0 +1,61 @@
1
+ import {
2
+ createDBClient
3
+ } from "../chunk-jz140n5a.js";
4
+ import {
5
+ ensureProjectDirs,
6
+ getProjectDBPath
7
+ } from "../chunk-w40c0y00.js";
8
+ import"../chunk-ns0dgdnb.js";
9
+
10
+ // src/hooks/stop.ts
11
+ async function stopHook(context, options) {
12
+ const {
13
+ sessionId,
14
+ projectPath,
15
+ signal = "SIGINT",
16
+ activeLoopRunId
17
+ } = context;
18
+ let client;
19
+ if (options?.client) {
20
+ client = options.client;
21
+ } else {
22
+ ensureProjectDirs(projectPath);
23
+ const dbPath = getProjectDBPath(projectPath);
24
+ client = createDBClient(dbPath);
25
+ }
26
+ try {
27
+ const session = client.getSession(sessionId);
28
+ if (!session || session.ended_at) {
29
+ return {
30
+ sessionEnded: false,
31
+ loopStopped: false,
32
+ summary: "세션이 이미 종료되었거나 존재하지 않습니다."
33
+ };
34
+ }
35
+ let loopStopped = false;
36
+ if (activeLoopRunId) {
37
+ client.db.prepare(`
38
+ UPDATE loop_runs
39
+ SET status = 'stopped', ended_at = datetime('now')
40
+ WHERE id = ? AND status = 'running'
41
+ `).run(activeLoopRunId);
42
+ loopStopped = true;
43
+ }
44
+ const observations = client.listObservations(sessionId, 1000);
45
+ const summary = `[${signal}] 세션 강제 종료. 작업 ${observations.length}건 기록됨.`;
46
+ client.endSession(sessionId, summary);
47
+ return {
48
+ sessionEnded: true,
49
+ loopStopped,
50
+ summary
51
+ };
52
+ } finally {
53
+ if (!options?.client) {
54
+ client.close();
55
+ }
56
+ }
57
+ }
58
+ export {
59
+ stopHook
60
+ };
61
+
@@ -3,13 +3,13 @@ import {
3
3
  } from "../chunk-c3a91ngd.js";
4
4
  import {
5
5
  estimateTokens
6
- } from "../chunk-v8anyhk1.js";
6
+ } from "../chunk-gy7xx0jv.js";
7
7
  import {
8
8
  createSearchEngine
9
- } from "../chunk-kga64hvg.js";
9
+ } from "../chunk-rcnembz5.js";
10
10
  import {
11
11
  createDBClient
12
- } from "../chunk-41rc1bhg.js";
12
+ } from "../chunk-jz140n5a.js";
13
13
  import {
14
14
  ensureProjectDirs,
15
15
  getProjectDBPath
@@ -231,4 +231,3 @@ export {
231
231
  extractKeywords
232
232
  };
233
233
 
234
- export { userPromptSubmitHook };
package/dist/index.js CHANGED
@@ -1,3 +1,22 @@
1
+ import"./chunk-c3a91ngd.js";
2
+ import {
3
+ createMemoryStore,
4
+ estimateTokens
5
+ } from "./chunk-gy7xx0jv.js";
6
+ import {
7
+ createSearchEngine
8
+ } from "./chunk-rcnembz5.js";
9
+ import {
10
+ createDBClient
11
+ } from "./chunk-jz140n5a.js";
12
+ import {
13
+ ensureProjectDirs,
14
+ getProjectDBPath
15
+ } from "./chunk-w40c0y00.js";
16
+ import"./chunk-ns0dgdnb.js";
17
+ import {
18
+ memInjectSkill
19
+ } from "./skills/mem-inject.js";
1
20
  import {
2
21
  ralphSkill
3
22
  } from "./skills/ralph.js";
@@ -7,36 +26,282 @@ import {
7
26
  import {
8
27
  userPromptSubmitHook
9
28
  } from "./hooks/user-prompt-submit.js";
29
+ import {
30
+ stopHook
31
+ } from "./hooks/stop.js";
10
32
  import {
11
33
  postToolUseHook
12
34
  } from "./hooks/post-tool-use.js";
13
35
  import {
14
36
  sessionStartHook
15
37
  } from "./hooks/session-start.js";
16
- import"./chunk-c3a91ngd.js";
17
38
  import {
18
39
  sessionEndHook
19
40
  } from "./hooks/session-end.js";
20
- import {
21
- createMemoryStore,
22
- estimateTokens
23
- } from "./chunk-v8anyhk1.js";
24
41
  import {
25
42
  memForgetSkill
26
43
  } from "./skills/mem-forget.js";
27
- import {
28
- memInjectSkill
29
- } from "./skills/mem-inject.js";
30
44
  import {
31
45
  memSearchSkill
32
46
  } from "./skills/mem-search.js";
33
- import {
34
- createSearchEngine
35
- } from "./chunk-kga64hvg.js";
36
- import"./chunk-41rc1bhg.js";
37
- import"./chunk-w40c0y00.js";
38
- import"./chunk-ns0dgdnb.js";
47
+ // src/mcp/server.ts
48
+ var MCP_TOOLS = [
49
+ {
50
+ name: "ralph_mem_search",
51
+ description: "Search memories using Progressive Disclosure. Layer 1 returns compact results (~50-100 tokens), Layer 2 adds context (~200-300 tokens), Layer 3 returns full details (~500-1000 tokens).",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: {
55
+ query: {
56
+ type: "string",
57
+ description: "Search query"
58
+ },
59
+ layer: {
60
+ type: "number",
61
+ description: "Detail level (1=index, 2=context, 3=full)",
62
+ enum: [1, 2, 3],
63
+ default: 1
64
+ },
65
+ limit: {
66
+ type: "number",
67
+ description: "Maximum results to return",
68
+ default: 10
69
+ },
70
+ since: {
71
+ type: "string",
72
+ description: "Filter results after this date (ISO format or relative like '7d')"
73
+ },
74
+ types: {
75
+ type: "array",
76
+ items: { type: "string" },
77
+ description: "Filter by observation types (tool_use, bash, error, success, note)"
78
+ }
79
+ },
80
+ required: ["query"]
81
+ }
82
+ },
83
+ {
84
+ name: "ralph_mem_timeline",
85
+ description: "Get time-ordered observations around a specific observation. Useful for understanding context.",
86
+ inputSchema: {
87
+ type: "object",
88
+ properties: {
89
+ observationId: {
90
+ type: "string",
91
+ description: "The observation ID to center the timeline around"
92
+ },
93
+ before: {
94
+ type: "number",
95
+ description: "Number of observations before",
96
+ default: 3
97
+ },
98
+ after: {
99
+ type: "number",
100
+ description: "Number of observations after",
101
+ default: 3
102
+ }
103
+ },
104
+ required: ["observationId"]
105
+ }
106
+ },
107
+ {
108
+ name: "ralph_mem_get",
109
+ description: "Get full details of a specific observation by ID.",
110
+ inputSchema: {
111
+ type: "object",
112
+ properties: {
113
+ id: {
114
+ type: "string",
115
+ description: "Observation ID"
116
+ }
117
+ },
118
+ required: ["id"]
119
+ }
120
+ }
121
+ ];
122
+ function parseRelativeDate(str) {
123
+ const match = str.match(/^(\d+)([dhm])$/);
124
+ if (!match)
125
+ return null;
126
+ const value = Number.parseInt(match[1], 10);
127
+ const unit = match[2];
128
+ const now = new Date;
129
+ switch (unit) {
130
+ case "d":
131
+ return new Date(now.getTime() - value * 24 * 60 * 60 * 1000);
132
+ case "h":
133
+ return new Date(now.getTime() - value * 60 * 60 * 1000);
134
+ case "m":
135
+ return new Date(now.getTime() - value * 60 * 1000);
136
+ default:
137
+ return null;
138
+ }
139
+ }
140
+ function handleToolCall(call, projectPath) {
141
+ try {
142
+ ensureProjectDirs(projectPath);
143
+ const dbPath = getProjectDBPath(projectPath);
144
+ switch (call.name) {
145
+ case "ralph_mem_search":
146
+ return handleSearch(call.arguments, dbPath);
147
+ case "ralph_mem_timeline":
148
+ return handleTimeline(call.arguments, dbPath);
149
+ case "ralph_mem_get":
150
+ return handleGet(call.arguments, dbPath);
151
+ default:
152
+ return {
153
+ content: [{ type: "text", text: `Unknown tool: ${call.name}` }],
154
+ isError: true
155
+ };
156
+ }
157
+ } catch (error) {
158
+ const message = error instanceof Error ? error.message : String(error);
159
+ return {
160
+ content: [{ type: "text", text: `Error: ${message}` }],
161
+ isError: true
162
+ };
163
+ }
164
+ }
165
+ function handleSearch(args, dbPath) {
166
+ const query = args.query;
167
+ const layer = args.layer ?? 1;
168
+ const limit = args.limit ?? 10;
169
+ const sinceStr = args.since;
170
+ const types = args.types;
171
+ const options = { layer, limit, types };
172
+ if (sinceStr) {
173
+ const relativeDate = parseRelativeDate(sinceStr);
174
+ if (relativeDate) {
175
+ options.since = relativeDate;
176
+ } else {
177
+ const isoDate = new Date(sinceStr);
178
+ if (!Number.isNaN(isoDate.getTime())) {
179
+ options.since = isoDate;
180
+ }
181
+ }
182
+ }
183
+ const engine = createSearchEngine(dbPath);
184
+ try {
185
+ const results = engine.search(query, options);
186
+ if (results.length === 0) {
187
+ return {
188
+ content: [{ type: "text", text: "No results found." }]
189
+ };
190
+ }
191
+ const formatted = results.map((r, i) => {
192
+ const parts = [`${i + 1}. [${r.id}] (score: ${r.score.toFixed(2)})`];
193
+ if (r.summary) {
194
+ parts.push(` ${r.summary}`);
195
+ }
196
+ if (layer >= 2 && r.createdAt) {
197
+ parts.push(` Time: ${r.createdAt.toISOString()}`);
198
+ if (r.type)
199
+ parts.push(` Type: ${r.type}`);
200
+ if (r.toolName)
201
+ parts.push(` Tool: ${r.toolName}`);
202
+ }
203
+ if (layer >= 3 && r.content) {
204
+ parts.push(` Content: ${r.content}`);
205
+ }
206
+ return parts.join(`
207
+ `);
208
+ });
209
+ return {
210
+ content: [
211
+ {
212
+ type: "text",
213
+ text: `Found ${results.length} results:
214
+
215
+ ${formatted.join(`
39
216
 
217
+ `)}`
218
+ }
219
+ ]
220
+ };
221
+ } finally {
222
+ engine.close();
223
+ }
224
+ }
225
+ function handleTimeline(args, dbPath) {
226
+ const observationId = args.observationId;
227
+ const before = args.before ?? 3;
228
+ const after = args.after ?? 3;
229
+ const client = createDBClient(dbPath);
230
+ try {
231
+ const target = client.getObservation(observationId);
232
+ if (!target) {
233
+ return {
234
+ content: [
235
+ { type: "text", text: `Observation not found: ${observationId}` }
236
+ ],
237
+ isError: true
238
+ };
239
+ }
240
+ const beforeObs = client.db.prepare(`
241
+ SELECT * FROM observations
242
+ WHERE session_id = ? AND created_at < ?
243
+ ORDER BY created_at DESC
244
+ LIMIT ?
245
+ `).all(target.session_id, target.created_at, before);
246
+ const afterObs = client.db.prepare(`
247
+ SELECT * FROM observations
248
+ WHERE session_id = ? AND created_at > ?
249
+ ORDER BY created_at ASC
250
+ LIMIT ?
251
+ `).all(target.session_id, target.created_at, after);
252
+ const formatObs = (o, marker = "") => {
253
+ const summary = o.content.slice(0, 100) + (o.content.length > 100 ? "..." : "");
254
+ return `${marker}[${o.id}] ${o.created_at}
255
+ Type: ${o.type}${o.tool_name ? `, Tool: ${o.tool_name}` : ""}
256
+ ${summary}`;
257
+ };
258
+ const lines = [
259
+ "=== Timeline ===",
260
+ "",
261
+ ...beforeObs.reverse().map((o) => formatObs(o)),
262
+ "",
263
+ formatObs(target, ">>> "),
264
+ "",
265
+ ...afterObs.map((o) => formatObs(o))
266
+ ];
267
+ return {
268
+ content: [{ type: "text", text: lines.join(`
269
+ `) }]
270
+ };
271
+ } finally {
272
+ client.close();
273
+ }
274
+ }
275
+ function handleGet(args, dbPath) {
276
+ const id = args.id;
277
+ const client = createDBClient(dbPath);
278
+ try {
279
+ const obs = client.getObservation(id);
280
+ if (!obs) {
281
+ return {
282
+ content: [{ type: "text", text: `Observation not found: ${id}` }],
283
+ isError: true
284
+ };
285
+ }
286
+ const details = [
287
+ `ID: ${obs.id}`,
288
+ `Session: ${obs.session_id}`,
289
+ `Type: ${obs.type}`,
290
+ `Tool: ${obs.tool_name ?? "N/A"}`,
291
+ `Importance: ${obs.importance}`,
292
+ `Created: ${obs.created_at}`,
293
+ "",
294
+ "Content:",
295
+ obs.content
296
+ ];
297
+ return {
298
+ content: [{ type: "text", text: details.join(`
299
+ `) }]
300
+ };
301
+ } finally {
302
+ client.close();
303
+ }
304
+ }
40
305
  // src/index.ts
41
306
  var VERSION = "0.1.0";
42
307
  async function activate() {
@@ -47,6 +312,7 @@ async function deactivate() {
47
312
  }
48
313
  export {
49
314
  userPromptSubmitHook,
315
+ stopHook,
50
316
  sessionStartHook,
51
317
  sessionEndHook,
52
318
  ralphSkill,
@@ -55,10 +321,12 @@ export {
55
321
  memSearchSkill,
56
322
  memInjectSkill,
57
323
  memForgetSkill,
324
+ handleToolCall,
58
325
  estimateTokens,
59
326
  deactivate,
60
327
  createSearchEngine,
61
328
  createMemoryStore,
62
329
  activate,
63
- VERSION
330
+ VERSION,
331
+ MCP_TOOLS
64
332
  };
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  createSearchEngine
3
- } from "../chunk-kga64hvg.js";
3
+ } from "../chunk-rcnembz5.js";
4
4
  import {
5
5
  createDBClient
6
- } from "../chunk-41rc1bhg.js";
6
+ } from "../chunk-jz140n5a.js";
7
7
  import {
8
8
  ensureProjectDirs,
9
9
  getProjectDBPath
@@ -197,4 +197,3 @@ export {
197
197
  createMemStatusSkill
198
198
  };
199
199
 
200
- export { memStatusSkill };
@@ -3,7 +3,7 @@ import {
3
3
  } from "../chunk-c3a91ngd.js";
4
4
  import {
5
5
  createDBClient
6
- } from "../chunk-41rc1bhg.js";
6
+ } from "../chunk-jz140n5a.js";
7
7
  import {
8
8
  ensureProjectDirs,
9
9
  getProjectDBPath,
@@ -651,4 +651,3 @@ export {
651
651
  createRalphSkill
652
652
  };
653
653
 
654
- export { ralphSkill };
@@ -0,0 +1,26 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": ".*",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node $CLAUDE_PLUGIN_ROOT/dist/hooks/post-tool-use.js"
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "Stop": [
15
+ {
16
+ "matcher": ".*",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "node $CLAUDE_PLUGIN_ROOT/dist/hooks/stop.js"
21
+ }
22
+ ]
23
+ }
24
+ ]
25
+ }
26
+ }
package/package.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "ralph-mem",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "description": "Persistent context management plugin for Claude Code with Ralph Loop",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "files": [
9
9
  "dist",
10
- "plugin.json",
10
+ ".claude-plugin",
11
+ "commands",
12
+ "skills",
13
+ "hooks",
11
14
  "prompts"
12
15
  ],
13
16
  "repository": {
@@ -0,0 +1,25 @@
1
+ ---
2
+ name: mem-forget
3
+ description: 특정 메모리 항목을 삭제합니다. 더 이상 필요 없는 컨텍스트를 정리할 때 사용합니다.
4
+ ---
5
+
6
+ # Memory Forget
7
+
8
+ 특정 메모리 항목을 삭제합니다.
9
+
10
+ ## 사용법
11
+
12
+ ```
13
+ /mem-forget obs-a1b2c3d4
14
+ /mem-forget --session sess-xyz123
15
+ /mem-forget --before 30d
16
+ ```
17
+
18
+ ## 옵션
19
+
20
+ - `--session <id>` - 특정 세션의 모든 관찰 삭제
21
+ - `--before <duration>` - 지정 기간 이전의 관찰 삭제
22
+ - `--type <type>` - 특정 타입의 관찰만 삭제
23
+ - `--dry-run` - 실제 삭제 없이 대상 확인
24
+
25
+ $ARGUMENTS
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: mem-inject
3
+ description: 수동으로 컨텍스트를 메모리에 주입합니다. 중요한 정보를 영구 저장할 때 사용합니다.
4
+ ---
5
+
6
+ # Memory Inject
7
+
8
+ 수동으로 컨텍스트를 메모리에 저장합니다.
9
+
10
+ ## 사용법
11
+
12
+ ```
13
+ /mem-inject "이 프로젝트는 Express + Prisma 기반"
14
+ /mem-inject "API 엔드포인트는 /api/v1 prefix 사용" --type note
15
+ ```
16
+
17
+ ## 옵션
18
+
19
+ - `--type <type>` - 관찰 타입 (note, tool_use, bash, error, success)
20
+ - `--tags <tags>` - 태그 (쉼표로 구분)
21
+
22
+ $ARGUMENTS
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: mem-search
3
+ description: 저장된 메모리를 검색합니다. Progressive disclosure로 토큰을 효율적으로 사용합니다.
4
+ ---
5
+
6
+ # Memory Search
7
+
8
+ 저장된 관찰 기록과 세션 정보를 검색합니다.
9
+
10
+ ## 사용법
11
+
12
+ ```
13
+ /mem-search "authentication error"
14
+ /mem-search "JWT" --since 7d
15
+ /mem-search --layer 3 obs-a1b2c3d4
16
+ ```
17
+
18
+ ## 옵션
19
+
20
+ - `--layer <1|2|3>` - 검색 레이어 지정
21
+ - Layer 1: Index (ID + 점수) - 50-100 토큰/결과
22
+ - Layer 2: Timeline (시간순 컨텍스트) - 200-300 토큰/결과
23
+ - Layer 3: Full Details - 500-1000 토큰/결과
24
+ - `--since <duration>` - 시간 범위 (예: 7d, 24h)
25
+ - `--limit <n>` - 최대 결과 수
26
+
27
+ $ARGUMENTS
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: mem-status
3
+ description: 메모리 상태와 통계를 확인합니다. 저장된 세션 수, 관찰 수, 스토리지 사용량 등을 표시합니다.
4
+ ---
5
+
6
+ # Memory Status
7
+
8
+ 메모리 시스템의 현재 상태를 확인합니다.
9
+
10
+ ## 사용법
11
+
12
+ ```
13
+ /mem-status
14
+ /mem-status --detailed
15
+ ```
16
+
17
+ ## 출력 정보
18
+
19
+ - 총 세션 수
20
+ - 총 관찰 수
21
+ - 타입별 관찰 분포
22
+ - 스토리지 사용량
23
+ - 최근 활동
24
+
25
+ ## 옵션
26
+
27
+ - `--detailed` - 상세 통계 표시
28
+ - `--json` - JSON 형식으로 출력
29
+
30
+ $ARGUMENTS
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: ralph
3
+ description: Ralph Loop 제어 - 성공할 때까지 반복 실행합니다. start, stop, status, config 서브커맨드를 지원합니다.
4
+ ---
5
+
6
+ # Ralph Loop
7
+
8
+ 성공 기준을 달성할 때까지 작업을 자동으로 반복 실행합니다.
9
+
10
+ ## 사용법
11
+
12
+ ### 시작
13
+ ```
14
+ /ralph start "Implement feature X"
15
+ /ralph start "Fix lint errors" --criteria lint_clean
16
+ ```
17
+
18
+ ### 상태 확인
19
+ ```
20
+ /ralph status
21
+ ```
22
+
23
+ ### 중단
24
+ ```
25
+ /ralph stop
26
+ /ralph stop --rollback # 변경사항 롤백
27
+ ```
28
+
29
+ ### 설정
30
+ ```
31
+ /ralph config
32
+ /ralph config --max-iterations 15
33
+ ```
34
+
35
+ ## 성공 기준
36
+
37
+ - `test_pass` - 테스트 통과 (기본값)
38
+ - `build_success` - 빌드 성공
39
+ - `lint_clean` - Lint 오류 없음
40
+ - `type_check` - 타입 체크 통과
41
+ - `custom` - 사용자 정의 명령
42
+
43
+ $ARGUMENTS
package/plugin.json DELETED
@@ -1,51 +0,0 @@
1
- {
2
- "name": "ralph-mem",
3
- "version": "0.1.0",
4
- "description": "Persistent context management with Ralph Loop",
5
- "main": "dist/index.js",
6
- "hooks": [
7
- {
8
- "event": "SessionStart",
9
- "handler": "dist/hooks/session-start.js"
10
- },
11
- {
12
- "event": "SessionEnd",
13
- "handler": "dist/hooks/session-end.js"
14
- },
15
- {
16
- "event": "UserPromptSubmit",
17
- "handler": "dist/hooks/user-prompt-submit.js"
18
- },
19
- {
20
- "event": "PostToolUse",
21
- "handler": "dist/hooks/post-tool-use.js"
22
- }
23
- ],
24
- "skills": [
25
- {
26
- "name": "ralph",
27
- "description": "Ralph Loop control - start, stop, status",
28
- "handler": "dist/skills/ralph.js"
29
- },
30
- {
31
- "name": "mem-search",
32
- "description": "Search memory with progressive disclosure",
33
- "handler": "dist/skills/mem-search.js"
34
- },
35
- {
36
- "name": "mem-inject",
37
- "description": "Manually inject context into memory",
38
- "handler": "dist/skills/mem-inject.js"
39
- },
40
- {
41
- "name": "mem-forget",
42
- "description": "Remove specific memory entries",
43
- "handler": "dist/skills/mem-forget.js"
44
- },
45
- {
46
- "name": "mem-status",
47
- "description": "View memory status and statistics",
48
- "handler": "dist/skills/mem-status.js"
49
- }
50
- ]
51
- }