@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/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
+ }