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.
- package/CHANGELOG.md +27 -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 +562 -0
- package/dist/commands/deactivate.d.ts +10 -0
- package/dist/commands/deactivate.js +95 -0
- package/dist/commands/migrate-local.d.ts +53 -0
- package/dist/commands/migrate-local.js +177 -0
- package/dist/hooks/format-utils.js +4 -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 +33 -2
- package/dist/services/analytics.d.ts +22 -0
- package/dist/services/analytics.js +68 -0
- package/dist/services/doc-chunker.d.ts +45 -0
- package/dist/services/doc-chunker.js +208 -0
- package/dist/services/doc-index.d.ts +88 -0
- package/dist/services/doc-index.js +328 -0
- 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/tools/definitions.d.ts +688 -0
- package/dist/tools/definitions.js +87 -0
- package/dist/tools/index-docs.d.ts +30 -0
- package/dist/tools/index-docs.js +163 -0
- package/dist/tools/prepare-context.js +7 -0
- package/dist/tools/recall.js +25 -4
- package/dist/tools/search-docs.d.ts +38 -0
- package/dist/tools/search-docs.js +94 -0
- package/dist/tools/search.js +11 -1
- package/dist/tools/session-close.js +76 -7
- package/dist/tools/session-start.js +57 -5
- package/package.json +1 -1
- 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
|
|
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");
|