gitmem-mcp 1.6.0 → 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 CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.6.1] - 2026-05-25
11
+
12
+ ### Fixed
13
+ - **Embeddings not generated on Pro tier**: `embedding.ts`, `variant-generation.ts`, and `transcript-chunker.ts` only checked `process.env.OPENROUTER_API_KEY` — never read the key from `.gitmem/config.json` written by `activate`. Now falls back to `getProConfig()` matching how `supabase-client.ts` already resolves credentials.
14
+ - **Lossy free→pro migration**: Migration sent all local JSON fields to PostgREST — unknown columns caused 400 rejections, silently dropping records (only first 3 errors shown). Added `KNOWN_COLUMNS` whitelist per table, type coercion for `action_protocol`/`self_check_criteria` (array→TEXT), and full error visibility (no cap).
15
+ - **Migration log file**: `.gitmem/migration.log` now written with per-record outcomes (OK/FAIL/SKIP) for debugging.
16
+ - **Mid-migration recovery**: `activate` now detects `.pre-migration` backup files from a previous failed upgrade and re-imports them automatically. No new command — just re-run `activate`.
17
+ - **Credential exposure**: `activate` now auto-adds `.gitmem/` to the project's `.gitignore` when inside a git repo.
18
+
19
+ ### Changed
20
+ - **E2E stress test v1.4**: Restructured from 6 to 8 simulated days. Day 0 wipes Supabase to blank slate. Day 1 seeds realistic 3+ month free-tier user data (starter scars, unknown fields, type mismatches, mixed projects) and tests full upgrade journey including mid-failure recovery and real embeddings from config.json (not env var). 178 tests total.
21
+
10
22
  ## [1.6.0] - 2026-05-25
11
23
 
12
24
  ### Added
@@ -22,7 +22,7 @@ import * as readline from "readline";
22
22
  import { fileURLToPath } from "url";
23
23
  import { getGitmemDir, getInstallId } from "../services/gitmem-dir.js";
24
24
  import { validateLicense, clearLicenseCache } from "../services/license.js";
