@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
package/src/cli/index.ts
CHANGED
|
@@ -8,31 +8,15 @@ import { discoverAllSessions } from "../parser/unified.js";
|
|
|
8
8
|
import { parseSession } from "../parser/parse.js";
|
|
9
9
|
import type { ParsedSession } from "../parser/types.js";
|
|
10
10
|
import { exportSession } from "../obsidian/exporter.js";
|
|
11
|
-
import { extractSkills } from "../skills/extractor.js";
|
|
12
11
|
import { reviewCode } from "../review/analyzer.js";
|
|
13
12
|
import { mapCodebase } from "../codebase/mapper.js";
|
|
14
13
|
import { generateOnboardingGuide } from "../codebase/onboard.js";
|
|
15
14
|
import { checkTestHealth } from "../testing/health-check.js";
|
|
16
15
|
import { suggestFixes } from "../testing/test-fixer.js";
|
|
17
16
|
import { validateConfig } from "../config/validator.js";
|
|
18
|
-
import {
|
|
19
|
-
import { createMemoryStore } from "../memory-engine/store.js";
|
|
17
|
+
import { createNexusMemory } from "../memory-engine/nexus-memory.js";
|
|
20
18
|
import { scan as scanPrompt } from "../promptguard/scanner.js";
|
|
21
|
-
import {
|
|
22
|
-
addSkills,
|
|
23
|
-
exportToObsidian,
|
|
24
|
-
loadSkillLibrary,
|
|
25
|
-
saveSkillLibrary,
|
|
26
|
-
searchSkills,
|
|
27
|
-
} from "../skills/library.js";
|
|
28
|
-
import type { SkillLibrary } from "../skills/types.js";
|
|
29
|
-
import { extractWisdom, exportWisdomToObsidian } from "../skills/wisdom-extractor.js";
|
|
30
|
-
import {
|
|
31
|
-
reconcileWisdom,
|
|
32
|
-
loadRefinedLibrary,
|
|
33
|
-
saveRefinedLibrary,
|
|
34
|
-
exportRefinedSkills,
|
|
35
|
-
} from "../skills/skill-reconciler.js";
|
|
19
|
+
import { extractMemorySkills, renderKnowledgeBase } from "../skills/memory-skill-engine.js";
|
|
36
20
|
import { readdirSync, rmSync, statSync } from "node:fs";
|
|
37
21
|
|
|
38
22
|
// ── ANSI Colors ──────────────────────────────────────────────────
|
|
@@ -88,7 +72,7 @@ function resolveConfig(flags: Record<string, string | undefined>): VaultConfig {
|
|
|
88
72
|
process.env["NEXUS_VAULT_PATH"] ??
|
|
89
73
|
join(homedir(), "ObsidianVault", "Claude");
|
|
90
74
|
|
|
91
|
-
const dataDir = join(
|
|
75
|
+
const dataDir = process.env["NEXUS_DATA"] ?? join(homedir(), ".nexus");
|
|
92
76
|
return { vaultPath, dataDir };
|
|
93
77
|
}
|
|
94
78
|
|
|
@@ -252,9 +236,6 @@ function cmdSync(flags: Record<string, string | undefined>): void {
|
|
|
252
236
|
|
|
253
237
|
const parsedSessions: ParsedSession[] = [];
|
|
254
238
|
const exportedFiles: string[] = [];
|
|
255
|
-
let skillsExtracted = 0;
|
|
256
|
-
|
|
257
|
-
const library = loadSkillLibrary(config.dataDir);
|
|
258
239
|
|
|
259
240
|
for (let i = 0; i < allSessionPaths.length; i++) {
|
|
260
241
|
const { path: sessionPath } = allSessionPaths[i];
|
|
@@ -264,24 +245,14 @@ function cmdSync(flags: Record<string, string | undefined>): void {
|
|
|
264
245
|
const session = parseSession(sessionPath);
|
|
265
246
|
parsedSessions.push(session);
|
|
266
247
|
|
|
267
|
-
// 3. Export to Obsidian
|
|
268
248
|
const filePath = exportSessionToObsidian(session, config.vaultPath);
|
|
269
249
|
exportedFiles.push(filePath);
|
|
270
|
-
|
|
271
|
-
// 4. Extract skills
|
|
272
|
-
const skills = extractSkills(session);
|
|
273
|
-
const added = addSkills(library, skills);
|
|
274
|
-
skillsExtracted += added;
|
|
275
250
|
} catch (err) {
|
|
276
251
|
const msg = err instanceof Error ? err.message : String(err);
|
|
277
252
|
log(`\n${c.yellow}Warning:${c.reset} Failed to parse ${sessionPath}: ${msg}`);
|
|
278
253
|
}
|
|
279
254
|
}
|
|
280
255
|
|
|
281
|
-
// 5. Save skill library & export to Obsidian
|
|
282
|
-
saveSkillLibrary(library, config.dataDir);
|
|
283
|
-
const skillFiles = exportToObsidian(library, config.vaultPath);
|
|
284
|
-
|
|
285
256
|
// 6. Update MOC and daily notes
|
|
286
257
|
logInfo("Updating MOC and daily notes...");
|
|
287
258
|
updateMOC(parsedSessions, config.vaultPath);
|
|
@@ -291,19 +262,16 @@ function cmdSync(flags: Record<string, string | undefined>): void {
|
|
|
291
262
|
const status: SyncStatus = {
|
|
292
263
|
lastSync: new Date().toISOString(),
|
|
293
264
|
totalSessions: parsedSessions.length,
|
|
294
|
-
totalSkills:
|
|
265
|
+
totalSkills: 0,
|
|
295
266
|
sessionsExported: parsedSessions.map((s) => s.sessionId),
|
|
296
267
|
};
|
|
297
268
|
saveStatus(status, config.dataDir);
|
|
298
269
|
|
|
299
|
-
// Summary
|
|
300
270
|
log("");
|
|
301
271
|
log(`${c.bold}${c.green}Sync complete!${c.reset}`);
|
|
302
272
|
log(` ${c.cyan}Sessions exported:${c.reset} ${exportedFiles.length}`);
|
|
303
|
-
log(` ${c.cyan}New skills found:${c.reset} ${skillsExtracted}`);
|
|
304
|
-
log(` ${c.cyan}Total skills:${c.reset} ${library.skills.length}`);
|
|
305
|
-
log(` ${c.cyan}Skill files:${c.reset} ${skillFiles.length}`);
|
|
306
273
|
log(` ${c.cyan}Vault path:${c.reset} ${config.vaultPath}`);
|
|
274
|
+
log(` ${c.dim}Run \`nexus reorganize\` to extract knowledge.${c.reset}`);
|
|
307
275
|
}
|
|
308
276
|
|
|
309
277
|
function cmdSessions(): void {
|
|
@@ -369,34 +337,41 @@ function cmdExport(sessionId: string | undefined, flags: Record<string, string |
|
|
|
369
337
|
const filePath = exportSessionToObsidian(session, config.vaultPath);
|
|
370
338
|
logSuccess(`Exported to ${filePath}`);
|
|
371
339
|
|
|
372
|
-
// Extract skills too
|
|
373
|
-
const library = loadSkillLibrary(config.dataDir);
|
|
374
|
-
const skills = extractSkills(session);
|
|
375
|
-
const added = addSkills(library, skills);
|
|
376
|
-
if (added > 0) {
|
|
377
|
-
saveSkillLibrary(library, config.dataDir);
|
|
378
|
-
logSuccess(`Extracted ${added} new skill(s)`);
|
|
379
|
-
}
|
|
380
340
|
}
|
|
381
341
|
|
|
382
342
|
function cmdSkills(flags: Record<string, string | undefined>): void {
|
|
383
343
|
const config = resolveConfig(flags);
|
|
384
|
-
const
|
|
344
|
+
const kbPath = join(config.dataDir, "knowledge.json");
|
|
385
345
|
|
|
386
|
-
if (
|
|
387
|
-
logInfo("No
|
|
346
|
+
if (!existsSync(kbPath)) {
|
|
347
|
+
logInfo("No knowledge extracted yet. Run `nexus reorganize` first.");
|
|
388
348
|
return;
|
|
389
349
|
}
|
|
390
350
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
351
|
+
const kb = JSON.parse(readFileSync(kbPath, "utf-8"));
|
|
352
|
+
const skills = kb.skills ?? [];
|
|
353
|
+
const tips = kb.tips ?? [];
|
|
354
|
+
const facts = kb.facts ?? [];
|
|
355
|
+
|
|
356
|
+
log(`\n${c.bold}Knowledge Base${c.reset} (${skills.length} skills | ${tips.length} tips | ${facts.length} facts)\n`);
|
|
394
357
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
358
|
+
if (skills.length > 0) {
|
|
359
|
+
log(`${c.bold}Skills:${c.reset}`);
|
|
360
|
+
for (const s of skills) {
|
|
361
|
+
log(` ${c.cyan}${s.name.slice(0, 50)}${c.reset} — ${(s.confidence * 100).toFixed(0)}% | ${s.evidenceCount} evidence`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (tips.length > 0) {
|
|
365
|
+
log(`\n${c.bold}Tips:${c.reset}`);
|
|
366
|
+
for (const t of tips) {
|
|
367
|
+
log(` 💡 ${t.advice?.slice(0, 60) ?? t.name}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (facts.length > 0) {
|
|
371
|
+
log(`\n${c.bold}Facts:${c.reset}`);
|
|
372
|
+
for (const f of facts) {
|
|
373
|
+
log(` 📌 ${f.statement?.slice(0, 60) ?? f.name}`);
|
|
374
|
+
}
|
|
400
375
|
}
|
|
401
376
|
log("");
|
|
402
377
|
}
|
|
@@ -407,37 +382,18 @@ function cmdSkillsSearch(query: string | undefined, flags: Record<string, string
|
|
|
407
382
|
process.exit(1);
|
|
408
383
|
}
|
|
409
384
|
|
|
410
|
-
|
|
411
|
-
const library = loadSkillLibrary(config.dataDir);
|
|
412
|
-
const results = searchSkills(library, query);
|
|
413
|
-
|
|
414
|
-
if (results.length === 0) {
|
|
415
|
-
logInfo(`No skills matching "${query}".`);
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
log(`\n${c.bold}Skills matching "${query}"${c.reset} (${results.length} results)\n`);
|
|
420
|
-
|
|
421
|
-
for (const skill of results) {
|
|
422
|
-
log(`${c.cyan}${c.bold}${skill.name}${c.reset} ${c.dim}(${(skill.confidence * 100).toFixed(0)}% confidence)${c.reset}`);
|
|
423
|
-
log(` ${c.dim}Trigger:${c.reset} ${skill.trigger}`);
|
|
424
|
-
log(` ${c.dim}Tools:${c.reset} ${skill.toolsUsed.join(", ")}`);
|
|
425
|
-
log(` ${c.dim}Steps:${c.reset} ${skill.steps.length}`);
|
|
426
|
-
log("");
|
|
427
|
-
}
|
|
385
|
+
logInfo(`Search not available yet. Use \`nexus skills\` to view all knowledge.`);
|
|
428
386
|
}
|
|
429
387
|
|
|
430
388
|
function cmdStatus(flags: Record<string, string | undefined>): void {
|
|
431
389
|
const config = resolveConfig(flags);
|
|
432
390
|
const status = loadStatus(config.dataDir);
|
|
433
|
-
const library = loadSkillLibrary(config.dataDir);
|
|
434
391
|
|
|
435
|
-
log(`\n${c.bold}
|
|
392
|
+
log(`\n${c.bold}Nexus Status${c.reset}\n`);
|
|
436
393
|
log(` ${c.cyan}Vault path:${c.reset} ${config.vaultPath}`);
|
|
437
394
|
log(` ${c.cyan}Last sync:${c.reset} ${status.lastSync || "never"}`);
|
|
438
|
-
log(` ${c.cyan}
|
|
439
|
-
log(` ${c.cyan}
|
|
440
|
-
log(` ${c.cyan}Library ver:${c.reset} ${library.version}`);
|
|
395
|
+
log(` ${c.cyan}Sessions:${c.reset} ${status.sessionsExported ?? 0}`);
|
|
396
|
+
log(` ${c.cyan}Knowledge:${c.reset} ${(status as Record<string, unknown>).totalKnowledge ?? "run reorganize"}`);
|
|
441
397
|
log("");
|
|
442
398
|
}
|
|
443
399
|
|
|
@@ -506,47 +462,42 @@ function cmdReorganize(flags: Record<string, string | undefined>): void {
|
|
|
506
462
|
process.stdout.write("\n");
|
|
507
463
|
logInfo(`Exported ${exportCount} sessions`);
|
|
508
464
|
|
|
509
|
-
// Step 3:
|
|
510
|
-
logInfo("Extracting
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
logInfo(`Quality gate: ${result.rejected.length} rejected, ${result.created.length + result.updated.length} passed`);
|
|
529
|
-
logInfo(`Refined skills: ${library.skills.length}`);
|
|
530
|
-
logInfo(`Preferences learned: ${result.preferencesLearned.length}`);
|
|
531
|
-
|
|
532
|
-
// Step 5: Export refined skills to Obsidian
|
|
533
|
-
const skillFiles = exportRefinedSkills(library, config.vaultPath);
|
|
534
|
-
logInfo(`Exported ${skillFiles.length} refined skill files`);
|
|
465
|
+
// Step 3: Memory-based knowledge extraction (Skills + Tips + Facts)
|
|
466
|
+
logInfo("Extracting knowledge from observations...");
|
|
467
|
+
const knowledgeResult = extractMemorySkills(parsedSessions, config.dataDir, 2);
|
|
468
|
+
|
|
469
|
+
logInfo(`Observations: ${knowledgeResult.observationsIngested} | Clusters: ${knowledgeResult.clustersFormed}`);
|
|
470
|
+
logInfo(`Skills: ${knowledgeResult.skills.length} | Tips: ${knowledgeResult.tips.length} | Facts: ${knowledgeResult.facts.length}`);
|
|
471
|
+
|
|
472
|
+
// Step 4: Export knowledge base to Obsidian
|
|
473
|
+
const kbPath = join(config.vaultPath, "Knowledge Base.md");
|
|
474
|
+
writeFileSync(kbPath, renderKnowledgeBase(knowledgeResult), "utf-8");
|
|
475
|
+
// Also save as JSON for MCP tool access
|
|
476
|
+
const knowledgeJson = {
|
|
477
|
+
skills: knowledgeResult.skills,
|
|
478
|
+
tips: knowledgeResult.tips,
|
|
479
|
+
facts: knowledgeResult.facts,
|
|
480
|
+
};
|
|
481
|
+
writeFileSync(join(config.dataDir, "knowledge.json"), JSON.stringify(knowledgeJson, null, 2), "utf-8");
|
|
482
|
+
logInfo("Exported Knowledge Base to Obsidian + MCP cache");
|
|
535
483
|
|
|
536
|
-
// Step
|
|
484
|
+
// Step 5: Update MOC and daily notes
|
|
537
485
|
logInfo("Updating MOC and daily notes...");
|
|
538
486
|
updateMOC(parsedSessions, config.vaultPath);
|
|
539
487
|
updateDailyNotes(parsedSessions, config.vaultPath);
|
|
540
488
|
|
|
541
|
-
// Step
|
|
489
|
+
// Step 6: Save status
|
|
490
|
+
const totalKnowledge = knowledgeResult.skills.length + knowledgeResult.tips.length + knowledgeResult.facts.length;
|
|
542
491
|
const status = {
|
|
543
492
|
lastSync: new Date().toISOString(),
|
|
544
493
|
lastReorg: new Date().toISOString(),
|
|
545
494
|
sessionsExported: exportCount,
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
495
|
+
skills: knowledgeResult.skills.length,
|
|
496
|
+
tips: knowledgeResult.tips.length,
|
|
497
|
+
facts: knowledgeResult.facts.length,
|
|
498
|
+
totalKnowledge,
|
|
499
|
+
observations: knowledgeResult.observationsIngested,
|
|
500
|
+
durationMs: knowledgeResult.durationMs,
|
|
550
501
|
};
|
|
551
502
|
mkdirSync(config.dataDir, { recursive: true });
|
|
552
503
|
writeFileSync(join(config.dataDir, "status.json"), JSON.stringify(status, null, 2), "utf-8");
|
|
@@ -555,11 +506,11 @@ function cmdReorganize(flags: Record<string, string | undefined>): void {
|
|
|
555
506
|
log("");
|
|
556
507
|
logSuccess("Reorganization complete!");
|
|
557
508
|
log(` ${c.cyan}Sessions:${c.reset} ${exportCount}`);
|
|
558
|
-
log(` ${c.cyan}
|
|
559
|
-
log(` ${c.cyan}
|
|
560
|
-
log(` ${c.cyan}
|
|
561
|
-
log(` ${c.cyan}
|
|
562
|
-
log(` ${c.cyan}
|
|
509
|
+
log(` ${c.cyan}Observations:${c.reset} ${knowledgeResult.observationsIngested}`);
|
|
510
|
+
log(` ${c.cyan}Skills:${c.reset} ${knowledgeResult.skills.length}`);
|
|
511
|
+
log(` ${c.cyan}Tips:${c.reset} ${knowledgeResult.tips.length}`);
|
|
512
|
+
log(` ${c.cyan}Facts:${c.reset} ${knowledgeResult.facts.length}`);
|
|
513
|
+
log(` ${c.cyan}Total knowledge:${c.reset} ${totalKnowledge}`);
|
|
563
514
|
log(` ${c.cyan}Vault:${c.reset} ${config.vaultPath}`);
|
|
564
515
|
}
|
|
565
516
|
|
|
@@ -746,51 +697,10 @@ async function cmdConfig(dir: string | undefined, flags: Record<string, string |
|
|
|
746
697
|
log("");
|
|
747
698
|
}
|
|
748
699
|
|
|
749
|
-
function cmdCost(flags: Record<string, string | undefined>): void {
|
|
750
|
-
const config = resolveConfig(flags);
|
|
751
|
-
const tracker = createCostTracker(config.dataDir);
|
|
752
|
-
const report = tracker.getReport();
|
|
753
|
-
const projected = tracker.getProjectedCost();
|
|
754
|
-
|
|
755
|
-
if ("--json" in flags) {
|
|
756
|
-
log(JSON.stringify({ ...report, projectedMonthlyCost: projected }, null, 2));
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
log(`\n${c.bold}AI Cost Report${c.reset}\n`);
|
|
761
|
-
log(` ${c.cyan}Total cost:${c.reset} $${report.totalCost.toFixed(4)}`);
|
|
762
|
-
log(` ${c.cyan}Total requests:${c.reset} ${report.totalRequests}`);
|
|
763
|
-
log(` ${c.cyan}Avg per request:${c.reset} $${report.averageCostPerRequest.toFixed(4)}`);
|
|
764
|
-
log(` ${c.cyan}Projected/month:${c.reset} $${report.projectedMonthlyCost.toFixed(2)}`);
|
|
765
|
-
|
|
766
|
-
const providers = Object.entries(report.byProvider);
|
|
767
|
-
if (providers.length > 0) {
|
|
768
|
-
log(`\n${c.bold}By Provider:${c.reset}`);
|
|
769
|
-
for (const [provider, cost] of providers) {
|
|
770
|
-
log(` ${c.yellow}${provider}${c.reset}: $${cost.toFixed(4)}`);
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
const models = Object.entries(report.byModel);
|
|
775
|
-
if (models.length > 0) {
|
|
776
|
-
log(`\n${c.bold}By Model:${c.reset}`);
|
|
777
|
-
for (const [model, cost] of models) {
|
|
778
|
-
log(` ${c.yellow}${model}${c.reset}: $${cost.toFixed(4)}`);
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
if (report.alerts.length > 0) {
|
|
783
|
-
log(`\n${c.bold}Alerts:${c.reset}`);
|
|
784
|
-
for (const alert of report.alerts) {
|
|
785
|
-
log(` ${c.red}[${alert.type}]${c.reset} ${alert.message}`);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
log("");
|
|
789
|
-
}
|
|
790
700
|
|
|
791
701
|
function cmdMemory(subcommand: string | undefined, query: string | undefined, flags: Record<string, string | undefined>): void {
|
|
792
702
|
const config = resolveConfig(flags);
|
|
793
|
-
const store =
|
|
703
|
+
const store = createNexusMemory(config.dataDir);
|
|
794
704
|
|
|
795
705
|
if (subcommand === "search") {
|
|
796
706
|
if (!query) {
|
|
@@ -798,7 +708,7 @@ function cmdMemory(subcommand: string | undefined, query: string | undefined, fl
|
|
|
798
708
|
process.exit(1);
|
|
799
709
|
}
|
|
800
710
|
|
|
801
|
-
const results = store.search(
|
|
711
|
+
const results = store.search(query);
|
|
802
712
|
|
|
803
713
|
if ("--json" in flags) {
|
|
804
714
|
log(JSON.stringify(results, null, 2));
|
|
@@ -811,10 +721,9 @@ function cmdMemory(subcommand: string | undefined, query: string | undefined, fl
|
|
|
811
721
|
}
|
|
812
722
|
|
|
813
723
|
log(`\n${c.bold}Memory Search: "${query}"${c.reset} (${results.length} results)\n`);
|
|
814
|
-
for (const
|
|
815
|
-
log(` ${c.cyan}${
|
|
816
|
-
log(` ${
|
|
817
|
-
log(` ${entry.content.slice(0, 120)}${entry.content.length > 120 ? "..." : ""}`);
|
|
724
|
+
for (const r of results) {
|
|
725
|
+
log(` ${c.cyan}[${r.retrievalLevel}]${c.reset} score=${r.score.toFixed(2)}`);
|
|
726
|
+
log(` ${r.observation.content.slice(0, 120)}`);
|
|
818
727
|
log("");
|
|
819
728
|
}
|
|
820
729
|
} else if (subcommand === "stats") {
|
|
@@ -826,13 +735,12 @@ function cmdMemory(subcommand: string | undefined, query: string | undefined, fl
|
|
|
826
735
|
}
|
|
827
736
|
|
|
828
737
|
log(`\n${c.bold}Memory Stats${c.reset}\n`);
|
|
829
|
-
log(` ${c.cyan}
|
|
830
|
-
log(` ${c.cyan}
|
|
831
|
-
log(` ${c.cyan}
|
|
832
|
-
log(
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
}
|
|
738
|
+
log(` ${c.cyan}Observations:${c.reset} ${stats.totalObservations}`);
|
|
739
|
+
log(` ${c.cyan}Valid:${c.reset} ${stats.validObservations}`);
|
|
740
|
+
log(` ${c.cyan}Graph nodes:${c.reset} ${stats.graphNodes}`);
|
|
741
|
+
log(` ${c.cyan}Graph edges:${c.reset} ${stats.graphEdges}`);
|
|
742
|
+
log(` ${c.cyan}Tunnels:${c.reset} ${stats.tunnels}`);
|
|
743
|
+
log(` ${c.cyan}Domains:${c.reset} ${stats.domains.join(", ")}`);
|
|
836
744
|
log("");
|
|
837
745
|
} else {
|
|
838
746
|
logError("Usage: nexus memory <search|stats> [query]");
|
|
@@ -890,7 +798,6 @@ ${c.bold}Commands:${c.reset}
|
|
|
890
798
|
${c.cyan}onboard${c.reset} [dir] Generate onboarding guide
|
|
891
799
|
${c.cyan}test-health${c.reset} [dir] Check test suite health
|
|
892
800
|
${c.cyan}config${c.reset} [dir] Validate config files
|
|
893
|
-
${c.cyan}cost${c.reset} Show AI cost report
|
|
894
801
|
${c.cyan}memory${c.reset} <search|stats> [query] Memory operations
|
|
895
802
|
${c.cyan}scan${c.reset} <text> Scan text for prompt injection
|
|
896
803
|
${c.cyan}--help${c.reset} Show this help
|
|
@@ -994,7 +901,7 @@ async function main(): Promise<void> {
|
|
|
994
901
|
await cmdConfig(args[0], flags);
|
|
995
902
|
break;
|
|
996
903
|
case "cost":
|
|
997
|
-
|
|
904
|
+
logError("Cost tracking has been removed.");
|
|
998
905
|
break;
|
|
999
906
|
case "memory":
|
|
1000
907
|
cmdMemory(args[0], args.slice(1).join(" ") || undefined, flags);
|
package/src/index.ts
CHANGED
|
@@ -10,44 +10,32 @@ export { exportSession } from "./obsidian/exporter.js";
|
|
|
10
10
|
export { updateMOC } from "./obsidian/moc.js";
|
|
11
11
|
export { appendToDailyNote } from "./obsidian/daily-note.js";
|
|
12
12
|
|
|
13
|
-
// Skills —
|
|
14
|
-
export {
|
|
15
|
-
export {
|
|
13
|
+
// Skills — Memory-based Knowledge Extraction
|
|
14
|
+
export { extractMemorySkills, renderKnowledgeBase } from "./skills/memory-skill-engine.js";
|
|
15
|
+
export type { MemorySkill, Tip, Fact, SkillExtractionResult } from "./skills/memory-skill-engine.js";
|
|
16
16
|
|
|
17
|
-
//
|
|
18
|
-
export {
|
|
19
|
-
export {
|
|
20
|
-
export {
|
|
21
|
-
export {
|
|
17
|
+
// Prompt Injection Detection
|
|
18
|
+
export { scan, isInjected, guard, scanBatch, PromptInjectionError } from "./promptguard/scanner.js";
|
|
19
|
+
export { BUILTIN_RULES } from "./promptguard/rules.js";
|
|
20
|
+
export { ADVANCED_RULES } from "./promptguard/advanced-rules.js";
|
|
21
|
+
export { MULTILINGUAL_RULES } from "./promptguard/multilingual-rules.js";
|
|
22
|
+
export { normalizeText } from "./promptguard/normalize.js";
|
|
23
|
+
|
|
24
|
+
// Memory Engine
|
|
25
|
+
export { createNexusMemory } from "./memory-engine/nexus-memory.js";
|
|
26
|
+
export { semanticSimilarity, expandQuery, getSynonyms } from "./memory-engine/semantic.js";
|
|
22
27
|
|
|
23
|
-
// Code Review
|
|
28
|
+
// Code Review
|
|
24
29
|
export { reviewCode } from "./review/analyzer.js";
|
|
25
30
|
export { reviewDiff } from "./review/diff-reviewer.js";
|
|
26
31
|
|
|
27
|
-
// Codebase Mapping
|
|
32
|
+
// Codebase Mapping
|
|
28
33
|
export { mapCodebase } from "./codebase/mapper.js";
|
|
29
34
|
export { generateOnboardingGuide } from "./codebase/onboard.js";
|
|
30
35
|
|
|
31
|
-
// Test Health
|
|
36
|
+
// Test Health
|
|
32
37
|
export { checkTestHealth } from "./testing/health-check.js";
|
|
33
38
|
export { suggestFixes } from "./testing/test-fixer.js";
|
|
34
39
|
|
|
35
|
-
//
|
|
36
|
-
export { createCostTracker } from "./cost/tracker.js";
|
|
37
|
-
|
|
38
|
-
// Config Validator (from superdev)
|
|
40
|
+
// Config Validator
|
|
39
41
|
export { validateConfig } from "./config/validator.js";
|
|
40
|
-
|
|
41
|
-
// Infinite Memory (from superdev)
|
|
42
|
-
export { createMemoryStore } from "./memory-engine/store.js";
|
|
43
|
-
export { createContextWindow } from "./memory-engine/context-window.js";
|
|
44
|
-
|
|
45
|
-
// Prompt Injection Detection (from promptguard)
|
|
46
|
-
export { scan, isInjected, guard, scanBatch, PromptInjectionError } from "./promptguard/scanner.js";
|
|
47
|
-
export { BUILTIN_RULES } from "./promptguard/rules.js";
|
|
48
|
-
export { ADVANCED_RULES } from "./promptguard/advanced-rules.js";
|
|
49
|
-
export { MULTILINGUAL_RULES } from "./promptguard/multilingual-rules.js";
|
|
50
|
-
export { normalizeText } from "./promptguard/normalize.js";
|
|
51
|
-
export { analyzeEntropy } from "./promptguard/entropy.js";
|
|
52
|
-
export { classifyIntent } from "./promptguard/semantic.js";
|
|
53
|
-
export { analyzeTokens } from "./promptguard/token-analysis.js";
|
package/src/mcp/server.ts
CHANGED
|
@@ -17,8 +17,25 @@
|
|
|
17
17
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
18
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
19
|
import { z } from "zod";
|
|
20
|
-
import { readFileSync } from "node:fs";
|
|
21
|
-
import { resolve } from "node:path";
|
|
20
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
21
|
+
import { resolve, relative, join } from "node:path";
|
|
22
|
+
|
|
23
|
+
/** Validate path is within allowed boundaries. */
|
|
24
|
+
function validatePath(filePath: string): string {
|
|
25
|
+
const resolved = resolve(filePath);
|
|
26
|
+
const cwd = process.cwd();
|
|
27
|
+
const home = process.env.HOME ?? "/home";
|
|
28
|
+
// Allow paths within cwd, home, or /tmp
|
|
29
|
+
if (resolved.startsWith(cwd) || resolved.startsWith(home) || resolved.startsWith("/tmp") || resolved.startsWith("/mnt/c/")) {
|
|
30
|
+
return resolved;
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`Path outside allowed boundary: ${filePath}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Limit input size to prevent DoS. */
|
|
36
|
+
function limitInput(text: string, maxLen = 500_000): string {
|
|
37
|
+
return text.length > maxLen ? text.slice(0, maxLen) : text;
|
|
38
|
+
}
|
|
22
39
|
|
|
23
40
|
// Session intelligence
|
|
24
41
|
import { discoverAllSessions } from "../parser/unified.js";
|
|
@@ -38,21 +55,25 @@ import { suggestFixes } from "../testing/test-fixer.js";
|
|
|
38
55
|
// Config
|
|
39
56
|
import { validateConfig } from "../config/validator.js";
|
|
40
57
|
|
|
41
|
-
// Cost
|
|
42
|
-
import { createCostTracker } from "../cost/tracker.js";
|
|
43
58
|
|
|
44
59
|
// Memory
|
|
45
|
-
import {
|
|
60
|
+
import { createNexusMemory } from "../memory-engine/nexus-memory.js";
|
|
46
61
|
|
|
47
62
|
// Prompt injection
|
|
48
63
|
import { scan, isInjected } from "../promptguard/scanner.js";
|
|
49
64
|
import type { InjectionContext, Severity } from "../promptguard/types.js";
|
|
50
65
|
|
|
51
|
-
//
|
|
52
|
-
import { loadRefinedLibrary } from "../skills/skill-reconciler.js";
|
|
66
|
+
// Knowledge — uses cached data from last `nexus reorganize`
|
|
53
67
|
|
|
54
68
|
const server = new McpServer({ name: "nexus", version: "0.1.0" });
|
|
55
|
-
const dataDir = resolve(process.env.NEXUS_DATA ?? ".nexus");
|
|
69
|
+
const dataDir = resolve(process.env.NEXUS_DATA ?? join(process.env.HOME ?? "/home", ".nexus"));
|
|
70
|
+
|
|
71
|
+
// Singleton memory store (avoid re-reading from disk on every MCP call)
|
|
72
|
+
let _memoryStore: ReturnType<typeof createNexusMemory> | null = null;
|
|
73
|
+
function getMemoryStore() {
|
|
74
|
+
if (!_memoryStore) _memoryStore = createNexusMemory(dataDir);
|
|
75
|
+
return _memoryStore;
|
|
76
|
+
}
|
|
56
77
|
|
|
57
78
|
// ─── Session Intelligence ────────────────────────────────────────
|
|
58
79
|
|
|
@@ -80,7 +101,8 @@ server.tool(
|
|
|
80
101
|
platform: z.enum(["claude-code", "openclaw"]).optional().describe("Platform (auto-detected if omitted)"),
|
|
81
102
|
},
|
|
82
103
|
async ({ path, platform }) => {
|
|
83
|
-
const
|
|
104
|
+
const safePath = validatePath(path);
|
|
105
|
+
const session = parseAnySession(safePath, platform ?? "claude-code");
|
|
84
106
|
return { content: [{ type: "text" as const, text: JSON.stringify({
|
|
85
107
|
sessionId: session.sessionId,
|
|
86
108
|
platform: session.platform,
|
|
@@ -102,7 +124,7 @@ server.tool(
|
|
|
102
124
|
context: z.enum(["user_input", "tool_result", "mcp_response", "document", "unknown"]).optional(),
|
|
103
125
|
},
|
|
104
126
|
async ({ text, context }) => {
|
|
105
|
-
const result = scan(text, { context: (context as InjectionContext) ?? "unknown" });
|
|
127
|
+
const result = scan(limitInput(text), { context: (context as InjectionContext) ?? "unknown" });
|
|
106
128
|
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
107
129
|
},
|
|
108
130
|
);
|
|
@@ -128,7 +150,8 @@ server.tool(
|
|
|
128
150
|
file_path: z.string().describe("Path to the file to review"),
|
|
129
151
|
},
|
|
130
152
|
async ({ file_path }) => {
|
|
131
|
-
const
|
|
153
|
+
const safePath = validatePath(file_path);
|
|
154
|
+
const code = readFileSync(safePath, "utf-8");
|
|
132
155
|
const result = reviewCode(code, file_path);
|
|
133
156
|
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
134
157
|
},
|
|
@@ -143,7 +166,7 @@ server.tool(
|
|
|
143
166
|
directory: z.string().optional().describe("Root directory (default: current dir)"),
|
|
144
167
|
},
|
|
145
168
|
async ({ directory }) => {
|
|
146
|
-
const map = await mapCodebase({ root:
|
|
169
|
+
const map = await mapCodebase({ root: validatePath(directory ?? ".") });
|
|
147
170
|
return { content: [{ type: "text" as const, text: JSON.stringify({
|
|
148
171
|
totalFiles: map.totalFiles,
|
|
149
172
|
totalLines: map.totalLines,
|
|
@@ -161,7 +184,7 @@ server.tool(
|
|
|
161
184
|
directory: z.string().optional().describe("Root directory (default: current dir)"),
|
|
162
185
|
},
|
|
163
186
|
async ({ directory }) => {
|
|
164
|
-
const map = await mapCodebase({ root:
|
|
187
|
+
const map = await mapCodebase({ root: validatePath(directory ?? ".") });
|
|
165
188
|
const guide = generateOnboardingGuide(map);
|
|
166
189
|
return { content: [{ type: "text" as const, text: guide }] };
|
|
167
190
|
},
|
|
@@ -176,7 +199,7 @@ server.tool(
|
|
|
176
199
|
directory: z.string().optional(),
|
|
177
200
|
},
|
|
178
201
|
async ({ directory }) => {
|
|
179
|
-
const report = await checkTestHealth(
|
|
202
|
+
const report = await checkTestHealth(validatePath(directory ?? "."));
|
|
180
203
|
const fixes = suggestFixes(report.issues);
|
|
181
204
|
return { content: [{ type: "text" as const, text: JSON.stringify({ ...report, suggestedFixes: fixes }, null, 2) }] };
|
|
182
205
|
},
|
|
@@ -191,22 +214,7 @@ server.tool(
|
|
|
191
214
|
directory: z.string().optional(),
|
|
192
215
|
},
|
|
193
216
|
async ({ directory }) => {
|
|
194
|
-
const report = await validateConfig(
|
|
195
|
-
return { content: [{ type: "text" as const, text: JSON.stringify(report, null, 2) }] };
|
|
196
|
-
},
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
// ─── Cost Monitor ────────────────────────────────────────────────
|
|
200
|
-
|
|
201
|
-
server.tool(
|
|
202
|
-
"nexus_cost",
|
|
203
|
-
"Show AI API cost report — total cost, by provider, by model, budget alerts.",
|
|
204
|
-
{
|
|
205
|
-
days: z.number().optional().describe("Days to report (default: 30)"),
|
|
206
|
-
},
|
|
207
|
-
async ({ days }) => {
|
|
208
|
-
const tracker = createCostTracker(dataDir);
|
|
209
|
-
const report = tracker.getReport(days ?? 30);
|
|
217
|
+
const report = await validateConfig(validatePath(directory ?? "."));
|
|
210
218
|
return { content: [{ type: "text" as const, text: JSON.stringify(report, null, 2) }] };
|
|
211
219
|
},
|
|
212
220
|
);
|
|
@@ -221,8 +229,8 @@ server.tool(
|
|
|
221
229
|
limit: z.number().optional().describe("Max results (default: 10)"),
|
|
222
230
|
},
|
|
223
231
|
async ({ query, limit }) => {
|
|
224
|
-
const store =
|
|
225
|
-
const results = store.search(
|
|
232
|
+
const store = getMemoryStore();
|
|
233
|
+
const results = store.search(query, limit ?? 10);
|
|
226
234
|
return { content: [{ type: "text" as const, text: JSON.stringify(results, null, 2) }] };
|
|
227
235
|
},
|
|
228
236
|
);
|
|
@@ -235,21 +243,39 @@ server.tool(
|
|
|
235
243
|
tags: z.array(z.string()).optional().describe("Tags for categorization"),
|
|
236
244
|
},
|
|
237
245
|
async ({ content, tags }) => {
|
|
238
|
-
const store =
|
|
239
|
-
const
|
|
240
|
-
|
|
246
|
+
const store = getMemoryStore();
|
|
247
|
+
const count = store.ingest(content, "manual");
|
|
248
|
+
store.save();
|
|
249
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ saved: true, observations: count }) }] };
|
|
241
250
|
},
|
|
242
251
|
);
|
|
243
252
|
|
|
244
|
-
// ─── Skills
|
|
253
|
+
// ─── Knowledge (Skills + Tips + Facts) ───────────────────────────
|
|
245
254
|
|
|
246
255
|
server.tool(
|
|
247
256
|
"nexus_skills",
|
|
248
|
-
"List all
|
|
249
|
-
{
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
257
|
+
"List all knowledge: skills (complex patterns), tips (quick advice), and facts (reference info).",
|
|
258
|
+
{
|
|
259
|
+
tier: z.enum(["all", "skill", "tip", "fact"]).optional().describe("Filter by tier (default: all)"),
|
|
260
|
+
},
|
|
261
|
+
async ({ tier }) => {
|
|
262
|
+
// Read cached knowledge from last reorganize
|
|
263
|
+
const statusPath = join(dataDir, "status.json");
|
|
264
|
+
const kbPath = join(dataDir, "knowledge.json");
|
|
265
|
+
let knowledge: { skills: unknown[]; tips: unknown[]; facts: unknown[] } = { skills: [], tips: [], facts: [] };
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
if (existsSync(kbPath)) {
|
|
269
|
+
knowledge = JSON.parse(readFileSync(kbPath, "utf-8"));
|
|
270
|
+
}
|
|
271
|
+
} catch { /* empty */ }
|
|
272
|
+
|
|
273
|
+
const filtered = tier === "skill" ? { skills: knowledge.skills }
|
|
274
|
+
: tier === "tip" ? { tips: knowledge.tips }
|
|
275
|
+
: tier === "fact" ? { facts: knowledge.facts }
|
|
276
|
+
: knowledge;
|
|
277
|
+
|
|
278
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(filtered, null, 2) }] };
|
|
253
279
|
},
|
|
254
280
|
);
|
|
255
281
|
|