@hawon/nexus 0.1.0 → 0.3.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/README.md +60 -38
- package/dist/cli/index.js +76 -145
- package/dist/index.js +15 -26
- package/dist/mcp/server.js +61 -32
- package/package.json +2 -1
- package/scripts/auto-skill.sh +54 -0
- package/scripts/auto-sync.sh +11 -0
- package/scripts/benchmark.ts +444 -0
- package/scripts/scan-tool-result.sh +46 -0
- package/src/cli/index.ts +79 -172
- package/src/index.ts +17 -29
- package/src/mcp/server.ts +67 -41
- package/src/memory-engine/index.ts +4 -6
- package/src/memory-engine/nexus-memory.test.ts +437 -0
- package/src/memory-engine/nexus-memory.ts +631 -0
- package/src/memory-engine/semantic.ts +380 -0
- package/src/parser/parse.ts +1 -21
- package/src/promptguard/advanced-rules.ts +129 -12
- package/src/promptguard/entropy.ts +21 -2
- package/src/promptguard/evolution/auto-update.ts +16 -6
- package/src/promptguard/multilingual-rules.ts +68 -0
- package/src/promptguard/rules.ts +87 -2
- package/src/promptguard/scanner.test.ts +262 -0
- package/src/promptguard/scanner.ts +1 -1
- package/src/promptguard/semantic.ts +19 -4
- package/src/promptguard/token-analysis.ts +17 -5
- package/src/review/analyzer.test.ts +279 -0
- package/src/review/analyzer.ts +112 -28
- package/src/shared/stop-words.ts +21 -0
- package/src/skills/index.ts +11 -27
- package/src/skills/memory-skill-engine.ts +1044 -0
- package/src/testing/health-check.ts +19 -2
- package/src/cost/index.ts +0 -3
- package/src/cost/tracker.ts +0 -290
- package/src/cost/types.ts +0 -34
- package/src/memory-engine/compressor.ts +0 -97
- package/src/memory-engine/context-window.ts +0 -113
- package/src/memory-engine/store.ts +0 -371
- package/src/memory-engine/types.ts +0 -32
- package/src/skills/context-engine.ts +0 -863
- package/src/skills/extractor.ts +0 -224
- package/src/skills/global-context.ts +0 -726
- package/src/skills/library.ts +0 -189
- package/src/skills/pattern-engine.ts +0 -712
- package/src/skills/render-evolved.ts +0 -160
- package/src/skills/skill-reconciler.ts +0 -703
- package/src/skills/smart-extractor.ts +0 -843
- package/src/skills/types.ts +0 -18
- package/src/skills/wisdom-extractor.ts +0 -737
- package/src/superdev-evolution/index.ts +0 -3
- package/src/superdev-evolution/skill-manager.ts +0 -266
- package/src/superdev-evolution/types.ts +0 -20
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, unlinkSync } from "node:fs";
|
|
2
|
-
import { join, dirname } from "node:path";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import type { MemoryEntry, MemoryQuery, MemoryStats, MemoryTier } from "./types.js";
|
|
5
|
-
import { compressEntry, estimateSizeBytes } from "./compressor.js";
|
|
6
|
-
|
|
7
|
-
const TIERS: MemoryTier[] = ["working", "short_term", "long_term", "archive"];
|
|
8
|
-
const TIER_ORDER: Record<MemoryTier, number> = { working: 0, short_term: 1, long_term: 2, archive: 3 };
|
|
9
|
-
const PROMOTE_THRESHOLD = 5;
|
|
10
|
-
const DEMOTE_DAYS: Record<MemoryTier, number> = { working: 1, short_term: 7, long_term: 30, archive: Infinity };
|
|
11
|
-
const ARCHIVE_MAX_AGE_DAYS = 90;
|
|
12
|
-
|
|
13
|
-
const STOP_WORDS = new Set([
|
|
14
|
-
"the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
|
|
15
|
-
"have", "has", "had", "do", "does", "did", "will", "would", "could",
|
|
16
|
-
"should", "may", "might", "shall", "can", "need", "to", "of", "in",
|
|
17
|
-
"for", "on", "with", "at", "by", "from", "as", "into", "through",
|
|
18
|
-
"during", "before", "after", "all", "both", "each", "few", "more",
|
|
19
|
-
"most", "other", "some", "such", "no", "not", "only", "so", "than",
|
|
20
|
-
"too", "very", "just", "but", "and", "or", "if", "while", "that",
|
|
21
|
-
"this", "it", "its", "i", "me", "my", "we", "our", "you", "your",
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
|
-
function tokenize(text: string): string[] {
|
|
25
|
-
return text
|
|
26
|
-
.toLowerCase()
|
|
27
|
-
.replace(/[^a-z0-9\s]/g, " ")
|
|
28
|
-
.split(/\s+/)
|
|
29
|
-
.filter((w) => w.length > 1 && !STOP_WORDS.has(w));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function buildBowVector(tokens: string[], vocab: Map<string, number>): number[] {
|
|
33
|
-
const vec = new Array<number>(vocab.size).fill(0);
|
|
34
|
-
for (const t of tokens) {
|
|
35
|
-
const idx = vocab.get(t);
|
|
36
|
-
if (idx !== undefined) vec[idx]++;
|
|
37
|
-
}
|
|
38
|
-
return vec;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function cosineSimilarity(a: number[], b: number[]): number {
|
|
42
|
-
let dot = 0;
|
|
43
|
-
let magA = 0;
|
|
44
|
-
let magB = 0;
|
|
45
|
-
for (let i = 0; i < a.length; i++) {
|
|
46
|
-
dot += a[i] * b[i];
|
|
47
|
-
magA += a[i] * a[i];
|
|
48
|
-
magB += b[i] * b[i];
|
|
49
|
-
}
|
|
50
|
-
const denom = Math.sqrt(magA) * Math.sqrt(magB);
|
|
51
|
-
return denom === 0 ? 0 : dot / denom;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function daysSince(isoDate: string): number {
|
|
55
|
-
return (Date.now() - new Date(isoDate).getTime()) / (1000 * 60 * 60 * 24);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export type MemoryStore = {
|
|
59
|
-
add(entry: Omit<MemoryEntry, "id" | "createdAt" | "accessedAt" | "accessCount" | "relevanceScore" | "compressed"> & Partial<Pick<MemoryEntry, "id" | "createdAt" | "accessedAt" | "accessCount" | "relevanceScore" | "compressed">>): MemoryEntry;
|
|
60
|
-
get(id: string): MemoryEntry | undefined;
|
|
61
|
-
search(query: MemoryQuery): MemoryEntry[];
|
|
62
|
-
promote(id: string): MemoryEntry | undefined;
|
|
63
|
-
demote(id: string): MemoryEntry | undefined;
|
|
64
|
-
compress(id: string): MemoryEntry | undefined;
|
|
65
|
-
prune(): number;
|
|
66
|
-
getStats(): MemoryStats;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export function createMemoryStore(dataDir: string): MemoryStore {
|
|
70
|
-
const memoryDir = join(dataDir, "memory");
|
|
71
|
-
|
|
72
|
-
function ensureDirs(): void {
|
|
73
|
-
for (const tier of TIERS) {
|
|
74
|
-
const dir = join(memoryDir, tier);
|
|
75
|
-
if (!existsSync(dir)) {
|
|
76
|
-
mkdirSync(dir, { recursive: true });
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function entryPath(tier: MemoryTier, id: string): string {
|
|
82
|
-
return join(memoryDir, tier, `${id}.json`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function writeEntry(entry: MemoryEntry): void {
|
|
86
|
-
const p = entryPath(entry.tier, entry.id);
|
|
87
|
-
const dir = dirname(p);
|
|
88
|
-
if (!existsSync(dir)) {
|
|
89
|
-
mkdirSync(dir, { recursive: true });
|
|
90
|
-
}
|
|
91
|
-
writeFileSync(p, JSON.stringify(entry, null, 2), "utf-8");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function readEntry(tier: MemoryTier, id: string): MemoryEntry | undefined {
|
|
95
|
-
const p = entryPath(tier, id);
|
|
96
|
-
if (!existsSync(p)) return undefined;
|
|
97
|
-
return JSON.parse(readFileSync(p, "utf-8")) as MemoryEntry;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function deleteEntry(tier: MemoryTier, id: string): void {
|
|
101
|
-
const p = entryPath(tier, id);
|
|
102
|
-
if (existsSync(p)) unlinkSync(p);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function allEntries(tier?: MemoryTier): MemoryEntry[] {
|
|
106
|
-
const tiers = tier ? [tier] : TIERS;
|
|
107
|
-
const entries: MemoryEntry[] = [];
|
|
108
|
-
for (const t of tiers) {
|
|
109
|
-
const dir = join(memoryDir, t);
|
|
110
|
-
if (!existsSync(dir)) continue;
|
|
111
|
-
const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
112
|
-
for (const f of files) {
|
|
113
|
-
try {
|
|
114
|
-
const data = readFileSync(join(dir, f), "utf-8");
|
|
115
|
-
entries.push(JSON.parse(data) as MemoryEntry);
|
|
116
|
-
} catch {
|
|
117
|
-
// skip corrupt entries
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return entries;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function findEntry(id: string): MemoryEntry | undefined {
|
|
125
|
-
for (const tier of TIERS) {
|
|
126
|
-
const entry = readEntry(tier, id);
|
|
127
|
-
if (entry) return entry;
|
|
128
|
-
}
|
|
129
|
-
return undefined;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
ensureDirs();
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
add(partial) {
|
|
136
|
-
const now = new Date().toISOString();
|
|
137
|
-
const entry: MemoryEntry = {
|
|
138
|
-
id: partial.id ?? randomUUID(),
|
|
139
|
-
content: partial.content,
|
|
140
|
-
summary: partial.summary,
|
|
141
|
-
embedding: partial.embedding,
|
|
142
|
-
tier: partial.tier ?? "working",
|
|
143
|
-
tags: partial.tags ?? [],
|
|
144
|
-
projectId: partial.projectId,
|
|
145
|
-
filePath: partial.filePath,
|
|
146
|
-
createdAt: partial.createdAt ?? now,
|
|
147
|
-
accessedAt: partial.accessedAt ?? now,
|
|
148
|
-
accessCount: partial.accessCount ?? 0,
|
|
149
|
-
relevanceScore: partial.relevanceScore ?? 1.0,
|
|
150
|
-
compressed: partial.compressed ?? false,
|
|
151
|
-
};
|
|
152
|
-
writeEntry(entry);
|
|
153
|
-
return entry;
|
|
154
|
-
},
|
|
155
|
-
|
|
156
|
-
get(id) {
|
|
157
|
-
const entry = findEntry(id);
|
|
158
|
-
if (!entry) return undefined;
|
|
159
|
-
entry.accessedAt = new Date().toISOString();
|
|
160
|
-
entry.accessCount++;
|
|
161
|
-
writeEntry(entry);
|
|
162
|
-
|
|
163
|
-
if (entry.accessCount >= PROMOTE_THRESHOLD && TIER_ORDER[entry.tier] > 0) {
|
|
164
|
-
const higherTier = TIERS[TIER_ORDER[entry.tier] - 1];
|
|
165
|
-
deleteEntry(entry.tier, entry.id);
|
|
166
|
-
entry.tier = higherTier;
|
|
167
|
-
writeEntry(entry);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return entry;
|
|
171
|
-
},
|
|
172
|
-
|
|
173
|
-
search(query) {
|
|
174
|
-
const candidates = allEntries(query.tier);
|
|
175
|
-
let filtered = candidates;
|
|
176
|
-
|
|
177
|
-
if (query.tags && query.tags.length > 0) {
|
|
178
|
-
const tagSet = new Set(query.tags);
|
|
179
|
-
filtered = filtered.filter((e) => e.tags.some((t) => tagSet.has(t)));
|
|
180
|
-
}
|
|
181
|
-
if (query.projectId) {
|
|
182
|
-
filtered = filtered.filter((e) => e.projectId === query.projectId);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const queryTokens = tokenize(query.query);
|
|
186
|
-
if (queryTokens.length === 0) {
|
|
187
|
-
const limit = query.limit ?? 10;
|
|
188
|
-
return filtered
|
|
189
|
-
.sort((a, b) => new Date(b.accessedAt).getTime() - new Date(a.accessedAt).getTime())
|
|
190
|
-
.slice(0, limit);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Build vocabulary from all candidates + query
|
|
194
|
-
const vocab = new Map<string, number>();
|
|
195
|
-
let idx = 0;
|
|
196
|
-
for (const t of queryTokens) {
|
|
197
|
-
if (!vocab.has(t)) vocab.set(t, idx++);
|
|
198
|
-
}
|
|
199
|
-
for (const entry of filtered) {
|
|
200
|
-
for (const t of tokenize(entry.content)) {
|
|
201
|
-
if (!vocab.has(t)) vocab.set(t, idx++);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Compute IDF
|
|
206
|
-
const docCount = filtered.length + 1;
|
|
207
|
-
const df = new Map<string, number>();
|
|
208
|
-
for (const entry of filtered) {
|
|
209
|
-
const unique = new Set(tokenize(entry.content));
|
|
210
|
-
for (const t of unique) {
|
|
211
|
-
df.set(t, (df.get(t) ?? 0) + 1);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
const queryUnique = new Set(queryTokens);
|
|
215
|
-
for (const t of queryUnique) {
|
|
216
|
-
df.set(t, (df.get(t) ?? 0) + 1);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const idf = new Map<string, number>();
|
|
220
|
-
for (const [term, count] of df) {
|
|
221
|
-
idf.set(term, Math.log(docCount / count));
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Build reverse index for O(1) lookup instead of O(vocab_size) per term
|
|
225
|
-
const indexToTerm = new Map<number, string>();
|
|
226
|
-
for (const [term, idx] of vocab) {
|
|
227
|
-
indexToTerm.set(idx, term);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// TF-IDF vectors
|
|
231
|
-
function tfidfVector(tokens: string[]): number[] {
|
|
232
|
-
const bow = buildBowVector(tokens, vocab);
|
|
233
|
-
for (let i = 0; i < bow.length; i++) {
|
|
234
|
-
if (bow[i] > 0) {
|
|
235
|
-
const term = indexToTerm.get(i);
|
|
236
|
-
if (term) {
|
|
237
|
-
bow[i] *= idf.get(term) ?? 1;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return bow;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const queryVec = tfidfVector(queryTokens);
|
|
245
|
-
|
|
246
|
-
const scored = filtered.map((entry) => {
|
|
247
|
-
const entryTokens = tokenize(entry.content);
|
|
248
|
-
const entryVec = tfidfVector(entryTokens);
|
|
249
|
-
const similarity = cosineSimilarity(queryVec, entryVec);
|
|
250
|
-
// Boost by access count and recency
|
|
251
|
-
const recencyBoost = 1 / (1 + daysSince(entry.accessedAt) * 0.01);
|
|
252
|
-
const accessBoost = 1 + Math.log1p(entry.accessCount) * 0.1;
|
|
253
|
-
const score = similarity * recencyBoost * accessBoost;
|
|
254
|
-
return { entry, score };
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
scored.sort((a, b) => b.score - a.score);
|
|
258
|
-
|
|
259
|
-
const limit = query.limit ?? 10;
|
|
260
|
-
return scored
|
|
261
|
-
.filter((s) => s.score > 0)
|
|
262
|
-
.slice(0, limit)
|
|
263
|
-
.map((s) => {
|
|
264
|
-
s.entry.relevanceScore = s.score;
|
|
265
|
-
return s.entry;
|
|
266
|
-
});
|
|
267
|
-
},
|
|
268
|
-
|
|
269
|
-
promote(id) {
|
|
270
|
-
const entry = findEntry(id);
|
|
271
|
-
if (!entry) return undefined;
|
|
272
|
-
const currentOrder = TIER_ORDER[entry.tier];
|
|
273
|
-
if (currentOrder === 0) return entry; // already at working
|
|
274
|
-
|
|
275
|
-
const newTier = TIERS[currentOrder - 1];
|
|
276
|
-
deleteEntry(entry.tier, entry.id);
|
|
277
|
-
entry.tier = newTier;
|
|
278
|
-
entry.accessedAt = new Date().toISOString();
|
|
279
|
-
writeEntry(entry);
|
|
280
|
-
return entry;
|
|
281
|
-
},
|
|
282
|
-
|
|
283
|
-
demote(id) {
|
|
284
|
-
const entry = findEntry(id);
|
|
285
|
-
if (!entry) return undefined;
|
|
286
|
-
const currentOrder = TIER_ORDER[entry.tier];
|
|
287
|
-
if (currentOrder === TIERS.length - 1) return entry; // already at archive
|
|
288
|
-
|
|
289
|
-
const newTier = TIERS[currentOrder + 1];
|
|
290
|
-
deleteEntry(entry.tier, entry.id);
|
|
291
|
-
|
|
292
|
-
// Compress when demoting to long_term or archive
|
|
293
|
-
let updated = entry;
|
|
294
|
-
if (newTier === "long_term" || newTier === "archive") {
|
|
295
|
-
updated = compressEntry(entry);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
updated.tier = newTier;
|
|
299
|
-
writeEntry(updated);
|
|
300
|
-
return updated;
|
|
301
|
-
},
|
|
302
|
-
|
|
303
|
-
compress(id) {
|
|
304
|
-
const entry = findEntry(id);
|
|
305
|
-
if (!entry) return undefined;
|
|
306
|
-
const compressed = compressEntry(entry);
|
|
307
|
-
writeEntry(compressed);
|
|
308
|
-
return compressed;
|
|
309
|
-
},
|
|
310
|
-
|
|
311
|
-
prune() {
|
|
312
|
-
const archiveEntries = allEntries("archive");
|
|
313
|
-
let pruned = 0;
|
|
314
|
-
for (const entry of archiveEntries) {
|
|
315
|
-
if (daysSince(entry.accessedAt) > ARCHIVE_MAX_AGE_DAYS) {
|
|
316
|
-
deleteEntry("archive", entry.id);
|
|
317
|
-
pruned++;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Auto-demote stale entries in other tiers
|
|
322
|
-
for (const tier of TIERS) {
|
|
323
|
-
if (tier === "archive") continue;
|
|
324
|
-
const maxDays = DEMOTE_DAYS[tier];
|
|
325
|
-
if (maxDays === Infinity) continue;
|
|
326
|
-
const entries = allEntries(tier);
|
|
327
|
-
for (const entry of entries) {
|
|
328
|
-
if (daysSince(entry.accessedAt) > maxDays) {
|
|
329
|
-
const currentOrder = TIER_ORDER[tier];
|
|
330
|
-
const newTier = TIERS[currentOrder + 1];
|
|
331
|
-
deleteEntry(tier, entry.id);
|
|
332
|
-
let updated = entry;
|
|
333
|
-
if (newTier === "long_term" || newTier === "archive") {
|
|
334
|
-
updated = compressEntry(entry);
|
|
335
|
-
}
|
|
336
|
-
updated.tier = newTier;
|
|
337
|
-
writeEntry(updated);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return pruned;
|
|
343
|
-
},
|
|
344
|
-
|
|
345
|
-
getStats() {
|
|
346
|
-
const byTier: Record<MemoryTier, number> = { working: 0, short_term: 0, long_term: 0, archive: 0 };
|
|
347
|
-
let totalEntries = 0;
|
|
348
|
-
let totalSizeBytes = 0;
|
|
349
|
-
let originalSize = 0;
|
|
350
|
-
|
|
351
|
-
for (const tier of TIERS) {
|
|
352
|
-
const entries = allEntries(tier);
|
|
353
|
-
byTier[tier] = entries.length;
|
|
354
|
-
totalEntries += entries.length;
|
|
355
|
-
for (const entry of entries) {
|
|
356
|
-
const size = estimateSizeBytes(entry);
|
|
357
|
-
totalSizeBytes += size;
|
|
358
|
-
// Estimate original size: if compressed, assume 3x expansion
|
|
359
|
-
originalSize += entry.compressed ? size * 3 : size;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return {
|
|
364
|
-
totalEntries,
|
|
365
|
-
byTier,
|
|
366
|
-
totalSizeBytes,
|
|
367
|
-
compressionRatio: originalSize === 0 ? 1 : totalSizeBytes / originalSize,
|
|
368
|
-
};
|
|
369
|
-
},
|
|
370
|
-
};
|
|
371
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export type MemoryTier = "working" | "short_term" | "long_term" | "archive";
|
|
2
|
-
|
|
3
|
-
export type MemoryEntry = {
|
|
4
|
-
id: string;
|
|
5
|
-
content: string;
|
|
6
|
-
summary?: string;
|
|
7
|
-
embedding?: number[];
|
|
8
|
-
tier: MemoryTier;
|
|
9
|
-
tags: string[];
|
|
10
|
-
projectId?: string;
|
|
11
|
-
filePath?: string;
|
|
12
|
-
createdAt: string;
|
|
13
|
-
accessedAt: string;
|
|
14
|
-
accessCount: number;
|
|
15
|
-
relevanceScore: number;
|
|
16
|
-
compressed: boolean;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export type MemoryQuery = {
|
|
20
|
-
query: string;
|
|
21
|
-
tags?: string[];
|
|
22
|
-
projectId?: string;
|
|
23
|
-
tier?: MemoryTier;
|
|
24
|
-
limit?: number;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export type MemoryStats = {
|
|
28
|
-
totalEntries: number;
|
|
29
|
-
byTier: Record<MemoryTier, number>;
|
|
30
|
-
totalSizeBytes: number;
|
|
31
|
-
compressionRatio: number;
|
|
32
|
-
};
|