25
- import { hasLocalData, migrateLocalToSupabase, archiveLocalData } from "./migrate-local.js";
25
+ import { hasLocalData, hasPreMigrationData, migrateLocalToSupabase, reimportFromBackups, archiveLocalData } from "./migrate-local.js";
26
26
  function createReadline() {
27
27
  return readline.createInterface({
28
28
  input: process.stdin,
@@ -485,56 +485,100 @@ export async function main(args) {
485
485
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
486
486
  // Clear any stale license cache
487
487
  clearLicenseCache();
488
- // Step 7: Migrate local data to Supabase (free pro upgrade)
489
- if (supabaseUrl && supabaseKey && missingTables.length === 0 && hasLocalData(gitmemDir)) {
490
- console.log("Migrating Local Data");
491
- console.log(" Found existing local data from free tier...");
492
- console.log("");
493
- const migrationResult = await migrateLocalToSupabase({
494
- supabaseUrl,
495
- supabaseKey,
496
- gitmemDir,
497
- onProgress: (msg) => console.log(msg),
498
- });
499
- // Report results
500
- const collections = Object.keys(migrationResult.migrated);
501
- let totalMigrated = 0;
502
- let totalSkipped = 0;
503
- let totalErrors = 0;
504
- for (const col of collections) {
505
- const m = migrationResult.migrated[col];
506
- const s = migrationResult.skipped[col];
507
- const e = migrationResult.errors[col]?.length || 0;
508
- totalMigrated += m;
509
- totalSkipped += s;
510
- totalErrors += e;
511
- if (m > 0) {
512
- console.log(` ✓ ${col}: ${m} records migrated${s > 0 ? ` (${s} skipped)` : ""}`);
488
+ // Ensure .gitmem/ is in .gitignore (prevents credential exposure)
489
+ try {
490
+ const projectRoot = process.cwd();
491
+ const gitignorePath = path.join(projectRoot, ".gitignore");
492
+ if (fs.existsSync(path.join(projectRoot, ".git"))) {
493
+ let gitignore = "";
494
+ if (fs.existsSync(gitignorePath)) {
495
+ gitignore = fs.readFileSync(gitignorePath, "utf-8");
496
+ }
497
+ if (!gitignore.split("\n").some(line => line.trim() === ".gitmem/" || line.trim() === ".gitmem")) {
498
+ const separator = gitignore.length > 0 && !gitignore.endsWith("\n") ? "\n" : "";
499
+ fs.appendFileSync(gitignorePath, `${separator}\n# GitMem local data (contains credentials)\n.gitmem/\n`);
500
+ console.log(" ✓ Added .gitmem/ to .gitignore");
513
501
  }
514
502
  }
515
- // Show errors if any
516
- if (totalErrors > 0) {
503
+ }
504
+ catch {
505
+ // Non-fatal — warn but don't block activation
506
+ console.log(" ⚠ Could not update .gitignore — manually add .gitmem/ to prevent credential exposure");
507
+ }
508
+ // Step 7: Migrate local data to Supabase (free → pro upgrade)
509
+ // Handles three scenarios:
510
+ // A) Fresh upgrade: .json files exist → migrate and archive
511
+ // B) Re-activation after failed migration: .pre-migration backups exist → reimport
512
+ // C) Already migrated: neither exists → skip
513
+ if (supabaseUrl && supabaseKey && missingTables.length === 0) {
514
+ const hasLive = hasLocalData(gitmemDir);
515
+ const hasBackups = hasPreMigrationData(gitmemDir);
516
+ if (hasLive || hasBackups) {
517
+ if (hasLive) {
518
+ console.log("Migrating Local Data");
519
+ console.log(" Found existing local data from free tier...");
520
+ }
521
+ else {
522
+ console.log("Re-importing From Backups");
523
+ console.log(" Found .pre-migration backup files from a previous upgrade...");
524
+ }
517
525
  console.log("");
526
+ const migrationResult = hasLive
527
+ ? await migrateLocalToSupabase({
528
+ supabaseUrl,
529
+ supabaseKey,
530
+ gitmemDir,
531
+ onProgress: (msg) => console.log(msg),
532
+ })
533
+ : await reimportFromBackups({
534
+ supabaseUrl,
535
+ supabaseKey,
536
+ gitmemDir,
537
+ onProgress: (msg) => console.log(msg),
538
+ });
539
+ // Report results
540
+ const collections = Object.keys(migrationResult.migrated);
541
+ let totalMigrated = 0;
542
+ let totalSkipped = 0;
543
+ let totalErrors = 0;
518
544
  for (const col of collections) {
519
- for (const err of migrationResult.errors[col] || []) {
520
- console.log(` ⚠ ${col}: ${err}`);
545
+ const m = migrationResult.migrated[col];
546
+ const s = migrationResult.skipped[col];
547
+ const e = migrationResult.errors[col]?.length || 0;
548
+ totalMigrated += m;
549
+ totalSkipped += s;
550
+ totalErrors += e;
551
+ if (m > 0 || s > 0) {
552
+ console.log(` ${m > 0 ? "✓" : "⚠"} ${col}: ${m} migrated${s > 0 ? `, ${s} failed` : ""}`);
521
553
  }
522
554
  }
523
- }
524
- if (totalMigrated > 0) {
525
- console.log("");
526
- console.log(` ✓ Migrated ${totalMigrated} records to Supabase`);
527
- // Archive local files so they aren't re-read
528
- const archived = archiveLocalData(gitmemDir);
529
- if (archived.length > 0) {
530
- console.log(` ✓ Local files archived (${archived.join(", ")}.json → .pre-migration)`);
555
+ // Show ALL errors
556
+ if (totalErrors > 0) {
557
+ console.log("");
558
+ for (const col of collections) {
559
+ for (const err of migrationResult.errors[col] || []) {
560
+ console.log(` ⚠ ${col}: ${err}`);
561
+ }
562
+ }
531
563
  }
564
+ if (totalMigrated > 0) {
565
+ console.log("");
566
+ console.log(` ✓ Migrated ${totalMigrated} records to Supabase`);
567
+ if (hasLive) {
568
+ // Archive local files so they aren't re-read
569
+ const archived = archiveLocalData(gitmemDir);
570
+ if (archived.length > 0) {
571
+ console.log(` ✓ Local files archived (${archived.join(", ")}.json → .pre-migration)`);
572
+ }
573
+ }
574
+ }
575
+ else if (migrationResult.hasLocalData) {
576
+ console.log(" ⚠ Migration encountered errors. Local data preserved.");
577
+ console.log(" Check .gitmem/migration.log for details.");
578
+ console.log(" Fix issues and re-run: npx gitmem-mcp activate");
579
+ }
580
+ console.log("");
532
581
  }
533
- else if (migrationResult.hasLocalData) {
534
- console.log(" ⚠ Migration encountered errors. Local data preserved.");
535
- console.log(" Re-run activate after resolving issues.");
536
- }
537
- console.log("");
538
582
  }
539
583
  // Summary
540
584
  console.log("");
@@ -45,6 +45,24 @@ export declare function migrateLocalToSupabase(opts: {
45
45
  gitmemDir?: string;
46
46
  onProgress?: (msg: string) => void;
47
47
  }): Promise<MigrationResult>;
48
+ /**
49
+ * Check if there are .pre-migration backup files that can be re-imported.
50
+ * This handles the case where a previous migration partially failed —
51
+ * the user runs `activate` again and we pick up from the backups.
52
+ */
53
+ export declare function hasPreMigrationData(gitmemDir?: string): boolean;
54
+ /**
55
+ * Re-import from .pre-migration backup files.
56
+ * Same as migrateLocalToSupabase but reads from *.json.pre-migration files.
57
+ * Idempotent: uses upsert (merge-duplicates) so re-running is safe.
58
+ */
59
+ export declare function reimportFromBackups(opts: {
60
+ supabaseUrl: string;
61
+ supabaseKey: string;
62
+ tablePrefix?: string;
63
+ gitmemDir?: string;
64
+ onProgress?: (msg: string) => void;
65
+ }): Promise<MigrationResult>;
48
66
  /**
49
67
  * Rename local collection files after successful migration
50
68
  * Adds .pre-migration suffix so data isn't lost but won't be re-read by free tier
@@ -27,6 +27,40 @@ const MIGRATABLE_COLLECTIONS = ["learnings", "sessions", "decisions", "scar_usag
27
27
  const STRIP_FIELDS = new Set(["is_starter"]);
28
28
  /** Fields that Supabase will reject if null (remove instead of sending null) */
29
29
  const NULLABLE_STRIP = new Set(["embedding"]);
30
+ /**
31
+ * Known columns per table — only these fields are sent to Supabase.
32
+ * PostgREST rejects POSTs with unknown columns, so we must whitelist.
33
+ * This prevents accumulated local fields from different code versions
34
+ * from causing migration failures.
35
+ */
36
+ const KNOWN_COLUMNS = {
37
+ learnings: new Set([
38
+ "id", "learning_type", "title", "description", "severity", "scar_type",
39
+ "counter_arguments", "problem_context", "solution_approach", "applies_when",
40
+ "keywords", "domain", "embedding", "project", "source_date",
41
+ "source_linear_issue", "persona_name", "why_this_matters",
42
+ "action_protocol", "self_check_criteria", "is_active",
43
+ "decay_multiplier", "repeat_mistake", "related_scar_id",
44
+ "repeat_mistake_details", "created_at", "updated_at",
45
+ ]),
46
+ sessions: new Set([
47
+ "id", "session_title", "session_date", "agent", "project",
48
+ "linear_issue", "recording_path", "transcript_path", "decisions",
49
+ "open_threads", "closing_reflection", "close_compliance",
50
+ "rapport_summary", "embedding", "created_at", "updated_at",
51
+ ]),
52
+ decisions: new Set([
53
+ "id", "decision_date", "title", "decision", "rationale",
54
+ "alternatives_considered", "personas_involved", "docs_affected",
55
+ "linear_issue", "session_id", "project", "embedding", "created_at",
56
+ ]),
57
+ scar_usage: new Set([
58
+ "id", "scar_id", "session_id", "agent", "issue_id",
59
+ "issue_identifier", "reference_type", "reference_context",
60
+ "surfaced_at", "acknowledged_at", "referenced",
61
+ "execution_successful", "variant_id", "created_at",
62
+ ]),
63
+ };
30
64
  /**
31
65
  * Check if there is local data worth migrating
32
66
  */
@@ -62,23 +96,50 @@ function readLocalCollection(dir, collection) {
62
96
  return [];
63
97
  }
64
98
  }
99
+ /**
100
+ * Columns that are TEXT in Supabase but may be stored as arrays locally.
101
+ * During migration, arrays are joined with newlines to fit the TEXT column.
102
+ * This prevents PostgREST 400 errors from type mismatches.
103
+ */
104
+ const TEXT_COERCE_FIELDS = new Set([
105
+ "action_protocol",
106
+ "self_check_criteria",
107
+ "why_this_matters",
108
+ ]);
65
109
  /**
66
110
  * Clean a record for Supabase insertion:
67
111
  * - Strip local-only fields
68
112
  * - Remove null values for non-nullable columns
113
+ * - Only include fields that exist in the target table schema
114
+ * - Coerce arrays to strings for TEXT columns
69
115
  * - Ensure id exists
70
116
  */
71
- function cleanRecord(record) {
117
+ function cleanRecord(record, collection) {
72
118
  if (!record.id)
73
119
  return null;
120
+ const knownCols = KNOWN_COLUMNS[collection];
74
121
  const cleaned = {};
122
+ const droppedFields = [];
75
123
  for (const [key, value] of Object.entries(record)) {
76
124
  if (STRIP_FIELDS.has(key))
77
125
  continue;
78
126
  if (NULLABLE_STRIP.has(key) && (value === null || value === undefined))
79
127
  continue;
128
+ // Only include fields that exist in the target table
129
+ if (knownCols && !knownCols.has(key)) {
130
+ droppedFields.push(key);
131
+ continue;
132
+ }
133
+ // Coerce arrays to strings for TEXT columns (schema says TEXT, code uses string[])
134
+ if (TEXT_COERCE_FIELDS.has(key) && Array.isArray(value)) {
135
+ cleaned[key] = value.join("\n");
136
+ continue;
137
+ }
80
138
  cleaned[key] = value;
81
139
  }
140
+ if (droppedFields.length > 0) {
141
+ console.error(`[migrate] Record ${String(record.id).substring(0, 8)}: dropped unknown fields: ${droppedFields.join(", ")}`);
142
+ }
82
143
  return cleaned;
83
144
  }
84
145
  /**
@@ -94,6 +155,14 @@ export async function migrateLocalToSupabase(opts) {
94
155
  const { supabaseUrl, supabaseKey, tablePrefix = "gitmem_", onProgress } = opts;
95
156
  const dir = opts.gitmemDir || getGitmemDir();
96
157
  const log = onProgress || ((msg) => console.log(msg));
158
+ // Migration log file — captures all details for debugging
159
+ const logLines = [];
160
+ const logFile = path.join(dir, "migration.log");
161
+ const mlog = (msg) => {
162
+ const ts = new Date().toISOString();
163
+ logLines.push(`[${ts}] ${msg}`);
164
+ };
165
+ mlog(`Migration started: ${supabaseUrl} (prefix: ${tablePrefix})`);
97
166
  const result = {
98
167
  migrated: {},
99
168
  skipped: {},
@@ -108,14 +177,21 @@ export async function migrateLocalToSupabase(opts) {
108
177
  result.migrated[collection] = 0;
109
178
  result.skipped[collection] = 0;
110
179
  result.errors[collection] = [];
111
- if (records.length === 0)
180
+ if (records.length === 0) {
181
+ mlog(`${collection}: no local data`);
112
182
  continue;
183
+ }
113
184
  result.hasLocalData = true;
185
+ mlog(`${collection}: ${records.length} records to migrate → ${tableName}`);
114
186
  log(` Migrating ${records.length} ${collection}...`);
115
187
  for (const record of records) {
116
- const cleaned = cleanRecord(record);
188
+ const cleaned = cleanRecord(record, collection);
117
189
  if (!cleaned) {
118
190
  result.skipped[collection]++;
191
+ const errMsg = `${String(record.id || "no-id").substring(0, 8)}: skipped (missing id)`;
192
+ result.errors[collection].push(errMsg);
193
+ mlog(`${collection} SKIP: ${errMsg}`);
194
+ result.total++;
119
195
  continue;
120
196
  }
121
197
  try {
@@ -133,25 +209,89 @@ export async function migrateLocalToSupabase(opts) {
133
209
  });
134
210
  if (response.ok) {
135
211
  result.migrated[collection]++;
212
+ mlog(`${collection} OK: ${String(cleaned.id).substring(0, 8)} (${cleaned.title || ""})`);
136
213
  }
137
214
  else {
138
215
  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
- }
216
+ const errMsg = `${String(cleaned.id).substring(0, 8)}: ${response.status} - ${text.substring(0, 200)}`;
217
+ result.errors[collection].push(errMsg);
218
+ mlog(`${collection} FAIL: ${errMsg}`);
219
+ mlog(`${collection} FAIL payload: ${JSON.stringify(Object.keys(cleaned))}`);
143
220
  result.skipped[collection]++;
144
221
  }
145
222
  }
146
223
  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
- }
224
+ const errMsg = `${String(cleaned.id).substring(0, 8)}: ${err instanceof Error ? err.message : "Unknown error"}`;
225
+ result.errors[collection].push(errMsg);
226
+ mlog(`${collection} ERROR: ${errMsg}`);
150
227
  result.skipped[collection]++;
151
228
  }
152
229
  result.total++;
153
230
  }
154
231
  }
232
+ // Write migration log to disk for debugging
233
+ mlog(`Migration complete: migrated=${JSON.stringify(result.migrated)} skipped=${JSON.stringify(result.skipped)}`);
234
+ try {
235
+ fs.writeFileSync(logFile, logLines.join("\n") + "\n", "utf-8");
236
+ log(` Migration log: ${logFile}`);
237
+ }
238
+ catch {
239
+ // Non-fatal — log file write failure shouldn't block migration
240
+ }
241
+ return result;
242
+ }
243
+ /**
244
+ * Check if there are .pre-migration backup files that can be re-imported.
245
+ * This handles the case where a previous migration partially failed —
246
+ * the user runs `activate` again and we pick up from the backups.
247
+ */
248
+ export function hasPreMigrationData(gitmemDir) {
249
+ const dir = gitmemDir || getGitmemDir();
250
+ for (const collection of MIGRATABLE_COLLECTIONS) {
251
+ const backupPath = path.join(dir, `${collection}.json.pre-migration`);
252
+ if (fs.existsSync(backupPath)) {
253
+ try {
254
+ const data = JSON.parse(fs.readFileSync(backupPath, "utf-8"));
255
+ if (Array.isArray(data) && data.length > 0)
256
+ return true;
257
+ }
258
+ catch {
259
+ // Corrupt backup
260
+ }
261
+ }
262
+ }
263
+ return false;
264
+ }
265
+ /**
266
+ * Re-import from .pre-migration backup files.
267
+ * Same as migrateLocalToSupabase but reads from *.json.pre-migration files.
268
+ * Idempotent: uses upsert (merge-duplicates) so re-running is safe.
269
+ */
270
+ export async function reimportFromBackups(opts) {
271
+ const dir = opts.gitmemDir || getGitmemDir();
272
+ // Temporarily restore .pre-migration files as .json for the migration function
273
+ const restored = [];
274
+ for (const collection of MIGRATABLE_COLLECTIONS) {
275
+ const backupPath = path.join(dir, `${collection}.json.pre-migration`);
276
+ const livePath = path.join(dir, `${collection}.json`);
277
+ if (fs.existsSync(backupPath) && !fs.existsSync(livePath)) {
278
+ // Copy (not move) backup to live path for migration
279
+ fs.copyFileSync(backupPath, livePath);
280
+ restored.push(collection);
281
+ }
282
+ }
283
+ // Run migration
284
+ const result = await migrateLocalToSupabase(opts);
285
+ // Clean up: remove the restored copies (backups remain untouched)
286
+ for (const collection of restored) {
287
+ const livePath = path.join(dir, `${collection}.json`);
288
+ try {
289
+ fs.unlinkSync(livePath);
290
+ }
291
+ catch {
292
+ // Non-fatal
293
+ }
294
+ }
155
295
  return result;
156
296
  }
157
297
  /**
@@ -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 = process.env.OPENROUTER_API_KEY;
79
+ const key = resolveOpenRouterKey();
64
80
  if (!key) {
65
- console.warn("[embedding] GITMEM_EMBEDDING_PROVIDER=openrouter but OPENROUTER_API_KEY not set");
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
- if (process.env.OPENROUTER_API_KEY) {
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: process.env.OPENROUTER_API_KEY,
123
+ apiKey: openrouterKey,
106
124
  model: OPENROUTER_EMBEDDING_MODEL,
107
125
  expectedDim: OPENAI_EMBEDDING_DIM,
108
126
  };
@@ -7,6 +7,7 @@
7
7
  *
8
8
  */
9
9
  import { getTableName } from "./tier.js";
10
+ import { getProConfig } from "./license.js";
10
11
  // OpenRouter API configuration (same as local-vector-search)
11
12
  const OPENROUTER_API_URL = "https://openrouter.ai/api/v1/embeddings";
12
13
  const EMBEDDING_MODEL = "openai/text-embedding-3-small";
@@ -32,9 +33,9 @@ function normalize(vec) {
32
33
  * Generate embedding using OpenRouter API
33
34
  */
34
35
  async function generateEmbedding(text) {
35
- const apiKey = process.env.OPENROUTER_API_KEY;
36
+ const apiKey = process.env.OPENROUTER_API_KEY || getProConfig().openrouterKey;
36
37
  if (!apiKey) {
37
- throw new Error("OPENROUTER_API_KEY not configured");
38
+ throw new Error("No OpenRouter key configured (set OPENROUTER_API_KEY env var or run: npx gitmem-mcp activate)");
38
39
  }
39
40
  const response = await fetch(OPENROUTER_API_URL, {
40
41
  method: "POST",
@@ -17,6 +17,7 @@
17
17
  * Called fire-and-forget from create_learning — zero impact on UX latency.
18
18
  */
19
19
  import * as supabase from "./supabase-client.js";
20
+ import { getProConfig } from "./license.js";
20
21
  // --- Pipeline versioning ---
21
22
  // Bump PIPELINE_VERSION when changing the prompt, model, or generation logic.
22
23
  // Format: "gen-{major}.{minor}" — major for prompt rewrites, minor for tweaks.
@@ -83,9 +84,9 @@ function buildUserPrompt(scar) {
83
84
  * Returns parsed output or null on failure.
84
85
  */
85
86
  async function generateWithLLM(scar) {
86
- const apiKey = process.env.OPENROUTER_API_KEY;
87
+ const apiKey = process.env.OPENROUTER_API_KEY || getProConfig().openrouterKey;
87
88
  if (!apiKey) {
88
- console.error("[variant-generation] No OPENROUTER_API_KEY — falling back to deterministic");
89
+ console.error("[variant-generation] No OpenRouter key (env or config.json) — falling back to deterministic");
89
90
  return null;
90
91
  }
91
92
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitmem-mcp",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "mcpName": "io.github.gitmem-dev/gitmem",
5
5
  "description": "Persistent learning memory for AI coding agents. Memory that compounds.",
6
6
  "type": "module",