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/index.js CHANGED
@@ -348,7 +348,100 @@ var init_fastembed_provider = __esm({
348
348
  }
349
349
  });
350
350
 
351
+ // src/embedding/transformers-provider.ts
352
+ var transformers_provider_exports = {};
353
+ __export(transformers_provider_exports, {
354
+ TransformersProvider: () => TransformersProvider
355
+ });
356
+ var cache2, MAX_CACHE_SIZE2, TransformersProvider;
357
+ var init_transformers_provider = __esm({
358
+ "src/embedding/transformers-provider.ts"() {
359
+ "use strict";
360
+ init_esm_shims();
361
+ cache2 = /* @__PURE__ */ new Map();
362
+ MAX_CACHE_SIZE2 = 5e3;
363
+ TransformersProvider = class _TransformersProvider {
364
+ name = "transformers-minilm";
365
+ dimensions = 384;
366
+ extractor;
367
+ // Pipeline instance
368
+ constructor(extractor) {
369
+ this.extractor = extractor;
370
+ }
371
+ /**
372
+ * Initialize the Transformers.js provider.
373
+ * Downloads model on first use (~22MB quantized), cached locally after.
374
+ */
375
+ static async create() {
376
+ const { pipeline } = await import("@huggingface/transformers");
377
+ const extractor = await pipeline(
378
+ "feature-extraction",
379
+ "Xenova/all-MiniLM-L6-v2",
380
+ { dtype: "q8" }
381
+ // Quantized for small footprint
382
+ );
383
+ return new _TransformersProvider(extractor);
384
+ }
385
+ async embed(text) {
386
+ const cached = cache2.get(text);
387
+ if (cached) return cached;
388
+ const output = await this.extractor(text, {
389
+ pooling: "mean",
390
+ normalize: true
391
+ });
392
+ const result = Array.from(output.tolist()[0]);
393
+ if (result.length !== this.dimensions) {
394
+ throw new Error(`Expected ${this.dimensions}d embedding, got ${result.length}d`);
395
+ }
396
+ this.cacheSet(text, result);
397
+ return result;
398
+ }
399
+ async embedBatch(texts) {
400
+ const results = new Array(texts.length);
401
+ const uncachedIndices = [];
402
+ const uncachedTexts = [];
403
+ for (let i = 0; i < texts.length; i++) {
404
+ const cached = cache2.get(texts[i]);
405
+ if (cached) {
406
+ results[i] = cached;
407
+ } else {
408
+ uncachedIndices.push(i);
409
+ uncachedTexts.push(texts[i]);
410
+ }
411
+ }
412
+ if (uncachedTexts.length > 0) {
413
+ const output = await this.extractor(uncachedTexts, {
414
+ pooling: "mean",
415
+ normalize: true
416
+ });
417
+ const allVecs = output.tolist();
418
+ for (let i = 0; i < allVecs.length; i++) {
419
+ const vec = Array.from(allVecs[i]);
420
+ const originalIdx = uncachedIndices[i];
421
+ results[originalIdx] = vec;
422
+ this.cacheSet(uncachedTexts[i], vec);
423
+ }
424
+ }
425
+ return results;
426
+ }
427
+ cacheSet(key, value) {
428
+ if (cache2.size >= MAX_CACHE_SIZE2) {
429
+ const firstKey = cache2.keys().next().value;
430
+ if (firstKey !== void 0) cache2.delete(firstKey);
431
+ }
432
+ cache2.set(key, value);
433
+ }
434
+ };
435
+ }
436
+ });
437
+
351
438
  // src/embedding/provider.ts
439
+ var provider_exports = {};
440
+ __export(provider_exports, {
441
+ getEmbeddingProvider: () => getEmbeddingProvider,
442
+ isVectorSearchAvailable: () => isVectorSearchAvailable,
443
+ resetProvider: () => resetProvider
444
+ });
352
445
  async function getEmbeddingProvider() {
353
446
  if (initialized) return provider;
354
447
  initialized = true;
@@ -359,9 +452,24 @@ async function getEmbeddingProvider() {
359
452
  return provider;
360
453
  } catch {
361
454
  }
455
+ try {
456
+ const { TransformersProvider: TransformersProvider2 } = await Promise.resolve().then(() => (init_transformers_provider(), transformers_provider_exports));
457
+ provider = await TransformersProvider2.create();
458
+ console.error(`[memorix] Embedding provider: ${provider.name} (${provider.dimensions}d)`);
459
+ return provider;
460
+ } catch {
461
+ }
362
462
  console.error("[memorix] No embedding provider available \u2014 using fulltext search only");
363
463
  return null;
364
464
  }
