@hir4ta/mneme 0.22.0 → 0.22.3

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.
@@ -30355,66 +30355,6 @@ function searchSessions(mnemeDir, keywords, limit = 5) {
30355
30355
  });
30356
30356
  return results.sort((a, b) => b.score - a.score).slice(0, limit);
30357
30357
  }
30358
- function searchUnits(mnemeDir, keywords, limit = 5) {
30359
- const unitsPath = path3.join(mnemeDir, "units", "units.json");
30360
- const results = [];
30361
- const pattern = new RegExp(keywords.map(escapeRegex2).join("|"), "i");
30362
- if (!fs3.existsSync(unitsPath)) return results;
30363
- try {
30364
- const cards = JSON.parse(fs3.readFileSync(unitsPath, "utf-8"));
30365
- const items = (cards.items || []).filter(
30366
- (item) => item.status === "approved"
30367
- );
30368
- for (const item of items) {
30369
- let score = 0;
30370
- const matchedFields = [];
30371
- const titleScore = fieldScore(item.title, pattern, 3);
30372
- if (titleScore > 0) {
30373
- score += titleScore;
30374
- matchedFields.push("title");
30375
- }
30376
- const summaryScore = fieldScore(item.summary, pattern, 2);
30377
- if (summaryScore > 0) {
30378
- score += summaryScore;
30379
- matchedFields.push("summary");
30380
- }
30381
- if (item.tags?.some((tag) => pattern.test(tag))) {
30382
- score += 1;
30383
- matchedFields.push("tags");
30384
- }
30385
- if (item.sourceType && pattern.test(item.sourceType)) {
30386
- score += 1;
30387
- matchedFields.push("sourceType");
30388
- }
30389
- if (score === 0 && keywords.length <= 2) {
30390
- const titleWords = (item.title || "").toLowerCase().split(/\s+/);
30391
- const tagWords = item.tags || [];
30392
- for (const keyword of keywords) {
30393
- if (titleWords.some((w) => isFuzzyMatch(keyword, w))) {
30394
- score += 1;
30395
- matchedFields.push("title~fuzzy");
30396
- }
30397
- if (tagWords.some((t) => isFuzzyMatch(keyword, t))) {
30398
- score += 0.5;
30399
- matchedFields.push("tags~fuzzy");
30400
- }
30401
- }
30402
- }
30403
- if (score > 0) {
30404
- results.push({
30405
- type: "unit",
30406
- id: item.id,
30407
- title: item.title || item.id,
30408
- snippet: item.summary || "",
30409
- score,
30410
- matchedFields
30411
- });
30412
- }
30413
- }
30414
- } catch {
30415
- }
30416
- return results.sort((a, b) => b.score - a.score).slice(0, limit);
30417
- }
30418
30358
  function normalizeRequestedTypes(types) {
30419
30359
  const normalized = /* @__PURE__ */ new Set();
30420
30360
  for (const type of types) {
@@ -30428,7 +30368,7 @@ function searchKnowledge(options) {
30428
30368
  mnemeDir,
30429
30369
  projectPath,
30430
30370
  database = null,
30431
- types = ["session", "unit", "interaction"],
30371
+ types = ["session", "interaction"],
30432
30372
  limit = 10,
30433
30373
  offset = 0
30434
30374
  } = options;
@@ -30445,9 +30385,6 @@ function searchKnowledge(options) {
30445
30385
  if (normalizedTypes.has("session")) {
30446
30386
  results.push(...searchSessions(mnemeDir, expandedKeywords, fetchLimit));
30447
30387
  }
30448
- if (normalizedTypes.has("unit")) {
30449
- results.push(...searchUnits(mnemeDir, expandedKeywords, fetchLimit));
30450
- }
30451
30388
  if (normalizedTypes.has("interaction")) {
30452
30389
  results.push(
30453
30390
  ...searchInteractions(
@@ -30473,7 +30410,6 @@ var LIST_LIMIT_MIN = 1;
30473
30410
  var LIST_LIMIT_MAX = 200;
30474
30411
  var INTERACTION_OFFSET_MIN = 0;
30475
30412
  var QUERY_MAX_LENGTH = 500;
30476
- var UNIT_LIMIT_MAX = 500;
30477
30413
  var SEARCH_EVAL_DEFAULT_LIMIT = 5;
30478
30414
  function ok(text) {
30479
30415
  return { content: [{ type: "text", text }] };
@@ -30512,56 +30448,12 @@ function listJsonFiles(dir) {
30512
30448
  return entry.isFile() && entry.name.endsWith(".json") ? [fullPath] : [];
30513
30449
  });
30514
30450
  }
30515
- function readUnits() {
30516
- const unitsPath = path4.join(getMnemeDir(), "units", "units.json");
30517
- const parsed = readJsonFile(unitsPath);
30518
- if (!parsed || !Array.isArray(parsed.items)) {
30519
- return {
30520
- schemaVersion: 1,
30521
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
30522
- items: []
30523
- };
30524
- }
30525
- return parsed;
30526
- }
30527
- function writeUnits(doc) {
30528
- const unitsPath = path4.join(getMnemeDir(), "units", "units.json");
30529
- fs4.mkdirSync(path4.dirname(unitsPath), { recursive: true });
30530
- fs4.writeFileSync(unitsPath, JSON.stringify(doc, null, 2));
30531
- }
30532
30451
  function readRuleItems(ruleType) {
30533
30452
  const filePath = path4.join(getMnemeDir(), "rules", `${ruleType}.json`);
30534
30453
  const parsed = readJsonFile(filePath);
30535
30454
  const items = parsed?.items ?? parsed?.rules;
30536
30455
  return Array.isArray(items) ? items : [];
30537
30456
  }
30538
- function readAuditEntries(options = {}) {
30539
- const auditDir = path4.join(getMnemeDir(), "audit");
30540
- if (!fs4.existsSync(auditDir)) return [];
30541
- const files = fs4.readdirSync(auditDir).filter((name) => name.endsWith(".jsonl")).sort();
30542
- const fromTime = options.from ? new Date(options.from).getTime() : null;
30543
- const toTime = options.to ? new Date(options.to).getTime() : null;
30544
- const entries = [];
30545
- for (const name of files) {
30546
- const fullPath = path4.join(auditDir, name);
30547
- const lines = fs4.readFileSync(fullPath, "utf-8").split("\n");
30548
- for (const line of lines) {
30549
- if (!line.trim()) continue;
30550
- try {
30551
- const parsed = JSON.parse(line);
30552
- const ts = new Date(parsed.timestamp).getTime();
30553
- if (fromTime !== null && ts < fromTime) continue;
30554
- if (toTime !== null && ts > toTime) continue;
30555
- if (options.entity && parsed.entity !== options.entity) continue;
30556
- entries.push(parsed);
30557
- } catch {
30558
- }
30559
- }
30560
- }
30561
- return entries.sort(
30562
- (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
30563
- );
30564
- }
30565
30457
  function readSessionsById() {
30566
30458
  const sessionsDir = path4.join(getMnemeDir(), "sessions");
30567
30459
  const map2 = /* @__PURE__ */ new Map();
@@ -30573,78 +30465,6 @@ function readSessionsById() {
30573
30465
  }
30574
30466
  return map2;
30575
30467
  }
30576
- function inferUnitPriority(unit) {
30577
- if (unit.sourceType === "rule") {
30578
- const [ruleFile, ruleId] = unit.sourceId.split(":", 2);
30579
- if ((ruleFile === "dev-rules" || ruleFile === "review-guidelines") && ruleId) {
30580
- const rule = readRuleItems(ruleFile).find((item) => item.id === ruleId);
30581
- const priority = typeof rule?.priority === "string" ? rule.priority.toLowerCase() : "";
30582
- if (priority === "p0" || priority === "p1" || priority === "p2") {
30583
- return priority;
30584
- }
30585
- }
30586
- }
30587
- const text = `${unit.title} ${unit.summary} ${unit.tags.join(" ")}`.toLowerCase();
30588
- if (/(security|auth|token|secret|password|injection|xss|csrf|compliance|outage|data[- ]?loss)/.test(
30589
- text
30590
- )) {
30591
- return "p0";
30592
- }
30593
- if (/(crash|error|correct|reliab|timeout|retry|integrity)/.test(text)) {
30594
- return "p1";
30595
- }
30596
- return "p2";
30597
- }
30598
- function extractChangedFilesFromDiff(diffText) {
30599
- const files = /* @__PURE__ */ new Set();
30600
- const lines = diffText.split("\n");
30601
- for (const line of lines) {
30602
- if (!line.startsWith("diff --git ")) continue;
30603
- const parts = line.split(" ");
30604
- if (parts.length >= 4) {
30605
- const bPath = parts[3].replace(/^b\//, "");
30606
- if (bPath) files.add(bPath);
30607
- }
30608
- }
30609
- return Array.from(files);
30610
- }
30611
- function scoreUnitAgainstDiff(unit, diffText, changedFiles) {
30612
- const reasons = [];
30613
- let score = 0;
30614
- const corpus = `${unit.title} ${unit.summary}`.toLowerCase();
30615
- const diffLower = diffText.toLowerCase();
30616
- for (const tag of unit.tags) {
30617
- if (!tag) continue;
30618
- const tagLower = tag.toLowerCase();
30619
- if (diffLower.includes(tagLower)) {
30620
- score += 3;
30621
- reasons.push(`tag:${tag}`);
30622
- }
30623
- }
30624
- const keywords = corpus.split(/[^a-zA-Z0-9_-]+/).filter((token) => token.length >= 5).slice(0, 20);
30625
- for (const token of keywords) {
30626
- if (diffLower.includes(token)) {
30627
- score += 1;
30628
- reasons.push(`keyword:${token}`);
30629
- }
30630
- }
30631
- for (const filePath of changedFiles) {
30632
- const lower = filePath.toLowerCase();
30633
- if (corpus.includes("test") && lower.includes("test")) {
30634
- score += 1;
30635
- reasons.push("path:test");
30636
- }
30637
- if ((corpus.includes("api") || unit.tags.includes("api")) && (lower.includes("api") || lower.includes("route"))) {
30638
- score += 1;
30639
- reasons.push("path:api");
30640
- }
30641
- if ((corpus.includes("db") || corpus.includes("sql")) && (lower.includes("db") || lower.includes("prisma") || lower.includes("migration"))) {
30642
- score += 1;
30643
- reasons.push("path:db");
30644
- }
30645
- }
30646
- return { score, reasons: Array.from(new Set(reasons)) };
30647
- }
30648
30468
  var db = null;
30649
30469
  function getDb() {
30650
30470
  if (db) return db;
@@ -31069,6 +30889,14 @@ async function saveInteractions(claudeSessionId, mnemeSessionId) {
31069
30889
  ...interaction,
31070
30890
  id: `int-${String(idx + 1).padStart(3, "0")}`
31071
30891
  }));
30892
+ if (finalInteractions.length === 0) {
30893
+ return {
30894
+ success: true,
30895
+ savedCount: 0,
30896
+ mergedFromBackup: backupInteractions.length,
30897
+ message: "No interactions to save (transcript may have no text user messages). Existing data preserved."
30898
+ };
30899
+ }
31072
30900
  try {
31073
30901
  const deleteStmt = database.prepare(
31074
30902
  "DELETE FROM interactions WHERE session_id = ?"
@@ -31235,25 +31063,6 @@ function runSearchBenchmark(limit = SEARCH_EVAL_DEFAULT_LIMIT) {
31235
31063
  details
31236
31064
  };
31237
31065
  }
31238
- function buildUnitGraph(units) {
31239
- const approved = units.filter((unit) => unit.status === "approved");
31240
- const edges = [];
31241
- for (let i = 0; i < approved.length; i++) {
31242
- for (let j = i + 1; j < approved.length; j++) {
31243
- const shared = approved[i].tags.filter(
31244
- (tag) => approved[j].tags.includes(tag)
31245
- );
31246
- if (shared.length > 0) {
31247
- edges.push({
31248
- source: approved[i].id,
31249
- target: approved[j].id,
31250
- weight: shared.length
31251
- });
31252
- }
31253
- }
31254
- }
31255
- return { nodes: approved, edges };
31256
- }
31257
31066
  var server = new McpServer({
31258
31067
  name: "mneme-db",
31259
31068
  version: "0.1.0"
@@ -31378,120 +31187,139 @@ server.registerTool(
31378
31187
  }
31379
31188
  );
31380
31189
  server.registerTool(
31381
- "mneme_mark_session_committed",
31190
+ "mneme_update_session_summary",
31382
31191
  {
31383
- description: "Mark a session as committed (saved with /mneme:save). This prevents the session's interactions from being deleted on SessionEnd. Call this after successfully saving session data.",
31192
+ description: "Update session JSON file with summary data. MUST be called during /mneme:save Phase 3 to persist session metadata. Creates the session file if it does not exist (e.g. when SessionStart hook was skipped).",
31384
31193
  inputSchema: {
31385
- claudeSessionId: external_exports3.string().min(8).describe("Full Claude Code session UUID (36 chars)")
31194
+ claudeSessionId: external_exports3.string().min(8).describe("Full Claude Code session UUID (36 chars)"),
31195
+ title: external_exports3.string().describe("Session title"),
31196
+ summary: external_exports3.object({
31197
+ goal: external_exports3.string().describe("What the session aimed to accomplish"),
31198
+ outcome: external_exports3.string().describe("What was actually accomplished"),
31199
+ description: external_exports3.string().optional().describe("Detailed description of the session")
31200
+ }).describe("Session summary object"),
31201
+ tags: external_exports3.array(external_exports3.string()).optional().describe("Semantic tags for the session"),
31202
+ sessionType: external_exports3.string().optional().describe(
31203
+ "Session type (e.g. implementation, research, bugfix, refactor)"
31204
+ )
31386
31205
  }
31387
31206
  },
31388
- async ({ claudeSessionId }) => {
31207
+ async ({ claudeSessionId, title, summary, tags, sessionType }) => {
31389
31208
  if (!claudeSessionId.trim()) {
31390
31209
  return fail("claudeSessionId must not be empty.");
31391
31210
  }
31392
- const success2 = markSessionCommitted(claudeSessionId);
31393
- return {
31394
- ...ok(JSON.stringify({ success: success2, claudeSessionId }, null, 2)),
31395
- isError: !success2
31211
+ const projectPath = getProjectPath();
31212
+ const sessionsDir = path4.join(projectPath, ".mneme", "sessions");
31213
+ const shortId = claudeSessionId.slice(0, 8);
31214
+ let sessionFile = null;
31215
+ const searchDir = (dir) => {
31216
+ if (!fs4.existsSync(dir)) return null;
31217
+ for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
31218
+ const fullPath = path4.join(dir, entry.name);
31219
+ if (entry.isDirectory()) {
31220
+ const result = searchDir(fullPath);
31221
+ if (result) return result;
31222
+ } else if (entry.name === `${shortId}.json`) {
31223
+ return fullPath;
31224
+ }
31225
+ }
31226
+ return null;
31396
31227
  };
31397
- }
31398
- );
31399
- server.registerTool(
31400
- "mneme_unit_queue_list_pending",
31401
- {
31402
- description: "List pending units in the approval queue. Use this in save/review flows to surface actionable approvals.",
31403
- inputSchema: {
31404
- limit: external_exports3.number().int().min(LIST_LIMIT_MIN).max(UNIT_LIMIT_MAX).optional().describe(
31405
- `Maximum items (${LIST_LIMIT_MIN}-${UNIT_LIMIT_MAX}, default: 100)`
31406
- )
31407
- }
31408
- },
31409
- async ({ limit }) => {
31410
- const units = readUnits().items.filter((item) => item.status === "pending").sort(
31411
- (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
31412
- ).slice(0, limit ?? 100);
31413
- return ok(
31414
- JSON.stringify(
31415
- {
31416
- count: units.length,
31417
- items: units
31228
+ sessionFile = searchDir(sessionsDir);
31229
+ if (!sessionFile) {
31230
+ const now = /* @__PURE__ */ new Date();
31231
+ const yearMonth = path4.join(
31232
+ sessionsDir,
31233
+ String(now.getFullYear()),
31234
+ String(now.getMonth() + 1).padStart(2, "0")
31235
+ );
31236
+ if (!fs4.existsSync(yearMonth)) {
31237
+ fs4.mkdirSync(yearMonth, { recursive: true });
31238
+ }
31239
+ sessionFile = path4.join(yearMonth, `${shortId}.json`);
31240
+ const initial = {
31241
+ id: shortId,
31242
+ sessionId: claudeSessionId,
31243
+ createdAt: now.toISOString(),
31244
+ title: "",
31245
+ tags: [],
31246
+ context: {
31247
+ projectDir: projectPath,
31248
+ projectName: path4.basename(projectPath)
31418
31249
  },
31419
- null,
31420
- 2
31421
- )
31422
- );
31423
- }
31424
- );
31425
- server.registerTool(
31426
- "mneme_unit_queue_update_status",
31427
- {
31428
- description: "Update unit status (approve/reject/pending) in bulk or single item.",
31429
- inputSchema: {
31430
- unitIds: external_exports3.array(external_exports3.string().min(1)).min(1).describe("Target unit IDs"),
31431
- status: external_exports3.enum(["pending", "approved", "rejected"]).describe("New status"),
31432
- reviewedBy: external_exports3.string().optional().describe("Reviewer name (optional)")
31433
- }
31434
- },
31435
- async ({ unitIds, status, reviewedBy }) => {
31436
- const doc = readUnits();
31437
- const target = new Set(unitIds);
31438
- const now = (/* @__PURE__ */ new Date()).toISOString();
31439
- let updated = 0;
31440
- doc.items = doc.items.map((item) => {
31441
- if (!target.has(item.id)) return item;
31442
- updated += 1;
31443
- return {
31444
- ...item,
31445
- status,
31446
- reviewedAt: now,
31447
- reviewedBy,
31448
- updatedAt: now
31250
+ metrics: {
31251
+ userMessages: 0,
31252
+ assistantResponses: 0,
31253
+ thinkingBlocks: 0,
31254
+ toolUsage: []
31255
+ },
31256
+ files: [],
31257
+ status: null
31449
31258
  };
31450
- });
31451
- doc.updatedAt = now;
31452
- writeUnits(doc);
31453
- return ok(
31454
- JSON.stringify(
31455
- { updated, status, requested: unitIds.length, updatedAt: now },
31456
- null,
31457
- 2
31458
- )
31459
- );
31460
- }
31461
- );
31462
- server.registerTool(
31463
- "mneme_unit_apply_suggest_for_diff",
31464
- {
31465
- description: "Suggest top approved units for a given git diff text. Intended for automatic review integration.",
31466
- inputSchema: {
31467
- diff: external_exports3.string().min(1).describe("Unified diff text"),
31468
- limit: external_exports3.number().int().min(LIST_LIMIT_MIN).max(50).optional().describe("Maximum suggested units (default: 10)")
31259
+ fs4.writeFileSync(sessionFile, JSON.stringify(initial, null, 2));
31260
+ }
31261
+ const data = readJsonFile(sessionFile) ?? {};
31262
+ data.title = title;
31263
+ data.summary = summary;
31264
+ data.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
31265
+ if (tags) data.tags = tags;
31266
+ if (sessionType) data.sessionType = sessionType;
31267
+ const transcriptPath = getTranscriptPath(claudeSessionId);
31268
+ if (transcriptPath) {
31269
+ try {
31270
+ const parsed = await parseTranscript(transcriptPath);
31271
+ data.metrics = {
31272
+ userMessages: parsed.metrics.userMessages,
31273
+ assistantResponses: parsed.metrics.assistantResponses,
31274
+ thinkingBlocks: parsed.metrics.thinkingBlocks,
31275
+ toolUsage: parsed.toolUsage
31276
+ };
31277
+ if (parsed.files.length > 0) {
31278
+ data.files = parsed.files;
31279
+ }
31280
+ } catch {
31281
+ }
31469
31282
  }
31470
- },
31471
- async ({ diff, limit }) => {
31472
- const changedFiles = extractChangedFilesFromDiff(diff);
31473
- const approved = readUnits().items.filter(
31474
- (item) => item.status === "approved"
31475
- );
31476
- const scored = approved.map((unit) => {
31477
- const { score, reasons } = scoreUnitAgainstDiff(
31478
- unit,
31479
- diff,
31480
- changedFiles
31481
- );
31482
- return { unit, score, reasons };
31483
- }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).slice(0, limit ?? 10);
31283
+ const ctx = data.context;
31284
+ if (ctx && !ctx.repository) {
31285
+ try {
31286
+ const { execSync } = await import("node:child_process");
31287
+ const cwd = ctx.projectDir || projectPath;
31288
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
31289
+ encoding: "utf8",
31290
+ cwd
31291
+ }).trim();
31292
+ if (branch) ctx.branch = branch;
31293
+ const remoteUrl = execSync("git remote get-url origin", {
31294
+ encoding: "utf8",
31295
+ cwd
31296
+ }).trim();
31297
+ const repoMatch = remoteUrl.match(/[:/]([^/]+\/[^/]+?)(\.git)?$/);
31298
+ if (repoMatch) ctx.repository = repoMatch[1].replace(/\.git$/, "");
31299
+ const userName = execSync("git config user.name", {
31300
+ encoding: "utf8",
31301
+ cwd
31302
+ }).trim();
31303
+ const userEmail = execSync("git config user.email", {
31304
+ encoding: "utf8",
31305
+ cwd
31306
+ }).trim();
31307
+ if (userName)
31308
+ ctx.user = {
31309
+ name: userName,
31310
+ ...userEmail ? { email: userEmail } : {}
31311
+ };
31312
+ } catch {
31313
+ }
31314
+ }
31315
+ fs4.writeFileSync(sessionFile, JSON.stringify(data, null, 2));
31316
+ markSessionCommitted(claudeSessionId);
31484
31317
  return ok(
31485
31318
  JSON.stringify(
31486
31319
  {
31487
- changedFiles,
31488
- suggestions: scored.map((item) => ({
31489
- id: item.unit.id,
31490
- title: item.unit.title,
31491
- score: item.score,
31492
- reasons: item.reasons,
31493
- source: `${item.unit.sourceType}:${item.unit.sourceId}`
31494
- }))
31320
+ success: true,
31321
+ sessionFile: sessionFile.replace(projectPath, "."),
31322
+ shortId
31495
31323
  },
31496
31324
  null,
31497
31325
  2
@@ -31500,33 +31328,22 @@ server.registerTool(
31500
31328
  }
31501
31329
  );
31502
31330
  server.registerTool(
31503
- "mneme_unit_apply_explain_match",
31331
+ "mneme_mark_session_committed",
31504
31332
  {
31505
- description: "Explain why a specific unit matches a diff.",
31333
+ description: "Mark a session as committed (saved with /mneme:save). This prevents the session's interactions from being deleted on SessionEnd. Call this after successfully saving session data.",
31506
31334
  inputSchema: {
31507
- unitId: external_exports3.string().min(1).describe("Unit ID"),
31508
- diff: external_exports3.string().min(1).describe("Unified diff text")
31335
+ claudeSessionId: external_exports3.string().min(8).describe("Full Claude Code session UUID (36 chars)")
31509
31336
  }
31510
31337
  },
31511
- async ({ unitId, diff }) => {
31512
- const unit = readUnits().items.find((item) => item.id === unitId);
31513
- if (!unit) return fail(`Unit not found: ${unitId}`);
31514
- const changedFiles = extractChangedFilesFromDiff(diff);
31515
- const scored = scoreUnitAgainstDiff(unit, diff, changedFiles);
31516
- return ok(
31517
- JSON.stringify(
31518
- {
31519
- unitId,
31520
- title: unit.title,
31521
- score: scored.score,
31522
- reasons: scored.reasons,
31523
- priority: inferUnitPriority(unit),
31524
- changedFiles
31525
- },
31526
- null,
31527
- 2
31528
- )
31529
- );
31338
+ async ({ claudeSessionId }) => {
31339
+ if (!claudeSessionId.trim()) {
31340
+ return fail("claudeSessionId must not be empty.");
31341
+ }
31342
+ const success2 = markSessionCommitted(claudeSessionId);
31343
+ return {
31344
+ ...ok(JSON.stringify({ success: success2, claudeSessionId }, null, 2)),
31345
+ isError: !success2
31346
+ };
31530
31347
  }
31531
31348
  );
31532
31349
  server.registerTool(
@@ -31688,61 +31505,6 @@ server.registerTool(
31688
31505
  );
31689
31506
  }
31690
31507
  );
31691
- server.registerTool(
31692
- "mneme_graph_insights",
31693
- {
31694
- description: "Compute graph insights from approved units: central units, tag communities, orphan units.",
31695
- inputSchema: {
31696
- limit: external_exports3.number().int().min(LIST_LIMIT_MIN).max(100).optional().describe("Limit for ranked outputs (default: 10)")
31697
- }
31698
- },
31699
- async ({ limit }) => {
31700
- const k = limit ?? 10;
31701
- const units = readUnits().items.filter(
31702
- (item) => item.status === "approved"
31703
- );
31704
- const graph = buildUnitGraph(units);
31705
- const degree = /* @__PURE__ */ new Map();
31706
- for (const unit of graph.nodes) degree.set(unit.id, 0);
31707
- for (const edge of graph.edges) {
31708
- degree.set(edge.source, (degree.get(edge.source) || 0) + 1);
31709
- degree.set(edge.target, (degree.get(edge.target) || 0) + 1);
31710
- }
31711
- const topCentral = graph.nodes.map((unit) => ({
31712
- id: unit.id,
31713
- title: unit.title,
31714
- degree: degree.get(unit.id) || 0
31715
- })).sort((a, b) => b.degree - a.degree).slice(0, k);
31716
- const tagCounts = /* @__PURE__ */ new Map();
31717
- for (const unit of graph.nodes) {
31718
- for (const tag of unit.tags) {
31719
- tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
31720
- }
31721
- }
31722
- const communities = Array.from(tagCounts.entries()).map(([tag, count]) => ({
31723
- tag,
31724
- count
31725
- })).sort((a, b) => b.count - a.count).slice(0, k);
31726
- const orphans = graph.nodes.filter((unit) => (degree.get(unit.id) || 0) === 0).map((unit) => ({
31727
- id: unit.id,
31728
- title: unit.title,
31729
- tags: unit.tags
31730
- })).slice(0, k);
31731
- return ok(
31732
- JSON.stringify(
31733
- {
31734
- approvedUnits: graph.nodes.length,
31735
- edges: graph.edges.length,
31736
- topCentral,
31737
- tagCommunities: communities,
31738
- orphanUnits: orphans
31739
- },
31740
- null,
31741
- 2
31742
- )
31743
- );
31744
- }
31745
- );
31746
31508
  server.registerTool(
31747
31509
  "mneme_search_eval",
31748
31510
  {
@@ -31778,64 +31540,6 @@ server.registerTool(
31778
31540
  return ok(JSON.stringify(payload, null, 2));
31779
31541
  }
31780
31542
  );
31781
- server.registerTool(
31782
- "mneme_audit_query",
31783
- {
31784
- description: "Query unit-related audit logs and summarize change history.",
31785
- inputSchema: {
31786
- from: external_exports3.string().optional().describe("Start ISO date/time"),
31787
- to: external_exports3.string().optional().describe("End ISO date/time"),
31788
- targetId: external_exports3.string().optional().describe("Filter by target unit ID"),
31789
- summaryMode: external_exports3.enum(["changes", "actors", "target"]).optional().describe(
31790
- "changes=list, actors=aggregate by actor, target=single target history"
31791
- )
31792
- }
31793
- },
31794
- async ({ from, to, targetId, summaryMode }) => {
31795
- const entries = readAuditEntries({ from, to, entity: "unit" }).filter(
31796
- (entry) => targetId ? entry.targetId === targetId : true
31797
- );
31798
- if ((summaryMode || "changes") === "actors") {
31799
- const byActor = /* @__PURE__ */ new Map();
31800
- for (const entry of entries) {
31801
- const actor = entry.actor || "unknown";
31802
- byActor.set(actor, (byActor.get(actor) || 0) + 1);
31803
- }
31804
- return ok(
31805
- JSON.stringify(
31806
- {
31807
- total: entries.length,
31808
- actors: Array.from(byActor.entries()).map(([actor, count]) => ({ actor, count })).sort((a, b) => b.count - a.count)
31809
- },
31810
- null,
31811
- 2
31812
- )
31813
- );
31814
- }
31815
- if ((summaryMode || "changes") === "target" && targetId) {
31816
- return ok(
31817
- JSON.stringify(
31818
- {
31819
- targetId,
31820
- history: entries
31821
- },
31822
- null,
31823
- 2
31824
- )
31825
- );
31826
- }
31827
- return ok(
31828
- JSON.stringify(
31829
- {
31830
- total: entries.length,
31831
- changes: entries
31832
- },
31833
- null,
31834
- 2
31835
- )
31836
- );
31837
- }
31838
- );
31839
31543
  async function main() {
31840
31544
  const transport = new StdioServerTransport();
31841
31545
  await server.connect(transport);