@claudetools/tools 0.8.2 → 0.8.4

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 (74) hide show
  1. package/dist/cli.js +41 -0
  2. package/dist/context/deduplication.d.ts +72 -0
  3. package/dist/context/deduplication.js +77 -0
  4. package/dist/context/deduplication.test.d.ts +6 -0
  5. package/dist/context/deduplication.test.js +84 -0
  6. package/dist/context/emergency-eviction.d.ts +73 -0
  7. package/dist/context/emergency-eviction.example.d.ts +13 -0
  8. package/dist/context/emergency-eviction.example.js +94 -0
  9. package/dist/context/emergency-eviction.js +226 -0
  10. package/dist/context/eviction-engine.d.ts +76 -0
  11. package/dist/context/eviction-engine.example.d.ts +7 -0
  12. package/dist/context/eviction-engine.example.js +144 -0
  13. package/dist/context/eviction-engine.js +176 -0
  14. package/dist/context/example-usage.d.ts +1 -0
  15. package/dist/context/example-usage.js +128 -0
  16. package/dist/context/exchange-summariser.d.ts +80 -0
  17. package/dist/context/exchange-summariser.js +261 -0
  18. package/dist/context/health-monitor.d.ts +97 -0
  19. package/dist/context/health-monitor.example.d.ts +1 -0
  20. package/dist/context/health-monitor.example.js +164 -0
  21. package/dist/context/health-monitor.js +210 -0
  22. package/dist/context/importance-scorer.d.ts +94 -0
  23. package/dist/context/importance-scorer.example.d.ts +1 -0
  24. package/dist/context/importance-scorer.example.js +140 -0
  25. package/dist/context/importance-scorer.js +187 -0
  26. package/dist/context/index.d.ts +9 -0
  27. package/dist/context/index.js +16 -0
  28. package/dist/context/session-helper.d.ts +10 -0
  29. package/dist/context/session-helper.js +51 -0
  30. package/dist/context/session-store.d.ts +94 -0
  31. package/dist/context/session-store.js +286 -0
  32. package/dist/context/usage-estimator.d.ts +131 -0
  33. package/dist/context/usage-estimator.js +260 -0
  34. package/dist/context/usage-estimator.test.d.ts +1 -0
  35. package/dist/context/usage-estimator.test.js +208 -0
  36. package/dist/context-cli.d.ts +16 -0
  37. package/dist/context-cli.js +309 -0
  38. package/dist/evaluation/build-dataset.d.ts +1 -0
  39. package/dist/evaluation/build-dataset.js +135 -0
  40. package/dist/evaluation/threshold-eval.d.ts +63 -0
  41. package/dist/evaluation/threshold-eval.js +250 -0
  42. package/dist/handlers/codedna-handlers.d.ts +2 -2
  43. package/dist/handlers/tool-handlers.js +126 -165
  44. package/dist/helpers/api-client.d.ts +5 -1
  45. package/dist/helpers/api-client.js +3 -1
  46. package/dist/helpers/compact-formatter.d.ts +51 -0
  47. package/dist/helpers/compact-formatter.js +130 -0
  48. package/dist/helpers/engagement-tracker.d.ts +10 -0
  49. package/dist/helpers/engagement-tracker.js +61 -0
  50. package/dist/helpers/error-tracking.js +1 -1
  51. package/dist/helpers/session-validation.d.ts +76 -0
  52. package/dist/helpers/session-validation.js +221 -0
  53. package/dist/helpers/usage-analytics.js +1 -1
  54. package/dist/hooks/index.d.ts +4 -0
  55. package/dist/hooks/index.js +6 -0
  56. package/dist/hooks/post-tool-use-hook-cli.d.ts +2 -0
  57. package/dist/hooks/post-tool-use-hook-cli.js +34 -0
  58. package/dist/hooks/post-tool-use.d.ts +67 -0
  59. package/dist/hooks/post-tool-use.js +234 -0
  60. package/dist/hooks/stop-hook-cli.d.ts +2 -0
  61. package/dist/hooks/stop-hook-cli.js +34 -0
  62. package/dist/hooks/stop.d.ts +64 -0
  63. package/dist/hooks/stop.js +192 -0
  64. package/dist/index.d.ts +3 -0
  65. package/dist/index.js +2 -0
  66. package/dist/logger.d.ts +1 -1
  67. package/dist/logger.js +4 -0
  68. package/dist/resources.js +3 -0
  69. package/dist/setup.js +206 -2
  70. package/dist/templates/claude-md.d.ts +1 -1
  71. package/dist/templates/claude-md.js +23 -35
  72. package/dist/templates/worker-prompt.js +35 -202
  73. package/dist/tools.js +26 -20
  74. package/package.json +6 -2
