gitmem-mcp 1.4.4 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +21 -4
  3. package/bin/gitmem.js +10 -0
  4. package/dist/commands/activate.d.ts +20 -0
  5. package/dist/commands/activate.js +562 -0
  6. package/dist/commands/deactivate.d.ts +10 -0
  7. package/dist/commands/deactivate.js +95 -0
  8. package/dist/commands/migrate-local.d.ts +53 -0
  9. package/dist/commands/migrate-local.js +177 -0
  10. package/dist/hooks/format-utils.js +4 -0
  11. package/dist/schemas/log.d.ts +2 -2
  12. package/dist/schemas/search.d.ts +2 -2
  13. package/dist/schemas/session-close.d.ts +12 -12
  14. package/dist/server.js +33 -2
  15. package/dist/services/analytics.d.ts +22 -0
  16. package/dist/services/analytics.js +68 -0
  17. package/dist/services/doc-chunker.d.ts +45 -0
  18. package/dist/services/doc-chunker.js +208 -0
  19. package/dist/services/doc-index.d.ts +88 -0
  20. package/dist/services/doc-index.js +328 -0
  21. package/dist/services/license.d.ts +57 -0
  22. package/dist/services/license.js +200 -0
  23. package/dist/services/supabase-client.d.ts +6 -0
  24. package/dist/services/supabase-client.js +75 -22
  25. package/dist/services/tier.d.ts +13 -3
  26. package/dist/services/tier.js +38 -7
  27. package/dist/tools/definitions.d.ts +688 -0
  28. package/dist/tools/definitions.js +87 -0
  29. package/dist/tools/index-docs.d.ts +30 -0
  30. package/dist/tools/index-docs.js +163 -0
  31. package/dist/tools/prepare-context.js +7 -0
  32. package/dist/tools/recall.js +25 -4
  33. package/dist/tools/search-docs.d.ts +38 -0
  34. package/dist/tools/search-docs.js +94 -0
  35. package/dist/tools/search.js +11 -1
  36. package/dist/tools/session-close.js +76 -7
  37. package/dist/tools/session-start.js +57 -5
  38. package/package.json +1 -1
  39. package/schema/setup.sql +489 -25
@@ -10,9 +10,9 @@ import { v4 as uuidv4 } from "uuid";
10
10
  import { detectAgent } from "../services/agent-detection.js";
11
11
  import * as supabase from "../services/supabase-client.js";
12
12
  import { embed, isEmbeddingAvailable } from "../services/embedding.js";
13
- import { hasSupabase, getTableName } from "../services/tier.js";
13
+ import { hasSupabase, hasProInsights, getTableName } from "../services/tier.js";
14
14
  import { getStorage } from "../services/storage.js";
15
- import { clearCurrentSession, getSurfacedScars, getConfirmations, getReflections, getObservations, getChildren, getThreads, getSessionActivity } from "../services/session-state.js";
15
+ import { clearCurrentSession, getSurfacedScars, getConfirmations, getReflections, getObservations, getChildren, getThreads, getSessionActivity, isRecallCalled } from "../services/session-state.js";
16
16
  import { normalizeThreads, mergeThreadStates, migrateStringThread, saveThreadsFile } from "../services/thread-manager.js"; //
17
17
  import { deduplicateThreadList } from "../services/thread-dedup.js";
18
18
  import { syncThreadsToSupabase, loadOpenThreadEmbeddings } from "../services/thread-supabase.js";
@@ -20,6 +20,7 @@ import { validateSessionClose, buildCloseCompliance, } from "../services/complia
20
20
  import { normalizeReflectionKeys } from "../constants/closing-questions.js";
21
21
  import { Timer, recordMetrics, buildPerformanceData, updateRelevanceData, } from "../services/metrics.js";
22
22
  import { wrapDisplay, truncate, productLine, dimText, STATUS, ANSI } from "../services/display-protocol.js";
23
+ import { queryScarUsageByDateRange, enrichScarUsageTitles, formatBlindspotSnippet } from "../services/analytics.js";
23
24
  import { recordScarUsageBatch } from "./record-scar-usage-batch.js";
24
25
  import { getEffectTracker } from "../services/effect-tracker.js";
25
26
  import { saveTranscript } from "./save-transcript.js";
@@ -264,7 +265,7 @@ async function sessionCloseFree(params, timer) {
264
265
  };
265
266
  }
266
267
  }
