@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.
Files changed (52) hide show
  1. package/README.md +60 -38
  2. package/dist/cli/index.js +76 -145
  3. package/dist/index.js +15 -26
  4. package/dist/mcp/server.js +61 -32
  5. package/package.json +2 -1
  6. package/scripts/auto-skill.sh +54 -0
  7. package/scripts/auto-sync.sh +11 -0
  8. package/scripts/benchmark.ts +444 -0
  9. package/scripts/scan-tool-result.sh +46 -0
  10. package/src/cli/index.ts +79 -172
  11. package/src/index.ts +17 -29
  12. package/src/mcp/server.ts +67 -41
  13. package/src/memory-engine/index.ts +4 -6
  14. package/src/memory-engine/nexus-memory.test.ts +437 -0
  15. package/src/memory-engine/nexus-memory.ts +631 -0
  16. package/src/memory-engine/semantic.ts +380 -0
  17. package/src/parser/parse.ts +1 -21
  18. package/src/promptguard/advanced-rules.ts +129 -12
  19. package/src/promptguard/entropy.ts +21 -2
  20. package/src/promptguard/evolution/auto-update.ts +16 -6
  21. package/src/promptguard/multilingual-rules.ts +68 -0
  22. package/src/promptguard/rules.ts +87 -2
  23. package/src/promptguard/scanner.test.ts +262 -0
  24. package/src/promptguard/scanner.ts +1 -1
  25. package/src/promptguard/semantic.ts +19 -4
  26. package/src/promptguard/token-analysis.ts +17 -5
  27. package/src/review/analyzer.test.ts +279 -0
  28. package/src/review/analyzer.ts +112 -28
  29. package/src/shared/stop-words.ts +21 -0
  30. package/src/skills/index.ts +11 -27
  31. package/src/skills/memory-skill-engine.ts +1044 -0
  32. package/src/testing/health-check.ts +19 -2
  33. package/src/cost/index.ts +0 -3
  34. package/src/cost/tracker.ts +0 -290
  35. package/src/cost/types.ts +0 -34
  36. package/src/memory-engine/compressor.ts +0 -97
  37. package/src/memory-engine/context-window.ts +0 -113
  38. package/src/memory-engine/store.ts +0 -371
  39. package/src/memory-engine/types.ts +0 -32
  40. package/src/skills/context-engine.ts +0 -863
  41. package/src/skills/extractor.ts +0 -224
  42. package/src/skills/global-context.ts +0 -726
  43. package/src/skills/library.ts +0 -189
  44. package/src/skills/pattern-engine.ts +0 -712
  45. package/src/skills/render-evolved.ts +0 -160
  46. package/src/skills/skill-reconciler.ts +0 -703
  47. package/src/skills/smart-extractor.ts +0 -843
  48. package/src/skills/types.ts +0 -18
  49. package/src/skills/wisdom-extractor.ts +0 -737
  50. package/src/superdev-evolution/index.ts +0 -3
  51. package/src/superdev-evolution/skill-manager.ts +0 -266
  52. 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
- };