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,90 @@
1
+ import {
2
+ createDBClient
3
+ } from "./chunk-41rc1bhg.js";
4
+
5
+ // src/core/search.ts
6
+ function escapeFtsQuery(query) {
7
+ const escaped = query.replace(/"/g, '""').trim();
8
+ if (!escaped)
9
+ return "";
10
+ const words = escaped.split(/\s+/).filter((w) => w.length > 0);
11
+ if (words.length === 0)
12
+ return "";
13
+ return words.map((w) => `"${w}"*`).join(" OR ");
14
+ }
15
+ function createSearchEngine(dbPathOrClient) {
16
+ const client = typeof dbPathOrClient === "string" ? createDBClient(dbPathOrClient) : dbPathOrClient;
17
+ return {
18
+ search(query, options = {}) {
19
+ const { limit = 10, layer = 1, since, types, projectPath } = options;
20
+ const escapedQuery = escapeFtsQuery(query);
21
+ if (!escapedQuery)
22
+ return [];
23
+ const whereClauses = [];
24
+ const params = [];
25
+ if (since) {
26
+ whereClauses.push("o.created_at >= ?");
27
+ params.push(since.toISOString());
28
+ }
29
+ if (types && types.length > 0) {
30
+ const placeholders = types.map(() => "?").join(", ");
31
+ whereClauses.push(`o.type IN (${placeholders})`);
32
+ params.push(...types);
33
+ }
34
+ if (projectPath) {
35
+ whereClauses.push("s.project_path = ?");
36
+ params.push(projectPath);
37
+ }
38
+ const whereClause = whereClauses.length > 0 ? `AND ${whereClauses.join(" AND ")}` : "";
39
+ const sql = `
40
+ SELECT
41
+ o.id,
42
+ o.session_id,
43
+ o.type,
44
+ o.tool_name,
45
+ o.content,
46
+ o.importance,
47
+ o.created_at,
48
+ s.project_path,
49
+ bm25(observations_fts) as score
50
+ FROM observations_fts fts
51
+ JOIN observations o ON fts.rowid = o.rowid
52
+ JOIN sessions s ON o.session_id = s.id
53
+ WHERE observations_fts MATCH ?
54
+ ${whereClause}
55
+ ORDER BY score
56
+ LIMIT ?
57
+ `;
58
+ params.unshift(escapedQuery);
59
+ params.push(limit);
60
+ const rows = client.db.prepare(sql).all(...params);
61
+ return rows.map((row) => {
62
+ const normalizedScore = Math.abs(row.score);
63
+ const result = {
64
+ id: row.id,
65
+ score: normalizedScore
66
+ };
67
+ result.summary = row.content.slice(0, 100) + (row.content.length > 100 ? "..." : "");
68
+ if (layer >= 2) {
69
+ result.createdAt = new Date(row.created_at);
70
+ result.sessionId = row.session_id;
71
+ result.type = row.type;
72
+ result.toolName = row.tool_name ?? undefined;
73
+ }
74
+ if (layer >= 3) {
75
+ result.content = row.content;
76
+ result.metadata = {
77
+ importance: row.importance,
78
+ projectPath: row.project_path
79
+ };
80
+ }
81
+ return result;
82
+ });
83
+ },
84
+ close() {
85
+ client.close();
86
+ }
87
+ };
88
+ }
89
+
90
+ export { createSearchEngine };
@@ -0,0 +1,21 @@
1
+ import { createRequire } from "node:module";
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __toESM = (mod, isNodeMode, target) => {
8
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
9
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
+ for (let key of __getOwnPropNames(mod))
11
+ if (!__hasOwnProp.call(to, key))
12
+ __defProp(to, key, {
13
+ get: () => mod[key],
14
+ enumerable: true
15
+ });
16
+ return to;
17
+ };
18
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
19
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
+
21
+ export { __toESM, __commonJS, __require };
@@ -0,0 +1,103 @@
1
+ import {
2
+ createDBClient
3
+ } from "./chunk-41rc1bhg.js";
4
+
5
+ // src/core/store.ts
6
+ function toSession(dbSession) {
7
+ return {
8
+ id: dbSession.id,
9
+ projectPath: dbSession.project_path,
10
+ startedAt: new Date(dbSession.started_at),
11
+ endedAt: dbSession.ended_at ? new Date(dbSession.ended_at) : null,
12
+ summary: dbSession.summary,
13
+ tokenCount: dbSession.token_count
14
+ };
15
+ }
16
+ function toObservation(dbObs) {
17
+ return {
18
+ id: dbObs.id,
19
+ sessionId: dbObs.session_id,
20
+ type: dbObs.type,
21
+ toolName: dbObs.tool_name,
22
+ content: dbObs.content,
23
+ importance: dbObs.importance,
24
+ createdAt: new Date(dbObs.created_at)
25
+ };
26
+ }
27
+ function estimateTokens(text) {
28
+ return Math.ceil(text.length / 4);
29
+ }
30
+ function createMemoryStore(dbPathOrClient) {
31
+ const client = typeof dbPathOrClient === "string" ? createDBClient(dbPathOrClient) : dbPathOrClient;
32
+ let currentSessionId = null;
33
+ let tokenCount = 0;
34
+ return {
35
+ createSession(projectPath) {
36
+ const dbSession = client.createSession({ project_path: projectPath });
37
+ currentSessionId = dbSession.id;
38
+ tokenCount = 0;
39
+ return toSession(dbSession);
40
+ },
41
+ getCurrentSession() {
42
+ if (!currentSessionId)
43
+ return null;
44
+ const dbSession = client.getSession(currentSessionId);
45
+ if (!dbSession) {
46
+ currentSessionId = null;
47
+ return null;
48
+ }
49
+ return toSession(dbSession);
50
+ },
51
+ endSession(summary) {
52
+ if (!currentSessionId)
53
+ return;
54
+ client.updateSession(currentSessionId, { token_count: tokenCount });
55
+ client.endSession(currentSessionId, summary);
56
+ currentSessionId = null;
57
+ tokenCount = 0;
58
+ },
59
+ addObservation(obs) {
60
+ if (!currentSessionId) {
61
+ throw new Error("No active session. Call createSession first.");
62
+ }
63
+ const dbObs = client.createObservation({
64
+ session_id: currentSessionId,
65
+ type: obs.type,
66
+ tool_name: obs.toolName,
67
+ content: obs.content,
68
+ importance: obs.importance
69
+ });
70
+ tokenCount += estimateTokens(obs.content);
71
+ return toObservation(dbObs);
72
+ },
73
+ getObservation(id) {
74
+ const dbObs = client.getObservation(id);
75
+ return dbObs ? toObservation(dbObs) : null;
76
+ },
77
+ getRecentObservations(limit = 50) {
78
+ if (!currentSessionId)
79
+ return [];
80
+ const dbObservations = client.listObservations(currentSessionId, limit);
81
+ return dbObservations.map(toObservation);
82
+ },
83
+ summarizeAndDelete(before) {
84
+ const beforeISO = before.toISOString();
85
+ const oldObservations = client.db.prepare(`
86
+ SELECT id FROM observations
87
+ WHERE created_at < ?
88
+ `).all(beforeISO);
89
+ for (const obs of oldObservations) {
90
+ client.deleteObservation(obs.id);
91
+ }
92
+ return oldObservations.length;
93
+ },
94
+ getTokenCount() {
95
+ return tokenCount;
96
+ },
97
+ close() {
98
+ client.close();
99
+ }
100
+ };
101
+ }
102
+
103
+ export { estimateTokens, createMemoryStore };
@@ -0,0 +1,36 @@
1
+ // src/core/db/paths.ts
2
+ import { homedir } from "os";
3
+ import { join, resolve } from "path";
4
+ import { mkdirSync, existsSync } from "fs";
5
+ function getGlobalConfigDir() {
6
+ const configDir = join(homedir(), ".config", "ralph-mem");
7
+ return configDir;
8
+ }
9
+ function getProjectDataDir(projectPath) {
10
+ return join(resolve(projectPath), ".ralph-mem");
11
+ }
12
+ function getProjectDBPath(projectPath) {
13
+ return join(getProjectDataDir(projectPath), "memory.db");
14
+ }
15
+ function getSnapshotsDir(projectPath) {
16
+ return join(getProjectDataDir(projectPath), "snapshots");
17
+ }
18
+ function getBackupsDir(projectPath) {
19
+ return join(getProjectDataDir(projectPath), "backups");
20
+ }
21
+ function ensureDir(dirPath) {
22
+ if (!existsSync(dirPath)) {
23
+ mkdirSync(dirPath, { recursive: true });
24
+ }
25
+ }
26
+ function ensureProjectDirs(projectPath) {
27
+ const dataDir = getProjectDataDir(projectPath);
28
+ const snapshotsDir = getSnapshotsDir(projectPath);
29
+ const backupsDir = getBackupsDir(projectPath);
30
+ ensureDir(dataDir);
31
+ ensureDir(snapshotsDir);
32
+ ensureDir(backupsDir);
33
+ return { dataDir, snapshotsDir, backupsDir };
34
+ }
35
+
36
+ export { getGlobalConfigDir, getProjectDataDir, getProjectDBPath, ensureProjectDirs };
@@ -0,0 +1,155 @@
1
+ import {
2
+ loadConfig
3
+ } from "../chunk-c3a91ngd.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/hooks/post-tool-use.ts
14
+ var RECORDABLE_TOOLS = new Set([
15
+ "Edit",
16
+ "Write",
17
+ "Bash",
18
+ "NotebookEdit",
19
+ "MultiEdit"
20
+ ]);
21
+ var READ_ONLY_TOOLS = new Set([
22
+ "Read",
23
+ "Glob",
24
+ "Grep",
25
+ "LS",
26
+ "WebFetch",
27
+ "WebSearch"
28
+ ]);
29
+ function shouldRecordTool(toolName) {
30
+ if (RECORDABLE_TOOLS.has(toolName)) {
31
+ return true;
32
+ }
33
+ if (READ_ONLY_TOOLS.has(toolName)) {
34
+ return false;
35
+ }
36
+ return true;
37
+ }
38
+ function getObservationType(toolName, success) {
39
+ if (!success) {
40
+ return "error";
41
+ }
42
+ if (toolName === "Bash") {
43
+ return "bash";
44
+ }
45
+ return "tool_use";
46
+ }
47
+ function calculateImportance(context) {
48
+ if (!context.success) {
49
+ return 1;
50
+ }
51
+ const output = String(context.toolOutput || "").toLowerCase();
52
+ if (context.toolName === "Bash" && (output.includes("test") || output.includes("passed") || output.includes("failed"))) {
53
+ return 0.9;
54
+ }
55
+ if (context.toolName === "Edit" || context.toolName === "Write" || context.toolName === "NotebookEdit") {
56
+ return 0.7;
57
+ }
58
+ return 0.5;
59
+ }
60
+ function formatOutput(output, maxLength = 2000) {
61
+ const str = typeof output === "string" ? output : JSON.stringify(output);
62
+ if (str.length <= maxLength) {
63
+ return str;
64
+ }
65
+ return `${str.slice(0, maxLength)}... [truncated]`;
66
+ }
67
+ function shouldExclude(content, patterns) {
68
+ for (const pattern of patterns) {
69
+ const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".");
70
+ const regex = new RegExp(regexPattern, "i");
71
+ if (regex.test(content)) {
72
+ return true;
73
+ }
74
+ }
75
+ return false;
76
+ }
77
+ function maskSensitiveData(content) {
78
+ let masked = content;
79
+ masked = masked.replace(/([a-zA-Z_]*(?:api|key|token|secret|password)[a-zA-Z_]*[\s:=]+)['"]?([a-zA-Z0-9_\-]{20,})['"]?/gi, "$1[MASKED]");
80
+ masked = masked.replace(/Bearer\s+[a-zA-Z0-9_\-\.]+/gi, "Bearer [MASKED]");
81
+ masked = masked.replace(/^([A-Z_]+_(?:KEY|SECRET|TOKEN|PASSWORD))\s*=\s*.+$/gm, "$1=[MASKED]");
82
+ return masked;
83
+ }
84
+ async function postToolUseHook(context, options) {
85
+ const { toolName, sessionId, projectPath, success } = context;
86
+ if (!shouldRecordTool(toolName)) {
87
+ return {
88
+ observationId: null,
89
+ recorded: false,
90
+ type: null,
91
+ importance: 0
92
+ };
93
+ }
94
+ const config = options?.config ? { ...loadConfig(projectPath), ...options.config } : loadConfig(projectPath);
95
+ let content = formatOutput(context.toolOutput);
96
+ if (context.error) {
97
+ content = `Error: ${context.error}
98
+ ${content}`;
99
+ }
100
+ if (shouldExclude(content, config.privacy.exclude_patterns)) {
101
+ return {
102
+ observationId: null,
103
+ recorded: false,
104
+ type: null,
105
+ importance: 0
106
+ };
107
+ }
108
+ content = maskSensitiveData(content);
109
+ const type = getObservationType(toolName, success);
110
+ const importance = calculateImportance(context);
111
+ ensureProjectDirs(projectPath);
112
+ const dbPath = getProjectDBPath(projectPath);
113
+ const client = options?.client ?? createDBClient(dbPath);
114
+ const session = client.getSession(sessionId);
115
+ if (!session || session.ended_at) {
116
+ if (!options?.client) {
117
+ client.close();
118
+ }
119
+ return {
120
+ observationId: null,
121
+ recorded: false,
122
+ type: null,
123
+ importance: 0
124
+ };
125
+ }
126
+ const observation = client.createObservation({
127
+ session_id: sessionId,
128
+ type,
129
+ tool_name: toolName,
130
+ content,
131
+ importance,
132
+ loop_run_id: context.loopContext?.runId,
133
+ iteration: context.loopContext?.iteration
134
+ });
135
+ if (!options?.client) {
136
+ client.close();
137
+ }
138
+ return {
139
+ observationId: observation.id,
140
+ recorded: true,
141
+ type,
142
+ importance
143
+ };
144
+ }
145
+ export {
146
+ shouldRecordTool,
147
+ shouldExclude,
148
+ postToolUseHook,
149
+ maskSensitiveData,
150
+ getObservationType,
151
+ formatOutput,
152
+ calculateImportance
153
+ };
154
+
155
+ export { postToolUseHook };
@@ -0,0 +1,89 @@
1
+ import {
2
+ createMemoryStore
3
+ } from "../chunk-v8anyhk1.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/hooks/session-end.ts
14
+ function generateSummary(observations) {
15
+ if (observations.length === 0) {
16
+ return { summary: "세션에서 기록된 작업이 없습니다.", toolStats: {} };
17
+ }
18
+ const typeCounts = {};
19
+ const toolStats = {};
20
+ for (const obs of observations) {
21
+ typeCounts[obs.type] = (typeCounts[obs.type] || 0) + 1;
22
+ if (obs.tool_name) {
23
+ toolStats[obs.tool_name] = (toolStats[obs.tool_name] || 0) + 1;
24
+ }
25
+ }
26
+ const parts = [];
27
+ const toolEntries = Object.entries(toolStats);
28
+ if (toolEntries.length > 0) {
29
+ const topTools = toolEntries.sort((a, b) => b[1] - a[1]).slice(0, 3).map(([name, count]) => `${name}(${count})`).join(", ");
30
+ parts.push(`주요 도구: ${topTools}`);
31
+ }
32
+ if (typeCounts.error && typeCounts.error > 0) {
33
+ parts.push(`에러 ${typeCounts.error}건 발생`);
34
+ }
35
+ if (typeCounts.success && typeCounts.success > 0) {
36
+ parts.push(`성공 ${typeCounts.success}건`);
37
+ }
38
+ const notes = observations.filter((o) => o.type === "note");
39
+ if (notes.length > 0) {
40
+ const lastNote = notes[notes.length - 1];
41
+ const notePreview = lastNote.content.slice(0, 50) + (lastNote.content.length > 50 ? "..." : "");
42
+ parts.push(`마지막 메모: ${notePreview}`);
43
+ }
44
+ const summary = parts.length > 0 ? parts.join(". ") : `작업 ${observations.length}건 처리`;
45
+ return { summary, toolStats };
46
+ }
47
+ async function sessionEndHook(context, options) {
48
+ const { sessionId, projectPath, reason = "user" } = context;
49
+ ensureProjectDirs(projectPath);
50
+ const dbPath = getProjectDBPath(projectPath);
51
+ const client = options?.client ?? createDBClient(dbPath);
52
+ const store = options?.store ?? createMemoryStore(client);
53
+ const session = client.getSession(sessionId);
54
+ if (!session) {
55
+ return {
56
+ summary: "",
57
+ observationCount: 0,
58
+ tokenCount: 0,
59
+ toolStats: {}
60
+ };
61
+ }
62
+ if (session.ended_at) {
63
+ return {
64
+ summary: session.summary || "",
65
+ observationCount: 0,
66
+ tokenCount: session.token_count,
67
+ toolStats: {}
68
+ };
69
+ }
70
+ const observations = client.listObservations(sessionId, 1000);
71
+ const { summary, toolStats } = generateSummary(observations);
72
+ const finalSummary = reason !== "user" ? `[${reason}] ${summary}` : summary;
73
+ client.endSession(sessionId, finalSummary);
74
+ const updatedSession = client.getSession(sessionId);
75
+ if (!options?.client) {
76
+ client.close();
77
+ }
78
+ return {
79
+ summary: finalSummary,
80
+ observationCount: observations.length,
81
+ tokenCount: updatedSession?.token_count ?? 0,
82
+ toolStats
83
+ };
84
+ }
85
+ export {
86
+ sessionEndHook,
87
+ generateSummary
88
+ };
89
+ export { sessionEndHook };
@@ -0,0 +1,95 @@
1
+ import {
2
+ loadConfig
3
+ } from "../chunk-c3a91ngd.js";
4
+ import {
5
+ createMemoryStore,
6
+ estimateTokens
7
+ } from "../chunk-v8anyhk1.js";
8
+ import {
9
+ createDBClient
10
+ } from "../chunk-41rc1bhg.js";
11
+ import {
12
+ ensureProjectDirs,
13
+ getProjectDBPath
14
+ } from "../chunk-w40c0y00.js";
15
+ import"../chunk-ns0dgdnb.js";
16
+
17
+ // src/hooks/session-start.ts
18
+ import { copyFileSync, existsSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ function backupDatabase(projectPath) {
21
+ const dbPath = getProjectDBPath(projectPath);
22
+ if (!existsSync(dbPath)) {
23
+ return;
24
+ }
25
+ const dirs = ensureProjectDirs(projectPath);
26
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
27
+ const backupPath = join(dirs.backupsDir, `memory-${timestamp}.db`);
28
+ copyFileSync(dbPath, backupPath);
29
+ return backupPath;
30
+ }
31
+ function formatSessionContext(sessions, maxTokens) {
32
+ if (sessions.length === 0) {
33
+ return { context: "", tokenCount: 0 };
34
+ }
35
+ const lines = ["\uD83D\uDCDD 이전 세션 컨텍스트:"];
36
+ let tokenCount = estimateTokens(lines[0]);
37
+ for (const session of sessions) {
38
+ if (!session.summary)
39
+ continue;
40
+ const date = new Date(session.started_at).toLocaleDateString("ko-KR", {
41
+ month: "numeric",
42
+ day: "numeric"
43
+ });
44
+ const line = `- [${date}] ${session.summary}`;
45
+ const lineTokens = estimateTokens(line);
46
+ if (tokenCount + lineTokens > maxTokens) {
47
+ break;
48
+ }
49
+ lines.push(line);
50
+ tokenCount += lineTokens;
51
+ }
52
+ if (lines.length === 1) {
53
+ return { context: "", tokenCount: 0 };
54
+ }
55
+ return {
56
+ context: lines.join(`
57
+ `),
58
+ tokenCount
59
+ };
60
+ }
61
+ async function sessionStartHook(context, options) {
62
+ const { projectPath } = context;
63
+ const config = options?.config ? { ...loadConfig(projectPath), ...options.config } : loadConfig(projectPath);
64
+ const backupPath = backupDatabase(projectPath);
65
+ const dbPath = getProjectDBPath(projectPath);
66
+ ensureProjectDirs(projectPath);
67
+ const client = options?.client ?? createDBClient(dbPath);
68
+ const store = options?.store ?? createMemoryStore(client);
69
+ const previousSessions = client.listSessions(projectPath, 10);
70
+ const session = store.createSession(projectPath);
71
+ let injectedContext = "";
72
+ let tokenCount = 0;
73
+ if (config.memory.auto_inject) {
74
+ const sessionsWithSummary = previousSessions.filter((s) => s.summary);
75
+ const formatted = formatSessionContext(sessionsWithSummary, config.memory.max_inject_tokens);
76
+ injectedContext = formatted.context;
77
+ tokenCount = formatted.tokenCount;
78
+ }
79
+ if (!options?.client) {}
80
+ return {
81
+ sessionId: session.id,
82
+ injectedContext,
83
+ tokenCount,
84
+ metadata: {
85
+ previousSessions: previousSessions.length,
86
+ backupPath
87
+ }
88
+ };
89
+ }
90
+ export {
91
+ sessionStartHook,
92
+ formatSessionContext,
93
+ backupDatabase
94
+ };
95
+ export { sessionStartHook };