sigma-memory 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts ADDED
@@ -0,0 +1,178 @@
1
+ import { join } from 'path';
2
+ import { homedir } from 'os';
3
+ import { existsSync, mkdirSync } from 'fs';
4
+ import { NotesManager } from './notes.js';
5
+ import { OntologyManager } from './ontology.js';
6
+ import { QMDManager } from './qmd.js';
7
+ import type { MemoryConfig, UnifiedSearchResult, MemoryStatus } from './types.js';
8
+
9
+ export class SigmaMemory {
10
+ public readonly notes: NotesManager;
11
+ public readonly ontology: OntologyManager;
12
+ public readonly qmd: QMDManager;
13
+ private readonly config: MemoryConfig;
14
+
15
+ constructor(config?: Partial<MemoryConfig>) {
16
+ // Configuration par défaut
17
+ const defaultConfig: MemoryConfig = {
18
+ memoryDir: join(homedir(), '.phi', 'memory'),
19
+ projectMemoryDir: join(process.cwd(), '.phi', 'memory'),
20
+ ontologyPath: join(homedir(), '.phi', 'memory', 'ontology', 'graph.jsonl'),
21
+ qmdEnabled: true,
22
+ qmdCommand: 'qmd'
23
+ };
24
+
25
+ this.config = { ...defaultConfig, ...config };
26
+
27
+ // Initialise les managers
28
+ this.notes = new NotesManager(this.config);
29
+ this.ontology = new OntologyManager(this.config);
30
+ this.qmd = new QMDManager(this.config);
31
+ }
32
+
33
+ /**
34
+ * Recherche unifiée : cherche dans notes + ontology + QMD, combine les résultats
35
+ */
36
+ async search(query: string): Promise<UnifiedSearchResult[]> {
37
+ const results: UnifiedSearchResult[] = [];
38
+
39
+ // Recherche dans les notes
40
+ try {
41
+ const notesResults = this.notes.search(query);
42
+ for (const result of notesResults) {
43
+ results.push({
44
+ source: 'notes',
45
+ type: 'note',
46
+ score: 0.8, // Score par défaut pour les notes
47
+ data: result
48
+ });
49
+ }
50
+ } catch (error) {
51
+ console.error('Notes search error:', error);
52
+ }
53
+
54
+ // Recherche dans l'ontologie
55
+ try {
56
+ const entityResults = this.ontology.findEntity({ name: query });
57
+ for (const entity of entityResults) {
58
+ results.push({
59
+ source: 'ontology',
60
+ type: 'entity',
61
+ score: 0.9, // Score élevé pour les entités
62
+ data: entity
63
+ });
64
+
65
+ // Inclut aussi les relations de cette entité
66
+ const relations = this.ontology.findRelations(entity.id);
67
+ for (const relation of relations) {
68
+ results.push({
69
+ source: 'ontology',
70
+ type: 'relation',
71
+ score: 0.7,
72
+ data: relation
73
+ });
74
+ }
75
+ }
76
+ } catch (error) {
77
+ console.error('Ontology search error:', error);
78
+ }
79
+
80
+ // Recherche QMD (vectorielle)
81
+ try {
82
+ const qmdResults = await this.qmd.search(query, 5);
83
+ for (const result of qmdResults) {
84
+ results.push({
85
+ source: 'qmd',
86
+ type: 'file',
87
+ score: result.score,
88
+ data: result
89
+ });
90
+ }
91
+ } catch (error) {
92
+ console.error('QMD search error:', error);
93
+ }
94
+
95
+ // Trie par score décroissant
96
+ results.sort((a, b) => b.score - a.score);
97
+
98
+ return results;
99
+ }
100
+
101
+ /**
102
+ * Initialise tous les dossiers nécessaires
103
+ */
104
+ async init(): Promise<void> {
105
+ // Crée les dossiers de base
106
+ if (!existsSync(this.config.memoryDir)) {
107
+ mkdirSync(this.config.memoryDir, { recursive: true });
108
+ }
109
+
110
+ if (!existsSync(this.config.projectMemoryDir)) {
111
+ mkdirSync(this.config.projectMemoryDir, { recursive: true });
112
+ }
113
+
114
+ // Initialise QMD si activé
115
+ if (this.config.qmdEnabled && this.qmd.isAvailable()) {
116
+ try {
117
+ await this.qmd.update();
118
+ } catch (error) {
119
+ console.error('QMD initialization error:', error);
120
+ }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Status de tous les sous-systèmes
126
+ */
127
+ async status(): Promise<MemoryStatus> {
128
+ // Status des notes
129
+ const notesList = this.notes.list();
130
+ const notesStatus = {
131
+ count: notesList.length,
132
+ totalSize: notesList.reduce((sum, note) => sum + note.size, 0),
133
+ lastModified: notesList.length > 0 ? notesList[0].date : null
134
+ };
135
+
136
+ // Status de l'ontologie
137
+ const ontologyStats = this.ontology.stats();
138
+ const ontologyGraph = this.ontology.getGraph();
139
+ const ontologyStatus = {
140
+ entities: ontologyGraph.entities.length,
141
+ relations: ontologyGraph.relations.length,
142
+ entitiesByType: ontologyStats.entitiesByType,
143
+ relationsByType: ontologyStats.relationsByType
144
+ };
145
+
146
+ // Status QMD
147
+ let qmdStatus: MemoryStatus['qmd'] = { available: false };
148
+ if (this.config.qmdEnabled && this.qmd.isAvailable()) {
149
+ const status = await this.qmd.status();
150
+ qmdStatus = {
151
+ available: true,
152
+ status: status || { files: 0, chunks: 0, lastUpdate: null }
153
+ };
154
+ }
155
+
156
+ return {
157
+ notes: notesStatus,
158
+ ontology: ontologyStatus,
159
+ qmd: qmdStatus
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Configuration actuelle
165
+ */
166
+ getConfig(): MemoryConfig {
167
+ return { ...this.config };
168
+ }
169
+ }
170
+
171
+ // Exports pour une utilisation facile
172
+ export { NotesManager } from './notes.js';
173
+ export { OntologyManager } from './ontology.js';
174
+ export { QMDManager } from './qmd.js';
175
+ export * from './types.js';
176
+
177
+ // Export par défaut
178
+ export default SigmaMemory;
package/src/notes.ts ADDED
@@ -0,0 +1,163 @@
1
+ import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, mkdirSync, appendFileSync } from 'fs';
2
+ import { join, resolve } from 'path';
3
+ import { execSync } from 'child_process';
4
+ import type { MemoryConfig, Note } from './types.js';
5
+
6
+ export class NotesManager {
7
+ private config: MemoryConfig;
8
+ private notesDir: string;
9
+
10
+ constructor(config: MemoryConfig) {
11
+ this.config = config;
12
+ this.notesDir = join(config.memoryDir, 'notes');
13
+ this.ensureDirectories();
14
+ }
15
+
16
+ private ensureDirectories(): void {
17
+ if (!existsSync(this.config.memoryDir)) {
18
+ mkdirSync(this.config.memoryDir, { recursive: true });
19
+ }
20
+ if (!existsSync(this.notesDir)) {
21
+ mkdirSync(this.notesDir, { recursive: true });
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Écrit dans un fichier .md (date du jour si pas de nom)
27
+ */
28
+ write(content: string, filename?: string): void {
29
+ const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
30
+ const file = filename || `${today}.md`;
31
+ const filePath = join(this.notesDir, file);
32
+
33
+ writeFileSync(filePath, content, 'utf8');
34
+ }
35
+
36
+ /**
37
+ * Lit un fichier
38
+ */
39
+ read(filename: string): string {
40
+ const filePath = join(this.notesDir, filename);
41
+ if (!existsSync(filePath)) {
42
+ throw new Error(`File not found: ${filename}`);
43
+ }
44
+ return readFileSync(filePath, 'utf8');
45
+ }
46
+
47
+ /**
48
+ * Liste tous les fichiers .md avec leur taille et date
49
+ */
50
+ list(): Array<{ name: string; size: number; date: string }> {
51
+ if (!existsSync(this.notesDir)) {
52
+ return [];
53
+ }
54
+
55
+ return readdirSync(this.notesDir)
56
+ .filter(file => file.endsWith('.md'))
57
+ .map(file => {
58
+ const filePath = join(this.notesDir, file);
59
+ const stats = statSync(filePath);
60
+ return {
61
+ name: file,
62
+ size: stats.size,
63
+ date: stats.mtime.toISOString()
64
+ };
65
+ })
66
+ .sort((a, b) => b.date.localeCompare(a.date));
67
+ }
68
+
69
+ /**
70
+ * Recherche full-text (grep-like) dans tous les .md
71
+ */
72
+ search(query: string): Array<{ file: string; line: number; content: string }> {
73
+ if (!existsSync(this.notesDir)) {
74
+ return [];
75
+ }
76
+
77
+ const results: Array<{ file: string; line: number; content: string }> = [];
78
+
79
+ try {
80
+ // Utilise grep pour une recherche efficace
81
+ const grepResult = execSync(
82
+ `grep -n "${query.replace(/"/g, '\\"')}" "${this.notesDir}"/*.md 2>/dev/null || true`,
83
+ { encoding: 'utf8' }
84
+ );
85
+
86
+ if (grepResult.trim()) {
87
+ const lines = grepResult.trim().split('\n');
88
+ for (const line of lines) {
89
+ const match = line.match(/^(.+?):(\d+):(.+)$/);
90
+ if (match) {
91
+ const [, fullPath, lineNumber, content] = match;
92
+ const filename = fullPath.replace(this.notesDir + '/', '');
93
+ results.push({
94
+ file: filename,
95
+ line: parseInt(lineNumber),
96
+ content: content.trim()
97
+ });
98
+ }
99
+ }
100
+ }
101
+ } catch (error) {
102
+ // Fallback à une recherche en JavaScript si grep échoue
103
+ const files = readdirSync(this.notesDir).filter(f => f.endsWith('.md'));
104
+ for (const file of files) {
105
+ const filePath = join(this.notesDir, file);
106
+ const content = readFileSync(filePath, 'utf8');
107
+ const lines = content.split('\n');
108
+
109
+ lines.forEach((line, index) => {
110
+ if (line.toLowerCase().includes(query.toLowerCase())) {
111
+ results.push({
112
+ file,
113
+ line: index + 1,
114
+ content: line.trim()
115
+ });
116
+ }
117
+ });
118
+ }
119
+ }
120
+
121
+ return results;
122
+ }
123
+
124
+ /**
125
+ * Retourne les notes des N derniers jours
126
+ */
127
+ getRecent(days: number): Note[] {
128
+ const cutoffDate = new Date();
129
+ cutoffDate.setDate(cutoffDate.getDate() - days);
130
+
131
+ const files = this.list().filter(file => {
132
+ const fileDate = new Date(file.date);
133
+ return fileDate >= cutoffDate;
134
+ });
135
+
136
+ return files.map(file => {
137
+ const content = this.read(file.name);
138
+ return {
139
+ file: file.name,
140
+ date: file.date,
141
+ content
142
+ };
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Ajoute à un fichier existant
148
+ */
149
+ append(content: string, filename?: string): void {
150
+ const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
151
+ const file = filename || `${today}.md`;
152
+ const filePath = join(this.notesDir, file);
153
+
154
+ // Ajoute une ligne vide si le fichier existe déjà et ne se termine pas par une ligne vide
155
+ if (existsSync(filePath)) {
156
+ const existingContent = readFileSync(filePath, 'utf8');
157
+ const separator = existingContent.endsWith('\n') ? '' : '\n';
158
+ appendFileSync(filePath, separator + content, 'utf8');
159
+ } else {
160
+ writeFileSync(filePath, content, 'utf8');
161
+ }
162
+ }
163
+ }
@@ -0,0 +1,348 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { randomBytes } from 'crypto';
4
+ import type {
5
+ MemoryConfig,
6
+ OntologyEntity,
7
+ OntologyRelation,
8
+ OntologyJSONLEntry,
9
+ OntologyEntityEntry,
10
+ OntologyRelationEntry,
11
+ OntologyDeleteEntry
12
+ } from './types.js';
13
+
14
+ export class OntologyManager {
15
+ private config: MemoryConfig;
16
+ private graphPath: string;
17
+ private entities: Map<string, OntologyEntity> = new Map();
18
+ private relations: Map<string, OntologyRelation> = new Map();
19
+ private loaded = false;
20
+
21
+ constructor(config: MemoryConfig) {
22
+ this.config = config;
23
+ this.graphPath = config.ontologyPath;
24
+ this.ensureDirectories();
25
+ }
26
+
27
+ private ensureDirectories(): void {
28
+ const dir = dirname(this.graphPath);
29
+ if (!existsSync(dir)) {
30
+ mkdirSync(dir, { recursive: true });
31
+ }
32
+ }
33
+
34
+ private generateId(): string {
35
+ return randomBytes(16).toString('hex');
36
+ }
37
+
38
+ private loadGraph(): void {
39
+ if (this.loaded) return;
40
+
41
+ this.entities.clear();
42
+ this.relations.clear();
43
+
44
+ if (!existsSync(this.graphPath)) {
45
+ this.loaded = true;
46
+ return;
47
+ }
48
+
49
+ const content = readFileSync(this.graphPath, 'utf8');
50
+ const lines = content.trim().split('\n').filter(line => line.trim());
51
+
52
+ for (const line of lines) {
53
+ try {
54
+ const entry: OntologyJSONLEntry = JSON.parse(line);
55
+
56
+ switch (entry.kind) {
57
+ case 'entity':
58
+ this.entities.set(entry.id, {
59
+ id: entry.id,
60
+ type: entry.type,
61
+ name: entry.name,
62
+ properties: entry.properties,
63
+ createdAt: entry.createdAt,
64
+ updatedAt: entry.updatedAt
65
+ });
66
+ break;
67
+
68
+ case 'relation':
69
+ this.relations.set(entry.id, {
70
+ id: entry.id,
71
+ from: entry.from,
72
+ to: entry.to,
73
+ type: entry.type,
74
+ properties: entry.properties,
75
+ createdAt: entry.createdAt
76
+ });
77
+ break;
78
+
79
+ case 'delete':
80
+ // Supprime l'entité ou la relation
81
+ this.entities.delete(entry.targetId);
82
+ this.relations.delete(entry.targetId);
83
+ // Supprime aussi toutes les relations liées à cette entité
84
+ for (const [relationId, relation] of this.relations) {
85
+ if (relation.from === entry.targetId || relation.to === entry.targetId) {
86
+ this.relations.delete(relationId);
87
+ }
88
+ }
89
+ break;
90
+ }
91
+ } catch (error) {
92
+ console.error(`Error parsing JSONL line: ${line}`, error);
93
+ }
94
+ }
95
+
96
+ this.loaded = true;
97
+ }
98
+
99
+ private appendToFile(entry: OntologyJSONLEntry): void {
100
+ this.ensureDirectories();
101
+ const line = JSON.stringify(entry) + '\n';
102
+ appendFileSync(this.graphPath, line, 'utf8');
103
+ }
104
+
105
+ /**
106
+ * Ajoute une entité
107
+ */
108
+ addEntity(entity: Omit<OntologyEntity, 'id' | 'createdAt' | 'updatedAt'>): string {
109
+ this.loadGraph();
110
+
111
+ const id = this.generateId();
112
+ const now = new Date().toISOString();
113
+
114
+ const newEntity: OntologyEntity = {
115
+ ...entity,
116
+ id,
117
+ createdAt: now,
118
+ updatedAt: now
119
+ };
120
+
121
+ this.entities.set(id, newEntity);
122
+
123
+ const entry: OntologyEntityEntry = {
124
+ kind: 'entity',
125
+ ...newEntity
126
+ };
127
+
128
+ this.appendToFile(entry);
129
+ return id;
130
+ }
131
+
132
+ /**
133
+ * Ajoute une relation
134
+ */
135
+ addRelation(relation: Omit<OntologyRelation, 'id' | 'createdAt'>): string {
136
+ this.loadGraph();
137
+
138
+ // Vérifie que les entités source et destination existent
139
+ if (!this.entities.has(relation.from)) {
140
+ throw new Error(`Source entity not found: ${relation.from}`);
141
+ }
142
+ if (!this.entities.has(relation.to)) {
143
+ throw new Error(`Target entity not found: ${relation.to}`);
144
+ }
145
+
146
+ const id = this.generateId();
147
+ const now = new Date().toISOString();
148
+
149
+ const newRelation: OntologyRelation = {
150
+ ...relation,
151
+ id,
152
+ createdAt: now
153
+ };
154
+
155
+ this.relations.set(id, newRelation);
156
+
157
+ const entry: OntologyRelationEntry = {
158
+ kind: 'relation',
159
+ ...newRelation
160
+ };
161
+
162
+ this.appendToFile(entry);
163
+ return id;
164
+ }
165
+
166
+ /**
167
+ * Recherche par type/nom
168
+ */
169
+ findEntity(query: { type?: string; name?: string }): OntologyEntity[] {
170
+ this.loadGraph();
171
+
172
+ const results: OntologyEntity[] = [];
173
+
174
+ for (const entity of this.entities.values()) {
175
+ let matches = true;
176
+
177
+ if (query.type && entity.type !== query.type) {
178
+ matches = false;
179
+ }
180
+
181
+ if (query.name && !entity.name.toLowerCase().includes(query.name.toLowerCase())) {
182
+ matches = false;
183
+ }
184
+
185
+ if (matches) {
186
+ results.push(entity);
187
+ }
188
+ }
189
+
190
+ return results;
191
+ }
192
+
193
+ /**
194
+ * Toutes les relations d'une entité
195
+ */
196
+ findRelations(entityId: string): OntologyRelation[] {
197
+ this.loadGraph();
198
+
199
+ const results: OntologyRelation[] = [];
200
+
201
+ for (const relation of this.relations.values()) {
202
+ if (relation.from === entityId || relation.to === entityId) {
203
+ results.push(relation);
204
+ }
205
+ }
206
+
207
+ return results;
208
+ }
209
+
210
+ /**
211
+ * Retourne le graphe complet
212
+ */
213
+ getGraph(): { entities: OntologyEntity[]; relations: OntologyRelation[] } {
214
+ this.loadGraph();
215
+
216
+ return {
217
+ entities: Array.from(this.entities.values()),
218
+ relations: Array.from(this.relations.values())
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Supprime entité + ses relations
224
+ */
225
+ removeEntity(id: string): void {
226
+ this.loadGraph();
227
+
228
+ if (!this.entities.has(id)) {
229
+ throw new Error(`Entity not found: ${id}`);
230
+ }
231
+
232
+ // Marque comme supprimé dans le fichier
233
+ const deleteEntry: OntologyDeleteEntry = {
234
+ kind: 'delete',
235
+ targetId: id,
236
+ deletedAt: new Date().toISOString()
237
+ };
238
+
239
+ this.appendToFile(deleteEntry);
240
+
241
+ // Supprime de la mémoire
242
+ this.entities.delete(id);
243
+
244
+ // Supprime toutes les relations liées
245
+ for (const [relationId, relation] of this.relations) {
246
+ if (relation.from === id || relation.to === id) {
247
+ this.relations.delete(relationId);
248
+ }
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Trouve le chemin entre deux entités (BFS)
254
+ */
255
+ queryPath(fromId: string, toId: string, maxDepth = 5): Array<{ entity: OntologyEntity; relation?: OntologyRelation }> | null {
256
+ this.loadGraph();
257
+
258
+ if (!this.entities.has(fromId) || !this.entities.has(toId)) {
259
+ return null;
260
+ }
261
+
262
+ if (fromId === toId) {
263
+ return [{ entity: this.entities.get(fromId)! }];
264
+ }
265
+
266
+ // BFS pour trouver le chemin le plus court
267
+ const queue: Array<{ entityId: string; path: Array<{ entity: OntologyEntity; relation?: OntologyRelation }> }> = [
268
+ { entityId: fromId, path: [{ entity: this.entities.get(fromId)! }] }
269
+ ];
270
+
271
+ const visited = new Set<string>([fromId]);
272
+
273
+ while (queue.length > 0) {
274
+ const { entityId, path } = queue.shift()!;
275
+
276
+ if (path.length > maxDepth) {
277
+ continue;
278
+ }
279
+
280
+ // Trouve toutes les relations sortantes
281
+ for (const relation of this.relations.values()) {
282
+ if (relation.from === entityId) {
283
+ const targetId = relation.to;
284
+
285
+ if (targetId === toId) {
286
+ // Trouvé !
287
+ return [...path, { entity: this.entities.get(targetId)!, relation }];
288
+ }
289
+
290
+ if (!visited.has(targetId)) {
291
+ visited.add(targetId);
292
+ queue.push({
293
+ entityId: targetId,
294
+ path: [...path, { entity: this.entities.get(targetId)!, relation }]
295
+ });
296
+ }
297
+ }
298
+
299
+ // Relations bidirectionnelles (from <-> to)
300
+ if (relation.to === entityId) {
301
+ const targetId = relation.from;
302
+
303
+ if (targetId === toId) {
304
+ // Trouvé !
305
+ return [...path, { entity: this.entities.get(targetId)!, relation }];
306
+ }
307
+
308
+ if (!visited.has(targetId)) {
309
+ visited.add(targetId);
310
+ queue.push({
311
+ entityId: targetId,
312
+ path: [...path, { entity: this.entities.get(targetId)!, relation }]
313
+ });
314
+ }
315
+ }
316
+ }
317
+ }
318
+
319
+ return null; // Aucun chemin trouvé
320
+ }
321
+
322
+ /**
323
+ * Exporte tout le graphe en JSON lisible
324
+ */
325
+ export(): { entities: OntologyEntity[]; relations: OntologyRelation[] } {
326
+ return this.getGraph();
327
+ }
328
+
329
+ /**
330
+ * Statistiques : nombre d'entités par type, nombre de relations par type
331
+ */
332
+ stats(): { entitiesByType: Record<string, number>; relationsByType: Record<string, number> } {
333
+ this.loadGraph();
334
+
335
+ const entitiesByType: Record<string, number> = {};
336
+ const relationsByType: Record<string, number> = {};
337
+
338
+ for (const entity of this.entities.values()) {
339
+ entitiesByType[entity.type] = (entitiesByType[entity.type] || 0) + 1;
340
+ }
341
+
342
+ for (const relation of this.relations.values()) {
343
+ relationsByType[relation.type] = (relationsByType[relation.type] || 0) + 1;
344
+ }
345
+
346
+ return { entitiesByType, relationsByType };
347
+ }
348
+ }