gitmem-mcp 1.5.1 → 1.6.1

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.
@@ -7,6 +7,7 @@
7
7
  *
8
8
  */
9
9
  import { getTableName } from "./tier.js";
10
+ import { getProConfig } from "./license.js";
10
11
  // OpenRouter API configuration (same as local-vector-search)
11
12
  const OPENROUTER_API_URL = "https://openrouter.ai/api/v1/embeddings";
12
13
  const EMBEDDING_MODEL = "openai/text-embedding-3-small";
@@ -32,9 +33,9 @@ function normalize(vec) {
32
33
  * Generate embedding using OpenRouter API
33
34
  */
34
35
  async function generateEmbedding(text) {
35
- const apiKey = process.env.OPENROUTER_API_KEY;
36
+ const apiKey = process.env.OPENROUTER_API_KEY || getProConfig().openrouterKey;
36
37
  if (!apiKey) {
37
- throw new Error("OPENROUTER_API_KEY not configured");
38
+ throw new Error("No OpenRouter key configured (set OPENROUTER_API_KEY env var or run: npx gitmem-mcp activate)");
38
39
  }
39
40
  const response = await fetch(OPENROUTER_API_URL, {
40
41
  method: "POST",
@@ -17,6 +17,7 @@
17
17
  * Called fire-and-forget from create_learning — zero impact on UX latency.
18
18
  */
19
19
  import * as supabase from "./supabase-client.js";
20
+ import { getProConfig } from "./license.js";
20
21
  // --- Pipeline versioning ---
21
22
  // Bump PIPELINE_VERSION when changing the prompt, model, or generation logic.
22
23
  // Format: "gen-{major}.{minor}" — major for prompt rewrites, minor for tweaks.
@@ -83,9 +84,9 @@ function buildUserPrompt(scar) {
83
84
  * Returns parsed output or null on failure.
84
85
  */
85
86
  async function generateWithLLM(scar) {
86
- const apiKey = process.env.OPENROUTER_API_KEY;
87
+ const apiKey = process.env.OPENROUTER_API_KEY || getProConfig().openrouterKey;
87
88
  if (!apiKey) {
88
- console.error("[variant-generation] No OPENROUTER_API_KEY — falling back to deterministic");
89
+ console.error("[variant-generation] No OpenRouter key (env or config.json) — falling back to deterministic");
89
90
  return null;
90
91
  }
91
92
  try {
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import * as supabase from "../services/supabase-client.js";
16
16
  import { localScarSearch, isLocalSearchReady } from "../services/local-vector-search.js";
17
- import { hasSupabase, hasVariants, hasMetrics, getTableName } from "../services/tier.js";
17
+ import { hasSupabase, hasVariants, hasMetrics, hasProInsights, getTableName } from "../services/tier.js";
18
18
  import { getProject } from "../services/session-state.js";
19
19
  import { getStorage } from "../services/storage.js";
20
20
  import { Timer, recordMetrics, buildPerformanceData, buildComponentPerformance, calculateContextBytes, } from "../services/metrics.js";
@@ -82,12 +82,16 @@ No past lessons match this plan closely enough. Scars accumulate as you work —
82
82
  const starterTag = scar.is_starter ? ` ${dimText("[starter]")}` : "";
83
83
  // Confidence tier: marginal matches (< 0.55) get flagged — 66% N/A rate in this range
84
84
  const confidenceTag = scar.similarity < 0.55 ? ` ${dimText("[low confidence]")}` : "";
85
- lines.push(`${sev} **${scar.title}** (${scar.severity}, ${scar.similarity.toFixed(2)}) ${dimText(`id:${scar.id.slice(0, 8)}`)}${starterTag}${confidenceTag}`);
85
+ // Pro: decay tag for scars with reduced behavioral relevance
86
+ const decayTag = hasProInsights() && scar.decay_multiplier !== undefined && scar.decay_multiplier < 0.8
87
+ ? ` ${dimText(`[decay: ${Math.round(scar.decay_multiplier * 100)}%]`)}`
88
+ : "";
89
+ lines.push(`${sev} **${scar.title}** (${scar.severity}, ${scar.similarity.toFixed(2)}) ${dimText(`id:${scar.id.slice(0, 8)}`)}${starterTag}${confidenceTag}${decayTag}`);
86
90
  // Inline archival hint: scars with high dismiss rates get annotated
87
91
  if (dismissals) {
88
92
  const counts = dismissals.get(scar.id);
89
- if (counts && counts.surfaced >= 5 && (counts.dismissed / counts.surfaced) >= 0.7) {
90
- lines.push(` _[${counts.dismissed}x dismissedconsider archiving with gm-archive]_`);
93
+ if (counts && counts.surfaced >= 3 && (counts.dismissed / counts.surfaced) >= 0.6) {
94
+ lines.push(` _[dismissed ${counts.dismissed}/${counts.surfaced} timesre-evaluate whether this still applies]_`);
91
95
  }
92
96
  }
93
97
  // Use variant enforcement text if available (blind to variant name)
@@ -145,6 +149,14 @@ No past lessons match this plan closely enough. Scars accumulate as you work —
145
149
  lines.push("");
146
150
  }
147
151
  lines.push("**Acknowledge these lessons before proceeding.**");
152
+ // Pro: graph nudge when triples exist on any scar
153
+ if (hasProInsights() && scars.some(s => s.related_triples && s.related_triples.length > 0)) {
154
+ const firstScarWithTriples = scars.find(s => s.related_triples && s.related_triples.length > 0);
155
+ if (firstScarWithTriples) {
156
+ lines.push("");
157
+ lines.push(dimText(`Pro: Use graph_traverse(lens: 'connected_to', node: '${firstScarWithTriples.title}') to explore deeper connections.`));
158
+ }
159
+ }
148
160
  return lines.join("\n");
149
161
  }
150
162
  /**
@@ -10,7 +10,7 @@ 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
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"; //
@@ -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("");
@@ -1147,11 +1153,31 @@ export async function sessionClose(params) {
1147
1153
  params = { ...params, scars_to_record: bridgedScars };
1148
1154
  }
1149
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
+ })();
1150
1173
  // 6. Persist to Supabase (direct REST API, bypasses ww-mcp)
1151
1174
  try {
1152
1175
  // Upsert session WITHOUT embedding (fast path)
1153
1176
  // Embedding + thread detection run fire-and-forget after
1154
- await supabase.directUpsert(getTableName("sessions"), sessionData);
1177
+ await Promise.all([
1178
+ supabase.directUpsert(getTableName("sessions"), sessionData),
1179
+ blindspotPromise,
1180
+ ]);
1155
1181
  // Tracked fire-and-forget embedding generation + session update + thread detection
1156
1182
  if (isEmbeddingAvailable()) {
1157
1183
  getEffectTracker().track("embedding", "session_close", async () => {
@@ -1262,7 +1288,7 @@ export async function sessionClose(params) {
1262
1288
  }
1263
1289
  catch { /* already gone */ }
1264
1290
  }
1265
- 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);
1266
1292
  return {
1267
1293
  success: true,
1268
1294
  session_id: sessionId,
@@ -1278,7 +1304,7 @@ export async function sessionClose(params) {
1278
1304
  const perfData = buildPerformanceData("session_close", latencyMs, 0);
1279
1305
  // Clear session state even on error (session is done either way)
1280
1306
  clearCurrentSession();
1281
- 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);
1282
1308
  return {
1283
1309
  success: false,
1284
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");
@@ -725,6 +752,16 @@ function formatStartDisplay(result, displayInfoMap, isFirstSession) {
725
752
  visual.push("");
726
753
  visual.push("No threads or decisions.");
727
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
+ }
728
765
  // First-session nudge — agent sees this once, internalizes to PMEM
729
766
  if (isFirstSession) {
730
767
  visual.push(FIRST_SESSION_NUDGE);
@@ -784,12 +821,13 @@ export async function sessionStart(params) {
784
821
  if (!hasSupabase()) {
785
822
  return sessionStartFree(params, env, agent, project, timer, metricsId, existingSession?.sessionId, existingSession?.startedAt || forceCarryStartedAt, priorSession ? { surfacedScars: forceCarrySurfacedScars, observations: forceCarryObservations, children: forceCarryChildren } : undefined);
786
823
  }
787
- // 2. Load last session + decisions in parallel (was sequential)
824
+ // 2. Load last session + decisions + analytics in parallel (was sequential)
788
825
  // Scars and wins removed from pipeline — load on-demand via recall/search
789
826
  // Rapport loading disabled — recording kept in session_close but not injected
790
- const [lastSessionResult, decisionsResult] = await Promise.all([
827
+ const [lastSessionResult, decisionsResult, analyticsResult] = await Promise.all([
791
828
  loadLastSession(agent, project),
792
829
  loadRecentDecisions(project, 3),
830
+ loadLightweightAnalytics(project),
793
831
  ]);
794
832
  const lastSession = lastSessionResult.session;
795
833
  const decisions = decisionsResult.decisions;
@@ -924,7 +962,7 @@ export async function sessionStart(params) {
924
962
  displayInfoMap.set(info.thread.id, info);
925
963
  }
926
964
  const isFirstSession = !isResuming && !slimLastSession;
927
- result.display = formatStartDisplay(result, displayInfoMap, isFirstSession);
965
+ result.display = formatStartDisplay(result, displayInfoMap, isFirstSession, analyticsResult);
928
966
  // Write display to per-session dir
929
967
  try {
930
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.5.1",
3
+ "version": "1.6.1",
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",