@imayuur/contexthub-core 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/dist/config.d.ts +21 -0
- package/dist/config.js +75 -0
- package/dist/contexthub-ignore.d.ts +7 -0
- package/dist/contexthub-ignore.js +96 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +78 -0
- package/dist/limits.d.ts +25 -0
- package/dist/limits.js +34 -0
- package/dist/memory-storage.d.ts +53 -0
- package/dist/memory-storage.js +419 -0
- package/dist/query-pipeline.d.ts +20 -0
- package/dist/query-pipeline.js +182 -0
- package/dist/security.d.ts +118 -0
- package/dist/security.js +408 -0
- package/package.json +54 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MemoryStorage — Secure, encrypted file-based storage for ContextHub
|
|
4
|
+
*
|
|
5
|
+
* Security features:
|
|
6
|
+
* - AES-256-GCM encryption at rest for all JSON data files
|
|
7
|
+
* - Atomic writes (write to .tmp, then rename) to prevent corruption
|
|
8
|
+
* - In-process mutex to prevent race conditions on concurrent access
|
|
9
|
+
* - File size limits to prevent OOM/DoS
|
|
10
|
+
* - Entry count caps with automatic archival
|
|
11
|
+
* - Secure file permissions (0600 for files, 0700 for directories)
|
|
12
|
+
* - Automatic migration from plaintext to encrypted format
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
+
var ownKeys = function(o) {
|
|
32
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
+
var ar = [];
|
|
34
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
+
return ar;
|
|
36
|
+
};
|
|
37
|
+
return ownKeys(o);
|
|
38
|
+
};
|
|
39
|
+
return function (mod) {
|
|
40
|
+
if (mod && mod.__esModule) return mod;
|
|
41
|
+
var result = {};
|
|
42
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
+
__setModuleDefault(result, mod);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.MemoryStorage = void 0;
|
|
49
|
+
const crypto = __importStar(require("crypto"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const security_1 = require("./security");
|
|
53
|
+
// Simple in-process async mutex for file locking
|
|
54
|
+
class Mutex {
|
|
55
|
+
constructor() {
|
|
56
|
+
this.queue = [];
|
|
57
|
+
this.locked = false;
|
|
58
|
+
}
|
|
59
|
+
async acquire() {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const tryAcquire = () => {
|
|
62
|
+
if (!this.locked) {
|
|
63
|
+
this.locked = true;
|
|
64
|
+
resolve(() => this.release());
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this.queue.push(tryAcquire);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
tryAcquire();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
release() {
|
|
74
|
+
this.locked = false;
|
|
75
|
+
const next = this.queue.shift();
|
|
76
|
+
if (next)
|
|
77
|
+
next();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
class MemoryStorage {
|
|
81
|
+
constructor(repoPath) {
|
|
82
|
+
this.repoPath = repoPath;
|
|
83
|
+
this.contexthubPath = path.join(repoPath, '.contexthub');
|
|
84
|
+
this.sessionsPath = path.join(this.contexthubPath, 'sessions.json');
|
|
85
|
+
this.memoriesPath = path.join(this.contexthubPath, 'memories.json');
|
|
86
|
+
this.projectMetadataPath = path.join(this.contexthubPath, 'project-metadata.json');
|
|
87
|
+
this.security = new security_1.SecurityManager(repoPath);
|
|
88
|
+
this.mutex = new Mutex();
|
|
89
|
+
// Create .contexthub directory if it doesn't exist (with secure permissions)
|
|
90
|
+
if (!fs.existsSync(this.contexthubPath)) {
|
|
91
|
+
fs.mkdirSync(this.contexthubPath, { recursive: true });
|
|
92
|
+
this.security.setSecurePermissions(this.contexthubPath, true);
|
|
93
|
+
}
|
|
94
|
+
// Initialize files if they don't exist
|
|
95
|
+
this.initFile(this.sessionsPath, []);
|
|
96
|
+
this.initFile(this.memoriesPath, []);
|
|
97
|
+
this.initFile(this.projectMetadataPath, null);
|
|
98
|
+
}
|
|
99
|
+
// ── File I/O (Encrypted + Atomic) ──────────────────────────────────────
|
|
100
|
+
initFile(filePath, defaultContent) {
|
|
101
|
+
if (!fs.existsSync(filePath)) {
|
|
102
|
+
this.writeJSONFileSync(filePath, defaultContent);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Read and decrypt a JSON file from disk.
|
|
107
|
+
* Handles both encrypted and plaintext formats (for migration).
|
|
108
|
+
*/
|
|
109
|
+
readJSONFile(filePath) {
|
|
110
|
+
// Check file size before reading
|
|
111
|
+
this.security.checkFileSize(filePath);
|
|
112
|
+
const raw = fs.readFileSync(filePath, 'utf8').trim();
|
|
113
|
+
// Empty file — return default
|
|
114
|
+
if (raw.length === 0) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
// Try parsing as plaintext JSON first (for migration / backwards compat)
|
|
118
|
+
if (raw.startsWith('[') || raw.startsWith('{') || raw === 'null') {
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(raw);
|
|
121
|
+
// Auto-migrate: re-write as encrypted
|
|
122
|
+
this.writeJSONFileSync(filePath, parsed);
|
|
123
|
+
return parsed;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Not valid JSON — try decrypting
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Decrypt
|
|
130
|
+
try {
|
|
131
|
+
const decrypted = this.security.decrypt(raw);
|
|
132
|
+
return JSON.parse(decrypted);
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
throw new Error(`Failed to read data file (may be corrupted): ${path.basename(filePath)}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Encrypt and write JSON data to disk using atomic write.
|
|
140
|
+
* Writes to a .tmp file first, then renames — prevents corruption on crash.
|
|
141
|
+
*/
|
|
142
|
+
writeJSONFileSync(filePath, data) {
|
|
143
|
+
const jsonStr = JSON.stringify(data, null, 2);
|
|
144
|
+
const encrypted = this.security.encrypt(jsonStr);
|
|
145
|
+
const tmpPath = filePath + `.tmp.${crypto.randomBytes(4).toString('hex')}`;
|
|
146
|
+
try {
|
|
147
|
+
fs.writeFileSync(tmpPath, encrypted, { mode: 0o600 });
|
|
148
|
+
fs.renameSync(tmpPath, filePath);
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
// Clean up temp file on error
|
|
152
|
+
try {
|
|
153
|
+
fs.unlinkSync(tmpPath);
|
|
154
|
+
}
|
|
155
|
+
catch { /* ignore */ }
|
|
156
|
+
throw e;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// ── Session Management ─────────────────────────────────────────────────
|
|
160
|
+
async createSession(agent, metadata = {}) {
|
|
161
|
+
const release = await this.mutex.acquire();
|
|
162
|
+
try {
|
|
163
|
+
const id = crypto.randomUUID();
|
|
164
|
+
const startTime = Date.now();
|
|
165
|
+
// Sanitize inputs
|
|
166
|
+
const sanitizedAgent = this.security.sanitizeInput(agent, 100);
|
|
167
|
+
const session = {
|
|
168
|
+
id,
|
|
169
|
+
repoPath: this.repoPath,
|
|
170
|
+
startTime,
|
|
171
|
+
agent: sanitizedAgent,
|
|
172
|
+
metadata
|
|
173
|
+
};
|
|
174
|
+
const sessions = this.readJSONFile(this.sessionsPath);
|
|
175
|
+
sessions.push(session);
|
|
176
|
+
// Cap sessions at max entries
|
|
177
|
+
const capped = sessions.length > this.security.maxMemoryEntries
|
|
178
|
+
? sessions.slice(-this.security.maxMemoryEntries)
|
|
179
|
+
: sessions;
|
|
180
|
+
this.writeJSONFileSync(this.sessionsPath, capped);
|
|
181
|
+
return id;
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
release();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async endSession(sessionId) {
|
|
188
|
+
const release = await this.mutex.acquire();
|
|
189
|
+
try {
|
|
190
|
+
const sessions = this.readJSONFile(this.sessionsPath);
|
|
191
|
+
const sessionIndex = sessions.findIndex(s => s.id === sessionId);
|
|
192
|
+
if (sessionIndex !== -1) {
|
|
193
|
+
sessions[sessionIndex].endTime = Date.now();
|
|
194
|
+
this.writeJSONFileSync(this.sessionsPath, sessions);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
release();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async getSession(sessionId) {
|
|
202
|
+
const sessions = this.readJSONFile(this.sessionsPath);
|
|
203
|
+
return sessions.find(s => s.id === sessionId) || null;
|
|
204
|
+
}
|
|
205
|
+
async getSessions(limit) {
|
|
206
|
+
let sessions = this.readJSONFile(this.sessionsPath);
|
|
207
|
+
sessions.sort((a, b) => b.startTime - a.startTime); // descending by startTime
|
|
208
|
+
if (limit !== undefined) {
|
|
209
|
+
const safeLimit = this.security.validateLimit(limit);
|
|
210
|
+
sessions = sessions.slice(0, safeLimit);
|
|
211
|
+
}
|
|
212
|
+
return sessions;
|
|
213
|
+
}
|
|
214
|
+
// ── Memory Management ──────────────────────────────────────────────────
|
|
215
|
+
calculateJaccard(str1, str2) {
|
|
216
|
+
const words1 = str1.toLowerCase().split(/\W+/).filter(w => w.length > 2);
|
|
217
|
+
const words2 = str2.toLowerCase().split(/\W+/).filter(w => w.length > 2);
|
|
218
|
+
if (words1.length === 0 && words2.length === 0)
|
|
219
|
+
return 1;
|
|
220
|
+
if (words1.length === 0 || words2.length === 0)
|
|
221
|
+
return 0;
|
|
222
|
+
const set1 = new Set(words1);
|
|
223
|
+
const set2 = new Set(words2);
|
|
224
|
+
const intersection = new Set([...set1].filter(x => set2.has(x)));
|
|
225
|
+
const union = new Set([...set1, ...set2]);
|
|
226
|
+
return intersection.size / union.size;
|
|
227
|
+
}
|
|
228
|
+
async saveMemory(memory) {
|
|
229
|
+
const release = await this.mutex.acquire();
|
|
230
|
+
try {
|
|
231
|
+
const id = crypto.randomUUID();
|
|
232
|
+
// Sanitize content — redact sensitive data
|
|
233
|
+
let content = this.security.sanitizeInput(memory.content);
|
|
234
|
+
if (this.security.isSensitive(content)) {
|
|
235
|
+
content = this.security.redactSensitive(content);
|
|
236
|
+
}
|
|
237
|
+
// Validate type
|
|
238
|
+
const validType = this.security.validateMemoryType(memory.type || 'manual');
|
|
239
|
+
// Sanitize tags
|
|
240
|
+
const sanitizedTags = (memory.tags || [])
|
|
241
|
+
.map(tag => this.security.sanitizeInput(tag, 100))
|
|
242
|
+
.filter(tag => tag.length > 0)
|
|
243
|
+
.slice(0, 20); // Max 20 tags
|
|
244
|
+
const memoryWithId = {
|
|
245
|
+
...memory,
|
|
246
|
+
id,
|
|
247
|
+
content,
|
|
248
|
+
type: validType,
|
|
249
|
+
tags: sanitizedTags,
|
|
250
|
+
relatedPaths: memory.relatedPaths ? this.security.validateRelatedPaths(memory.relatedPaths) : undefined,
|
|
251
|
+
relatedSymbols: memory.relatedSymbols ? this.security.validateRelatedSymbols(memory.relatedSymbols) : undefined,
|
|
252
|
+
commitHash: memory.commitHash ? this.security.validateCommitHash(memory.commitHash) : undefined,
|
|
253
|
+
branch: memory.branch ? this.security.sanitizeInput(String(memory.branch), 100) : undefined,
|
|
254
|
+
};
|
|
255
|
+
let memories = this.readJSONFile(this.memoriesPath);
|
|
256
|
+
// Duplicate detection
|
|
257
|
+
if (memory.sessionId) {
|
|
258
|
+
const sessionMemories = memories.filter(m => m.sessionId === memory.sessionId).slice(-20);
|
|
259
|
+
for (const sm of sessionMemories) {
|
|
260
|
+
const jaccard = this.calculateJaccard(sm.content, content);
|
|
261
|
+
if (jaccard > 0.9) {
|
|
262
|
+
return sm.id; // Return existing ID if it's a near duplicate
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
memories.push(memoryWithId);
|
|
267
|
+
// Cap at max entries — archive old ones
|
|
268
|
+
const capped = memories.length > this.security.maxMemoryEntries
|
|
269
|
+
? memories.slice(-this.security.maxMemoryEntries)
|
|
270
|
+
: memories;
|
|
271
|
+
this.writeJSONFileSync(this.memoriesPath, capped);
|
|
272
|
+
return id;
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
release();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async getMemory(id) {
|
|
279
|
+
const memories = this.readJSONFile(this.memoriesPath);
|
|
280
|
+
return memories.find(mem => mem.id === id) || null;
|
|
281
|
+
}
|
|
282
|
+
async searchMemories(options) {
|
|
283
|
+
let memories = this.readJSONFile(this.memoriesPath);
|
|
284
|
+
if (options.sessionId) {
|
|
285
|
+
memories = memories.filter(mem => mem.sessionId === options.sessionId);
|
|
286
|
+
}
|
|
287
|
+
if (options.type) {
|
|
288
|
+
memories = memories.filter(mem => mem.type === options.type);
|
|
289
|
+
}
|
|
290
|
+
if (options.tags && options.tags.length > 0) {
|
|
291
|
+
const tags = options.tags;
|
|
292
|
+
memories = memories.filter(mem => tags.some(tag => mem.tags.includes(tag)));
|
|
293
|
+
}
|
|
294
|
+
memories.sort((a, b) => b.timestamp - a.timestamp); // descending by timestamp
|
|
295
|
+
if (options.offset !== undefined) {
|
|
296
|
+
const safeOffset = Math.max(0, Math.floor(options.offset));
|
|
297
|
+
memories = memories.slice(safeOffset);
|
|
298
|
+
}
|
|
299
|
+
if (options.limit !== undefined) {
|
|
300
|
+
const safeLimit = this.security.validateLimit(options.limit);
|
|
301
|
+
memories = memories.slice(0, safeLimit);
|
|
302
|
+
}
|
|
303
|
+
return memories;
|
|
304
|
+
}
|
|
305
|
+
// ── Project Metadata ───────────────────────────────────────────────────
|
|
306
|
+
async saveProjectMetadata(metadata) {
|
|
307
|
+
const release = await this.mutex.acquire();
|
|
308
|
+
try {
|
|
309
|
+
this.writeJSONFileSync(this.projectMetadataPath, metadata);
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
release();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
async getProjectMetadata() {
|
|
316
|
+
try {
|
|
317
|
+
return this.readJSONFile(this.projectMetadataPath);
|
|
318
|
+
}
|
|
319
|
+
catch (e) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// ── Maintenance & Compaction ───────────────────────────────────────────
|
|
324
|
+
async archiveOldMemories(maxAgeDays) {
|
|
325
|
+
const release = await this.mutex.acquire();
|
|
326
|
+
try {
|
|
327
|
+
const memories = this.readJSONFile(this.memoriesPath);
|
|
328
|
+
const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
|
|
329
|
+
const toKeep = [];
|
|
330
|
+
const toArchive = [];
|
|
331
|
+
for (const m of memories) {
|
|
332
|
+
if (m.timestamp < cutoff && !m.tags.includes('pinned')) {
|
|
333
|
+
toArchive.push(m);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
toKeep.push(m);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (toArchive.length > 0) {
|
|
340
|
+
this.writeJSONFileSync(this.memoriesPath, toKeep);
|
|
341
|
+
// Write to archive file
|
|
342
|
+
const archiveDir = path.join(this.contexthubPath, 'archive');
|
|
343
|
+
if (!fs.existsSync(archiveDir)) {
|
|
344
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
345
|
+
this.security.setSecurePermissions(archiveDir, true);
|
|
346
|
+
}
|
|
347
|
+
const archivePath = path.join(archiveDir, `memories-${Date.now()}.json`);
|
|
348
|
+
this.writeJSONFileSync(archivePath, toArchive);
|
|
349
|
+
}
|
|
350
|
+
return toArchive.length;
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
release();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async compactMemories() {
|
|
357
|
+
const release = await this.mutex.acquire();
|
|
358
|
+
try {
|
|
359
|
+
let memories = this.readJSONFile(this.memoriesPath);
|
|
360
|
+
let mergedCount = 0;
|
|
361
|
+
// We will group by sessionId and then look for adjacent prompt -> response
|
|
362
|
+
const sessionMap = new Map();
|
|
363
|
+
for (const m of memories) {
|
|
364
|
+
if (!sessionMap.has(m.sessionId))
|
|
365
|
+
sessionMap.set(m.sessionId, []);
|
|
366
|
+
sessionMap.get(m.sessionId).push(m);
|
|
367
|
+
}
|
|
368
|
+
const newMemories = [];
|
|
369
|
+
for (const [sessionId, sessionMems] of sessionMap.entries()) {
|
|
370
|
+
// Sort chronologically to find adjacent pairs easily
|
|
371
|
+
sessionMems.sort((a, b) => a.timestamp - b.timestamp);
|
|
372
|
+
let i = 0;
|
|
373
|
+
while (i < sessionMems.length) {
|
|
374
|
+
const m1 = sessionMems[i];
|
|
375
|
+
const m2 = i + 1 < sessionMems.length ? sessionMems[i + 1] : null;
|
|
376
|
+
if (m2 &&
|
|
377
|
+
m1.type === 'prompt' &&
|
|
378
|
+
m2.type === 'response' &&
|
|
379
|
+
!m1.tags.includes('pinned') &&
|
|
380
|
+
!m2.tags.includes('pinned')) {
|
|
381
|
+
// Merge them
|
|
382
|
+
const mergedContent = `Prompt: ${m1.content}\n\nResponse: ${m2.content}`;
|
|
383
|
+
const mergedTags = Array.from(new Set([...m1.tags, ...m2.tags]));
|
|
384
|
+
const mergedPaths = Array.from(new Set([...(m1.relatedPaths || []), ...(m2.relatedPaths || [])])).slice(0, 20);
|
|
385
|
+
newMemories.push({
|
|
386
|
+
id: crypto.randomUUID(),
|
|
387
|
+
sessionId: sessionId,
|
|
388
|
+
type: 'summary',
|
|
389
|
+
content: mergedContent,
|
|
390
|
+
timestamp: m2.timestamp,
|
|
391
|
+
tags: mergedTags,
|
|
392
|
+
relatedPaths: mergedPaths.length > 0 ? mergedPaths : undefined,
|
|
393
|
+
commitHash: m2.commitHash || m1.commitHash,
|
|
394
|
+
branch: m2.branch || m1.branch
|
|
395
|
+
});
|
|
396
|
+
mergedCount++;
|
|
397
|
+
i += 2; // skip both
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
newMemories.push(m1);
|
|
401
|
+
i++;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (mergedCount > 0) {
|
|
406
|
+
this.writeJSONFileSync(this.memoriesPath, newMemories);
|
|
407
|
+
}
|
|
408
|
+
return mergedCount;
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
release();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// ── Cleanup ────────────────────────────────────────────────────────────
|
|
415
|
+
async close() {
|
|
416
|
+
// Nothing to clean up
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
exports.MemoryStorage = MemoryStorage;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ContextHubCore } from './index';
|
|
2
|
+
import type { MemoryEntry } from '@imayuur/contexthub-shared-types';
|
|
3
|
+
export interface UnifiedQueryResult {
|
|
4
|
+
answerSummary: string;
|
|
5
|
+
memories: MemoryEntry[];
|
|
6
|
+
codeHits: Array<{
|
|
7
|
+
path: string;
|
|
8
|
+
symbol?: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
}>;
|
|
11
|
+
gitHits?: any[];
|
|
12
|
+
trace?: {
|
|
13
|
+
hops: Array<{
|
|
14
|
+
type: string;
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
}>;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export declare function runUnifiedQuery(query: string, limit: number, core: ContextHubCore, vectorEngine: any, graphManager: any, gitIntegration: any): Promise<UnifiedQueryResult>;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runUnifiedQuery = runUnifiedQuery;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
async function runUnifiedQuery(query, limit, core, vectorEngine, graphManager, gitIntegration) {
|
|
39
|
+
const security = core['storage']['security']; // Access security manager safely
|
|
40
|
+
const sanitizedQuery = security.sanitizeQuery(query);
|
|
41
|
+
const safeLimit = security.validateLimit(limit, 1, 100);
|
|
42
|
+
const queryLower = sanitizedQuery.toLowerCase();
|
|
43
|
+
function rrfMerge(lists, k = 60) {
|
|
44
|
+
const scores = new Map();
|
|
45
|
+
for (const list of lists) {
|
|
46
|
+
list.forEach((item, i) => {
|
|
47
|
+
scores.set(item.id, (scores.get(item.id) || 0) + 1 / (k + i + 1));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return scores;
|
|
51
|
+
}
|
|
52
|
+
// 1. Code Graph Traversal
|
|
53
|
+
const codeHits = [];
|
|
54
|
+
let trace = undefined;
|
|
55
|
+
try {
|
|
56
|
+
if (graphManager) {
|
|
57
|
+
const graph = await graphManager.loadGraph();
|
|
58
|
+
const referencedNodes = [];
|
|
59
|
+
for (const node of graph.nodes) {
|
|
60
|
+
if (node.kind === 'file') {
|
|
61
|
+
const basename = path.basename(node.path).toLowerCase();
|
|
62
|
+
if (queryLower.includes(basename)) {
|
|
63
|
+
referencedNodes.push(node);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (node.kind === 'symbol' && node.name) {
|
|
67
|
+
const symLower = node.name.toLowerCase();
|
|
68
|
+
const regex = new RegExp('\\b' + symLower + '\\b');
|
|
69
|
+
if (regex.test(queryLower)) {
|
|
70
|
+
referencedNodes.push(node);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const node of referencedNodes) {
|
|
75
|
+
codeHits.push({
|
|
76
|
+
path: node.path,
|
|
77
|
+
symbol: node.kind === 'symbol' ? node.name : undefined,
|
|
78
|
+
reason: `Directly mentioned in query`
|
|
79
|
+
});
|
|
80
|
+
const related = await graphManager.getRelatedSymbols(node.id, 5);
|
|
81
|
+
for (const rel of related) {
|
|
82
|
+
if (!codeHits.some(h => h.path === rel.path && h.symbol === rel.name)) {
|
|
83
|
+
codeHits.push({
|
|
84
|
+
path: rel.path,
|
|
85
|
+
symbol: rel.name,
|
|
86
|
+
reason: `Related symbol to '${node.name || path.basename(node.path)}' in graph`
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (referencedNodes.length >= 2) {
|
|
92
|
+
const fromNode = referencedNodes[0];
|
|
93
|
+
const toNode = referencedNodes[1];
|
|
94
|
+
const pathHops = await graphManager.tracePath(fromNode.id, toNode.id, 5);
|
|
95
|
+
if (pathHops) {
|
|
96
|
+
trace = { hops: pathHops };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
// Ignore
|
|
103
|
+
}
|
|
104
|
+
// 2. Git Recent Changes
|
|
105
|
+
let gitHits = undefined;
|
|
106
|
+
const historyKeywords = ['recent', 'change', 'git', 'commit', 'log', 'history', 'branch', 'timeline'];
|
|
107
|
+
const wantsHistory = historyKeywords.some(kw => queryLower.includes(kw));
|
|
108
|
+
if (wantsHistory && gitIntegration) {
|
|
109
|
+
try {
|
|
110
|
+
const summary = await gitIntegration.getGitSummary();
|
|
111
|
+
gitHits = summary.recentCommits.slice(0, 5).map((c) => ({
|
|
112
|
+
hash: c.hash.substring(0, 7),
|
|
113
|
+
message: c.message,
|
|
114
|
+
author: c.author,
|
|
115
|
+
date: c.date
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
// Ignore
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// 3. Load Memories
|
|
123
|
+
let allMemories = [];
|
|
124
|
+
try {
|
|
125
|
+
allMemories = await core.searchMemories({ limit: 1000 });
|
|
126
|
+
}
|
|
127
|
+
catch (e) { }
|
|
128
|
+
// 4. Semantic Search
|
|
129
|
+
let semanticResults = [];
|
|
130
|
+
try {
|
|
131
|
+
if (vectorEngine && allMemories.length > 0) {
|
|
132
|
+
semanticResults = await vectorEngine.searchSimilarText(sanitizedQuery, allMemories, 50);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (e) { }
|
|
136
|
+
// 5. Keyword Search
|
|
137
|
+
const keywordResults = allMemories.filter(mem => mem.content.toLowerCase().includes(queryLower) ||
|
|
138
|
+
mem.tags.some(tag => tag.toLowerCase().includes(queryLower))).slice(0, 50);
|
|
139
|
+
// 6. Graph Pseudo-hits
|
|
140
|
+
const graphMemories = allMemories.filter(mem => {
|
|
141
|
+
const matchPath = mem.relatedPaths?.some(p => codeHits.some(c => c.path === p));
|
|
142
|
+
const matchSym = mem.relatedSymbols?.some(s => codeHits.some(c => c.symbol === s));
|
|
143
|
+
return matchPath || matchSym;
|
|
144
|
+
}).slice(0, 50);
|
|
145
|
+
// 7. Git Pseudo-hits
|
|
146
|
+
const gitMemories = allMemories.filter(mem => {
|
|
147
|
+
if (!mem.commitHash || !gitHits)
|
|
148
|
+
return false;
|
|
149
|
+
return gitHits.some(g => mem.commitHash.startsWith(g.hash));
|
|
150
|
+
}).slice(0, 50);
|
|
151
|
+
// 8. RRF Merge
|
|
152
|
+
const rrfScores = rrfMerge([
|
|
153
|
+
semanticResults,
|
|
154
|
+
keywordResults,
|
|
155
|
+
graphMemories,
|
|
156
|
+
gitMemories
|
|
157
|
+
]);
|
|
158
|
+
const sortedMemories = Array.from(rrfScores.entries())
|
|
159
|
+
.sort((a, b) => b[1] - a[1])
|
|
160
|
+
.map(([id]) => allMemories.find(m => m.id === id))
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
.slice(0, safeLimit);
|
|
163
|
+
// 9. Deterministic Stitched Answer Summary
|
|
164
|
+
let answerSummary = `### ContextHub Query Results for "${sanitizedQuery}"\n\n`;
|
|
165
|
+
answerSummary += `* Found ${sortedMemories.length} relevant memory entries.\n`;
|
|
166
|
+
if (codeHits.length > 0) {
|
|
167
|
+
answerSummary += `* Identified ${codeHits.length} related symbols/files in the local knowledge graph.\n`;
|
|
168
|
+
}
|
|
169
|
+
if (gitHits && gitHits.length > 0) {
|
|
170
|
+
answerSummary += `* Retrieved ${gitHits.length} recent commits from git repository history.\n`;
|
|
171
|
+
}
|
|
172
|
+
if (trace) {
|
|
173
|
+
answerSummary += `* Traced dependency connection path: ${trace.hops.map(h => `\`${h.label}\` (${h.type})`).join(' → ')}\n`;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
answerSummary,
|
|
177
|
+
memories: sortedMemories,
|
|
178
|
+
codeHits: codeHits.slice(0, safeLimit),
|
|
179
|
+
gitHits,
|
|
180
|
+
trace
|
|
181
|
+
};
|
|
182
|
+
}
|