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.
- package/CHANGELOG.md +9 -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/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/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/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
|
@@ -0,0 +1,200 @@
|
|
|
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
|
+
import * as fs from "fs";
|
|
20
|
+
import * as path from "path";
|
|
21
|
+
import { getGitmemDir } from "./gitmem-dir.js";
|
|
22
|
+
// Hardcoded validation endpoint — calls RPC directly on our Supabase via PostgREST.
|
|
23
|
+
// Users never see or configure this URL.
|
|
24
|
+
const VALIDATION_URL = "https://cjptxyezuxdiinufgrrm.supabase.co/rest/v1/rpc/gitmem_validate_license";
|
|
25
|
+
// Anon key for our project (safe to embed — RPC is SECURITY DEFINER, RLS blocks direct table access)
|
|
26
|
+
const VALIDATION_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNqcHR4eWV6dXhkaWludWZncnJtIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjYxODY3MDMsImV4cCI6MjA4MTc2MjcwM30.L0oZy3LYCMikmZ15IUU5DnfJmucM37DJ14nUkM3AreY";
|
|
27
|
+
// Cache TTL: 72 hours
|
|
28
|
+
const CACHE_TTL_MS = 72 * 60 * 60 * 1000;
|
|
29
|
+
/**
|
|
30
|
+
* Get license key from env var or config.json
|
|
31
|
+
*/
|
|
32
|
+
export function getLicenseKey() {
|
|
33
|
+
// Env var takes priority
|
|
34
|
+
const envKey = process.env.GITMEM_API_KEY;
|
|
35
|
+
if (envKey)
|
|
36
|
+
return envKey;
|
|
37
|
+
// Read from config.json
|
|
38
|
+
try {
|
|
39
|
+
const configPath = path.join(getGitmemDir(), "config.json");
|
|
40
|
+
if (fs.existsSync(configPath)) {
|
|
41
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
42
|
+
if (raw.api_key && typeof raw.api_key === "string") {
|
|
43
|
+
return raw.api_key;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// File doesn't exist or is invalid
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get Pro config (Supabase + OpenRouter credentials) from config.json
|
|
54
|
+
* Env vars override config.json values.
|
|
55
|
+
*/
|
|
56
|
+
export function getProConfig() {
|
|
57
|
+
let supabaseUrl = process.env.SUPABASE_URL || "";
|
|
58
|
+
let supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_KEY || "";
|
|
59
|
+
let openrouterKey = process.env.OPENROUTER_API_KEY || "";
|
|
60
|
+
// Fall back to config.json
|
|
61
|
+
try {
|
|
62
|
+
const configPath = path.join(getGitmemDir(), "config.json");
|
|
63
|
+
if (fs.existsSync(configPath)) {
|
|
64
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
65
|
+
if (!supabaseUrl && raw.supabase_url)
|
|
66
|
+
supabaseUrl = raw.supabase_url;
|
|
67
|
+
if (!supabaseKey && raw.supabase_key)
|
|
68
|
+
supabaseKey = raw.supabase_key;
|
|
69
|
+
if (!openrouterKey && raw.openrouter_key)
|
|
70
|
+
openrouterKey = raw.openrouter_key;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// File doesn't exist or is invalid
|
|
75
|
+
}
|
|
76
|
+
return { supabaseUrl, supabaseKey, openrouterKey };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Read cached license validation result
|
|
80
|
+
*/
|
|
81
|
+
function readLicenseCache() {
|
|
82
|
+
try {
|
|
83
|
+
const cachePath = path.join(getGitmemDir(), "license-cache.json");
|
|
84
|
+
if (!fs.existsSync(cachePath))
|
|
85
|
+
return null;
|
|
86
|
+
const raw = JSON.parse(fs.readFileSync(cachePath, "utf-8"));
|
|
87
|
+
const validatedAt = new Date(raw.validated_at).getTime();
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
// Check TTL
|
|
90
|
+
if (now - validatedAt > CACHE_TTL_MS) {
|
|
91
|
+
console.error("[gitmem:license] Cache expired");
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return raw;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Write license validation result to cache
|
|
102
|
+
*/
|
|
103
|
+
function writeLicenseCache(result) {
|
|
104
|
+
try {
|
|
105
|
+
const gitmemDir = getGitmemDir();
|
|
106
|
+
if (!fs.existsSync(gitmemDir)) {
|
|
107
|
+
fs.mkdirSync(gitmemDir, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
const cachePath = path.join(gitmemDir, "license-cache.json");
|
|
110
|
+
fs.writeFileSync(cachePath, JSON.stringify(result, null, 2));
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
console.error("[gitmem:license] Failed to write cache:", err);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Delete license cache (used by deactivate)
|
|
118
|
+
*/
|
|
119
|
+
export function clearLicenseCache() {
|
|
120
|
+
try {
|
|
121
|
+
const cachePath = path.join(getGitmemDir(), "license-cache.json");
|
|
122
|
+
if (fs.existsSync(cachePath)) {
|
|
123
|
+
fs.unlinkSync(cachePath);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Ignore
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Check if license key has a valid cached result (non-async, for tier detection)
|
|
132
|
+
*/
|
|
133
|
+
export function getCachedLicenseTier() {
|
|
134
|
+
const cache = readLicenseCache();
|
|
135
|
+
if (cache && cache.valid) {
|
|
136
|
+
return cache.tier;
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Validate license key against GitMem's Supabase RPC endpoint.
|
|
142
|
+
* Calls gitmem_validate_license() via PostgREST using the anon key.
|
|
143
|
+
* Returns the validation result.
|
|
144
|
+
*
|
|
145
|
+
* This is async and should be called non-blocking during startup.
|
|
146
|
+
*/
|
|
147
|
+
export async function validateLicense(apiKey, installId) {
|
|
148
|
+
try {
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
const timeout = setTimeout(() => controller.abort(), 10000); // 10s timeout
|
|
151
|
+
const response = await fetch(VALIDATION_URL, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
headers: {
|
|
154
|
+
"Content-Type": "application/json",
|
|
155
|
+
apikey: VALIDATION_ANON_KEY,
|
|
156
|
+
Authorization: `Bearer ${VALIDATION_ANON_KEY}`,
|
|
157
|
+
},
|
|
158
|
+
body: JSON.stringify({ p_api_key: apiKey, p_install_id: installId }),
|
|
159
|
+
signal: controller.signal,
|
|
160
|
+
});
|
|
161
|
+
clearTimeout(timeout);
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
const text = await response.text().catch(() => "");
|
|
164
|
+
return { valid: false, tier: null, message: `HTTP ${response.status}: ${text}` };
|
|
165
|
+
}
|
|
166
|
+
// PostgREST RPC returns an array of rows
|
|
167
|
+
const rows = await response.json();
|
|
168
|
+
const data = Array.isArray(rows) ? rows[0] : rows;
|
|
169
|
+
if (!data) {
|
|
170
|
+
return { valid: false, tier: null, message: "Empty validation response" };
|
|
171
|
+
}
|
|
172
|
+
// Cache successful validation
|
|
173
|
+
if (data.valid && data.tier) {
|
|
174
|
+
writeLicenseCache({
|
|
175
|
+
valid: true,
|
|
176
|
+
tier: data.tier,
|
|
177
|
+
validated_at: new Date().toISOString(),
|
|
178
|
+
api_key_prefix: apiKey.substring(0, 16) + "...",
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return data;
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
185
|
+
// Network error: honor existing cache
|
|
186
|
+
const cache = readLicenseCache();
|
|
187
|
+
if (cache && cache.valid) {
|
|
188
|
+
console.error(`[gitmem:license] Network error, using cached validation: ${message}`);
|
|
189
|
+
return { valid: true, tier: cache.tier, message: "Using cached validation (offline)" };
|
|
190
|
+
}
|
|
191
|
+
return { valid: false, tier: null, message: `Network error: ${message}` };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get the validation URL (for diagnostics/testing)
|
|
196
|
+
*/
|
|
197
|
+
export function getValidationUrl() {
|
|
198
|
+
return VALIDATION_URL;
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=license.js.map
|
|
@@ -37,6 +37,9 @@ export declare function getRecord<T = unknown>(table: string, id: string): Promi
|
|
|
37
37
|
export declare function upsertRecord<T = unknown>(table: string, data: Record<string, unknown>): Promise<T>;
|
|
38
38
|
/**
|
|
39
39
|
* Semantic search across tables
|
|
40
|
+
*
|
|
41
|
+
* Generates an embedding for the query, then calls the gitmem_semantic_search
|
|
42
|
+
* RPC function directly via PostgREST.
|
|
40
43
|
*/
|
|
41
44
|
export declare function semanticSearch<T = unknown>(options: SupabaseSearchOptions): Promise<T[]>;
|
|
42
45
|
/**
|
|
@@ -115,6 +118,9 @@ export declare function directPatch<T = unknown>(table: string, filters: Record<
|
|
|
115
118
|
export declare function loadScarsWithEmbeddings<T = unknown>(project?: string, limit?: number): Promise<T[]>;
|
|
116
119
|
/**
|
|
117
120
|
* Scar search with severity weighting
|
|
121
|
+
*
|
|
122
|
+
* Generates an embedding for the query, then calls the gitmem_scar_search
|
|
123
|
+
* RPC function directly via PostgREST. No Edge Function required.
|
|
118
124
|
*/
|
|
119
125
|
export declare function scarSearch<T = unknown>(query: string, matchCount?: number, project?: Project): Promise<T[]>;
|
|
120
126
|
/**
|
|
@@ -45,9 +45,11 @@ export function escapePostgRESTValue(value) {
|
|
|
45
45
|
// PostgREST uses ( ) , as structural delimiters in or=() expressions
|
|
46
46
|
return value.replace(/[(),]/g, "");
|
|
47
47
|
}
|
|
48
|
-
// Configuration from environment
|
|
49
|
-
|
|
50
|
-
const
|
|
48
|
+
// Configuration from environment, with config.json fallback for Pro tier
|
|
49
|
+
import { getProConfig } from "./license.js";
|
|
50
|
+
const _proConfig = getProConfig();
|
|
51
|
+
const SUPABASE_URL = _proConfig.supabaseUrl;
|
|
52
|
+
const SUPABASE_KEY = _proConfig.supabaseKey;
|
|
51
53
|
// Direct REST API base URL
|
|
52
54
|
const SUPABASE_REST_URL = SUPABASE_URL ? `${SUPABASE_URL}/rest/v1` : "";
|
|
53
55
|
/**
|
|
@@ -141,21 +143,45 @@ export async function upsertRecord(table, data) {
|
|
|
141
143
|
}
|
|
142
144
|
/**
|
|
143
145
|
* Semantic search across tables
|
|
146
|
+
*
|
|
147
|
+
* Generates an embedding for the query, then calls the gitmem_semantic_search
|
|
148
|
+
* RPC function directly via PostgREST.
|
|
144
149
|
*/
|
|
145
150
|
export async function semanticSearch(options) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
query,
|
|
149
|
-
match_count,
|
|
150
|
-
};
|
|
151
|
-
if (tables && tables.length > 0) {
|
|
152
|
-
args.tables = tables;
|
|
151
|
+
if (!isConfigured()) {
|
|
152
|
+
throw new Error("Supabase not configured");
|
|
153
153
|
}
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
const { query, match_count = 10 } = options;
|
|
155
|
+
// Generate embedding for the query
|
|
156
|
+
const { embed } = await import("./embedding.js");
|
|
157
|
+
const embedding = await embed(query);
|
|
158
|
+
if (!embedding) {
|
|
159
|
+
console.error("[semantic-search] No embedding provider configured — cannot run semantic search");
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
// Call the RPC function directly via PostgREST
|
|
163
|
+
const rpcName = `${getTableName("").replace(/_$/, "")}_semantic_search`;
|
|
164
|
+
const url = `${SUPABASE_URL}/rest/v1/rpc/${rpcName}`;
|
|
165
|
+
const response = await fetch(url, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: {
|
|
168
|
+
"Content-Type": "application/json",
|
|
169
|
+
apikey: SUPABASE_KEY,
|
|
170
|
+
Authorization: `Bearer ${SUPABASE_KEY}`,
|
|
171
|
+
},
|
|
172
|
+
body: JSON.stringify({
|
|
173
|
+
query_embedding: `[${embedding.join(",")}]`,
|
|
174
|
+
match_count,
|
|
175
|
+
similarity_threshold: 0.0,
|
|
176
|
+
}),
|
|
177
|
+
signal: AbortSignal.timeout(15_000),
|
|
178
|
+
});
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
const text = await response.text();
|
|
181
|
+
throw new Error(`Supabase RPC error: ${response.status} - ${text.slice(0, 200)}`);
|
|
156
182
|
}
|
|
157
|
-
const
|
|
158
|
-
return
|
|
183
|
+
const rows = (await response.json());
|
|
184
|
+
return rows || [];
|
|
159
185
|
}
|
|
160
186
|
// ============================================================================
|
|
161
187
|
// DIRECT SUPABASE QUERIES (bypass ww-mcp for bulk operations)
|
|
@@ -408,17 +434,44 @@ export async function loadScarsWithEmbeddings(project, limit = 500) {
|
|
|
408
434
|
}
|
|
409
435
|
/**
|
|
410
436
|
* Scar search with severity weighting
|
|
437
|
+
*
|
|
438
|
+
* Generates an embedding for the query, then calls the gitmem_scar_search
|
|
439
|
+
* RPC function directly via PostgREST. No Edge Function required.
|
|
411
440
|
*/
|
|
412
441
|
export async function scarSearch(query, matchCount = 5, project) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
442
|
+
if (!isConfigured()) {
|
|
443
|
+
throw new Error("Supabase not configured");
|
|
444
|
+
}
|
|
445
|
+
// Generate embedding for the query
|
|
446
|
+
const { embed } = await import("./embedding.js");
|
|
447
|
+
const embedding = await embed(query);
|
|
448
|
+
if (!embedding) {
|
|
449
|
+
console.error("[scar-search] No embedding provider configured — cannot run semantic search");
|
|
450
|
+
return [];
|
|
451
|
+
}
|
|
452
|
+
// Call the RPC function directly via PostgREST
|
|
453
|
+
const rpcName = `${getTableName("").replace(/_$/, "")}_scar_search`;
|
|
454
|
+
const url = `${SUPABASE_URL}/rest/v1/rpc/${rpcName}`;
|
|
455
|
+
const response = await fetch(url, {
|
|
456
|
+
method: "POST",
|
|
457
|
+
headers: {
|
|
458
|
+
"Content-Type": "application/json",
|
|
459
|
+
apikey: SUPABASE_KEY,
|
|
460
|
+
Authorization: `Bearer ${SUPABASE_KEY}`,
|
|
461
|
+
},
|
|
462
|
+
body: JSON.stringify({
|
|
463
|
+
query_embedding: `[${embedding.join(",")}]`,
|
|
464
|
+
match_count: matchCount,
|
|
465
|
+
similarity_threshold: 0.0,
|
|
466
|
+
}),
|
|
467
|
+
signal: AbortSignal.timeout(15_000),
|
|
468
|
+
});
|
|
469
|
+
if (!response.ok) {
|
|
470
|
+
const text = await response.text();
|
|
471
|
+
throw new Error(`Supabase RPC error: ${response.status} - ${text.slice(0, 200)}`);
|
|
419
472
|
}
|
|
420
|
-
const
|
|
421
|
-
return
|
|
473
|
+
const rows = (await response.json());
|
|
474
|
+
return rows || [];
|
|
422
475
|
}
|
|
423
476
|
/**
|
|
424
477
|
* Scar search with caching
|
package/dist/services/tier.d.ts
CHANGED
|
@@ -6,15 +6,23 @@
|
|
|
6
6
|
* pro — Supabase + embeddings, semantic search, cloud persistence, variants
|
|
7
7
|
* dev — Everything in pro + compliance, transcripts, metrics
|
|
8
8
|
*
|
|
9
|
-
* Detection:
|
|
10
|
-
* GITMEM_TIER
|
|
11
|
-
*
|
|
9
|
+
* Detection chain:
|
|
10
|
+
* 1. GITMEM_TIER env var (explicit override — testing/dev)
|
|
11
|
+
* 2. api_key in config.json or GITMEM_API_KEY env var:
|
|
12
|
+
* a. Check license-cache.json → if valid + not expired (72h) → return cached tier
|
|
13
|
+
* b. No cache → optimistic "pro" (validated async in runServer)
|
|
14
|
+
* 3. No key + no SUPABASE_URL + no config.supabase_url → free
|
|
15
|
+
* 4. No key + SUPABASE_URL set (env var) → pro (backward compat for us)
|
|
12
16
|
*/
|
|
13
17
|
export type GitMemTier = "free" | "pro" | "dev";
|
|
14
18
|
/**
|
|
15
19
|
* Get the current tier (cached after first call)
|
|
16
20
|
*/
|
|
17
21
|
export declare function getTier(): GitMemTier;
|
|
22
|
+
/**
|
|
23
|
+
* Force-set tier (used by license validation on failure)
|
|
24
|
+
*/
|
|
25
|
+
export declare function setTier(tier: GitMemTier): void;
|
|
18
26
|
/**
|
|
19
27
|
* Reset tier detection (for testing)
|
|
20
28
|
*/
|
|
@@ -33,6 +41,8 @@ export declare function hasTranscripts(): boolean;
|
|
|
33
41
|
export declare function hasBatchOperations(): boolean;
|
|
34
42
|
/** Whether cache management tools are available (pro, dev) */
|
|
35
43
|
export declare function hasCacheManagement(): boolean;
|
|
44
|
+
/** Whether Pro-tier insights (decay tags, analytics snippets, blindspots) are active (pro, dev) */
|
|
45
|
+
export declare function hasProInsights(): boolean;
|
|
36
46
|
/** Whether detailed performance metrics recording is active (pro, dev — aligned with hasVariants) */
|
|
37
47
|
export declare function hasMetrics(): boolean;
|
|
38
48
|
/** Whether advanced agent detection (5-agent matrix) is active (dev only) */
|
package/dist/services/tier.js
CHANGED
|
@@ -6,23 +6,42 @@
|
|
|
6
6
|
* pro — Supabase + embeddings, semantic search, cloud persistence, variants
|
|
7
7
|
* dev — Everything in pro + compliance, transcripts, metrics
|
|
8
8
|
*
|
|
9
|
-
* Detection:
|
|
10
|
-
* GITMEM_TIER
|
|
11
|
-
*
|
|
9
|
+
* Detection chain:
|
|
10
|
+
* 1. GITMEM_TIER env var (explicit override — testing/dev)
|
|
11
|
+
* 2. api_key in config.json or GITMEM_API_KEY env var:
|
|
12
|
+
* a. Check license-cache.json → if valid + not expired (72h) → return cached tier
|
|
13
|
+
* b. No cache → optimistic "pro" (validated async in runServer)
|
|
14
|
+
* 3. No key + no SUPABASE_URL + no config.supabase_url → free
|
|
15
|
+
* 4. No key + SUPABASE_URL set (env var) → pro (backward compat for us)
|
|
12
16
|
*/
|
|
17
|
+
import { getLicenseKey, getCachedLicenseTier, getProConfig } from "./license.js";
|
|
13
18
|
let _tier = null;
|
|
14
19
|
/**
|
|
15
|
-
* Detect tier from environment variables
|
|
20
|
+
* Detect tier from environment variables, license key, and config
|
|
16
21
|
*/
|
|
17
22
|
function detectTier() {
|
|
23
|
+
// 1. Explicit override via env var (testing/dev)
|
|
18
24
|
const explicit = process.env.GITMEM_TIER?.toLowerCase();
|
|
19
25
|
if (explicit === "free" || explicit === "pro" || explicit === "dev") {
|
|
20
26
|
return explicit;
|
|
21
27
|
}
|
|
22
|
-
|
|
28
|
+
// 2. License key present → check cache or optimistic pro
|
|
29
|
+
const apiKey = getLicenseKey();
|
|
30
|
+
if (apiKey) {
|
|
31
|
+
// 2a. Check cached validation
|
|
32
|
+
const cachedTier = getCachedLicenseTier();
|
|
33
|
+
if (cachedTier === "pro" || cachedTier === "dev") {
|
|
34
|
+
return cachedTier;
|
|
35
|
+
}
|
|
36
|
+
// 2b. Key present but no valid cache → optimistic pro
|
|
37
|
+
// (validated async in runServer, downgraded if invalid)
|
|
38
|
+
return "pro";
|
|
39
|
+
}
|
|
40
|
+
// 3. No key — check for Supabase URL (env var or config.json)
|
|
41
|
+
const supabaseUrl = process.env.SUPABASE_URL || getProConfig().supabaseUrl;
|
|
23
42
|
if (!supabaseUrl)
|
|
24
43
|
return "free";
|
|
25
|
-
//
|
|
44
|
+
// 4. Supabase URL set but no license key → backward compat (internal dev)
|
|
26
45
|
if (process.env.GITMEM_DEV === "true" || process.env.GITMEM_DEV === "1") {
|
|
27
46
|
return "dev";
|
|
28
47
|
}
|
|
@@ -38,6 +57,13 @@ export function getTier() {
|
|
|
38
57
|
}
|
|
39
58
|
return _tier;
|
|
40
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Force-set tier (used by license validation on failure)
|
|
62
|
+
*/
|
|
63
|
+
export function setTier(tier) {
|
|
64
|
+
_tier = tier;
|
|
65
|
+
console.error(`[gitmem] Tier updated: ${tier}`);
|
|
66
|
+
}
|
|
41
67
|
/**
|
|
42
68
|
* Reset tier detection (for testing)
|
|
43
69
|
*/
|
|
@@ -75,6 +101,10 @@ export function hasBatchOperations() {
|
|
|
75
101
|
export function hasCacheManagement() {
|
|
76
102
|
return getTier() !== "free";
|
|
77
103
|
}
|
|
104
|
+
/** Whether Pro-tier insights (decay tags, analytics snippets, blindspots) are active (pro, dev) */
|
|
105
|
+
export function hasProInsights() {
|
|
106
|
+
return getTier() !== "free";
|
|
107
|
+
}
|
|
78
108
|
/** Whether detailed performance metrics recording is active (pro, dev — aligned with hasVariants) */
|
|
79
109
|
export function hasMetrics() {
|
|
80
110
|
return getTier() !== "free";
|
|
@@ -96,7 +126,8 @@ export function hasEnforcementFields() {
|
|
|
96
126
|
*/
|
|
97
127
|
export function getTablePrefix() {
|
|
98
128
|
// Default prefix for all tiers. Override with GITMEM_TABLE_PREFIX env var.
|
|
99
|
-
|
|
129
|
+
// User schema uses gitmem_ prefix. Orchestra infra uses orchestra_ (set via env var).
|
|
130
|
+
return process.env.GITMEM_TABLE_PREFIX || "gitmem_";
|
|
100
131
|
}
|
|
101
132
|
/**
|
|
102
133
|
* Get the fully-qualified table name for a base table name
|
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,
|