33 *
44 * Usage: npx gitmem-mcp activate [license-key]
55 *
6+ * Credential resolution (priority order):
7+ * 1. Environment variables (SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, OPENROUTER_API_KEY)
8+ * 2. Existing values in .gitmem/config.json (re-activation)
9+ * 3. Interactive prompt (TTY only)
10+ *
611 * Steps:
712 * 1. Accept key as argument or prompt for it
813 * 2. Validate key against our endpoint (register device)
9- * 3. Prompt for Supabase URL + service role key (with re-activation safety check )
14+ * 3. Resolve Supabase URL + service role key (env → config → prompt )
1015 * 4. Test Supabase connection (verify tables exist)
11- * 5. Prompt for OpenRouter API key
12- * 6. Tell user to run schema setup manually if tables missing
13- * 7. Write everything to ~/.gitmem/config.json
16+ * 5. Resolve OpenRouter API key (env → config → prompt)
17+ * 6. Write everything to ~/.gitmem/config.json
1418 */
1519
1620import * as fs from "fs" ;
@@ -32,6 +36,47 @@ function ask(rl: readline.Interface, question: string): Promise<string> {
3236 } ) ;
3337}
3438
39+ /**
40+ * Resolve a credential value using the priority chain:
41+ * 1. Environment variable
42+ * 2. Existing config value
43+ * 3. Interactive prompt (if TTY available)
44+ *
45+ * Returns the resolved value or empty string.
46+ */
47+ async function resolveCredential ( opts : {
48+ envVar : string ;
49+ configValue : string | undefined ;
50+ promptLabel : string ;
51+ required : boolean ;
52+ rl : readline . Interface | null ;
53+ existingHint ?: string ;
54+ } ) : Promise < { value : string ; source : "env" | "config" | "prompt" | "none" } > {
55+ // 1. Environment variable
56+ const envValue = process . env [ opts . envVar ] ;
57+ if ( envValue ) {
58+ return { value : envValue , source : "env" } ;
59+ }
60+
61+ // 2. Existing config value
62+ if ( opts . configValue ) {
63+ return { value : opts . configValue , source : "config" } ;
64+ }
65+
66+ // 3. Interactive prompt
67+ if ( opts . rl ) {
68+ const prompt = opts . existingHint
69+ ? ` ${ opts . promptLabel } [${ opts . existingHint } ]: `
70+ : ` ${ opts . promptLabel } : ` ;
71+ const input = await ask ( opts . rl , prompt ) ;
72+ if ( input ) {
73+ return { value : input , source : "prompt" } ;
74+ }
75+ }
76+
77+ return { value : "" , source : "none" } ;
78+ }
79+
3580/**
3681 * Test basic Supabase connectivity via REST API
3782 */
@@ -67,7 +112,6 @@ async function checkSchemaExists(url: string, key: string): Promise<string[]> {
67112 Authorization : `Bearer ${ key } ` ,
68113 } ,
69114 } ) ;
70- // 404 or 400 means table doesn't exist; 200 (even empty) means it does
71115 if ( ! response . ok ) {
72116 missing . push ( table ) ;
73117 }
@@ -106,13 +150,13 @@ export async function main(args: string[]): Promise<void> {
106150 config . install_id = installId ;
107151 }
108152
109- // Step 1: Get license key
110- let apiKey = args [ 0 ] || "" ;
153+ // Step 1: Get license key (arg → env → prompt)
154+ let apiKey = args [ 0 ] || process . env . GITMEM_API_KEY || ( config . api_key as string ) || "" ;
111155
112156 if ( ! apiKey ) {
113- // Check if non-interactive (piped stdin)
114157 if ( ! process . stdin . isTTY ) {
115158 console . error ( "Error: License key required. Usage: npx gitmem-mcp activate <key>" ) ;
159+ console . error ( " Or set GITMEM_API_KEY environment variable." ) ;
116160 process . exit ( 1 ) ;
117161 }
118162
@@ -144,135 +188,150 @@ export async function main(args: string[]): Promise<void> {
144188 console . log ( `✓ Key validated (${ result . tier } tier)` ) ;
145189 console . log ( "" ) ;
146190
147- // Interactive mode for credentials
148- if ( ! process . stdin . isTTY ) {
149- // Non-interactive: just save the key
150- config . api_key = apiKey ;
151- if ( ! fs . existsSync ( gitmemDir ) ) {
152- fs . mkdirSync ( gitmemDir , { recursive : true } ) ;
153- }
154- fs . writeFileSync ( configPath , JSON . stringify ( config , null , 2 ) ) ;
155- console . log ( "License key saved. Set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY env vars for Supabase." ) ;
156- return ;
157- }
191+ // Create readline only if TTY is available
192+ const rl = process . stdin . isTTY ? createReadline ( ) : null ;
158193
159- const rl = createReadline ( ) ;
160-
161- // Step 3: Supabase credentials (with re-activation safety check)
194+ // Step 3: Resolve Supabase credentials (env → config → prompt)
162195 const existingUrl = config . supabase_url as string | undefined ;
196+ const existingKey = config . supabase_key as string | undefined ;
163197
164- console . log ( "Supabase Setup" ) ;
165- console . log ( " (Create a free project at https://database.new)" ) ;
166- if ( existingUrl ) {
167- console . log ( ` Current: ${ existingUrl } ` ) ;
168- }
169- console . log ( "" ) ;
170-
171- let supabaseUrl : string ;
172- if ( existingUrl ) {
173- const urlInput = await ask ( rl , ` Project URL [${ existingUrl } ]: ` ) ;
174- supabaseUrl = urlInput || existingUrl ;
175-
176- // Warn if changing to a different Supabase instance
177- if ( urlInput && urlInput !== existingUrl ) {
178- console . log ( "" ) ;
179- console . log ( " ⚠ WARNING: You are changing your Supabase URL." ) ;
180- console . log ( ` Old: ${ existingUrl } ` ) ;
181- console . log ( ` New: ${ urlInput } ` ) ;
182- console . log ( " Your existing data in the old project will NOT be migrated." ) ;
183- console . log ( "" ) ;
184- const confirm = await ask ( rl , " Continue with new URL? (y/N): " ) ;
185- if ( confirm . toLowerCase ( ) !== "y" && confirm . toLowerCase ( ) !== "yes" ) {
186- console . log ( " Keeping existing URL." ) ;
187- supabaseUrl = existingUrl ;
188- }
198+ if ( rl ) {
199+ console . log ( "Supabase Setup" ) ;
200+ console . log ( " (Create a free project at https://database.new)" ) ;
201+ if ( existingUrl ) {
202+ console . log ( ` Current: ${ existingUrl } ` ) ;
189203 }
190- } else {
191- supabaseUrl = await ask ( rl , " Project URL: " ) ;
204+ console . log ( "" ) ;
192205 }
193206
194- if ( ! supabaseUrl ) {
195- console . error ( "Error: Supabase URL is required for Pro tier." ) ;
196- rl . close ( ) ;
197- process . exit ( 1 ) ;
198- }
207+ const supabaseUrlResult = await resolveCredential ( {
208+ envVar : "SUPABASE_URL" ,
209+ configValue : existingUrl ,
210+ promptLabel : "Project URL" ,
211+ required : true ,
212+ rl,
213+ } ) ;
199214
200- const existingKey = config . supabase_key as string | undefined ;
201- let supabaseKey : string ;
202- if ( existingKey && supabaseUrl === existingUrl ) {
203- // Same URL, offer to keep existing key
204- const keyInput = await ask ( rl , " Service Role Key [keep existing]: " ) ;
205- supabaseKey = keyInput || existingKey ;
206- } else {
207- supabaseKey = await ask ( rl , " Service Role Key: " ) ;
208- }
215+ const supabaseUrl = supabaseUrlResult . value ;
209216
210- if ( ! supabaseKey ) {
211- console . error ( "Error: Service Role Key is required." ) ;
212- rl . close ( ) ;
213- process . exit ( 1 ) ;
217+ // Re-activation safety: warn if changing URL interactively
218+ if ( rl && existingUrl && supabaseUrl !== existingUrl && supabaseUrlResult . source === "prompt" ) {
219+ console . log ( "" ) ;
220+ console . log ( " ⚠ WARNING: You are changing your Supabase URL." ) ;
221+ console . log ( ` Old: ${ existingUrl } ` ) ;
222+ console . log ( ` New: ${ supabaseUrl } ` ) ;
223+ console . log ( " Your existing data in the old project will NOT be migrated." ) ;
224+ console . log ( "" ) ;
225+ const confirm = await ask ( rl , " Continue with new URL? (y/N): " ) ;
226+ if ( confirm . toLowerCase ( ) !== "y" && confirm . toLowerCase ( ) !== "yes" ) {
227+ console . log ( " Keeping existing URL." ) ;
228+ // Fall back handled below by using existingUrl
229+ }
214230 }
215231
216- // Step 4: Test connection and check schema
217- console . log ( " Testing connection..." ) ;
218- const connected = await testSupabaseConnection ( supabaseUrl , supabaseKey ) ;
219- if ( ! connected ) {
220- console . error ( " ✗ Could not connect to Supabase. Check your URL and key." ) ;
221- rl . close ( ) ;
222- process . exit ( 1 ) ;
223- }
224- console . log ( " ✓ Connected to Supabase" ) ;
232+ // Resolve service role key — if same URL, offer to keep existing
233+ const supabaseKeyResult = await resolveCredential ( {
234+ envVar : "SUPABASE_SERVICE_ROLE_KEY" ,
235+ configValue : ( supabaseUrl === existingUrl ) ? existingKey : undefined ,
236+ promptLabel : "Service Role Key" ,
237+ required : true ,
238+ rl ,
239+ existingHint : ( existingKey && supabaseUrl === existingUrl ) ? "keep existing" : undefined ,
240+ } ) ;
225241
226- // Check if required tables exist
227- const missingTables = await checkSchemaExists ( supabaseUrl , supabaseKey ) ;
228- if ( missingTables . length > 0 ) {
229- console . log ( "" ) ;
230- console . log ( " ⚠ Missing tables: " + missingTables . join ( ", " ) ) ;
231- console . log ( " Run the schema setup in your Supabase SQL Editor:" ) ;
232- console . log ( "" ) ;
233- console . log ( " npx gitmem-mcp setup | pbcopy (macOS — copies SQL to clipboard)" ) ;
234- console . log ( " npx gitmem-mcp setup (prints SQL to paste manually)" ) ;
242+ const supabaseKey = supabaseKeyResult . value ;
243+
244+ // Step 4: Test connection if we have credentials
245+ let missingTables : string [ ] = [ ] ;
246+ let connectionFailed = false ;
247+ if ( supabaseUrl && supabaseKey ) {
248+ if ( supabaseUrlResult . source !== "config" || supabaseKeyResult . source !== "config" ) {
249+ // Only test if credentials are new (not just re-read from config)
250+ console . log ( " Testing connection..." ) ;
251+ const connected = await testSupabaseConnection ( supabaseUrl , supabaseKey ) ;
252+ if ( ! connected ) {
253+ connectionFailed = true ;
254+ if ( rl ) {
255+ // Interactive: hard failure — user can re-enter
256+ console . error ( " ✗ Could not connect to Supabase. Check your URL and key." ) ;
257+ rl . close ( ) ;
258+ process . exit ( 1 ) ;
259+ } else {
260+ // Non-interactive: warn but save credentials anyway
261+ console . log ( " ⚠ Could not connect to Supabase (credentials saved anyway)." ) ;
262+ console . log ( " Verify your SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are correct." ) ;
263+ }
264+ } else {
265+ console . log ( " ✓ Connected to Supabase" ) ;
266+ }
267+ }
268+
269+ if ( ! connectionFailed ) {
270+ missingTables = await checkSchemaExists ( supabaseUrl , supabaseKey ) ;
271+ if ( missingTables . length > 0 ) {
272+ console . log ( "" ) ;
273+ console . log ( " ⚠ Missing tables: " + missingTables . join ( ", " ) ) ;
274+ console . log ( " Run the schema setup in your Supabase SQL Editor:" ) ;
275+ console . log ( "" ) ;
276+ console . log ( " npx gitmem-mcp setup | pbcopy (macOS — copies SQL to clipboard)" ) ;
277+ console . log ( " npx gitmem-mcp setup (prints SQL to paste manually)" ) ;
278+ console . log ( "" ) ;
279+ console . log ( " Then: Supabase Dashboard → SQL Editor → New query → Paste → Run" ) ;
280+ } else {
281+ console . log ( " ✓ Schema verified (all tables present)" ) ;
282+ }
283+ }
235284 console . log ( "" ) ;
236- console . log ( " Then: Supabase Dashboard → SQL Editor → New query → Paste → Run" ) ;
285+ } else if ( ! supabaseUrl || ! supabaseKey ) {
286+ console . log ( " ⚠ Supabase credentials not provided." ) ;
287+ console . log ( " Pro features require Supabase. Set via:" ) ;
288+ console . log ( " - Environment: SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY" ) ;
289+ console . log ( " - Config: edit .gitmem/config.json (supabase_url, supabase_key)" ) ;
290+ console . log ( " - Re-run: npx gitmem-mcp activate (interactive)" ) ;
237291 console . log ( "" ) ;
238- } else {
239- console . log ( " ✓ Schema verified (all tables present)" ) ;
240292 }
241- console . log ( "" ) ;
242-
243- // Step 5: OpenRouter key
244- console . log ( "OpenRouter Setup" ) ;
245- console . log ( " (Get a key at https://openrouter.ai/keys)" ) ;
246293
247- const existingOpenRouter = config . openrouter_key as string | undefined ;
248- if ( existingOpenRouter ) {
249- console . log ( ` Current: ${ existingOpenRouter . substring ( 0 , 12 ) } ...` ) ;
294+ // Step 5: Resolve OpenRouter key (env → config → prompt)
295+ if ( rl ) {
296+ console . log ( "OpenRouter Setup" ) ;
297+ console . log ( " (Get a key at https://openrouter.ai/keys)" ) ;
298+ const existingOpenRouter = config . openrouter_key as string | undefined ;
299+ if ( existingOpenRouter ) {
300+ console . log ( ` Current: ${ existingOpenRouter . substring ( 0 , 12 ) } ...` ) ;
301+ }
302+ console . log ( "" ) ;
250303 }
251- console . log ( "" ) ;
252304
253- let openrouterKey : string ;
254- if ( existingOpenRouter ) {
255- const orInput = await ask ( rl , " API Key [keep existing]: " ) ;
256- openrouterKey = orInput || existingOpenRouter ;
257- } else {
258- openrouterKey = await ask ( rl , " API Key: " ) ;
259- }
305+ const openrouterResult = await resolveCredential ( {
306+ envVar : "OPENROUTER_API_KEY" ,
307+ configValue : config . openrouter_key as string | undefined ,
308+ promptLabel : "API Key" ,
309+ required : false ,
310+ rl,
311+ existingHint : ( config . openrouter_key as string ) ? "keep existing" : undefined ,
312+ } ) ;
313+
314+ const openrouterKey = openrouterResult . value ;
260315
261316 if ( openrouterKey ) {
262- console . log ( " ✓ OpenRouter configured" ) ;
263- } else {
317+ if ( openrouterResult . source === "env" ) {
318+ console . log ( " ✓ OpenRouter configured (from env)" ) ;
319+ } else if ( openrouterResult . source === "config" ) {
320+ // Silent — already configured
321+ } else {
322+ console . log ( " ✓ OpenRouter configured" ) ;
323+ }
324+ } else if ( rl ) {
264325 console . log ( " ⚠ Skipped (semantic search will not work without embeddings)" ) ;
265326 }
266327
267- rl . close ( ) ;
328+ if ( rl ) rl . close ( ) ;
268329
269330 // Step 6: Write config (preserves existing fields like project, install_id, feedback_enabled)
270331 config . api_key = apiKey ;
271- config . supabase_url = supabaseUrl ;
272- config . supabase_key = supabaseKey ;
273- if ( openrouterKey ) {
274- config . openrouter_key = openrouterKey ;
275- }
332+ if ( supabaseUrl ) config . supabase_url = supabaseUrl ;
333+ if ( supabaseKey ) config . supabase_key = supabaseKey ;
334+ if ( openrouterKey ) config . openrouter_key = openrouterKey ;
276335
277336 if ( ! fs . existsSync ( gitmemDir ) ) {
278337 fs . mkdirSync ( gitmemDir , { recursive : true } ) ;
@@ -282,13 +341,25 @@ export async function main(args: string[]): Promise<void> {
282341 // Clear any stale license cache
283342 clearLicenseCache ( ) ;
284343
344+ // Summary
285345 console . log ( "" ) ;
286346 console . log ( "─────────────────────" ) ;
287- if ( missingTables . length > 0 ) {
347+
348+ const sources : string [ ] = [ ] ;
349+ if ( supabaseUrl ) sources . push ( `Supabase (${ supabaseUrlResult . source } )` ) ;
350+ if ( openrouterKey ) sources . push ( `OpenRouter (${ openrouterResult . source } )` ) ;
351+
352+ if ( ! supabaseUrl ) {
353+ console . log ( "License key activated. Supabase credentials still needed for Pro features." ) ;
354+ } else if ( missingTables . length > 0 ) {
288355 console . log ( "Pro tier activated! Run schema setup, then restart your editor." ) ;
289356 } else {
290357 console . log ( "Pro tier activated! Restart your editor to apply." ) ;
291358 }
292- console . log ( `Config saved to ${ configPath } ` ) ;
359+
360+ if ( sources . length > 0 ) {
361+ console . log ( ` Credentials: ${ sources . join ( ", " ) } ` ) ;
362+ }
363+ console . log ( ` Config: ${ configPath } ` ) ;
293364 console . log ( "" ) ;
294365}
0 commit comments