memorix 0.6.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -476,7 +476,100 @@ var init_fastembed_provider = __esm({
476
476
  }
477
477
  });
478
478
 
479
+ // src/embedding/transformers-provider.ts
480
+ var transformers_provider_exports = {};
481
+ __export(transformers_provider_exports, {
482
+ TransformersProvider: () => TransformersProvider
483
+ });
484
+ var cache2, MAX_CACHE_SIZE2, TransformersProvider;
485
+ var init_transformers_provider = __esm({
486
+ "src/embedding/transformers-provider.ts"() {
487
+ "use strict";
488
+ init_esm_shims();
489
+ cache2 = /* @__PURE__ */ new Map();
490
+ MAX_CACHE_SIZE2 = 5e3;
491
+ TransformersProvider = class _TransformersProvider {
492
+ name = "transformers-minilm";
493
+ dimensions = 384;
494
+ extractor;
495
+ // Pipeline instance
496
+ constructor(extractor) {
497
+ this.extractor = extractor;
498
+ }
499
+ /**
500
+ * Initialize the Transformers.js provider.
501
+ * Downloads model on first use (~22MB quantized), cached locally after.
502
+ */
503
+ static async create() {
504
+ const { pipeline } = await import("@huggingface/transformers");
505
+ const extractor = await pipeline(
506
+ "feature-extraction",
507
+ "Xenova/all-MiniLM-L6-v2",
508
+ { dtype: "q8" }
509
+ // Quantized for small footprint
510
+ );
511
+ return new _TransformersProvider(extractor);
512
+ }
513
+ async embed(text) {
514
+ const cached = cache2.get(text);
515
+ if (cached) return cached;
516
+ const output = await this.extractor(text, {
517
+ pooling: "mean",
518
+ normalize: true
519
+ });
520
+ const result = Array.from(output.tolist()[0]);
521
+ if (result.length !== this.dimensions) {
522
+ throw new Error(`Expected ${this.dimensions}d embedding, got ${result.length}d`);
523
+ }
524
+ this.cacheSet(text, result);
525
+ return result;
526
+ }
527
+ async embedBatch(texts) {
528
+ const results = new Array(texts.length);
529
+ const uncachedIndices = [];
530
+ const uncachedTexts = [];
531
+ for (let i = 0; i < texts.length; i++) {
532
+ const cached = cache2.get(texts[i]);
533
+ if (cached) {
534
+ results[i] = cached;
535
+ } else {
536
+ uncachedIndices.push(i);
537
+ uncachedTexts.push(texts[i]);
538
+ }
539
+ }
540
+ if (uncachedTexts.length > 0) {
541
+ const output = await this.extractor(uncachedTexts, {
542
+ pooling: "mean",
543
+ normalize: true
544
+ });
545
+ const allVecs = output.tolist();
546
+ for (let i = 0; i < allVecs.length; i++) {
547
+ const vec = Array.from(allVecs[i]);
548
+ const originalIdx = uncachedIndices[i];
549
+ results[originalIdx] = vec;
550
+ this.cacheSet(uncachedTexts[i], vec);
551
+ }
552
+ }
553
+ return results;
554
+ }
555
+ cacheSet(key, value) {
556
+ if (cache2.size >= MAX_CACHE_SIZE2) {
557
+ const firstKey = cache2.keys().next().value;
558
+ if (firstKey !== void 0) cache2.delete(firstKey);
559
+ }
560
+ cache2.set(key, value);
561
+ }
562
+ };
563
+ }
564
+ });
565
+
479
566
  // src/embedding/provider.ts
567
+ var provider_exports = {};
568
+ __export(provider_exports, {
569
+ getEmbeddingProvider: () => getEmbeddingProvider,
570
+ isVectorSearchAvailable: () => isVectorSearchAvailable,
571
+ resetProvider: () => resetProvider
572
+ });
480
573
  async function getEmbeddingProvider() {
481
574
  if (initialized) return provider;
482
575
  initialized = true;
@@ -487,9 +580,24 @@ async function getEmbeddingProvider() {
487
580
  return provider;
488
581
  } catch {
489
582
  }
583
+ try {
584
+ const { TransformersProvider: TransformersProvider2 } = await Promise.resolve().then(() => (init_transformers_provider(), transformers_provider_exports));
585
+ provider = await TransformersProvider2.create();
586
+ console.error(`[memorix] Embedding provider: ${provider.name} (${provider.dimensions}d)`);
587
+ return provider;
588
+ } catch {
589
+ }
490
590
  console.error("[memorix] No embedding provider available \u2014 using fulltext search only");
491
591
  return null;
492
592
  }
