@aitytech/agentkits-memory 1.0.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/README.md +250 -0
- package/dist/cache-manager.d.ts +134 -0
- package/dist/cache-manager.d.ts.map +1 -0
- package/dist/cache-manager.js +407 -0
- package/dist/cache-manager.js.map +1 -0
- package/dist/cli/save.d.ts +20 -0
- package/dist/cli/save.d.ts.map +1 -0
- package/dist/cli/save.js +94 -0
- package/dist/cli/save.js.map +1 -0
- package/dist/cli/setup.d.ts +18 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +163 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/viewer.d.ts +21 -0
- package/dist/cli/viewer.d.ts.map +1 -0
- package/dist/cli/viewer.js +182 -0
- package/dist/cli/viewer.js.map +1 -0
- package/dist/hnsw-index.d.ts +111 -0
- package/dist/hnsw-index.d.ts.map +1 -0
- package/dist/hnsw-index.js +781 -0
- package/dist/hnsw-index.js.map +1 -0
- package/dist/hooks/cli.d.ts +20 -0
- package/dist/hooks/cli.d.ts.map +1 -0
- package/dist/hooks/cli.js +102 -0
- package/dist/hooks/cli.js.map +1 -0
- package/dist/hooks/context.d.ts +31 -0
- package/dist/hooks/context.d.ts.map +1 -0
- package/dist/hooks/context.js +64 -0
- package/dist/hooks/context.js.map +1 -0
- package/dist/hooks/index.d.ts +16 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +20 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/observation.d.ts +30 -0
- package/dist/hooks/observation.d.ts.map +1 -0
- package/dist/hooks/observation.js +79 -0
- package/dist/hooks/observation.js.map +1 -0
- package/dist/hooks/service.d.ts +102 -0
- package/dist/hooks/service.d.ts.map +1 -0
- package/dist/hooks/service.js +454 -0
- package/dist/hooks/service.js.map +1 -0
- package/dist/hooks/session-init.d.ts +30 -0
- package/dist/hooks/session-init.d.ts.map +1 -0
- package/dist/hooks/session-init.js +54 -0
- package/dist/hooks/session-init.js.map +1 -0
- package/dist/hooks/summarize.d.ts +30 -0
- package/dist/hooks/summarize.d.ts.map +1 -0
- package/dist/hooks/summarize.js +74 -0
- package/dist/hooks/summarize.js.map +1 -0
- package/dist/hooks/types.d.ts +193 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +137 -0
- package/dist/hooks/types.js.map +1 -0
- package/dist/index.d.ts +173 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +564 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +9 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +9 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +22 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +368 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +14 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +110 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mcp/types.d.ts +100 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +9 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/migration.d.ts +77 -0
- package/dist/migration.d.ts.map +1 -0
- package/dist/migration.js +457 -0
- package/dist/migration.js.map +1 -0
- package/dist/sqljs-backend.d.ts +128 -0
- package/dist/sqljs-backend.d.ts.map +1 -0
- package/dist/sqljs-backend.js +623 -0
- package/dist/sqljs-backend.js.map +1 -0
- package/dist/types.d.ts +481 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +73 -0
- package/dist/types.js.map +1 -0
- package/hooks.json +46 -0
- package/package.json +67 -0
- package/src/__tests__/index.test.ts +407 -0
- package/src/__tests__/sqljs-backend.test.ts +410 -0
- package/src/cache-manager.ts +515 -0
- package/src/cli/save.ts +109 -0
- package/src/cli/setup.ts +203 -0
- package/src/cli/viewer.ts +218 -0
- package/src/hnsw-index.ts +1013 -0
- package/src/hooks/__tests__/handlers.test.ts +298 -0
- package/src/hooks/__tests__/integration.test.ts +431 -0
- package/src/hooks/__tests__/service.test.ts +487 -0
- package/src/hooks/__tests__/types.test.ts +341 -0
- package/src/hooks/cli.ts +121 -0
- package/src/hooks/context.ts +77 -0
- package/src/hooks/index.ts +23 -0
- package/src/hooks/observation.ts +102 -0
- package/src/hooks/service.ts +582 -0
- package/src/hooks/session-init.ts +70 -0
- package/src/hooks/summarize.ts +89 -0
- package/src/hooks/types.ts +365 -0
- package/src/index.ts +755 -0
- package/src/mcp/__tests__/server.test.ts +181 -0
- package/src/mcp/index.ts +9 -0
- package/src/mcp/server.ts +441 -0
- package/src/mcp/tools.ts +113 -0
- package/src/mcp/types.ts +109 -0
- package/src/migration.ts +574 -0
- package/src/sql.js.d.ts +70 -0
- package/src/sqljs-backend.ts +789 -0
- package/src/types.ts +715 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Hook Service
|
|
3
|
+
*
|
|
4
|
+
* Lightweight service for hooks to store/retrieve memory.
|
|
5
|
+
* Direct SQLite access without HTTP worker (simpler than claude-mem).
|
|
6
|
+
*
|
|
7
|
+
* @module @agentkits/memory/hooks/service
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import initSqlJs, { Database as SqlJsDatabase } from 'sql.js';
|
|
14
|
+
|
|
15
|
+
// ESM-compatible require for resolving sql.js WASM path
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
import {
|
|
18
|
+
Observation,
|
|
19
|
+
SessionRecord,
|
|
20
|
+
MemoryContext,
|
|
21
|
+
generateObservationId,
|
|
22
|
+
getObservationType,
|
|
23
|
+
generateObservationTitle,
|
|
24
|
+
truncate,
|
|
25
|
+
} from './types.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Memory Hook Service Configuration
|
|
29
|
+
*/
|
|
30
|
+
export interface MemoryHookServiceConfig {
|
|
31
|
+
/** Base directory for memory storage */
|
|
32
|
+
baseDir: string;
|
|
33
|
+
|
|
34
|
+
/** Database filename */
|
|
35
|
+
dbFilename: string;
|
|
36
|
+
|
|
37
|
+
/** Maximum observations to return in context */
|
|
38
|
+
maxContextObservations: number;
|
|
39
|
+
|
|
40
|
+
/** Maximum sessions to return in context */
|
|
41
|
+
maxContextSessions: number;
|
|
42
|
+
|
|
43
|
+
/** Maximum response size to store (bytes) */
|
|
44
|
+
maxResponseSize: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const DEFAULT_CONFIG: MemoryHookServiceConfig = {
|
|
48
|
+
baseDir: '.claude/memory',
|
|
49
|
+
dbFilename: 'hooks.db',
|
|
50
|
+
maxContextObservations: 20,
|
|
51
|
+
maxContextSessions: 5,
|
|
52
|
+
maxResponseSize: 5000,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Memory Hook Service
|
|
57
|
+
*
|
|
58
|
+
* Provides direct SQLite access for hooks without HTTP overhead.
|
|
59
|
+
* Stores observations and sessions for context injection.
|
|
60
|
+
*/
|
|
61
|
+
export class MemoryHookService {
|
|
62
|
+
private config: MemoryHookServiceConfig;
|
|
63
|
+
private db: SqlJsDatabase | null = null;
|
|
64
|
+
private SQL: any = null;
|
|
65
|
+
private initialized: boolean = false;
|
|
66
|
+
private dbPath: string;
|
|
67
|
+
|
|
68
|
+
constructor(cwd: string, config: Partial<MemoryHookServiceConfig> = {}) {
|
|
69
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
70
|
+
this.dbPath = path.join(cwd, this.config.baseDir, this.config.dbFilename);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Initialize the service
|
|
75
|
+
*/
|
|
76
|
+
async initialize(): Promise<void> {
|
|
77
|
+
if (this.initialized) return;
|
|
78
|
+
|
|
79
|
+
// Ensure directory exists
|
|
80
|
+
const dir = path.dirname(this.dbPath);
|
|
81
|
+
if (!existsSync(dir)) {
|
|
82
|
+
mkdirSync(dir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Load sql.js - use local wasm file from node_modules
|
|
86
|
+
this.SQL = await initSqlJs({
|
|
87
|
+
locateFile: (file: string) => {
|
|
88
|
+
// Try to find the wasm file in node_modules
|
|
89
|
+
const localPath = path.join(
|
|
90
|
+
path.dirname(require.resolve('sql.js')),
|
|
91
|
+
file
|
|
92
|
+
);
|
|
93
|
+
return localPath;
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Load or create database
|
|
98
|
+
if (existsSync(this.dbPath)) {
|
|
99
|
+
const buffer = readFileSync(this.dbPath);
|
|
100
|
+
this.db = new this.SQL.Database(new Uint8Array(buffer));
|
|
101
|
+
} else {
|
|
102
|
+
this.db = new this.SQL.Database();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Create schema
|
|
106
|
+
this.createSchema();
|
|
107
|
+
|
|
108
|
+
this.initialized = true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Persist database to disk
|
|
113
|
+
*/
|
|
114
|
+
async persist(): Promise<void> {
|
|
115
|
+
if (!this.db) return;
|
|
116
|
+
|
|
117
|
+
const data = this.db.export();
|
|
118
|
+
const buffer = Buffer.from(data);
|
|
119
|
+
writeFileSync(this.dbPath, buffer);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Shutdown the service
|
|
124
|
+
*/
|
|
125
|
+
async shutdown(): Promise<void> {
|
|
126
|
+
if (!this.initialized || !this.db) return;
|
|
127
|
+
|
|
128
|
+
await this.persist();
|
|
129
|
+
this.db.close();
|
|
130
|
+
this.db = null;
|
|
131
|
+
this.initialized = false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ===== Session Management =====
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Initialize or get session
|
|
138
|
+
*/
|
|
139
|
+
async initSession(sessionId: string, project: string, prompt?: string): Promise<SessionRecord> {
|
|
140
|
+
await this.ensureInitialized();
|
|
141
|
+
|
|
142
|
+
// Check if session exists
|
|
143
|
+
const existing = this.getSession(sessionId);
|
|
144
|
+
if (existing) {
|
|
145
|
+
return existing;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Create new session
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
this.db!.run(`
|
|
151
|
+
INSERT INTO sessions (session_id, project, prompt, started_at, observation_count, status)
|
|
152
|
+
VALUES (?, ?, ?, ?, 0, 'active')
|
|
153
|
+
`, [sessionId, project, prompt || '', now]);
|
|
154
|
+
|
|
155
|
+
await this.persist();
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
id: this.db!.exec('SELECT last_insert_rowid()')[0]?.values[0]?.[0] as number || 0,
|
|
159
|
+
sessionId,
|
|
160
|
+
project,
|
|
161
|
+
prompt: prompt || '',
|
|
162
|
+
startedAt: now,
|
|
163
|
+
observationCount: 0,
|
|
164
|
+
status: 'active',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get session by ID
|
|
170
|
+
*/
|
|
171
|
+
getSession(sessionId: string): SessionRecord | null {
|
|
172
|
+
if (!this.db) return null;
|
|
173
|
+
|
|
174
|
+
const stmt = this.db.prepare('SELECT * FROM sessions WHERE session_id = ?');
|
|
175
|
+
stmt.bind([sessionId]);
|
|
176
|
+
|
|
177
|
+
if (stmt.step()) {
|
|
178
|
+
const row = stmt.getAsObject();
|
|
179
|
+
stmt.free();
|
|
180
|
+
return this.rowToSession(row);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
stmt.free();
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Complete a session with summary
|
|
189
|
+
*/
|
|
190
|
+
async completeSession(sessionId: string, summary?: string): Promise<void> {
|
|
191
|
+
await this.ensureInitialized();
|
|
192
|
+
|
|
193
|
+
const now = Date.now();
|
|
194
|
+
this.db!.run(`
|
|
195
|
+
UPDATE sessions
|
|
196
|
+
SET ended_at = ?, summary = ?, status = 'completed'
|
|
197
|
+
WHERE session_id = ?
|
|
198
|
+
`, [now, summary || '', sessionId]);
|
|
199
|
+
|
|
200
|
+
await this.persist();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get recent sessions
|
|
205
|
+
*/
|
|
206
|
+
async getRecentSessions(project: string, limit: number = 5): Promise<SessionRecord[]> {
|
|
207
|
+
await this.ensureInitialized();
|
|
208
|
+
|
|
209
|
+
const stmt = this.db!.prepare(`
|
|
210
|
+
SELECT * FROM sessions
|
|
211
|
+
WHERE project = ?
|
|
212
|
+
ORDER BY started_at DESC
|
|
213
|
+
LIMIT ?
|
|
214
|
+
`);
|
|
215
|
+
stmt.bind([project, limit]);
|
|
216
|
+
|
|
217
|
+
const sessions: SessionRecord[] = [];
|
|
218
|
+
while (stmt.step()) {
|
|
219
|
+
sessions.push(this.rowToSession(stmt.getAsObject()));
|
|
220
|
+
}
|
|
221
|
+
stmt.free();
|
|
222
|
+
|
|
223
|
+
return sessions;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ===== Observation Management =====
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Store an observation
|
|
230
|
+
*/
|
|
231
|
+
async storeObservation(
|
|
232
|
+
sessionId: string,
|
|
233
|
+
project: string,
|
|
234
|
+
toolName: string,
|
|
235
|
+
toolInput: unknown,
|
|
236
|
+
toolResponse: unknown,
|
|
237
|
+
cwd: string
|
|
238
|
+
): Promise<Observation> {
|
|
239
|
+
await this.ensureInitialized();
|
|
240
|
+
|
|
241
|
+
const id = generateObservationId();
|
|
242
|
+
const now = Date.now();
|
|
243
|
+
const type = getObservationType(toolName);
|
|
244
|
+
const title = generateObservationTitle(toolName, toolInput);
|
|
245
|
+
|
|
246
|
+
// Truncate large responses
|
|
247
|
+
const inputStr = JSON.stringify(toolInput || {});
|
|
248
|
+
const responseStr = truncate(
|
|
249
|
+
JSON.stringify(toolResponse || {}),
|
|
250
|
+
this.config.maxResponseSize
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
this.db!.run(`
|
|
254
|
+
INSERT INTO observations (id, session_id, project, tool_name, tool_input, tool_response, cwd, timestamp, type, title)
|
|
255
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
256
|
+
`, [id, sessionId, project, toolName, inputStr, responseStr, cwd, now, type, title]);
|
|
257
|
+
|
|
258
|
+
// Update session observation count
|
|
259
|
+
this.db!.run(`
|
|
260
|
+
UPDATE sessions
|
|
261
|
+
SET observation_count = observation_count + 1
|
|
262
|
+
WHERE session_id = ?
|
|
263
|
+
`, [sessionId]);
|
|
264
|
+
|
|
265
|
+
await this.persist();
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
id,
|
|
269
|
+
sessionId,
|
|
270
|
+
project,
|
|
271
|
+
toolName,
|
|
272
|
+
toolInput: inputStr,
|
|
273
|
+
toolResponse: responseStr,
|
|
274
|
+
cwd,
|
|
275
|
+
timestamp: now,
|
|
276
|
+
type,
|
|
277
|
+
title,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Get observations for a session
|
|
283
|
+
*/
|
|
284
|
+
async getSessionObservations(sessionId: string, limit: number = 50): Promise<Observation[]> {
|
|
285
|
+
await this.ensureInitialized();
|
|
286
|
+
|
|
287
|
+
const stmt = this.db!.prepare(`
|
|
288
|
+
SELECT * FROM observations
|
|
289
|
+
WHERE session_id = ?
|
|
290
|
+
ORDER BY timestamp DESC
|
|
291
|
+
LIMIT ?
|
|
292
|
+
`);
|
|
293
|
+
stmt.bind([sessionId, limit]);
|
|
294
|
+
|
|
295
|
+
const observations: Observation[] = [];
|
|
296
|
+
while (stmt.step()) {
|
|
297
|
+
observations.push(this.rowToObservation(stmt.getAsObject()));
|
|
298
|
+
}
|
|
299
|
+
stmt.free();
|
|
300
|
+
|
|
301
|
+
return observations;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Get recent observations for a project
|
|
306
|
+
*/
|
|
307
|
+
async getRecentObservations(project: string, limit: number = 20): Promise<Observation[]> {
|
|
308
|
+
await this.ensureInitialized();
|
|
309
|
+
|
|
310
|
+
const stmt = this.db!.prepare(`
|
|
311
|
+
SELECT * FROM observations
|
|
312
|
+
WHERE project = ?
|
|
313
|
+
ORDER BY timestamp DESC
|
|
314
|
+
LIMIT ?
|
|
315
|
+
`);
|
|
316
|
+
stmt.bind([project, limit]);
|
|
317
|
+
|
|
318
|
+
const observations: Observation[] = [];
|
|
319
|
+
while (stmt.step()) {
|
|
320
|
+
observations.push(this.rowToObservation(stmt.getAsObject()));
|
|
321
|
+
}
|
|
322
|
+
stmt.free();
|
|
323
|
+
|
|
324
|
+
return observations;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ===== Context Generation =====
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get memory context for session start
|
|
331
|
+
*/
|
|
332
|
+
async getContext(project: string): Promise<MemoryContext> {
|
|
333
|
+
await this.ensureInitialized();
|
|
334
|
+
|
|
335
|
+
const recentObservations = await this.getRecentObservations(
|
|
336
|
+
project,
|
|
337
|
+
this.config.maxContextObservations
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const previousSessions = await this.getRecentSessions(
|
|
341
|
+
project,
|
|
342
|
+
this.config.maxContextSessions
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// Generate markdown
|
|
346
|
+
const markdown = this.formatContextMarkdown(recentObservations, previousSessions, project);
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
recentObservations,
|
|
350
|
+
previousSessions,
|
|
351
|
+
markdown,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Format context as markdown
|
|
357
|
+
*/
|
|
358
|
+
private formatContextMarkdown(
|
|
359
|
+
observations: Observation[],
|
|
360
|
+
sessions: SessionRecord[],
|
|
361
|
+
project: string
|
|
362
|
+
): string {
|
|
363
|
+
const lines: string[] = [];
|
|
364
|
+
|
|
365
|
+
lines.push(`# Memory Context - ${project}`);
|
|
366
|
+
lines.push('');
|
|
367
|
+
lines.push('*AgentKits CPS™ - Auto-captured session memory*');
|
|
368
|
+
lines.push('');
|
|
369
|
+
|
|
370
|
+
// Recent observations
|
|
371
|
+
if (observations.length > 0) {
|
|
372
|
+
lines.push('## Recent Activity');
|
|
373
|
+
lines.push('');
|
|
374
|
+
lines.push('| Time | Action | Details |');
|
|
375
|
+
lines.push('|------|--------|---------|');
|
|
376
|
+
|
|
377
|
+
for (const obs of observations.slice(0, 10)) {
|
|
378
|
+
const time = this.formatRelativeTime(obs.timestamp);
|
|
379
|
+
const icon = this.getObservationIcon(obs.type);
|
|
380
|
+
lines.push(`| ${time} | ${icon} ${obs.toolName} | ${obs.title || ''} |`);
|
|
381
|
+
}
|
|
382
|
+
lines.push('');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Previous sessions
|
|
386
|
+
if (sessions.length > 0) {
|
|
387
|
+
lines.push('## Previous Sessions');
|
|
388
|
+
lines.push('');
|
|
389
|
+
|
|
390
|
+
for (const session of sessions.slice(0, 3)) {
|
|
391
|
+
const time = this.formatRelativeTime(session.startedAt);
|
|
392
|
+
const status = session.status === 'completed' ? '✓' : '→';
|
|
393
|
+
lines.push(`### ${status} Session (${time})`);
|
|
394
|
+
|
|
395
|
+
if (session.prompt) {
|
|
396
|
+
lines.push(`**Task:** ${session.prompt.substring(0, 100)}${session.prompt.length > 100 ? '...' : ''}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (session.summary) {
|
|
400
|
+
lines.push(`**Summary:** ${session.summary}`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
lines.push(`*Observations: ${session.observationCount}*`);
|
|
404
|
+
lines.push('');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// No context available
|
|
409
|
+
if (observations.length === 0 && sessions.length === 0) {
|
|
410
|
+
lines.push('*No previous session context available.*');
|
|
411
|
+
lines.push('');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return lines.join('\n');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Generate session summary from observations
|
|
419
|
+
*/
|
|
420
|
+
async generateSummary(sessionId: string): Promise<string> {
|
|
421
|
+
const observations = await this.getSessionObservations(sessionId);
|
|
422
|
+
|
|
423
|
+
if (observations.length === 0) {
|
|
424
|
+
return 'No activity recorded in this session.';
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Group by type
|
|
428
|
+
const byType: Record<string, number> = {};
|
|
429
|
+
const files: Set<string> = new Set();
|
|
430
|
+
|
|
431
|
+
for (const obs of observations) {
|
|
432
|
+
byType[obs.type] = (byType[obs.type] || 0) + 1;
|
|
433
|
+
|
|
434
|
+
// Extract file paths
|
|
435
|
+
try {
|
|
436
|
+
const input = JSON.parse(obs.toolInput);
|
|
437
|
+
if (input.file_path || input.path) {
|
|
438
|
+
files.add(input.file_path || input.path);
|
|
439
|
+
}
|
|
440
|
+
} catch {
|
|
441
|
+
// Ignore parse errors
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Build summary
|
|
446
|
+
const parts: string[] = [];
|
|
447
|
+
|
|
448
|
+
if (byType.write) {
|
|
449
|
+
parts.push(`${byType.write} file(s) modified`);
|
|
450
|
+
}
|
|
451
|
+
if (byType.read) {
|
|
452
|
+
parts.push(`${byType.read} file(s) read`);
|
|
453
|
+
}
|
|
454
|
+
if (byType.execute) {
|
|
455
|
+
parts.push(`${byType.execute} command(s) executed`);
|
|
456
|
+
}
|
|
457
|
+
if (byType.search) {
|
|
458
|
+
parts.push(`${byType.search} search(es)`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
let summary = parts.join(', ') || 'Various operations performed';
|
|
462
|
+
|
|
463
|
+
if (files.size > 0 && files.size <= 5) {
|
|
464
|
+
summary += `. Files: ${Array.from(files).join(', ')}`;
|
|
465
|
+
} else if (files.size > 5) {
|
|
466
|
+
summary += `. ${files.size} files touched.`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return summary;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ===== Private Methods =====
|
|
473
|
+
|
|
474
|
+
private async ensureInitialized(): Promise<void> {
|
|
475
|
+
if (!this.initialized) {
|
|
476
|
+
await this.initialize();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private createSchema(): void {
|
|
481
|
+
if (!this.db) return;
|
|
482
|
+
|
|
483
|
+
this.db.run(`
|
|
484
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
485
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
486
|
+
session_id TEXT UNIQUE NOT NULL,
|
|
487
|
+
project TEXT NOT NULL,
|
|
488
|
+
prompt TEXT,
|
|
489
|
+
started_at INTEGER NOT NULL,
|
|
490
|
+
ended_at INTEGER,
|
|
491
|
+
observation_count INTEGER DEFAULT 0,
|
|
492
|
+
summary TEXT,
|
|
493
|
+
status TEXT DEFAULT 'active'
|
|
494
|
+
)
|
|
495
|
+
`);
|
|
496
|
+
|
|
497
|
+
this.db.run(`
|
|
498
|
+
CREATE TABLE IF NOT EXISTS observations (
|
|
499
|
+
id TEXT PRIMARY KEY,
|
|
500
|
+
session_id TEXT NOT NULL,
|
|
501
|
+
project TEXT NOT NULL,
|
|
502
|
+
tool_name TEXT NOT NULL,
|
|
503
|
+
tool_input TEXT,
|
|
504
|
+
tool_response TEXT,
|
|
505
|
+
cwd TEXT,
|
|
506
|
+
timestamp INTEGER NOT NULL,
|
|
507
|
+
type TEXT,
|
|
508
|
+
title TEXT,
|
|
509
|
+
FOREIGN KEY (session_id) REFERENCES sessions(session_id)
|
|
510
|
+
)
|
|
511
|
+
`);
|
|
512
|
+
|
|
513
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_obs_session ON observations(session_id)');
|
|
514
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_obs_project ON observations(project)');
|
|
515
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_obs_timestamp ON observations(timestamp)');
|
|
516
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project)');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private rowToSession(row: any): SessionRecord {
|
|
520
|
+
return {
|
|
521
|
+
id: row.id as number,
|
|
522
|
+
sessionId: row.session_id as string,
|
|
523
|
+
project: row.project as string,
|
|
524
|
+
prompt: row.prompt as string,
|
|
525
|
+
startedAt: row.started_at as number,
|
|
526
|
+
endedAt: row.ended_at as number | undefined,
|
|
527
|
+
observationCount: row.observation_count as number,
|
|
528
|
+
summary: row.summary as string | undefined,
|
|
529
|
+
status: row.status as 'active' | 'completed' | 'abandoned',
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private rowToObservation(row: any): Observation {
|
|
534
|
+
return {
|
|
535
|
+
id: row.id as string,
|
|
536
|
+
sessionId: row.session_id as string,
|
|
537
|
+
project: row.project as string,
|
|
538
|
+
toolName: row.tool_name as string,
|
|
539
|
+
toolInput: row.tool_input as string,
|
|
540
|
+
toolResponse: row.tool_response as string,
|
|
541
|
+
cwd: row.cwd as string,
|
|
542
|
+
timestamp: row.timestamp as number,
|
|
543
|
+
type: row.type as any,
|
|
544
|
+
title: row.title as string | undefined,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private formatRelativeTime(timestamp: number): string {
|
|
549
|
+
const now = Date.now();
|
|
550
|
+
const diff = now - timestamp;
|
|
551
|
+
|
|
552
|
+
const minutes = Math.floor(diff / 60000);
|
|
553
|
+
const hours = Math.floor(diff / 3600000);
|
|
554
|
+
const days = Math.floor(diff / 86400000);
|
|
555
|
+
|
|
556
|
+
if (minutes < 1) return 'just now';
|
|
557
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
558
|
+
if (hours < 24) return `${hours}h ago`;
|
|
559
|
+
if (days < 7) return `${days}d ago`;
|
|
560
|
+
|
|
561
|
+
return new Date(timestamp).toLocaleDateString();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private getObservationIcon(type: string): string {
|
|
565
|
+
switch (type) {
|
|
566
|
+
case 'read': return '📖';
|
|
567
|
+
case 'write': return '✏️';
|
|
568
|
+
case 'execute': return '⚡';
|
|
569
|
+
case 'search': return '🔍';
|
|
570
|
+
default: return '•';
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Create a hook service for the given project directory
|
|
577
|
+
*/
|
|
578
|
+
export function createHookService(cwd: string): MemoryHookService {
|
|
579
|
+
return new MemoryHookService(cwd);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export default MemoryHookService;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Init Hook Handler (UserPromptSubmit)
|
|
3
|
+
*
|
|
4
|
+
* Initializes a session record when the user submits their first prompt.
|
|
5
|
+
* Captures the initial prompt for context.
|
|
6
|
+
*
|
|
7
|
+
* @module @agentkits/memory/hooks/session-init
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
NormalizedHookInput,
|
|
12
|
+
HookResult,
|
|
13
|
+
EventHandler,
|
|
14
|
+
} from './types.js';
|
|
15
|
+
import { MemoryHookService } from './service.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Session Init Hook - UserPromptSubmit Event
|
|
19
|
+
*
|
|
20
|
+
* Called when the user submits a prompt.
|
|
21
|
+
* Creates or updates the session record with the prompt.
|
|
22
|
+
*/
|
|
23
|
+
export class SessionInitHook implements EventHandler {
|
|
24
|
+
private service: MemoryHookService;
|
|
25
|
+
|
|
26
|
+
constructor(service: MemoryHookService) {
|
|
27
|
+
this.service = service;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Execute the session init hook
|
|
32
|
+
*/
|
|
33
|
+
async execute(input: NormalizedHookInput): Promise<HookResult> {
|
|
34
|
+
try {
|
|
35
|
+
// Initialize service
|
|
36
|
+
await this.service.initialize();
|
|
37
|
+
|
|
38
|
+
// Initialize or get existing session
|
|
39
|
+
await this.service.initSession(
|
|
40
|
+
input.sessionId,
|
|
41
|
+
input.project,
|
|
42
|
+
input.prompt
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
continue: true,
|
|
47
|
+
suppressOutput: true,
|
|
48
|
+
};
|
|
49
|
+
} catch (error) {
|
|
50
|
+
// Log error but don't block prompt
|
|
51
|
+
console.error('[AgentKits Memory] Session init hook error:', error);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
continue: true,
|
|
55
|
+
suppressOutput: true,
|
|
56
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create session init hook handler
|
|
64
|
+
*/
|
|
65
|
+
export function createSessionInitHook(cwd: string): SessionInitHook {
|
|
66
|
+
const service = new MemoryHookService(cwd);
|
|
67
|
+
return new SessionInitHook(service);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default SessionInitHook;
|