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,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
|
package/dist/schemas/log.d.ts
CHANGED
|
@@ -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
|
}>;
|
package/dist/schemas/search.d.ts
CHANGED
|
@@ -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
|