gitmem-mcp 1.4.4 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +21 -4
  3. package/bin/gitmem.js +10 -0
  4. package/dist/commands/activate.d.ts +20 -0
  5. package/dist/commands/activate.js +562 -0
  6. package/dist/commands/deactivate.d.ts +10 -0
  7. package/dist/commands/deactivate.js +95 -0
  8. package/dist/commands/migrate-local.d.ts +53 -0
  9. package/dist/commands/migrate-local.js +177 -0
  10. package/dist/hooks/format-utils.js +4 -0
  11. package/dist/schemas/log.d.ts +2 -2
  12. package/dist/schemas/search.d.ts +2 -2
  13. package/dist/schemas/session-close.d.ts +12 -12
  14. package/dist/server.js +33 -2
  15. package/dist/services/analytics.d.ts +22 -0
  16. package/dist/services/analytics.js +68 -0
  17. package/dist/services/doc-chunker.d.ts +45 -0
  18. package/dist/services/doc-chunker.js +208 -0
  19. package/dist/services/doc-index.d.ts +88 -0
  20. package/dist/services/doc-index.js +328 -0
  21. package/dist/services/license.d.ts +57 -0
  22. package/dist/services/license.js +200 -0
  23. package/dist/services/supabase-client.d.ts +6 -0
  24. package/dist/services/supabase-client.js +75 -22
  25. package/dist/services/tier.d.ts +13 -3
  26. package/dist/services/tier.js +38 -7
  27. package/dist/tools/definitions.d.ts +688 -0
  28. package/dist/tools/definitions.js +87 -0
  29. package/dist/tools/index-docs.d.ts +30 -0
  30. package/dist/tools/index-docs.js +163 -0
  31. package/dist/tools/prepare-context.js +7 -0
  32. package/dist/tools/recall.js +25 -4
  33. package/dist/tools/search-docs.d.ts +38 -0
  34. package/dist/tools/search-docs.js +94 -0
  35. package/dist/tools/search.js +11 -1
  36. package/dist/tools/session-close.js +76 -7
  37. package/dist/tools/session-start.js +57 -5
  38. package/package.json +1 -1
  39. package/schema/setup.sql +489 -25
@@ -0,0 +1,10 @@
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
+ export declare function main(_args: string[]): Promise<void>;
10
+ //# sourceMappingURL=deactivate.d.ts.map
@@ -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
@@ -57,6 +57,10 @@ export function formatCompact(scars, plan, maxTokens) {
57
57
  lines.push(line);
58
58
  included++;
59
59
  }
60
+ // Citation reminder for sub-agent context (compact — one line)
61
+ if (included > 0) {
62
+ lines.push("Cite record IDs for any factual claims from these scars.");
63
+ }
60
64
  return { payload: lines.join("\n"), included };
61
65
  }
