gitmem-mcp 1.2.2 → 1.3.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.
@@ -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) {
@@ -268,109 +298,40 @@ function buildClaudeHooks() {
268
298
  {
269
299
  matcher: "Bash",
270
300
  hooks: [
271
- {
272
- type: "command",
273
- command: `bash ${relScripts}/credential-guard.sh`,
274
- timeout: 3000,
275
- },
276
- {
277
- type: "command",
278
- command: `bash ${relScripts}/recall-check.sh`,
279
- timeout: 5000,
280
- },
301
+ { type: "command", command: `bash ${relScripts}/credential-guard.sh`, timeout: 3000 },
302
+ { type: "command", command: `bash ${relScripts}/recall-check.sh`, timeout: 5000 },
281
303
  ],
282
304
  },
283
305
  {
284
306
  matcher: "Read",
285
307
  hooks: [
286
- {
287
- type: "command",
288
- command: `bash ${relScripts}/credential-guard.sh`,
289
- timeout: 3000,
290
- },
308
+ { type: "command", command: `bash ${relScripts}/credential-guard.sh`, timeout: 3000 },
291
309
  ],
292
310
  },
293
311
  {
294
312
  matcher: "Write",
295
313
  hooks: [
296
- {
297
- type: "command",
298
- command: `bash ${relScripts}/recall-check.sh`,
299
- timeout: 5000,
300
- },
314
+ { type: "command", command: `bash ${relScripts}/recall-check.sh`, timeout: 5000 },
301
315
  ],
302
316
  },
303
317
  {
304
318
  matcher: "Edit",
305
319
  hooks: [
306
- {
307
- type: "command",
308
- command: `bash ${relScripts}/recall-check.sh`,
309
- timeout: 5000,
310
- },
320
+ { type: "command", command: `bash ${relScripts}/recall-check.sh`, timeout: 5000 },
311
321
  ],
312
322
  },
313
323
  ],
314
324
  PostToolUse: [
315
- {
316
- matcher: "mcp__gitmem__recall",
317
- hooks: [
318
- {
319
- type: "command",
320
- command: `bash ${relScripts}/post-tool-use.sh`,
321
- timeout: 3000,
322
- },
323
- ],
324
- },
325
- {
326
- matcher: "mcp__gitmem__search",
327
- hooks: [
328
- {
329
- type: "command",
330
- command: `bash ${relScripts}/post-tool-use.sh`,
331
- timeout: 3000,
332
- },
333
- ],
334
- },
335
- {
336
- matcher: "Bash",
337
- hooks: [
338
- {
339
- type: "command",
340
- command: `bash ${relScripts}/post-tool-use.sh`,
341
- timeout: 3000,
342
- },
343
- ],
344
- },
345
- {
346
- matcher: "Write",
347
- hooks: [
348
- {
349
- type: "command",
350
- command: `bash ${relScripts}/post-tool-use.sh`,
351
- timeout: 3000,
352
- },
353
- ],
354
- },
355
- {
356
- matcher: "Edit",
357
- hooks: [
358
- {
359
- type: "command",
360
- command: `bash ${relScripts}/post-tool-use.sh`,
361
- timeout: 3000,
362
- },
363
- ],
364
- },
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 }] },
365
330
  ],
366
331
  Stop: [
367
332
  {
368
333
  hooks: [
369
- {
370
- type: "command",
371
- command: `bash ${relScripts}/session-close-check.sh`,
372
- timeout: 5000,
373
- },
334
+ { type: "command", command: `bash ${relScripts}/session-close-check.sh`, timeout: 5000 },
374
335
  ],
375
336
  },
376
337
  ],
