@abarcenas/ai-workflow-template 1.44.0 → 1.46.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/.agents/config/README.md +43 -0
- package/.agents/config/domain.md +37 -0
- package/.agents/instructions/agent.instructions.md +10 -10
- package/.agents/instructions/learned-knowledge.instructions.md +170 -0
- package/.agents/instructions/memory-bank.instructions.md +11 -0
- package/.agents/skills/caveman-compress/README.md +2 -0
- package/.agents/skills/caveman-compress/SECURITY.md +3 -1
- package/.agents/skills/caveman-compress/SKILL.md +4 -0
- package/.agents/skills/caveman-compress/scripts/compress.py +9 -2
- package/.agents/skills/compress/SKILL.md +4 -0
- package/.agents/skills/compress/scripts/compress.py +9 -2
- package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/.agents/skills/domain-modeling/GLOSSARY-FORMAT.md +60 -0
- package/.agents/skills/domain-modeling/SKILL.md +74 -0
- package/.agents/skills/grill-me/SKILL.md +7 -0
- package/.agents/skills/grill-with-docs/SKILL.md +7 -0
- package/.agents/skills/grilling/SKILL.md +28 -0
- package/.agents/skills/prototype/LOGIC.md +67 -0
- package/.agents/skills/prototype/SKILL.md +26 -0
- package/.agents/skills/prototype/UI.md +112 -0
- package/.github/workflows/npm-publish.yml +10 -0
- package/.github/workflows/pr-test.yml +31 -0
- package/.opencode/agents/architect.agent.md +0 -1
- package/.opencode/agents/coder.agent.md +0 -1
- package/.opencode/agents/deployer.agent.md +0 -1
- package/.opencode/agents/designer.agent.md +0 -1
- package/.opencode/agents/e2e-tester.agent.md +0 -1
- package/.opencode/agents/implementer.agent.md +0 -1
- package/.opencode/agents/orchestrator/feature-pipeline.agent.md +0 -1
- package/.opencode/agents/orchestrator/orchestrator.agent.md +68 -4
- package/.opencode/agents/orchestrator/tdd-orchestrator.agent.md +0 -1
- package/.opencode/agents/plan.agent.md +0 -1
- package/.opencode/agents/researcher.agent.md +0 -1
- package/.opencode/agents/reviewer.agent.md +0 -1
- package/.opencode/agents/scope.agent.md +299 -0
- package/.opencode/agents/tracker.agent.md +0 -1
- package/.opencode/agents/unit-tester.agent.md +0 -1
- package/AGENTS.md +44 -5
- package/README.md +6 -3
- package/package.json +1 -1
- package/scripts/bump-version.js +7 -0
- package/scripts/bump-version.test.js +238 -0
- package/scripts/mcp/playwright-mcp-launcher.js +55 -29
- package/scripts/mcp/playwright-mcp-launcher.test.js +217 -0
- package/scripts/mcp-memory-server.js +93 -76
- package/scripts/mcp-memory-server.test.js +398 -0
- package/scripts/memory-cli.test.js +297 -0
- package/scripts/memory-index.js +119 -31
- package/scripts/memory-index.test.js +1173 -0
- package/scripts/normalize-memory.js +26 -1
- package/scripts/normalize-memory.test.js +753 -0
- package/scripts/setup/discover.test.js +124 -2
- package/scripts/setup/hooks.test.js +188 -1
- package/scripts/setup/husky-init.test.js +311 -0
- package/scripts/setup/index.js +35 -5
- package/scripts/setup/index.test.js +279 -0
- package/scripts/setup/knowledgebase.test.js +100 -0
- package/scripts/setup/skills-phase.js +568 -0
- package/scripts/setup/skills-phase.test.js +1274 -0
- package/scripts/setup/sync-phase.js +6 -1
- package/scripts/setup/sync-phase.test.js +180 -2
- package/scripts/setup/ui.test.js +579 -0
- package/scripts/setup/utils.test.js +586 -0
- package/scripts/sync-unit.test.js +1342 -0
- package/scripts/sync.js +540 -246
- package/scripts/sync.test.js +283 -0
- package/scripts/vocab-sync.test.js +88 -0
- package/skills-lock.json +90 -0
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
CallToolRequestSchema,
|
|
21
21
|
ListToolsRequestSchema,
|
|
22
22
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
23
|
+
import { resolve } from 'path';
|
|
24
|
+
import { fileURLToPath } from 'url';
|
|
23
25
|
import { search, rebuildIndex, incrementalUpdate, getFile, getStats } from './memory-index.js';
|
|
24
26
|
|
|
25
27
|
const VALID_FILES = [
|
|
@@ -43,88 +45,92 @@ const server = new Server(
|
|
|
43
45
|
|
|
44
46
|
// ── List tools ───────────────────────────────────────────────────────
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
48
|
+
export async function listTools() {
|
|
49
|
+
return {
|
|
50
|
+
tools: [
|
|
51
|
+
{
|
|
52
|
+
name: 'memory_search',
|
|
53
|
+
description:
|
|
54
|
+
'Semantic search across the project memory bank (activeContext, systemPatterns, progress, decisions, tasks). ' +
|
|
55
|
+
'Returns ranked chunks with source file and similarity score. ' +
|
|
56
|
+
'Use this instead of reading all memory files to save tokens.',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
query: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
description: 'Natural language query about project context, decisions, or status',
|
|
63
|
+
},
|
|
64
|
+
topK: {
|
|
65
|
+
type: 'number',
|
|
66
|
+
default: 5,
|
|
67
|
+
description: 'Number of results to return (1-20)',
|
|
68
|
+
},
|
|
69
|
+
sourceFile: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
description: 'Optional: filter to a specific memory file (e.g., "activeContext", "systemPatterns")',
|
|
72
|
+
},
|
|
69
73
|
},
|
|
74
|
+
required: ['query'],
|
|
70
75
|
},
|
|
71
|
-
required: ['query'],
|
|
72
76
|
},
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
{
|
|
78
|
+
name: 'memory_rebuild',
|
|
79
|
+
description:
|
|
80
|
+
'Full rebuild of the memory vector index from memory-bank/*.md and docs/*.md files. ' +
|
|
81
|
+
'Re-indexes every file regardless of change. ' +
|
|
82
|
+
'Use memory_update instead for faster incremental sync (only changed files). ' +
|
|
83
|
+
'The index is disposable — rebuilding is safe.',
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {},
|
|
87
|
+
},
|
|
84
88
|
},
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
{
|
|
90
|
+
name: 'memory_update',
|
|
91
|
+
description:
|
|
92
|
+
'Incremental update — only re-indexes files whose content changed (SHA-256 content hash). ' +
|
|
93
|
+
'Much faster than memory_rebuild for daily use (~2 seconds vs minutes). ' +
|
|
94
|
+
'Run this after completing tasks, writing to memory-bank, or after git pull.',
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {},
|
|
98
|
+
},
|
|
95
99
|
},
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
100
|
+
{
|
|
101
|
+
name: 'memory_get',
|
|
102
|
+
description:
|
|
103
|
+
'Read the full content of a specific memory file by name. ' +
|
|
104
|
+
'Use when you need the complete file, not just search snippets.',
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: {
|
|
108
|
+
file: {
|
|
109
|
+
type: 'string',
|
|
110
|
+
description: 'File ID without extension. One of: ' + VALID_FILES.join(', '),
|
|
111
|
+
enum: VALID_FILES,
|
|
112
|
+
},
|
|
109
113
|
},
|
|
114
|
+
required: ['file'],
|
|
110
115
|
},
|
|
111
|
-
required: ['file'],
|
|
112
116
|
},
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
{
|
|
118
|
+
name: 'memory_stats',
|
|
119
|
+
description: 'Get statistics about the memory index (vector count, file count, database size).',
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {},
|
|
123
|
+
},
|
|
120
124
|
},
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
server.setRequestHandler(ListToolsRequestSchema, listTools);
|
|
124
130
|
|
|
125
131
|
// ── Handle tool calls ────────────────────────────────────────────────
|
|
126
132
|
|
|
127
|
-
|
|
133
|
+
export async function handleToolCall(request) {
|
|
128
134
|
const { name, arguments: args } = request.params;
|
|
129
135
|
|
|
130
136
|
try {
|
|
@@ -227,17 +233,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
227
233
|
isError: true,
|
|
228
234
|
};
|
|
229
235
|
}
|
|
230
|
-
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
server.setRequestHandler(CallToolRequestSchema, handleToolCall);
|
|
231
239
|
|
|
232
240
|
// ── Start server ─────────────────────────────────────────────────────
|
|
233
241
|
|
|
234
|
-
async function main() {
|
|
242
|
+
export async function main() {
|
|
235
243
|
const transport = new StdioServerTransport();
|
|
236
244
|
await server.connect(transport);
|
|
237
245
|
console.error('[memory-bank-mcp] Server started via stdio transport');
|
|
238
246
|
}
|
|
239
247
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
248
|
+
export function bootstrap() {
|
|
249
|
+
return main().catch((err) => {
|
|
250
|
+
console.error('[memory-bank-mcp] Fatal error:', err);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const isDirectRun =
|
|
256
|
+
process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
257
|
+
|
|
258
|
+
if (isDirectRun) {
|
|
259
|
+
bootstrap();
|
|
260
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// mcp-memory-server.test.js — Unit tests for the memory-bank MCP server
|
|
3
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// The stdio bootstrap is direct-run guarded, so importing the module in tests
|
|
6
|
+
// does not open a transport. The MCP SDK and the index engine are mocked:
|
|
7
|
+
// tests exercise the exported `listTools` / `handleToolCall` handlers, the
|
|
8
|
+
// formatting of every tool response, error propagation, and the start-up
|
|
9
|
+
// (`main` / `bootstrap`) paths — including the fatal path.
|
|
10
|
+
//
|
|
11
|
+
// Covered scenarios:
|
|
12
|
+
// 1. listTools exposes all five tools with schemas
|
|
13
|
+
// 2. memory_search formats ranked results
|
|
14
|
+
// 3. memory_search defaults (topK=5, no source filter)
|
|
15
|
+
// 4. memory_search honours explicit topK/sourceFile
|
|
16
|
+
// 5. memory_search empty index message
|
|
17
|
+
// 6. memory_search error → isError response
|
|
18
|
+
// 7. memory_rebuild formats counts and stats
|
|
19
|
+
// 8. memory_update with changed files + stats
|
|
20
|
+
// 9. memory_update with no changes
|
|
21
|
+
// 10. memory_get returns content
|
|
22
|
+
// 11. memory_get missing file → available-files hint
|
|
23
|
+
// 12. memory_stats formats the index statistics
|
|
24
|
+
// 13. unknown tool → caught error response
|
|
25
|
+
// 14. main() connects a stdio transport and logs start-up
|
|
26
|
+
// 15. bootstrap() exits 1 when the transport fails
|
|
27
|
+
// 16. direct run auto-bootstraps; empty argv[1] does not
|
|
28
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
29
|
+
|
|
30
|
+
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
31
|
+
import { fileURLToPath } from 'url'
|
|
32
|
+
|
|
33
|
+
const sdk = vi.hoisted(() => ({ connect: vi.fn() }))
|
|
34
|
+
|
|
35
|
+
vi.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
|
|
36
|
+
Server: class {
|
|
37
|
+
setRequestHandler() {}
|
|
38
|
+
connect(transport) {
|
|
39
|
+
return sdk.connect(transport)
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
|
|
45
|
+
StdioServerTransport: class {
|
|
46
|
+
constructor() {
|
|
47
|
+
this.kind = 'stdio'
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
}))
|
|
51
|
+
|
|
52
|
+
vi.mock('@modelcontextprotocol/sdk/types.js', () => ({
|
|
53
|
+
CallToolRequestSchema: 'call-tool',
|
|
54
|
+
ListToolsRequestSchema: 'list-tools',
|
|
55
|
+
}))
|
|
56
|
+
|
|
57
|
+
vi.mock('./memory-index.js', () => ({
|
|
58
|
+
search: vi.fn(),
|
|
59
|
+
rebuildIndex: vi.fn(),
|
|
60
|
+
incrementalUpdate: vi.fn(),
|
|
61
|
+
getFile: vi.fn(),
|
|
62
|
+
getStats: vi.fn(),
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Builds an MCP tool-call request as the SDK delivers it.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} name
|
|
69
|
+
* @param {object} [args]
|
|
70
|
+
* @returns {{params: {name: string, arguments: object}}}
|
|
71
|
+
*/
|
|
72
|
+
function buildRequest(name, args = {}) {
|
|
73
|
+
return { params: { name, arguments: args } }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sampleResults = [
|
|
77
|
+
{
|
|
78
|
+
score: 0.91,
|
|
79
|
+
source_file: 'activeContext',
|
|
80
|
+
content: 'Current focus is the coverage push.',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
score: 0.72,
|
|
84
|
+
source_file: 'progress',
|
|
85
|
+
content: 'All five scripts now have tests.',
|
|
86
|
+
},
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
describe('mcp-memory-server', () => {
|
|
90
|
+
let mod
|
|
91
|
+
let index
|
|
92
|
+
let consoleErrorSpy
|
|
93
|
+
let exitSpy
|
|
94
|
+
let originalArgv1
|
|
95
|
+
|
|
96
|
+
beforeEach(async () => {
|
|
97
|
+
vi.resetModules()
|
|
98
|
+
vi.clearAllMocks()
|
|
99
|
+
originalArgv1 = process.argv[1]
|
|
100
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
101
|
+
exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {})
|
|
102
|
+
mod = await import('./mcp-memory-server.js')
|
|
103
|
+
index = await import('./memory-index.js')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
afterEach(() => {
|
|
107
|
+
process.argv[1] = originalArgv1
|
|
108
|
+
consoleErrorSpy.mockRestore()
|
|
109
|
+
exitSpy.mockRestore()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// ── 1. listTools ─────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
it('listTools: exposes the five memory-bank tools with schemas', async () => {
|
|
115
|
+
const result = await mod.listTools()
|
|
116
|
+
|
|
117
|
+
expect(result.tools.map((tool) => tool.name)).toEqual([
|
|
118
|
+
'memory_search',
|
|
119
|
+
'memory_rebuild',
|
|
120
|
+
'memory_update',
|
|
121
|
+
'memory_get',
|
|
122
|
+
'memory_stats',
|
|
123
|
+
])
|
|
124
|
+
|
|
125
|
+
const search = result.tools.find((tool) => tool.name === 'memory_search')
|
|
126
|
+
expect(search.inputSchema.required).toEqual(['query'])
|
|
127
|
+
expect(search.inputSchema.properties.topK.default).toBe(5)
|
|
128
|
+
|
|
129
|
+
const get = result.tools.find((tool) => tool.name === 'memory_get')
|
|
130
|
+
expect(get.inputSchema.required).toEqual(['file'])
|
|
131
|
+
expect(get.inputSchema.properties.file.enum).toContain('activeContext')
|
|
132
|
+
expect(get.inputSchema.properties.file.enum).toContain(
|
|
133
|
+
'playwright-mcp-configuration',
|
|
134
|
+
)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// ── 2. memory_search formatting ──────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
it('memory_search: formats ranked results with scores and source files', async () => {
|
|
140
|
+
index.search.mockResolvedValue(sampleResults)
|
|
141
|
+
|
|
142
|
+
const response = await mod.handleToolCall(
|
|
143
|
+
buildRequest('memory_search', { query: 'what is the focus?' }),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
expect(index.search).toHaveBeenCalledWith('what is the focus?', {
|
|
147
|
+
topK: 5,
|
|
148
|
+
sourceFile: undefined,
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const text = response.content[0].text
|
|
152
|
+
expect(text).toContain('### Result 1 (score: 0.91)')
|
|
153
|
+
expect(text).toContain('**File:** `activeContext`')
|
|
154
|
+
expect(text).toContain('Current focus is the coverage push.')
|
|
155
|
+
expect(text).toContain('### Result 2 (score: 0.72)')
|
|
156
|
+
expect(text).toContain('\n\n---\n\n')
|
|
157
|
+
expect(response.isError).toBeUndefined()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// ── 3/4. memory_search options ───────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
it('memory_search: defaults topK to 5 when omitted', async () => {
|
|
163
|
+
index.search.mockResolvedValue([])
|
|
164
|
+
|
|
165
|
+
await mod.handleToolCall(buildRequest('memory_search', { query: 'x' }))
|
|
166
|
+
|
|
167
|
+
expect(index.search).toHaveBeenCalledWith('x', {
|
|
168
|
+
topK: 5,
|
|
169
|
+
sourceFile: undefined,
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('memory_search: forwards explicit topK and sourceFile', async () => {
|
|
174
|
+
index.search.mockResolvedValue(sampleResults)
|
|
175
|
+
|
|
176
|
+
await mod.handleToolCall(
|
|
177
|
+
buildRequest('memory_search', {
|
|
178
|
+
query: 'focus',
|
|
179
|
+
topK: 2,
|
|
180
|
+
sourceFile: 'activeContext',
|
|
181
|
+
}),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
expect(index.search).toHaveBeenCalledWith('focus', {
|
|
185
|
+
topK: 2,
|
|
186
|
+
sourceFile: 'activeContext',
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
// ── 5. Empty results ─────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
it('memory_search: returns a rebuild hint when the index is empty', async () => {
|
|
193
|
+
index.search.mockResolvedValue([])
|
|
194
|
+
|
|
195
|
+
const response = await mod.handleToolCall(
|
|
196
|
+
buildRequest('memory_search', { query: 'nothing' }),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
expect(response.content[0].text).toBe(
|
|
200
|
+
'No results found. The index may be empty. Try running memory_rebuild first.',
|
|
201
|
+
)
|
|
202
|
+
expect(response.isError).toBeUndefined()
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
// ── 6. Error propagation ─────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
it('memory_search: converts engine errors into an isError response', async () => {
|
|
208
|
+
index.search.mockRejectedValue(new Error('vector store offline'))
|
|
209
|
+
|
|
210
|
+
const response = await mod.handleToolCall(
|
|
211
|
+
buildRequest('memory_search', { query: 'x' }),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
expect(response.isError).toBe(true)
|
|
215
|
+
expect(response.content[0].text).toBe('Error: vector store offline')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
// ── 7. memory_rebuild ────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
it('memory_rebuild: formats indexed/skipped counts and stats', async () => {
|
|
221
|
+
index.rebuildIndex.mockResolvedValue({
|
|
222
|
+
indexed: 12,
|
|
223
|
+
skipped: 3,
|
|
224
|
+
stats: { totalVectors: 42, totalFiles: 8, dbSize: '512 kB' },
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
const response = await mod.handleToolCall(
|
|
228
|
+
buildRequest('memory_rebuild', {}),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
const text = response.content[0].text
|
|
232
|
+
expect(text).toContain('Index rebuilt successfully.')
|
|
233
|
+
expect(text).toContain('- Chunks indexed: 12')
|
|
234
|
+
expect(text).toContain('- Chunks skipped (unchanged): 3')
|
|
235
|
+
expect(text).toContain('- Total vectors: 42')
|
|
236
|
+
expect(text).toContain('- Total files: 8')
|
|
237
|
+
expect(text).toContain('- DB size: 512 kB')
|
|
238
|
+
expect(response.isError).toBeUndefined()
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// ── 8. memory_update with changes ────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
it('memory_update: lists changed files and the new vector total', async () => {
|
|
244
|
+
index.incrementalUpdate.mockResolvedValue({
|
|
245
|
+
indexed: 4,
|
|
246
|
+
skipped: 1,
|
|
247
|
+
changed: ['activeContext.md', 'progress.md'],
|
|
248
|
+
})
|
|
249
|
+
index.getStats.mockReturnValue({ totalVectors: 100 })
|
|
250
|
+
|
|
251
|
+
const response = await mod.handleToolCall(
|
|
252
|
+
buildRequest('memory_update', {}),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
const text = response.content[0].text
|
|
256
|
+
expect(text).toContain('Index updated incrementally.')
|
|
257
|
+
expect(text).toContain('- Chunks re-indexed: 4')
|
|
258
|
+
expect(text).toContain('- Chunks cached (unchanged): 1')
|
|
259
|
+
expect(text).toContain('- Changed files: activeContext.md, progress.md')
|
|
260
|
+
expect(text).toContain('- Total vectors: 100')
|
|
261
|
+
expect(response.isError).toBeUndefined()
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
// ── 9. memory_update with no changes ────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
it('memory_update: reports "No files changed." when nothing changed', async () => {
|
|
267
|
+
index.incrementalUpdate.mockResolvedValue({
|
|
268
|
+
indexed: 0,
|
|
269
|
+
skipped: 7,
|
|
270
|
+
changed: [],
|
|
271
|
+
})
|
|
272
|
+
index.getStats.mockReturnValue({ totalVectors: 100 })
|
|
273
|
+
|
|
274
|
+
const response = await mod.handleToolCall(
|
|
275
|
+
buildRequest('memory_update', {}),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
expect(response.content[0].text).toContain('- No files changed.')
|
|
279
|
+
expect(response.content[0].text).not.toContain('- Changed files:')
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
// ── 10. memory_get ───────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
it('memory_get: returns the full file content', async () => {
|
|
285
|
+
index.getFile.mockReturnValue('# Active Context\n\nWorking.')
|
|
286
|
+
|
|
287
|
+
const response = await mod.handleToolCall(
|
|
288
|
+
buildRequest('memory_get', { file: 'activeContext' }),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
expect(index.getFile).toHaveBeenCalledWith('activeContext')
|
|
292
|
+
expect(response.content[0].text).toBe('# Active Context\n\nWorking.')
|
|
293
|
+
expect(response.isError).toBeUndefined()
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
// ── 11. memory_get missing file ─────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
it('memory_get: lists available files when the file does not exist', async () => {
|
|
299
|
+
index.getFile.mockReturnValue(undefined)
|
|
300
|
+
|
|
301
|
+
const response = await mod.handleToolCall(
|
|
302
|
+
buildRequest('memory_get', { file: 'nope' }),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
const text = response.content[0].text
|
|
306
|
+
expect(text).toContain('File not found: nope.md.')
|
|
307
|
+
expect(text).toContain('Available files: activeContext,')
|
|
308
|
+
expect(response.isError).toBeUndefined()
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
// ── 12. memory_stats ─────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
it('memory_stats: formats vector, file, and size statistics', async () => {
|
|
314
|
+
index.getStats.mockReturnValue({
|
|
315
|
+
totalVectors: 321,
|
|
316
|
+
totalFiles: 17,
|
|
317
|
+
dbSize: '1.2 MB',
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const response = await mod.handleToolCall(
|
|
321
|
+
buildRequest('memory_stats', {}),
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
const text = response.content[0].text
|
|
325
|
+
expect(text).toContain('**Memory Index Stats**')
|
|
326
|
+
expect(text).toContain('- Total vectors: 321')
|
|
327
|
+
expect(text).toContain('- Total files indexed: 17')
|
|
328
|
+
expect(text).toContain('- Database size: 1.2 MB')
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
// ── 13. Unknown tool ─────────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
it('unknown tool: returns a caught error response', async () => {
|
|
334
|
+
const response = await mod.handleToolCall(
|
|
335
|
+
buildRequest('memory_bogus', {}),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
expect(response.isError).toBe(true)
|
|
339
|
+
expect(response.content[0].text).toBe('Error: Unknown tool: memory_bogus')
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
// ── 14. main() ───────────────────────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
it('main: connects a stdio transport and logs start-up', async () => {
|
|
345
|
+
sdk.connect.mockResolvedValue(undefined)
|
|
346
|
+
|
|
347
|
+
await mod.main()
|
|
348
|
+
|
|
349
|
+
expect(sdk.connect).toHaveBeenCalledWith(
|
|
350
|
+
expect.objectContaining({ kind: 'stdio' }),
|
|
351
|
+
)
|
|
352
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
353
|
+
'[memory-bank-mcp] Server started via stdio transport',
|
|
354
|
+
)
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
// ── 15. bootstrap() fatal path ───────────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
it('bootstrap: logs the fatal error and exits 1 when connect fails', async () => {
|
|
360
|
+
const err = new Error('transport exploded')
|
|
361
|
+
sdk.connect.mockRejectedValue(err)
|
|
362
|
+
|
|
363
|
+
await mod.bootstrap()
|
|
364
|
+
|
|
365
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
366
|
+
'[memory-bank-mcp] Fatal error:',
|
|
367
|
+
err,
|
|
368
|
+
)
|
|
369
|
+
expect(exitSpy).toHaveBeenCalledWith(1)
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
// ── 16. Direct-run guard ─────────────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
it('direct run: auto-bootstraps when argv[1] is this module', async () => {
|
|
375
|
+
const modulePath = fileURLToPath(
|
|
376
|
+
new URL('./mcp-memory-server.js', import.meta.url),
|
|
377
|
+
)
|
|
378
|
+
process.argv[1] = modulePath
|
|
379
|
+
sdk.connect.mockResolvedValue(undefined)
|
|
380
|
+
vi.resetModules()
|
|
381
|
+
|
|
382
|
+
await import('./mcp-memory-server.js')
|
|
383
|
+
|
|
384
|
+
await vi.waitFor(() => expect(sdk.connect).toHaveBeenCalledTimes(1))
|
|
385
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
386
|
+
'[memory-bank-mcp] Server started via stdio transport',
|
|
387
|
+
)
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
it('imported with an empty argv[1]: does not start the server', async () => {
|
|
391
|
+
process.argv[1] = ''
|
|
392
|
+
vi.resetModules()
|
|
393
|
+
|
|
394
|
+
await import('./mcp-memory-server.js')
|
|
395
|
+
|
|
396
|
+
expect(sdk.connect).not.toHaveBeenCalled()
|
|
397
|
+
})
|
|
398
|
+
})
|