gitmem-mcp 1.5.1 → 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.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * GitMem Pro Deactivation
3
+ *
4
+ * 1. Calls gitmem_deactivate_device RPC to remove this device server-side
5
+ * 2. Removes api_key, supabase_url, supabase_key, openrouter_key from config.json
6
+ * 3. Deletes license-cache.json
7
+ * Does NOT remove .gitmem/ directory or local data.
8
+ */
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ import { getGitmemDir, getInstallId } from "../services/gitmem-dir.js";
12
+ import { clearLicenseCache, getLicenseKey, getValidationUrl, } from "../services/license.js";
13
+ // Same infra endpoint as validation — just different RPC
14
+ const DEACTIVATION_URL = getValidationUrl().replace("gitmem_validate_license", "gitmem_deactivate_device");
15
+ const VALIDATION_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNqcHR4eWV6dXhkaWludWZncnJtIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjYxODY3MDMsImV4cCI6MjA4MTc2MjcwM30.L0oZy3LYCMikmZ15IUU5DnfJmucM37DJ14nUkM3AreY";
16
+ async function deactivateDeviceRemote(apiKey, installId) {
17
+ try {
18
+ const controller = new AbortController();
19
+ const timeout = setTimeout(() => controller.abort(), 10000);
20
+ const response = await fetch(DEACTIVATION_URL, {
21
+ method: "POST",
22
+ headers: {
23
+ "Content-Type": "application/json",
24
+ apikey: VALIDATION_ANON_KEY,
25
+ Authorization: `Bearer ${VALIDATION_ANON_KEY}`,
26
+ },
27
+ body: JSON.stringify({ p_api_key: apiKey, p_install_id: installId }),
28
+ signal: controller.signal,
29
+ });
30
+ clearTimeout(timeout);
31
+ if (!response.ok) {
32
+ return { success: false, message: `HTTP ${response.status}` };
33
+ }
34
+ const rows = (await response.json());
35
+ const data = Array.isArray(rows) ? rows[0] : rows;
36
+ return data || { success: false, message: "Empty response" };
37
+ }
38
+ catch (err) {
39
+ const message = err instanceof Error ? err.message : "Unknown error";
40
+ return { success: false, message: `Network error: ${message}` };
41
+ }
42
+ }
43
+ export async function main(_args) {
44
+ const gitmemDir = getGitmemDir();
45
+ const configPath = path.join(gitmemDir, "config.json");
46
+ if (!fs.existsSync(configPath)) {
47
+ console.log("No config.json found — nothing to deactivate.");
48
+ return;
49
+ }
50
+ let config = {};
51
+ try {
52
+ config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
53
+ }
54
+ catch {
55
+ console.error("Error reading config.json");
56
+ process.exit(1);
57
+ }
58
+ const apiKey = getLicenseKey();
59
+ const installId = getInstallId();
60
+ const hadKey = !!apiKey;
61
+ // Step 1: Remove device server-side (if we have both key and install_id)
62
+ if (apiKey && installId) {
63
+ const result = await deactivateDeviceRemote(apiKey, installId);
64
+ if (result.success) {
65
+ console.log(` ✓ ${result.message}`);
66
+ }
67
+ else {
68
+ console.log(` ⚠ Server deactivation failed: ${result.message}`);
69
+ console.log(" Local credentials will still be removed.");
70
+ }
71
+ }
72
+ // Step 2: Remove Pro credentials from config
73
+ delete config.api_key;
74
+ delete config.supabase_url;
75
+ delete config.supabase_key;
76
+ delete config.openrouter_key;
77
+ // Write back config (preserving project, install_id, feedback_enabled)
78
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
79
+ // Step 3: Clear license cache
80
+ clearLicenseCache();
81
+ if (hadKey) {
82
+ console.log("\nPro tier deactivated.");
83
+ console.log(" - Device removed from license server");
84
+ console.log(" - License key removed from config.json");
85
+ console.log(" - Supabase and OpenRouter credentials removed");
86
+ console.log(" - License cache cleared");
87
+ console.log("");
88
+ console.log("Local data in .gitmem/ is preserved (scars, threads, sessions).");
89
+ console.log("Restart your editor to switch to free tier.");
90
+ }
91
+ else {
92
+ console.log("No active Pro license found. Already on free tier.");
93
+ }
94
+ }
95
+ //# sourceMappingURL=deactivate.js.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Local-to-Supabase Migration
3
+ *
4
+ * Migrates existing free-tier local .gitmem/ data to Supabase when
5
+ * a user upgrades to Pro. Called during `activate` after schema is
6
+ * verified and credentials are saved.
7
+ *
8
+ * Collections migrated:
9
+ * - learnings (scars, wins, patterns, anti-patterns)
10
+ * - sessions
11
+ * - decisions
12
+ * - scar_usage
13
+ *
14
+ * Threads are NOT migrated — they remain local (thread lifecycle is
15
+ * tied to .gitmem/threads.json and managed by session_start).
16
+ *
17
+ * Migration is idempotent: uses Supabase upsert (merge-duplicates)
18
+ * so re-running is safe. Existing Supabase records with same ID are
19
+ * updated, not duplicated.
20
+ */
21
+ export interface MigrationResult {
22
+ migrated: Record<string, number>;
23
+ skipped: Record<string, number>;
24
+ errors: Record<string, string[]>;
25
+ total: number;
26
+ hasLocalData: boolean;
27
+ }
28
+ /**
29
+ * Check if there is local data worth migrating
30
+ */
31
+ export declare function hasLocalData(gitmemDir?: string): boolean;
32
+ /**
33
+ * Migrate local .gitmem data to Supabase
34
+ *
35
+ * @param supabaseUrl - User's Supabase project URL
36
+ * @param supabaseKey - User's service role key
37
+ * @param tablePrefix - Table prefix (default: "gitmem_")
38
+ * @param gitmemDir - Override .gitmem directory path
39
+ * @param onProgress - Callback for progress reporting
40
+ */
41
+ export declare function migrateLocalToSupabase(opts: {
42
+ supabaseUrl: string;
43
+ supabaseKey: string;
44
+ tablePrefix?: string;
45
+ gitmemDir?: string;
46
+ onProgress?: (msg: string) => void;
47
+ }): Promise<MigrationResult>;
48
+ /**
49
+ * Rename local collection files after successful migration
50
+ * Adds .pre-migration suffix so data isn't lost but won't be re-read by free tier
51
+ */
52
+ export declare function archiveLocalData(gitmemDir?: string): string[];
53
+ //# sourceMappingURL=migrate-local.d.ts.map
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Local-to-Supabase Migration
3
+ *
4
+ * Migrates existing free-tier local .gitmem/ data to Supabase when
5
+ * a user upgrades to Pro. Called during `activate` after schema is
6
+ * verified and credentials are saved.
7
+ *
8
+ * Collections migrated:
9
+ * - learnings (scars, wins, patterns, anti-patterns)
10
+ * - sessions
11
+ * - decisions
12
+ * - scar_usage
13
+ *
14
+ * Threads are NOT migrated — they remain local (thread lifecycle is
15
+ * tied to .gitmem/threads.json and managed by session_start).
16
+ *
17
+ * Migration is idempotent: uses Supabase upsert (merge-duplicates)
18
+ * so re-running is safe. Existing Supabase records with same ID are
19
+ * updated, not duplicated.
20
+ */
21
+ import * as fs from "fs";
22
+ import * as path from "path";
23
+ import { getGitmemDir } from "../services/gitmem-dir.js";
24
+ /** Collections that map to Supabase tables */
25
+ const MIGRATABLE_COLLECTIONS = ["learnings", "sessions", "decisions", "scar_usage"];
26
+ /** Fields that should NOT be sent to Supabase (local-only or computed) */
27
+ const STRIP_FIELDS = new Set(["is_starter"]);
28
+ /** Fields that Supabase will reject if null (remove instead of sending null) */
29
+ const NULLABLE_STRIP = new Set(["embedding"]);
30
+ /**
31
+ * Check if there is local data worth migrating
32
+ */
33
+ export function hasLocalData(gitmemDir) {
34
+ const dir = gitmemDir || getGitmemDir();
35
+ for (const collection of MIGRATABLE_COLLECTIONS) {
36
+ const filePath = path.join(dir, `${collection}.json`);
37
+ if (fs.existsSync(filePath)) {
38
+ try {
39
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
40
+ if (Array.isArray(data) && data.length > 0)
41
+ return true;
42
+ }
43
+ catch {
44
+ // Corrupt file — skip
45
+ }
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+ /**
51
+ * Read a local collection JSON file
52
+ */
53
+ function readLocalCollection(dir, collection) {
54
+ const filePath = path.join(dir, `${collection}.json`);
55
+ if (!fs.existsSync(filePath))
56
+ return [];
57
+ try {
58
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
59
+ return Array.isArray(data) ? data : [];
60
+ }
61
+ catch {
62
+ return [];
63
+ }
64
+ }
65
+ /**
66
+ * Clean a record for Supabase insertion:
67
+ * - Strip local-only fields
68
+ * - Remove null values for non-nullable columns
69
+ * - Ensure id exists
70
+ */
71
+ function cleanRecord(record) {
72
+ if (!record.id)
73
+ return null;
74
+ const cleaned = {};
75
+ for (const [key, value] of Object.entries(record)) {
76
+ if (STRIP_FIELDS.has(key))
77
+ continue;
78
+ if (NULLABLE_STRIP.has(key) && (value === null || value === undefined))
79
+ continue;
80
+ cleaned[key] = value;
81
+ }
82
+ return cleaned;
83
+ }
84
+ /**
85
+ * Migrate local .gitmem data to Supabase
86
+ *
87
+ * @param supabaseUrl - User's Supabase project URL
88
+ * @param supabaseKey - User's service role key
89
+ * @param tablePrefix - Table prefix (default: "gitmem_")
90
+ * @param gitmemDir - Override .gitmem directory path
91
+ * @param onProgress - Callback for progress reporting
92
+ */
93
+ export async function migrateLocalToSupabase(opts) {
94
+ const { supabaseUrl, supabaseKey, tablePrefix = "gitmem_", onProgress } = opts;
95
+ const dir = opts.gitmemDir || getGitmemDir();
96
+ const log = onProgress || ((msg) => console.log(msg));
97
+ const result = {
98
+ migrated: {},
99
+ skipped: {},
100
+ errors: {},
101
+ total: 0,
102
+ hasLocalData: false,
103
+ };
104
+ const restUrl = `${supabaseUrl}/rest/v1`;
105
+ for (const collection of MIGRATABLE_COLLECTIONS) {
106
+ const records = readLocalCollection(dir, collection);
107
+ const tableName = `${tablePrefix}${collection}`;
108
+ result.migrated[collection] = 0;
109
+ result.skipped[collection] = 0;
110
+ result.errors[collection] = [];
111
+ if (records.length === 0)
112
+ continue;
113
+ result.hasLocalData = true;
114
+ log(` Migrating ${records.length} ${collection}...`);
115
+ for (const record of records) {
116
+ const cleaned = cleanRecord(record);
117
+ if (!cleaned) {
118
+ result.skipped[collection]++;
119
+ continue;
120
+ }
121
+ try {
122
+ const response = await fetch(`${restUrl}/${tableName}`, {
123
+ method: "POST",
124
+ headers: {
125
+ "apikey": supabaseKey,
126
+ "Authorization": `Bearer ${supabaseKey}`,
127
+ "Content-Type": "application/json",
128
+ "Prefer": "return=minimal,resolution=merge-duplicates",
129
+ "Content-Profile": "public",
130
+ },
131
+ body: JSON.stringify(cleaned),
132
+ signal: AbortSignal.timeout(10_000),
133
+ });
134
+ if (response.ok) {
135
+ result.migrated[collection]++;
136
+ }
137
+ else {
138
+ const text = await response.text();
139
+ // Only log first 3 errors per collection to avoid spam
140
+ if (result.errors[collection].length < 3) {
141
+ result.errors[collection].push(`${String(cleaned.id).substring(0, 8)}: ${response.status} - ${text.substring(0, 100)}`);
142
+ }
143
+ result.skipped[collection]++;
144
+ }
145
+ }
146
+ catch (err) {
147
+ if (result.errors[collection].length < 3) {
148
+ result.errors[collection].push(`${String(cleaned.id).substring(0, 8)}: ${err instanceof Error ? err.message : "Unknown error"}`);
149
+ }
150
+ result.skipped[collection]++;
151
+ }
152
+ result.total++;
153
+ }
154
+ }
155
+ return result;
156
+ }
157
+ /**
158
+ * Rename local collection files after successful migration
159
+ * Adds .pre-migration suffix so data isn't lost but won't be re-read by free tier
160
+ */
161
+ export function archiveLocalData(gitmemDir) {
162
+ const dir = gitmemDir || getGitmemDir();
163
+ const archived = [];
164
+ for (const collection of MIGRATABLE_COLLECTIONS) {
165
+ const filePath = path.join(dir, `${collection}.json`);
166
+ if (fs.existsSync(filePath)) {
167
+ const archivePath = `${filePath}.pre-migration`;
168
+ // Don't overwrite existing archive
169
+ if (!fs.existsSync(archivePath)) {
170
+ fs.renameSync(filePath, archivePath);
171
+ archived.push(collection);
172
+ }
173
+ }
174
+ }
175
+ return archived;
176
+ }
177
+ //# sourceMappingURL=migrate-local.js.map
@@ -13,14 +13,14 @@ export declare const LogParamsSchema: z.ZodObject<{
13
13
  since: z.ZodOptional<z.ZodNumber>;
14
14
  }, "strip", z.ZodTypeAny, {
15
15
  limit?: number | undefined;
16
- project?: string | undefined;
17
16
  learning_type?: "scar" | "win" | "pattern" | "anti_pattern" | undefined;
17
+ project?: string | undefined;
18
18
  severity?: "critical" | "high" | "medium" | "low" | undefined;
19
19
  since?: number | undefined;
20
20
  }, {
21
21
  limit?: number | undefined;
22
- project?: string | undefined;
23
22
  learning_type?: "scar" | "win" | "pattern" | "anti_pattern" | undefined;
23
+ project?: string | undefined;
24
24
  severity?: "critical" | "high" | "medium" | "low" | undefined;
25
25
  since?: number | undefined;
26
26
  }>;
@@ -14,14 +14,14 @@ export declare const SearchParamsSchema: z.ZodObject<{
14
14
  }, "strip", z.ZodTypeAny, {
15
15
  query: string;
16
16
  match_count?: number | undefined;
17
- project?: string | undefined;
18
17
  learning_type?: "scar" | "win" | "pattern" | "anti_pattern" | undefined;
18
+ project?: string | undefined;
19
19
  severity?: "critical" | "high" | "medium" | "low" | undefined;
20
20
  }, {
21
21
  query: string;
22
22
  match_count?: number | undefined;
23
- project?: string | undefined;
24
23
  learning_type?: "scar" | "win" | "pattern" | "anti_pattern" | undefined;
24
+ project?: string | undefined;
25
25
  severity?: "critical" | "high" | "medium" | "low" | undefined;
26
26
  }>;
27
27
  export type SearchParams = z.infer<typeof SearchParamsSchema>;
@@ -20,20 +20,20 @@ export declare const ClosingReflectionSchema: z.ZodObject<{
20
20
  rapport_notes: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodEffects<z.ZodArray<z.ZodString, "many">, string, string[]>]>>;
21
21
  }, "strip", z.ZodTypeAny, {
22
22
  what_broke: string;
23
- what_took_longer: string;
24
- do_differently: string;
25
23
  what_worked: string;
26
24
  wrong_assumption: string;
25
+ do_differently: string;
26
+ what_took_longer: string;
27
27
  scars_applied: string | string[];
28
28
  institutional_memory_items?: string | undefined;
29
29
  collaborative_dynamic?: string | undefined;
30
30
  rapport_notes?: string | undefined;
31
31
  }, {
32
32
  what_broke: string;
33
- what_took_longer: string;
34
- do_differently: string;
35
33
  what_worked: string;
36
34
  wrong_assumption: string;
35
+ do_differently: string;
36
+ what_took_longer: string;
37
37
  scars_applied: string | string[];
38
38
  institutional_memory_items?: string | string[] | undefined;
39
39
  collaborative_dynamic?: string | string[] | undefined;
@@ -161,20 +161,20 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
161
161
  rapport_notes: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodEffects<z.ZodArray<z.ZodString, "many">, string, string[]>]>>;
162
162
  }, "strip", z.ZodTypeAny, {
163
163
  what_broke: string;
164
- what_took_longer: string;
165
- do_differently: string;
166
164
  what_worked: string;
167
165
  wrong_assumption: string;
166
+ do_differently: string;
167
+ what_took_longer: string;
168
168
  scars_applied: string | string[];
169
169
  institutional_memory_items?: string | undefined;
170
170
  collaborative_dynamic?: string | undefined;
171
171
  rapport_notes?: string | undefined;
172
172
  }, {
173
173
  what_broke: string;
174
- what_took_longer: string;
175
- do_differently: string;
176
174
  what_worked: string;
177
175
  wrong_assumption: string;
176
+ do_differently: string;
177
+ what_took_longer: string;
178
178
  scars_applied: string | string[];
179
179
  institutional_memory_items?: string | string[] | undefined;
180
180
  collaborative_dynamic?: string | string[] | undefined;
@@ -281,10 +281,10 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
281
281
  linear_issue?: string | undefined;
282
282
  closing_reflection?: {
283
283
  what_broke: string;
284
- what_took_longer: string;
285
- do_differently: string;
286
284
  what_worked: string;
287
285
  wrong_assumption: string;
286
+ do_differently: string;
287
+ what_took_longer: string;
288
288
  scars_applied: string | string[];
289
289
  institutional_memory_items?: string | undefined;
290
290
  collaborative_dynamic?: string | undefined;
@@ -338,10 +338,10 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
338
338
  linear_issue?: string | undefined;
339
339
  closing_reflection?: {
340
340
  what_broke: string;
341
- what_took_longer: string;
342
- do_differently: string;
343
341
  what_worked: string;
344
342
  wrong_assumption: string;
343
+ do_differently: string;
344
+ what_took_longer: string;
345
345
  scars_applied: string | string[];
346
346
  institutional_memory_items?: string | string[] | undefined;
347
347
  collaborative_dynamic?: string | string[] | undefined;
package/dist/server.js CHANGED
@@ -43,7 +43,9 @@ import { getEffectTracker } from "./services/effect-tracker.js";
43
43
  import { RIPPLE, ANSI } from "./services/display-protocol.js";
44
44
  import { getProject } from "./services/session-state.js";
45
45
  import { checkEnforcement } from "./services/enforcement.js";
46
- import { getTier, hasSupabase, hasCacheManagement, hasBatchOperations, hasTranscripts, } from "./services/tier.js";
46
+ import { getTier, setTier, hasSupabase, hasCacheManagement, hasBatchOperations, hasTranscripts, } from "./services/tier.js";
47
+ import { getLicenseKey, validateLicense } from "./services/license.js";
48
+ import { getInstallId } from "./services/gitmem-dir.js";
47
49
  import { getRegisteredTools } from "./tools/definitions.js";
48
50
  import { validateToolArgs } from "./schemas/registry.js";
49
51
  /**
@@ -393,13 +395,29 @@ export function createServer() {
393
395
  */
394
396
  export async function runServer() {
395
397
  const tier = getTier();
396
- // Start server immediately (don't block on cache loading)
398
+ // Start server immediately (don't block on cache loading or license validation)
397
399
  const server = createServer();
398
400
  const transport = new StdioServerTransport();
399
401
  await server.connect(transport);
400
402
  const toolCount = getRegisteredTools().length;
401
403
  const storage = hasSupabase() ? "supabase" : "local";
402
404
  console.error(`[gitmem] Tier: ${tier} | Storage: ${storage} | Tools: ${toolCount}`);
405
+ // Async license validation (non-blocking)
406
+ const apiKey = getLicenseKey();
407
+ if (apiKey) {
408
+ const installId = getInstallId() || "unknown";
409
+ validateLicense(apiKey, installId).then((result) => {
410
+ if (!result.valid) {
411
+ console.error(`[gitmem:license] Validation failed: ${result.message}`);
412
+ setTier("free");
413
+ }
414
+ else {
415
+ console.error(`[gitmem:license] Validated: ${result.tier} tier`);
416
+ }
417
+ }).catch((err) => {
418
+ console.error(`[gitmem:license] Validation error: ${err}`);
419
+ });
420
+ }
403
421
  if (hasSupabase()) {
404
422
  // Pro/Dev: Initialize local vector search in background (non-blocking)
405
423
  // This loads scars with embeddings directly from Supabase REST API
@@ -173,6 +173,28 @@ export declare function aggregateClosingReflections(sessions: SessionRecord[], m
173
173
  wrong_assumptions: ReflectionCategory;
174
174
  do_differently: ReflectionCategory;
175
175
  };
176
+ /**
177
+ * Lightweight summary for session_start Pro insights.
178
+ * Returns a compact object with key 30-day metrics.
179
+ */
180
+ export interface LightweightSummary {
181
+ total_sessions: number;
182
+ scars_surfaced: number;
183
+ scars_applied: number;
184
+ application_rate: number;
185
+ top_blindspot: {
186
+ title: string;
187
+ ignore_rate: string;
188
+ times: string;
189
+ } | null;
190
+ }
191
+ export declare function computeLightweightSummary(sessions: SessionRecord[], usages: ScarUsageRecord[]): LightweightSummary;
192
+ /**
193
+ * Format blindspot snippet for session_close display.
194
+ * Returns 2-3 lines showing top 2 most-ignored scars, or null if none qualify.
195
+ * Threshold: ignore_rate > 50%, surfaced >= 3 times.
196
+ */
197
+ export declare function formatBlindspotSnippet(usages: ScarUsageRecord[]): string | null;
176
198
  /**
177
199
  * Format summary analytics as compact markdown.
178
200
  */
@@ -356,6 +356,74 @@ export function aggregateClosingReflections(sessions, maxPerCategory = 30, maxTe
356
356
  do_differently: truncate(all.do_differently),
357
357
  };
358
358
  }
359
+ export function computeLightweightSummary(sessions, usages) {
360
+ const totalSessions = sessions.length;
361
+ const scarsSurfaced = new Set(usages.map(u => u.scar_id)).size;
362
+ const applied = usages.filter(u => u.reference_type !== "none").length;
363
+ const applicationRate = usages.length > 0 ? applied / usages.length : 0;
364
+ // Find top blindspot: most-ignored scar with >= 3 surfacings
365
+ const scarGroups = new Map();
366
+ for (const u of usages) {
367
+ const entry = scarGroups.get(u.scar_id) || { title: u.scar_title || "Unknown", surfaced: 0, dismissed: 0 };
368
+ entry.surfaced++;
369
+ if (u.reference_type === "none")
370
+ entry.dismissed++;
371
+ if (u.scar_title)
372
+ entry.title = u.scar_title;
373
+ scarGroups.set(u.scar_id, entry);
374
+ }
375
+ let topBlindspot = null;
376
+ let highestIgnoreRate = 0;
377
+ for (const [, stats] of scarGroups) {
378
+ if (stats.surfaced >= 3) {
379
+ const rate = stats.dismissed / stats.surfaced;
380
+ if (rate > highestIgnoreRate && rate > 0.5) {
381
+ highestIgnoreRate = rate;
382
+ topBlindspot = {
383
+ title: stats.title,
384
+ ignore_rate: `${Math.round(rate * 100)}%`,
385
+ times: `${stats.dismissed}/${stats.surfaced}`,
386
+ };
387
+ }
388
+ }
389
+ }
390
+ return {
391
+ total_sessions: totalSessions,
392
+ scars_surfaced: scarsSurfaced,
393
+ scars_applied: applied,
394
+ application_rate: applicationRate,
395
+ top_blindspot: topBlindspot,
396
+ };
397
+ }
398
+ /**
399
+ * Format blindspot snippet for session_close display.
400
+ * Returns 2-3 lines showing top 2 most-ignored scars, or null if none qualify.
401
+ * Threshold: ignore_rate > 50%, surfaced >= 3 times.
402
+ */
403
+ export function formatBlindspotSnippet(usages) {
404
+ const scarGroups = new Map();
405
+ for (const u of usages) {
406
+ const entry = scarGroups.get(u.scar_id) || { title: u.scar_title || "Unknown", surfaced: 0, dismissed: 0 };
407
+ entry.surfaced++;
408
+ if (u.reference_type === "none")
409
+ entry.dismissed++;
410
+ if (u.scar_title)
411
+ entry.title = u.scar_title;
412
+ scarGroups.set(u.scar_id, entry);
413
+ }
414
+ const blindspots = Array.from(scarGroups.values())
415
+ .filter(s => s.surfaced >= 3 && (s.dismissed / s.surfaced) > 0.5)
416
+ .sort((a, b) => (b.dismissed / b.surfaced) - (a.dismissed / a.surfaced))
417
+ .slice(0, 2);
418
+ if (blindspots.length === 0)
419
+ return null;
420
+ const lines = ["Blindspots (30d)"];
421
+ for (const b of blindspots) {
422
+ const pct = Math.round((b.dismissed / b.surfaced) * 100);
423
+ lines.push(` "${b.title}" — ignored ${pct}% (${b.dismissed}/${b.surfaced} times)`);
424
+ }
425
+ return lines.join("\n");
426
+ }
359
427
  // --- Formatters ---
360
428
  /**
361
429
  * Format summary analytics as compact markdown.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * License Key Validation for GitMem Pro Tier
3
+ *
4
+ * Detection chain:
5
+ * 1. GITMEM_TIER env var (explicit override — testing/dev)
6
+ * 2. api_key in config.json or GITMEM_API_KEY env var:
7
+ * a. Check license-cache.json → if valid + not expired (72h) → return cached tier
8
+ * b. No cache → optimistic "pro" (validated async in runServer)
9
+ * 3. No key + no SUPABASE_URL + no config.supabase_url → free
10
+ * 4. No key + SUPABASE_URL set (env var) → pro (backward compat for us)
11
+ *
12
+ * Async validation (validateLicense()):
13
+ * - Called in runServer() startup (non-blocking)
14
+ * - POST to hardcoded validation endpoint with api_key + install_id
15
+ * - Success: cache to ~/.gitmem/license-cache.json (72h TTL)
16
+ * - Failure: downgrade _tier to free, log warning
17
+ * - Network error: honor existing cache if valid, else downgrade
18
+ */
19
+ export interface LicenseValidationResult {
20
+ valid: boolean;
21
+ tier: string | null;
22
+ message: string;
23
+ }
24
+ /**
25
+ * Get license key from env var or config.json
26
+ */
27
+ export declare function getLicenseKey(): string | null;
28
+ /**
29
+ * Get Pro config (Supabase + OpenRouter credentials) from config.json
30
+ * Env vars override config.json values.
31
+ */
32
+ export declare function getProConfig(): {
33
+ supabaseUrl: string;
34
+ supabaseKey: string;
35
+ openrouterKey: string;
36
+ };
37
+ /**
38
+ * Delete license cache (used by deactivate)
39
+ */
40
+ export declare function clearLicenseCache(): void;
41
+ /**
42
+ * Check if license key has a valid cached result (non-async, for tier detection)
43
+ */
44
+ export declare function getCachedLicenseTier(): string | null;
45
+ /**
46
+ * Validate license key against GitMem's Supabase RPC endpoint.
47
+ * Calls gitmem_validate_license() via PostgREST using the anon key.
48
+ * Returns the validation result.
49
+ *
50
+ * This is async and should be called non-blocking during startup.
51
+ */
52
+ export declare function validateLicense(apiKey: string, installId: string): Promise<LicenseValidationResult>;
53
+ /**
54
+ * Get the validation URL (for diagnostics/testing)
55
+ */
56
+ export declare function getValidationUrl(): string;
57
+ //# sourceMappingURL=license.d.ts.map