@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.
Files changed (116) hide show
  1. package/README.md +250 -0
  2. package/dist/cache-manager.d.ts +134 -0
  3. package/dist/cache-manager.d.ts.map +1 -0
  4. package/dist/cache-manager.js +407 -0
  5. package/dist/cache-manager.js.map +1 -0
  6. package/dist/cli/save.d.ts +20 -0
  7. package/dist/cli/save.d.ts.map +1 -0
  8. package/dist/cli/save.js +94 -0
  9. package/dist/cli/save.js.map +1 -0
  10. package/dist/cli/setup.d.ts +18 -0
  11. package/dist/cli/setup.d.ts.map +1 -0
  12. package/dist/cli/setup.js +163 -0
  13. package/dist/cli/setup.js.map +1 -0
  14. package/dist/cli/viewer.d.ts +21 -0
  15. package/dist/cli/viewer.d.ts.map +1 -0
  16. package/dist/cli/viewer.js +182 -0
  17. package/dist/cli/viewer.js.map +1 -0
  18. package/dist/hnsw-index.d.ts +111 -0
  19. package/dist/hnsw-index.d.ts.map +1 -0
  20. package/dist/hnsw-index.js +781 -0
  21. package/dist/hnsw-index.js.map +1 -0
  22. package/dist/hooks/cli.d.ts +20 -0
  23. package/dist/hooks/cli.d.ts.map +1 -0
  24. package/dist/hooks/cli.js +102 -0
  25. package/dist/hooks/cli.js.map +1 -0
  26. package/dist/hooks/context.d.ts +31 -0
  27. package/dist/hooks/context.d.ts.map +1 -0
  28. package/dist/hooks/context.js +64 -0
  29. package/dist/hooks/context.js.map +1 -0
  30. package/dist/hooks/index.d.ts +16 -0
  31. package/dist/hooks/index.d.ts.map +1 -0
  32. package/dist/hooks/index.js +20 -0
  33. package/dist/hooks/index.js.map +1 -0
  34. package/dist/hooks/observation.d.ts +30 -0
  35. package/dist/hooks/observation.d.ts.map +1 -0
  36. package/dist/hooks/observation.js +79 -0
  37. package/dist/hooks/observation.js.map +1 -0
  38. package/dist/hooks/service.d.ts +102 -0
  39. package/dist/hooks/service.d.ts.map +1 -0
  40. package/dist/hooks/service.js +454 -0
  41. package/dist/hooks/service.js.map +1 -0
  42. package/dist/hooks/session-init.d.ts +30 -0
  43. package/dist/hooks/session-init.d.ts.map +1 -0
  44. package/dist/hooks/session-init.js +54 -0
  45. package/dist/hooks/session-init.js.map +1 -0
  46. package/dist/hooks/summarize.d.ts +30 -0
  47. package/dist/hooks/summarize.d.ts.map +1 -0
  48. package/dist/hooks/summarize.js +74 -0
  49. package/dist/hooks/summarize.js.map +1 -0
  50. package/dist/hooks/types.d.ts +193 -0
  51. package/dist/hooks/types.d.ts.map +1 -0
  52. package/dist/hooks/types.js +137 -0
  53. package/dist/hooks/types.js.map +1 -0
  54. package/dist/index.d.ts +173 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +564 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/mcp/index.d.ts +9 -0
  59. package/dist/mcp/index.d.ts.map +1 -0
  60. package/dist/mcp/index.js +9 -0
  61. package/dist/mcp/index.js.map +1 -0
  62. package/dist/mcp/server.d.ts +22 -0
  63. package/dist/mcp/server.d.ts.map +1 -0
  64. package/dist/mcp/server.js +368 -0
  65. package/dist/mcp/server.js.map +1 -0
  66. package/dist/mcp/tools.d.ts +14 -0
  67. package/dist/mcp/tools.d.ts.map +1 -0
  68. package/dist/mcp/tools.js +110 -0
  69. package/dist/mcp/tools.js.map +1 -0
  70. package/dist/mcp/types.d.ts +100 -0
  71. package/dist/mcp/types.d.ts.map +1 -0
  72. package/dist/mcp/types.js +9 -0
  73. package/dist/mcp/types.js.map +1 -0
  74. package/dist/migration.d.ts +77 -0
  75. package/dist/migration.d.ts.map +1 -0
  76. package/dist/migration.js +457 -0
  77. package/dist/migration.js.map +1 -0
  78. package/dist/sqljs-backend.d.ts +128 -0
  79. package/dist/sqljs-backend.d.ts.map +1 -0
  80. package/dist/sqljs-backend.js +623 -0
  81. package/dist/sqljs-backend.js.map +1 -0
  82. package/dist/types.d.ts +481 -0
  83. package/dist/types.d.ts.map +1 -0
  84. package/dist/types.js +73 -0
  85. package/dist/types.js.map +1 -0
  86. package/hooks.json +46 -0
  87. package/package.json +67 -0
  88. package/src/__tests__/index.test.ts +407 -0
  89. package/src/__tests__/sqljs-backend.test.ts +410 -0
  90. package/src/cache-manager.ts +515 -0
  91. package/src/cli/save.ts +109 -0
  92. package/src/cli/setup.ts +203 -0
  93. package/src/cli/viewer.ts +218 -0
  94. package/src/hnsw-index.ts +1013 -0
  95. package/src/hooks/__tests__/handlers.test.ts +298 -0
  96. package/src/hooks/__tests__/integration.test.ts +431 -0
  97. package/src/hooks/__tests__/service.test.ts +487 -0
  98. package/src/hooks/__tests__/types.test.ts +341 -0
  99. package/src/hooks/cli.ts +121 -0
  100. package/src/hooks/context.ts +77 -0
  101. package/src/hooks/index.ts +23 -0
  102. package/src/hooks/observation.ts +102 -0
  103. package/src/hooks/service.ts +582 -0
  104. package/src/hooks/session-init.ts +70 -0
  105. package/src/hooks/summarize.ts +89 -0
  106. package/src/hooks/types.ts +365 -0
  107. package/src/index.ts +755 -0
  108. package/src/mcp/__tests__/server.test.ts +181 -0
  109. package/src/mcp/index.ts +9 -0
  110. package/src/mcp/server.ts +441 -0
  111. package/src/mcp/tools.ts +113 -0
  112. package/src/mcp/types.ts +109 -0
  113. package/src/migration.ts +574 -0
  114. package/src/sql.js.d.ts +70 -0
  115. package/src/sqljs-backend.ts +789 -0
  116. 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;