62
66
  /**
@@ -13,14 +13,14 @@ export declare const LogParamsSchema: z.ZodObject<{
13
13
  since: z.ZodOptional<z.ZodNumber>;
14
14
  }, "strip", z.ZodTypeAny, {
15
15
  limit?: number | undefined;
16
- project?: string | undefined;
17
16
  learning_type?: "scar" | "win" | "pattern" | "anti_pattern" | undefined;
17
+ project?: string | undefined;
18
18
  severity?: "critical" | "high" | "medium" | "low" | undefined;
19
19
  since?: number | undefined;
20
20
  }, {
21
21
  limit?: number | undefined;
22
- project?: string | undefined;
23
22
  learning_type?: "scar" | "win" | "pattern" | "anti_pattern" | undefined;
23
+ project?: string | undefined;
24
24
  severity?: "critical" | "high" | "medium" | "low" | undefined;
25
25
  since?: number | undefined;
26
26
  }>;
@@ -14,14 +14,14 @@ export declare const SearchParamsSchema: z.ZodObject<{
14
14
  }, "strip", z.ZodTypeAny, {
15
15
  query: string;
16
16
  match_count?: number | undefined;
17
- project?: string | undefined;
18
17
  learning_type?: "scar" | "win" | "pattern" | "anti_pattern" | undefined;
18
+ project?: string | undefined;
19
19
  severity?: "critical" | "high" | "medium" | "low" | undefined;
20
20
  }, {
21
21
  query: string;
22
22
  match_count?: number | undefined;
23
- project?: string | undefined;
24
23
  learning_type?: "scar" | "win" | "pattern" | "anti_pattern" | undefined;
24
+ project?: string | undefined;
25
25
  severity?: "critical" | "high" | "medium" | "low" | undefined;
26
26
  }>;
27
27
  export type SearchParams = z.infer<typeof SearchParamsSchema>;
@@ -20,20 +20,20 @@ export declare const ClosingReflectionSchema: z.ZodObject<{
20
20
  rapport_notes: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodEffects<z.ZodArray<z.ZodString, "many">, string, string[]>]>>;
21
21
  }, "strip", z.ZodTypeAny, {
22
22
  what_broke: string;
23
- what_took_longer: string;
24
- do_differently: string;
25
23
  what_worked: string;
26
24
  wrong_assumption: string;
25
+ do_differently: string;
26
+ what_took_longer: string;
27
27
  scars_applied: string | string[];
28
28
  institutional_memory_items?: string | undefined;
29
29
  collaborative_dynamic?: string | undefined;
30
30
  rapport_notes?: string | undefined;
31
31
  }, {
32
32
  what_broke: string;
33
- what_took_longer: string;
34
- do_differently: string;
35
33
  what_worked: string;
36
34
  wrong_assumption: string;
35
+ do_differently: string;
36
+ what_took_longer: string;
37
37
  scars_applied: string | string[];
38
38
  institutional_memory_items?: string | string[] | undefined;
39
39
  collaborative_dynamic?: string | string[] | undefined;
@@ -161,20 +161,20 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
161
161
  rapport_notes: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodEffects<z.ZodArray<z.ZodString, "many">, string, string[]>]>>;
162
162
  }, "strip", z.ZodTypeAny, {
163
163
  what_broke: string;
164
- what_took_longer: string;
165
- do_differently: string;
166
164
  what_worked: string;
167
165
  wrong_assumption: string;
166
+ do_differently: string;
167
+ what_took_longer: string;
168
168
  scars_applied: string | string[];
169
169
  institutional_memory_items?: string | undefined;
170
170
  collaborative_dynamic?: string | undefined;
171
171
  rapport_notes?: string | undefined;
172
172
  }, {
173
173
  what_broke: string;
174
- what_took_longer: string;
175
- do_differently: string;
176
174
  what_worked: string;
177
175
  wrong_assumption: string;
176
+ do_differently: string;
177
+ what_took_longer: string;
178
178
  scars_applied: string | string[];
179
179
  institutional_memory_items?: string | string[] | undefined;
180
180
  collaborative_dynamic?: string | string[] | undefined;
@@ -281,10 +281,10 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
281
281
  linear_issue?: string | undefined;
282
282
  closing_reflection?: {
283
283
  what_broke: string;
284
- what_took_longer: string;
285
- do_differently: string;
286
284
  what_worked: string;
287
285
  wrong_assumption: string;
286
+ do_differently: string;
287
+ what_took_longer: string;
288
288
  scars_applied: string | string[];
289
289
  institutional_memory_items?: string | undefined;
290
290
  collaborative_dynamic?: string | undefined;
@@ -338,10 +338,10 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
338
338
  linear_issue?: string | undefined;
339
339
  closing_reflection?: {
340
340
  what_broke: string;
341
- what_took_longer: string;
342
- do_differently: string;
343
341
  what_worked: string;
344
342
  wrong_assumption: string;
343
+ do_differently: string;
344
+ what_took_longer: string;
345
345
  scars_applied: string | string[];
346
346
  institutional_memory_items?: string | string[] | undefined;
347
347
  collaborative_dynamic?: string | string[] | undefined;
package/dist/server.js CHANGED
@@ -36,12 +36,16 @@ import { dismissSuggestion } from "./tools/dismiss-suggestion.js";
36
36
  import { cleanupThreads } from "./tools/cleanup-threads.js";
37
37
  import { archiveLearning } from "./tools/archive-learning.js";
38
38
  import { contributeFeedback } from "./tools/contribute-feedback.js";
39
+ import { indexDocs } from "./tools/index-docs.js";
40
+ import { searchDocsHandler } from "./tools/search-docs.js";
39
41
  import { getCacheStatus, checkCacheHealth, flushCache, startBackgroundInit, } from "./services/startup.js";
40
42
  import { getEffectTracker } from "./services/effect-tracker.js";
41
43
  import { RIPPLE, ANSI } from "./services/display-protocol.js";
42
44
  import { getProject } from "./services/session-state.js";
43
45
  import { checkEnforcement } from "./services/enforcement.js";
44
- 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";
45
49
  import { getRegisteredTools } from "./tools/definitions.js";
46
50
  import { validateToolArgs } from "./schemas/registry.js";
47
51
  /**
@@ -246,6 +250,8 @@ export function createServer() {
246
250
  { alias: "gitmem-al", full: "archive_learning", description: "Archive a scar/win/pattern (is_active=false)" },
247
251
  { alias: "gitmem-graph", full: "graph_traverse", description: "Traverse knowledge graph over institutional memory" },
248
252
  { alias: "gitmem-fb", full: "contribute_feedback", description: "Submit feedback about gitmem (10/session limit)" },
253
+ { alias: "gitmem-idx", full: "index_docs", description: "Index markdown docs for semantic search" },
254
+ { alias: "gitmem-sd", full: "search_docs", description: "Search indexed repository docs" },
249
255
  ];
250
256
  if (hasBatchOperations()) {
251
257
  commands.push({ alias: "gitmem-rsb", full: "record_scar_usage_batch", description: "Track multiple scars (batch)" });
@@ -315,6 +321,15 @@ export function createServer() {
315
321
  case "gm-cache-f":
316
322
  result = await flushCache(toolArgs.project || getProject() || "default");
317
323
  break;
324
+ // Doc indexing and search
325
+ case "index_docs":
326
+ case "gitmem-idx":
327
+ result = await indexDocs(toolArgs);
328
+ break;
329
+ case "search_docs":
330
+ case "gitmem-sd":
331
+ result = await searchDocsHandler(toolArgs);
332
+ break;
318
333
  default:
319
334
  throw new Error(`Unknown tool: ${name}`);
320
335
  }
@@ -380,13 +395,29 @@ export function createServer() {
380
395
  */
381
396
  export async function runServer() {
382
397
  const tier = getTier();
383
- // Start server immediately (don't block on cache loading)
398
+ // Start server immediately (don't block on cache loading or license validation)
384
399
  const server = createServer();
385
400
  const transport = new StdioServerTransport();
386
401
  await server.connect(transport);
387
402
  const toolCount = getRegisteredTools().length;
388
403
  const storage = hasSupabase() ? "supabase" : "local";
389
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
+ }
390
421
  if (hasSupabase()) {
391
422
  // Pro/Dev: Initialize local vector search in background (non-blocking)
392
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.