memory-journal-mcp 7.2.0 → 7.4.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.
@@ -1,5 +1,5 @@
1
- import { withSessionInit, withPriority, ASSISTANT_FOCUSED, TOOL_GROUPS, HIGH_PRIORITY, LOW_PRIORITY, MEDIUM_PRIORITY, setDefaultSandboxMode, initializeAuditLogger, parseToolFilter, getFilterSummary, getToolFilterFromEnv, getTools, getEnabledGroups, callTool, getGlobalAuditLogger, sendProgress, SUPPORTED_SCOPES, getRequiredScope, hasScope, getAuditResourceDef, execQuery, transformEntryRow, resolveGitHubRepo, isResourceError, milestoneCompletionPct, parseScopes, BASE_SCOPES, getAllToolNames, globalMetrics, DEFAULT_BRIEFING_CONFIG } from './chunk-ORV7ZZOE.js';
2
- import { logger, GitHubIntegration, ConfigurationError, ResourceNotFoundError, ConnectionError, QueryError, assertNoPathTraversal, ValidationError, MemoryJournalMcpError, validateDateFormatPattern } from './chunk-IWKLHSPU.js';
1
+ import { withSessionInit, withPriority, ASSISTANT_FOCUSED, TOOL_GROUPS, HIGH_PRIORITY, LOW_PRIORITY, MEDIUM_PRIORITY, setDefaultSandboxMode, initializeAuditLogger, parseToolFilter, getFilterSummary, getToolFilterFromEnv, getTools, getEnabledGroups, callTool, getGlobalAuditLogger, sendProgress, SUPPORTED_SCOPES, getRequiredScope, hasScope, getAuditResourceDef, execQuery, transformEntryRow, resolveGitHubRepo, isResourceError, milestoneCompletionPct, parseScopes, BASE_SCOPES, getAllToolNames, globalMetrics, DEFAULT_BRIEFING_CONFIG } from './chunk-P5V2VY6N.js';
2
+ import { logger, GitHubIntegration, ConfigurationError, ResourceNotFoundError, ConnectionError, QueryError, assertNoPathTraversal, ValidationError, MemoryJournalMcpError, validateDateFormatPattern } from './chunk-WXDEVIFL.js';
3
3
  import { createRequire } from 'module';
4
4
  import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
5
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -99,6 +99,17 @@ CREATE INDEX IF NOT EXISTS idx_relationships_to ON relationships(to_entry_id);
99
99
  -- Composite covering index for getRecentEntries (WHERE deleted_at IS NULL ORDER BY timestamp DESC, id DESC)
100
100
  CREATE INDEX IF NOT EXISTS idx_memory_journal_recent ON memory_journal(deleted_at, timestamp DESC, id DESC);
101
101
 
102
+ -- Analytics snapshots for persisted digest data across server restarts
103
+ CREATE TABLE IF NOT EXISTS analytics_snapshots (
104
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
105
+ snapshot_type TEXT NOT NULL DEFAULT 'digest',
106
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
107
+ data TEXT NOT NULL
108
+ );
109
+
110
+ CREATE INDEX IF NOT EXISTS idx_analytics_snapshots_type_created
111
+ ON analytics_snapshots(snapshot_type, created_at DESC);
112
+
102
113
  -- FTS5 full-text search index (content-sync: reads from source table, no duplicate storage)
