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
|
@@ -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.
|
|
@@ -23,7 +23,13 @@ interface EmbeddingConfig {
|
|
|
23
23
|
expectedDim: number;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
* Detect the best available embedding provider from environment
|
|
26
|
+
* Detect the best available embedding provider from environment.
|
|
27
|
+
*
|
|
28
|
+
* Resolution chain for OpenRouter key:
|
|
29
|
+
* 1. process.env.OPENROUTER_API_KEY (env var)
|
|
30
|
+
* 2. .gitmem/config.json → openrouter_key (written by `activate`)
|
|
31
|
+
*
|
|
32
|
+
* This matches how supabase-client.ts resolves credentials via getProConfig().
|
|
27
33
|
*/
|
|
28
34
|
export declare function detectProvider(): EmbeddingConfig;
|
|
29
35
|
/**
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* Records stored without embeddings can still be retrieved by ID/filters,
|
|
15
15
|
* but won't appear in semantic search results.
|
|
16
16
|
*/
|
|
17
|
+
import { getProConfig } from "./license.js";
|
|
17
18
|
// Default embedding dimensions per provider
|
|
18
19
|
const OPENAI_EMBEDDING_DIM = 1536;
|
|
19
20
|
const OLLAMA_DEFAULT_DIM = 768;
|
|
@@ -38,10 +39,25 @@ function normalize(vec) {
|
|
|
38
39
|
return vec.map((v) => v / magnitude);
|
|
39
40
|
}
|
|
40
41
|
/**
|
|
41
|
-
* Detect the best available embedding provider from environment
|
|
42
|
+
* Detect the best available embedding provider from environment.
|
|
43
|
+
*
|
|
44
|
+
* Resolution chain for OpenRouter key:
|
|
45
|
+
* 1. process.env.OPENROUTER_API_KEY (env var)
|
|
46
|
+
* 2. .gitmem/config.json → openrouter_key (written by `activate`)
|
|
47
|
+
*
|
|
48
|
+
* This matches how supabase-client.ts resolves credentials via getProConfig().
|
|
42
49
|
*/
|
|
43
50
|
export function detectProvider() {
|
|
44
51
|
const forced = process.env.GITMEM_EMBEDDING_PROVIDER?.toLowerCase();
|
|
52
|
+
// Resolve OpenRouter key from env var OR config.json (same as supabase-client)
|
|
53
|
+
const resolveOpenRouterKey = () => {
|
|
54
|
+
if (process.env.OPENROUTER_API_KEY)
|
|
55
|
+
return process.env.OPENROUTER_API_KEY;
|
|
56
|
+
const config = getProConfig();
|
|
57
|
+
if (config.openrouterKey)
|
|
58
|
+
return config.openrouterKey;
|
|
59
|
+
return undefined;
|
|
60
|
+
};
|
|
45
61
|
// Forced provider
|
|
46
62
|
if (forced && forced !== "auto") {
|
|
47
63
|
switch (forced) {
|
|
@@ -60,9 +76,9 @@ export function detectProvider() {
|
|
|
60
76
|
};
|
|
61
77
|
}
|
|
62
78
|
case "openrouter": {
|
|
63
|
-
const key =
|
|
79
|
+
const key = resolveOpenRouterKey();
|
|
64
80
|
if (!key) {
|
|
65
|
-
console.warn("[embedding] GITMEM_EMBEDDING_PROVIDER=openrouter but
|
|
81
|
+
console.warn("[embedding] GITMEM_EMBEDDING_PROVIDER=openrouter but no OpenRouter key found (env or config.json)");
|
|
66
82
|
return { provider: "none", apiUrl: "", apiKey: "", model: "", expectedDim: 0 };
|
|
67
83
|
}
|
|
68
84
|
return {
|
|
@@ -98,11 +114,13 @@ export function detectProvider() {
|
|
|
98
114
|
expectedDim: OPENAI_EMBEDDING_DIM,
|
|
99
115
|
};
|
|
100
116
|
}
|
|
101
|
-
|
|
117
|
+
// Check env var AND config.json for OpenRouter key
|
|
118
|
+
const openrouterKey = resolveOpenRouterKey();
|
|
119
|
+
if (openrouterKey) {
|
|
102
120
|
return {
|
|
103
121
|
provider: "openrouter",
|
|
104
122
|
apiUrl: OPENROUTER_API_URL,
|
|
105
|
-
apiKey:
|
|
123
|
+
apiKey: openrouterKey,
|
|
106
124
|
model: OPENROUTER_EMBEDDING_MODEL,
|
|
107
125
|
expectedDim: OPENAI_EMBEDDING_DIM,
|
|
108
126
|
};
|
|
@@ -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
|
|
@@ -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
|