gitmem-mcp 1.0.14 → 1.1.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.
- package/CHANGELOG.md +22 -0
- package/README.md +2 -2
- package/bin/gitmem.js +264 -123
- package/bin/init-wizard.js +332 -93
- package/bin/uninstall.js +187 -45
- package/cursorrules.template +92 -0
- package/dist/commands/check.js +8 -1
- package/dist/schemas/absorb-observations.js +3 -3
- package/dist/schemas/create-decision.js +8 -8
- package/dist/schemas/create-learning.js +13 -13
- package/dist/schemas/prepare-context.js +2 -2
- package/dist/schemas/thread.js +10 -10
- package/dist/server.js +1 -1
- package/dist/services/behavioral-decay.js +3 -1
- package/dist/services/embedding.js +2 -0
- package/dist/services/local-file-storage.js +3 -1
- package/dist/services/thread-dedup.d.ts +27 -7
- package/dist/services/thread-dedup.js +112 -10
- package/dist/services/thread-manager.js +5 -2
- package/dist/tools/confirm-scars.js +6 -3
- package/dist/tools/create-thread.d.ts +1 -1
- package/dist/tools/recall.js +3 -1
- package/hooks/scripts/recall-check.sh +51 -52
- package/package.json +2 -1
package/bin/init-wizard.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* GitMem Init Wizard
|
|
5
5
|
*
|
|
6
6
|
* Interactive setup that detects existing config, prompts, and merges.
|
|
7
|
-
*
|
|
7
|
+
* Supports Claude Code and Cursor IDE.
|
|
8
|
+
*
|
|
9
|
+
* Usage: npx gitmem-mcp init [--yes] [--dry-run] [--project <name>] [--client <claude|cursor>]
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
import {
|
|
@@ -28,20 +30,93 @@ const autoYes = args.includes("--yes") || args.includes("-y");
|
|
|
28
30
|
const dryRun = args.includes("--dry-run");
|
|
29
31
|
const projectIdx = args.indexOf("--project");
|
|
30
32
|
const projectName = projectIdx !== -1 ? args[projectIdx + 1] : null;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
const clientIdx = args.indexOf("--client");
|
|
34
|
+
const clientFlag = clientIdx !== -1 ? args[clientIdx + 1]?.toLowerCase() : null;
|
|
35
|
+
|
|
36
|
+
// ── Client Configuration ──
|
|
37
|
+
|
|
38
|
+
const CLIENT_CONFIGS = {
|
|
39
|
+
claude: {
|
|
40
|
+
name: "Claude Code",
|
|
41
|
+
mcpConfigPath: join(cwd, ".mcp.json"),
|
|
42
|
+
mcpConfigName: ".mcp.json",
|
|
43
|
+
instructionsFile: join(cwd, "CLAUDE.md"),
|
|
44
|
+
instructionsName: "CLAUDE.md",
|
|
45
|
+
templateFile: join(__dirname, "..", "CLAUDE.md.template"),
|
|
46
|
+
startMarker: "<!-- gitmem:start -->",
|
|
47
|
+
endMarker: "<!-- gitmem:end -->",
|
|
48
|
+
configDir: join(cwd, ".claude"),
|
|
49
|
+
settingsFile: join(cwd, ".claude", "settings.json"),
|
|
50
|
+
settingsLocalFile: join(cwd, ".claude", "settings.local.json"),
|
|
51
|
+
hasPermissions: true,
|
|
52
|
+
hooksInSettings: true,
|
|
53
|
+
completionMsg: "Setup complete! Start Claude Code \u2014 memory is active.",
|
|
54
|
+
},
|
|
55
|
+
cursor: {
|
|
56
|
+
name: "Cursor",
|
|
57
|
+
mcpConfigPath: join(cwd, ".cursor", "mcp.json"),
|
|
58
|
+
mcpConfigName: ".cursor/mcp.json",
|
|
59
|
+
instructionsFile: join(cwd, ".cursorrules"),
|
|
60
|
+
instructionsName: ".cursorrules",
|
|
61
|
+
templateFile: join(__dirname, "..", "cursorrules.template"),
|
|
62
|
+
startMarker: "# --- gitmem:start ---",
|
|
63
|
+
endMarker: "# --- gitmem:end ---",
|
|
64
|
+
configDir: join(cwd, ".cursor"),
|
|
65
|
+
settingsFile: null,
|
|
66
|
+
settingsLocalFile: null,
|
|
67
|
+
hasPermissions: false,
|
|
68
|
+
hooksInSettings: false,
|
|
69
|
+
hooksFile: join(cwd, ".cursor", "hooks.json"),
|
|
70
|
+
hooksFileName: ".cursor/hooks.json",
|
|
71
|
+
completionMsg: "Setup complete! Open Cursor (Agent mode) \u2014 memory is active.",
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Shared paths (client-agnostic)
|
|
33
76
|
const gitmemDir = join(cwd, ".gitmem");
|
|
34
|
-
const mcpJsonPath = join(cwd, ".mcp.json");
|
|
35
|
-
const claudeMdPath = join(cwd, "CLAUDE.md");
|
|
36
|
-
const claudeDir = join(cwd, ".claude");
|
|
37
|
-
const settingsPath = join(claudeDir, "settings.json");
|
|
38
|
-
const settingsLocalPath = join(claudeDir, "settings.local.json");
|
|
39
77
|
const gitignorePath = join(cwd, ".gitignore");
|
|
40
|
-
const templatePath = join(__dirname, "..", "CLAUDE.md.template");
|
|
41
78
|
const starterScarsPath = join(__dirname, "..", "schema", "starter-scars.json");
|
|
42
79
|
const hooksScriptsDir = join(__dirname, "..", "hooks", "scripts");
|
|
43
80
|
|
|
44
81
|
let rl;
|
|
82
|
+
let client; // "claude" | "cursor" — set by detectClient()
|
|
83
|
+
let cc; // shorthand for CLIENT_CONFIGS[client]
|
|
84
|
+
|
|
85
|
+
// ── Client Detection ──
|
|
86
|
+
|
|
87
|
+
function detectClient() {
|
|
88
|
+
// Explicit flag takes priority
|
|
89
|
+
if (clientFlag) {
|
|
90
|
+
if (clientFlag !== "claude" && clientFlag !== "cursor") {
|
|
91
|
+
console.error(` Error: Unknown client "${clientFlag}". Use --client claude or --client cursor.`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
return clientFlag;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Auto-detect based on directory presence
|
|
98
|
+
const hasCursorDir = existsSync(join(cwd, ".cursor"));
|
|
99
|
+
const hasClaudeDir = existsSync(join(cwd, ".claude"));
|
|
100
|
+
const hasMcpJson = existsSync(join(cwd, ".mcp.json"));
|
|
101
|
+
const hasClaudeMd = existsSync(join(cwd, "CLAUDE.md"));
|
|
102
|
+
const hasCursorRules = existsSync(join(cwd, ".cursorrules"));
|
|
103
|
+
const hasCursorMcp = existsSync(join(cwd, ".cursor", "mcp.json"));
|
|
104
|
+
|
|
105
|
+
// Strong Cursor signals
|
|
106
|
+
if (hasCursorDir && !hasClaudeDir && !hasMcpJson && !hasClaudeMd) return "cursor";
|
|
107
|
+
if (hasCursorRules && !hasClaudeMd) return "cursor";
|
|
108
|
+
if (hasCursorMcp && !hasMcpJson) return "cursor";
|
|
109
|
+
|
|
110
|
+
// Strong Claude signals
|
|
111
|
+
if (hasClaudeDir && !hasCursorDir) return "claude";
|
|
112
|
+
if (hasMcpJson && !hasCursorMcp) return "claude";
|
|
113
|
+
if (hasClaudeMd && !hasCursorRules) return "claude";
|
|
114
|
+
|
|
115
|
+
// Default to Claude Code (most common)
|
|
116
|
+
return "claude";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Helpers ──
|
|
45
120
|
|
|
46
121
|
async function confirm(message, defaultYes = true) {
|
|
47
122
|
if (autoYes) return true;
|
|
@@ -85,7 +160,7 @@ function buildMcpConfig() {
|
|
|
85
160
|
};
|
|
86
161
|
}
|
|
87
162
|
|
|
88
|
-
function
|
|
163
|
+
function buildClaudeHooks() {
|
|
89
164
|
const relScripts = ".gitmem/hooks";
|
|
90
165
|
return {
|
|
91
166
|
SessionStart: [
|
|
@@ -213,16 +288,60 @@ function buildHooks() {
|
|
|
213
288
|
};
|
|
214
289
|
}
|
|
215
290
|
|
|
291
|
+
function buildCursorHooks() {
|
|
292
|
+
const relScripts = ".gitmem/hooks";
|
|
293
|
+
// Cursor hooks format: .cursor/hooks.json
|
|
294
|
+
// Events: sessionStart, beforeMCPExecution, afterMCPExecution, stop
|
|
295
|
+
// No per-tool matchers — all MCP calls go through beforeMCPExecution
|
|
296
|
+
return {
|
|
297
|
+
sessionStart: [
|
|
298
|
+
{
|
|
299
|
+
command: `bash ${relScripts}/session-start.sh`,
|
|
300
|
+
timeout: 5000,
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
beforeMCPExecution: [
|
|
304
|
+
{
|
|
305
|
+
command: `bash ${relScripts}/credential-guard.sh`,
|
|
306
|
+
timeout: 3000,
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
command: `bash ${relScripts}/recall-check.sh`,
|
|
310
|
+
timeout: 5000,
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
afterMCPExecution: [
|
|
314
|
+
{
|
|
315
|
+
command: `bash ${relScripts}/post-tool-use.sh`,
|
|
316
|
+
timeout: 3000,
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
stop: [
|
|
320
|
+
{
|
|
321
|
+
command: `bash ${relScripts}/session-close-check.sh`,
|
|
322
|
+
timeout: 5000,
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
216
328
|
function isGitmemHook(entry) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
329
|
+
// Claude Code format: entry.hooks is an array of {command: "..."}
|
|
330
|
+
if (entry.hooks && Array.isArray(entry.hooks)) {
|
|
331
|
+
return entry.hooks.some(
|
|
332
|
+
(h) => typeof h.command === "string" && h.command.includes("gitmem")
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
// Cursor format: entry itself has {command: "..."}
|
|
336
|
+
if (typeof entry.command === "string") {
|
|
337
|
+
return entry.command.includes("gitmem");
|
|
338
|
+
}
|
|
339
|
+
return false;
|
|
221
340
|
}
|
|
222
341
|
|
|
223
|
-
function
|
|
342
|
+
function getInstructionsTemplate() {
|
|
224
343
|
try {
|
|
225
|
-
return readFileSync(
|
|
344
|
+
return readFileSync(cc.templateFile, "utf-8");
|
|
226
345
|
} catch {
|
|
227
346
|
return null;
|
|
228
347
|
}
|
|
@@ -318,12 +437,15 @@ async function stepMemoryStore() {
|
|
|
318
437
|
}
|
|
319
438
|
|
|
320
439
|
async function stepMcpServer() {
|
|
321
|
-
const
|
|
440
|
+
const mcpPath = cc.mcpConfigPath;
|
|
441
|
+
const mcpName = cc.mcpConfigName;
|
|
442
|
+
|
|
443
|
+
const existing = readJson(mcpPath);
|
|
322
444
|
const hasGitmem =
|
|
323
445
|
existing?.mcpServers?.gitmem || existing?.mcpServers?.["gitmem-mcp"];
|
|
324
446
|
|
|
325
447
|
if (hasGitmem) {
|
|
326
|
-
console.log(
|
|
448
|
+
console.log(` Already configured in ${mcpName}. Skipping.`);
|
|
327
449
|
return;
|
|
328
450
|
}
|
|
329
451
|
|
|
@@ -332,8 +454,8 @@ async function stepMcpServer() {
|
|
|
332
454
|
: 0;
|
|
333
455
|
const tierLabel = process.env.SUPABASE_URL ? "pro" : "free";
|
|
334
456
|
const prompt = existing
|
|
335
|
-
? `Add gitmem to
|
|
336
|
-
:
|
|
457
|
+
? `Add gitmem to ${mcpName}? (${serverCount} existing server${serverCount !== 1 ? "s" : ""} preserved)`
|
|
458
|
+
: `Create ${mcpName} with gitmem server?`;
|
|
337
459
|
|
|
338
460
|
if (!(await confirm(prompt))) {
|
|
339
461
|
console.log(" Skipped.");
|
|
@@ -341,40 +463,49 @@ async function stepMcpServer() {
|
|
|
341
463
|
}
|
|
342
464
|
|
|
343
465
|
if (dryRun) {
|
|
344
|
-
console.log(` [dry-run] Would add gitmem entry to
|
|
466
|
+
console.log(` [dry-run] Would add gitmem entry to ${mcpName} (${tierLabel} tier)`);
|
|
345
467
|
return;
|
|
346
468
|
}
|
|
347
469
|
|
|
470
|
+
// Ensure parent directory exists (for .cursor/mcp.json)
|
|
471
|
+
const parentDir = dirname(mcpPath);
|
|
472
|
+
if (!existsSync(parentDir)) {
|
|
473
|
+
mkdirSync(parentDir, { recursive: true });
|
|
474
|
+
}
|
|
475
|
+
|
|
348
476
|
const config = existing || { mcpServers: {} };
|
|
349
477
|
if (!config.mcpServers) config.mcpServers = {};
|
|
350
478
|
config.mcpServers.gitmem = buildMcpConfig();
|
|
351
|
-
writeJson(
|
|
479
|
+
writeJson(mcpPath, config);
|
|
352
480
|
|
|
353
481
|
console.log(
|
|
354
|
-
` Added gitmem entry to
|
|
355
|
-
(process.env.SUPABASE_URL ? "
|
|
482
|
+
` Added gitmem entry to ${mcpName} (${tierLabel} tier` +
|
|
483
|
+
(process.env.SUPABASE_URL ? " \u2014 Supabase detected" : " \u2014 local storage") +
|
|
356
484
|
")"
|
|
357
485
|
);
|
|
358
486
|
}
|
|
359
487
|
|
|
360
|
-
async function
|
|
361
|
-
const template =
|
|
488
|
+
async function stepInstructions() {
|
|
489
|
+
const template = getInstructionsTemplate();
|
|
490
|
+
const instrName = cc.instructionsName;
|
|
491
|
+
|
|
362
492
|
if (!template) {
|
|
363
|
-
console.log(
|
|
493
|
+
console.log(` ! ${instrName} template not found. Skipping.`);
|
|
364
494
|
return;
|
|
365
495
|
}
|
|
366
496
|
|
|
367
|
-
const
|
|
368
|
-
|
|
497
|
+
const instrPath = cc.instructionsFile;
|
|
498
|
+
const exists = existsSync(instrPath);
|
|
499
|
+
let content = exists ? readFileSync(instrPath, "utf-8") : "";
|
|
369
500
|
|
|
370
|
-
if (content.includes(
|
|
371
|
-
console.log(
|
|
501
|
+
if (content.includes(cc.startMarker)) {
|
|
502
|
+
console.log(` Already configured in ${instrName}. Skipping.`);
|
|
372
503
|
return;
|
|
373
504
|
}
|
|
374
505
|
|
|
375
506
|
const prompt = exists
|
|
376
|
-
?
|
|
377
|
-
:
|
|
507
|
+
? `Append gitmem section to ${instrName}?`
|
|
508
|
+
: `Create ${instrName} with gitmem instructions?`;
|
|
378
509
|
|
|
379
510
|
if (!(await confirm(prompt))) {
|
|
380
511
|
console.log(" Skipped.");
|
|
@@ -383,15 +514,15 @@ async function stepClaudeMd() {
|
|
|
383
514
|
|
|
384
515
|
if (dryRun) {
|
|
385
516
|
console.log(
|
|
386
|
-
` [dry-run] Would ${exists ? "append gitmem section to" : "create"}
|
|
517
|
+
` [dry-run] Would ${exists ? "append gitmem section to" : "create"} ${instrName}`
|
|
387
518
|
);
|
|
388
519
|
return;
|
|
389
520
|
}
|
|
390
521
|
|
|
391
522
|
// Template should already have delimiters, but ensure they're there
|
|
392
523
|
let block = template;
|
|
393
|
-
if (!block.includes(
|
|
394
|
-
block =
|
|
524
|
+
if (!block.includes(cc.startMarker)) {
|
|
525
|
+
block = `${cc.startMarker}\n${block}\n${cc.endMarker}`;
|
|
395
526
|
}
|
|
396
527
|
|
|
397
528
|
if (exists) {
|
|
@@ -400,23 +531,29 @@ async function stepClaudeMd() {
|
|
|
400
531
|
content = block + "\n";
|
|
401
532
|
}
|
|
402
533
|
|
|
403
|
-
writeFileSync(
|
|
534
|
+
writeFileSync(instrPath, content);
|
|
404
535
|
console.log(
|
|
405
|
-
` ${exists ? "Added gitmem section to" : "Created"}
|
|
536
|
+
` ${exists ? "Added gitmem section to" : "Created"} ${instrName}`
|
|
406
537
|
);
|
|
407
538
|
}
|
|
408
539
|
|
|
409
540
|
async function stepPermissions() {
|
|
410
|
-
|
|
541
|
+
// Cursor doesn't have an equivalent permissions system
|
|
542
|
+
if (!cc.hasPermissions) {
|
|
543
|
+
console.log(` Not needed for ${cc.name}. Skipping.`);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const existing = readJson(cc.settingsFile);
|
|
411
548
|
const allow = existing?.permissions?.allow || [];
|
|
412
549
|
const pattern = "mcp__gitmem__*";
|
|
413
550
|
|
|
414
551
|
if (allow.includes(pattern)) {
|
|
415
|
-
console.log(
|
|
552
|
+
console.log(` Already configured in ${cc.configDir}/settings.json. Skipping.`);
|
|
416
553
|
return;
|
|
417
554
|
}
|
|
418
555
|
|
|
419
|
-
if (!(await confirm(
|
|
556
|
+
if (!(await confirm(`Add mcp__gitmem__* to ${cc.configDir}/settings.json?`))) {
|
|
420
557
|
console.log(" Skipped.");
|
|
421
558
|
return;
|
|
422
559
|
}
|
|
@@ -427,20 +564,48 @@ async function stepPermissions() {
|
|
|
427
564
|
}
|
|
428
565
|
|
|
429
566
|
const settings = existing || {};
|
|
430
|
-
if (!existsSync(
|
|
431
|
-
mkdirSync(
|
|
567
|
+
if (!existsSync(cc.configDir)) {
|
|
568
|
+
mkdirSync(cc.configDir, { recursive: true });
|
|
432
569
|
}
|
|
433
570
|
const permissions = settings.permissions || {};
|
|
434
571
|
const newAllow = permissions.allow || [];
|
|
435
572
|
newAllow.push(pattern);
|
|
436
573
|
settings.permissions = { ...permissions, allow: newAllow };
|
|
437
|
-
writeJson(
|
|
574
|
+
writeJson(cc.settingsFile, settings);
|
|
438
575
|
|
|
439
576
|
console.log(" Added gitmem tool permissions");
|
|
440
577
|
}
|
|
441
578
|
|
|
579
|
+
function copyHookScripts() {
|
|
580
|
+
const destHooksDir = join(gitmemDir, "hooks");
|
|
581
|
+
if (!existsSync(destHooksDir)) {
|
|
582
|
+
mkdirSync(destHooksDir, { recursive: true });
|
|
583
|
+
}
|
|
584
|
+
if (existsSync(hooksScriptsDir)) {
|
|
585
|
+
try {
|
|
586
|
+
for (const file of readdirSync(hooksScriptsDir)) {
|
|
587
|
+
if (file.endsWith(".sh")) {
|
|
588
|
+
const src = join(hooksScriptsDir, file);
|
|
589
|
+
const dest = join(destHooksDir, file);
|
|
590
|
+
writeFileSync(dest, readFileSync(src));
|
|
591
|
+
chmodSync(dest, 0o755);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
} catch {
|
|
595
|
+
// Non-critical
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
442
600
|
async function stepHooks() {
|
|
443
|
-
|
|
601
|
+
if (cc.hooksInSettings) {
|
|
602
|
+
return stepHooksClaude();
|
|
603
|
+
}
|
|
604
|
+
return stepHooksCursor();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async function stepHooksClaude() {
|
|
608
|
+
const existing = readJson(cc.settingsFile);
|
|
444
609
|
const hooks = existing?.hooks || {};
|
|
445
610
|
const hasGitmem = JSON.stringify(hooks).includes("gitmem");
|
|
446
611
|
|
|
@@ -472,32 +637,14 @@ async function stepHooks() {
|
|
|
472
637
|
return;
|
|
473
638
|
}
|
|
474
639
|
|
|
475
|
-
|
|
476
|
-
const destHooksDir = join(gitmemDir, "hooks");
|
|
477
|
-
if (!existsSync(destHooksDir)) {
|
|
478
|
-
mkdirSync(destHooksDir, { recursive: true });
|
|
479
|
-
}
|
|
480
|
-
if (existsSync(hooksScriptsDir)) {
|
|
481
|
-
try {
|
|
482
|
-
for (const file of readdirSync(hooksScriptsDir)) {
|
|
483
|
-
if (file.endsWith(".sh")) {
|
|
484
|
-
const src = join(hooksScriptsDir, file);
|
|
485
|
-
const dest = join(destHooksDir, file);
|
|
486
|
-
writeFileSync(dest, readFileSync(src));
|
|
487
|
-
chmodSync(dest, 0o755);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
} catch {
|
|
491
|
-
// Non-critical
|
|
492
|
-
}
|
|
493
|
-
}
|
|
640
|
+
copyHookScripts();
|
|
494
641
|
|
|
495
642
|
const settings = existing || {};
|
|
496
|
-
if (!existsSync(
|
|
497
|
-
mkdirSync(
|
|
643
|
+
if (!existsSync(cc.configDir)) {
|
|
644
|
+
mkdirSync(cc.configDir, { recursive: true });
|
|
498
645
|
}
|
|
499
646
|
|
|
500
|
-
const gitmemHooks =
|
|
647
|
+
const gitmemHooks = buildClaudeHooks();
|
|
501
648
|
const merged = { ...(settings.hooks || {}) };
|
|
502
649
|
|
|
503
650
|
for (const [eventType, gitmemEntries] of Object.entries(gitmemHooks)) {
|
|
@@ -507,7 +654,7 @@ async function stepHooks() {
|
|
|
507
654
|
}
|
|
508
655
|
|
|
509
656
|
settings.hooks = merged;
|
|
510
|
-
writeJson(
|
|
657
|
+
writeJson(cc.settingsFile, settings);
|
|
511
658
|
|
|
512
659
|
const preservedMsg =
|
|
513
660
|
existingHookCount > 0
|
|
@@ -516,8 +663,8 @@ async function stepHooks() {
|
|
|
516
663
|
console.log(` Merged 4 gitmem hook types${preservedMsg}`);
|
|
517
664
|
|
|
518
665
|
// Warn about settings.local.json
|
|
519
|
-
if (existsSync(
|
|
520
|
-
const local = readJson(
|
|
666
|
+
if (cc.settingsLocalFile && existsSync(cc.settingsLocalFile)) {
|
|
667
|
+
const local = readJson(cc.settingsLocalFile);
|
|
521
668
|
if (local?.hooks) {
|
|
522
669
|
console.log("");
|
|
523
670
|
console.log(
|
|
@@ -530,6 +677,69 @@ async function stepHooks() {
|
|
|
530
677
|
}
|
|
531
678
|
}
|
|
532
679
|
|
|
680
|
+
async function stepHooksCursor() {
|
|
681
|
+
const hooksPath = cc.hooksFile;
|
|
682
|
+
const hooksName = cc.hooksFileName;
|
|
683
|
+
|
|
684
|
+
const existing = readJson(hooksPath);
|
|
685
|
+
const hasGitmem = existing ? JSON.stringify(existing).includes("gitmem") : false;
|
|
686
|
+
|
|
687
|
+
if (hasGitmem) {
|
|
688
|
+
console.log(` Already configured in ${hooksName}. Skipping.`);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Count existing non-gitmem hooks
|
|
693
|
+
let existingHookCount = 0;
|
|
694
|
+
if (existing?.hooks) {
|
|
695
|
+
for (const entries of Object.values(existing.hooks)) {
|
|
696
|
+
if (Array.isArray(entries)) {
|
|
697
|
+
existingHookCount += entries.filter((e) => !isGitmemHook(e)).length;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const prompt =
|
|
703
|
+
existingHookCount > 0
|
|
704
|
+
? `Merge gitmem hooks into ${hooksName}? (${existingHookCount} existing hook${existingHookCount !== 1 ? "s" : ""} preserved)`
|
|
705
|
+
: `Add gitmem lifecycle hooks to ${hooksName}?`;
|
|
706
|
+
|
|
707
|
+
if (!(await confirm(prompt))) {
|
|
708
|
+
console.log(" Skipped.");
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (dryRun) {
|
|
713
|
+
console.log(" [dry-run] Would merge 4 gitmem hook types");
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
copyHookScripts();
|
|
718
|
+
|
|
719
|
+
if (!existsSync(cc.configDir)) {
|
|
720
|
+
mkdirSync(cc.configDir, { recursive: true });
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const gitmemHooks = buildCursorHooks();
|
|
724
|
+
const config = existing || {};
|
|
725
|
+
const merged = { ...(config.hooks || {}) };
|
|
726
|
+
|
|
727
|
+
for (const [eventType, gitmemEntries] of Object.entries(gitmemHooks)) {
|
|
728
|
+
const existingEntries = merged[eventType] || [];
|
|
729
|
+
const nonGitmem = existingEntries.filter((e) => !isGitmemHook(e));
|
|
730
|
+
merged[eventType] = [...nonGitmem, ...gitmemEntries];
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
config.hooks = merged;
|
|
734
|
+
writeJson(hooksPath, config);
|
|
735
|
+
|
|
736
|
+
const preservedMsg =
|
|
737
|
+
existingHookCount > 0
|
|
738
|
+
? ` (${existingHookCount} existing hook${existingHookCount !== 1 ? "s" : ""} preserved)`
|
|
739
|
+
: "";
|
|
740
|
+
console.log(` Merged 4 gitmem hook types${preservedMsg}`);
|
|
741
|
+
}
|
|
742
|
+
|
|
533
743
|
async function stepGitignore() {
|
|
534
744
|
const exists = existsSync(gitignorePath);
|
|
535
745
|
let content = exists ? readFileSync(gitignorePath, "utf-8") : "";
|
|
@@ -565,10 +775,19 @@ async function main() {
|
|
|
565
775
|
const pkg = readJson(join(__dirname, "..", "package.json"));
|
|
566
776
|
const version = pkg?.version || "1.0.0";
|
|
567
777
|
|
|
778
|
+
// Detect client before anything else
|
|
779
|
+
client = detectClient();
|
|
780
|
+
cc = CLIENT_CONFIGS[client];
|
|
781
|
+
|
|
568
782
|
console.log("");
|
|
569
|
-
console.log(` gitmem v${version}
|
|
783
|
+
console.log(` gitmem v${version} \u2014 Setup for ${cc.name}`);
|
|
570
784
|
if (dryRun) {
|
|
571
|
-
console.log(" (dry-run mode
|
|
785
|
+
console.log(" (dry-run mode \u2014 no files will be written)");
|
|
786
|
+
}
|
|
787
|
+
if (clientFlag) {
|
|
788
|
+
console.log(` (client: ${client} \u2014 via --client flag)`);
|
|
789
|
+
} else {
|
|
790
|
+
console.log(` (client: ${client} \u2014 auto-detected)`);
|
|
572
791
|
}
|
|
573
792
|
console.log("");
|
|
574
793
|
|
|
@@ -576,24 +795,24 @@ async function main() {
|
|
|
576
795
|
console.log(" Detecting environment...");
|
|
577
796
|
const detections = [];
|
|
578
797
|
|
|
579
|
-
if (existsSync(
|
|
580
|
-
const mcp = readJson(
|
|
798
|
+
if (existsSync(cc.mcpConfigPath)) {
|
|
799
|
+
const mcp = readJson(cc.mcpConfigPath);
|
|
581
800
|
const count = mcp?.mcpServers ? Object.keys(mcp.mcpServers).length : 0;
|
|
582
801
|
detections.push(
|
|
583
|
-
` .
|
|
802
|
+
` ${cc.mcpConfigName} found (${count} server${count !== 1 ? "s" : ""})`
|
|
584
803
|
);
|
|
585
804
|
}
|
|
586
805
|
|
|
587
|
-
if (existsSync(
|
|
588
|
-
const content = readFileSync(
|
|
589
|
-
const hasGitmem = content.includes(
|
|
806
|
+
if (existsSync(cc.instructionsFile)) {
|
|
807
|
+
const content = readFileSync(cc.instructionsFile, "utf-8");
|
|
808
|
+
const hasGitmem = content.includes(cc.startMarker);
|
|
590
809
|
detections.push(
|
|
591
|
-
`
|
|
810
|
+
` ${cc.instructionsName} found (${hasGitmem ? "has gitmem section" : "no gitmem section"})`
|
|
592
811
|
);
|
|
593
812
|
}
|
|
594
813
|
|
|
595
|
-
if (existsSync(
|
|
596
|
-
const settings = readJson(
|
|
814
|
+
if (cc.settingsFile && existsSync(cc.settingsFile)) {
|
|
815
|
+
const settings = readJson(cc.settingsFile);
|
|
597
816
|
const hookCount = settings?.hooks
|
|
598
817
|
? Object.values(settings.hooks).flat().length
|
|
599
818
|
: 0;
|
|
@@ -602,6 +821,16 @@ async function main() {
|
|
|
602
821
|
);
|
|
603
822
|
}
|
|
604
823
|
|
|
824
|
+
if (!cc.hooksInSettings && cc.hooksFile && existsSync(cc.hooksFile)) {
|
|
825
|
+
const hooks = readJson(cc.hooksFile);
|
|
826
|
+
const hookCount = hooks?.hooks
|
|
827
|
+
? Object.values(hooks.hooks).flat().length
|
|
828
|
+
: 0;
|
|
829
|
+
detections.push(
|
|
830
|
+
` ${cc.hooksFileName} found (${hookCount} hook${hookCount !== 1 ? "s" : ""})`
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
605
834
|
if (existsSync(gitignorePath)) {
|
|
606
835
|
detections.push(" .gitignore found");
|
|
607
836
|
}
|
|
@@ -621,35 +850,45 @@ async function main() {
|
|
|
621
850
|
);
|
|
622
851
|
console.log("");
|
|
623
852
|
|
|
624
|
-
// Run steps
|
|
625
|
-
|
|
853
|
+
// Run steps — step count depends on client
|
|
854
|
+
const stepCount = cc.hasPermissions ? 6 : 5;
|
|
855
|
+
let step = 1;
|
|
856
|
+
|
|
857
|
+
console.log(` Step ${step}/${stepCount} \u2014 Memory Store`);
|
|
626
858
|
await stepMemoryStore();
|
|
627
859
|
console.log("");
|
|
860
|
+
step++;
|
|
628
861
|
|
|
629
|
-
console.log(
|
|
862
|
+
console.log(` Step ${step}/${stepCount} \u2014 MCP Server`);
|
|
630
863
|
await stepMcpServer();
|
|
631
864
|
console.log("");
|
|
865
|
+
step++;
|
|
632
866
|
|
|
633
|
-
console.log(
|
|
634
|
-
await
|
|
867
|
+
console.log(` Step ${step}/${stepCount} \u2014 Project Instructions`);
|
|
868
|
+
await stepInstructions();
|
|
635
869
|
console.log("");
|
|
870
|
+
step++;
|
|
636
871
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
872
|
+
if (cc.hasPermissions) {
|
|
873
|
+
console.log(` Step ${step}/${stepCount} \u2014 Tool Permissions`);
|
|
874
|
+
await stepPermissions();
|
|
875
|
+
console.log("");
|
|
876
|
+
step++;
|
|
877
|
+
}
|
|
640
878
|
|
|
641
|
-
console.log(
|
|
879
|
+
console.log(` Step ${step}/${stepCount} \u2014 Lifecycle Hooks`);
|
|
642
880
|
await stepHooks();
|
|
643
881
|
console.log("");
|
|
882
|
+
step++;
|
|
644
883
|
|
|
645
|
-
console.log(
|
|
884
|
+
console.log(` Step ${step}/${stepCount} \u2014 Gitignore`);
|
|
646
885
|
await stepGitignore();
|
|
647
886
|
console.log("");
|
|
648
887
|
|
|
649
888
|
if (dryRun) {
|
|
650
|
-
console.log(" Dry run complete
|
|
889
|
+
console.log(" Dry run complete \u2014 no files were modified.");
|
|
651
890
|
} else {
|
|
652
|
-
console.log(
|
|
891
|
+
console.log(` ${cc.completionMsg}`);
|
|
653
892
|
console.log(" To remove: npx gitmem-mcp uninstall");
|
|
654
893
|
}
|
|
655
894
|
console.log("");
|