ralph-mem 0.1.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.
@@ -0,0 +1,303 @@
1
+ import {
2
+ createSearchEngine
3
+ } from "../chunk-kga64hvg.js";
4
+ import {
5
+ createDBClient
6
+ } from "../chunk-41rc1bhg.js";
7
+ import {
8
+ ensureProjectDirs,
9
+ getProjectDBPath
10
+ } from "../chunk-w40c0y00.js";
11
+ import"../chunk-ns0dgdnb.js";
12
+
13
+ // src/skills/mem-search.ts
14
+ function parseSince(since) {
15
+ const relativeMatch = since.match(/^(\d+)d$/);
16
+ if (relativeMatch) {
17
+ const days = Number.parseInt(relativeMatch[1], 10);
18
+ const date2 = new Date;
19
+ date2.setDate(date2.getDate() - days);
20
+ return date2;
21
+ }
22
+ const date = new Date(since);
23
+ if (!Number.isNaN(date.getTime())) {
24
+ return date;
25
+ }
26
+ return null;
27
+ }
28
+ function parseType(type) {
29
+ const validTypes = [
30
+ "note",
31
+ "tool_use",
32
+ "bash",
33
+ "error",
34
+ "success"
35
+ ];
36
+ const types = type.split(",").map((t) => t.trim().toLowerCase());
37
+ return types.filter((t) => validTypes.includes(t));
38
+ }
39
+ function formatTable(results) {
40
+ if (results.length === 0) {
41
+ return "검색 결과가 없습니다.";
42
+ }
43
+ const lines = [];
44
+ lines.push("┌────────────────┬───────┬─────────────────────────────────────┐");
45
+ lines.push("│ ID │ 점수 │ 요약 │");
46
+ lines.push("├────────────────┼───────┼─────────────────────────────────────┤");
47
+ for (const result of results) {
48
+ const id = result.id.slice(0, 14).padEnd(14);
49
+ const score = result.score.toFixed(2).padStart(5);
50
+ const summary = (result.summary || "(없음)").slice(0, 35).padEnd(35);
51
+ lines.push(`│ ${id} │ ${score} │ ${summary} │`);
52
+ }
53
+ lines.push("└────────────────┴───────┴─────────────────────────────────────┘");
54
+ return lines.join(`
55
+ `);
56
+ }
57
+ function formatTimeline(results) {
58
+ if (results.length === 0) {
59
+ return "검색 결과가 없습니다.";
60
+ }
61
+ const lines = [];
62
+ for (const result of results) {
63
+ const date = result.createdAt ? result.createdAt.toLocaleDateString("ko-KR", {
64
+ year: "numeric",
65
+ month: "2-digit",
66
+ day: "2-digit"
67
+ }) : "날짜 없음";
68
+ const type = result.type || "unknown";
69
+ const tool = result.toolName ? ` (${result.toolName})` : "";
70
+ lines.push(`\uD83D\uDCCC ${result.id}`);
71
+ lines.push(` 날짜: ${date}`);
72
+ lines.push(` 유형: ${type}${tool}`);
73
+ lines.push(` 점수: ${result.score.toFixed(2)}`);
74
+ lines.push(` 요약: ${result.summary || "(없음)"}`);
75
+ lines.push("");
76
+ }
77
+ return lines.join(`
78
+ `);
79
+ }
80
+ function formatDetail(result) {
81
+ const lines = [];
82
+ lines.push(`\uD83D\uDCC4 ${result.id} 상세`);
83
+ lines.push("");
84
+ if (result.createdAt) {
85
+ const date = result.createdAt.toLocaleDateString("ko-KR", {
86
+ year: "numeric",
87
+ month: "2-digit",
88
+ day: "2-digit",
89
+ hour: "2-digit",
90
+ minute: "2-digit"
91
+ });
92
+ lines.push(`세션: ${date}`);
93
+ }
94
+ if (result.sessionId) {
95
+ lines.push(`세션 ID: ${result.sessionId}`);
96
+ }
97
+ lines.push(`유형: ${result.type || "unknown"}`);
98
+ if (result.toolName) {
99
+ lines.push(`도구: ${result.toolName}`);
100
+ }
101
+ lines.push(`점수: ${result.score.toFixed(4)}`);
102
+ if (result.metadata) {
103
+ lines.push(`중요도: ${result.metadata.importance || 0.5}`);
104
+ if (result.metadata.projectPath) {
105
+ lines.push(`프로젝트: ${result.metadata.projectPath}`);
106
+ }
107
+ }
108
+ lines.push("");
109
+ lines.push("내용:");
110
+ lines.push("─".repeat(60));
111
+ lines.push(result.content || result.summary || "(내용 없음)");
112
+ lines.push("─".repeat(60));
113
+ return lines.join(`
114
+ `);
115
+ }
116
+ function formatDetails(results) {
117
+ if (results.length === 0) {
118
+ return "검색 결과가 없습니다.";
119
+ }
120
+ return results.map(formatDetail).join(`
121
+
122
+ `);
123
+ }
124
+ async function memSearchSkill(input, options) {
125
+ const { query, projectPath, layer = 1, limit = 10, since, type, id } = input;
126
+ ensureProjectDirs(projectPath);
127
+ const dbPath = getProjectDBPath(projectPath);
128
+ const client = options?.client ?? createDBClient(dbPath);
129
+ const engine = options?.engine ?? createSearchEngine(client);
130
+ if (id) {
131
+ const observation = client.getObservation(id);
132
+ if (!options?.client) {
133
+ client.close();
134
+ }
135
+ if (!observation) {
136
+ return {
137
+ results: [],
138
+ totalCount: 0,
139
+ layer: 3,
140
+ formatted: `ID '${id}'를 찾을 수 없습니다.`
141
+ };
142
+ }
143
+ const result = {
144
+ id: observation.id,
145
+ score: 1,
146
+ summary: observation.content.slice(0, 100),
147
+ createdAt: new Date(observation.created_at),
148
+ sessionId: observation.session_id,
149
+ type: observation.type,
150
+ toolName: observation.tool_name ?? undefined,
151
+ content: observation.content,
152
+ metadata: {
153
+ importance: observation.importance
154
+ }
155
+ };
156
+ return {
157
+ results: [result],
158
+ totalCount: 1,
159
+ layer: 3,
160
+ formatted: formatDetail(result)
161
+ };
162
+ }
163
+ const searchOptions = {
164
+ limit,
165
+ layer,
166
+ projectPath
167
+ };
168
+ if (since) {
169
+ const sinceDate = parseSince(since);
170
+ if (sinceDate) {
171
+ searchOptions.since = sinceDate;
172
+ }
173
+ }
174
+ if (type) {
175
+ const types = parseType(type);
176
+ if (types.length > 0) {
177
+ searchOptions.types = types;
178
+ }
179
+ }
180
+ let results = [];
181
+ try {
182
+ results = engine.search(query, searchOptions);
183
+ } catch {
184
+ if (!options?.client) {
185
+ client.close();
186
+ }
187
+ return {
188
+ results: [],
189
+ totalCount: 0,
190
+ layer,
191
+ formatted: "검색 중 오류가 발생했습니다."
192
+ };
193
+ }
194
+ if (!options?.client) {
195
+ client.close();
196
+ }
197
+ let formatted;
198
+ if (layer === 1) {
199
+ formatted = `\uD83D\uDD0D 검색 결과: "${query}" (${results.length}건)
200
+
201
+ ${formatTable(results)}`;
202
+ } else if (layer === 2) {
203
+ formatted = `\uD83D\uDD0D 검색 결과: "${query}" (${results.length}건)
204
+
205
+ ${formatTimeline(results)}`;
206
+ } else {
207
+ formatted = `\uD83D\uDD0D 검색 결과: "${query}" (${results.length}건)
208
+
209
+ ${formatDetails(results)}`;
210
+ }
211
+ return {
212
+ results,
213
+ totalCount: results.length,
214
+ layer,
215
+ formatted
216
+ };
217
+ }
218
+ function parseArgs(argsString, projectPath) {
219
+ const args = {
220
+ query: "",
221
+ projectPath
222
+ };
223
+ const tokens = [];
224
+ let current = "";
225
+ let inQuotes = false;
226
+ for (const char of argsString) {
227
+ if (char === '"' || char === "'") {
228
+ inQuotes = !inQuotes;
229
+ } else if (char === " " && !inQuotes) {
230
+ if (current) {
231
+ tokens.push(current);
232
+ current = "";
233
+ }
234
+ } else {
235
+ current += char;
236
+ }
237
+ }
238
+ if (current) {
239
+ tokens.push(current);
240
+ }
241
+ let i = 0;
242
+ while (i < tokens.length) {
243
+ const token = tokens[i];
244
+ if (token === "--layer" && i + 1 < tokens.length) {
245
+ const layerNum = Number.parseInt(tokens[i + 1], 10);
246
+ if (layerNum >= 1 && layerNum <= 3) {
247
+ args.layer = layerNum;
248
+ }
249
+ i += 2;
250
+ } else if (token === "--limit" && i + 1 < tokens.length) {
251
+ args.limit = Number.parseInt(tokens[i + 1], 10);
252
+ i += 2;
253
+ } else if (token === "--since" && i + 1 < tokens.length) {
254
+ args.since = tokens[i + 1];
255
+ i += 2;
256
+ } else if (token === "--type" && i + 1 < tokens.length) {
257
+ args.type = tokens[i + 1];
258
+ i += 2;
259
+ } else if (token.startsWith("obs-")) {
260
+ args.id = token;
261
+ args.layer = 3;
262
+ i++;
263
+ } else if (!token.startsWith("--")) {
264
+ args.query = args.query ? `${args.query} ${token}` : token;
265
+ i++;
266
+ } else {
267
+ i++;
268
+ }
269
+ }
270
+ return args;
271
+ }
272
+ async function executeMemSearch(argsString, projectPath, options) {
273
+ const input = parseArgs(argsString, projectPath);
274
+ if (!input.query && !input.id) {
275
+ return `사용법: /mem-search <query> [options]
276
+
277
+ 옵션:
278
+ --layer <1|2|3> 상세 수준 (기본: 1)
279
+ --since <7d|30d|YYYY-MM-DD> 기간 필터
280
+ --type <error|success|bash|tool_use|note> 유형 필터
281
+ --limit <n> 결과 수 제한
282
+
283
+ 예시:
284
+ /mem-search "JWT authentication"
285
+ /mem-search --layer 3 obs-a1b2c3d4
286
+ /mem-search "database" --since 7d --type error`;
287
+ }
288
+ const result = await memSearchSkill(input, options);
289
+ return result.formatted;
290
+ }
291
+ export {
292
+ parseType,
293
+ parseSince,
294
+ parseArgs,
295
+ memSearchSkill,
296
+ formatTimeline,
297
+ formatTable,
298
+ formatDetails,
299
+ formatDetail,
300
+ executeMemSearch
301
+ };
302
+
303
+ export { memSearchSkill };
@@ -0,0 +1,200 @@
1
+ import {
2
+ getProjectDBPath
3
+ } from "../chunk-w40c0y00.js";
4
+ import"../chunk-ns0dgdnb.js";
5
+
6
+ // src/skills/mem-status.ts
7
+ import { existsSync, statSync } from "node:fs";
8
+
9
+ // src/utils/tokens.ts
10
+ var CHARS_PER_TOKEN_EN = 4;
11
+ var CHARS_PER_TOKEN_KO = 1.5;
12
+ var CHARS_PER_TOKEN_CODE = 3.5;
13
+ function isKorean(char) {
14
+ const code = char.charCodeAt(0);
15
+ return code >= 44032 && code <= 55203 || code >= 4352 && code <= 4607 || code >= 12592 && code <= 12687;
16
+ }
17
+ function isCodeLike(text) {
18
+ const codePatterns = [
19
+ /\bfunction\b/,
20
+ /\bconst\b/,
21
+ /\blet\b/,
22
+ /\bvar\b/,
23
+ /\bclass\b/,
24
+ /\bimport\b/,
25
+ /\bexport\b/,
26
+ /\breturn\b/,
27
+ /\bif\s*\(/,
28
+ /\bfor\s*\(/,
29
+ /\bwhile\s*\(/,
30
+ /=>/,
31
+ /\{\s*$/m,
32
+ /^\s*\}/m,
33
+ /\[\s*$/m,
34
+ /^\s*\]/m,
35
+ /;\s*$/m
36
+ ];
37
+ let matches = 0;
38
+ for (const pattern of codePatterns) {
39
+ if (pattern.test(text)) {
40
+ matches++;
41
+ }
42
+ }
43
+ return matches >= 3;
44
+ }
45
+ function estimateTokens(text) {
46
+ if (!text)
47
+ return 0;
48
+ if (isCodeLike(text)) {
49
+ return Math.ceil(text.length / CHARS_PER_TOKEN_CODE);
50
+ }
51
+ let koreanChars = 0;
52
+ let otherChars = 0;
53
+ for (const char of text) {
54
+ if (isKorean(char)) {
55
+ koreanChars++;
56
+ } else {
57
+ otherChars++;
58
+ }
59
+ }
60
+ const koreanTokens = Math.ceil(koreanChars / CHARS_PER_TOKEN_KO);
61
+ const otherTokens = Math.ceil(otherChars / CHARS_PER_TOKEN_EN);
62
+ return koreanTokens + otherTokens;
63
+ }
64
+ function countTokens(text) {
65
+ return estimateTokens(text);
66
+ }
67
+
68
+ // src/skills/mem-status.ts
69
+ function getDBSize(projectPath) {
70
+ try {
71
+ const dbPath = getProjectDBPath(projectPath);
72
+ if (!existsSync(dbPath)) {
73
+ return 0;
74
+ }
75
+ const stats = statSync(dbPath);
76
+ return Number((stats.size / (1024 * 1024)).toFixed(2));
77
+ } catch {
78
+ return 0;
79
+ }
80
+ }
81
+ function getRecentSessionCount(client, days) {
82
+ const cutoff = new Date;
83
+ cutoff.setDate(cutoff.getDate() - days);
84
+ const cutoffStr = cutoff.toISOString();
85
+ const result = client.db.prepare("SELECT COUNT(*) as count FROM sessions WHERE started_at >= ?").get(cutoffStr);
86
+ return result.count;
87
+ }
88
+ function getSessionTokenUsage(client, sessionId) {
89
+ const observations = client.listObservations(sessionId);
90
+ let totalTokens = 0;
91
+ for (const obs of observations) {
92
+ const content = obs.content_compressed || obs.content;
93
+ totalTokens += countTokens(content);
94
+ }
95
+ return totalTokens;
96
+ }
97
+ function getLoopStats(client, sessionId) {
98
+ const runs = client.listLoopRuns(sessionId, 100);
99
+ const total = runs.length;
100
+ if (total === 0) {
101
+ return { total: 0, successRate: 0 };
102
+ }
103
+ const successful = runs.filter((r) => r.status === "success").length;
104
+ const successRate = Math.round(successful / total * 100);
105
+ return { total, successRate };
106
+ }
107
+ function isLoopActive(client, sessionId) {
108
+ const runs = client.listLoopRuns(sessionId, 1);
109
+ return runs.length > 0 && runs[0].status === "running";
110
+ }
111
+ function getMemStatus(context) {
112
+ const { projectPath, sessionId, client, contextBudget = 1e5 } = context;
113
+ const allSessions = client.db.prepare("SELECT COUNT(*) as count FROM sessions").get();
114
+ const recentSessions = getRecentSessionCount(client, 30);
115
+ const allObservations = client.db.prepare("SELECT COUNT(*) as count FROM observations").get();
116
+ const dbSizeMB = getDBSize(projectPath);
117
+ const currentTokens = getSessionTokenUsage(client, sessionId);
118
+ const budgetPercent = Math.round(currentTokens / contextBudget * 100);
119
+ const loopStats = getLoopStats(client, sessionId);
120
+ const loopActive = isLoopActive(client, sessionId);
121
+ const configPath = `${projectPath}/.ralph-mem/config.yaml`;
122
+ return {
123
+ sessions: {
124
+ total: allSessions.count,
125
+ recent: recentSessions
126
+ },
127
+ observations: {
128
+ total: allObservations.count
129
+ },
130
+ storage: {
131
+ dbSizeMB
132
+ },
133
+ tokens: {
134
+ currentSession: currentTokens,
135
+ budgetUsed: currentTokens,
136
+ budgetPercent
137
+ },
138
+ loop: {
139
+ isActive: loopActive,
140
+ totalRuns: loopStats.total,
141
+ successRate: loopStats.successRate
142
+ },
143
+ configPath: existsSync(configPath) ? configPath : null
144
+ };
145
+ }
146
+ function formatNumber(n) {
147
+ return n.toLocaleString();
148
+ }
149
+ function formatMemStatus(status) {
150
+ const loopStatus = status.loop.isActive ? "실행 중" : "비활성";
151
+ return `\uD83D\uDCCA ralph-mem 상태
152
+
153
+ 메모리:
154
+ ├─ 세션: ${status.sessions.total}개 (최근 30일: ${status.sessions.recent}개)
155
+ ├─ 관찰: ${formatNumber(status.observations.total)}개
156
+ └─ 용량: ${status.storage.dbSizeMB} MB
157
+
158
+ 토큰:
159
+ ├─ 현재 세션: ${formatNumber(status.tokens.currentSession)} tokens
160
+ ├─ Budget: ${formatNumber(status.tokens.budgetUsed)} tokens (${status.tokens.budgetPercent}%)
161
+ └─ 사용률: ${status.tokens.budgetPercent}%
162
+
163
+ Loop:
164
+ ├─ 현재: ${loopStatus}
165
+ ├─ 총 실행: ${status.loop.totalRuns}회
166
+ └─ 성공률: ${status.loop.successRate}%
167
+
168
+ 설정: ${status.configPath || "(없음)"}`;
169
+ }
170
+ async function executeMemStatus(context) {
171
+ const status = getMemStatus(context);
172
+ return formatMemStatus(status);
173
+ }
174
+ function createMemStatusSkill(context) {
175
+ return {
176
+ name: "/mem-status",
177
+ async execute() {
178
+ return executeMemStatus(context);
179
+ },
180
+ getStatus() {
181
+ return getMemStatus(context);
182
+ }
183
+ };
184
+ }
185
+ async function memStatusSkill(_input) {
186
+ return {
187
+ sessionCount: 0,
188
+ observationCount: 0,
189
+ totalTokens: 0
190
+ };
191
+ }
192
+ export {
193
+ memStatusSkill,
194
+ getMemStatus,
195
+ formatMemStatus,
196
+ executeMemStatus,
197
+ createMemStatusSkill
198
+ };
199
+
200
+ export { memStatusSkill };