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.
- package/CHANGELOG.md +21 -0
- package/README.md +21 -4
- package/bin/gitmem.js +10 -0
- package/dist/commands/activate.d.ts +20 -0
- package/dist/commands/activate.js +606 -0
- package/dist/commands/deactivate.d.ts +10 -0
- package/dist/commands/deactivate.js +95 -0
- package/dist/commands/migrate-local.d.ts +71 -0
- package/dist/commands/migrate-local.js +317 -0
- package/dist/schemas/log.d.ts +2 -2
- package/dist/schemas/search.d.ts +2 -2
- package/dist/schemas/session-close.d.ts +12 -12
- package/dist/server.js +20 -2
- package/dist/services/analytics.d.ts +22 -0
- package/dist/services/analytics.js +68 -0
- package/dist/services/embedding.d.ts +7 -1
- package/dist/services/embedding.js +23 -5
- package/dist/services/license.d.ts +57 -0
- package/dist/services/license.js +200 -0
- package/dist/services/supabase-client.d.ts +6 -0
- package/dist/services/supabase-client.js +75 -22
- package/dist/services/tier.d.ts +13 -3
- package/dist/services/tier.js +38 -7
- package/dist/services/transcript-chunker.js +3 -2
- package/dist/services/variant-generation.js +3 -2
- package/dist/tools/recall.js +16 -4
- package/dist/tools/session-close.js +31 -5
- package/dist/tools/session-start.js +43 -5
- package/package.json +1 -1
- package/schema/setup.sql +489 -25
|
@@ -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
|
|
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
|
|
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 {
|
package/dist/tools/recall.js
CHANGED
|
@@ -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
|
-
|
|
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 >=
|
|
90
|
-
lines.push(` _[${counts.dismissed}
|
|
93
|
+
if (counts && counts.surfaced >= 3 && (counts.dismissed / counts.surfaced) >= 0.6) {
|
|
94
|
+
lines.push(` _[dismissed ${counts.dismissed}/${counts.surfaced} times — re-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
|
|
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");
|