267
- function formatCloseDisplay(sessionId, compliance, params, learningsCount, success, errors, transcriptStatus) {
268
+ function formatCloseDisplay(sessionId, compliance, params, learningsCount, success, errors, transcriptStatus, blindspotSnippet) {
268
269
  const lines = [];
269
270
  // Header: branded product line
270
271
  const status = success ? STATUS.complete : STATUS.failed;
@@ -320,6 +321,11 @@ function formatCloseDisplay(sessionId, compliance, params, learningsCount, succe
320
321
  lines.push(` ${indicator} ${truncate(s.reference_context || s.scar_identifier || "", 70)}`);
321
322
  }
322
323
  }
324
+ // Pro: blindspot section
325
+ if (blindspotSnippet) {
326
+ lines.push("");
327
+ lines.push(blindspotSnippet);
328
+ }
323
329
  // Transcript — only on failure
324
330
  if (transcriptStatus && !transcriptStatus.saved) {
325
331
  lines.push("");
@@ -858,13 +864,56 @@ export async function sessionClose(params) {
858
864
  `(${Math.round(activity.duration_min)} min, no substantive activity). ` +
859
865
  `Proceeding with standard as requested.`);
860
866
  }
867
+ // Hard gate: quick close requires session under 30 minutes
868
+ if (params.close_type === "quick" && activity.duration_min >= 30) {
869
+ return {
870
+ success: false,
871
+ session_id: params.session_id || "",
872
+ close_compliance: {
873
+ close_type: "quick",
874
+ agent: detectAgent().agent,
875
+ checklist_displayed: false,
876
+ questions_answered_by_agent: false,
877
+ human_asked_for_corrections: false,
878
+ learnings_stored: 0,
879
+ scars_applied: 0,
880
+ },
881
+ validation_errors: [
882
+ `Session has been active for ${Math.round(activity.duration_min)} minutes. ` +
883
+ `Quick close requires sessions under 30 minutes. Use close_type: "standard".`,
884
+ ],
885
+ performance: buildPerformanceData("session_close", timer.stop(), 0),
886
+ };
887
+ }
861
888
  if (params.close_type === "quick" && recommendedLevel === "full") {
862
- // Warn but don't reject — agent chose quick on a substantive session
889
+ // Warn but don't reject — agent chose quick on a substantive session under 30 min
863
890
  console.error(`[session_close] Warning: "quick" close on substantive session ` +
864
891
  `(${Math.round(activity.duration_min)} min, ${activity.recall_count} recalls, ` +
865
892
  `${activity.observation_count} observations). Consider "standard" close.`);
866
893
  }
867
894
  }
895
+ // Hard gate: standard close requires at least one recall() call
896
+ // Exemptions: quick (micro sessions), autonomous (CODA-1), hasReflection (agent already wrote full reflection)
897
+ if (params.close_type === "standard" && !isRecallCalled() && !hasReflection) {
898
+ return {
899
+ success: false,
900
+ session_id: params.session_id || "",
901
+ close_compliance: {
902
+ close_type: "standard",
903
+ agent: detectAgent().agent,
904
+ checklist_displayed: false,
905
+ questions_answered_by_agent: false,
906
+ human_asked_for_corrections: false,
907
+ learnings_stored: 0,
908
+ scars_applied: 0,
909
+ },
910
+ validation_errors: [
911
+ `No recall() was run this session. Standard close requires at least one recall. ` +
912
+ `Run recall("session close ceremony") first, then retry session_close.`,
913
+ ],
914
+ performance: buildPerformanceData("session_close", timer.stop(), 0),
915
+ };
916
+ }
868
917
  // Free tier: simple local persistence, skip Supabase recovery and compliance
869
918
  if (!hasSupabase()) {
870
919
  return sessionCloseFree(params, timer);
@@ -1104,11 +1153,31 @@ export async function sessionClose(params) {
1104
1153
  params = { ...params, scars_to_record: bridgedScars };
1105
1154
  }
1106
1155
  }
1156
+ // Pro: fetch blindspot data in parallel with session persistence
1157
+ let blindspotSnippet = null;
1158
+ const blindspotPromise = (async () => {
1159
+ if (!hasProInsights())
1160
+ return;
1161
+ try {
1162
+ const endDate = new Date().toISOString();
1163
+ const startDate = new Date(Date.now() - 30 * 86400000).toISOString();
1164
+ const project = isRetroactive ? "default" : existingSession?.project || "default";
1165
+ const rawUsages = await queryScarUsageByDateRange(startDate, endDate, project);
1166
+ const usages = await enrichScarUsageTitles(rawUsages);
1167
+ blindspotSnippet = formatBlindspotSnippet(usages);
1168
+ }
1169
+ catch (error) {
1170
+ console.error("[session_close] Blindspot fetch failed (non-fatal):", error);
1171
+ }
1172
+ })();
1107
1173
  // 6. Persist to Supabase (direct REST API, bypasses ww-mcp)
