gitmem-mcp 1.2.1 → 1.3.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.
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * GitMem Init Wizard
4
+ * GitMem Init Wizard — v2
5
5
  *
6
- * Interactive setup that detects existing config, prompts, and merges.
7
- * Supports Claude Code and Cursor IDE.
6
+ * Non-interactive by default on fresh install. Prompts only when
7
+ * existing config needs a merge decision.
8
8
  *
9
- * Usage: npx gitmem-mcp init [--yes] [--dry-run] [--project <name>] [--client <claude|cursor|vscode|windsurf|generic>]
9
+ * Usage: npx gitmem-mcp init [--yes] [--interactive] [--dry-run] [--project <name>] [--client <claude|cursor|vscode|windsurf|generic>]
10
10
  */
11
11
 
12
12
  import {
@@ -24,9 +24,42 @@ import { createInterface } from "readline/promises";
24
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
25
25
  const cwd = process.cwd();
26
26
 
27
- // Parse flags
27
+ // ── ANSI Colors — matches gitmem MCP display-protocol.ts ──
28
+
29
+ function useColor() {
30
+ if (process.env.NO_COLOR !== undefined) return false;
31
+ if (process.env.GITMEM_NO_COLOR !== undefined) return false;
32
+ return true;
33
+ }
34
+
35
+ const _color = useColor();
36
+
37
+ const C = {
38
+ reset: _color ? "\x1b[0m" : "",
39
+ bold: _color ? "\x1b[1m" : "",
40
+ dim: _color ? "\x1b[2m" : "",
41
+ red: _color ? "\x1b[31m" : "", // brand accent (Racing Red)
42
+ green: _color ? "\x1b[32m" : "", // success
43
+ yellow: _color ? "\x1b[33m" : "", // warning / prompts
44
+ underline: _color ? "\x1b[4m" : "",
45
+ italic: _color ? "\x1b[3m" : "",
46
+ };
47
+
48
+ // Brand mark: ripple icon — dim outer ring, red inner ring, bold center dot
49
+ const RIPPLE = `${C.dim}(${C.reset}${C.red}(${C.reset}${C.bold}\u25cf${C.reset}${C.red})${C.reset}${C.dim})${C.reset}`;
50
+ const PRODUCT = `${RIPPLE} ${C.red}gitmem${C.reset}`;
51
+
52
+ const CHECK = `${C.bold}\u2714${C.reset}`;
53
+ const SKIP = `${C.dim}\u00b7${C.reset}`;
54
+ const WARN = `${C.yellow}!${C.reset}`;
55
+
56
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
57
+
58
+ // ── Parse flags ──
59
+
28
60
  const args = process.argv.slice(2);
29
61
  const autoYes = args.includes("--yes") || args.includes("-y");
62
+ const interactive = args.includes("--interactive") || args.includes("-i");
30
63
  const dryRun = args.includes("--dry-run");
31
64
  const projectIdx = args.indexOf("--project");
32
65
  const projectName = projectIdx !== -1 ? args[projectIdx + 1] : null;
@@ -35,7 +68,6 @@ const clientFlag = clientIdx !== -1 ? args[clientIdx + 1]?.toLowerCase() : null;
35
68
 
36
69
  // ── Client Configuration ──
37
70
 
38
- // Resolve user home directory for clients that use user-level config
39
71
  const homeDir = process.env.HOME || process.env.USERPROFILE || "~";
40
72
 
41
73
  const CLIENT_CONFIGS = {
@@ -55,7 +87,7 @@ const CLIENT_CONFIGS = {
55
87
  hasPermissions: true,
56
88
  hooksInSettings: true,
57
89
  hasHooks: true,
58
- completionMsg: "Setup complete! Start Claude Code \u2014 memory is active.",
90
+ completionVerb: "Start Claude Code",
59
91
  },
60
92
  cursor: {
61
93
  name: "Cursor",
@@ -75,7 +107,7 @@ const CLIENT_CONFIGS = {
75
107
  hasHooks: true,
76
108
  hooksFile: join(cwd, ".cursor", "hooks.json"),
77
109
  hooksFileName: ".cursor/hooks.json",
78
- completionMsg: "Setup complete! Open Cursor (Agent mode) \u2014 memory is active.",
110
+ completionVerb: "Open Cursor (Agent mode)",
79
111
  },
80
112
  vscode: {
81
113
  name: "VS Code (Copilot)",
@@ -93,7 +125,7 @@ const CLIENT_CONFIGS = {
93
125
  hasPermissions: false,
94
126
  hooksInSettings: false,
95
127
  hasHooks: false,
96
- completionMsg: "Setup complete! Open VS Code \u2014 memory is active via Copilot.",
128
+ completionVerb: "Open VS Code",
97
129
  },
98
130
  windsurf: {
99
131
  name: "Windsurf",
@@ -111,7 +143,7 @@ const CLIENT_CONFIGS = {
111
143
  hasPermissions: false,
112
144
  hooksInSettings: false,
113
145
  hasHooks: false,
114
- completionMsg: "Setup complete! Open Windsurf \u2014 memory is active.",
146
+ completionVerb: "Open Windsurf",
115
147
  },
116
148
  generic: {
117
149
  name: "Generic MCP Client",
@@ -129,27 +161,25 @@ const CLIENT_CONFIGS = {
129
161
  hasPermissions: false,
130
162
  hooksInSettings: false,
131
163
  hasHooks: false,
132
- completionMsg:
133
- "Setup complete! Configure your MCP client to use the gitmem server from .mcp.json.",
164
+ completionVerb: "Configure your MCP client with .mcp.json",
134
165
  },
135
166
  };
136
167
 
137
- // Shared paths (client-agnostic)
168
+ // Shared paths
138
169
  const gitmemDir = join(cwd, ".gitmem");
139
170
  const gitignorePath = join(cwd, ".gitignore");
140
171
  const starterScarsPath = join(__dirname, "..", "schema", "starter-scars.json");
141
172
  const hooksScriptsDir = join(__dirname, "..", "hooks", "scripts");
142
173
 
143
174
  let rl;
144
- let client; // "claude" | "cursor" — set by detectClient()
145
- let cc; // shorthand for CLIENT_CONFIGS[client]
175
+ let client;
176
+ let cc;
146
177
 
147
178
  // ── Client Detection ──
148
179
 
149
180
  const VALID_CLIENTS = Object.keys(CLIENT_CONFIGS);
150
181
 
151
182
  function detectClient() {
152
- // Explicit flag takes priority
153
183
  if (clientFlag) {
154
184
  if (!VALID_CLIENTS.includes(clientFlag)) {
155
185
  console.error(` Error: Unknown client "${clientFlag}". Use --client ${VALID_CLIENTS.join("|")}.`);
@@ -158,7 +188,6 @@ function detectClient() {
158
188
  return clientFlag;
159
189
  }
160
190
 
161
- // Auto-detect based on directory/file presence
162
191
  const hasCursorDir = existsSync(join(cwd, ".cursor"));
163
192
  const hasClaudeDir = existsSync(join(cwd, ".claude"));
164
193
  const hasMcpJson = existsSync(join(cwd, ".mcp.json"));
@@ -169,28 +198,20 @@ function detectClient() {
169
198
  const hasVscodeMcp = existsSync(join(cwd, ".vscode", "mcp.json"));
170
199
  const hasCopilotInstructions = existsSync(join(cwd, ".github", "copilot-instructions.md"));
171
200
  const hasWindsurfRules = existsSync(join(cwd, ".windsurfrules"));
172
- const hasWindsurfMcp = existsSync(
173
- join(homeDir, ".codeium", "windsurf", "mcp_config.json")
174
- );
175
201
 
176
- // Strong Cursor signals
177
202
  if (hasCursorDir && !hasClaudeDir && !hasMcpJson && !hasClaudeMd) return "cursor";
178
203
  if (hasCursorRules && !hasClaudeMd && !hasCopilotInstructions) return "cursor";
179
204
  if (hasCursorMcp && !hasMcpJson && !hasVscodeMcp) return "cursor";
180
205
 
181
- // Strong Claude signals
182
206
  if (hasClaudeDir && !hasCursorDir && !hasVscodeDir) return "claude";
183
207
  if (hasMcpJson && !hasCursorMcp && !hasVscodeMcp) return "claude";
184
208
  if (hasClaudeMd && !hasCursorRules && !hasCopilotInstructions) return "claude";
185
209
 
186
- // VS Code signals
187
210
  if (hasVscodeMcp && !hasMcpJson && !hasCursorMcp) return "vscode";
188
211
  if (hasCopilotInstructions && !hasClaudeMd && !hasCursorRules) return "vscode";
189
212
 
190
- // Windsurf signals
191
213
  if (hasWindsurfRules && !hasClaudeMd && !hasCursorRules && !hasCopilotInstructions) return "windsurf";
192
214
 
193
- // Default to Claude Code (most common)
194
215
  return "claude";
195
216
  }
196
217
 
@@ -202,7 +223,7 @@ async function confirm(message, defaultYes = true) {
202
223
  rl = createInterface({ input: process.stdin, output: process.stdout });
203
224
  }
204
225
  const suffix = defaultYes ? "[Y/n]" : "[y/N]";
205
- const answer = await rl.question(` ${message} ${suffix} `);
226
+ const answer = await rl.question(`${C.yellow}?${C.reset} ${message} ${C.dim}${suffix}${C.reset} `);
206
227
  const trimmed = answer.trim().toLowerCase();
207
228
  if (trimmed === "") return defaultYes;
208
229
  return trimmed === "y" || trimmed === "yes";
@@ -220,6 +241,15 @@ function writeJson(path, data) {
220
241
  writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
221
242
  }
222
243
 
244
+ function log(icon, main, detail) {
245
+ if (detail) {
246
+ console.log(`${icon} ${C.bold}${main}${C.reset}`);
247
+ console.log(` ${C.dim}${detail}${C.reset}`);
248
+ } else {
249
+ console.log(`${icon} ${main}`);
250
+ }
251
+ }
252
+
223
253
  function buildMcpConfig() {
224
254
  const supabaseUrl = process.env.SUPABASE_URL;
225
255
  if (!supabaseUrl) {
@@ -253,113 +283,55 @@ function buildClaudeHooks() {
253
283
  ],
254
284
  },
255
285
  ],
256
- PreToolUse: [
257
- {
258
- matcher: "Bash",
259
- hooks: [
260
- {
261
- type: "command",
262
- command: `bash ${relScripts}/credential-guard.sh`,
263
- timeout: 3000,
264
- },
265
- {
266
- type: "command",
267
- command: `bash ${relScripts}/recall-check.sh`,
268
- timeout: 5000,
269
- },
270
- ],
271
- },
286
+ UserPromptSubmit: [
272
287
  {
273
- matcher: "Read",
274
288
  hooks: [
275
289
  {
276
290
  type: "command",
277
- command: `bash ${relScripts}/credential-guard.sh`,
291
+ command: `bash ${relScripts}/auto-retrieve-hook.sh`,
278
292
  timeout: 3000,
279
293
  },
280
294
  ],
281
295
  },
282
- {
283
- matcher: "Write",
284
- hooks: [
285
- {
286
- type: "command",
287
- command: `bash ${relScripts}/recall-check.sh`,
288
- timeout: 5000,
289
- },
290
- ],
291
- },
292
- {
293
- matcher: "Edit",
294
- hooks: [
295
- {
296
- type: "command",
297
- command: `bash ${relScripts}/recall-check.sh`,
298
- timeout: 5000,
299
- },
300
- ],
301
- },
302
296
  ],
303
- PostToolUse: [
304
- {
305
- matcher: "mcp__gitmem__recall",
306
- hooks: [
307
- {
308
- type: "command",
309
- command: `bash ${relScripts}/post-tool-use.sh`,
310
- timeout: 3000,
311
- },
312
- ],
313
- },
297
+ PreToolUse: [
314
298
  {
315
- matcher: "mcp__gitmem__search",
299
+ matcher: "Bash",
316
300
  hooks: [
317
- {
318
- type: "command",
319
- command: `bash ${relScripts}/post-tool-use.sh`,
320
- timeout: 3000,
321
- },
301
+ { type: "command", command: `bash ${relScripts}/credential-guard.sh`, timeout: 3000 },
302
+ { type: "command", command: `bash ${relScripts}/recall-check.sh`, timeout: 5000 },
322
303
  ],
323
304
  },
324
305
  {
325
- matcher: "Bash",
306
+ matcher: "Read",
326
307
  hooks: [
327
- {
328
- type: "command",
329
- command: `bash ${relScripts}/post-tool-use.sh`,
330
- timeout: 3000,
331
- },
308
+ { type: "command", command: `bash ${relScripts}/credential-guard.sh`, timeout: 3000 },
332
309
  ],
333
310
  },
334
311
  {
335
312
  matcher: "Write",
336
313
  hooks: [
337
- {
338
- type: "command",
339
- command: `bash ${relScripts}/post-tool-use.sh`,
340
- timeout: 3000,
341
- },
314
+ { type: "command", command: `bash ${relScripts}/recall-check.sh`, timeout: 5000 },
342
315
  ],
343
316
  },
344
317
  {
345
318
  matcher: "Edit",
346
319
  hooks: [
347
- {
348
- type: "command",
349
- command: `bash ${relScripts}/post-tool-use.sh`,
350
- timeout: 3000,
351
- },
320
+ { type: "command", command: `bash ${relScripts}/recall-check.sh`, timeout: 5000 },
352
321
  ],
353
322
  },
354
323
  ],
324
+ PostToolUse: [
325
+ { matcher: "mcp__gitmem__recall", hooks: [{ type: "command", command: `bash ${relScripts}/post-tool-use.sh`, timeout: 3000 }] },
326
+ { matcher: "mcp__gitmem__search", hooks: [{ type: "command", command: `bash ${relScripts}/post-tool-use.sh`, timeout: 3000 }] },
327
+ { matcher: "Bash", hooks: [{ type: "command", command: `bash ${relScripts}/post-tool-use.sh`, timeout: 3000 }] },
328
+ { matcher: "Write", hooks: [{ type: "command", command: `bash ${relScripts}/post-tool-use.sh`, timeout: 3000 }] },
329
+ { matcher: "Edit", hooks: [{ type: "command", command: `bash ${relScripts}/post-tool-use.sh`, timeout: 3000 }] },
330
+ ],
355
331
  Stop: [
356
332
  {
357
333
  hooks: [
358
- {
359
- type: "command",
360
- command: `bash ${relScripts}/session-close-check.sh`,
361
- timeout: 5000,
362
- },
334
+ { type: "command", command: `bash ${relScripts}/session-close-check.sh`, timeout: 5000 },
363
335
  ],
364
336
  },
365
337
  ],
@@ -368,49 +340,21 @@ function buildClaudeHooks() {
368
340
 
369
341
  function buildCursorHooks() {
370
342
  const relScripts = ".gitmem/hooks";
371
- // Cursor hooks format: .cursor/hooks.json
372
- // Events: sessionStart, beforeMCPExecution, afterMCPExecution, stop
373
- // No per-tool matchers — all MCP calls go through beforeMCPExecution
374
343
  return {
375
- sessionStart: [
376
- {
377
- command: `bash ${relScripts}/session-start.sh`,
378
- timeout: 5000,
379
- },
380
- ],
344
+ sessionStart: [{ command: `bash ${relScripts}/session-start.sh`, timeout: 5000 }],
381
345
  beforeMCPExecution: [
382
- {
383
- command: `bash ${relScripts}/credential-guard.sh`,
384
- timeout: 3000,
385
- },
386
- {
387
- command: `bash ${relScripts}/recall-check.sh`,
388
- timeout: 5000,
389
- },
390
- ],
391
- afterMCPExecution: [
392
- {
393
- command: `bash ${relScripts}/post-tool-use.sh`,
394
- timeout: 3000,
395
- },
396
- ],
397
- stop: [
398
- {
399
- command: `bash ${relScripts}/session-close-check.sh`,
400
- timeout: 5000,
401
- },
346
+ { command: `bash ${relScripts}/credential-guard.sh`, timeout: 3000 },
347
+ { command: `bash ${relScripts}/recall-check.sh`, timeout: 5000 },
402
348
  ],
349
+ afterMCPExecution: [{ command: `bash ${relScripts}/post-tool-use.sh`, timeout: 3000 }],
350
+ stop: [{ command: `bash ${relScripts}/session-close-check.sh`, timeout: 5000 }],
403
351
  };
404
352
  }
405
353
 
406
354
  function isGitmemHook(entry) {
407
- // Claude Code format: entry.hooks is an array of {command: "..."}
408
355
  if (entry.hooks && Array.isArray(entry.hooks)) {
409
- return entry.hooks.some(
410
- (h) => typeof h.command === "string" && h.command.includes("gitmem")
411
- );
356
+ return entry.hooks.some((h) => typeof h.command === "string" && h.command.includes("gitmem"));
412
357
  }
413
- // Cursor format: entry itself has {command: "..."}
414
358
  if (typeof entry.command === "string") {
415
359
  return entry.command.includes("gitmem");
416
360
  }
@@ -425,7 +369,29 @@ function getInstructionsTemplate() {
425
369
  }
426
370
  }
427
371
 
372
+ function copyHookScripts() {
373
+ const destHooksDir = join(gitmemDir, "hooks");
374
+ if (!existsSync(destHooksDir)) {
375
+ mkdirSync(destHooksDir, { recursive: true });
376
+ }
377
+ if (existsSync(hooksScriptsDir)) {
378
+ try {
379
+ for (const file of readdirSync(hooksScriptsDir)) {
380
+ if (file.endsWith(".sh")) {
381
+ const src = join(hooksScriptsDir, file);
382
+ const dest = join(destHooksDir, file);
383
+ writeFileSync(dest, readFileSync(src));
384
+ chmodSync(dest, 0o755);
385
+ }
386
+ }
387
+ } catch {
388
+ // Non-critical
389
+ }
390
+ }
391
+ }
392
+
428
393
  // ── Steps ──
394
+ // Each returns { done: bool } so main can track progress
429
395
 
430
396
  async function stepMemoryStore() {
431
397
  const learningsPath = join(gitmemDir, "learnings.json");
@@ -441,29 +407,29 @@ async function stepMemoryStore() {
441
407
  try {
442
408
  starterScars = JSON.parse(readFileSync(starterScarsPath, "utf-8"));
443
409
  } catch {
444
- console.log(" ! Could not read starter-scars.json. Skipping.");
445
- return;
410
+ log(WARN, "Could not read starter lessons. Skipping.");
411
+ return { done: false };
446
412
  }
447
413
 
448
414
  if (exists && existingCount >= starterScars.length) {
449
- console.log(
450
- ` Already configured (${existingCount} scars in .gitmem/). Skipping.`
451
- );
452
- return;
453
- }
454
-
455
- const prompt = exists
456
- ? `Merge ${starterScars.length} starter scars into .gitmem/? (${existingCount} existing)`
457
- : `Create .gitmem/ with ${starterScars.length} starter scars?`;
458
-
459
- if (!(await confirm(prompt))) {
460
- console.log(" Skipped.");
461
- return;
415
+ log(CHECK, `Memory store already set up ${C.dim}(${existingCount} lessons in .gitmem/)${C.reset}`);
416
+ return { done: false };
417
+ }
418
+
419
+ // Needs merge — prompt if existing data OR interactive mode
420
+ if (exists || interactive) {
421
+ const prompt = exists
422
+ ? `Merge ${starterScars.length} lessons into .gitmem/? (${existingCount} existing)`
423
+ : `Create .gitmem/ with ${starterScars.length} starter lessons?`;
424
+ if (!(await confirm(prompt))) {
425
+ log(SKIP, "Memory store skipped");
426
+ return { done: false };
427
+ }
462
428
  }
463
429
 
464
430
  if (dryRun) {
465
- console.log(` [dry-run] Would create .gitmem/ with ${starterScars.length} starter scars`);
466
- return;
431
+ log(CHECK, `Would create .gitmem/ with ${starterScars.length} starter lessons`, "[dry-run]");
432
+ return { done: true };
467
433
  }
468
434
 
469
435
  if (!existsSync(gitmemDir)) {
@@ -498,6 +464,19 @@ async function stepMemoryStore() {
498
464
  }
499
465
  writeJson(learningsPath, existing);
500
466
 
467
+ // Starter thread — nudges user to add their own project-specific scar
468
+ const threadsPath = join(gitmemDir, "threads.json");
469
+ if (!existsSync(threadsPath)) {
470
+ writeJson(threadsPath, [
471
+ {
472
+ id: "t-welcome01",
473
+ text: "Add your first project-specific scar from a real mistake — starter lessons are generic, yours will be relevant",
474
+ status: "open",
475
+ created_at: now,
476
+ },
477
+ ]);
478
+ }
479
+
501
480
  // Empty collection files
502
481
  for (const file of ["sessions.json", "decisions.json", "scar-usage.json"]) {
503
482
  const filePath = join(gitmemDir, file);
@@ -506,20 +485,14 @@ async function stepMemoryStore() {
506
485
  }
507
486
  }
508
487
 
509
- // Closing payload template — agents read this before writing closing-payload.json
488
+ // Closing payload template
510
489
  const templatePath = join(gitmemDir, "closing-payload-template.json");
511
490
  if (!existsSync(templatePath)) {
512
491
  writeJson(templatePath, {
513
492
  closing_reflection: {
514
- what_broke: "",
515
- what_took_longer: "",
516
- do_differently: "",
517
- what_worked: "",
518
- wrong_assumption: "",
519
- scars_applied: [],
520
- institutional_memory_items: "",
521
- collaborative_dynamic: "",
522
- rapport_notes: ""
493
+ what_broke: "", what_took_longer: "", do_differently: "",
494
+ what_worked: "", wrong_assumption: "", scars_applied: [],
495
+ institutional_memory_items: "", collaborative_dynamic: "", rapport_notes: ""
523
496
  },
524
497
  task_completion: {
525
498
  questions_displayed_at: "ISO-8601 timestamp",
@@ -528,56 +501,52 @@ async function stepMemoryStore() {
528
501
  human_response_at: "ISO-8601 timestamp",
529
502
  human_response: "no corrections | actual corrections text"
530
503
  },
531
- human_corrections: "",
532
- scars_to_record: [],
533
- learnings_created: [],
534
- open_threads: [],
535
- decisions: []
504
+ human_corrections: "", scars_to_record: [],
505
+ learnings_created: [], open_threads: [], decisions: []
536
506
  });
537
507
  }
538
508
 
539
- console.log(
540
- ` Created .gitmem/ with ${starterScars.length} starter scars` +
541
- (added < starterScars.length
542
- ? ` (${added} new, ${starterScars.length - added} already existed)`
543
- : "")
509
+ const mergeNote = added < starterScars.length
510
+ ? ` (${added} new, ${starterScars.length - added} already existed)`
511
+ : "";
512
+
513
+ log(CHECK,
514
+ "Created .gitmem/ \u2014 your local memory store",
515
+ `${starterScars.length} lessons from common mistakes included${mergeNote}`
544
516
  );
517
+ return { done: true };
545
518
  }
546
519
 
547
520
  async function stepMcpServer() {
548
521
  const mcpPath = cc.mcpConfigPath;
549
522
  const mcpName = cc.mcpConfigName;
550
- const isUserLevel = cc.mcpConfigScope === "user";
551
523
 
552
524
  const existing = readJson(mcpPath);
553
- const hasGitmem =
554
- existing?.mcpServers?.gitmem || existing?.mcpServers?.["gitmem-mcp"];
525
+ const hasGitmem = existing?.mcpServers?.gitmem || existing?.mcpServers?.["gitmem-mcp"];
555
526
 
556
527
  if (hasGitmem) {
557
- console.log(` Already configured in ${mcpName}. Skipping.`);
558
- return;
528
+ log(CHECK, `MCP server already configured ${C.dim}(${mcpName})${C.reset}`);
529
+ return { done: false };
559
530
  }
560
531
 
561
- const serverCount = existing?.mcpServers
562
- ? Object.keys(existing.mcpServers).length
563
- : 0;
564
- const tierLabel = process.env.SUPABASE_URL ? "pro" : "free";
565
- const scopeNote = isUserLevel ? " (user-level config)" : "";
566
- const prompt = existing
567
- ? `Add gitmem to ${mcpName}?${scopeNote} (${serverCount} existing server${serverCount !== 1 ? "s" : ""} preserved)`
568
- : `Create ${mcpName} with gitmem server?${scopeNote}`;
569
-
570
- if (!(await confirm(prompt))) {
571
- console.log(" Skipped.");
572
- return;
532
+ const serverCount = existing?.mcpServers ? Object.keys(existing.mcpServers).length : 0;
533
+
534
+ // Existing servers — prompt for merge
535
+ if ((existing && serverCount > 0) || interactive) {
536
+ const prompt = existing
537
+ ? `Add gitmem to ${mcpName}? (${serverCount} existing server${serverCount !== 1 ? "s" : ""} preserved)`
538
+ : `Create ${mcpName} with gitmem server?`;
539
+ if (!(await confirm(prompt))) {
540
+ log(SKIP, "MCP server skipped");
541
+ return { done: false };
542
+ }
573
543
  }
574
544
 
575
545
  if (dryRun) {
576
- console.log(` [dry-run] Would add gitmem entry to ${mcpName} (${tierLabel} tier${scopeNote})`);
577
- return;
546
+ log(CHECK, `Would configure MCP server in ${mcpName}`, "[dry-run]");
547
+ return { done: true };
578
548
  }
579
549
 
580
- // Ensure parent directory exists (for .cursor/mcp.json, .vscode/mcp.json, ~/.codeium/windsurf/)
581
550
  const parentDir = dirname(mcpPath);
582
551
  if (!existsSync(parentDir)) {
583
552
  mkdirSync(parentDir, { recursive: true });
@@ -588,12 +557,12 @@ async function stepMcpServer() {
588
557
  config.mcpServers.gitmem = buildMcpConfig();
589
558
  writeJson(mcpPath, config);
590
559
 
591
- console.log(
592
- ` Added gitmem entry to ${mcpName} (${tierLabel} tier` +
593
- (process.env.SUPABASE_URL ? " \u2014 Supabase detected" : " \u2014 local storage") +
594
- ")" +
595
- (isUserLevel ? " [user-level]" : "")
560
+ const preserveNote = serverCount > 0 ? ` (${serverCount} existing server${serverCount !== 1 ? "s" : ""} preserved)` : "";
561
+ log(CHECK,
562
+ `Configured MCP server${preserveNote}`,
563
+ `${cc.name} connects to gitmem automatically`
596
564
  );
565
+ return { done: true };
597
566
  }
598
567
 
599
568
  async function stepInstructions() {
@@ -601,8 +570,8 @@ async function stepInstructions() {
601
570
  const instrName = cc.instructionsName;
602
571
 
603
572
  if (!template) {
604
- console.log(` ! ${instrName} template not found. Skipping.`);
605
- return;
573
+ log(WARN, `${instrName} template not found. Skipping.`);
574
+ return { done: false };
606
575
  }
607
576
 
608
577
  const instrPath = cc.instructionsFile;
@@ -610,33 +579,31 @@ async function stepInstructions() {
610
579
  let content = exists ? readFileSync(instrPath, "utf-8") : "";
611
580
 
612
581
  if (content.includes(cc.startMarker)) {
613
- console.log(` Already configured in ${instrName}. Skipping.`);
614
- return;
615
- }
616
-
617
- const prompt = exists
618
- ? `Append gitmem section to ${instrName}?`
619
- : `Create ${instrName} with gitmem instructions?`;
620
-
621
- if (!(await confirm(prompt))) {
622
- console.log(" Skipped.");
623
- return;
582
+ log(CHECK, `Instructions already configured ${C.dim}(${instrName})${C.reset}`);
583
+ return { done: false };
584
+ }
585
+
586
+ // Existing file without gitmem section — prompt for append
587
+ if (exists || interactive) {
588
+ const prompt = exists
589
+ ? `Add gitmem section to existing ${instrName}?`
590
+ : `Create ${instrName} with gitmem instructions?`;
591
+ if (!(await confirm(prompt))) {
592
+ log(SKIP, "Instructions skipped");
593
+ return { done: false };
594
+ }
624
595
  }
625
596
 
626
597
  if (dryRun) {
627
- console.log(
628
- ` [dry-run] Would ${exists ? "append gitmem section to" : "create"} ${instrName}`
629
- );
630
- return;
598
+ log(CHECK, `Would ${exists ? "update" : "create"} ${instrName}`, "[dry-run]");
599
+ return { done: true };
631
600
  }
632
601
 
633
- // Template should already have delimiters, but ensure they're there
634
602
  let block = template;
635
603
  if (!block.includes(cc.startMarker)) {
636
604
  block = `${cc.startMarker}\n${block}\n${cc.endMarker}`;
637
605
  }
638
606
 
639
- // Ensure parent directory exists (for .github/copilot-instructions.md)
640
607
  const instrParentDir = dirname(instrPath);
641
608
  if (!existsSync(instrParentDir)) {
642
609
  mkdirSync(instrParentDir, { recursive: true });
@@ -649,35 +616,38 @@ async function stepInstructions() {
649
616
  }
650
617
 
651
618
  writeFileSync(instrPath, content);
652
- console.log(
653
- ` ${exists ? "Added gitmem section to" : "Created"} ${instrName}`
619
+
620
+ log(CHECK,
621
+ `${exists ? "Updated" : "Created"} ${instrName}`,
622
+ exists
623
+ ? "Added gitmem section (your existing content is preserved)"
624
+ : "Teaches your agent how to use memory"
654
625
  );
626
+ return { done: true };
655
627
  }
656
628
 
657
629
  async function stepPermissions() {
658
- // Cursor doesn't have an equivalent permissions system
659
- if (!cc.hasPermissions) {
660
- console.log(` Not needed for ${cc.name}. Skipping.`);
661
- return;
662
- }
630
+ if (!cc.hasPermissions) return { done: false };
663
631
 
664
632
  const existing = readJson(cc.settingsFile);
665
633
  const allow = existing?.permissions?.allow || [];
666
634
  const pattern = "mcp__gitmem__*";
667
635
 
668
636
  if (allow.includes(pattern)) {
669
- console.log(` Already configured in ${cc.configDir}/settings.json. Skipping.`);
670
- return;
637
+ log(CHECK, `Tool permissions already configured`);
638
+ return { done: false };
671
639
  }
672
640
 
673
- if (!(await confirm(`Add mcp__gitmem__* to ${cc.configDir}/settings.json?`))) {
674
- console.log(" Skipped.");
675
- return;
641
+ if (interactive) {
642
+ if (!(await confirm(`Auto-approve gitmem tools in ${cc.configDir}/settings.json?`))) {
643
+ log(SKIP, "Tool permissions skipped");
644
+ return { done: false };
645
+ }
676
646
  }
677
647
 
678
648
  if (dryRun) {
679
- console.log(" [dry-run] Would add gitmem tool permissions");
680
- return;
649
+ log(CHECK, "Would auto-approve gitmem tools", "[dry-run]");
650
+ return { done: true };
681
651
  }
682
652
 
683
653
  const settings = existing || {};
@@ -690,35 +660,16 @@ async function stepPermissions() {
690
660
  settings.permissions = { ...permissions, allow: newAllow };
691
661
  writeJson(cc.settingsFile, settings);
692
662
 
693
- console.log(" Added gitmem tool permissions");
694
- }
695
-
696
- function copyHookScripts() {
697
- const destHooksDir = join(gitmemDir, "hooks");
698
- if (!existsSync(destHooksDir)) {
699
- mkdirSync(destHooksDir, { recursive: true });
700
- }
701
- if (existsSync(hooksScriptsDir)) {
702
- try {
703
- for (const file of readdirSync(hooksScriptsDir)) {
704
- if (file.endsWith(".sh")) {
705
- const src = join(hooksScriptsDir, file);
706
- const dest = join(destHooksDir, file);
707
- writeFileSync(dest, readFileSync(src));
708
- chmodSync(dest, 0o755);
709
- }
710
- }
711
- } catch {
712
- // Non-critical
713
- }
714
- }
663
+ log(CHECK,
664
+ "Auto-approved gitmem tools",
665
+ "Memory tools run without interrupting you"
666
+ );
667
+ return { done: true };
715
668
  }
716
669
 
717
670
  async function stepHooks() {
718
671
  if (!cc.hasHooks) {
719
- console.log(` ${cc.name} does not support lifecycle hooks. Skipping.`);
720
- console.log(" Enforcement relies on system prompt instructions instead.");
721
- return;
672
+ return { done: false };
722
673
  }
723
674
  if (cc.hooksInSettings) {
724
675
  return stepHooksClaude();
@@ -732,11 +683,10 @@ async function stepHooksClaude() {
732
683
  const hasGitmem = JSON.stringify(hooks).includes("gitmem");
733
684
 
734
685
  if (hasGitmem) {
735
- console.log(" Already configured in .claude/settings.json. Skipping.");
736
- return;
686
+ log(CHECK, `Automatic memory hooks already configured`);
687
+ return { done: false };
737
688
  }
738
689
 
739
- // Count existing non-gitmem hooks
740
690
  let existingHookCount = 0;
741
691
  for (const entries of Object.values(hooks)) {
742
692
  if (Array.isArray(entries)) {
@@ -744,19 +694,20 @@ async function stepHooksClaude() {
744
694
  }
745
695
  }
746
696
 
747
- const prompt =
748
- existingHookCount > 0
749
- ? `Merge gitmem hooks into .claude/settings.json? (${existingHookCount} existing hook${existingHookCount !== 1 ? "s" : ""} preserved)`
750
- : "Add gitmem lifecycle hooks to .claude/settings.json?";
751
-
752
- if (!(await confirm(prompt))) {
753
- console.log(" Skipped.");
754
- return;
697
+ // Existing hooks — prompt for merge
698
+ if (existingHookCount > 0 || interactive) {
699
+ const prompt = existingHookCount > 0
700
+ ? `Add memory hooks? (${existingHookCount} existing hook${existingHookCount !== 1 ? "s" : ""} preserved)`
701
+ : "Add automatic memory hooks for session tracking?";
702
+ if (!(await confirm(prompt))) {
703
+ log(SKIP, "Hooks skipped");
704
+ return { done: false };
705
+ }
755
706
  }
756
707
 
757
708
  if (dryRun) {
758
- console.log(" [dry-run] Would merge 4 gitmem hook types");
759
- return;
709
+ log(CHECK, "Would add automatic memory hooks", "[dry-run]");
710
+ return { done: true };
760
711
  }
761
712
 
762
713
  copyHookScripts();
@@ -778,25 +729,23 @@ async function stepHooksClaude() {
778
729
  settings.hooks = merged;
779
730
  writeJson(cc.settingsFile, settings);
780
731
 
781
- const preservedMsg =
782
- existingHookCount > 0
783
- ? ` (${existingHookCount} existing hook${existingHookCount !== 1 ? "s" : ""} preserved)`
784
- : "";
785
- console.log(` Merged 4 gitmem hook types${preservedMsg}`);
732
+ const preserveNote = existingHookCount > 0
733
+ ? ` (${existingHookCount} existing hook${existingHookCount !== 1 ? "s" : ""} preserved)`
734
+ : "";
735
+
736
+ log(CHECK,
737
+ `Added automatic memory hooks${preserveNote}`,
738
+ "Sessions auto-start, memory retrieval on key actions"
739
+ );
786
740
 
787
- // Warn about settings.local.json
788
741
  if (cc.settingsLocalFile && existsSync(cc.settingsLocalFile)) {
789
742
  const local = readJson(cc.settingsLocalFile);
790
743
  if (local?.hooks) {
791
- console.log("");
792
- console.log(
793
- " Note: .claude/settings.local.json also has hooks."
794
- );
795
- console.log(
796
- " Local hooks take precedence. You may need to manually merge."
797
- );
744
+ console.log(` ${C.yellow}Note:${C.reset} ${C.dim}.claude/settings.local.json also has hooks \u2014 may need manual merge${C.reset}`);
798
745
  }
799
746
  }
747
+
748
+ return { done: true };
800
749
  }
801
750
 
802
751
  async function stepHooksCursor() {
@@ -807,11 +756,10 @@ async function stepHooksCursor() {
807
756
  const hasGitmem = existing ? JSON.stringify(existing).includes("gitmem") : false;
808
757
 
809
758
  if (hasGitmem) {
810
- console.log(` Already configured in ${hooksName}. Skipping.`);
811
- return;
759
+ log(CHECK, `Automatic memory hooks already configured ${C.dim}(${hooksName})${C.reset}`);
760
+ return { done: false };
812
761
  }
813
762
 
814
- // Count existing non-gitmem hooks
815
763
  let existingHookCount = 0;
816
764
  if (existing?.hooks) {
817
765
  for (const entries of Object.values(existing.hooks)) {
@@ -821,19 +769,19 @@ async function stepHooksCursor() {
821
769
  }
822
770
  }
823
771
 
824
- const prompt =
825
- existingHookCount > 0
826
- ? `Merge gitmem hooks into ${hooksName}? (${existingHookCount} existing hook${existingHookCount !== 1 ? "s" : ""} preserved)`
827
- : `Add gitmem lifecycle hooks to ${hooksName}?`;
828
-
829
- if (!(await confirm(prompt))) {
830
- console.log(" Skipped.");
831
- return;
772
+ if (existingHookCount > 0 || interactive) {
773
+ const prompt = existingHookCount > 0
774
+ ? `Add memory hooks to ${hooksName}? (${existingHookCount} existing hook${existingHookCount !== 1 ? "s" : ""} preserved)`
775
+ : `Add automatic memory hooks to ${hooksName}?`;
776
+ if (!(await confirm(prompt))) {
777
+ log(SKIP, "Hooks skipped");
778
+ return { done: false };
779
+ }
832
780
  }
833
781
 
834
782
  if (dryRun) {
835
- console.log(" [dry-run] Would merge 4 gitmem hook types");
836
- return;
783
+ log(CHECK, "Would add automatic memory hooks", "[dry-run]");
784
+ return { done: true };
837
785
  }
838
786
 
839
787
  copyHookScripts();
@@ -855,11 +803,15 @@ async function stepHooksCursor() {
855
803
  config.hooks = merged;
856
804
  writeJson(hooksPath, config);
857
805
 
858
- const preservedMsg =
859
- existingHookCount > 0
860
- ? ` (${existingHookCount} existing hook${existingHookCount !== 1 ? "s" : ""} preserved)`
861
- : "";
862
- console.log(` Merged 4 gitmem hook types${preservedMsg}`);
806
+ const preserveNote = existingHookCount > 0
807
+ ? ` (${existingHookCount} existing hook${existingHookCount !== 1 ? "s" : ""} preserved)`
808
+ : "";
809
+
810
+ log(CHECK,
811
+ `Added automatic memory hooks${preserveNote}`,
812
+ "Sessions auto-start, memory retrieval on key actions"
813
+ );
814
+ return { done: true };
863
815
  }
864
816
 
865
817
  async function stepGitignore() {
@@ -867,18 +819,20 @@ async function stepGitignore() {
867
819
  let content = exists ? readFileSync(gitignorePath, "utf-8") : "";
868
820
 
869
821
  if (content.includes(".gitmem/")) {
870
- console.log(" Already configured in .gitignore. Skipping.");
871
- return;
822
+ log(CHECK, `.gitignore already configured`);
823
+ return { done: false };
872
824
  }
873
825
 
874
- if (!(await confirm("Add .gitmem/ to .gitignore?"))) {
875
- console.log(" Skipped.");
876
- return;
826
+ if (interactive) {
827
+ if (!(await confirm("Add .gitmem/ to .gitignore?"))) {
828
+ log(SKIP, "Gitignore skipped");
829
+ return { done: false };
830
+ }
877
831
  }
878
832
 
879
833
  if (dryRun) {
880
- console.log(" [dry-run] Would add .gitmem/ to .gitignore");
881
- return;
834
+ log(CHECK, "Would update .gitignore", "[dry-run]");
835
+ return { done: true };
882
836
  }
883
837
 
884
838
  if (exists) {
@@ -888,7 +842,11 @@ async function stepGitignore() {
888
842
  }
889
843
  writeFileSync(gitignorePath, content);
890
844
 
891
- console.log(` ${exists ? "Updated" : "Created"} .gitignore`);
845
+ log(CHECK,
846
+ `${exists ? "Updated" : "Created"} .gitignore`,
847
+ "Memory stays local \u2014 not committed to your repo"
848
+ );
849
+ return { done: true };
892
850
  }
893
851
 
894
852
  // ── Main ──
@@ -897,126 +855,71 @@ async function main() {
897
855
  const pkg = readJson(join(__dirname, "..", "package.json"));
898
856
  const version = pkg?.version || "1.0.0";
899
857
 
900
- // Detect client before anything else
901
858
  client = detectClient();
902
859
  cc = CLIENT_CONFIGS[client];
903
860
 
861
+ // ── Header — matches gitmem MCP product line format ──
904
862
  console.log("");
905
- console.log(` gitmem v${version} \u2014 Setup for ${cc.name}`);
906
- if (dryRun) {
907
- console.log(" (dry-run mode \u2014 no files will be written)");
908
- }
909
- if (clientFlag) {
910
- console.log(` (client: ${client} \u2014 via --client flag)`);
911
- } else {
912
- console.log(` (client: ${client} \u2014 auto-detected)`);
913
- }
914
- console.log("");
915
-
916
- // Detect environment
917
- console.log(" Detecting environment...");
918
- const detections = [];
863
+ console.log(`${PRODUCT} \u2500\u2500 init v${version}`);
864
+ console.log(`${C.dim}Setting up for ${cc.name}${clientFlag ? "" : " (auto-detected)"}${C.reset}`);
919
865
 
920
- if (existsSync(cc.mcpConfigPath)) {
921
- const mcp = readJson(cc.mcpConfigPath);
922
- const count = mcp?.mcpServers ? Object.keys(mcp.mcpServers).length : 0;
923
- detections.push(
924
- ` ${cc.mcpConfigName} found (${count} server${count !== 1 ? "s" : ""})`
925
- );
926
- }
927
-
928
- if (existsSync(cc.instructionsFile)) {
929
- const content = readFileSync(cc.instructionsFile, "utf-8");
930
- const hasGitmem = content.includes(cc.startMarker);
931
- detections.push(
932
- ` ${cc.instructionsName} found (${hasGitmem ? "has gitmem section" : "no gitmem section"})`
933
- );
934
- }
935
-
936
- if (cc.settingsFile && existsSync(cc.settingsFile)) {
937
- const settings = readJson(cc.settingsFile);
938
- const hookCount = settings?.hooks
939
- ? Object.values(settings.hooks).flat().length
940
- : 0;
941
- detections.push(
942
- ` .claude/settings.json found (${hookCount} hook${hookCount !== 1 ? "s" : ""})`
943
- );
944
- }
945
-
946
- if (!cc.hooksInSettings && cc.hasHooks && cc.hooksFile && existsSync(cc.hooksFile)) {
947
- const hooks = readJson(cc.hooksFile);
948
- const hookCount = hooks?.hooks
949
- ? Object.values(hooks.hooks).flat().length
950
- : 0;
951
- detections.push(
952
- ` ${cc.hooksFileName} found (${hookCount} hook${hookCount !== 1 ? "s" : ""})`
953
- );
954
- }
955
-
956
- if (existsSync(gitignorePath)) {
957
- detections.push(" .gitignore found");
866
+ if (dryRun) {
867
+ console.log(`${C.dim}dry-run mode \u2014 no files will be written${C.reset}`);
958
868
  }
959
869
 
960
- if (existsSync(gitmemDir)) {
961
- detections.push(" .gitmem/ found");
962
- }
870
+ console.log("");
963
871
 
964
- for (const d of detections) {
965
- console.log(d);
966
- }
872
+ // ── Run steps ──
967
873
 
968
- const tier = process.env.SUPABASE_URL ? "pro" : "free";
969
- console.log(
970
- ` Tier: ${tier}` +
971
- (tier === "free" ? " (no SUPABASE_URL detected)" : " (SUPABASE_URL detected)")
972
- );
973
- console.log("");
874
+ let configured = 0;
974
875
 
975
- // Run steps step count depends on client capabilities
976
- let stepCount = 4; // memory store + mcp server + instructions + gitignore
977
- if (cc.hasPermissions) stepCount++;
978
- if (cc.hasHooks) stepCount++;
979
- let step = 1;
876
+ const d = _color && !dryRun ? 500 : 0;
980
877
 
981
- console.log(` Step ${step}/${stepCount} \u2014 Memory Store`);
982
- await stepMemoryStore();
983
- console.log("");
984
- step++;
878
+ const r1 = await stepMemoryStore();
879
+ if (r1.done) configured++;
880
+ if (d) await sleep(d);
985
881
 
986
- console.log(` Step ${step}/${stepCount} \u2014 MCP Server`);
987
- await stepMcpServer();
988
- console.log("");
989
- step++;
882
+ const r2 = await stepMcpServer();
883
+ if (r2.done) configured++;
884
+ if (d) await sleep(d);
990
885
 
991
- console.log(` Step ${step}/${stepCount} \u2014 Project Instructions`);
992
- await stepInstructions();
993
- console.log("");
994
- step++;
886
+ const r3 = await stepInstructions();
887
+ if (r3.done) configured++;
888
+ if (d) await sleep(d);
995
889
 
996
890
  if (cc.hasPermissions) {
997
- console.log(` Step ${step}/${stepCount} \u2014 Tool Permissions`);
998
- await stepPermissions();
999
- console.log("");
1000
- step++;
891
+ const r4 = await stepPermissions();
892
+ if (r4.done) configured++;
893
+ if (d) await sleep(d);
1001
894
  }
1002
895
 
1003
896
  if (cc.hasHooks) {
1004
- console.log(` Step ${step}/${stepCount} \u2014 Lifecycle Hooks`);
1005
- await stepHooks();
1006
- console.log("");
1007
- step++;
897
+ const r5 = await stepHooks();
898
+ if (r5.done) configured++;
899
+ if (d) await sleep(d);
1008
900
  }
1009
901
 
1010
- console.log(` Step ${step}/${stepCount} \u2014 Gitignore`);
1011
- await stepGitignore();
1012
- console.log("");
902
+ const r6 = await stepGitignore();
903
+ if (r6.done) configured++;
1013
904
 
905
+ // ── Footer ──
1014
906
  if (dryRun) {
1015
- console.log(" Dry run complete \u2014 no files were modified.");
907
+ console.log(`${C.dim}Dry run complete \u2014 no files were modified.${C.reset}`);
908
+ } else if (configured === 0) {
909
+ console.log(`${C.dim}gitmem-mcp is already installed and configured.${C.reset}`);
910
+ console.log(`${C.dim}Need help? ${C.reset}${C.red}https://gitmem.ai/docs${C.reset}`);
1016
911
  } else {
1017
- console.log(` ${cc.completionMsg}`);
1018
- console.log(" To remove: npx gitmem-mcp uninstall");
912
+ console.log("");
913
+ console.log("───────────────────────────────────────────────────");
914
+ console.log("");
915
+ console.log(`${PRODUCT} ${C.red}${C.bold}installed successfully!${C.reset}`);
916
+ console.log(`${C.dim}Docs:${C.reset} ${C.red}https://gitmem.ai/docs${C.reset}`);
917
+ console.log(`${C.dim}Remove:${C.reset} npx gitmem-mcp uninstall`);
918
+ console.log("");
919
+ console.log(`${C.dim}Try asking your agent:${C.reset}`);
920
+ console.log(` ${C.italic}"Review the gitmem tools, test them, convince yourself"${C.reset}`);
1019
921
  }
922
+
1020
923
  console.log("");
1021
924
 
1022
925
  if (rl) rl.close();