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.
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/dist/chunk-41rc1bhg.js +1116 -0
- package/dist/chunk-c3a91ngd.js +2734 -0
- package/dist/chunk-kga64hvg.js +90 -0
- package/dist/chunk-ns0dgdnb.js +21 -0
- package/dist/chunk-v8anyhk1.js +103 -0
- package/dist/chunk-w40c0y00.js +36 -0
- package/dist/hooks/post-tool-use.js +155 -0
- package/dist/hooks/session-end.js +89 -0
- package/dist/hooks/session-start.js +95 -0
- package/dist/hooks/user-prompt-submit.js +234 -0
- package/dist/index.js +64 -0
- package/dist/skills/mem-forget.js +192 -0
- package/dist/skills/mem-inject.js +130 -0
- package/dist/skills/mem-search.js +303 -0
- package/dist/skills/mem-status.js +200 -0
- package/dist/skills/ralph-config.js +404 -0
- package/dist/skills/ralph.js +654 -0
- package/package.json +64 -0
- package/plugin.json +51 -0
|
@@ -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 };
|