1108
1174
  try {
1109
1175
  // Upsert session WITHOUT embedding (fast path)
1110
1176
  // Embedding + thread detection run fire-and-forget after
1111
- await supabase.directUpsert(getTableName("sessions"), sessionData);
1177
+ await Promise.all([
1178
+ supabase.directUpsert(getTableName("sessions"), sessionData),
1179
+ blindspotPromise,
1180
+ ]);
1112
1181
  // Tracked fire-and-forget embedding generation + session update + thread detection
1113
1182
  if (isEmbeddingAvailable()) {
1114
1183
  getEffectTracker().track("embedding", "session_close", async () => {
@@ -1219,7 +1288,7 @@ export async function sessionClose(params) {
1219
1288
  }
1220
1289
  catch { /* already gone */ }
1221
1290
  }
1222
- const display = formatCloseDisplay(sessionId, closeCompliance, params, learningsCount, true, validation.warnings.length > 0 ? validation.warnings : undefined, transcriptStatus);
1291
+ const display = formatCloseDisplay(sessionId, closeCompliance, params, learningsCount, true, validation.warnings.length > 0 ? validation.warnings : undefined, transcriptStatus, blindspotSnippet);
1223
1292
  return {
1224
1293
  success: true,
1225
1294
  session_id: sessionId,
@@ -1235,7 +1304,7 @@ export async function sessionClose(params) {
1235
1304
  const perfData = buildPerformanceData("session_close", latencyMs, 0);
1236
1305
  // Clear session state even on error (session is done either way)
1237
1306
  clearCurrentSession();
1238
- const errorDisplay = formatCloseDisplay(sessionId, closeCompliance, params, learningsCount, false, [`Failed to persist session: ${errorMessage}`], transcriptStatus);
1307
+ const errorDisplay = formatCloseDisplay(sessionId, closeCompliance, params, learningsCount, false, [`Failed to persist session: ${errorMessage}`], transcriptStatus, blindspotSnippet);
1239
1308
  return {
1240
1309
  success: false,
1241
1310
  session_id: sessionId,
@@ -17,7 +17,7 @@ import { detectAgent } from "../services/agent-detection.js";
17
17
  import * as supabase from "../services/supabase-client.js";
18
18
  // Scar search removed from start pipeline (loads on-demand via recall)
19
19
  import { ensureInitialized } from "../services/startup.js";
20
- import { hasSupabase, getTableName } from "../services/tier.js";
20
+ import { hasSupabase, hasProInsights, getTableName } from "../services/tier.js";
21
21
  import { getStorage } from "../services/storage.js";
22
22
  import { Timer, recordMetrics, calculateContextBytes, buildPerformanceData, buildComponentPerformance, } from "../services/metrics.js";
23
23
  import { setCurrentSession, getCurrentSession, addSurfacedScars, getSurfacedScars } from "../services/session-state.js";
@@ -29,6 +29,7 @@ import { registerSession, findSessionByHostPid, pruneStale, migrateFromLegacy }
29
29
  import * as os from "os";
30
30
  import { formatDate } from "../services/timezone.js";
31
31
  import { productLine, dimText, boldText } from "../services/display-protocol.js";
32
+ import { querySessionsByDateRange, queryScarUsageByDateRange, enrichScarUsageTitles, computeLightweightSummary, } from "../services/analytics.js";
32
33
  /**
33
34
  * Closing payload schema — returned in session_start/refresh so agents
34
35
  * know the exact field names for closing-payload.json without guessing.
@@ -252,6 +253,32 @@ async function loadRecentDecisions(project, limit = 5) {
252
253
  };
253
254
  }
254
255
  }
256
+ /**
257
+ * Load lightweight analytics for Pro insights snippet.
258
+ * Reuses cached analytics queries — ~200ms on cache miss, near-instant on hit.
259
+ * Returns null on any error (never blocks session start).
260
+ */
261
+ async function loadLightweightAnalytics(project) {
262
+ if (!hasProInsights() || !hasSupabase())
263
+ return null;
264
+ try {
265
+ const endDate = new Date().toISOString();
266
+ const startDate = new Date(Date.now() - 30 * 86400000).toISOString();
267
+ const [sessions, rawUsages] = await Promise.all([
268
+ querySessionsByDateRange(startDate, endDate, project),
269
+ queryScarUsageByDateRange(startDate, endDate, project),
270
+ ]);
271
+ // Skip if not enough data for meaningful insights
272
+ if (sessions.length < 5)
273
+ return null;
274
+ const usages = await enrichScarUsageTitles(rawUsages);
275
+ return computeLightweightSummary(sessions, usages);
276
+ }
277
+ catch (error) {
278
+ console.error("[session_start] Lightweight analytics failed (non-fatal):", error);
279
+ return null;
280
+ }
281
+ }
255
282
  // loadRecentWins removed — wins available via search/log on-demand
256
283
  /**
257
284
  * Create a new session record
@@ -666,7 +693,7 @@ function writeSessionFiles(sessionId, agent, project, surfacedScars, threads, re
666
693
  function stripThreadPrefix(text) {
667
694
  return text.replace(/^t-[a-f0-9]+:\s*/i, "");
668
695
  }
669
- function formatStartDisplay(result, displayInfoMap, isFirstSession) {
696
+ function formatStartDisplay(result, displayInfoMap, isFirstSession, analytics) {
670
697
  const visual = [];
671
698
  // Line 1: branded product line + session state
672
699
  const stateLabel = result.refreshed ? "refreshed" : (result.resumed ? "resumed" : "active");
@@ -676,6 +703,20 @@ function formatStartDisplay(result, displayInfoMap, isFirstSession) {
676
703
  if (result.project)
677
704
  parts.push(result.project);
678
705
  visual.push(dimText(parts.join(" · ")));
706
+ // Line 3: duration + surfaced scars for resumed/refreshed sessions
707
+ if (result.resumed || result.refreshed) {
708
+ const session = getCurrentSession();
709
+ if (session?.startedAt) {
710
+ const durationMs = Date.now() - session.startedAt.getTime();
711
+ const totalMin = Math.floor(durationMs / 60000);
712
+ const hours = Math.floor(totalMin / 60);
713
+ const mins = totalMin % 60;
714
+ const durationStr = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
715
+ const scarCount = session.surfacedScars?.length || 0;
716
+ const scarSuffix = scarCount > 0 ? ` · ${scarCount} scars loaded from earlier` : "";
717
+ visual.push(dimText(`Session active for: ${durationStr}${scarSuffix}`));
718
+ }
719
+ }
679
720
  // Threads section — top 5 by vitality, truncated to 60 chars
680
721
  const hasThreads = result.open_threads && result.open_threads.length > 0;
681
722
  const hasDecisions = result.recent_decisions && result.recent_decisions.length > 0;
@@ -711,6 +752,16 @@ function formatStartDisplay(result, displayInfoMap, isFirstSession) {
711
752
  visual.push("");
712
753
  visual.push("No threads or decisions.");
713
754
  }
755
+ // Pro insights snippet — 30-day analytics summary
756
+ if (analytics) {
757
+ visual.push("");
758
+ const appPct = Math.round(analytics.application_rate * 100);
759
+ visual.push(boldText("Pro Insights (30d)"));
760
+ visual.push(` ${analytics.total_sessions} sessions · ${analytics.scars_surfaced} scars surfaced · ${appPct}% applied`);
761
+ if (analytics.top_blindspot) {
762
+ visual.push(` Top blindspot: "${analytics.top_blindspot.title}" (ignored ${analytics.top_blindspot.times} times)`);
763
+ }
764
+ }
714
765
  // First-session nudge — agent sees this once, internalizes to PMEM
715
766
  if (isFirstSession) {
716
767
  visual.push(FIRST_SESSION_NUDGE);
@@ -770,12 +821,13 @@ export async function sessionStart(params) {
770
821
  if (!hasSupabase()) {
771
822
  return sessionStartFree(params, env, agent, project, timer, metricsId, existingSession?.sessionId, existingSession?.startedAt || forceCarryStartedAt, priorSession ? { surfacedScars: forceCarrySurfacedScars, observations: forceCarryObservations, children: forceCarryChildren } : undefined);
772
823
  }
773
- // 2. Load last session + decisions in parallel (was sequential)
824
+ // 2. Load last session + decisions + analytics in parallel (was sequential)
774
825
  // Scars and wins removed from pipeline — load on-demand via recall/search
775
826
  // Rapport loading disabled — recording kept in session_close but not injected
776
- const [lastSessionResult, decisionsResult] = await Promise.all([
827
+ const [lastSessionResult, decisionsResult, analyticsResult] = await Promise.all([
777
828
  loadLastSession(agent, project),
778
829
  loadRecentDecisions(project, 3),
830
+ loadLightweightAnalytics(project),
779
831
  ]);
780
832
  const lastSession = lastSessionResult.session;
781
833
  const decisions = decisionsResult.decisions;
@@ -910,7 +962,7 @@ export async function sessionStart(params) {
910
962
  displayInfoMap.set(info.thread.id, info);
911
963
  }
912
964
  const isFirstSession = !isResuming && !slimLastSession;
913
- result.display = formatStartDisplay(result, displayInfoMap, isFirstSession);
965
+ result.display = formatStartDisplay(result, displayInfoMap, isFirstSession, analyticsResult);
914
966
  // Write display to per-session dir
915
967
  try {
916
968
  const sessionFilePath = getSessionPath(sessionId, "session.json");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitmem-mcp",
3
- "version": "1.4.4",
3
+ "version": "1.6.0",
4
4
  "mcpName": "io.github.gitmem-dev/gitmem",
5
5
  "description": "Persistent learning memory for AI coding agents. Memory that compounds.",
6
6
  "type": "module",