claude-memory-layer 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,289 @@
1
+ /**
2
+ * SharedStore - Cross-project troubleshooting knowledge store
3
+ * Manages promotion from verified entries to shared storage
4
+ */
5
+
6
+ import { randomUUID } from 'crypto';
7
+ import { dbRun, dbAll, toDate, type Database } from './db-wrapper.js';
8
+ import type {
9
+ SharedTroubleshootingEntry,
10
+ SharedTroubleshootingInput
11
+ } from './types.js';
12
+ import { SharedEventStore } from './shared-event-store.js';
13
+
14
+ export class SharedStore {
15
+ constructor(private sharedEventStore: SharedEventStore) {}
16
+
17
+ private get db(): Database {
18
+ return this.sharedEventStore.getDatabase();
19
+ }
20
+
21
+ /**
22
+ * Promote a verified troubleshooting entry to shared storage
23
+ */
24
+ async promoteEntry(
25
+ input: SharedTroubleshootingInput
26
+ ): Promise<string> {
27
+ const entryId = randomUUID();
28
+
29
+ await dbRun(
30
+ this.db,
31
+ `INSERT INTO shared_troubleshooting (
32
+ entry_id, source_project_hash, source_entry_id,
33
+ title, symptoms, root_cause, solution, topics,
34
+ technologies, confidence, promoted_at
35
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
36
+ ON CONFLICT (source_project_hash, source_entry_id)
37
+ DO UPDATE SET
38
+ title = excluded.title,
39
+ symptoms = excluded.symptoms,
40
+ root_cause = excluded.root_cause,
41
+ solution = excluded.solution,
42
+ topics = excluded.topics,
43
+ technologies = excluded.technologies,
44
+ confidence = CASE
45
+ WHEN excluded.confidence > shared_troubleshooting.confidence
46
+ THEN excluded.confidence
47
+ ELSE shared_troubleshooting.confidence
48
+ END`,
49
+ [
50
+ entryId,
51
+ input.sourceProjectHash,
52
+ input.sourceEntryId,
53
+ input.title,
54
+ JSON.stringify(input.symptoms),
55
+ input.rootCause,
56
+ input.solution,
57
+ JSON.stringify(input.topics),
58
+ JSON.stringify(input.technologies || []),
59
+ input.confidence
60
+ ]
61
+ );
62
+
63
+ return entryId;
64
+ }
65
+
66
+ /**
67
+ * Search troubleshooting entries by text query
68
+ */
69
+ async search(
70
+ query: string,
71
+ options?: { topK?: number; minConfidence?: number }
72
+ ): Promise<SharedTroubleshootingEntry[]> {
73
+ const topK = options?.topK || 5;
74
+ const minConfidence = options?.minConfidence || 0.5;
75
+ const searchPattern = `%${query}%`;
76
+
77
+ const rows = await dbAll<Record<string, unknown>>(
78
+ this.db,
79
+ `SELECT * FROM shared_troubleshooting
80
+ WHERE (title LIKE ? OR root_cause LIKE ? OR solution LIKE ?)
81
+ AND confidence >= ?
82
+ ORDER BY confidence DESC, usage_count DESC
83
+ LIMIT ?`,
84
+ [searchPattern, searchPattern, searchPattern, minConfidence, topK]
85
+ );
86
+
87
+ return rows.map(this.rowToEntry);
88
+ }
89
+
90
+ /**
91
+ * Search by topics
92
+ */
93
+ async searchByTopics(
94
+ topics: string[],
95
+ options?: { topK?: number; excludeProjectHash?: string }
96
+ ): Promise<SharedTroubleshootingEntry[]> {
97
+ const topK = options?.topK || 5;
98
+
99
+ if (topics.length === 0) {
100
+ return [];
101
+ }
102
+
103
+ const topicConditions = topics.map(() => `topics LIKE ?`).join(' OR ');
104
+ const topicParams = topics.map(t => `%"${t}"%`);
105
+
106
+ let query = `SELECT * FROM shared_troubleshooting WHERE (${topicConditions})`;
107
+ const params: unknown[] = [...topicParams];
108
+
109
+ if (options?.excludeProjectHash) {
110
+ query += ` AND source_project_hash != ?`;
111
+ params.push(options.excludeProjectHash);
112
+ }
113
+
114
+ query += ` ORDER BY confidence DESC, usage_count DESC LIMIT ?`;
115
+ params.push(topK);
116
+
117
+ const rows = await dbAll<Record<string, unknown>>(this.db, query, params);
118
+ return rows.map(this.rowToEntry);
119
+ }
120
+
121
+ /**
122
+ * Record usage of a shared entry (for ranking)
123
+ */
124
+ async recordUsage(entryId: string): Promise<void> {
125
+ await dbRun(
126
+ this.db,
127
+ `UPDATE shared_troubleshooting
128
+ SET usage_count = usage_count + 1,
129
+ last_used_at = CURRENT_TIMESTAMP
130
+ WHERE entry_id = ?`,
131
+ [entryId]
132
+ );
133
+ }
134
+
135
+ /**
136
+ * Get entry by ID
137
+ */
138
+ async get(entryId: string): Promise<SharedTroubleshootingEntry | null> {
139
+ const rows = await dbAll<Record<string, unknown>>(
140
+ this.db,
141
+ `SELECT * FROM shared_troubleshooting WHERE entry_id = ?`,
142
+ [entryId]
143
+ );
144
+
145
+ if (rows.length === 0) return null;
146
+ return this.rowToEntry(rows[0]);
147
+ }
148
+
149
+ /**
150
+ * Get entry by source (project hash + entry ID)
151
+ */
152
+ async getBySource(
153
+ projectHash: string,
154
+ sourceEntryId: string
155
+ ): Promise<SharedTroubleshootingEntry | null> {
156
+ const rows = await dbAll<Record<string, unknown>>(
157
+ this.db,
158
+ `SELECT * FROM shared_troubleshooting
159
+ WHERE source_project_hash = ? AND source_entry_id = ?`,
160
+ [projectHash, sourceEntryId]
161
+ );
162
+
163
+ if (rows.length === 0) return null;
164
+ return this.rowToEntry(rows[0]);
165
+ }
166
+
167
+ /**
168
+ * Check if an entry already exists in shared store
169
+ */
170
+ async exists(projectHash: string, sourceEntryId: string): Promise<boolean> {
171
+ const result = await dbAll<{ count: number }>(
172
+ this.db,
173
+ `SELECT COUNT(*) as count FROM shared_troubleshooting
174
+ WHERE source_project_hash = ? AND source_entry_id = ?`,
175
+ [projectHash, sourceEntryId]
176
+ );
177
+ return (result[0]?.count || 0) > 0;
178
+ }
179
+
180
+ /**
181
+ * Get all entries (with limit for safety)
182
+ */
183
+ async getAll(options?: { limit?: number }): Promise<SharedTroubleshootingEntry[]> {
184
+ const limit = options?.limit || 100;
185
+ const rows = await dbAll<Record<string, unknown>>(
186
+ this.db,
187
+ `SELECT * FROM shared_troubleshooting
188
+ ORDER BY confidence DESC, usage_count DESC
189
+ LIMIT ?`,
190
+ [limit]
191
+ );
192
+
193
+ return rows.map(this.rowToEntry);
194
+ }
195
+
196
+ /**
197
+ * Get total count
198
+ */
199
+ async count(): Promise<number> {
200
+ const result = await dbAll<{ count: number }>(
201
+ this.db,
202
+ `SELECT COUNT(*) as count FROM shared_troubleshooting`
203
+ );
204
+ return result[0]?.count || 0;
205
+ }
206
+
207
+ /**
208
+ * Get statistics
209
+ */
210
+ async getStats(): Promise<{
211
+ total: number;
212
+ averageConfidence: number;
213
+ topTopics: Array<{ topic: string; count: number }>;
214
+ totalUsageCount: number;
215
+ }> {
216
+ const countResult = await dbAll<{ count: number }>(
217
+ this.db,
218
+ `SELECT COUNT(*) as count FROM shared_troubleshooting`
219
+ );
220
+ const total = countResult[0]?.count || 0;
221
+
222
+ const avgResult = await dbAll<{ avg: number | null }>(
223
+ this.db,
224
+ `SELECT AVG(confidence) as avg FROM shared_troubleshooting`
225
+ );
226
+ const averageConfidence = avgResult[0]?.avg || 0;
227
+
228
+ const usageResult = await dbAll<{ total: number }>(
229
+ this.db,
230
+ `SELECT SUM(usage_count) as total FROM shared_troubleshooting`
231
+ );
232
+ const totalUsageCount = usageResult[0]?.total || 0;
233
+
234
+ // Get topic counts
235
+ const entries = await this.getAll({ limit: 1000 });
236
+ const topicCounts: Record<string, number> = {};
237
+ for (const entry of entries) {
238
+ for (const topic of entry.topics) {
239
+ topicCounts[topic] = (topicCounts[topic] || 0) + 1;
240
+ }
241
+ }
242
+
243
+ const topTopics = Object.entries(topicCounts)
244
+ .map(([topic, count]) => ({ topic, count }))
245
+ .sort((a, b) => b.count - a.count)
246
+ .slice(0, 10);
247
+
248
+ return { total, averageConfidence, topTopics, totalUsageCount };
249
+ }
250
+
251
+ /**
252
+ * Delete an entry
253
+ */
254
+ async delete(entryId: string): Promise<boolean> {
255
+ const before = await this.count();
256
+ await dbRun(
257
+ this.db,
258
+ `DELETE FROM shared_troubleshooting WHERE entry_id = ?`,
259
+ [entryId]
260
+ );
261
+ const after = await this.count();
262
+ return before > after;
263
+ }
264
+
265
+ private rowToEntry(row: Record<string, unknown>): SharedTroubleshootingEntry {
266
+ return {
267
+ entryId: row.entry_id as string,
268
+ sourceProjectHash: row.source_project_hash as string,
269
+ sourceEntryId: row.source_entry_id as string,
270
+ title: row.title as string,
271
+ symptoms: JSON.parse(row.symptoms as string || '[]'),
272
+ rootCause: row.root_cause as string,
273
+ solution: row.solution as string,
274
+ topics: JSON.parse(row.topics as string || '[]'),
275
+ technologies: JSON.parse(row.technologies as string || '[]'),
276
+ confidence: row.confidence as number,
277
+ usageCount: row.usage_count as number || 0,
278
+ lastUsedAt: row.last_used_at ? toDate(row.last_used_at) : undefined,
279
+ promotedAt: toDate(row.promoted_at),
280
+ createdAt: toDate(row.created_at)
281
+ };
282
+ }
283
+ }
284
+
285
+ export function createSharedStore(
286
+ sharedEventStore: SharedEventStore
287
+ ): SharedStore {
288
+ return new SharedStore(sharedEventStore);
289
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * SharedVectorStore - Vector store for cross-project semantic search
3
+ * Location: ~/.claude-code/memory/shared/vectors/
4
+ */
5
+
6
+ import * as lancedb from '@lancedb/lancedb';
7
+ import type { SharedEntryType, SharedSearchResult } from './types.js';
8
+
9
+ export interface SharedVectorRecord {
10
+ id: string;
11
+ entryId: string;
12
+ entryType: SharedEntryType;
13
+ content: string;
14
+ vector: number[];
15
+ topics: string[];
16
+ sourceProjectHash?: string;
17
+ }
18
+
19
+ export class SharedVectorStore {
20
+ private db: lancedb.Connection | null = null;
21
+ private table: lancedb.Table | null = null;
22
+ private readonly tableName = 'shared_knowledge';
23
+
24
+ constructor(private dbPath: string) {}
25
+
26
+ /**
27
+ * Initialize LanceDB connection
28
+ */
29
+ async initialize(): Promise<void> {
30
+ if (this.db) return;
31
+
32
+ this.db = await lancedb.connect(this.dbPath);
33
+
34
+ try {
35
+ const tables = await this.db.tableNames();
36
+ if (tables.includes(this.tableName)) {
37
+ this.table = await this.db.openTable(this.tableName);
38
+ }
39
+ } catch {
40
+ this.table = null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Add or update a shared vector record
46
+ */
47
+ async upsert(record: SharedVectorRecord): Promise<void> {
48
+ await this.initialize();
49
+
50
+ if (!this.db) {
51
+ throw new Error('Database not initialized');
52
+ }
53
+
54
+ const data = {
55
+ id: record.id,
56
+ entryId: record.entryId,
57
+ entryType: record.entryType,
58
+ content: record.content,
59
+ vector: record.vector,
60
+ topics: JSON.stringify(record.topics),
61
+ sourceProjectHash: record.sourceProjectHash || ''
62
+ };
63
+
64
+ if (!this.table) {
65
+ this.table = await this.db.createTable(this.tableName, [data]);
66
+ } else {
67
+ // Delete existing entry before adding (upsert behavior)
68
+ try {
69
+ await this.table.delete(`entryId = '${record.entryId}'`);
70
+ } catch {
71
+ // Entry might not exist, ignore
72
+ }
73
+ await this.table.add([data]);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Add multiple records in batch
79
+ */
80
+ async upsertBatch(records: SharedVectorRecord[]): Promise<void> {
81
+ if (records.length === 0) return;
82
+
83
+ await this.initialize();
84
+
85
+ if (!this.db) {
86
+ throw new Error('Database not initialized');
87
+ }
88
+
89
+ const data = records.map(record => ({
90
+ id: record.id,
91
+ entryId: record.entryId,
92
+ entryType: record.entryType,
93
+ content: record.content,
94
+ vector: record.vector,
95
+ topics: JSON.stringify(record.topics),
96
+ sourceProjectHash: record.sourceProjectHash || ''
97
+ }));
98
+
99
+ if (!this.table) {
100
+ this.table = await this.db.createTable(this.tableName, data);
101
+ } else {
102
+ await this.table.add(data);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Search for similar vectors
108
+ */
109
+ async search(
110
+ queryVector: number[],
111
+ options: {
112
+ limit?: number;
113
+ minScore?: number;
114
+ excludeProjectHash?: string;
115
+ entryType?: SharedEntryType;
116
+ } = {}
117
+ ): Promise<SharedSearchResult[]> {
118
+ await this.initialize();
119
+
120
+ if (!this.table) {
121
+ return [];
122
+ }
123
+
124
+ const { limit = 5, minScore = 0.7, excludeProjectHash, entryType } = options;
125
+
126
+ let query = this.table
127
+ .search(queryVector)
128
+ .distanceType('cosine')
129
+ .limit(limit * 2);
130
+
131
+ // Apply filters
132
+ const filters: string[] = [];
133
+ if (excludeProjectHash) {
134
+ filters.push(`sourceProjectHash != '${excludeProjectHash}'`);
135
+ }
136
+ if (entryType) {
137
+ filters.push(`entryType = '${entryType}'`);
138
+ }
139
+
140
+ if (filters.length > 0) {
141
+ query = query.where(filters.join(' AND '));
142
+ }
143
+
144
+ const results = await query.toArray();
145
+
146
+ return results
147
+ .filter(r => {
148
+ const distance = r._distance || 0;
149
+ const score = 1 - (distance / 2);
150
+ return score >= minScore;
151
+ })
152
+ .slice(0, limit)
153
+ .map(r => {
154
+ const distance = r._distance || 0;
155
+ const score = 1 - (distance / 2);
156
+ return {
157
+ id: r.id as string,
158
+ entryId: r.entryId as string,
159
+ content: r.content as string,
160
+ score,
161
+ entryType: r.entryType as SharedEntryType
162
+ };
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Delete vector by entry ID
168
+ */
169
+ async delete(entryId: string): Promise<void> {
170
+ if (!this.table) return;
171
+ await this.table.delete(`entryId = '${entryId}'`);
172
+ }
173
+
174
+ /**
175
+ * Get total count
176
+ */
177
+ async count(): Promise<number> {
178
+ if (!this.table) return 0;
179
+ return this.table.countRows();
180
+ }
181
+
182
+ /**
183
+ * Check if vector exists for entry
184
+ */
185
+ async exists(entryId: string): Promise<boolean> {
186
+ if (!this.table) return false;
187
+
188
+ try {
189
+ const results = await this.table
190
+ .search([])
191
+ .where(`entryId = '${entryId}'`)
192
+ .limit(1)
193
+ .toArray();
194
+ return results.length > 0;
195
+ } catch {
196
+ return false;
197
+ }
198
+ }
199
+ }
200
+
201
+ export function createSharedVectorStore(dbPath: string): SharedVectorStore {
202
+ return new SharedVectorStore(dbPath);
203
+ }
package/src/core/types.ts CHANGED
@@ -194,7 +194,14 @@ export const ConfigSchema = z.object({
194
194
  sessionSummary: z.boolean().default(true),
195
195
  insightExtraction: z.boolean().default(true),
196
196
  crossProjectLearning: z.boolean().default(false),
197
- singleWriterMode: z.boolean().default(true)
197
+ singleWriterMode: z.boolean().default(true),
198
+ sharedStore: z.object({
199
+ enabled: z.boolean().default(true),
200
+ autoPromote: z.boolean().default(true),
201
+ searchShared: z.boolean().default(true),
202
+ minConfidenceForPromotion: z.number().default(0.8),
203
+ sharedStoragePath: z.string().default('~/.claude-code/memory/shared')
204
+ }).default({})
198
205
  }).default({}),
199
206
  mode: z.enum(['session', 'endless']).default('session'),
200
207
  endless: z.object({
@@ -499,7 +506,8 @@ export const EntryTypeSchema = z.enum([
499
506
  'task_note',
500
507
  'reference',
501
508
  'preference',
502
- 'pattern'
509
+ 'pattern',
510
+ 'troubleshooting'
503
511
  ]);
504
512
  export type EntryType = z.infer<typeof EntryTypeSchema>;
505
513
 
@@ -839,3 +847,62 @@ export interface EndlessModeStatus {
839
847
  consolidatedCount: number;
840
848
  lastConsolidation: Date | null;
841
849
  }
850
+
851
+ // ============================================================
852
+ // Shared Store Types (Cross-Project Knowledge)
853
+ // ============================================================
854
+
855
+ export const SharedEntryTypeSchema = z.enum([
856
+ 'troubleshooting',
857
+ 'best_practice',
858
+ 'common_error'
859
+ ]);
860
+ export type SharedEntryType = z.infer<typeof SharedEntryTypeSchema>;
861
+
862
+ export const SharedTroubleshootingEntrySchema = z.object({
863
+ entryId: z.string(),
864
+ sourceProjectHash: z.string(),
865
+ sourceEntryId: z.string(),
866
+ title: z.string(),
867
+ symptoms: z.array(z.string()),
868
+ rootCause: z.string(),
869
+ solution: z.string(),
870
+ topics: z.array(z.string()),
871
+ technologies: z.array(z.string()).optional(),
872
+ confidence: z.number().min(0).max(1),
873
+ usageCount: z.number().default(0),
874
+ lastUsedAt: z.date().optional(),
875
+ promotedAt: z.date(),
876
+ createdAt: z.date()
877
+ });
878
+ export type SharedTroubleshootingEntry = z.infer<typeof SharedTroubleshootingEntrySchema>;
879
+
880
+ export interface SharedTroubleshootingInput {
881
+ sourceProjectHash: string;
882
+ sourceEntryId: string;
883
+ title: string;
884
+ symptoms: string[];
885
+ rootCause: string;
886
+ solution: string;
887
+ topics: string[];
888
+ technologies?: string[];
889
+ confidence: number;
890
+ }
891
+
892
+ export const SharedStoreConfigSchema = z.object({
893
+ enabled: z.boolean().default(true),
894
+ autoPromote: z.boolean().default(true),
895
+ searchShared: z.boolean().default(true),
896
+ minConfidenceForPromotion: z.number().default(0.8),
897
+ sharedStoragePath: z.string().default('~/.claude-code/memory/shared')
898
+ });
899
+ export type SharedStoreConfig = z.infer<typeof SharedStoreConfigSchema>;
900
+
901
+ // Shared search result
902
+ export interface SharedSearchResult {
903
+ id: string;
904
+ entryId: string;
905
+ content: string;
906
+ score: number;
907
+ entryType: SharedEntryType;
908
+ }
@@ -16,10 +16,14 @@ async function main(): Promise<void> {
16
16
  const memoryService = getMemoryServiceForSession(input.session_id);
17
17
 
18
18
  try {
19
- // Retrieve relevant memories for the prompt
19
+ // Check if shared store is enabled
20
+ const includeShared = memoryService.isSharedStoreEnabled();
21
+
22
+ // Retrieve relevant memories for the prompt (including shared if enabled)
20
23
  const retrievalResult = await memoryService.retrieveMemories(input.prompt, {
21
24
  topK: 5,
22
- minScore: 0.7
25
+ minScore: 0.7,
26
+ includeShared
23
27
  });
24
28
 
25
29
  // Store the user prompt for future retrieval