103
114
  CREATE VIRTUAL TABLE IF NOT EXISTS fts_content USING fts5(
104
115
  content,
@@ -481,12 +492,16 @@ function rowsToEntries(tagsMgr, rows) {
481
492
  if (rows.length === 0) return [];
482
493
  const entries = rows.map((r) => {
483
494
  const p = r;
484
- return {
485
- ...p,
495
+ const { importanceScore, ...rest } = p;
496
+ const entry = {
497
+ ...rest,
486
498
  isPersonal: Boolean(p.isPersonal),
487
499
  // SQLite uses 0/1
488
- tags: []
500
+ tags: [],
501
+ // Attach importanceScore if the query computed it (importance-sorted results)
502
+ ...importanceScore !== void 0 ? { importanceScore: Math.round(importanceScore * 100) / 100 } : {}
489
503
  };
504
+ return entry;
490
505
  });
491
506
  const ids = entries.map((e) => e.id);
492
507
  const tagsMap = tagsMgr.batchGetTagsForEntries(ids);
@@ -500,6 +515,12 @@ function rowToObject(row) {
500
515
  if (typeof row !== "object") return void 0;
501
516
  return row;
502
517
  }
518
+ function queryRow(db, sql, ...params) {
519
+ return db.prepare(sql).get(...params);
520
+ }
521
+ function queryRows(db, sql, ...params) {
522
+ return db.prepare(sql).all(...params);
523
+ }
503
524
 
504
525
  // src/database/sqlite-adapter/entries/crud.ts
505
526
  function createEntry(context, input) {
@@ -671,9 +692,92 @@ function deleteEntry(context, id, permanent = false) {
671
692
  return result.changes > 0;
672
693
  }
673
694
 
695
+ // src/database/sqlite-adapter/entries/importance.ts
696
+ var IMPORTANCE_WEIGHTS = {
697
+ significance: 0.3,
698
+ relationships: 0.35,
699
+ causal: 0.2,
700
+ recency: 0.15
701
+ };
702
+ var MAX_RELATIONSHIP_SCORE_AT = 5;
703
+ var MAX_CAUSAL_SCORE_AT = 3;
704
+ var RECENCY_WINDOW_DAYS = 90;
705
+ function buildImportanceSqlExpression() {
706
+ const w = IMPORTANCE_WEIGHTS;
707
+ return `(
708
+ CASE WHEN e.significance_type IS NOT NULL THEN ${String(w.significance)} ELSE 0.0 END
709
+ + MIN(
710
+ COALESCE((SELECT COUNT(*) FROM relationships
711
+ WHERE from_entry_id = e.id OR to_entry_id = e.id), 0) * 1.0 / ${String(MAX_RELATIONSHIP_SCORE_AT)},
712
+ 1.0
713
+ ) * ${String(w.relationships)}
714
+ + MIN(
715
+ COALESCE((SELECT COUNT(*) FROM relationships
716
+ WHERE (from_entry_id = e.id OR to_entry_id = e.id)
717
+ AND relationship_type IN ('blocked_by', 'resolved', 'caused')), 0) * 1.0 / ${String(MAX_CAUSAL_SCORE_AT)},
718
+ 1.0
719
+ ) * ${String(w.causal)}
720
+ + MAX(0, 1.0 - (julianday('now') - julianday(e.timestamp)) / ${String(RECENCY_WINDOW_DAYS)}.0) * ${String(w.recency)}
721
+ )`;
722
+ }
723
+ function calculateImportance(context, entryId) {
724
+ const { db } = context;
725
+ const round2 = (n) => Math.round(n * 100) / 100;
726
+ const stmt = db.prepare(`SELECT
727
+ m.significance_type as significanceType,
728
+ m.timestamp,
729
+ (SELECT COUNT(*) FROM relationships
730
+ WHERE from_entry_id = ? OR to_entry_id = ?) AS rel_count,
731
+ (SELECT COUNT(*) FROM relationships
732
+ WHERE (from_entry_id = ? OR to_entry_id = ?)
733
+ AND relationship_type IN ('blocked_by', 'resolved', 'caused')) AS causal_count
734
+ FROM memory_journal m
735
+ WHERE m.id = ? AND m.deleted_at IS NULL`);
736
+ const row = rowToObject(stmt.get(entryId, entryId, entryId, entryId, entryId));
737
+ if (!row) {
738
+ return {
739
+ score: 0,
740
+ breakdown: { significance: 0, relationships: 0, causal: 0, recency: 0 }
741
+ };
742
+ }
743
+ const significanceType = row["significanceType"];
744
+ const timestamp = row["timestamp"];
745
+ const relCount = row["rel_count"] ?? 0;
746
+ const causalCount = row["causal_count"] ?? 0;
747
+ const significanceRaw = significanceType ? 1 : 0;
748
+ const relationshipsRaw = Math.min(relCount / MAX_RELATIONSHIP_SCORE_AT, 1);
749
+ const causalRaw = Math.min(causalCount / MAX_CAUSAL_SCORE_AT, 1);
750
+ const entryDate = new Date(timestamp);
751
+ const now = /* @__PURE__ */ new Date();
752
+ const daysSince = Math.floor((now.getTime() - entryDate.getTime()) / (1e3 * 60 * 60 * 24));
753
+ const recencyRaw = Math.max(0, 1 - daysSince / RECENCY_WINDOW_DAYS);
754
+ const w = IMPORTANCE_WEIGHTS;
755
+ const breakdown = {
756
+ significance: round2(significanceRaw * w.significance),
757
+ relationships: round2(relationshipsRaw * w.relationships),
758
+ causal: round2(causalRaw * w.causal),
759
+ recency: round2(recencyRaw * w.recency)
760
+ };
761
+ const score = round2(
762
+ significanceRaw * w.significance + relationshipsRaw * w.relationships + causalRaw * w.causal + recencyRaw * w.recency
763
+ );
764
+ return { score, breakdown };
765
+ }
766
+
674
767
  // src/database/sqlite-adapter/entries/search.ts
675
- function getRecentEntries(context, limit) {
768
+ function getRecentEntries(context, limit, sortBy = "timestamp") {
676
769
  const { db, tagsMgr } = context;
770
+ if (sortBy === "importance") {
771
+ const importanceExpr = buildImportanceSqlExpression();
772
+ const stmt2 = db.prepare(`
773
+ SELECT ${ALIASED_ENTRY_COLUMNS}, ${importanceExpr} AS importanceScore
774
+ FROM memory_journal e
775
+ WHERE e.deleted_at IS NULL
776
+ ORDER BY importanceScore DESC, e.timestamp DESC, e.id DESC LIMIT ?
777
+ `);
778
+ const rows2 = stmt2.all([limit]);
779
+ return rowsToEntries(tagsMgr, rows2);
780
+ }
677
781
  const stmt = db.prepare(`
678
782
  SELECT ${ENTRY_COLUMNS} FROM memory_journal
679
783
  WHERE deleted_at IS NULL
@@ -712,15 +816,17 @@ function searchEntries(context, queryStr, options) {
712
816
  }
713
817
  function buildSearchQuery(queryStr, options, useFts) {
714
818
  let query;
819
+ const useImportance = options?.sortBy === "importance";
820
+ const importanceCol = useImportance ? `, ${buildImportanceSqlExpression()} AS importanceScore` : "";
715
821
  if (useFts) {
716
822
  query = `
717
- SELECT DISTINCT ${ALIASED_ENTRY_COLUMNS}
823
+ SELECT DISTINCT ${ALIASED_ENTRY_COLUMNS}${importanceCol}
718
824
  FROM memory_journal e
719
825
  JOIN fts_content fts ON fts.rowid = e.id
720
826
  `;
721
827
  } else {
722
828
  query = `
723
- SELECT DISTINCT ${ALIASED_ENTRY_COLUMNS}
829
+ SELECT DISTINCT ${ALIASED_ENTRY_COLUMNS}${importanceCol}
724
830
  FROM memory_journal e
725
831
  `;
726
832
  }
@@ -789,7 +895,9 @@ function buildSearchQuery(queryStr, options, useFts) {
789
895
  if (conditions.length > 0) {
790
896
  query += ` WHERE ${conditions.join(" AND ")}`;
791
897
  }
792
- if (useFts) {
898
+ if (useImportance) {
899
+ query += ` ORDER BY importanceScore DESC, e.timestamp DESC, e.id DESC`;
900
+ } else if (useFts) {
793
901
  query += ` ORDER BY rank, e.timestamp DESC, e.id DESC`;
794
902
  } else {
795
903
  query += ` ORDER BY e.timestamp DESC, e.id DESC`;
@@ -810,8 +918,10 @@ function searchByDateRange(context, startDate, endDate, options) {
810
918
  if (!end.includes("T")) end += "T23:59:59.999Z";
811
919
  params.push(end);
812
920
  }
921
+ const useImportance = options?.sortBy === "importance";
922
+ const importanceCol = useImportance ? `, ${buildImportanceSqlExpression()} AS importanceScore` : "";
813
923
  let query = `
814
- SELECT DISTINCT ${ALIASED_ENTRY_COLUMNS} FROM memory_journal e
924
+ SELECT DISTINCT ${ALIASED_ENTRY_COLUMNS}${importanceCol} FROM memory_journal e
815
925
  `;
816
926
  if (options?.tags && options.tags.length > 0) {
817
927
  query += `
@@ -846,7 +956,11 @@ function searchByDateRange(context, startDate, endDate, options) {
846
956
  conditions.push(`e.workflow_run_id = ?`);
847
957
  params.push(options.workflowRunId);
848
958
  }
849
- query += ` WHERE ${conditions.join(" AND ")} ORDER BY e.timestamp DESC, e.id DESC`;
959
+ if (useImportance) {
960
+ query += ` WHERE ${conditions.join(" AND ")} ORDER BY importanceScore DESC, e.timestamp DESC, e.id DESC`;
961
+ } else {
962
+ query += ` WHERE ${conditions.join(" AND ")} ORDER BY e.timestamp DESC, e.id DESC`;
963
+ }
850
964
  query += ` LIMIT ?`;
851
965
  params.push(options?.limit ?? 500);
852
966
  const stmt = db.prepare(query);
@@ -866,60 +980,6 @@ function sanitizeFtsQuery(query) {
866
980
  return query;
867
981
  }
868
982
 
869
- // src/database/sqlite-adapter/entries/importance.ts
870
- var IMPORTANCE_WEIGHTS = {
871
- significance: 0.3,
872
- relationships: 0.35,
873
- causal: 0.2,
874
- recency: 0.15
875
- };
876
- var MAX_RELATIONSHIP_SCORE_AT = 5;
877
- var MAX_CAUSAL_SCORE_AT = 3;
878
- var RECENCY_WINDOW_DAYS = 90;
879
- function calculateImportance(context, entryId) {
880
- const { db } = context;
881
- const round2 = (n) => Math.round(n * 100) / 100;
882
- const stmt = db.prepare(`SELECT
883
- m.significance_type as significanceType,
884
- m.timestamp,
885
- (SELECT COUNT(*) FROM relationships
886
- WHERE from_entry_id = ? OR to_entry_id = ?) AS rel_count,
887
- (SELECT COUNT(*) FROM relationships
888
- WHERE (from_entry_id = ? OR to_entry_id = ?)
889
- AND relationship_type IN ('blocked_by', 'resolved', 'caused')) AS causal_count
890
- FROM memory_journal m
891
- WHERE m.id = ? AND m.deleted_at IS NULL`);
892
- const row = rowToObject(stmt.get(entryId, entryId, entryId, entryId, entryId));
893
- if (!row) {
894
- return {
895
- score: 0,
896
- breakdown: { significance: 0, relationships: 0, causal: 0, recency: 0 }
897
- };
898
- }
899
- const significanceType = row["significanceType"];
900
- const timestamp = row["timestamp"];
901
- const relCount = row["rel_count"] ?? 0;
902
- const causalCount = row["causal_count"] ?? 0;
903
- const significanceRaw = significanceType ? 1 : 0;
904
- const relationshipsRaw = Math.min(relCount / MAX_RELATIONSHIP_SCORE_AT, 1);
905
- const causalRaw = Math.min(causalCount / MAX_CAUSAL_SCORE_AT, 1);
906
- const entryDate = new Date(timestamp);
907
- const now = /* @__PURE__ */ new Date();
908
- const daysSince = Math.floor((now.getTime() - entryDate.getTime()) / (1e3 * 60 * 60 * 24));
909
- const recencyRaw = Math.max(0, 1 - daysSince / RECENCY_WINDOW_DAYS);
910
- const w = IMPORTANCE_WEIGHTS;
911
- const breakdown = {
912
- significance: round2(significanceRaw * w.significance),
913
- relationships: round2(relationshipsRaw * w.relationships),
914
- causal: round2(causalRaw * w.causal),
915
- recency: round2(recencyRaw * w.recency)
916
- };
917
- const score = round2(
918
- significanceRaw * w.significance + relationshipsRaw * w.relationships + causalRaw * w.causal + recencyRaw * w.recency
919
- );
920
- return { score, breakdown };
921
- }
922
-
923
983
  // src/database/sqlite-adapter/entries/statistics.ts
924
984
  var MAX_PERIOD_ROWS = 52;
925
985
  function getStatistics(context, groupBy = "week", startDate, endDate, projectBreakdown) {
@@ -1068,11 +1128,11 @@ var EntriesManager = class {
1068
1128
  deleteEntry(id, permanent = false) {
1069
1129
  return deleteEntry(this.sharedContext, id, permanent);
1070
1130
  }
1071
- getRecentEntries(limit = 10, isPersonal) {
1131
+ getRecentEntries(limit = 10, isPersonal, sortBy) {
1072
1132
  if (isPersonal !== void 0) {
1073
- return searchEntries(this.sharedContext, "", { limit, isPersonal });
1133
+ return searchEntries(this.sharedContext, "", { limit, isPersonal, sortBy });
1074
1134
  }
1075
- return getRecentEntries(this.sharedContext, limit);
1135
+ return getRecentEntries(this.sharedContext, limit, sortBy);
1076
1136
  }
1077
1137
  getEntriesPage(offset, limit, order = "desc") {
1078
1138
  return getEntriesPage(this.sharedContext, offset, limit, order);
@@ -1242,6 +1302,143 @@ var BackupManager = class {
1242
1302
  return { restoredFrom: filename, previousEntryCount, newEntryCount };
1243
1303
  }
1244
1304
  };
1305
+
1306
+ // src/database/sqlite-adapter/entries/digest.ts
1307
+ var STALE_PROJECT_THRESHOLD_DAYS = 14;
1308
+ var PREVIEW_LENGTH = 80;
1309
+ var TOP_IMPORTANCE_COUNT = 3;
1310
+ function computeDigest(db) {
1311
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1312
+ const activityRow = queryRow(
1313
+ db,
1314
+ `SELECT
1315
+ COALESCE(SUM(CASE WHEN strftime('%Y-%m', timestamp) = strftime('%Y-%m', 'now') THEN 1 ELSE 0 END), 0) AS current_count,
1316
+ COALESCE(SUM(CASE WHEN strftime('%Y-%m', timestamp) = strftime('%Y-%m', 'now', '-1 month') THEN 1 ELSE 0 END), 0) AS previous_count
1317
+ FROM memory_journal
1318
+ WHERE deleted_at IS NULL`
1319
+ );
1320
+ const currentPeriodEntries = activityRow?.["current_count"] ?? 0;
1321
+ const previousPeriodEntries = activityRow?.["previous_count"] ?? 0;
1322
+ const activityGrowthPercent = previousPeriodEntries > 0 ? Math.round(
1323
+ (currentPeriodEntries - previousPeriodEntries) / previousPeriodEntries * 100
1324
+ ) : null;
1325
+ const sigRow = queryRow(
1326
+ db,
1327
+ `SELECT
1328
+ COALESCE(SUM(CASE
1329
+ WHEN significance_type IS NOT NULL AND strftime('%Y-%m', timestamp) = strftime('%Y-%m', 'now')
1330
+ THEN 1 ELSE 0 END), 0) AS current_significant,
1331
+ COUNT(DISTINCT strftime('%Y-%m', timestamp)) AS total_periods,
1332
+ COALESCE(SUM(CASE WHEN significance_type IS NOT NULL THEN 1 ELSE 0 END), 0) AS total_significant
1333
+ FROM memory_journal
1334
+ WHERE deleted_at IS NULL`
1335
+ );
1336
+ const currentPeriodSignificant = sigRow?.["current_significant"] ?? 0;
1337
+ const totalPeriods = sigRow?.["total_periods"] ?? 1;
1338
+ const totalSignificant = sigRow?.["total_significant"] ?? 0;
1339
+ const historicalAvgSignificant = totalPeriods > 0 ? Math.round(totalSignificant / totalPeriods * 10) / 10 : 0;
1340
+ const significanceMultiplier = historicalAvgSignificant > 0 ? Math.round(currentPeriodSignificant / historicalAvgSignificant * 10) / 10 : null;
1341
+ const staleRows = queryRows(
1342
+ db,
1343
+ `SELECT
1344
+ project_number,
1345
+ MAX(timestamp) AS last_entry_date,
1346
+ CAST(julianday('now') - julianday(MAX(timestamp)) AS INTEGER) AS days_silent
1347
+ FROM memory_journal
1348
+ WHERE deleted_at IS NULL AND project_number IS NOT NULL
1349
+ GROUP BY project_number
1350
+ HAVING days_silent > ?`,
1351
+ STALE_PROJECT_THRESHOLD_DAYS
1352
+ );
1353
+ const staleProjects = staleRows.map((r) => ({
1354
+ projectNumber: r["project_number"],
1355
+ lastEntryDate: r["last_entry_date"],
1356
+ daysSilent: r["days_silent"]
1357
+ }));
1358
+ const relDensityRow = queryRow(
1359
+ db,
1360
+ `SELECT
1361
+ COALESCE((SELECT COUNT(*) FROM relationships r
1362
+ JOIN memory_journal m ON r.from_entry_id = m.id
1363
+ WHERE strftime('%Y-%m', m.timestamp) = strftime('%Y-%m', 'now')
1364
+ ), 0) AS current_rels,
1365
+ COALESCE((SELECT COUNT(*) FROM relationships r
1366
+ JOIN memory_journal m ON r.from_entry_id = m.id
1367
+ WHERE strftime('%Y-%m', m.timestamp) = strftime('%Y-%m', 'now', '-1 month')
1368
+ ), 0) AS previous_rels`
1369
+ );
1370
+ const currentRels = relDensityRow?.["current_rels"] ?? 0;
1371
+ const previousRels = relDensityRow?.["previous_rels"] ?? 0;
1372
+ const currentRelDensity = currentPeriodEntries > 0 ? Math.round(currentRels / currentPeriodEntries * 100) / 100 : 0;
1373
+ const previousRelDensity = previousPeriodEntries > 0 ? Math.round(previousRels / previousPeriodEntries * 100) / 100 : 0;
1374
+ const importanceExpr = buildImportanceSqlExpression();
1375
+ const topRows = queryRows(
1376
+ db,
1377
+ `SELECT
1378
+ e.id,
1379
+ ${importanceExpr} AS importance_score,
1380
+ SUBSTR(e.content, 1, ${String(PREVIEW_LENGTH)}) AS preview
1381
+ FROM memory_journal e
1382
+ WHERE e.deleted_at IS NULL
1383
+ ORDER BY importance_score DESC
1384
+ LIMIT ${String(TOP_IMPORTANCE_COUNT)}`
1385
+ );
1386
+ const topImportanceEntries = topRows.map((r) => ({
1387
+ id: r["id"],
1388
+ score: Math.round(r["importance_score"] * 100) / 100,
1389
+ preview: r["preview"]
1390
+ }));
1391
+ return {
1392
+ computedAt: now,
1393
+ currentPeriodEntries,
1394
+ previousPeriodEntries,
1395
+ activityGrowthPercent,
1396
+ currentPeriodSignificant,
1397
+ historicalAvgSignificant,
1398
+ significanceMultiplier,
1399
+ staleProjects,
1400
+ currentRelDensity,
1401
+ previousRelDensity,
1402
+ topImportanceEntries
1403
+ };
1404
+ }
1405
+ function saveAnalyticsSnapshot(db, type, data) {
1406
+ const stmt = db.prepare(`INSERT INTO analytics_snapshots (snapshot_type, data) VALUES (?, ?)`);
1407
+ const result = stmt.run(type, JSON.stringify(data));
1408
+ return Number(result.lastInsertRowid);
1409
+ }
1410
+ function getLatestAnalyticsSnapshot(db, type) {
1411
+ const row = queryRow(
1412
+ db,
1413
+ `SELECT id, created_at, data FROM analytics_snapshots
1414
+ WHERE snapshot_type = ?
1415
+ ORDER BY created_at DESC
1416
+ LIMIT 1`,
1417
+ type
1418
+ );
1419
+ if (!row) return null;
1420
+ return {
1421
+ id: row["id"],
1422
+ createdAt: row["created_at"],
1423
+ data: JSON.parse(row["data"])
1424
+ };
1425
+ }
1426
+ function getAnalyticsSnapshots(db, type, limit = 10) {
1427
+ const rows = queryRows(
1428
+ db,
1429
+ `SELECT id, created_at, data FROM analytics_snapshots
1430
+ WHERE snapshot_type = ?
1431
+ ORDER BY created_at DESC
1432
+ LIMIT ?`,
1433
+ type,
1434
+ limit
1435
+ );
1436
+ return rows.map((r) => ({
1437
+ id: r["id"],
1438
+ createdAt: r["created_at"],
1439
+ data: JSON.parse(r["data"])
1440
+ }));
1441
+ }
1245
1442
  var DatabaseAdapter2 = class {
1246
1443
  connection;
1247
1444
  tagsMgr;
@@ -1282,8 +1479,8 @@ var DatabaseAdapter2 = class {
1282
1479
  calculateImportance(entryId) {
1283
1480
  return this.entriesMgr.calculateImportance(entryId);
1284
1481
  }
1285
- getRecentEntries(limit, isPersonal) {
1286
- return this.entriesMgr.getRecentEntries(limit ?? 10, isPersonal);
1482
+ getRecentEntries(limit, isPersonal, sortBy) {
1483
+ return this.entriesMgr.getRecentEntries(limit ?? 10, isPersonal, sortBy);
1287
1484
  }
1288
1485
  getEntriesPage(offset, limit) {
1289
1486
  return this.entriesMgr.getEntriesPage(offset, limit);
@@ -1392,6 +1589,15 @@ var DatabaseAdapter2 = class {
1392
1589
  executeRawQuery(sql, params) {
1393
1590
  return this.connection.exec(sql, params);
1394
1591
  }
1592
+ saveAnalyticsSnapshot(type, data) {
1593
+ return saveAnalyticsSnapshot(this.connection.getRawDb(), type, data);
1594
+ }
1595
+ getLatestAnalyticsSnapshot(type) {
1596
+ return getLatestAnalyticsSnapshot(this.connection.getRawDb(), type);
1597
+ }
1598
+ getAnalyticsSnapshots(type, limit) {
1599
+ return getAnalyticsSnapshots(this.connection.getRawDb(), type, limit);
1600
+ }
1395
1601
  };
1396
1602
 
1397
1603
  // src/database/adapter-factory.ts
@@ -1540,7 +1746,7 @@ var VectorSearchManager = class {
1540
1746
  WHERE embedding MATCH ?
1541
1747
  ORDER BY distance
1542
1748
  LIMIT ?`
1543
- ).all(queryVec, limit * 2);
1749
+ ).all(queryVec, limit);
1544
1750
  const filteredResults = results.map((r) => ({
1545
1751
  entryId: r.entry_id,
1546
1752
  score: 1 / (1 + r.distance)
@@ -1593,7 +1799,7 @@ var VectorSearchManager = class {
1593
1799
  WHERE embedding MATCH ?
1594
1800
  ORDER BY distance
1595
1801
  LIMIT ?`
1596
- ).all(queryVec, (limit + 1) * 2);
1802
+ ).all(queryVec, limit + 1);
1597
1803
  const filteredResults = results.filter((r) => r.entry_id !== entryId).map((r) => ({
1598
1804
  entryId: r.entry_id,
1599
1805
  score: 1 / (1 + r.distance)
@@ -2039,7 +2245,7 @@ async function fetchCopilotReviews(github, owner, repo) {
2039
2245
  return void 0;
2040
2246
  }
2041
2247
  }
2042
- var PREVIEW_LENGTH = 80;
2248
+ var PREVIEW_LENGTH2 = 80;
2043
2249
  function buildJournalContext(context, config, projectNumber) {
2044
2250
  const recentEntries = typeof projectNumber === "number" ? context.db.searchEntries("", { limit: config.entryCount, projectNumber }) : context.db.getRecentEntries(config.entryCount);
2045
2251
  const latestEntries = recentEntries.map((e) => {
@@ -2048,7 +2254,7 @@ function buildJournalContext(context, config, projectNumber) {
2048
2254
  id: e.id,
2049
2255
  timestamp: e.timestamp,
2050
2256
  type: e.entryType,
2051
- preview: content.slice(0, PREVIEW_LENGTH) + (content.length > PREVIEW_LENGTH ? "..." : "")
2257
+ preview: content.slice(0, PREVIEW_LENGTH2) + (content.length > PREVIEW_LENGTH2 ? "..." : "")
2052
2258
  };
2053
2259
  });
2054
2260
  const summaryEntries = typeof projectNumber === "number" ? context.db.searchEntries("", {
@@ -2077,7 +2283,7 @@ function buildJournalContext(context, config, projectNumber) {
2077
2283
  id: entry.id,
2078
2284
  timestamp: entry.timestamp,
2079
2285
  type: entry.entryType,
2080
- preview: c.slice(0, PREVIEW_LENGTH) + (c.length > PREVIEW_LENGTH ? "..." : "")
2286
+ preview: c.slice(0, PREVIEW_LENGTH2) + (c.length > PREVIEW_LENGTH2 ? "..." : "")
2081
2287
  };
2082
2288
  });
2083
2289
  latestSessionSummary = sessionSummaries[0];
@@ -2178,7 +2384,8 @@ function formatUserMessage(opts) {
2178
2384
  summaryPreviews,
2179
2385
  github,
2180
2386
  rulesFile,
2181
- skillsDir
2387
+ skillsDir,
2388
+ analyticsInsights
2182
2389
  } = opts;
2183
2390
  let ciDisplay = opts.ciStatus;
2184
2391
  if (github?.workflowSummary) {
@@ -2248,6 +2455,19 @@ function formatUserMessage(opts) {
2248
2455
  }
2249
2456
  const copilotRow = github?.copilotReviews ? `
2250
2457
  | **Copilot** | ${String(github.copilotReviews.reviewed)} reviewed \xB7 ${String(github.copilotReviews.approved)} approved${github.copilotReviews.changesRequested > 0 ? ` \xB7 ${String(github.copilotReviews.changesRequested)} changes requested` : ""}${github.copilotReviews.totalComments > 0 ? ` (${String(github.copilotReviews.totalComments)} comments)` : ""} |` : "";
2458
+ let analyticsRow = "";
2459
+ if (analyticsInsights) {
2460
+ const parts = [];
2461
+ parts.push(`\u{1F4C8} ${analyticsInsights.activityTrend}`);
2462
+ if (analyticsInsights.significanceSpike !== null)
2463
+ parts.push(`\u{1F525} ${analyticsInsights.significanceSpike}`);
2464
+ if (analyticsInsights.relationshipDensity !== void 0 && analyticsInsights.relationshipDensity >= 0)
2465
+ parts.push(`\u{1F517} Matrix Density: ${analyticsInsights.relationshipDensity}`);
2466
+ if (analyticsInsights.staleProjects.length > 0)
2467
+ parts.push(`\u{1F4A4} ${analyticsInsights.staleProjects.length} stale projects`);
2468
+ analyticsRow = `
2469
+ | **Analytics** | ${escapeTableCell(parts.join(" \xB7 "))} |`;
2470
+ }
2251
2471
  const summariesOutput = summaryPreviews && summaryPreviews.length > 0 ? summaryPreviews.map((s) => `
2252
2472
  | **Summary** | ${escapeTableCell(s)} |`).join("") : "";
2253
2473
  return `\u{1F4CB} **Session Context Loaded**
@@ -2258,11 +2478,56 @@ function formatUserMessage(opts) {
2258
2478
  | **CI** | ${escapeTableCell(ciDisplay)} |
2259
2479
  | **Journal** | ${totalEntries} entries |${opts.teamTotalEntries !== void 0 ? `
2260
2480
  | **Team DB** | ${opts.teamTotalEntries} entries |` : ""}
2261
- | **Latest** | ${escapeTableCell(latestPreview)} |${summariesOutput}${issuesRow}${prsRow}${milestoneRow}${insightsRow}${copilotRow}${rulesFile ? `
2481
+ | **Latest** | ${escapeTableCell(latestPreview)} |${summariesOutput}${issuesRow}${prsRow}${milestoneRow}${insightsRow}${copilotRow}${analyticsRow}${rulesFile ? `
2262
2482
  | **Rules** | ${escapeTableCell(rulesFile.name)} (${String(rulesFile.sizeKB)} KB, updated ${rulesFile.lastModified}) |` : ""}${skillsDir ? `
2263
2483
  | **Skills** | ${String(skillsDir.count)} skill${skillsDir.count !== 1 ? "s" : ""} available |` : ""}`;
2264
2484
  }
2265
2485
 
2486
+ // src/handlers/resources/core/briefing/insights-section.ts
2487
+ function buildInsightsSection(context) {
2488
+ const snapshot = resolveDigestSnapshot(context);
2489
+ if (!snapshot) return null;
2490
+ return formatDigest(snapshot);
2491
+ }
2492
+ function resolveDigestSnapshot(context) {
2493
+ const schedulerDigest = context.scheduler?.getLatestDigest?.();
2494
+ if (schedulerDigest) return schedulerDigest;
2495
+ const dbSnapshot = context.db?.getLatestAnalyticsSnapshot?.("digest");
2496
+ if (dbSnapshot) return dbSnapshot.data;
2497
+ return null;
2498
+ }
2499
+ function formatDigest(snapshot) {
2500
+ let activityTrend;
2501
+ if (snapshot.activityGrowthPercent !== null) {
2502
+ const sign = snapshot.activityGrowthPercent >= 0 ? "+" : "";
2503
+ activityTrend = `${sign}${String(snapshot.activityGrowthPercent)}% vs. last period (${String(snapshot.currentPeriodEntries)} entries)`;
2504
+ } else {
2505
+ activityTrend = `${String(snapshot.currentPeriodEntries)} entries this period (no previous data)`;
2506
+ }
2507
+ let significanceSpike = null;
2508
+ if (snapshot.currentPeriodSignificant > 0) {
2509
+ if (snapshot.significanceMultiplier !== null && snapshot.significanceMultiplier > 1.5) {
2510
+ significanceSpike = `${String(snapshot.currentPeriodSignificant)} significant entries (${String(snapshot.significanceMultiplier)}\xD7 avg)`;
2511
+ } else {
2512
+ significanceSpike = `${String(snapshot.currentPeriodSignificant)} significant entries this period`;
2513
+ }
2514
+ }
2515
+ return {
2516
+ activityTrend,
2517
+ significanceSpike,
2518
+ staleProjects: snapshot.staleProjects.map((p) => ({
2519
+ projectNumber: p.projectNumber,
2520
+ daysSilent: p.daysSilent
2521
+ })),
2522
+ topImportance: snapshot.topImportanceEntries.map((e) => ({
2523
+ id: e.id,
2524
+ score: e.score,
2525
+ preview: e.preview
2526
+ })),
2527
+ ...snapshot.currentRelDensity > 0 ? { relationshipDensity: snapshot.currentRelDensity } : {}
2528
+ };
2529
+ }
2530
+
2266
2531
  // src/handlers/resources/core/briefing/index.ts
2267
2532
  var briefingResource = {
2268
2533
  uri: "memory://briefing",
@@ -2309,6 +2574,7 @@ async function buildBriefingData(context, targetRepo) {
2309
2574
  const team = buildTeamContext(context, config, activeProjectNumber);
2310
2575
  const rulesFile = buildRulesFileInfo(config.rulesFilePath);
2311
2576
  const skillsDir = buildSkillsDirInfo(config.skillsDirPath);
2577
+ const insights = buildInsightsSection(context);
2312
2578
  const latestPreview = journal.latestEntries[0] ? `#${journal.latestEntries[0].id} (${journal.latestEntries[0].type}): ${journal.latestEntries[0].preview}` : "No entries yet";
2313
2579
  const summaryPreviews = journal.sessionSummaries ? journal.sessionSummaries.map((s) => `#${s.id} (${s.type}): ${s.preview}`) : null;
2314
2580
  const userMessage = formatUserMessage({
@@ -2321,7 +2587,8 @@ async function buildBriefingData(context, targetRepo) {
2321
2587
  github,
2322
2588
  teamTotalEntries: team?.teamInfo.totalEntries,
2323
2589
  rulesFile,
2324
- skillsDir
2590
+ skillsDir,
2591
+ analyticsInsights: insights ?? void 0
2325
2592
  });
2326
2593
  return {
2327
2594
  data: {
@@ -2337,6 +2604,7 @@ async function buildBriefingData(context, targetRepo) {
2337
2604
  ...team?.teamLatestEntries ? { teamLatestEntries: team.teamLatestEntries } : {},
2338
2605
  ...rulesFile ? { rulesFile } : {},
2339
2606
  ...skillsDir ? { skillsDir } : {},
2607
+ ...insights ? { insights } : {},
2340
2608
  ...config.projectRegistry ? { registeredWorkspaces: config.projectRegistry } : {},
2341
2609
  behaviors: {
2342
2610
  create: "implementations, decisions, bug-fixes, milestones",
@@ -2387,7 +2655,7 @@ var CORE_INSTRUCTIONS = `# memory-journal-mcp
2387
2655
  - Milestone progress (if any)
2388
2656
  - Template resources count
2389
2657
  - Registered Workspaces (if available - provides automatic repo-to-project routing)
2390
- - Optional metadata present (rulesFile, skillsDir, workflowSummary, copilotReviews, Team DB)
2658
+ - Optional metadata present (rulesFile, skillsDir, workflowSummary, copilotReviews, Team DB, insights)
2391
2659
 
2392
2660
  - **AntiGravity**: Tools are \`mcp_{name}_{tool}\` \u2192 server name = \`memory-journal-mcp\`
2393
2661
  - **Cursor**: Tools are \`user-{name}-{tool}\` \u2192 server name = \`user-memory-journal-mcp\`
@@ -2399,8 +2667,8 @@ var CORE_INSTRUCTIONS = `# memory-journal-mcp
2399
2667
 
2400
2668
  - **Personal vs Team**: **ALWAYS use the personal journal** (e.g., \`create_entry\`) by default. ONLY save to the team journal (e.g., \`team_create_entry\`) if the user explicitly requests it.
2401
2669
  - **Create entries for**: implementations, decisions, bug fixes, milestones, user requests to "remember"
2402
- - **Search before**: major decisions, referencing prior work, understanding project context
2403
- - **Analyze insights**: Use cross-project insights (\`get_cross_project_insights\`) before defining architectures or cross-cutting abstractions. Use repo insights (\`memory://github/insights\`) to gauge traction.
2670
+ - **Search before**: major decisions, referencing prior work, understanding project context. Use \`sort_by: "importance"\` on \`search_entries\`, \`get_recent_entries\`, or \`search_by_date_range\` to surface structurally significant entries (decisions, milestones, highly-connected nodes) over simply recent ones.
2671
+ - **Analyze insights**: Use cross-project insights (\`get_cross_project_insights\`) before defining architectures. Use \`team_get_collaboration_matrix\` to evaluate team health, cross-author activity patterns, and collaboration impact. Use repo insights (\`memory://github/insights\`) to gauge traction. View \`memory://insights/digest\` and \`memory://insights/team-collaboration\` for automated analytics snapshots.
2404
2672
  - **Link entries**: implementation\u2192spec, bugfix\u2192issue, followup\u2192prior work
2405
2673
 
2406
2674
  ### Rule & Skill Suggestions
@@ -2436,7 +2704,7 @@ When you notice the user consistently applies patterns, preferences, or workflow
2436
2704
 
2437
2705
  ### Native Agent Skills (NPM Distribution)
2438
2706
 
2439
- This server leverages the \`neverinfamous-agent-skills\` package. If the user's \`SKILLS_DIR_PATH\` environment variable targets these, you have native access to foundational frameworks (\`mastering-typescript\`, \`react-best-practices\`, \`playwright-standard\`, \`golang\`, \`rust\`, \`shadcn-ui\`) and the \`github-commander\` DevOps workflows (\`issue-triage\`, \`pr-review\`, etc.).
2707
+ This server leverages the \`neverinfamous-agent-skills\` package. If the user's \`SKILLS_DIR_PATH\` environment variable targets these, you have native access to foundational frameworks (\`mastering-typescript\`, \`react-best-practices\`, \`playwright-standard\`, \`golang\`, \`rust\`, \`python\`, \`docker\`, \`tailwind-css\`, \`shadcn-ui\`) and the \`github-commander\` DevOps workflows (\`issue-triage\`, \`pr-review\`, \`github-actions\`, etc.).
2440
2708
 
2441
2709
  - The user can distribute or update these skills across their repositories by running \`npx neverinfamous-agent-skills@latest\`.
2442
2710
  - If you need to create a new skill, reference the bundled \`skill-builder\` instructions!
@@ -2476,18 +2744,71 @@ function buildQuickAccess(groups) {
2476
2744
  return table;
2477
2745
  }
2478
2746
  var CODE_MODE_NAMESPACE_ROWS = [
2479
- { group: "core", label: "Core", namespace: "`mj.core.*`", example: '`mj.core.createEntry("Implemented feature X")`' },
2480
- { group: "search", label: "Search", namespace: "`mj.search.*`", example: '`mj.search.searchEntries("performance")`' },
2481
- { group: "analytics", label: "Analytics", namespace: "`mj.analytics.*`", example: "`mj.analytics.getStatistics()`" },
2482
- { group: "relationships", label: "Relationships", namespace: "`mj.relationships.*`", example: '`mj.relationships.linkEntries(1, 2, "implements")`' },
2483
- { group: "io", label: "IO", namespace: "`mj.io.*`", example: '`mj.io.exportEntries("json")`' },
2484
- { group: "admin", label: "Admin", namespace: "`mj.admin.*`", example: "`mj.admin.rebuildVectorIndex()`" },
2485
- { group: "github", label: "GitHub", namespace: "`mj.github.*`", example: '`mj.github.getGithubIssues({ state: "open" })`' },
2486
- { group: "backup", label: "Backup", namespace: "`mj.backup.*`", example: "`mj.backup.backupJournal()`" },
2487
- { group: "team", label: "Team", namespace: "`mj.team.*`", example: '`mj.team.teamCreateEntry("Team update")`' }
2747
+ {
2748
+ group: "core",
2749
+ label: "Core",
2750
+ namespace: "`mj.core.*`",
2751
+ example: '`mj.core.createEntry("Implemented feature X")`'
2752
+ },
2753
+ {
2754
+ group: "search",
2755
+ label: "Search",
2756
+ namespace: "`mj.search.*`",
2757
+ example: '`mj.search.searchEntries("performance")`'
2758
+ },
2759
+ {
2760
+ group: "analytics",
2761
+ label: "Analytics",
2762
+ namespace: "`mj.analytics.*`",
2763
+ example: "`mj.analytics.getStatistics()`"
2764
+ },
2765
+ {
2766
+ group: "relationships",
2767
+ label: "Relationships",
2768
+ namespace: "`mj.relationships.*`",
2769
+ example: '`mj.relationships.linkEntries(1, 2, "implements")`'
2770
+ },
2771
+ {
2772
+ group: "io",
2773
+ label: "IO",
2774
+ namespace: "`mj.io.*`",
2775
+ example: '`mj.io.importMarkdown("content")`'
2776
+ },
2777
+ {
2778
+ group: "io",
2779
+ label: "Export",
2780
+ namespace: "`mj.export.*`",
2781
+ example: '`mj.export.exportEntries("json")`'
2782
+ },
2783
+ {
2784
+ group: "admin",
2785
+ label: "Admin",
2786
+ namespace: "`mj.admin.*`",
2787
+ example: "`mj.admin.rebuildVectorIndex()`"
2788
+ },
2789
+ {
2790
+ group: "github",
2791
+ label: "GitHub",
2792
+ namespace: "`mj.github.*`",
2793
+ example: '`mj.github.getGithubIssues({ state: "open" })`'
2794
+ },
2795
+ {
2796
+ group: "backup",
2797
+ label: "Backup",
2798
+ namespace: "`mj.backup.*`",
2799
+ example: "`mj.backup.backupJournal()`"
2800
+ },
2801
+ {
2802
+ group: "team",
2803
+ label: "Team",
2804
+ namespace: "`mj.team.*`",
2805
+ example: '`mj.team.teamCreateEntry("Team update")`'
2806
+ }
2488
2807
  ];
2489
2808
  function buildCodeModeInstructions(groups) {
2490
- const rows = CODE_MODE_NAMESPACE_ROWS.filter((r) => groups.has(r.group)).map((r) => `| ${r.label.padEnd(13)} | ${r.namespace.padEnd(20)} | ${r.example.padEnd(50)} |`).join("\n");
2809
+ const rows = CODE_MODE_NAMESPACE_ROWS.filter((r) => groups.has(r.group)).map(
2810
+ (r) => `| ${r.label.padEnd(13)} | ${r.namespace.padEnd(20)} | ${r.example.padEnd(50)} |`
2811
+ ).join("\n");
2491
2812
  const fullSection = CODE_MODE_FULL_TEXT;
2492
2813
  const tableStart = fullSection.indexOf("| Group");
2493
2814
  const tableEnd = fullSection.indexOf("\n\n**Features**");
@@ -2511,7 +2832,8 @@ This executes JavaScript in a sandboxed environment with all tools available as
2511
2832
  | Search | \`mj.search.*\` | \`mj.search.searchEntries("performance")\` |
2512
2833
  | Analytics | \`mj.analytics.*\` | \`mj.analytics.getStatistics()\` |
2513
2834
  | Relationships | \`mj.relationships.*\` | \`mj.relationships.linkEntries(1, 2, "implements")\` |
2514
- | IO | \`mj.io.*\` | \`mj.io.exportEntries("json")\` |
2835
+ | IO | \`mj.io.*\` | \`mj.io.importMarkdown("content")\` |
2836
+ | Export | \`mj.export.*\` | \`mj.export.exportEntries("json")\` |
2515
2837
  | Admin | \`mj.admin.*\` | \`mj.admin.rebuildVectorIndex()\` |
2516
2838
  | GitHub | \`mj.github.*\` | \`mj.github.getGithubIssues({ state: "open" })\` |
2517
2839
  | Backup | \`mj.backup.*\` | \`mj.backup.backupJournal()\` |
@@ -2635,7 +2957,7 @@ var GOTCHAS_CONTENT = `# memory-journal-mcp \u2014 Field Notes & Gotchas
2635
2957
 
2636
2958
  - **Team cross-database search**: \`search_entries\` and \`search_by_date_range\` automatically merge team DB results when \`TEAM_DB_PATH\` is configured. Results include a \`source\` field ("personal" or "team").
2637
2959
  - **Team vector search**: Team has its own isolated vector index. Use \`team_rebuild_vector_index\` if the team index drifts. \`team_semantic_search\` works identically to personal \`semantic_search\`.
2638
- - **Team tools without \`TEAM_DB_PATH\`**: All 22 team tools return \`{ success: false, error: "Team collaboration is not configured..." }\` \u2014 no crash, no partial results.
2960
+ - **Team tools without \`TEAM_DB_PATH\`**: All 23 team tools return \`{ success: false, error: "Team collaboration is not configured..." }\` \u2014 no crash, no partial results.
2639
2961
  `;
2640
2962
  function generateInstructions(enabledTools, prompts, latestEntry, level = "standard", enabledGroups) {
2641
2963
  const groups = enabledGroups ?? getEnabledGroups(enabledTools);
@@ -2743,13 +3065,14 @@ ${entries.map((e) => `- [${String(e.id)}] ${e.content.slice(0, 100)}...`).join("
2743
3065
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0] ?? "";
2744
3066
  const yesterday = new Date(Date.now() - MS_PER_DAY2).toISOString().split("T")[0] ?? "";
2745
3067
  const entries = db.searchByDateRange(yesterday, today);
3068
+ const digestSignal = buildDigestSignalForPrompt(db);
2746
3069
  return {
2747
3070
  messages: [
2748
3071
  {
2749
3072
  role: "user",
2750
3073
  content: {
2751
3074
  type: "text",
2752
- text: `Prepare a standup summary based on these recent entries:
3075
+ text: `${digestSignal}Prepare a standup summary based on these recent entries:
2753
3076
 
2754
3077
  ${entries.map((e) => `[${e.timestamp}] ${e.entryType}: ${e.content}`).join("\n\n")}
2755
3078
 
@@ -2779,13 +3102,14 @@ Format as:
2779
3102
  const endDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0] ?? "";
2780
3103
  const startDate = new Date(Date.now() - days * MS_PER_DAY2).toISOString().split("T")[0] ?? "";
2781
3104
  const entries = db.searchByDateRange(startDate, endDate);
3105
+ const digestSignal = buildDigestSignalForPrompt(db);
2782
3106
  return {
2783
3107
  messages: [
2784
3108
  {
2785
3109
  role: "user",
2786
3110
  content: {
2787
3111
  type: "text",
2788
- text: `Prepare a retrospective for the last ${String(days)} days based on these entries:
3112
+ text: `${digestSignal}Prepare a retrospective for the last ${String(days)} days based on these entries:
2789
3113
 
2790
3114
  ${entries.slice(0, 20).map(
2791
3115
  (e) => `[${e.timestamp}] ${e.entryType}: ${e.content.slice(0, 200)}`
@@ -3082,6 +3406,34 @@ ${entrySummary}
3082
3406
  }
3083
3407
  ];
3084
3408
  }
3409
+ function buildDigestSignalForPrompt(db) {
3410
+ const snapshot = db.getLatestAnalyticsSnapshot?.("digest");
3411
+ if (!snapshot) return "";
3412
+ const data = snapshot.data;
3413
+ const lines = ["[Analytics Context]"];
3414
+ const growth = data["activityGrowthPercent"];
3415
+ const currentEntries = data["currentPeriodEntries"];
3416
+ if (growth !== null && growth !== void 0) {
3417
+ const sign = growth >= 0 ? "+" : "";
3418
+ lines.push(
3419
+ `Activity: ${sign}${String(growth)}% vs. last period (${String(currentEntries)} entries)`
3420
+ );
3421
+ }
3422
+ const sigMultiplier = data["significanceMultiplier"];
3423
+ const sigCount = data["currentPeriodSignificant"];
3424
+ if (sigMultiplier !== null && sigMultiplier !== void 0 && sigMultiplier > 1.5) {
3425
+ lines.push(
3426
+ `Significance: ${String(sigCount)} significant entries (${String(sigMultiplier)}\xD7 historical avg)`
3427
+ );
3428
+ }
3429
+ const stale = data["staleProjects"];
3430
+ if (stale && stale.length > 0) {
3431
+ const staleStr = stale.map((p) => `P${String(p.projectNumber)} (${String(p.daysSilent)}d silent)`).join(", ");
3432
+ lines.push(`\u26A0 Stale: ${staleStr}`);
3433
+ }
3434
+ if (lines.length <= 1) return "";
3435
+ return lines.join("\\n") + "\\n\\n";
3436
+ }
3085
3437
 
3086
3438
  // src/handlers/prompts/github.ts
3087
3439
  function formatPromptEntries(entries, maxCount = 50) {
@@ -4173,21 +4525,25 @@ function getGitHubResourceDefinitions() {
4173
4525
  error: kanbanResult.reason
4174
4526
  });
4175
4527
  }
4176
- let milestoneSummary = null;
4177
- if (milestoneResult.status === "fulfilled" && milestoneResult.value.length > 0) {
4178
- milestoneSummary = milestoneResult.value.map((ms) => {
4179
- const pct = milestoneCompletionPct(ms.openIssues, ms.closedIssues);
4180
- return {
4181
- number: ms.number,
4182
- title: ms.title,
4183
- state: ms.state,
4184
- openIssues: ms.openIssues,
4185
- closedIssues: ms.closedIssues,
4186
- completionPercentage: pct,
4187
- dueOn: ms.dueOn
4188
- };
4189
- });
4528
+ let milestoneSummary = { openCount: 0, items: [] };
4529
+ if (milestoneResult.status === "fulfilled") {
4530
+ milestoneSummary = {
4531
+ openCount: milestoneResult.value.length,
4532
+ items: milestoneResult.value.map((ms) => {
4533
+ const pct = milestoneCompletionPct(ms.openIssues, ms.closedIssues);
4534
+ return {
4535
+ number: ms.number,
4536
+ title: ms.title,
4537
+ state: ms.state,
4538
+ openIssues: ms.openIssues,
4539
+ closedIssues: ms.closedIssues,
4540
+ completionPercentage: pct,
4541
+ dueOn: ms.dueOn
4542
+ };
4543
+ })
4544
+ };
4190
4545
  } else if (milestoneResult.status === "rejected") {
4546
+ milestoneSummary = null;
4191
4547
  logger.debug("Failed to fetch milestones", {
4192
4548
  module: "RESOURCE",
4193
4549
  operation: "github-status",
@@ -4933,7 +5289,7 @@ function getHelpResourceDefinitions() {
4933
5289
  var toolIndexModule = null;
4934
5290
  async function getAllToolDefinitionsAsync(context) {
4935
5291
  try {
4936
- toolIndexModule ??= await import('./tools-CXR2FEB2.js');
5292
+ toolIndexModule ??= await import('./tools-WZUENKJ6.js');
4937
5293
  if (toolIndexModule === null) return [];
4938
5294
  const tools = toolIndexModule.getTools(context.db, null);
4939
5295
  return tools.map((t) => ({
@@ -5010,6 +5366,163 @@ function inferGroupFromName(name) {
5010
5366
  return groupMap[name] ?? "core";
5011
5367
  }
5012
5368
 
5369
+ // src/handlers/resources/insights.ts
5370
+ var digestInsightsResource = {
5371
+ uri: "memory://insights/digest",
5372
+ name: "Analytics Digest",
5373
+ title: "Latest Analytics Digest Snapshot",
5374
+ description: "Full pre-computed analytics digest with activity trends, significance spikes, stale projects, relationship density, and top importance entries. Updated by the scheduled digest job.",
5375
+ mimeType: "application/json",
5376
+ icons: [ICON_ANALYTICS],
5377
+ annotations: {
5378
+ ...withPriority(0.5, ASSISTANT_FOCUSED)
5379
+ },
5380
+ handler: (_uri, context) => {
5381
+ const lastModified = (/* @__PURE__ */ new Date()).toISOString();
5382
+ const schedulerDigest = context.scheduler?.getLatestDigest?.();
5383
+ if (schedulerDigest) {
5384
+ return {
5385
+ data: { success: true, snapshot: schedulerDigest },
5386
+ annotations: { lastModified }
5387
+ };
5388
+ }
5389
+ const dbSnapshot = context.db?.getLatestAnalyticsSnapshot?.("digest");
5390
+ if (dbSnapshot) {
5391
+ return {
5392
+ data: {
5393
+ success: true,
5394
+ snapshot: dbSnapshot.data,
5395
+ computedAt: dbSnapshot.createdAt,
5396
+ source: "persisted"
5397
+ },
5398
+ annotations: { lastModified: dbSnapshot.createdAt }
5399
+ };
5400
+ }
5401
+ return {
5402
+ data: {
5403
+ success: true,
5404
+ snapshot: null,
5405
+ message: "No digest available \u2014 enable with --digest-interval <minutes> (HTTP transport only)"
5406
+ },
5407
+ annotations: { lastModified }
5408
+ };
5409
+ }
5410
+ };
5411
+ var teamCollaborationResource = {
5412
+ uri: "memory://insights/team-collaboration",
5413
+ name: "Team Collaboration Matrix",
5414
+ title: "Team Collaboration Insights",
5415
+ description: "Cross-author collaboration metrics: activity heatmap, cross-linking patterns, and impact factor per contributor. Requires TEAM_DB_PATH.",
5416
+ mimeType: "application/json",
5417
+ icons: [ICON_ANALYTICS],
5418
+ annotations: {
5419
+ ...withPriority(0.4, ASSISTANT_FOCUSED)
5420
+ },
5421
+ handler: (_uri, context) => {
5422
+ const lastModified = (/* @__PURE__ */ new Date()).toISOString();
5423
+ if (!context.teamDb) {
5424
+ return {
5425
+ data: {
5426
+ success: true,
5427
+ matrix: null,
5428
+ message: "Team database not configured \u2014 set TEAM_DB_PATH to enable."
5429
+ },
5430
+ annotations: { lastModified }
5431
+ };
5432
+ }
5433
+ try {
5434
+ const matrix = computeTeamCollaborationMatrix(context.teamDb);
5435
+ return {
5436
+ data: { success: true, ...matrix },
5437
+ annotations: { lastModified }
5438
+ };
5439
+ } catch (error) {
5440
+ return {
5441
+ data: {
5442
+ success: false,
5443
+ error: error instanceof Error ? error.message : String(error)
5444
+ },
5445
+ annotations: { lastModified }
5446
+ };
5447
+ }
5448
+ }
5449
+ };
5450
+ function computeTeamCollaborationMatrix(teamDb) {
5451
+ const activityRows = execQuery(
5452
+ teamDb,
5453
+ `SELECT
5454
+ COALESCE(author, 'unknown') AS author,
5455
+ strftime('%Y-%m', timestamp) AS period,
5456
+ COUNT(*) AS entry_count
5457
+ FROM memory_journal
5458
+ WHERE deleted_at IS NULL
5459
+ GROUP BY author, period
5460
+ ORDER BY period DESC, entry_count DESC
5461
+ LIMIT 100`
5462
+ );
5463
+ const authorActivity = activityRows.map((r) => ({
5464
+ author: r["author"],
5465
+ period: r["period"],
5466
+ entryCount: r["entry_count"]
5467
+ }));
5468
+ const crossLinkRows = execQuery(
5469
+ teamDb,
5470
+ `SELECT
5471
+ COALESCE(m1.author, 'unknown') AS from_author,
5472
+ COALESCE(m2.author, 'unknown') AS to_author,
5473
+ COUNT(*) AS link_count
5474
+ FROM relationships r
5475
+ JOIN memory_journal m1 ON r.from_entry_id = m1.id
5476
+ JOIN memory_journal m2 ON r.to_entry_id = m2.id
5477
+ WHERE m1.deleted_at IS NULL AND m2.deleted_at IS NULL
5478
+ AND COALESCE(m1.author, 'unknown') != COALESCE(m2.author, 'unknown')
5479
+ GROUP BY from_author, to_author
5480
+ ORDER BY link_count DESC
5481
+ LIMIT 50`
5482
+ );
5483
+ const crossAuthorLinks = crossLinkRows.map((r) => ({
5484
+ fromAuthor: r["from_author"],
5485
+ toAuthor: r["to_author"],
5486
+ linkCount: r["link_count"]
5487
+ }));
5488
+ const impactRows = execQuery(
5489
+ teamDb,
5490
+ `SELECT
5491
+ COALESCE(m2.author, 'unknown') AS author,
5492
+ COUNT(*) AS inbound_links
5493
+ FROM relationships r
5494
+ JOIN memory_journal m2 ON r.to_entry_id = m2.id
5495
+ WHERE m2.deleted_at IS NULL
5496
+ GROUP BY author
5497
+ ORDER BY inbound_links DESC
5498
+ LIMIT 20`
5499
+ );
5500
+ const impactFactor = impactRows.map((r) => ({
5501
+ author: r["author"],
5502
+ inboundLinks: r["inbound_links"]
5503
+ }));
5504
+ const totalsRow = execQuery(
5505
+ teamDb,
5506
+ `SELECT
5507
+ COUNT(DISTINCT COALESCE(author, 'unknown')) AS total_authors,
5508
+ COUNT(*) AS total_entries
5509
+ FROM memory_journal
5510
+ WHERE deleted_at IS NULL`
5511
+ );
5512
+ const totalAuthors = totalsRow[0]?.["total_authors"] ?? 0;
5513
+ const totalEntries = totalsRow[0]?.["total_entries"] ?? 0;
5514
+ return {
5515
+ authorActivity,
5516
+ crossAuthorLinks,
5517
+ impactFactor,
5518
+ totalAuthors,
5519
+ totalEntries
5520
+ };
5521
+ }
5522
+ function getInsightResourceDefinitions() {
5523
+ return [digestInsightsResource, teamCollaborationResource];
5524
+ }
5525
+
5013
5526
  // src/handlers/resources/index.ts
5014
5527
  function getResources() {
5015
5528
  const resources = getAllResourceDefinitions();
@@ -5083,6 +5596,7 @@ function getAllResourceDefinitions() {
5083
5596
  ...getTemplateResourceDefinitions(),
5084
5597
  ...getTeamResourceDefinitions(),
5085
5598
  ...getHelpResourceDefinitions(),
5599
+ ...getInsightResourceDefinitions(),
5086
5600
  // Audit resource — bound to the global audit logger (or null if unconfigured)
5087
5601
  getAuditResourceDef(getGlobalAuditLogger)
5088
5602
  ];
@@ -5136,6 +5650,12 @@ var Scheduler = class {
5136
5650
  );
5137
5651
  }
5138
5652
  }
5653
+ if (this.options.digestIntervalMinutes > 0) {
5654
+ this.scheduleJob("digest", this.options.digestIntervalMinutes, () => {
5655
+ this.runDigest();
5656
+ return Promise.resolve();
5657
+ });
5658
+ }
5139
5659
  if (this.timers.length > 0) {
5140
5660
  const summary = this.timers.map(
5141
5661
  (t) => `${t.name} (${String(t.intervalMinutes)}min)`
@@ -5277,6 +5797,38 @@ var Scheduler = class {
5277
5797
  context: { entriesIndexed: count }
5278
5798
  });
5279
5799
  }
5800
+ // ========================================================================
5801
+ // Private — Digest job
5802
+ // ========================================================================
5803
+ /**
5804
+ * Digest job: compute analytics snapshot and persist to database.
5805
+ */
5806
+ runDigest() {
5807
+ const rawDb = this.db.getRawDb();
5808
+ const snapshot = computeDigest(rawDb);
5809
+ this.db.saveAnalyticsSnapshot("digest", snapshot);
5810
+ logger.info("Scheduled analytics digest computed", {
5811
+ module: "Scheduler",
5812
+ operation: "digest",
5813
+ context: {
5814
+ currentPeriodEntries: snapshot.currentPeriodEntries,
5815
+ activityGrowthPercent: snapshot.activityGrowthPercent,
5816
+ topImportanceCount: snapshot.topImportanceEntries.length
5817
+ }
5818
+ });
5819
+ }
5820
+ // ========================================================================
5821
+ // Public — Digest accessor
5822
+ // ========================================================================
5823
+ /**
5824
+ * Get the latest digest snapshot from the database.
5825
+ * Returns null if no digest has been computed yet.
5826
+ */
5827
+ getLatestDigest() {
5828
+ const snapshot = this.db.getLatestAnalyticsSnapshot("digest");
5829
+ if (!snapshot) return null;
5830
+ return snapshot.data;
5831
+ }
5280
5832
  };
5281
5833
 
5282
5834
  // src/transports/http/types.ts
@@ -6489,15 +7041,30 @@ function registerPrompts(server, prompts, db, teamDb) {
6489
7041
  ...promptDef.icons ? { icons: promptDef.icons } : {}
6490
7042
  },
6491
7043
  (providedArgs) => {
6492
- const args = providedArgs;
6493
- const promptResult = getPrompt(promptDef.name, args, db, teamDb);
6494
- const result = {
6495
- messages: promptResult.messages.map((m) => ({
6496
- role: m.role,
6497
- content: m.content
6498
- }))
6499
- };
6500
- return Promise.resolve(result);
7044
+ try {
7045
+ const args = providedArgs;
7046
+ const promptResult = getPrompt(promptDef.name, args, db, teamDb);
7047
+ const result = {
7048
+ messages: promptResult.messages.map((m) => ({
7049
+ role: m.role,
7050
+ content: m.content
7051
+ }))
7052
+ };
7053
+ return Promise.resolve(result);
7054
+ } catch (err) {
7055
+ const message = err instanceof Error ? err.message : String(err);
7056
+ return Promise.resolve({
7057
+ messages: [
7058
+ {
7059
+ role: "user",
7060
+ content: {
7061
+ type: "text",
7062
+ text: `[Prompt handler error] ${message}`
7063
+ }
7064
+ }
7065
+ ]
7066
+ });
7067
+ }
6501
7068
  }
6502
7069
  );
6503
7070
  }
@@ -6748,7 +7315,7 @@ async function createServer(options) {
6748
7315
  }
6749
7316
  let scheduler = null;
6750
7317
  if (options.scheduler) {
6751
- const hasAnyJob = options.scheduler.backupIntervalMinutes > 0 || options.scheduler.vacuumIntervalMinutes > 0 || options.scheduler.rebuildIndexIntervalMinutes > 0;
7318
+ const hasAnyJob = options.scheduler.backupIntervalMinutes > 0 || options.scheduler.vacuumIntervalMinutes > 0 || options.scheduler.rebuildIndexIntervalMinutes > 0 || options.scheduler.digestIntervalMinutes > 0;
6752
7319
  if (hasAnyJob && transport === "stdio") {
6753
7320
  logger.warning(
6754
7321
  "Scheduler options ignored for stdio transport (session is ephemeral). Use HTTP/SSE transport for automated scheduling.",