@vheins/local-memory-mcp 0.1.4 → 0.1.5

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.
@@ -0,0 +1,84 @@
1
+ // Feature: memory-mcp-optimization
2
+ // Property 20: StructuredLogger output adalah JSON valid dengan field wajib
3
+ // Validates: Requirements 21.1, 21.2
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
5
+ import * as fc from "fast-check";
6
+ describe("Property 20: StructuredLogger output adalah JSON valid dengan field wajib", () => {
7
+ let stderrOutput = [];
8
+ let stderrSpy;
9
+ beforeEach(() => {
10
+ stderrOutput = [];
11
+ stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation((chunk) => {
12
+ stderrOutput.push(typeof chunk === "string" ? chunk : chunk.toString());
13
+ return true;
14
+ });
15
+ // Reset LOG_LEVEL to ensure all levels are logged
16
+ process.env.LOG_LEVEL = "debug";
17
+ });
18
+ afterEach(() => {
19
+ stderrSpy.mockRestore();
20
+ delete process.env.LOG_LEVEL;
21
+ });
22
+ it("each log call produces valid JSON output with required fields", async () => {
23
+ // Re-import logger after setting LOG_LEVEL
24
+ const { logger } = await import("./logger.js?t=" + Date.now());
25
+ fc.assert(fc.property(fc.oneof(fc.constant("debug"), fc.constant("info"), fc.constant("warn"), fc.constant("error")), fc.string({ minLength: 1, maxLength: 100 }), (level, message) => {
26
+ stderrOutput = [];
27
+ logger[level](message);
28
+ expect(stderrOutput.length).toBeGreaterThanOrEqual(1);
29
+ const lastOutput = stderrOutput[stderrOutput.length - 1];
30
+ // Must be parseable JSON
31
+ let parsed;
32
+ expect(() => {
33
+ parsed = JSON.parse(lastOutput.trim());
34
+ }).not.toThrow();
35
+ parsed = JSON.parse(lastOutput.trim());
36
+ // Must have required fields
37
+ expect(parsed).toHaveProperty("level");
38
+ expect(parsed).toHaveProperty("timestamp");
39
+ expect(parsed).toHaveProperty("message");
40
+ // level must match
41
+ expect(parsed.level).toBe(level);
42
+ // message must match
43
+ expect(parsed.message).toBe(message);
44
+ // timestamp must be ISO 8601
45
+ expect(typeof parsed.timestamp).toBe("string");
46
+ const ts = new Date(parsed.timestamp);
47
+ expect(isNaN(ts.getTime())).toBe(false);
48
+ }), { numRuns: 100 });
49
+ });
50
+ it("log output with context includes context field", async () => {
51
+ const { logger } = await import("./logger.js?t=" + Date.now() + "ctx");
52
+ stderrOutput = [];
53
+ logger.info("test message", { key: "value", count: 42 });
54
+ expect(stderrOutput.length).toBeGreaterThanOrEqual(1);
55
+ const lastOutput = stderrOutput[stderrOutput.length - 1];
56
+ const parsed = JSON.parse(lastOutput.trim());
57
+ expect(parsed).toHaveProperty("context");
58
+ expect(parsed.context).toEqual({ key: "value", count: 42 });
59
+ });
60
+ it("log output without context does not include context field", async () => {
61
+ const { logger } = await import("./logger.js?t=" + Date.now() + "noctx");
62
+ stderrOutput = [];
63
+ logger.info("no context message");
64
+ expect(stderrOutput.length).toBeGreaterThanOrEqual(1);
65
+ const lastOutput = stderrOutput[stderrOutput.length - 1];
66
+ const parsed = JSON.parse(lastOutput.trim());
67
+ expect(parsed).not.toHaveProperty("context");
68
+ });
69
+ it("all four log levels produce valid JSON with correct level field", async () => {
70
+ const { logger } = await import("./logger.js?t=" + Date.now() + "levels");
71
+ const levels = ["debug", "info", "warn", "error"];
72
+ for (const level of levels) {
73
+ stderrOutput = [];
74
+ logger[level](`test ${level} message`);
75
+ expect(stderrOutput.length).toBeGreaterThanOrEqual(1);
76
+ const lastOutput = stderrOutput[stderrOutput.length - 1];
77
+ const parsed = JSON.parse(lastOutput.trim());
78
+ expect(parsed.level).toBe(level);
79
+ expect(parsed.message).toBe(`test ${level} message`);
80
+ expect(typeof parsed.timestamp).toBe("string");
81
+ }
82
+ });
83
+ });
84
+ //# sourceMappingURL=logger.test.js.map
@@ -0,0 +1,159 @@
1
+ // Feature: memory-mcp-optimization
2
+ // Unit + Property tests for normalize(), tokenize(), and STOPWORDS
3
+ // Requirements: 5.4, 5.5, 17.3, 17.4, 17.7
4
+ import { describe, it, expect } from "vitest";
5
+ import * as fc from "fast-check";
6
+ import { normalize, tokenize, STOPWORDS } from "./normalize.js";
7
+ // ─── Unit Tests: normalize() ─────────────────────────────────────────────────
8
+ describe("normalize() — unit tests", () => {
9
+ it("converts text to lowercase", () => {
10
+ expect(normalize("Hello World")).toBe("hello world");
11
+ expect(normalize("TYPESCRIPT")).toBe("typescript");
12
+ expect(normalize("CamelCase")).toBe("camelcase");
13
+ });
14
+ it("trims leading and trailing whitespace", () => {
15
+ expect(normalize(" hello ")).toBe("hello");
16
+ expect(normalize("\t text \n")).toBe("text");
17
+ });
18
+ it("replaces special characters with spaces", () => {
19
+ const result = normalize("hello!world@test#value");
20
+ // Special chars replaced by spaces, then collapsed
21
+ expect(result).not.toContain("!");
22
+ expect(result).not.toContain("@");
23
+ expect(result).not.toContain("#");
24
+ });
25
+ it("collapses multiple spaces into one", () => {
26
+ expect(normalize("hello world")).toBe("hello world");
27
+ expect(normalize("a b c")).toBe("a b c");
28
+ });
29
+ it("handles empty string", () => {
30
+ expect(normalize("")).toBe("");
31
+ });
32
+ it("handles string with only special characters", () => {
33
+ expect(normalize("!!!@@@###")).toBe("");
34
+ });
35
+ it("preserves alphanumeric characters", () => {
36
+ expect(normalize("abc123")).toBe("abc123");
37
+ });
38
+ });
39
+ // ─── Unit Tests: tokenize() ──────────────────────────────────────────────────
40
+ describe("tokenize() — unit tests", () => {
41
+ it("removes stopwords from output", () => {
42
+ const tokens = tokenize("the quick brown fox");
43
+ expect(tokens).not.toContain("the");
44
+ expect(tokens).toContain("quick");
45
+ expect(tokens).toContain("brown");
46
+ expect(tokens).toContain("fox");
47
+ });
48
+ it("removes short words (length <= 0 after normalize)", () => {
49
+ // tokenize filters tokens with length > 0 and not in STOPWORDS
50
+ const tokens = tokenize("a b c hello");
51
+ // "a", "b", "c" are stopwords or single chars
52
+ expect(tokens).toContain("hello");
53
+ });
54
+ it("returns empty array for all-stopword input", () => {
55
+ const tokens = tokenize("the and or but");
56
+ expect(tokens).toEqual([]);
57
+ });
58
+ it("handles empty string", () => {
59
+ expect(tokenize("")).toEqual([]);
60
+ });
61
+ it("normalizes before tokenizing (lowercase, trim)", () => {
62
+ const tokens1 = tokenize("TypeScript");
63
+ const tokens2 = tokenize("typescript");
64
+ expect(tokens1).toEqual(tokens2);
65
+ });
66
+ it("removes Indonesian stopwords", () => {
67
+ const tokens = tokenize("yang dan di ke dari ini itu coding");
68
+ expect(tokens).not.toContain("yang");
69
+ expect(tokens).not.toContain("dan");
70
+ expect(tokens).toContain("coding");
71
+ });
72
+ });
73
+ // ─── Property 4: Tokenisasi konsisten antara SQLiteStore dan StubVectorStore ──
74
+ // Feature: memory-mcp-optimization, Property 4: Tokenisasi konsisten
75
+ // Validates: Requirements 4.4, 5.2, 5.3
76
+ describe("Property 4: Tokenisasi konsisten antara SQLiteStore dan StubVectorStore", () => {
77
+ it("tokenize() is deterministic — same input always produces same output", () => {
78
+ fc.assert(fc.property(fc.string({ minLength: 0, maxLength: 100 }), (text) => {
79
+ // Both SQLiteStore and StubVectorStore use tokenize() from normalize.ts
80
+ // So testing tokenize() determinism validates consistency between them
81
+ const result1 = tokenize(text);
82
+ const result2 = tokenize(text);
83
+ expect(result1).toEqual(result2);
84
+ }), { numRuns: 200 });
85
+ });
86
+ it("tokenize() output is a subset of normalize() tokens", () => {
87
+ fc.assert(fc.property(fc.string({ minLength: 0, maxLength: 100 }), (text) => {
88
+ const normalized = normalize(text);
89
+ const allTokens = normalized.split(" ").filter((t) => t.length > 0);
90
+ const filtered = tokenize(text);
91
+ // Every token in filtered must appear in allTokens
92
+ for (const token of filtered) {
93
+ expect(allTokens).toContain(token);
94
+ }
95
+ }), { numRuns: 200 });
96
+ });
97
+ });
98
+ // ─── Property 5: normalize() idempoten ganda ─────────────────────────────────
99
+ // Feature: memory-mcp-optimization, Property 5: normalize() idempoten ganda
100
+ // Validates: Requirements 5.4, 5.5
101
+ describe("Property 5: normalize() idempoten ganda", () => {
102
+ it("normalize(normalize(text)) === normalize(text) for 100+ random inputs", () => {
103
+ fc.assert(fc.property(fc.string({ minLength: 0, maxLength: 200 }), (text) => {
104
+ const once = normalize(text);
105
+ const twice = normalize(once);
106
+ expect(twice).toBe(once);
107
+ }), { numRuns: 200 });
108
+ });
109
+ it("normalize is idempotent for specific edge cases", () => {
110
+ const cases = [
111
+ "",
112
+ " ",
113
+ "Hello World!",
114
+ "UPPERCASE",
115
+ "already normalized",
116
+ "multiple spaces",
117
+ "special!@#chars",
118
+ "123numbers456",
119
+ ];
120
+ for (const text of cases) {
121
+ const once = normalize(text);
122
+ const twice = normalize(once);
123
+ expect(twice).toBe(once);
124
+ }
125
+ });
126
+ });
127
+ // ─── Property 21: Stopword list tidak mengandung duplikat ────────────────────
128
+ // Feature: memory-mcp-optimization, Property 21: Stopword no duplicates
129
+ // Validates: Requirement 17.4
130
+ describe("Property 21: Stopword list tidak mengandung duplikat", () => {
131
+ it("STOPWORDS has no duplicate entries", () => {
132
+ // Since STOPWORDS is a Set, duplicates are automatically removed.
133
+ // We verify the source array (before Set construction) has no duplicates
134
+ // by checking that Set size equals the number of unique entries.
135
+ // The Set itself guarantees uniqueness, so we verify the exported Set is consistent.
136
+ const stopwordsArray = Array.from(STOPWORDS);
137
+ const uniqueSet = new Set(stopwordsArray);
138
+ expect(uniqueSet.size).toBe(stopwordsArray.length);
139
+ });
140
+ it("all STOPWORDS entries are lowercase strings", () => {
141
+ for (const word of STOPWORDS) {
142
+ expect(typeof word).toBe("string");
143
+ expect(word).toBe(word.toLowerCase());
144
+ }
145
+ });
146
+ it("STOPWORDS is non-empty", () => {
147
+ expect(STOPWORDS.size).toBeGreaterThan(0);
148
+ });
149
+ it("property: no string appears more than once in STOPWORDS", () => {
150
+ // This is a structural property — verified once (not random inputs needed)
151
+ // since STOPWORDS is a constant Set
152
+ const seen = new Set();
153
+ for (const word of STOPWORDS) {
154
+ expect(seen.has(word)).toBe(false);
155
+ seen.add(word);
156
+ }
157
+ });
158
+ });
159
+ //# sourceMappingURL=normalize.test.js.map
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { expandQuery } from "../utils/query-expander.js";
3
+ describe("expandQuery", () => {
4
+ it("returns original query when no prompt provided", () => {
5
+ expect(expandQuery("database")).toBe("database");
6
+ expect(expandQuery("api endpoint")).toBe("api endpoint");
7
+ });
8
+ it("expands query with prompt keywords", () => {
9
+ const result = expandQuery("database", "user authentication implementation");
10
+ expect(result).toContain("database");
11
+ expect(result).toContain("user");
12
+ expect(result).toContain("auth");
13
+ });
14
+ it("expands known keywords", () => {
15
+ const result = expandQuery("auth", "login system");
16
+ expect(result).toContain("auth");
17
+ expect(result).toContain("login");
18
+ expect(result).toContain("password");
19
+ });
20
+ it("limits to 10 keywords", () => {
21
+ const result = expandQuery("api", "building a rest endpoint with controller and route handling");
22
+ const words = result.split(" ");
23
+ expect(words.length).toBeLessThanOrEqual(10);
24
+ });
25
+ it("removes duplicates", () => {
26
+ const result = expandQuery("database", "database query optimization");
27
+ const words = result.split(" ");
28
+ const unique = new Set(words);
29
+ expect(words.length).toBe(unique.size);
30
+ });
31
+ it("handles empty prompt gracefully", () => {
32
+ expect(expandQuery("test", "")).toBe("test");
33
+ });
34
+ });
35
+ //# sourceMappingURL=query-expander.test.js.map
package/package.json CHANGED
@@ -1,17 +1,22 @@
1
1
  {
2
2
  "name": "@vheins/local-memory-mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "MCP Local Memory Service for coding copilot agents",
5
+ "mcpName": "io.github.vheins/local-memory-mcp",
5
6
  "type": "module",
6
7
  "main": "dist/server.js",
7
8
  "bin": {
8
9
  "local-memory-mcp": "dist/server.js",
9
10
  "mcp-memory-dashboard": "dist/dashboard/server.js"
10
11
  },
12
+ "files": [
13
+ "dist"
14
+ ],
11
15
  "author": "Muhammad Rheza Alfin <m.rheza.alfin@gmail.com>",
12
16
  "license": "MIT",
13
17
  "scripts": {
14
- "build": "tsc && mkdir -p dist/dashboard/public && rsync -a --delete src/dashboard/public/ dist/dashboard/public/",
18
+ "build": "tsc && mkdir -p dist/dashboard/public && rsync -a --delete src/dashboard/public/ dist/dashboard/public/ && shx chmod +x dist/server.js dist/dashboard/server.js",
19
+ "prepare": "npm run build",
15
20
  "dev": "tsc --watch",
16
21
  "start": "node dist/server.js",
17
22
  "dashboard": "node dist/dashboard/server.js",
@@ -31,6 +36,7 @@
31
36
  "@types/express": "^5.0.6",
32
37
  "@types/node": "^22.19.7",
33
38
  "fast-check": "^4.6.0",
39
+ "shx": "^0.4.0",
34
40
  "typescript": "^5.7.3",
35
41
  "vitest": "^4.1.1"
36
42
  }
@@ -1 +0,0 @@
1
- {"specId": "a9aa2a01-bac5-484c-8b95-7f5536bca00e", "workflowType": "requirements-first", "specType": "feature"}
@@ -1,27 +0,0 @@
1
- {
2
- "version": "2.0.0",
3
- "tasks": [
4
- {
5
- "label": "Start Dashboard",
6
- "type": "shell",
7
- "command": "npm",
8
- "args": ["run", "dashboard"],
9
- "options": {
10
- "cwd": "/home/vheins/Projects/local-memory-mcp"
11
- },
12
- "group": "build",
13
- "presentation": {
14
- "echo": true,
15
- "reveal": "silent",
16
- "focus": false,
17
- "panel": "shared",
18
- "showReuseMessage": true,
19
- "clear": false
20
- },
21
- "isBackground": true,
22
- "runOptions": {
23
- "runOn": "folderOpen"
24
- }
25
- }
26
- ]
27
- }