@@ -0,0 +1,187 @@
1
+ // =============================================================================
2
+ // ClaudeTools Memory - Importance Scorer
3
+ // =============================================================================
4
+ // Dynamic importance scoring for automatic context management with decay
5
+ //
6
+ // Algorithm:
7
+ // - Base importance (critical: 1.0, high: 0.7, normal: 0.4)
8
+ // - Exponential time decay (half-life: 30 minutes)
9
+ // - Reference boost (recently-referenced facts stay longer)
10
+ // - Recency boost (newer injections weighted higher)
11
+ //
12
+ // Score range: 0.0 to 1.0
13
+ // =============================================================================
14
+ // -----------------------------------------------------------------------------
15
+ // Configuration
16
+ // -----------------------------------------------------------------------------
17
+ const SCORING_CONFIG = {
18
+ // Base importance weights
19
+ BASE_IMPORTANCE: {
20
+ critical: 1.0,
21
+ high: 0.7,
22
+ normal: 0.4,
23
+ },
24
+ // Time decay settings
25
+ TIME_DECAY_HALF_LIFE_MINUTES: 30,
26
+ // Reference boost settings
27
+ REFERENCE_BOOST_DECAY_MINUTES: 10, // 10 minutes = 600,000ms
28
+ REFERENCE_BOOST_MIN: 0.3,
29
+ // Component weights (must sum to 1.0)
30
+ WEIGHTS: {
31
+ baseWithDecay: 0.5,
32
+ referenceBoost: 0.3,
33
+ recencyBoost: 0.2,
34
+ },
35
+ };
36
+ // -----------------------------------------------------------------------------
37
+ // Importance Scorer
38
+ // -----------------------------------------------------------------------------
39
+ export class ImportanceScorer {
40
+ /**
41
+ * Calculate dynamic importance score for a single fact
42
+ *
43
+ * @param fact - The injected fact to score
44
+ * @param session - Current session state
45
+ * @returns Importance score between 0.0 and 1.0
46
+ */
47
+ calculateImportance(fact, session) {
48
+ const scored = this.calculateWithBreakdown(fact, session);
49
+ return scored.score;
50
+ }
51
+ /**
52
+ * Calculate importance with component breakdown for debugging
53
+ *
54
+ * @param fact - The injected fact to score
55
+ * @param session - Current session state
56
+ * @returns Scored fact with breakdown
57
+ */
58
+ calculateWithBreakdown(fact, session) {
59
+ const now = Date.now();
60
+ // 1. Base importance from metadata
61
+ const importance = this.extractImportanceLevel(fact);
62
+ const baseImportance = SCORING_CONFIG.BASE_IMPORTANCE[importance];
63
+ // 2. Time decay (exponential with half-life)
64
+ const minutesSinceInjection = (now - fact.injected_at.getTime()) / 60000;
65
+ const timeDecay = Math.exp((-0.693 * minutesSinceInjection) /
66
+ SCORING_CONFIG.TIME_DECAY_HALF_LIFE_MINUTES);
67
+ // 3. Reference boost (facts that were recently referenced stay longer)
68
+ const referenceBoost = this.calculateReferenceBoost(fact, now);
69
+ // 4. Recency of injection (newer facts weighted higher in session)
70
+ const recencyBoost = this.calculateRecencyBoost(fact, session, now);
71
+ // Combine components with weights
72
+ const score = baseImportance * timeDecay * SCORING_CONFIG.WEIGHTS.baseWithDecay +
73
+ referenceBoost * SCORING_CONFIG.WEIGHTS.referenceBoost +
74
+ recencyBoost * SCORING_CONFIG.WEIGHTS.recencyBoost;
75
+ return {
76
+ fact,
77
+ score: Math.max(0, Math.min(1, score)), // Clamp to [0, 1]
78
+ breakdown: {
79
+ base: baseImportance,
80
+ timeDecay,
81
+ referenceBoost,
82
+ recencyBoost,
83
+ },
84
+ };
85
+ }
86
+ /**
87
+ * Score all facts in a session
88
+ *
89
+ * @param session - Session state with injected facts
90
+ * @returns Array of scored facts sorted by score (descending)
91
+ */
92
+ scoreAllFacts(session) {
93
+ return session.injected_facts
94
+ .map((fact) => this.calculateWithBreakdown(fact, session))
95
+ .sort((a, b) => b.score - a.score);
96
+ }
97
+ /**
98
+ * Get facts that should be evicted (lowest importance scores)
99
+ *
100
+ * @param session - Session state
101
+ * @param count - Number of facts to evict
102
+ * @returns Facts to evict (lowest scores first)
103
+ */
104
+ getEvictionCandidates(session, count) {
105
+ const scored = this.scoreAllFacts(session);
106
+ return scored
107
+ .slice(-count) // Take last N (lowest scores)
108
+ .map((s) => s.fact);
109
+ }
110
+ /**
111
+ * Get facts above a minimum importance threshold
112
+ *
113
+ * @param session - Session state
114
+ * @param minScore - Minimum score threshold (0.0 to 1.0)
115
+ * @returns Facts above threshold
116
+ */
117
+ getFactsAboveThreshold(session, minScore) {
118
+ const scored = this.scoreAllFacts(session);
119
+ return scored.filter((s) => s.score >= minScore).map((s) => s.fact);
120
+ }
121
+ // ---------------------------------------------------------------------------
122
+ // Private Helpers
123
+ // ---------------------------------------------------------------------------
124
+ /**
125
+ * Extract importance level from fact metadata
126
+ */
127
+ extractImportanceLevel(fact) {
128
+ const metadata = fact.metadata;
129
+ if (!metadata) {
130
+ return 'normal';
131
+ }
132
+ // Check explicit importance flags
133
+ if (metadata.critical)
134
+ return 'critical';
135
+ if (metadata.important)
136
+ return 'high';
137
+ // Check category-based importance
138
+ if (metadata.category === 'architecture')
139
+ return 'critical';
140
+ if (metadata.category === 'pattern')
141
+ return 'high';
142
+ if (metadata.category === 'decision')
143
+ return 'high';
144
+ return 'normal';
145
+ }
146
+ /**
147
+ * Calculate reference boost
148
+ * Recently-referenced facts get a boost to stay in context longer
149
+ */
150
+ calculateReferenceBoost(fact, now) {
151
+ if (!fact.last_referenced) {
152
+ return 0;
153
+ }
154
+ const minutesSinceReference = (now - fact.last_referenced.getTime()) / 60000;
155
+ const decayMs = SCORING_CONFIG.REFERENCE_BOOST_DECAY_MINUTES * 60000;
156
+ const timeSinceReference = now - fact.last_referenced.getTime();
157
+ // Linear decay from 1.0 to min over decay period
158
+ const boost = Math.max(SCORING_CONFIG.REFERENCE_BOOST_MIN, 1 - timeSinceReference / decayMs);
159
+ return boost;
160
+ }
161
+ /**
162
+ * Calculate recency boost
163
+ * Newer injections in the session get a boost
164
+ */
165
+ calculateRecencyBoost(fact, session, now) {
166
+ const sessionDuration = now - session.started_at.getTime();
167
+ if (sessionDuration <= 0) {
168
+ return 1; // Session just started, all facts are equally recent
169
+ }
170
+ const factAge = now - fact.injected_at.getTime();
171
+ const recency = 1 - factAge / sessionDuration;
172
+ return Math.max(0, Math.min(1, recency)); // Clamp to [0, 1]
173
+ }
174
+ }
175
+ // -----------------------------------------------------------------------------
176
+ // Factory
177
+ // -----------------------------------------------------------------------------
178
+ /**
179
+ * Create a new importance scorer instance
180
+ */
181
+ export function createImportanceScorer() {
182
+ return new ImportanceScorer();
183
+ }
184
+ // -----------------------------------------------------------------------------
185
+ // Exports
186
+ // -----------------------------------------------------------------------------
187
+ // Types are exported inline above via 'export interface' and 'export type'
@@ -0,0 +1,9 @@
1
+ export { UsageEstimator, usageEstimator, estimateTokens, isContextNearLimit, getContextFill, MODEL_CONTEXT_LIMITS, DEFAULT_MODEL_LIMIT, type TokenUsage, type UsageSnapshot, } from './usage-estimator.js';
2
+ export { ImportanceScorer, createImportanceScorer, type InjectedFact, type SessionState, type ScoredImportance, type ImportanceLevel, } from './importance-scorer.js';
3
+ export { SessionStore, getSessionStore, closeSessionStore, CONTEXT_LIMITS, type ModelType, type Exchange, type PersistedSessionState, } from './session-store.js';
4
+ export { type DeduplicationTracker, InMemoryDeduplicationTracker, createDeduplicationTracker, } from './deduplication.js';
5
+ export { EvictionEngine, createEvictionEngine, type EvictionResult, type EvictionPlan, } from './eviction-engine.js';
6
+ export { HealthMonitor, createHealthMonitor, getHealthMonitor, resetHealthMonitor, type HealthStatus, type DriftRecord, } from './health-monitor.js';
7
+ export { ExchangeSummariser, createExchangeSummariser, type SessionStateWithExchanges, type SummarisationResult, type AIEnvironment, } from './exchange-summariser.js';
8
+ export { EmergencyEviction, createEmergencyEviction, type EmergencyResult, type EmergencyConfig, } from './emergency-eviction.js';
9
+ export { getSessionStateJSON, } from './session-helper.js';
@@ -0,0 +1,16 @@
1
+ // =============================================================================
2
+ // Context Module Exports
3
+ // =============================================================================
4
+ //
5
+ // Automatic context management, token usage tracking, and injection control.
6
+ //
7
+ // =============================================================================
8
+ export { UsageEstimator, usageEstimator, estimateTokens, isContextNearLimit, getContextFill, MODEL_CONTEXT_LIMITS, DEFAULT_MODEL_LIMIT, } from './usage-estimator.js';
9
+ export { ImportanceScorer, createImportanceScorer, } from './importance-scorer.js';
10
+ export { SessionStore, getSessionStore, closeSessionStore, CONTEXT_LIMITS, } from './session-store.js';
11
+ export { InMemoryDeduplicationTracker, createDeduplicationTracker, } from './deduplication.js';
12
+ export { EvictionEngine, createEvictionEngine, } from './eviction-engine.js';
13
+ export { HealthMonitor, createHealthMonitor, getHealthMonitor, resetHealthMonitor, } from './health-monitor.js';
14
+ export { ExchangeSummariser, createExchangeSummariser, } from './exchange-summariser.js';
15
+ export { EmergencyEviction, createEmergencyEviction, } from './emergency-eviction.js';
16
+ export { getSessionStateJSON, } from './session-helper.js';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Get current session state for injection budget calculation
3
+ *
4
+ * Returns JSON with estimated_fill and context_limit for the current session.
5
+ * If no session found or error occurs, returns empty object for graceful degradation.
6
+ *
7
+ * Usage from bash:
8
+ * SESSION_STATE=$(node -e "import('./session-helper.js').then(m => m.getSessionStateJSON('$SESSION_ID'))")
9
+ */
10
+ export declare function getSessionStateJSON(sessionId: string): Promise<void>;
@@ -0,0 +1,51 @@
1
+ // =============================================================================
2
+ // Session Helper - Bridge between hooks and SessionStore
3
+ // =============================================================================
4
+ // Provides a simple CLI interface for bash hooks to query session state
5
+ // =============================================================================
6
+ import { getSessionStore } from './session-store.js';
7
+ import { mcpLogger } from '../logger.js';
8
+ /**
9
+ * Get current session state for injection budget calculation
10
+ *
11
+ * Returns JSON with estimated_fill and context_limit for the current session.
12
+ * If no session found or error occurs, returns empty object for graceful degradation.
13
+ *
14
+ * Usage from bash:
15
+ * SESSION_STATE=$(node -e "import('./session-helper.js').then(m => m.getSessionStateJSON('$SESSION_ID'))")
16
+ */
17
+ export async function getSessionStateJSON(sessionId) {
18
+ try {
19
+ const store = getSessionStore();
20
+ const session = await store.getSession(sessionId);
21
+ if (!session) {
22
+ // No session found - return empty object for graceful degradation
23
+ console.log('{}');
24
+ return;
25
+ }
26
+ // Return minimal state needed for budget calculation
27
+ const state = {
28
+ estimated_fill: session.estimated_fill,
29
+ context_limit: session.context_limit,
30
+ };
31
+ console.log(JSON.stringify(state));
32
+ }
33
+ catch (error) {
34
+ // Error occurred - log for debugging but return empty for graceful degradation
35
+ mcpLogger.error('SESSION_HELPER', `Failed to get session state: ${error}`);
36
+ console.log('{}');
37
+ }
38
+ }
39
+ // CLI entry point
40
+ if (import.meta.url === `file://${process.argv[1]}`) {
41
+ const sessionId = process.argv[2];
42
+ if (!sessionId) {
43
+ console.error('Usage: session-helper.ts <session_id>');
44
+ process.exit(1);
45
+ }
46
+ getSessionStateJSON(sessionId).catch((error) => {
47
+ mcpLogger.error('SESSION_HELPER', `Error: ${error}`);
48
+ console.log('{}');
49
+ process.exit(0); // Exit 0 for graceful degradation
50
+ });
51
+ }
@@ -0,0 +1,94 @@
1
+ import type { InjectedFact } from './importance-scorer.js';
2
+ /**
3
+ * Model types supported by Claude Code
4
+ */
5
+ export type ModelType = 'sonnet' | 'opus' | 'haiku';
6
+ /**
7
+ * Context window limits by model (in tokens)
8
+ */
9
+ export declare const CONTEXT_LIMITS: Record<ModelType, number>;
10
+ /**
11
+ * An exchange (turn) in the conversation
12
+ */
13
+ export interface Exchange {
14
+ turn: number;
15
+ user_tokens: number;
16
+ assistant_tokens: number;
17
+ tool_tokens: number;
18
+ summarised_at: Date | null;
19
+ }
20
+ /**
21
+ * Complete session state for persistence
22
+ * Extends the in-memory SessionState with additional persistence fields
23
+ */
24
+ export interface PersistedSessionState {
25
+ session_id: string;
26
+ started_at: Date;
27
+ model: ModelType;
28
+ context_limit: number;
29
+ injected_facts: InjectedFact[];
30
+ exchanges: Exchange[];
31
+ estimated_fill: number;
32
+ last_updated: Date;
33
+ }
34
+ export declare class SessionStore {
35
+ private db;
36
+ private dbPath;
37
+ constructor(dbPath?: string);
38
+ /**
39
+ * Create tables if they don't exist
40
+ */
41
+ private initializeSchema;
42
+ /**
43
+ * Create a new session
44
+ */
45
+ createSession(sessionId: string, model: ModelType): Promise<PersistedSessionState>;
46
+ /**
47
+ * Get session state by ID
48
+ */
49
+ getSession(sessionId: string): Promise<PersistedSessionState | null>;
50
+ /**
51
+ * Update session state
52
+ */
53
+ updateSession(sessionId: string, updates: Partial<Pick<PersistedSessionState, 'estimated_fill'>>): Promise<void>;
54
+ /**
55
+ * Add an injected fact to the session
56
+ */
57
+ addInjectedFact(sessionId: string, fact: InjectedFact): Promise<void>;
58
+ /**
59
+ * Add an exchange to the session
60
+ */
61
+ addExchange(sessionId: string, exchange: Exchange): Promise<void>;
62
+ /**
63
+ * Update last_referenced for a fact
64
+ */
65
+ updateFactReference(sessionId: string, edgeId: string): Promise<void>;
66
+ /**
67
+ * Mark an exchange as summarised
68
+ */
69
+ markExchangeSummarised(sessionId: string, turn: number): Promise<void>;
70
+ /**
71
+ * Delete a session and all its data
72
+ */
73
+ deleteSession(sessionId: string): Promise<void>;
74
+ /**
75
+ * List all active sessions
76
+ */
77
+ listSessions(): Promise<Array<Pick<PersistedSessionState, 'session_id' | 'started_at' | 'model' | 'last_updated'>>>;
78
+ /**
79
+ * Clean up old sessions (older than N days)
80
+ */
81
+ cleanupOldSessions(daysOld?: number): Promise<number>;
82
+ /**
83
+ * Close the database connection
84
+ */
85
+ close(): void;
86
+ }
87
+ /**
88
+ * Get or create the singleton session store instance
89
+ */
90
+ export declare function getSessionStore(): SessionStore;
91
+ /**
92
+ * Close the singleton instance (for cleanup)
93
+ */
94
+ export declare function closeSessionStore(): void;
@@ -0,0 +1,286 @@
1
+ // =============================================================================
2
+ // Session State Store - Local SQLite storage for context management
3
+ // =============================================================================
4
+ // Stores session state including injected facts, exchanges, and token estimates
5
+ // for automatic context window management in Claude Code sessions.
6
+ // =============================================================================
7
+ import Database from 'better-sqlite3';
8
+ import { mcpLogger } from '../logger.js';
9
+ import * as path from 'path';
10
+ import * as fs from 'fs';
11
+ import { getConfigDir } from '../helpers/config-manager.js';
12
+ /**
13
+ * Context window limits by model (in tokens)
14
+ */
15
+ export const CONTEXT_LIMITS = {
16
+ sonnet: 200_000,
17
+ opus: 200_000,
18
+ haiku: 200_000,
19
+ };
20
+ // -----------------------------------------------------------------------------
21
+ // Session Store Implementation
22
+ // -----------------------------------------------------------------------------
23
+ export class SessionStore {
24
+ db;
25
+ dbPath;
26
+ constructor(dbPath) {
27
+ // Default to ~/.claudetools/sessions.db
28
+ this.dbPath = dbPath || path.join(getConfigDir(), 'sessions.db');
29
+ // Ensure directory exists
30
+ const dir = path.dirname(this.dbPath);
31
+ if (!fs.existsSync(dir)) {
32
+ fs.mkdirSync(dir, { recursive: true });
33
+ }
34
+ // Open database
35
+ this.db = new Database(this.dbPath);
36
+ this.db.pragma('journal_mode = WAL'); // Better concurrency
37
+ this.db.pragma('foreign_keys = ON');
38
+ // Initialize schema
39
+ this.initializeSchema();
40
+ }
41
+ /**
42
+ * Create tables if they don't exist
43
+ */
44
+ initializeSchema() {
45
+ this.db.exec(`
46
+ CREATE TABLE IF NOT EXISTS sessions (
47
+ session_id TEXT PRIMARY KEY,
48
+ started_at TEXT NOT NULL,
49
+ model TEXT NOT NULL CHECK(model IN ('sonnet', 'opus', 'haiku')),
50
+ context_limit INTEGER NOT NULL,
51
+ estimated_fill REAL NOT NULL DEFAULT 0,
52
+ last_updated TEXT NOT NULL
53
+ );
54
+
55
+ CREATE TABLE IF NOT EXISTS injected_facts (
56
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
57
+ session_id TEXT NOT NULL,
58
+ fact_data TEXT NOT NULL,
59
+ injected_at TEXT NOT NULL,
60
+ last_referenced TEXT,
61
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
62
+ );
63
+
64
+ CREATE TABLE IF NOT EXISTS exchanges (
65
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
66
+ session_id TEXT NOT NULL,
67
+ turn INTEGER NOT NULL,
68
+ user_tokens INTEGER NOT NULL,
69
+ assistant_tokens INTEGER NOT NULL,
70
+ tool_tokens INTEGER NOT NULL,
71
+ summarised_at TEXT,
72
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
73
+ );
74
+
75
+ -- Indexes for performance
76
+ CREATE INDEX IF NOT EXISTS idx_injected_facts_session
77
+ ON injected_facts(session_id);
78
+ CREATE INDEX IF NOT EXISTS idx_exchanges_session
79
+ ON exchanges(session_id);
80
+ CREATE INDEX IF NOT EXISTS idx_exchanges_turn
81
+ ON exchanges(session_id, turn);
82
+ `);
83
+ mcpLogger.debug('MEMORY', `Initialized session database at ${this.dbPath}`);
84
+ }
85
+ /**
86
+ * Create a new session
87
+ */
88
+ async createSession(sessionId, model) {
89
+ const now = new Date().toISOString();
90
+ const contextLimit = CONTEXT_LIMITS[model];
91
+ const stmt = this.db.prepare(`
92
+ INSERT INTO sessions (session_id, started_at, model, context_limit, estimated_fill, last_updated)
93
+ VALUES (?, ?, ?, ?, 0, ?)
94
+ `);
95
+ stmt.run(sessionId, now, model, contextLimit, now);
96
+ mcpLogger.debug('MEMORY', `Created session ${sessionId} for model ${model}`);
97
+ return {
98
+ session_id: sessionId,
99
+ started_at: new Date(now),
100
+ model,
101
+ context_limit: contextLimit,
102
+ injected_facts: [],
103
+ exchanges: [],
104
+ estimated_fill: 0,
105
+ last_updated: new Date(now),
106
+ };
107
+ }
108
+ /**
109
+ * Get session state by ID
110
+ */
111
+ async getSession(sessionId) {
112
+ const session = this.db
113
+ .prepare('SELECT * FROM sessions WHERE session_id = ?')
114
+ .get(sessionId);
115
+ if (!session) {
116
+ return null;
117
+ }
118
+ // Load injected facts
119
+ const factRows = this.db
120
+ .prepare('SELECT * FROM injected_facts WHERE session_id = ? ORDER BY injected_at')
121
+ .all(sessionId);
122
+ // Load exchanges
123
+ const exchanges = this.db
124
+ .prepare('SELECT * FROM exchanges WHERE session_id = ? ORDER BY turn')
125
+ .all(sessionId);
126
+ // Parse fact data from JSON
127
+ const facts = factRows.map((row) => {
128
+ const fact = JSON.parse(row.fact_data);
129
+ return {
130
+ ...fact,
131
+ injected_at: new Date(row.injected_at),
132
+ last_referenced: row.last_referenced ? new Date(row.last_referenced) : undefined,
133
+ };
134
+ });
135
+ return {
136
+ session_id: session.session_id,
137
+ started_at: new Date(session.started_at),
138
+ model: session.model,
139
+ context_limit: session.context_limit,
140
+ injected_facts: facts,
141
+ exchanges: exchanges.map((e) => ({
142
+ turn: e.turn,
143
+ user_tokens: e.user_tokens,
144
+ assistant_tokens: e.assistant_tokens,
145
+ tool_tokens: e.tool_tokens,
146
+ summarised_at: e.summarised_at ? new Date(e.summarised_at) : null,
147
+ })),
148
+ estimated_fill: session.estimated_fill,
149
+ last_updated: new Date(session.last_updated),
150
+ };
151
+ }
152
+ /**
153
+ * Update session state
154
+ */
155
+ async updateSession(sessionId, updates) {
156
+ const now = new Date().toISOString();
157
+ const fields = ['last_updated = ?'];
158
+ const values = [now];
159
+ if (updates.estimated_fill !== undefined) {
160
+ fields.push('estimated_fill = ?');
161
+ values.push(updates.estimated_fill);
162
+ }
163
+ values.push(sessionId);
164
+ const stmt = this.db.prepare(`
165
+ UPDATE sessions
166
+ SET ${fields.join(', ')}
167
+ WHERE session_id = ?
168
+ `);
169
+ stmt.run(...values);
170
+ mcpLogger.debug('MEMORY', `Updated session ${sessionId}`);
171
+ }
172
+ /**
173
+ * Add an injected fact to the session
174
+ */
175
+ async addInjectedFact(sessionId, fact) {
176
+ // Store the fact as JSON, excluding the temporal fields we track separately
177
+ const { injected_at, last_referenced, ...factData } = fact;
178
+ const stmt = this.db.prepare(`
179
+ INSERT INTO injected_facts (session_id, fact_data, injected_at, last_referenced)
180
+ VALUES (?, ?, ?, ?)
181
+ `);
182
+ stmt.run(sessionId, JSON.stringify(factData), injected_at.toISOString(), last_referenced ? last_referenced.toISOString() : null);
183
+ mcpLogger.debug('MEMORY', `Added fact ${fact.edge_id} to session ${sessionId}`);
184
+ }
185
+ /**
186
+ * Add an exchange to the session
187
+ */
188
+ async addExchange(sessionId, exchange) {
189
+ const stmt = this.db.prepare(`
190
+ INSERT INTO exchanges (session_id, turn, user_tokens, assistant_tokens, tool_tokens, summarised_at)
191
+ VALUES (?, ?, ?, ?, ?, ?)
192
+ `);
193
+ stmt.run(sessionId, exchange.turn, exchange.user_tokens, exchange.assistant_tokens, exchange.tool_tokens, exchange.summarised_at ? exchange.summarised_at.toISOString() : null);
194
+ mcpLogger.debug('MEMORY', `Added exchange turn ${exchange.turn} to session ${sessionId}`);
195
+ }
196
+ /**
197
+ * Update last_referenced for a fact
198
+ */
199
+ async updateFactReference(sessionId, edgeId) {
200
+ const now = new Date().toISOString();
201
+ // Need to find the fact by parsing JSON - SQLite JSON functions
202
+ const stmt = this.db.prepare(`
203
+ UPDATE injected_facts
204
+ SET last_referenced = ?
205
+ WHERE session_id = ? AND json_extract(fact_data, '$.edge_id') = ?
206
+ `);
207
+ stmt.run(now, sessionId, edgeId);
208
+ }
209
+ /**
210
+ * Mark an exchange as summarised
211
+ */
212
+ async markExchangeSummarised(sessionId, turn) {
213
+ const now = new Date().toISOString();
214
+ const stmt = this.db.prepare(`
215
+ UPDATE exchanges
216
+ SET summarised_at = ?
217
+ WHERE session_id = ? AND turn = ?
218
+ `);
219
+ stmt.run(now, sessionId, turn);
220
+ }
221
+ /**
222
+ * Delete a session and all its data
223
+ */
224
+ async deleteSession(sessionId) {
225
+ const stmt = this.db.prepare('DELETE FROM sessions WHERE session_id = ?');
226
+ stmt.run(sessionId);
227
+ mcpLogger.debug('MEMORY', `Deleted session ${sessionId}`);
228
+ }
229
+ /**
230
+ * List all active sessions
231
+ */
232
+ async listSessions() {
233
+ const sessions = this.db
234
+ .prepare('SELECT session_id, started_at, model, last_updated FROM sessions ORDER BY started_at DESC')
235
+ .all();
236
+ return sessions.map((s) => ({
237
+ session_id: s.session_id,
238
+ started_at: new Date(s.started_at),
239
+ model: s.model,
240
+ last_updated: new Date(s.last_updated),
241
+ }));
242
+ }
243
+ /**
244
+ * Clean up old sessions (older than N days)
245
+ */
246
+ async cleanupOldSessions(daysOld = 7) {
247
+ const cutoffDate = new Date();
248
+ cutoffDate.setDate(cutoffDate.getDate() - daysOld);
249
+ const stmt = this.db.prepare(`
250
+ DELETE FROM sessions
251
+ WHERE started_at < ?
252
+ `);
253
+ const result = stmt.run(cutoffDate.toISOString());
254
+ mcpLogger.info('MEMORY', `Cleaned up ${result.changes} sessions older than ${daysOld} days`);
255
+ return result.changes;
256
+ }
257
+ /**
258
+ * Close the database connection
259
+ */
260
+ close() {
261
+ this.db.close();
262
+ mcpLogger.debug('MEMORY', 'Closed session database connection');
263
+ }
264
+ }
265
+ // -----------------------------------------------------------------------------
266
+ // Singleton Instance
267
+ // -----------------------------------------------------------------------------
268
+ let storeInstance = null;
269
+ /**
270
+ * Get or create the singleton session store instance
271
+ */
272
+ export function getSessionStore() {
273
+ if (!storeInstance) {
274
+ storeInstance = new SessionStore();
275
+ }
276
+ return storeInstance;
277
+ }
278
+ /**
279
+ * Close the singleton instance (for cleanup)
280
+ */
281
+ export function closeSessionStore() {
282
+ if (storeInstance) {
283
+ storeInstance.close();
284
+ storeInstance = null;
285
+ }
286
+ }