@@ -379,49 +340,21 @@ function buildClaudeHooks() {
379
340
 
380
341
  function buildCursorHooks() {
381
342
  const relScripts = ".gitmem/hooks";
382
- // Cursor hooks format: .cursor/hooks.json
383
- // Events: sessionStart, beforeMCPExecution, afterMCPExecution, stop
384
- // No per-tool matchers — all MCP calls go through beforeMCPExecution
385
343
  return {
386
- sessionStart: [
387
- {
388
- command: `bash ${relScripts}/session-start.sh`,
389
- timeout: 5000,
390
- },
391
- ],
344
+ sessionStart: [{ command: `bash ${relScripts}/session-start.sh`, timeout: 5000 }],
392
345
  beforeMCPExecution: [
393
- {
394
- command: `bash ${relScripts}/credential-guard.sh`,
395
- timeout: 3000,
396
- },
397
- {
398
- command: `bash ${relScripts}/recall-check.sh`,
399
- timeout: 5000,
400
- },
401
- ],
402
- afterMCPExecution: [
403
- {
404
- command: `bash ${relScripts}/post-tool-use.sh`,
405
- timeout: 3000,
406
- },
407
- ],
408
- stop: [
409
- {
410
- command: `bash ${relScripts}/session-close-check.sh`,
411
- timeout: 5000,
412
- },
346
+ { command: `bash ${relScripts}/credential-guard.sh`, timeout: 3000 },
347
+ { command: `bash ${relScripts}/recall-check.sh`, timeout: 5000 },
413
348
  ],
349
+ afterMCPExecution: [{ command: `bash ${relScripts}/post-tool-use.sh`, timeout: 3000 }],
350
+ stop: [{ command: `bash ${relScripts}/session-close-check.sh`, timeout: 5000 }],
414
351
  };
415
352
  }
416
353
 
417
354
  function isGitmemHook(entry) {
418
- // Claude Code format: entry.hooks is an array of {command: "..."}
419
355
  if (entry.hooks && Array.isArray(entry.hooks)) {
420
- return entry.hooks.some(
421
- (h) => typeof h.command === "string" && h.command.includes("gitmem")
422
- );
356
+ return entry.hooks.some((h) => typeof h.command === "string" && h.command.includes("gitmem"));
423
357
  }
424
- // Cursor format: entry itself has {command: "..."}
425
358
  if (typeof entry.command === "string") {
426
359
  return entry.command.includes("gitmem");
427
360
  }
@@ -436,7 +369,29 @@ function getInstructionsTemplate() {
436
369
  }
437
370
  }
438
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
+
439
393
  // ── Steps ──
394
+ // Each returns { done: bool } so main can track progress
440
395
 
441
396
  async function stepMemoryStore() {
442
397
  const learningsPath = join(gitmemDir, "learnings.json");
@@ -452,29 +407,29 @@ async function stepMemoryStore() {
452
407
  try {
453
408
  starterScars = JSON.parse(readFileSync(starterScarsPath, "utf-8"));
454
409
  } catch {
455
- console.log(" ! Could not read starter-scars.json. Skipping.");
456
- return;
410
+ log(WARN, "Could not read starter lessons. Skipping.");
411
+ return { done: false };
457
412
  }
458
413
 
459
414
  if (exists && existingCount >= starterScars.length) {
460
- console.log(
461
- ` Already configured (${existingCount} scars in .gitmem/). Skipping.`
462
- );
463
- return;
464
- }
465
-
466
- const prompt = exists
467
- ? `Merge ${starterScars.length} starter scars into .gitmem/? (${existingCount} existing)`
468
- : `Create .gitmem/ with ${starterScars.length} starter scars?`;
469
-
470
- if (!(await confirm(prompt))) {
471
- console.log(" Skipped.");
472
- 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
+ }
473
428
  }
474
429
 
475
430
  if (dryRun) {
476
- console.log(` [dry-run] Would create .gitmem/ with ${starterScars.length} starter scars`);
477
- return;
431
+ log(CHECK, `Would create .gitmem/ with ${starterScars.length} starter lessons`, "[dry-run]");
432
+ return { done: true };
478
433
  }
479
434
 
480
435
  if (!existsSync(gitmemDir)) {
@@ -509,6 +464,19 @@ async function stepMemoryStore() {
509
464
  }
510
465
  writeJson(learningsPath, existing);
511
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
+
512
480
  // Empty collection files
513
481
  for (const file of ["sessions.json", "decisions.json", "scar-usage.json"]) {
514
482
  const filePath = join(gitmemDir, file);
@@ -517,20 +485,14 @@ async function stepMemoryStore() {
517
485
  }
518
486
  }
519
487
 
520
- // Closing payload template — agents read this before writing closing-payload.json
488
+ // Closing payload template
521
489
  const templatePath = join(gitmemDir, "closing-payload-template.json");
522
490
  if (!existsSync(templatePath)) {
523
491
  writeJson(templatePath, {
524
492
  closing_reflection: {
525
- what_broke: "",
526
- what_took_longer: "",
527
- do_differently: "",
528
- what_worked: "",
529
- wrong_assumption: "",
530
- scars_applied: [],
531
- institutional_memory_items: "",
532
- collaborative_dynamic: "",
533
- 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: ""
534
496
  },
535
497
  task_completion: {
536
498
  questions_displayed_at: "ISO-8601 timestamp",
@@ -539,56 +501,52 @@ async function stepMemoryStore() {
539
501
  human_response_at: "ISO-8601 timestamp",
540
502
  human_response: "no corrections | actual corrections text"
541
503
  },
542
- human_corrections: "",
543
- scars_to_record: [],
544
- learnings_created: [],
545
- open_threads: [],
546
- decisions: []
504
+ human_corrections: "", scars_to_record: [],
505
+ learnings_created: [], open_threads: [], decisions: []
547
506
  });
548
507
  }
549
508
 
550
- console.log(
551
- ` Created .gitmem/ with ${starterScars.length} starter scars` +
552
- (added < starterScars.length
553
- ? ` (${added} new, ${starterScars.length - added} already existed)`
554
- : "")
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}`
555
516
  );
517
+ return { done: true };
556
518
  }
557
519
 
558
520
  async function stepMcpServer() {
559
521
  const mcpPath = cc.mcpConfigPath;
560
522
  const mcpName = cc.mcpConfigName;
561
- const isUserLevel = cc.mcpConfigScope === "user";
562
523
 
563
524
  const existing = readJson(mcpPath);
564
- const hasGitmem =
565
- existing?.mcpServers?.gitmem || existing?.mcpServers?.["gitmem-mcp"];
525
+ const hasGitmem = existing?.mcpServers?.gitmem || existing?.mcpServers?.["gitmem-mcp"];
566
526
 
567
527
  if (hasGitmem) {
568
- console.log(` Already configured in ${mcpName}. Skipping.`);
569
- return;
528
+ log(CHECK, `MCP server already configured ${C.dim}(${mcpName})${C.reset}`);
529
+ return { done: false };
570
530
  }
571
531
 
572
- const serverCount = existing?.mcpServers
573
- ? Object.keys(existing.mcpServers).length
574
- : 0;
575
- const tierLabel = process.env.SUPABASE_URL ? "pro" : "free";
576
- const scopeNote = isUserLevel ? " (user-level config)" : "";
577
- const prompt = existing
578
- ? `Add gitmem to ${mcpName}?${scopeNote} (${serverCount} existing server${serverCount !== 1 ? "s" : ""} preserved)`
579
- : `Create ${mcpName} with gitmem server?${scopeNote}`;
580
-
581
- if (!(await confirm(prompt))) {
582
- console.log(" Skipped.");
583
- 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
+ }
584
543
  }
585
544
 
586
545
  if (dryRun) {
587
- console.log(` [dry-run] Would add gitmem entry to ${mcpName} (${tierLabel} tier${scopeNote})`);
588
- return;
546
+ log(CHECK, `Would configure MCP server in ${mcpName}`, "[dry-run]");
547
+ return { done: true };
589
548
  }
590
549
 
591
- // Ensure parent directory exists (for .cursor/mcp.json, .vscode/mcp.json, ~/.codeium/windsurf/)
592
550
  const parentDir = dirname(mcpPath);
593
551
  if (!existsSync(parentDir)) {
594
552
  mkdirSync(parentDir, { recursive: true });
@@ -599,12 +557,12 @@ async function stepMcpServer() {
599
557
  config.mcpServers.gitmem = buildMcpConfig();
600
558
  writeJson(mcpPath, config);
601
559
 
602
- console.log(
603
- ` Added gitmem entry to ${mcpName} (${tierLabel} tier` +
604
- (process.env.SUPABASE_URL ? " \u2014 Supabase detected" : " \u2014 local storage") +
605
- ")" +
606
- (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`
607
564
  );
565
+ return { done: true };
608
566
  }
609
567
 
610
568
  async function stepInstructions() {
@@ -612,8 +570,8 @@ async function stepInstructions() {
612
570
  const instrName = cc.instructionsName;
613
571
 
614
572
  if (!template) {
615
- console.log(` ! ${instrName} template not found. Skipping.`);
616
- return;
573
+ log(WARN, `${instrName} template not found. Skipping.`);
574
+ return { done: false };
617
575
  }
618
576
 
619
577
  const instrPath = cc.instructionsFile;
@@ -621,33 +579,31 @@ async function stepInstructions() {
621
579
  let content = exists ? readFileSync(instrPath, "utf-8") : "";
622
580
 
623
581
  if (content.includes(cc.startMarker)) {
624
- console.log(` Already configured in ${instrName}. Skipping.`);
625
- return;
626
- }
627
-
628
- const prompt = exists
629
- ? `Append gitmem section to ${instrName}?`
630
- : `Create ${instrName} with gitmem instructions?`;
631
-
632
- if (!(await confirm(prompt))) {
633
- console.log(" Skipped.");
634
- 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
+ }
635
595
  }
636
596
 
637
597
  if (dryRun) {
638
- console.log(
639
- ` [dry-run] Would ${exists ? "append gitmem section to" : "create"} ${instrName}`
640
- );
641
- return;
598
+ log(CHECK, `Would ${exists ? "update" : "create"} ${instrName}`, "[dry-run]");
599
+ return { done: true };
642
600
  }
643
601
 
644
- // Template should already have delimiters, but ensure they're there
645
602
  let block = template;
646
603
  if (!block.includes(cc.startMarker)) {
647
604
  block = `${cc.startMarker}\n${block}\n${cc.endMarker}`;
648
605
  }
649
606
 
650
- // Ensure parent directory exists (for .github/copilot-instructions.md)
651
607
  const instrParentDir = dirname(instrPath);
652
608
  if (!existsSync(instrParentDir)) {
653
609
  mkdirSync(instrParentDir, { recursive: true });
@@ -660,35 +616,38 @@ async function stepInstructions() {
660
616
  }
661
617
 
662
618
  writeFileSync(instrPath, content);
663
- console.log(
664
- ` ${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"
665
625
  );
626
+ return { done: true };
666
627
  }
667
628
 
668
629
  async function stepPermissions() {
669
- // Cursor doesn't have an equivalent permissions system
670
- if (!cc.hasPermissions) {
671
- console.log(` Not needed for ${cc.name}. Skipping.`);
672
- return;
673
- }
630
+ if (!cc.hasPermissions) return { done: false };
674
631
 
675
632
  const existing = readJson(cc.settingsFile);
676
633
  const allow = existing?.permissions?.allow || [];
677
634
  const pattern = "mcp__gitmem__*";
678
635
 
679
636
  if (allow.includes(pattern)) {
680
- console.log(` Already configured in ${cc.configDir}/settings.json. Skipping.`);
681
- return;
637
+ log(CHECK, `Tool permissions already configured`);
638
+ return { done: false };
682
639
  }
683
640
 
684
- if (!(await confirm(`Add mcp__gitmem__* to ${cc.configDir}/settings.json?`))) {
685
- console.log(" Skipped.");
686
- 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
+ }
687
646
  }
688
647
 
689
648
  if (dryRun) {
690
- console.log(" [dry-run] Would add gitmem tool permissions");
691
- return;
649
+ log(CHECK, "Would auto-approve gitmem tools", "[dry-run]");
650
+ return { done: true };
692
651
  }
693
652
 
694
653
  const settings = existing || {};
@@ -701,35 +660,16 @@ async function stepPermissions() {
701
660
  settings.permissions = { ...permissions, allow: newAllow };
702
661
  writeJson(cc.settingsFile, settings);
703
662
 
704
- console.log(" Added gitmem tool permissions");
705
- }
706
-
707
- function copyHookScripts() {
708
- const destHooksDir = join(gitmemDir, "hooks");
709
- if (!existsSync(destHooksDir)) {
710
- mkdirSync(destHooksDir, { recursive: true });
711
- }
712
- if (existsSync(hooksScriptsDir)) {
713
- try {
714
- for (const file of readdirSync(hooksScriptsDir)) {
715
- if (file.endsWith(".sh")) {
716
- const src = join(hooksScriptsDir, file);
717
- const dest = join(destHooksDir, file);
718
- writeFileSync(dest, readFileSync(src));
719
- chmodSync(dest, 0o755);
720
- }
721
- }
722
- } catch {
723
- // Non-critical
724
- }
725
- }
663
+ log(CHECK,
664
+ "Auto-approved gitmem tools",
665
+ "Memory tools run without interrupting you"
666
+ );
667
+ return { done: true };
726
668
  }
727
669
 
728
670
  async function stepHooks() {
729
671
  if (!cc.hasHooks) {
730
- console.log(` ${cc.name} does not support lifecycle hooks. Skipping.`);
731
- console.log(" Enforcement relies on system prompt instructions instead.");
732
- return;
672
+ return { done: false };
733
673
  }
734
674
  if (cc.hooksInSettings) {
735
675
  return stepHooksClaude();
@@ -743,11 +683,10 @@ async function stepHooksClaude() {
743
683
  const hasGitmem = JSON.stringify(hooks).includes("gitmem");
744
684
 
745
685
  if (hasGitmem) {
746
- console.log(" Already configured in .claude/settings.json. Skipping.");
747
- return;
686
+ log(CHECK, `Automatic memory hooks already configured`);
687
+ return { done: false };
748
688
  }
749
689
 
750
- // Count existing non-gitmem hooks
751
690
  let existingHookCount = 0;
752
691
  for (const entries of Object.values(hooks)) {
753
692
  if (Array.isArray(entries)) {
@@ -755,19 +694,20 @@ async function stepHooksClaude() {
755
694
  }
756
695
  }
757
696
 
758
- const prompt =
759
- existingHookCount > 0
760
- ? `Merge gitmem hooks into .claude/settings.json? (${existingHookCount} existing hook${existingHookCount !== 1 ? "s" : ""} preserved)`
761
- : "Add gitmem lifecycle hooks to .claude/settings.json?";
762
-
763
- if (!(await confirm(prompt))) {
764
- console.log(" Skipped.");
765
- 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
+ }
766
706
  }
767
707
 
768
708
  if (dryRun) {
769
- console.log(" [dry-run] Would merge 4 gitmem hook types");
770
- return;
709
+ log(CHECK, "Would add automatic memory hooks", "[dry-run]");
710
+ return { done: true };
771
711
  }
772
712
 
773
713
  copyHookScripts();
@@ -789,25 +729,23 @@ async function stepHooksClaude() {
789
729
  settings.hooks = merged;
790
730
  writeJson(cc.settingsFile, settings);
791
731
 
792
- const preservedMsg =
793
- existingHookCount > 0
794
- ? ` (${existingHookCount} existing hook${existingHookCount !== 1 ? "s" : ""} preserved)`
795
- : "";
796
- 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
+ );
797
740
 
798
- // Warn about settings.local.json
799
741
  if (cc.settingsLocalFile && existsSync(cc.settingsLocalFile)) {
800
742
  const local = readJson(cc.settingsLocalFile);
801
743
  if (local?.hooks) {
802
- console.log("");
803
- console.log(
804
- " Note: .claude/settings.local.json also has hooks."
805
- );
806
- console.log(
807
- " Local hooks take precedence. You may need to manually merge."
808
- );
744
+ console.log(` ${C.yellow}Note:${C.reset} ${C.dim}.claude/settings.local.json also has hooks \u2014 may need manual merge${C.reset}`);
809
745
  }
810
746
  }
747
+
748
+ return { done: true };
811
749
  }
812
750
 
813
751
  async function stepHooksCursor() {
@@ -818,11 +756,10 @@ async function stepHooksCursor() {
818
756
  const hasGitmem = existing ? JSON.stringify(existing).includes("gitmem") : false;
819
757
 
820
758
  if (hasGitmem) {
821
- console.log(` Already configured in ${hooksName}. Skipping.`);
822
- return;
759
+ log(CHECK, `Automatic memory hooks already configured ${C.dim}(${hooksName})${C.reset}`);
760
+ return { done: false };
823
761
  }
824
762
 
825
- // Count existing non-gitmem hooks
826
763
  let existingHookCount = 0;
827
764
  if (existing?.hooks) {
828
765
  for (const entries of Object.values(existing.hooks)) {
@@ -832,19 +769,19 @@ async function stepHooksCursor() {
832
769
  }
833
770
  }
834
771
 
835
- const prompt =
836
- existingHookCount > 0
837
- ? `Merge gitmem hooks into ${hooksName}? (${existingHookCount} existing hook${existingHookCount !== 1 ? "s" : ""} preserved)`
838
- : `Add gitmem lifecycle hooks to ${hooksName}?`;
839
-
840
- if (!(await confirm(prompt))) {
841
- console.log(" Skipped.");
842
- 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
+ }
843
780
  }
844
781
 
845
782
  if (dryRun) {
846
- console.log(" [dry-run] Would merge 4 gitmem hook types");
847
- return;
783
+ log(CHECK, "Would add automatic memory hooks", "[dry-run]");
784
+ return { done: true };
848
785
  }
849
786
 
850
787
  copyHookScripts();
@@ -866,11 +803,15 @@ async function stepHooksCursor() {
866
803
  config.hooks = merged;
867
804
  writeJson(hooksPath, config);
868
805
 
869
- const preservedMsg =
870
- existingHookCount > 0
871
- ? ` (${existingHookCount} existing hook${existingHookCount !== 1 ? "s" : ""} preserved)`
872
- : "";
873
- 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 };
874
815
  }
875
816
 
876
817
  async function stepGitignore() {
@@ -878,18 +819,20 @@ async function stepGitignore() {
878
819
  let content = exists ? readFileSync(gitignorePath, "utf-8") : "";
879
820
 
880
821
  if (content.includes(".gitmem/")) {
881
- console.log(" Already configured in .gitignore. Skipping.");
882
- return;
822
+ log(CHECK, `.gitignore already configured`);
823
+ return { done: false };
883
824
  }
884
825
 
885
- if (!(await confirm("Add .gitmem/ to .gitignore?"))) {
886
- console.log(" Skipped.");
887
- return;
826
+ if (interactive) {
827
+ if (!(await confirm("Add .gitmem/ to .gitignore?"))) {
828
+ log(SKIP, "Gitignore skipped");
829
+ return { done: false };
830
+ }
888
831
  }
889
832
 
890
833
  if (dryRun) {
891
- console.log(" [dry-run] Would add .gitmem/ to .gitignore");
892
- return;
834
+ log(CHECK, "Would update .gitignore", "[dry-run]");
835
+ return { done: true };
893
836
  }
894
837
 
895
838
  if (exists) {
@@ -899,7 +842,11 @@ async function stepGitignore() {
899
842
  }
900
843
  writeFileSync(gitignorePath, content);
901
844
 
902
- 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 };
903
850
  }
904
851
 
905
852
  // ── Main ──
@@ -908,126 +855,70 @@ async function main() {
908
855
  const pkg = readJson(join(__dirname, "..", "package.json"));
909
856
  const version = pkg?.version || "1.0.0";
910
857
 
911
- // Detect client before anything else
912
858
  client = detectClient();
913
859
  cc = CLIENT_CONFIGS[client];
914
860
 
861
+ // ── Header — matches gitmem MCP product line format ──
915
862
  console.log("");
916
- console.log(` gitmem v${version} \u2014 Setup for ${cc.name}`);
917
- if (dryRun) {
918
- console.log(" (dry-run mode \u2014 no files will be written)");
919
- }
920
- if (clientFlag) {
921
- console.log(` (client: ${client} \u2014 via --client flag)`);
922
- } else {
923
- console.log(` (client: ${client} \u2014 auto-detected)`);
924
- }
925
- console.log("");
926
-
927
- // Detect environment
928
- console.log(" Detecting environment...");
929
- 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}`);
930
865
 
931
- if (existsSync(cc.mcpConfigPath)) {
932
- const mcp = readJson(cc.mcpConfigPath);
933
- const count = mcp?.mcpServers ? Object.keys(mcp.mcpServers).length : 0;
934
- detections.push(
935
- ` ${cc.mcpConfigName} found (${count} server${count !== 1 ? "s" : ""})`
936
- );
937
- }
938
-
939
- if (existsSync(cc.instructionsFile)) {
940
- const content = readFileSync(cc.instructionsFile, "utf-8");
941
- const hasGitmem = content.includes(cc.startMarker);
942
- detections.push(
943
- ` ${cc.instructionsName} found (${hasGitmem ? "has gitmem section" : "no gitmem section"})`
944
- );
945
- }
946
-
947
- if (cc.settingsFile && existsSync(cc.settingsFile)) {
948
- const settings = readJson(cc.settingsFile);
949
- const hookCount = settings?.hooks
950
- ? Object.values(settings.hooks).flat().length
951
- : 0;
952
- detections.push(
953
- ` .claude/settings.json found (${hookCount} hook${hookCount !== 1 ? "s" : ""})`
954
- );
955
- }
956
-
957
- if (!cc.hooksInSettings && cc.hasHooks && cc.hooksFile && existsSync(cc.hooksFile)) {
958
- const hooks = readJson(cc.hooksFile);
959
- const hookCount = hooks?.hooks
960
- ? Object.values(hooks.hooks).flat().length
961
- : 0;
962
- detections.push(
963
- ` ${cc.hooksFileName} found (${hookCount} hook${hookCount !== 1 ? "s" : ""})`
964
- );
965
- }
966
-
967
- if (existsSync(gitignorePath)) {
968
- detections.push(" .gitignore found");
866
+ if (dryRun) {
867
+ console.log(`${C.dim}dry-run mode \u2014 no files will be written${C.reset}`);
969
868
  }
970
869
 
971
- if (existsSync(gitmemDir)) {
972
- detections.push(" .gitmem/ found");
973
- }
870
+ console.log("");
974
871
 
975
- for (const d of detections) {
976
- console.log(d);
977
- }
872
+ // ── Run steps ──
978
873
 
979
- const tier = process.env.SUPABASE_URL ? "pro" : "free";
980
- console.log(
981
- ` Tier: ${tier}` +
982
- (tier === "free" ? " (no SUPABASE_URL detected)" : " (SUPABASE_URL detected)")
983
- );
984
- console.log("");
874
+ let configured = 0;
985
875
 
986
- // Run steps step count depends on client capabilities
987
- let stepCount = 4; // memory store + mcp server + instructions + gitignore
988
- if (cc.hasPermissions) stepCount++;
989
- if (cc.hasHooks) stepCount++;
990
- let step = 1;
876
+ const d = _color && !dryRun ? 500 : 0;
991
877
 
992
- console.log(` Step ${step}/${stepCount} \u2014 Memory Store`);
993
- await stepMemoryStore();
994
- console.log("");
995
- step++;
878
+ const r1 = await stepMemoryStore();
879
+ if (r1.done) configured++;
880
+ if (d) await sleep(d);
996
881
 
997
- console.log(` Step ${step}/${stepCount} \u2014 MCP Server`);
998
- await stepMcpServer();
999
- console.log("");
1000
- step++;
882
+ const r2 = await stepMcpServer();
883
+ if (r2.done) configured++;
884
+ if (d) await sleep(d);
1001
885
 
1002
- console.log(` Step ${step}/${stepCount} \u2014 Project Instructions`);
1003
- await stepInstructions();
1004
- console.log("");
1005
- step++;
886
+ const r3 = await stepInstructions();
887
+ if (r3.done) configured++;
888
+ if (d) await sleep(d);
1006
889
 
1007
890
  if (cc.hasPermissions) {
1008
- console.log(` Step ${step}/${stepCount} \u2014 Tool Permissions`);
1009
- await stepPermissions();
1010
- console.log("");
1011
- step++;
891
+ const r4 = await stepPermissions();
892
+ if (r4.done) configured++;
893
+ if (d) await sleep(d);
1012
894
  }
1013
895
 
1014
896
  if (cc.hasHooks) {
1015
- console.log(` Step ${step}/${stepCount} \u2014 Lifecycle Hooks`);
1016
- await stepHooks();
1017
- console.log("");
1018
- step++;
897
+ const r5 = await stepHooks();
898
+ if (r5.done) configured++;
899
+ if (d) await sleep(d);
1019
900
  }
1020
901
 
1021
- console.log(` Step ${step}/${stepCount} \u2014 Gitignore`);
1022
- await stepGitignore();
1023
- console.log("");
902
+ const r6 = await stepGitignore();
903
+ if (r6.done) configured++;
1024
904
 
905
+ // ── Footer ──
1025
906
  if (dryRun) {
1026
- 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}`);
1027
911
  } else {
1028
- console.log(` ${cc.completionMsg}`);
1029
- 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("");
918
+ console.log(`${C.dim}Try asking your agent:${C.reset}`);
919
+ console.log(` ${C.italic}"Review the gitmem tools, test them, convince yourself"${C.reset}`);
1030
920
  }
921
+
1031
922
  console.log("");
1032
923
 
1033
924
  if (rl) rl.close();