593
+ async function isVectorSearchAvailable() {
594
+ const p3 = await getEmbeddingProvider();
595
+ return p3 !== null;
596
+ }
597
+ function resetProvider() {
598
+ provider = null;
599
+ initialized = false;
600
+ }
493
601
  var provider, initialized;
494
602
  var init_provider = __esm({
495
603
  "src/embedding/provider.ts"() {
@@ -3749,6 +3857,320 @@ var init_retention = __esm({
3749
3857
  }
3750
3858
  });
3751
3859
 
3860
+ // src/skills/engine.ts
3861
+ var engine_exports = {};
3862
+ __export(engine_exports, {
3863
+ SkillsEngine: () => SkillsEngine
3864
+ });
3865
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync2 } from "fs";
3866
+ import { join as join11 } from "path";
3867
+ import { homedir as homedir10 } from "os";
3868
+ var SKILLS_DIRS, SKILL_WORTHY_TYPES, MIN_OBS_FOR_SKILL, MIN_SCORE_FOR_SKILL, SkillsEngine;
3869
+ var init_engine3 = __esm({
3870
+ "src/skills/engine.ts"() {
3871
+ "use strict";
3872
+ init_esm_shims();
3873
+ SKILLS_DIRS = {
3874
+ codex: [".codex/skills", ".agents/skills"],
3875
+ cursor: [".cursor/skills", ".cursor/skills-cursor"],
3876
+ windsurf: [".windsurf/skills"],
3877
+ "claude-code": [".claude/skills"],
3878
+ copilot: [".github/skills", ".copilot/skills"],
3879
+ antigravity: [".agent/skills", ".gemini/skills", ".gemini/antigravity/skills"],
3880
+ kiro: [".kiro/skills"]
3881
+ };
3882
+ SKILL_WORTHY_TYPES = /* @__PURE__ */ new Set([
3883
+ "gotcha",
3884
+ "decision",
3885
+ "how-it-works",
3886
+ "problem-solution",
3887
+ "trade-off"
3888
+ ]);
3889
+ MIN_OBS_FOR_SKILL = 3;
3890
+ MIN_SCORE_FOR_SKILL = 5;
3891
+ SkillsEngine = class {
3892
+ constructor(projectRoot, options) {
3893
+ this.projectRoot = projectRoot;
3894
+ this.skipGlobal = options?.skipGlobal ?? false;
3895
+ }
3896
+ skipGlobal;
3897
+ // ============================================================
3898
+ // List: Discover all available skills
3899
+ // ============================================================
3900
+ /**
3901
+ * List all available skills from all agents + generated suggestions.
3902
+ */
3903
+ listSkills() {
3904
+ const skills = [];
3905
+ const seen = /* @__PURE__ */ new Set();
3906
+ const home = homedir10();
3907
+ for (const [agent, dirs] of Object.entries(SKILLS_DIRS)) {
3908
+ for (const dir of dirs) {
3909
+ const paths = [join11(this.projectRoot, dir)];
3910
+ if (!this.skipGlobal) {
3911
+ paths.push(join11(home, dir));
3912
+ }
3913
+ for (const skillsRoot of paths) {
3914
+ if (!existsSync5(skillsRoot)) continue;
3915
+ try {
3916
+ const entries = readdirSync2(skillsRoot, { withFileTypes: true });
3917
+ for (const entry of entries) {
3918
+ if (!entry.isDirectory()) continue;
3919
+ const name = entry.name;
3920
+ if (seen.has(name)) continue;
3921
+ const skillMd = join11(skillsRoot, name, "SKILL.md");
3922
+ if (!existsSync5(skillMd)) continue;
3923
+ try {
3924
+ const content = readFileSync3(skillMd, "utf-8");
3925
+ const description = this.parseDescription(content);
3926
+ skills.push({
3927
+ name,
3928
+ description,
3929
+ sourcePath: join11(skillsRoot, name),
3930
+ sourceAgent: agent,
3931
+ content,
3932
+ generated: false
3933
+ });
3934
+ seen.add(name);
3935
+ } catch {
3936
+ }
3937
+ }
3938
+ } catch {
3939
+ }
3940
+ }
3941
+ }
3942
+ }
3943
+ return skills;
3944
+ }
3945
+ // ============================================================
3946
+ // Generate: Create skills from observation patterns
3947
+ // ============================================================
3948
+ /**
3949
+ * Analyze observations and generate SKILL.md content for entities with
3950
+ * rich knowledge accumulation.
3951
+ */
3952
+ generateFromObservations(observations2) {
3953
+ const clusters = this.clusterByEntity(observations2);
3954
+ for (const cluster of clusters.values()) {
3955
+ cluster.score = this.scoreCluster(cluster);
3956
+ }
3957
+ const results = [];
3958
+ const sortedClusters = [...clusters.values()].filter((c) => c.score >= MIN_SCORE_FOR_SKILL).sort((a, b) => b.score - a.score).slice(0, 10);
3959
+ for (const cluster of sortedClusters) {
3960
+ const skill = this.clusterToSkill(cluster);
3961
+ if (skill) results.push(skill);
3962
+ }
3963
+ return results;
3964
+ }
3965
+ /**
3966
+ * Write a generated skill to the target agent's skills directory.
3967
+ */
3968
+ writeSkill(skill, target) {
3969
+ const dirs = SKILLS_DIRS[target];
3970
+ if (!dirs || dirs.length === 0) return null;
3971
+ const targetDir = join11(this.projectRoot, dirs[0], skill.name);
3972
+ try {
3973
+ mkdirSync3(targetDir, { recursive: true });
3974
+ writeFileSync2(join11(targetDir, "SKILL.md"), skill.content, "utf-8");
3975
+ return join11(dirs[0], skill.name, "SKILL.md");
3976
+ } catch {
3977
+ return null;
3978
+ }
3979
+ }
3980
+ // ============================================================
3981
+ // Inject: Return skill content for direct agent consumption
3982
+ // ============================================================
3983
+ /**
3984
+ * Get full content of a skill by name (for direct injection).
3985
+ */
3986
+ injectSkill(name) {
3987
+ const all = this.listSkills();
3988
+ return all.find((s) => s.name.toLowerCase() === name.toLowerCase()) || null;
3989
+ }
3990
+ // ============================================================
3991
+ // Internal helpers
3992
+ // ============================================================
3993
+ parseDescription(content) {
3994
+ const match = content.match(/^---[\s\S]*?description:\s*["']?(.+?)["']?\s*$/m);
3995
+ return match ? match[1] : "";
3996
+ }
3997
+ clusterByEntity(observations2) {
3998
+ const clusters = /* @__PURE__ */ new Map();
3999
+ for (const obs of observations2) {
4000
+ const entity = obs.entityName || "unknown";
4001
+ let cluster = clusters.get(entity);
4002
+ if (!cluster) {
4003
+ cluster = { entity, observations: [], types: /* @__PURE__ */ new Set(), score: 0 };
4004
+ clusters.set(entity, cluster);
4005
+ }
4006
+ cluster.observations.push(obs);
4007
+ cluster.types.add(obs.type);
4008
+ }
4009
+ return clusters;
4010
+ }
4011
+ scoreCluster(cluster) {
4012
+ let score = 0;
4013
+ const obs = cluster.observations;
4014
+ if (obs.length < MIN_OBS_FOR_SKILL) return 0;
4015
+ let hasSkillWorthyType = false;
4016
+ for (const type of cluster.types) {
4017
+ if (SKILL_WORTHY_TYPES.has(type)) {
4018
+ hasSkillWorthyType = true;
4019
+ break;
4020
+ }
4021
+ }
4022
+ if (!hasSkillWorthyType) return 0;
4023
+ score += Math.min(obs.length, 5);
4024
+ for (const type of cluster.types) {
4025
+ if (SKILL_WORTHY_TYPES.has(type)) score += 3;
4026
+ }
4027
+ const gotchas = obs.filter((o) => o.type === "gotcha").length;
4028
+ score += gotchas * 3;
4029
+ const decisions = obs.filter((o) => o.type === "decision").length;
4030
+ score += decisions * 2;
4031
+ const totalFacts = obs.reduce((sum, o) => sum + (o.facts?.length || 0), 0);
4032
+ score += Math.min(totalFacts, 5);
4033
+ const totalFiles = new Set(obs.flatMap((o) => o.filesModified || [])).size;
4034
+ score += Math.min(totalFiles, 5);
4035
+ return score;
4036
+ }
4037
+ clusterToSkill(cluster) {
4038
+ const { entity, observations: observations2 } = cluster;
4039
+ const safeName = entity.replace(/[^a-zA-Z0-9_-]/g, "-").toLowerCase();
4040
+ const gotchas = observations2.filter((o) => o.type === "gotcha");
4041
+ const decisions = observations2.filter((o) => o.type === "decision");
4042
+ const howItWorks = observations2.filter((o) => o.type === "how-it-works");
4043
+ const problems = observations2.filter((o) => o.type === "problem-solution");
4044
+ const tradeoffs = observations2.filter((o) => o.type === "trade-off");
4045
+ const others = observations2.filter(
4046
+ (o) => !["gotcha", "decision", "how-it-works", "problem-solution", "trade-off"].includes(o.type)
4047
+ );
4048
+ const allFacts = [...new Set(observations2.flatMap((o) => o.facts || []))];
4049
+ const allConcepts = [...new Set(observations2.flatMap((o) => o.concepts || []))];
4050
+ const allFiles = [...new Set(observations2.flatMap((o) => o.filesModified || []))];
4051
+ const lines = [];
4052
+ const description = this.generateDescription(cluster);
4053
+ lines.push("---");
4054
+ lines.push(`description: ${description}`);
4055
+ lines.push("---");
4056
+ lines.push("");
4057
+ lines.push(`# ${entity}`);
4058
+ lines.push("");
4059
+ lines.push(`> Auto-generated from ${observations2.length} project observations by Memorix.`);
4060
+ lines.push("> Adapt to your actual project context before relying on this skill.");
4061
+ lines.push("");
4062
+ if (allFiles.length > 0) {
4063
+ lines.push("## Key Files");
4064
+ lines.push("");
4065
+ for (const f of allFiles.slice(0, 15)) {
4066
+ lines.push(`- \`${f}\``);
4067
+ }
4068
+ lines.push("");
4069
+ }
4070
+ if (gotchas.length > 0) {
4071
+ lines.push("## \u26A0\uFE0F Critical Gotchas");
4072
+ lines.push("");
4073
+ for (const g of gotchas) {
4074
+ lines.push(`### ${g.title}`);
4075
+ if (g.narrative) lines.push("", g.narrative);
4076
+ if (g.facts && g.facts.length > 0) {
4077
+ lines.push("", ...g.facts.map((f) => `- ${f}`));
4078
+ }
4079
+ lines.push("");
4080
+ }
4081
+ }
4082
+ if (decisions.length > 0) {
4083
+ lines.push("## \u{1F3D7}\uFE0F Architecture Decisions");
4084
+ lines.push("");
4085
+ for (const d of decisions) {
4086
+ lines.push(`### ${d.title}`);
4087
+ if (d.narrative) lines.push("", d.narrative);
4088
+ if (d.facts && d.facts.length > 0) {
4089
+ lines.push("", ...d.facts.map((f) => `- ${f}`));
4090
+ }
4091
+ lines.push("");
4092
+ }
4093
+ }
4094
+ if (howItWorks.length > 0) {
4095
+ lines.push("## \u{1F4D6} How It Works");
4096
+ lines.push("");
4097
+ for (const h of howItWorks) {
4098
+ lines.push(`### ${h.title}`);
4099
+ if (h.narrative) lines.push("", h.narrative);
4100
+ lines.push("");
4101
+ }
4102
+ }
4103
+ if (problems.length > 0) {
4104
+ lines.push("## \u{1F527} Common Problems & Solutions");
4105
+ lines.push("");
4106
+ for (const p3 of problems) {
4107
+ lines.push(`### ${p3.title}`);
4108
+ if (p3.narrative) lines.push("", p3.narrative);
4109
+ if (p3.facts && p3.facts.length > 0) {
4110
+ lines.push("", ...p3.facts.map((f) => `- ${f}`));
4111
+ }
4112
+ lines.push("");
4113
+ }
4114
+ }
4115
+ if (tradeoffs.length > 0) {
4116
+ lines.push("## \u2696\uFE0F Trade-offs");
4117
+ lines.push("");
4118
+ for (const t of tradeoffs) {
4119
+ lines.push(`### ${t.title}`);
4120
+ if (t.narrative) lines.push("", t.narrative);
4121
+ lines.push("");
4122
+ }
4123
+ }
4124
+ if (others.length > 0) {
4125
+ lines.push("## \u{1F4DD} Notes");
4126
+ lines.push("");
4127
+ for (const o of others.slice(0, 5)) {
4128
+ lines.push(`- **${o.title}**: ${o.narrative?.split("\n")[0] || ""}`);
4129
+ }
4130
+ lines.push("");
4131
+ }
4132
+ if (allConcepts.length > 0) {
4133
+ lines.push("## \u{1F3F7}\uFE0F Related Concepts");
4134
+ lines.push("");
4135
+ lines.push(allConcepts.map((c) => `\`${c}\``).join(", "));
4136
+ lines.push("");
4137
+ }
4138
+ if (allFacts.length > 0) {
4139
+ lines.push("## \u{1F4CC} Quick Facts");
4140
+ lines.push("");
4141
+ for (const f of allFacts.slice(0, 15)) {
4142
+ lines.push(`- ${f}`);
4143
+ }
4144
+ lines.push("");
4145
+ }
4146
+ const content = lines.join("\n");
4147
+ return {
4148
+ name: safeName,
4149
+ description,
4150
+ sourcePath: "",
4151
+ sourceAgent: "codex",
4152
+ // generated skills follow SKILL.md standard
4153
+ content,
4154
+ generated: true
4155
+ };
4156
+ }
4157
+ generateDescription(cluster) {
4158
+ const parts = [];
4159
+ const typeCounts = {};
4160
+ for (const obs of cluster.observations) {
4161
+ typeCounts[obs.type] = (typeCounts[obs.type] || 0) + 1;
4162
+ }
4163
+ if (typeCounts["gotcha"]) parts.push(`${typeCounts["gotcha"]} gotcha(s)`);
4164
+ if (typeCounts["decision"]) parts.push(`${typeCounts["decision"]} decision(s)`);
4165
+ if (typeCounts["how-it-works"]) parts.push(`${typeCounts["how-it-works"]} explanation(s)`);
4166
+ if (typeCounts["problem-solution"]) parts.push(`${typeCounts["problem-solution"]} fix(es)`);
4167
+ const summary = parts.length > 0 ? parts.join(", ") : `${cluster.observations.length} observations`;
4168
+ return `Project patterns for ${cluster.entity}: ${summary}`;
4169
+ }
4170
+ };
4171
+ }
4172
+ });
4173
+
3752
4174
  // src/dashboard/server.ts
3753
4175
  var server_exports = {};
3754
4176
  __export(server_exports, {
@@ -3836,13 +4258,26 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir) {
3836
4258
  typeCounts[t] = (typeCounts[t] || 0) + 1;
3837
4259
  }
3838
4260
  const sorted = [...observations2].sort((a, b) => (b.id || 0) - (a.id || 0)).slice(0, 10);
4261
+ let embeddingStatus = { enabled: false, provider: "", dimensions: 0 };
4262
+ try {
4263
+ const { isEmbeddingEnabled: isEmbeddingEnabled2 } = await Promise.resolve().then(() => (init_orama_store(), orama_store_exports));
4264
+ const { getEmbeddingProvider: getEmbeddingProvider2 } = await Promise.resolve().then(() => (init_provider(), provider_exports));
4265
+ const provider2 = await getEmbeddingProvider2();
4266
+ embeddingStatus = {
4267
+ enabled: isEmbeddingEnabled2(),
4268
+ provider: provider2?.name || "",
4269
+ dimensions: provider2?.dimensions || 0
4270
+ };
4271
+ } catch {
4272
+ }
3839
4273
  sendJson(res, {
3840
4274
  entities: graph.entities.length,
3841
4275
  relations: graph.relations.length,
3842
4276
  observations: observations2.length,
3843
4277
  nextId: nextId2,
3844
4278
  typeCounts,
3845
- recentObservations: sorted
4279
+ recentObservations: sorted,
4280
+ embedding: embeddingStatus
3846
4281
  });
3847
4282
  break;
3848
4283
  }
@@ -4643,6 +5078,114 @@ Entity: ${entityName} | Type: ${type} | Project: ${project.id}${enrichment}`
4643
5078
  };
4644
5079
  }
4645
5080
  );
5081
+ server.registerTool(
5082
+ "memorix_skills",
5083
+ {
5084
+ title: "Project Skills",
5085
+ description: `Memory-driven project skills. Action "list": show all available skills from all agents. Action "generate": auto-generate project-specific skills from observation patterns (gotchas, decisions, how-it-works). Action "inject": return a specific skill's full content for direct use. Generated skills follow the SKILL.md standard and can be synced across agents.`,
5086
+ inputSchema: {
5087
+ action: z.enum(["list", "generate", "inject"]).describe('Action: "list" to discover skills, "generate" to create from memory, "inject" to get skill content'),
5088
+ name: z.string().optional().describe('Skill name (required for "inject")'),
5089
+ target: z.enum(AGENT_TARGETS).optional().describe('Target agent to write generated skills to (optional for "generate")'),
5090
+ write: z.boolean().optional().describe("Whether to write generated skills to disk (default: false, preview only)")
5091
+ }
5092
+ },
5093
+ async ({ action, name, target, write }) => {
5094
+ const { SkillsEngine: SkillsEngine2 } = await Promise.resolve().then(() => (init_engine3(), engine_exports));
5095
+ const engine = new SkillsEngine2(project.rootPath);
5096
+ if (action === "list") {
5097
+ const skills = engine.listSkills();
5098
+ if (skills.length === 0) {
5099
+ return {
5100
+ content: [{ type: "text", text: 'No skills found in any agent directory.\n\nSkills are discovered from:\n- `.cursor/skills/*/SKILL.md`\n- `.agents/skills/*/SKILL.md`\n- `.agent/skills/*/SKILL.md`\n- `.windsurf/skills/*/SKILL.md`\n- etc.\n\nUse action "generate" to auto-create skills from your project observations.' }]
5101
+ };
5102
+ }
5103
+ const lines2 = [
5104
+ `## Available Skills (${skills.length})`,
5105
+ ""
5106
+ ];
5107
+ for (const sk of skills) {
5108
+ lines2.push(`- **${sk.name}** (${sk.sourceAgent}): ${sk.description || "(no description)"}`);
5109
+ }
5110
+ lines2.push("", '> Use `action: "inject", name: "<skill-name>"` to get full skill content.');
5111
+ return {
5112
+ content: [{ type: "text", text: lines2.join("\n") }]
5113
+ };
5114
+ }
5115
+ if (action === "inject") {
5116
+ if (!name) {
5117
+ return {
5118
+ content: [{ type: "text", text: 'Error: `name` is required for inject action. Use `action: "list"` first to see available skills.' }],
5119
+ isError: true
5120
+ };
5121
+ }
5122
+ const skill = engine.injectSkill(name);
5123
+ if (!skill) {
5124
+ return {
5125
+ content: [{ type: "text", text: `Skill "${name}" not found. Use \`action: "list"\` to see available skills.` }],
5126
+ isError: true
5127
+ };
5128
+ }
5129
+ return {
5130
+ content: [{ type: "text", text: `## Skill: ${skill.name}
5131
+ **Source**: ${skill.sourceAgent}
5132
+ **Path**: ${skill.sourcePath}
5133
+
5134
+ ---
5135
+
5136
+ ${skill.content}` }]
5137
+ };
5138
+ }
5139
+ const { loadObservationsJson: loadObservationsJson2 } = await Promise.resolve().then(() => (init_persistence(), persistence_exports));
5140
+ const allObs = await loadObservationsJson2(projectDir2);
5141
+ const obsData = allObs.map((o) => ({
5142
+ id: o.id || 0,
5143
+ entityName: o.entityName || "unknown",
5144
+ type: o.type || "discovery",
5145
+ title: o.title || "",
5146
+ narrative: o.narrative || "",
5147
+ facts: o.facts,
5148
+ concepts: o.concepts,
5149
+ filesModified: o.filesModified,
5150
+ createdAt: o.createdAt
5151
+ }));
5152
+ const generated = engine.generateFromObservations(obsData);
5153
+ if (generated.length === 0) {
5154
+ return {
5155
+ content: [{ type: "text", text: "No skill-worthy patterns found yet.\n\nSkills are auto-generated when entities accumulate enough observations (3+), especially gotchas, decisions, and how-it-works notes.\n\nKeep using memorix_store to build up project knowledge!" }]
5156
+ };
5157
+ }
5158
+ const lines = [
5159
+ `## Generated Skills (${generated.length})`,
5160
+ "",
5161
+ "Based on observation patterns in your project memory:",
5162
+ ""
5163
+ ];
5164
+ for (const sk of generated) {
5165
+ lines.push(`### ${sk.name}`);
5166
+ lines.push(`- **Description**: ${sk.description}`);
5167
+ lines.push(`- **Observations**: ${sk.content.split("\n").length} lines of knowledge`);
5168
+ if (write && target) {
5169
+ const path8 = engine.writeSkill(sk, target);
5170
+ if (path8) {
5171
+ lines.push(`- \u2705 **Written**: \`${path8}\``);
5172
+ } else {
5173
+ lines.push(`- \u274C Failed to write`);
5174
+ }
5175
+ }
5176
+ lines.push("");
5177
+ }
5178
+ if (!write) {
5179
+ lines.push('> Preview only. Add `write: true, target: "<agent>"` to save skills to disk.');
5180
+ }
5181
+ if (generated.length > 0) {
5182
+ lines.push("", "---", "### Preview: " + generated[0].name, "", "```markdown", generated[0].content, "```");
5183
+ }
5184
+ return {
5185
+ content: [{ type: "text", text: lines.join("\n") }]
5186
+ };
5187
+ }
5188
+ );
4646
5189
  let dashboardRunning = false;
4647
5190
  server.registerTool(
4648
5191
  "memorix_dashboard",
@@ -5405,13 +5948,32 @@ async function handleHookEvent(input) {
5405
5948
  "discovery": 2,
5406
5949
  "how-it-works": 1
5407
5950
  };
5408
- const scored = allObs.map((obs, i) => ({
5409
- obs,
5410
- priority: PRIORITY_ORDER[obs.type ?? ""] ?? 0,
5411
- recency: i
5412
- // higher index = more recent
5413
- })).sort((a, b) => {
5414
- if (b.priority !== a.priority) return b.priority - a.priority;
5951
+ const LOW_QUALITY_PATTERNS2 = [
5952
+ /^Session activity/i,
5953
+ /^Updated \S+\.\w+$/i,
5954
+ // "Updated foo.ts" — too generic
5955
+ /^Created \S+\.\w+$/i,
5956
+ // "Created bar.js"
5957
+ /^Deleted \S+\.\w+$/i,
5958
+ /^Modified \S+\.\w+$/i
5959
+ ];
5960
+ const isLowQuality2 = (title) => LOW_QUALITY_PATTERNS2.some((p3) => p3.test(title));
5961
+ const scored = allObs.map((obs, i) => {
5962
+ const title = obs.title ?? "";
5963
+ const hasFacts = (obs.facts?.length ?? 0) > 0;
5964
+ const hasSubstance = title.length > 20 || hasFacts;
5965
+ const quality = isLowQuality2(title) ? 0.1 : hasSubstance ? 1 : 0.5;
5966
+ return {
5967
+ obs,
5968
+ priority: PRIORITY_ORDER[obs.type ?? ""] ?? 0,
5969
+ quality,
5970
+ recency: i
5971
+ // higher index = more recent
5972
+ };
5973
+ }).sort((a, b) => {
5974
+ const scoreA = a.priority * a.quality;
5975
+ const scoreB = b.priority * b.quality;
5976
+ if (scoreB !== scoreA) return scoreB - scoreA;
5415
5977
  return b.recency - a.recency;
5416
5978
  });
5417
5979
  const top = scored.slice(0, 5);
@@ -5831,13 +6393,130 @@ var init_dashboard = __esm({
5831
6393
  }
5832
6394
  });
5833
6395
 
6396
+ // src/cli/commands/cleanup.ts
6397
+ var cleanup_exports = {};
6398
+ __export(cleanup_exports, {
6399
+ default: () => cleanup_default
6400
+ });
6401
+ import { defineCommand as defineCommand10 } from "citty";
6402
+ function isLowQuality(title) {
6403
+ return LOW_QUALITY_PATTERNS.some((p3) => p3.test(title.trim()));
6404
+ }
6405
+ var LOW_QUALITY_PATTERNS, cleanup_default;
6406
+ var init_cleanup = __esm({
6407
+ "src/cli/commands/cleanup.ts"() {
6408
+ "use strict";
6409
+ init_esm_shims();
6410
+ init_detector();
6411
+ init_persistence();
6412
+ LOW_QUALITY_PATTERNS = [
6413
+ /^Session activity/i,
6414
+ /^Updated \S+\.\w+$/i,
6415
+ /^Created \S+\.\w+$/i,
6416
+ /^Deleted \S+\.\w+$/i,
6417
+ /^Modified \S+\.\w+$/i,
6418
+ /^Ran command:/i,
6419
+ /^Read file:/i
6420
+ ];
6421
+ cleanup_default = defineCommand10({
6422
+ meta: {
6423
+ name: "cleanup",
6424
+ description: "Remove low-quality auto-generated observations"
6425
+ },
6426
+ args: {
6427
+ dry: {
6428
+ type: "boolean",
6429
+ description: "Preview only \u2014 do not delete anything",
6430
+ default: false
6431
+ },
6432
+ force: {
6433
+ type: "boolean",
6434
+ description: "Delete without confirmation",
6435
+ default: false
6436
+ }
6437
+ },
6438
+ async run({ args }) {
6439
+ const project = detectProject();
6440
+ if (project.id === "__invalid__") {
6441
+ console.error("\u274C Not in a valid project directory.");
6442
+ process.exit(1);
6443
+ }
6444
+ console.log(`
6445
+ \u{1F4E6} Project: ${project.name} (${project.id})
6446
+ `);
6447
+ const dataDir = await getProjectDataDir(project.id);
6448
+ const allObs = await loadObservationsJson(dataDir);
6449
+ if (allObs.length === 0) {
6450
+ console.log("\u2705 No observations found \u2014 nothing to clean up.");
6451
+ return;
6452
+ }
6453
+ const lowQuality = allObs.filter((o) => isLowQuality(o.title ?? ""));
6454
+ const highQuality = allObs.filter((o) => !isLowQuality(o.title ?? ""));
6455
+ const seen = /* @__PURE__ */ new Set();
6456
+ const duplicates = [];
6457
+ const unique = [];
6458
+ for (const obs of highQuality) {
6459
+ const key = `${obs.type}|${obs.title}|${obs.entityName}`;
6460
+ if (seen.has(key)) {
6461
+ duplicates.push(obs);
6462
+ } else {
6463
+ seen.add(key);
6464
+ unique.push(obs);
6465
+ }
6466
+ }
6467
+ const toRemove = [...lowQuality, ...duplicates];
6468
+ console.log(`\u{1F4CA} Analysis:`);
6469
+ console.log(` Total observations: ${allObs.length}`);
6470
+ console.log(` \u{1F7E2} High quality: ${unique.length}`);
6471
+ console.log(` \u{1F534} Low quality: ${lowQuality.length}`);
6472
+ console.log(` \u{1F7E1} Duplicates: ${duplicates.length}`);
6473
+ console.log(` \u{1F5D1}\uFE0F To remove: ${toRemove.length}`);
6474
+ console.log();
6475
+ if (toRemove.length === 0) {
6476
+ console.log("\u2705 All observations are high quality \u2014 nothing to clean up!");
6477
+ return;
6478
+ }
6479
+ console.log("\u{1F50D} Examples of items to remove:");
6480
+ toRemove.slice(0, 10).forEach((o) => {
6481
+ const tag = isLowQuality(o.title ?? "") ? "(low-quality)" : "(duplicate)";
6482
+ console.log(` ${tag} #${o.id ?? "?"} "${o.title}" [${o.type}]`);
6483
+ });
6484
+ if (toRemove.length > 10) {
6485
+ console.log(` ... and ${toRemove.length - 10} more`);
6486
+ }
6487
+ console.log();
6488
+ if (args.dry) {
6489
+ console.log("\u{1F512} Dry run \u2014 no changes made.");
6490
+ return;
6491
+ }
6492
+ if (!args.force) {
6493
+ const readline = await import("readline");
6494
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6495
+ const answer = await new Promise((resolve2) => {
6496
+ rl.question(`\u26A0\uFE0F Delete ${toRemove.length} observations? (y/N) `, resolve2);
6497
+ });
6498
+ rl.close();
6499
+ if (answer.trim().toLowerCase() !== "y") {
6500
+ console.log("\u274C Cancelled.");
6501
+ return;
6502
+ }
6503
+ }
6504
+ const removeIds = new Set(toRemove.map((o) => JSON.stringify(o)));
6505
+ const remaining = allObs.filter((o) => !removeIds.has(JSON.stringify(o)));
6506
+ await saveObservationsJson(dataDir, remaining);
6507
+ console.log(`\u2705 Removed ${toRemove.length} observations. ${remaining.length} remaining.`);
6508
+ }
6509
+ });
6510
+ }
6511
+ });
6512
+
5834
6513
  // src/cli/index.ts
5835
6514
  init_esm_shims();
5836
- import { defineCommand as defineCommand10, runMain } from "citty";
6515
+ import { defineCommand as defineCommand11, runMain } from "citty";
5837
6516
  import { createRequire as createRequire2 } from "module";
5838
6517
  var require2 = createRequire2(import.meta.url);
5839
6518
  var pkg = require2("../../package.json");
5840
- var main = defineCommand10({
6519
+ var main = defineCommand11({
5841
6520
  meta: {
5842
6521
  name: "memorix",
5843
6522
  version: pkg.version,
@@ -5849,7 +6528,8 @@ var main = defineCommand10({
5849
6528
  sync: () => Promise.resolve().then(() => (init_sync(), sync_exports)).then((m) => m.default),
5850
6529
  hook: () => Promise.resolve().then(() => (init_hook(), hook_exports)).then((m) => m.default),
5851
6530
  hooks: () => Promise.resolve().then(() => (init_hooks(), hooks_exports)).then((m) => m.default),
5852
- dashboard: () => Promise.resolve().then(() => (init_dashboard(), dashboard_exports)).then((m) => m.default)
6531
+ dashboard: () => Promise.resolve().then(() => (init_dashboard(), dashboard_exports)).then((m) => m.default),
6532
+ cleanup: () => Promise.resolve().then(() => (init_cleanup(), cleanup_exports)).then((m) => m.default)
5853
6533
  },
5854
6534
  run() {
5855
6535
  }