@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.
Files changed (68) hide show
  1. package/.agents/config/README.md +43 -0
  2. package/.agents/config/domain.md +37 -0
  3. package/.agents/instructions/agent.instructions.md +10 -10
  4. package/.agents/instructions/learned-knowledge.instructions.md +170 -0
  5. package/.agents/instructions/memory-bank.instructions.md +11 -0
  6. package/.agents/skills/caveman-compress/README.md +2 -0
  7. package/.agents/skills/caveman-compress/SECURITY.md +3 -1
  8. package/.agents/skills/caveman-compress/SKILL.md +4 -0
  9. package/.agents/skills/caveman-compress/scripts/compress.py +9 -2
  10. package/.agents/skills/compress/SKILL.md +4 -0
  11. package/.agents/skills/compress/scripts/compress.py +9 -2
  12. package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
  13. package/.agents/skills/domain-modeling/GLOSSARY-FORMAT.md +60 -0
  14. package/.agents/skills/domain-modeling/SKILL.md +74 -0
  15. package/.agents/skills/grill-me/SKILL.md +7 -0
  16. package/.agents/skills/grill-with-docs/SKILL.md +7 -0
  17. package/.agents/skills/grilling/SKILL.md +28 -0
  18. package/.agents/skills/prototype/LOGIC.md +67 -0
  19. package/.agents/skills/prototype/SKILL.md +26 -0
  20. package/.agents/skills/prototype/UI.md +112 -0
  21. package/.github/workflows/npm-publish.yml +10 -0
  22. package/.github/workflows/pr-test.yml +31 -0
  23. package/.opencode/agents/architect.agent.md +0 -1
  24. package/.opencode/agents/coder.agent.md +0 -1
  25. package/.opencode/agents/deployer.agent.md +0 -1
  26. package/.opencode/agents/designer.agent.md +0 -1
  27. package/.opencode/agents/e2e-tester.agent.md +0 -1
  28. package/.opencode/agents/implementer.agent.md +0 -1
  29. package/.opencode/agents/orchestrator/feature-pipeline.agent.md +0 -1
  30. package/.opencode/agents/orchestrator/orchestrator.agent.md +68 -4
  31. package/.opencode/agents/orchestrator/tdd-orchestrator.agent.md +0 -1
  32. package/.opencode/agents/plan.agent.md +0 -1
  33. package/.opencode/agents/researcher.agent.md +0 -1
  34. package/.opencode/agents/reviewer.agent.md +0 -1
  35. package/.opencode/agents/scope.agent.md +299 -0
  36. package/.opencode/agents/tracker.agent.md +0 -1
  37. package/.opencode/agents/unit-tester.agent.md +0 -1
  38. package/AGENTS.md +44 -5
  39. package/README.md +6 -3
  40. package/package.json +1 -1
  41. package/scripts/bump-version.js +7 -0
  42. package/scripts/bump-version.test.js +238 -0
  43. package/scripts/mcp/playwright-mcp-launcher.js +55 -29
  44. package/scripts/mcp/playwright-mcp-launcher.test.js +217 -0
  45. package/scripts/mcp-memory-server.js +93 -76
  46. package/scripts/mcp-memory-server.test.js +398 -0
  47. package/scripts/memory-cli.test.js +297 -0
  48. package/scripts/memory-index.js +119 -31
  49. package/scripts/memory-index.test.js +1173 -0
  50. package/scripts/normalize-memory.js +26 -1
  51. package/scripts/normalize-memory.test.js +753 -0
  52. package/scripts/setup/discover.test.js +124 -2
  53. package/scripts/setup/hooks.test.js +188 -1
  54. package/scripts/setup/husky-init.test.js +311 -0
  55. package/scripts/setup/index.js +35 -5
  56. package/scripts/setup/index.test.js +279 -0
  57. package/scripts/setup/knowledgebase.test.js +100 -0
  58. package/scripts/setup/skills-phase.js +568 -0
  59. package/scripts/setup/skills-phase.test.js +1274 -0
  60. package/scripts/setup/sync-phase.js +6 -1
  61. package/scripts/setup/sync-phase.test.js +180 -2
  62. package/scripts/setup/ui.test.js +579 -0
  63. package/scripts/setup/utils.test.js +586 -0
  64. package/scripts/sync-unit.test.js +1342 -0
  65. package/scripts/sync.js +540 -246
  66. package/scripts/sync.test.js +283 -0
  67. package/scripts/vocab-sync.test.js +88 -0
  68. 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
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
47
- tools: [
48
- {
49
- name: 'memory_search',
50
- description:
51
- 'Semantic search across the project memory bank (activeContext, systemPatterns, progress, decisions, tasks). ' +
52
- 'Returns ranked chunks with source file and similarity score. ' +
53
- 'Use this instead of reading all memory files to save tokens.',
54
- inputSchema: {
55
- type: 'object',
56
- properties: {
57
- query: {
58
- type: 'string',
59
- description: 'Natural language query about project context, decisions, or status',
60
- },
61
- topK: {
62
- type: 'number',
63
- default: 5,
64
- description: 'Number of results to return (1-20)',
65
- },
66
- sourceFile: {
67
- type: 'string',
68
- description: 'Optional: filter to a specific memory file (e.g., "activeContext", "systemPatterns")',
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
- name: 'memory_rebuild',
76
- description:
77
- 'Full rebuild of the memory vector index from memory-bank/*.md and docs/*.md files. ' +
78
- 'Re-indexes every file regardless of change. ' +
79
- 'Use memory_update instead for faster incremental sync (only changed files). ' +
80
- 'The index is disposable — rebuilding is safe.',
81
- inputSchema: {
82
- type: 'object',
83
- properties: {},
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
- name: 'memory_update',
88
- description:
89
- 'Incremental update — only re-indexes files whose content changed (SHA-256 content hash). ' +
90
- 'Much faster than memory_rebuild for daily use (~2 seconds vs minutes). ' +
91
- 'Run this after completing tasks, writing to memory-bank, or after git pull.',
92
- inputSchema: {
93
- type: 'object',
94
- properties: {},
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
- name: 'memory_get',
99
- description:
100
- 'Read the full content of a specific memory file by name. ' +
101
- 'Use when you need the complete file, not just search snippets.',
102
- inputSchema: {
103
- type: 'object',
104
- properties: {
105
- file: {
106
- type: 'string',
107
- description: 'File ID without extension. One of: ' + VALID_FILES.join(', '),
108
- enum: VALID_FILES,
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
- name: 'memory_stats',
116
- description: 'Get statistics about the memory index (vector count, file count, database size).',
117
- inputSchema: {
118
- type: 'object',
119
- properties: {},
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
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
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
- main().catch((err) => {
241
- console.error('[memory-bank-mcp] Fatal error:', err);
242
- process.exit(1);
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
+ })