@sfrangulov/shared-memory-mcp 1.0.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/github-memory-server.js +1360 -0
- package/lib/atomic-commit.js +126 -0
- package/lib/github-client.js +220 -0
- package/lib/root-parser.js +323 -0
- package/lib/slugify.js +77 -0
- package/lib/state-manager.js +153 -0
- package/package.json +33 -0
package/lib/slugify.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { transliterate } from "transliteration";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reserved slug names that conflict with system files/folders.
|
|
5
|
+
* These are checked in their slug form (after transliteration and normalization).
|
|
6
|
+
* Original system names: root, _meta, _shared, shared
|
|
7
|
+
* After slug processing: root, meta, shared
|
|
8
|
+
*/
|
|
9
|
+
const RESERVED_SLUGS = new Set(["root", "meta", "shared"]);
|
|
10
|
+
|
|
11
|
+
const MAX_LENGTH = 60;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Converts a title string into a URL/filename-safe slug.
|
|
15
|
+
*
|
|
16
|
+
* Rules (applied in order):
|
|
17
|
+
* 1. Transliterate non-Latin characters (e.g. "Привет" -> "privet")
|
|
18
|
+
* 2. Lowercase
|
|
19
|
+
* 3. Replace everything except a-z, 0-9 with hyphens
|
|
20
|
+
* 4. Remove leading/trailing hyphens and collapse duplicate hyphens
|
|
21
|
+
* 5. Truncate to max 60 characters (trim any trailing hyphen from truncation)
|
|
22
|
+
* 6. Check reserved names: root, meta, shared -> add suffix "-entry"
|
|
23
|
+
*
|
|
24
|
+
* @param {string} title - The title to convert
|
|
25
|
+
* @returns {string} The generated slug
|
|
26
|
+
*/
|
|
27
|
+
export function slugify(title) {
|
|
28
|
+
if (!title) return "";
|
|
29
|
+
|
|
30
|
+
// 1. Transliterate non-Latin characters
|
|
31
|
+
let slug = transliterate(title);
|
|
32
|
+
|
|
33
|
+
// 2. Lowercase
|
|
34
|
+
slug = slug.toLowerCase();
|
|
35
|
+
|
|
36
|
+
// 3. Replace everything except a-z, 0-9 with hyphens
|
|
37
|
+
slug = slug.replace(/[^a-z0-9]/g, "-");
|
|
38
|
+
|
|
39
|
+
// 4. Collapse duplicate hyphens and remove leading/trailing hyphens
|
|
40
|
+
slug = slug.replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
41
|
+
|
|
42
|
+
// 5. Truncate to max 60 characters, then trim any trailing hyphen
|
|
43
|
+
if (slug.length > MAX_LENGTH) {
|
|
44
|
+
slug = slug.slice(0, MAX_LENGTH).replace(/-$/, "");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 6. Check reserved names
|
|
48
|
+
if (RESERVED_SLUGS.has(slug)) {
|
|
49
|
+
slug = `${slug}-entry`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return slug;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Ensures a slug is unique among existing files.
|
|
57
|
+
* If `slug.md` already exists in the file list, appends an incrementing
|
|
58
|
+
* suffix: slug-2, slug-3, etc.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} slug - The base slug to check
|
|
61
|
+
* @param {string[]} existingFiles - Array of existing filenames (e.g. ["hello.md", "world.md"])
|
|
62
|
+
* @returns {string} A unique slug
|
|
63
|
+
*/
|
|
64
|
+
export function ensureUnique(slug, existingFiles) {
|
|
65
|
+
const fileSet = new Set(existingFiles);
|
|
66
|
+
|
|
67
|
+
if (!fileSet.has(`${slug}.md`)) {
|
|
68
|
+
return slug;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let counter = 2;
|
|
72
|
+
while (fileSet.has(`${slug}-${counter}.md`)) {
|
|
73
|
+
counter++;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return `${slug}-${counter}`;
|
|
77
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { readFile, writeFile, rename } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const STATE_FILENAME = ".shared-memory-state";
|
|
5
|
+
const TEMP_SUFFIX = ".tmp";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default state returned when no state file exists or file is corrupted.
|
|
9
|
+
* @returns {{ active_project: null, version: 1 }}
|
|
10
|
+
*/
|
|
11
|
+
function defaultState() {
|
|
12
|
+
return { active_project: null, version: 1 };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a session state manager for the given working directory.
|
|
17
|
+
*
|
|
18
|
+
* Manages the `.shared-memory-state` JSON file with atomic writes
|
|
19
|
+
* (write to temp file, then rename). Author cache is in-memory,
|
|
20
|
+
* per-project, session-only.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} workdir - Absolute path to the working directory
|
|
23
|
+
* @returns {{
|
|
24
|
+
* readState: () => Promise<{ active_project: string | null, version: number }>,
|
|
25
|
+
* writeState: (state: object) => Promise<void>,
|
|
26
|
+
* getAuthorCache: (project: string) => object | null,
|
|
27
|
+
* setAuthorCache: (project: string, cache: object) => void,
|
|
28
|
+
* invalidateAuthorCache: (project?: string) => void
|
|
29
|
+
* }}
|
|
30
|
+
*/
|
|
31
|
+
export function createStateManager(workdir) {
|
|
32
|
+
const statePath = join(workdir, STATE_FILENAME);
|
|
33
|
+
const tempPath = join(workdir, STATE_FILENAME + TEMP_SUFFIX);
|
|
34
|
+
|
|
35
|
+
/** @type {Map<string, object>} In-memory author cache, keyed by project */
|
|
36
|
+
const authorCacheMap = new Map();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Reads the session state from the state file.
|
|
40
|
+
*
|
|
41
|
+
* - File missing -> returns defaults
|
|
42
|
+
* - File corrupted (invalid JSON) -> logs warning, deletes, recreates with defaults
|
|
43
|
+
* - Version field missing or non-numeric -> treats as version 1, rewrites
|
|
44
|
+
*
|
|
45
|
+
* @returns {Promise<{ active_project: string | null, version: number }>}
|
|
46
|
+
*/
|
|
47
|
+
async function readState() {
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = await readFile(statePath, "utf-8");
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (err.code === "ENOENT") {
|
|
53
|
+
return defaultState();
|
|
54
|
+
}
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let parsed;
|
|
59
|
+
try {
|
|
60
|
+
parsed = JSON.parse(raw);
|
|
61
|
+
} catch {
|
|
62
|
+
// Corrupted JSON — log warning, delete and recreate with defaults
|
|
63
|
+
console.warn(
|
|
64
|
+
`[state-manager] Corrupted state file at ${statePath}, resetting to defaults`
|
|
65
|
+
);
|
|
66
|
+
const defaults = defaultState();
|
|
67
|
+
await atomicWrite(defaults);
|
|
68
|
+
return defaults;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Validate version field
|
|
72
|
+
let needsRewrite = false;
|
|
73
|
+
if (typeof parsed.version !== "number") {
|
|
74
|
+
parsed.version = 1;
|
|
75
|
+
needsRewrite = true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (needsRewrite) {
|
|
79
|
+
await atomicWrite(parsed);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
active_project: parsed.active_project ?? null,
|
|
84
|
+
version: parsed.version,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Writes state to the state file atomically.
|
|
90
|
+
* Writes to a temp file first, then renames to the final path.
|
|
91
|
+
*
|
|
92
|
+
* @param {object} state - The state object to write
|
|
93
|
+
* @returns {Promise<void>}
|
|
94
|
+
*/
|
|
95
|
+
async function writeState(state) {
|
|
96
|
+
await atomicWrite(state);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Internal: atomic write via temp file + rename.
|
|
101
|
+
*
|
|
102
|
+
* @param {object} data - JSON-serializable data
|
|
103
|
+
* @returns {Promise<void>}
|
|
104
|
+
*/
|
|
105
|
+
async function atomicWrite(data) {
|
|
106
|
+
const json = JSON.stringify(data, null, 2);
|
|
107
|
+
await writeFile(tempPath, json, "utf-8");
|
|
108
|
+
await rename(tempPath, statePath);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Returns the author cache for a given project, or null if not cached.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} project - Project identifier
|
|
115
|
+
* @returns {object | null}
|
|
116
|
+
*/
|
|
117
|
+
function getAuthorCache(project) {
|
|
118
|
+
return authorCacheMap.get(project) ?? null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Stores the author cache for a given project (in-memory only).
|
|
123
|
+
*
|
|
124
|
+
* @param {string} project - Project identifier
|
|
125
|
+
* @param {object} cache - Map-like object of filename -> author
|
|
126
|
+
*/
|
|
127
|
+
function setAuthorCache(project, cache) {
|
|
128
|
+
authorCacheMap.set(project, cache);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Invalidates author cache.
|
|
133
|
+
* If project is specified, clears only that project's cache.
|
|
134
|
+
* If not specified, clears the entire cache.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} [project] - Optional project identifier
|
|
137
|
+
*/
|
|
138
|
+
function invalidateAuthorCache(project) {
|
|
139
|
+
if (project !== undefined) {
|
|
140
|
+
authorCacheMap.delete(project);
|
|
141
|
+
} else {
|
|
142
|
+
authorCacheMap.clear();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
readState,
|
|
148
|
+
writeState,
|
|
149
|
+
getAuthorCache,
|
|
150
|
+
setAuthorCache,
|
|
151
|
+
invalidateAuthorCache,
|
|
152
|
+
};
|
|
153
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sfrangulov/shared-memory-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Claude Shared Memory — turns a GitHub repo into a shared team knowledge base",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"shared-memory-mcp": "github-memory-server.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=20.0.0"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"github-memory-server.js",
|
|
14
|
+
"lib/"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node github-memory-server.js",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.27.0",
|
|
23
|
+
"zod": "^3.24.0",
|
|
24
|
+
"@octokit/rest": "^22.0.0",
|
|
25
|
+
"@octokit/plugin-retry": "^8.0.0",
|
|
26
|
+
"@octokit/plugin-throttling": "^11.0.0",
|
|
27
|
+
"p-limit": "^7.0.0",
|
|
28
|
+
"transliteration": "^2.6.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"vitest": "^3.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|