@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
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 { createCostTracker } from "../cost/tracker.js";
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(vaultPath, ".nexus");
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: library.skills.length,
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 library = loadSkillLibrary(config.dataDir);
344
+ const kbPath = join(config.dataDir, "knowledge.json");
385
345
 
386
- if (library.skills.length === 0) {
387
- logInfo("No skills extracted yet. Run `nexus sync` first.");
346
+ if (!existsSync(kbPath)) {
347
+ logInfo("No knowledge extracted yet. Run `nexus reorganize` first.");
388
348
  return;
389
349
  }
390
350
 
391
- log(`\n${c.bold}Extracted Skills${c.reset} (${library.skills.length} total)\n`);
392
- log(`${"Name".padEnd(45)} ${"Confidence".padEnd(12)} Tools`);
393
- log(`${"-".repeat(45)} ${"-".repeat(12)} ${"-".repeat(30)}`);
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
- for (const skill of library.skills) {
396
- const name = skill.name.slice(0, 43).padEnd(45);
397
- const conf = `${(skill.confidence * 100).toFixed(0)}%`.padEnd(12);
398
- const tools = skill.toolsUsed.join(", ");
399
- log(`${c.cyan}${name}${c.reset} ${c.yellow}${conf}${c.reset} ${tools}`);
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
- const config = resolveConfig(flags);
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}Claude Vault Status${c.reset}\n`);
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}Total sessions:${c.reset} ${status.totalSessions}`);
439
- log(` ${c.cyan}Total skills:${c.reset} ${library.skills.length}`);
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: Extract wisdom from all sessions
510
- logInfo("Extracting wisdom...");
511
- const allWisdoms = [];
512
- for (const session of parsedSessions) {
513
- const wisdoms = extractWisdom(session.messages, session.sessionId);
514
- allWisdoms.push(...wisdoms);
515
- }
516
- logInfo(`Extracted ${allWisdoms.length} raw wisdoms`);
517
-
518
- // Step 4: Reconcile into refined skills
519
- logInfo("Reconciling into refined skills...");
520
- const library = loadRefinedLibrary(config.dataDir);
521
- // Clear old skills for full reorg
522
- library.skills = [];
523
- library.version = 1;
524
-
525
- const result = reconcileWisdom(allWisdoms, library);
526
- saveRefinedLibrary(library, config.dataDir);
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 6: Update MOC and daily notes
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 7: Save status
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
- refinedSkills: library.skills.length,
547
- wisdomsExtracted: allWisdoms.length,
548
- wisdomsRejected: result.rejected.length,
549
- preferencesLearned: result.preferencesLearned.length,
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}Raw wisdoms:${c.reset} ${allWisdoms.length}`);
559
- log(` ${c.cyan}Rejected (noise):${c.reset} ${result.rejected.length}`);
560
- log(` ${c.cyan}Refined skills:${c.reset} ${library.skills.length}`);
561
- log(` ${c.cyan}Preferences:${c.reset} ${result.preferencesLearned.length}`);
562
- log(` ${c.cyan}Old folders cleaned:${c.reset} ${foldersToClean.filter((f) => existsSync(join(config.vaultPath, f))).length === 0 ? "yes" : "partial"}`);
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 = createMemoryStore(config.dataDir);
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({ query });
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 entry of results) {
815
- log(` ${c.cyan}${c.bold}${entry.id}${c.reset} ${c.dim}[${entry.tier}]${c.reset}`);
816
- log(` ${c.dim}Tags:${c.reset} ${entry.tags.join(", ")}`);
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}Total entries:${c.reset} ${stats.totalEntries}`);
830
- log(` ${c.cyan}Total size:${c.reset} ${(stats.totalSizeBytes / 1024).toFixed(1)} KB`);
831
- log(` ${c.cyan}Compression ratio:${c.reset} ${(stats.compressionRatio * 100).toFixed(1)}%`);
832
- log(`\n${c.bold}By Tier:${c.reset}`);
833
- for (const [tier, count] of Object.entries(stats.byTier)) {
834
- log(` ${c.yellow}${tier}${c.reset}: ${count}`);
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
- cmdCost(flags);
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 — Wisdom Pipeline
14
- export { extractWisdom, renderWisdomMarkdown } from "./skills/wisdom-extractor.js";
15
- export { reconcileWisdom, loadRefinedLibrary, saveRefinedLibrary, exportRefinedSkills } from "./skills/skill-reconciler.js";
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
- // Skills Context Engine
18
- export { createContextEngine } from "./skills/context-engine.js";
19
- export { createGlobalContext } from "./skills/global-context.js";
20
- export { analyzePatternEvolution } from "./skills/pattern-engine.js";
21
- export { smartExtract } from "./skills/smart-extractor.js";
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 (from superdev)
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 (from superdev)
32
+ // Codebase Mapping
28
33
  export { mapCodebase } from "./codebase/mapper.js";
29
34
  export { generateOnboardingGuide } from "./codebase/onboard.js";
30
35
 
31
- // Test Health (from superdev)
36
+ // Test Health
32
37
  export { checkTestHealth } from "./testing/health-check.js";
33
38
  export { suggestFixes } from "./testing/test-fixer.js";
34
39
 
35
- // Cost Monitor (from superdev)
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 { createMemoryStore } from "../memory-engine/store.js";
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
- // Skills
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 session = parseAnySession(path, platform ?? "claude-code");
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 code = readFileSync(resolve(file_path), "utf-8");
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: resolve(directory ?? ".") });
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: resolve(directory ?? ".") });
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(resolve(directory ?? "."));
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(resolve(directory ?? "."));
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 = createMemoryStore(dataDir);
225
- const results = store.search({ query, limit: limit ?? 10 });
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 = createMemoryStore(dataDir);
239
- const entry = store.add({ content, tags: tags ?? [], tier: "working" });
240
- return { content: [{ type: "text" as const, text: JSON.stringify({ saved: true, id: entry.id }) }] };
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 refined skills extracted from past sessions.",
249
- {},
250
- async () => {
251
- const library = loadRefinedLibrary(dataDir);
252
- return { content: [{ type: "text" as const, text: JSON.stringify(library.skills, null, 2) }] };
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