465
+ async function isVectorSearchAvailable() {
466
+ const p = await getEmbeddingProvider();
467
+ return p !== null;
468
+ }
469
+ function resetProvider() {
470
+ provider = null;
471
+ initialized = false;
472
+ }
365
473
  var provider, initialized;
366
474
  var init_provider = __esm({
367
475
  "src/embedding/provider.ts"() {
@@ -1131,6 +1239,320 @@ var init_retention = __esm({
1131
1239
  }
1132
1240
  });
1133
1241
 
1242
+ // src/skills/engine.ts
1243
+ var engine_exports = {};
1244
+ __export(engine_exports, {
1245
+ SkillsEngine: () => SkillsEngine
1246
+ });
1247
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync2 } from "fs";
1248
+ import { join as join11 } from "path";
1249
+ import { homedir as homedir10 } from "os";
1250
+ var SKILLS_DIRS, SKILL_WORTHY_TYPES, MIN_OBS_FOR_SKILL, MIN_SCORE_FOR_SKILL, SkillsEngine;
1251
+ var init_engine = __esm({
1252
+ "src/skills/engine.ts"() {
1253
+ "use strict";
1254
+ init_esm_shims();
1255
+ SKILLS_DIRS = {
1256
+ codex: [".codex/skills", ".agents/skills"],
1257
+ cursor: [".cursor/skills", ".cursor/skills-cursor"],
1258
+ windsurf: [".windsurf/skills"],
1259
+ "claude-code": [".claude/skills"],
1260
+ copilot: [".github/skills", ".copilot/skills"],
1261
+ antigravity: [".agent/skills", ".gemini/skills", ".gemini/antigravity/skills"],
1262
+ kiro: [".kiro/skills"]
1263
+ };
1264
+ SKILL_WORTHY_TYPES = /* @__PURE__ */ new Set([
1265
+ "gotcha",
1266
+ "decision",
1267
+ "how-it-works",
1268
+ "problem-solution",
1269
+ "trade-off"
1270
+ ]);
1271
+ MIN_OBS_FOR_SKILL = 3;
1272
+ MIN_SCORE_FOR_SKILL = 5;
1273
+ SkillsEngine = class {
1274
+ constructor(projectRoot, options) {
1275
+ this.projectRoot = projectRoot;
1276
+ this.skipGlobal = options?.skipGlobal ?? false;
1277
+ }
1278
+ skipGlobal;
1279
+ // ============================================================
1280
+ // List: Discover all available skills
1281
+ // ============================================================
1282
+ /**
1283
+ * List all available skills from all agents + generated suggestions.
1284
+ */
1285
+ listSkills() {
1286
+ const skills = [];
1287
+ const seen = /* @__PURE__ */ new Set();
1288
+ const home = homedir10();
1289
+ for (const [agent, dirs] of Object.entries(SKILLS_DIRS)) {
1290
+ for (const dir of dirs) {
1291
+ const paths = [join11(this.projectRoot, dir)];
1292
+ if (!this.skipGlobal) {
1293
+ paths.push(join11(home, dir));
1294
+ }
1295
+ for (const skillsRoot of paths) {
1296
+ if (!existsSync5(skillsRoot)) continue;
1297
+ try {
1298
+ const entries = readdirSync2(skillsRoot, { withFileTypes: true });
1299
+ for (const entry of entries) {
1300
+ if (!entry.isDirectory()) continue;
1301
+ const name = entry.name;
1302
+ if (seen.has(name)) continue;
1303
+ const skillMd = join11(skillsRoot, name, "SKILL.md");
1304
+ if (!existsSync5(skillMd)) continue;
1305
+ try {
1306
+ const content = readFileSync3(skillMd, "utf-8");
1307
+ const description = this.parseDescription(content);
1308
+ skills.push({
1309
+ name,
1310
+ description,
1311
+ sourcePath: join11(skillsRoot, name),
1312
+ sourceAgent: agent,
1313
+ content,
1314
+ generated: false
1315
+ });
1316
+ seen.add(name);
1317
+ } catch {
1318
+ }
1319
+ }
1320
+ } catch {
1321
+ }
1322
+ }
1323
+ }
1324
+ }
1325
+ return skills;
1326
+ }
1327
+ // ============================================================
1328
+ // Generate: Create skills from observation patterns
1329
+ // ============================================================
1330
+ /**
1331
+ * Analyze observations and generate SKILL.md content for entities with
1332
+ * rich knowledge accumulation.
1333
+ */
1334
+ generateFromObservations(observations2) {
1335
+ const clusters = this.clusterByEntity(observations2);
1336
+ for (const cluster of clusters.values()) {
1337
+ cluster.score = this.scoreCluster(cluster);
1338
+ }
1339
+ const results = [];
1340
+ const sortedClusters = [...clusters.values()].filter((c) => c.score >= MIN_SCORE_FOR_SKILL).sort((a, b) => b.score - a.score).slice(0, 10);
1341
+ for (const cluster of sortedClusters) {
1342
+ const skill = this.clusterToSkill(cluster);
1343
+ if (skill) results.push(skill);
1344
+ }
1345
+ return results;
1346
+ }
1347
+ /**
1348
+ * Write a generated skill to the target agent's skills directory.
1349
+ */
1350
+ writeSkill(skill, target) {
1351
+ const dirs = SKILLS_DIRS[target];
1352
+ if (!dirs || dirs.length === 0) return null;
1353
+ const targetDir = join11(this.projectRoot, dirs[0], skill.name);
1354
+ try {
1355
+ mkdirSync3(targetDir, { recursive: true });
1356
+ writeFileSync2(join11(targetDir, "SKILL.md"), skill.content, "utf-8");
1357
+ return join11(dirs[0], skill.name, "SKILL.md");
1358
+ } catch {
1359
+ return null;
1360
+ }
1361
+ }
1362
+ // ============================================================
1363
+ // Inject: Return skill content for direct agent consumption
1364
+ // ============================================================
1365
+ /**
1366
+ * Get full content of a skill by name (for direct injection).
1367
+ */
1368
+ injectSkill(name) {
1369
+ const all = this.listSkills();
1370
+ return all.find((s) => s.name.toLowerCase() === name.toLowerCase()) || null;
1371
+ }
1372
+ // ============================================================
1373
+ // Internal helpers
1374
+ // ============================================================
1375
+ parseDescription(content) {
1376
+ const match = content.match(/^---[\s\S]*?description:\s*["']?(.+?)["']?\s*$/m);
1377
+ return match ? match[1] : "";
1378
+ }
1379
+ clusterByEntity(observations2) {
1380
+ const clusters = /* @__PURE__ */ new Map();
1381
+ for (const obs of observations2) {
1382
+ const entity = obs.entityName || "unknown";
1383
+ let cluster = clusters.get(entity);
1384
+ if (!cluster) {
1385
+ cluster = { entity, observations: [], types: /* @__PURE__ */ new Set(), score: 0 };
1386
+ clusters.set(entity, cluster);
1387
+ }
1388
+ cluster.observations.push(obs);
1389
+ cluster.types.add(obs.type);
1390
+ }
1391
+ return clusters;
1392
+ }
1393
+ scoreCluster(cluster) {
1394
+ let score = 0;
1395
+ const obs = cluster.observations;
1396
+ if (obs.length < MIN_OBS_FOR_SKILL) return 0;
1397
+ let hasSkillWorthyType = false;
1398
+ for (const type of cluster.types) {
1399
+ if (SKILL_WORTHY_TYPES.has(type)) {
1400
+ hasSkillWorthyType = true;
1401
+ break;
1402
+ }
1403
+ }
1404
+ if (!hasSkillWorthyType) return 0;
1405
+ score += Math.min(obs.length, 5);
1406
+ for (const type of cluster.types) {
1407
+ if (SKILL_WORTHY_TYPES.has(type)) score += 3;
1408
+ }
1409
+ const gotchas = obs.filter((o) => o.type === "gotcha").length;
1410
+ score += gotchas * 3;
1411
+ const decisions = obs.filter((o) => o.type === "decision").length;
1412
+ score += decisions * 2;
1413
+ const totalFacts = obs.reduce((sum, o) => sum + (o.facts?.length || 0), 0);
1414
+ score += Math.min(totalFacts, 5);
1415
+ const totalFiles = new Set(obs.flatMap((o) => o.filesModified || [])).size;
1416
+ score += Math.min(totalFiles, 5);
1417
+ return score;
1418
+ }
1419
+ clusterToSkill(cluster) {
1420
+ const { entity, observations: observations2 } = cluster;
1421
+ const safeName = entity.replace(/[^a-zA-Z0-9_-]/g, "-").toLowerCase();
1422
+ const gotchas = observations2.filter((o) => o.type === "gotcha");
1423
+ const decisions = observations2.filter((o) => o.type === "decision");
1424
+ const howItWorks = observations2.filter((o) => o.type === "how-it-works");
1425
+ const problems = observations2.filter((o) => o.type === "problem-solution");
1426
+ const tradeoffs = observations2.filter((o) => o.type === "trade-off");
1427
+ const others = observations2.filter(
1428
+ (o) => !["gotcha", "decision", "how-it-works", "problem-solution", "trade-off"].includes(o.type)
1429
+ );
1430
+ const allFacts = [...new Set(observations2.flatMap((o) => o.facts || []))];
1431
+ const allConcepts = [...new Set(observations2.flatMap((o) => o.concepts || []))];
1432
+ const allFiles = [...new Set(observations2.flatMap((o) => o.filesModified || []))];
1433
+ const lines = [];
1434
+ const description = this.generateDescription(cluster);
1435
+ lines.push("---");
1436
+ lines.push(`description: ${description}`);
1437
+ lines.push("---");
1438
+ lines.push("");
1439
+ lines.push(`# ${entity}`);
1440
+ lines.push("");
1441
+ lines.push(`> Auto-generated from ${observations2.length} project observations by Memorix.`);
1442
+ lines.push("> Adapt to your actual project context before relying on this skill.");
1443
+ lines.push("");
1444
+ if (allFiles.length > 0) {
1445
+ lines.push("## Key Files");
1446
+ lines.push("");
1447
+ for (const f of allFiles.slice(0, 15)) {
1448
+ lines.push(`- \`${f}\``);
1449
+ }
1450
+ lines.push("");
1451
+ }
1452
+ if (gotchas.length > 0) {
1453
+ lines.push("## \u26A0\uFE0F Critical Gotchas");
1454
+ lines.push("");
1455
+ for (const g of gotchas) {
1456
+ lines.push(`### ${g.title}`);
1457
+ if (g.narrative) lines.push("", g.narrative);
1458
+ if (g.facts && g.facts.length > 0) {
1459
+ lines.push("", ...g.facts.map((f) => `- ${f}`));
1460
+ }
1461
+ lines.push("");
1462
+ }
1463
+ }
1464
+ if (decisions.length > 0) {
1465
+ lines.push("## \u{1F3D7}\uFE0F Architecture Decisions");
1466
+ lines.push("");
1467
+ for (const d of decisions) {
1468
+ lines.push(`### ${d.title}`);
1469
+ if (d.narrative) lines.push("", d.narrative);
1470
+ if (d.facts && d.facts.length > 0) {
1471
+ lines.push("", ...d.facts.map((f) => `- ${f}`));
1472
+ }
1473
+ lines.push("");
1474
+ }
1475
+ }
1476
+ if (howItWorks.length > 0) {
1477
+ lines.push("## \u{1F4D6} How It Works");
1478
+ lines.push("");
1479
+ for (const h of howItWorks) {
1480
+ lines.push(`### ${h.title}`);
1481
+ if (h.narrative) lines.push("", h.narrative);
1482
+ lines.push("");
1483
+ }
1484
+ }
1485
+ if (problems.length > 0) {
1486
+ lines.push("## \u{1F527} Common Problems & Solutions");
1487
+ lines.push("");
1488
+ for (const p of problems) {
1489
+ lines.push(`### ${p.title}`);
1490
+ if (p.narrative) lines.push("", p.narrative);
1491
+ if (p.facts && p.facts.length > 0) {
1492
+ lines.push("", ...p.facts.map((f) => `- ${f}`));
1493
+ }
1494
+ lines.push("");
1495
+ }
1496
+ }
1497
+ if (tradeoffs.length > 0) {
1498
+ lines.push("## \u2696\uFE0F Trade-offs");
1499
+ lines.push("");
1500
+ for (const t of tradeoffs) {
1501
+ lines.push(`### ${t.title}`);
1502
+ if (t.narrative) lines.push("", t.narrative);
1503
+ lines.push("");
1504
+ }
1505
+ }
1506
+ if (others.length > 0) {
1507
+ lines.push("## \u{1F4DD} Notes");
1508
+ lines.push("");
1509
+ for (const o of others.slice(0, 5)) {
1510
+ lines.push(`- **${o.title}**: ${o.narrative?.split("\n")[0] || ""}`);
1511
+ }
1512
+ lines.push("");
1513
+ }
1514
+ if (allConcepts.length > 0) {
1515
+ lines.push("## \u{1F3F7}\uFE0F Related Concepts");
1516
+ lines.push("");
1517
+ lines.push(allConcepts.map((c) => `\`${c}\``).join(", "));
1518
+ lines.push("");
1519
+ }
1520
+ if (allFacts.length > 0) {
1521
+ lines.push("## \u{1F4CC} Quick Facts");
1522
+ lines.push("");
1523
+ for (const f of allFacts.slice(0, 15)) {
1524
+ lines.push(`- ${f}`);
1525
+ }
1526
+ lines.push("");
1527
+ }
1528
+ const content = lines.join("\n");
1529
+ return {
1530
+ name: safeName,
1531
+ description,
1532
+ sourcePath: "",
1533
+ sourceAgent: "codex",
1534
+ // generated skills follow SKILL.md standard
1535
+ content,
1536
+ generated: true
1537
+ };
1538
+ }
1539
+ generateDescription(cluster) {
1540
+ const parts = [];
1541
+ const typeCounts = {};
1542
+ for (const obs of cluster.observations) {
1543
+ typeCounts[obs.type] = (typeCounts[obs.type] || 0) + 1;
1544
+ }
1545
+ if (typeCounts["gotcha"]) parts.push(`${typeCounts["gotcha"]} gotcha(s)`);
1546
+ if (typeCounts["decision"]) parts.push(`${typeCounts["decision"]} decision(s)`);
1547
+ if (typeCounts["how-it-works"]) parts.push(`${typeCounts["how-it-works"]} explanation(s)`);
1548
+ if (typeCounts["problem-solution"]) parts.push(`${typeCounts["problem-solution"]} fix(es)`);
1549
+ const summary = parts.length > 0 ? parts.join(", ") : `${cluster.observations.length} observations`;
1550
+ return `Project patterns for ${cluster.entity}: ${summary}`;
1551
+ }
1552
+ };
1553
+ }
1554
+ });
1555
+
1134
1556
  // src/dashboard/server.ts
1135
1557
  var server_exports = {};
1136
1558
  __export(server_exports, {
@@ -1218,13 +1640,26 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir) {
1218
1640
  typeCounts[t] = (typeCounts[t] || 0) + 1;
1219
1641
  }
1220
1642
  const sorted = [...observations2].sort((a, b) => (b.id || 0) - (a.id || 0)).slice(0, 10);
1643
+ let embeddingStatus = { enabled: false, provider: "", dimensions: 0 };
1644
+ try {
1645
+ const { isEmbeddingEnabled: isEmbeddingEnabled2 } = await Promise.resolve().then(() => (init_orama_store(), orama_store_exports));
1646
+ const { getEmbeddingProvider: getEmbeddingProvider2 } = await Promise.resolve().then(() => (init_provider(), provider_exports));
1647
+ const provider2 = await getEmbeddingProvider2();
1648
+ embeddingStatus = {
1649
+ enabled: isEmbeddingEnabled2(),
1650
+ provider: provider2?.name || "",
1651
+ dimensions: provider2?.dimensions || 0
1652
+ };
1653
+ } catch {
1654
+ }
1221
1655
  sendJson(res, {
1222
1656
  entities: graph.entities.length,
1223
1657
  relations: graph.relations.length,
1224
1658
  observations: observations2.length,
1225
1659
  nextId: nextId2,
1226
1660
  typeCounts,
1227
- recentObservations: sorted
1661
+ recentObservations: sorted,
1662
+ embedding: embeddingStatus
1228
1663
  });
1229
1664
  break;
1230
1665
  }
@@ -4457,6 +4892,114 @@ Entity: ${entityName} | Type: ${type} | Project: ${project.id}${enrichment}`
4457
4892
  };
4458
4893
  }
4459
4894
  );
4895
+ server.registerTool(
4896
+ "memorix_skills",
4897
+ {
4898
+ title: "Project Skills",
4899
+ 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.`,
4900
+ inputSchema: {
4901
+ action: z.enum(["list", "generate", "inject"]).describe('Action: "list" to discover skills, "generate" to create from memory, "inject" to get skill content'),
4902
+ name: z.string().optional().describe('Skill name (required for "inject")'),
4903
+ target: z.enum(AGENT_TARGETS).optional().describe('Target agent to write generated skills to (optional for "generate")'),
4904
+ write: z.boolean().optional().describe("Whether to write generated skills to disk (default: false, preview only)")
4905
+ }
4906
+ },
4907
+ async ({ action, name, target, write }) => {
4908
+ const { SkillsEngine: SkillsEngine2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
4909
+ const engine = new SkillsEngine2(project.rootPath);
4910
+ if (action === "list") {
4911
+ const skills = engine.listSkills();
4912
+ if (skills.length === 0) {
4913
+ return {
4914
+ 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.' }]
4915
+ };
4916
+ }
4917
+ const lines2 = [
4918
+ `## Available Skills (${skills.length})`,
4919
+ ""
4920
+ ];
4921
+ for (const sk of skills) {
4922
+ lines2.push(`- **${sk.name}** (${sk.sourceAgent}): ${sk.description || "(no description)"}`);
4923
+ }
4924
+ lines2.push("", '> Use `action: "inject", name: "<skill-name>"` to get full skill content.');
4925
+ return {
4926
+ content: [{ type: "text", text: lines2.join("\n") }]
4927
+ };
4928
+ }
4929
+ if (action === "inject") {
4930
+ if (!name) {
4931
+ return {
4932
+ content: [{ type: "text", text: 'Error: `name` is required for inject action. Use `action: "list"` first to see available skills.' }],
4933
+ isError: true
4934
+ };
4935
+ }
4936
+ const skill = engine.injectSkill(name);
4937
+ if (!skill) {
4938
+ return {
4939
+ content: [{ type: "text", text: `Skill "${name}" not found. Use \`action: "list"\` to see available skills.` }],
4940
+ isError: true
4941
+ };
4942
+ }
4943
+ return {
4944
+ content: [{ type: "text", text: `## Skill: ${skill.name}
4945
+ **Source**: ${skill.sourceAgent}
4946
+ **Path**: ${skill.sourcePath}
4947
+
4948
+ ---
4949
+
4950
+ ${skill.content}` }]
4951
+ };
4952
+ }
4953
+ const { loadObservationsJson: loadObservationsJson2 } = await Promise.resolve().then(() => (init_persistence(), persistence_exports));
4954
+ const allObs = await loadObservationsJson2(projectDir2);
4955
+ const obsData = allObs.map((o) => ({
4956
+ id: o.id || 0,
4957
+ entityName: o.entityName || "unknown",
4958
+ type: o.type || "discovery",
4959
+ title: o.title || "",
4960
+ narrative: o.narrative || "",
4961
+ facts: o.facts,
4962
+ concepts: o.concepts,
4963
+ filesModified: o.filesModified,
4964
+ createdAt: o.createdAt
4965
+ }));
4966
+ const generated = engine.generateFromObservations(obsData);
4967
+ if (generated.length === 0) {
4968
+ return {
4969
+ 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!" }]
4970
+ };
4971
+ }
4972
+ const lines = [
4973
+ `## Generated Skills (${generated.length})`,
4974
+ "",
4975
+ "Based on observation patterns in your project memory:",
4976
+ ""
4977
+ ];
4978
+ for (const sk of generated) {
4979
+ lines.push(`### ${sk.name}`);
4980
+ lines.push(`- **Description**: ${sk.description}`);
4981
+ lines.push(`- **Observations**: ${sk.content.split("\n").length} lines of knowledge`);
4982
+ if (write && target) {
4983
+ const path7 = engine.writeSkill(sk, target);
4984
+ if (path7) {
4985
+ lines.push(`- \u2705 **Written**: \`${path7}\``);
4986
+ } else {
4987
+ lines.push(`- \u274C Failed to write`);
4988
+ }
4989
+ }
4990
+ lines.push("");
4991
+ }
4992
+ if (!write) {
4993
+ lines.push('> Preview only. Add `write: true, target: "<agent>"` to save skills to disk.');
4994
+ }
4995
+ if (generated.length > 0) {
4996
+ lines.push("", "---", "### Preview: " + generated[0].name, "", "```markdown", generated[0].content, "```");
4997
+ }
4998
+ return {
4999
+ content: [{ type: "text", text: lines.join("\n") }]
5000
+ };
5001
+ }
5002
+ );
4460
5003
  let dashboardRunning = false;
4461
5004
  server.registerTool(
4462
5005
  "memorix_dashboard",