mcp-memory-keeper 0.10.2 → 0.12.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/CHANGELOG.md +46 -1
- package/README.md +86 -0
- package/dist/__tests__/integration/git-integration.test.js +3 -0
- package/dist/__tests__/integration/issue-token-limit-channel-query.test.js +128 -0
- package/dist/__tests__/integration/project-directory.test.js +6 -0
- package/dist/__tests__/integration/tool-profiles-integration.test.js +150 -0
- package/dist/__tests__/utils/project-directory-messages.test.js +3 -0
- package/dist/__tests__/utils/tool-profiles.test.js +374 -0
- package/dist/index.js +1075 -1052
- package/dist/utils/tool-profiles.js +242 -0
- package/examples/config.json +31 -0
- package/examples/project-directory-setup.md +114 -0
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.12.0] - 2026-02-06
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Selective Tool Filtering via Profiles** (#29)
|
|
15
|
+
- Control which tools are exposed to reduce context window usage (~10-15K tokens saved with minimal profile)
|
|
16
|
+
- Three built-in profiles: `minimal` (8 tools), `standard` (22 tools), `full` (38 tools, default)
|
|
17
|
+
- `TOOL_PROFILE` environment variable to select active profile at startup
|
|
18
|
+
- `TOOL_PROFILE_CONFIG` environment variable to specify custom config file path
|
|
19
|
+
- Custom profile definitions via `~/.mcp-memory-keeper/config.json`
|
|
20
|
+
- Config file profiles take precedence over built-in defaults
|
|
21
|
+
- Helpful error messages when disabled tools are called, with guidance on enabling them
|
|
22
|
+
- Startup logging shows active profile, tool count, and source
|
|
23
|
+
- Example config file included in `examples/config.json`
|
|
24
|
+
|
|
25
|
+
### Technical
|
|
26
|
+
|
|
27
|
+
- New `src/utils/tool-profiles.ts` module with `ALL_TOOL_NAMES` source of truth
|
|
28
|
+
- `ToolName` union type for compile-time safety
|
|
29
|
+
- Deep config validation (guards against malformed JSON, null values, non-array profiles, non-string elements)
|
|
30
|
+
- Drift-detection integration test verifies `ALL_TOOL_NAMES` stays in sync with `index.ts` tool definitions
|
|
31
|
+
- Defense-in-depth: both `ListTools` filtering and `CallTool` guard for disabled tools
|
|
32
|
+
- 100% backwards compatible — no env var + no config = all 38 tools (existing behavior unchanged)
|
|
33
|
+
- All 1185 tests passing across Node.js 20, 22, and 24
|
|
34
|
+
|
|
35
|
+
## [0.11.0] - 2025-12-10
|
|
36
|
+
|
|
37
|
+
### Breaking Changes
|
|
38
|
+
|
|
39
|
+
- **Node.js 18 support dropped** - Minimum required Node.js version is now 20.0.0
|
|
40
|
+
- Node.js 18 reached End-of-Life on April 30, 2025
|
|
41
|
+
- Users on Node.js 18 must upgrade to Node.js 20 or later
|
|
42
|
+
- Existing installations will continue working until updated
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **Installation fails on Node.js 24 (#28)** - Updated `better-sqlite3` dependency
|
|
47
|
+
- Upgraded from `^11.10.0` to `^12.1.0` to support Node.js 24 (LTS "Krypton")
|
|
48
|
+
- Prebuilt binaries now available for Node.js 20, 22, and 24
|
|
49
|
+
- Resolves `gyp ERR!` build failures on Node.js 24
|
|
50
|
+
|
|
10
51
|
## [0.10.2] - 2025-09-16
|
|
11
52
|
|
|
12
53
|
### Fixed
|
|
@@ -450,7 +491,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
450
491
|
- **Security**: Security updates
|
|
451
492
|
- **Technical**: Internal improvements
|
|
452
493
|
|
|
453
|
-
[Unreleased]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.
|
|
494
|
+
[Unreleased]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.12.0...HEAD
|
|
495
|
+
[0.12.0]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.11.0...v0.12.0
|
|
496
|
+
[0.11.0]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.10.2...v0.11.0
|
|
497
|
+
[0.10.2]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.10.1...v0.10.2
|
|
498
|
+
[0.10.1]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.10.0...v0.10.1
|
|
454
499
|
[0.10.0]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.9.0...v0.10.0
|
|
455
500
|
[0.9.0]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.8.4...v0.9.0
|
|
456
501
|
[0.8.4]: https://github.com/mkreyman/mcp-memory-keeper/compare/v0.8.3...v0.8.4
|
package/README.md
CHANGED
|
@@ -213,6 +213,92 @@ export MCP_MAX_ITEMS=50 # Fewer items per response
|
|
|
213
213
|
export MCP_CHARS_PER_TOKEN=3.0 # More conservative estimation (optional)
|
|
214
214
|
```
|
|
215
215
|
|
|
216
|
+
#### Tool Profiles
|
|
217
|
+
|
|
218
|
+
By default, all 38 tools are exposed. To reduce context overhead in your AI assistant, you can activate a tool profile that limits which tools are available.
|
|
219
|
+
|
|
220
|
+
**Quick usage:**
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# Essential tools only (8 tools)
|
|
224
|
+
TOOL_PROFILE=minimal npx mcp-memory-keeper
|
|
225
|
+
|
|
226
|
+
# Standard workflow set (22 tools)
|
|
227
|
+
TOOL_PROFILE=standard npx mcp-memory-keeper
|
|
228
|
+
|
|
229
|
+
# All tools (default)
|
|
230
|
+
TOOL_PROFILE=full npx mcp-memory-keeper
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Built-in profiles:**
|
|
234
|
+
|
|
235
|
+
| Profile | Tools | Description |
|
|
236
|
+
| ---------- | ----- | -------------------------------------------------------------- |
|
|
237
|
+
| `minimal` | 8 | Core persistence: save, get, search, status, checkpoint |
|
|
238
|
+
| `standard` | 22 | Daily workflow: core + git, batch ops, channels, export/import |
|
|
239
|
+
| `full` | 38 | All tools (default, backwards compatible) |
|
|
240
|
+
|
|
241
|
+
**Custom profiles via config file:**
|
|
242
|
+
|
|
243
|
+
Create `~/.mcp-memory-keeper/config.json` to define or override profiles:
|
|
244
|
+
|
|
245
|
+
```json
|
|
246
|
+
{
|
|
247
|
+
"profiles": {
|
|
248
|
+
"my_workflow": [
|
|
249
|
+
"context_session_start",
|
|
250
|
+
"context_save",
|
|
251
|
+
"context_get",
|
|
252
|
+
"context_search",
|
|
253
|
+
"context_checkpoint",
|
|
254
|
+
"context_restore_checkpoint",
|
|
255
|
+
"context_diff",
|
|
256
|
+
"context_timeline"
|
|
257
|
+
]
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Then activate it: `TOOL_PROFILE=my_workflow npx mcp-memory-keeper`
|
|
263
|
+
|
|
264
|
+
Config file profiles take precedence over built-in defaults with the same name.
|
|
265
|
+
|
|
266
|
+
**Profile resolution precedence:**
|
|
267
|
+
|
|
268
|
+
| `TOOL_PROFILE` | Config file has profile? | Built-in exists? | Result |
|
|
269
|
+
| -------------- | ------------------------ | ---------------- | -------------------------------- |
|
|
270
|
+
| Set | Yes | — | Uses config file definition |
|
|
271
|
+
| Set | No | Yes | Uses built-in definition |
|
|
272
|
+
| Set | No | No | Warning + falls back to `full` |
|
|
273
|
+
| Not set | — | — | Uses built-in `full` (all tools) |
|
|
274
|
+
|
|
275
|
+
**Environment variables:**
|
|
276
|
+
|
|
277
|
+
| Variable | Description |
|
|
278
|
+
| --------------------- | ------------------------------------------------------------------------- |
|
|
279
|
+
| `TOOL_PROFILE` | Profile name to activate (e.g., `minimal`, `standard`, `full`, or custom) |
|
|
280
|
+
| `TOOL_PROFILE_CONFIG` | Override config file path (default: `~/.mcp-memory-keeper/config.json`) |
|
|
281
|
+
|
|
282
|
+
> Note: Profile resolution happens once at server startup. Changes to the env var or config file take effect on the next server restart.
|
|
283
|
+
|
|
284
|
+
**Claude Code / Claude Desktop configuration:**
|
|
285
|
+
|
|
286
|
+
```json
|
|
287
|
+
{
|
|
288
|
+
"mcpServers": {
|
|
289
|
+
"memory-keeper": {
|
|
290
|
+
"command": "npx",
|
|
291
|
+
"args": ["mcp-memory-keeper"],
|
|
292
|
+
"env": {
|
|
293
|
+
"TOOL_PROFILE": "minimal"
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
See `examples/config.json` for a complete example config file.
|
|
301
|
+
|
|
216
302
|
### Claude Code (CLI)
|
|
217
303
|
|
|
218
304
|
#### Configuration Scopes
|
|
@@ -58,6 +58,9 @@ describe('Git Integration Tests', () => {
|
|
|
58
58
|
await git.init();
|
|
59
59
|
await git.addConfig('user.name', 'Test User');
|
|
60
60
|
await git.addConfig('user.email', 'test@example.com');
|
|
61
|
+
// Use repo-local hooks directory to prevent global hooks from interfering
|
|
62
|
+
const localHooksDir = path.join(tempRepoPath, '.git', 'hooks');
|
|
63
|
+
await git.addConfig('core.hooksPath', localHooksDir);
|
|
61
64
|
// Create initial commit
|
|
62
65
|
fs.writeFileSync(path.join(tempRepoPath, 'README.md'), '# Test Repo');
|
|
63
66
|
await git.add('.');
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Test for token limit issue with channel queries
|
|
4
|
+
*
|
|
5
|
+
* Reproduces the bug where context_get with limit: 50 still exceeds token limits
|
|
6
|
+
* when querying a channel with large items.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
const globals_1 = require("@jest/globals");
|
|
10
|
+
const database_js_1 = require("../../utils/database.js");
|
|
11
|
+
const RepositoryManager_js_1 = require("../../repositories/RepositoryManager.js");
|
|
12
|
+
const token_limits_js_1 = require("../../utils/token-limits.js");
|
|
13
|
+
(0, globals_1.describe)('Token Limit with Channel Query Bug', () => {
|
|
14
|
+
let dbManager;
|
|
15
|
+
let repositories;
|
|
16
|
+
let sessionId;
|
|
17
|
+
(0, globals_1.beforeEach)(() => {
|
|
18
|
+
dbManager = new database_js_1.DatabaseManager({ filename: ':memory:' });
|
|
19
|
+
repositories = new RepositoryManager_js_1.RepositoryManager(dbManager);
|
|
20
|
+
const session = repositories.sessions.create({
|
|
21
|
+
name: 'Test Session',
|
|
22
|
+
defaultChannel: 'test-channel',
|
|
23
|
+
});
|
|
24
|
+
sessionId = session.id;
|
|
25
|
+
});
|
|
26
|
+
(0, globals_1.afterEach)(() => {
|
|
27
|
+
dbManager.close();
|
|
28
|
+
});
|
|
29
|
+
(0, globals_1.it)('should enforce token limits even when limit parameter is provided', () => {
|
|
30
|
+
// Create 50 large context items (each ~1000 chars)
|
|
31
|
+
const largeValue = 'x'.repeat(1000);
|
|
32
|
+
for (let i = 0; i < 50; i++) {
|
|
33
|
+
repositories.contexts.save(sessionId, {
|
|
34
|
+
key: `large-item-${i}`,
|
|
35
|
+
value: largeValue,
|
|
36
|
+
channel: 'outbound-call-center',
|
|
37
|
+
priority: 'normal',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// Query with limit: 50
|
|
41
|
+
const result = repositories.contexts.queryEnhanced({
|
|
42
|
+
sessionId,
|
|
43
|
+
channel: 'outbound-call-center',
|
|
44
|
+
limit: 50,
|
|
45
|
+
sort: 'updated_desc',
|
|
46
|
+
includeMetadata: false,
|
|
47
|
+
});
|
|
48
|
+
(0, globals_1.expect)(result.items.length).toBe(50);
|
|
49
|
+
// Check if response would exceed token limit
|
|
50
|
+
const tokenConfig = (0, token_limits_js_1.getTokenConfig)();
|
|
51
|
+
const { exceedsLimit, safeItemCount } = (0, token_limits_js_1.checkTokenLimit)(result.items, false, tokenConfig);
|
|
52
|
+
// Build the actual response structure
|
|
53
|
+
const response = {
|
|
54
|
+
items: result.items,
|
|
55
|
+
pagination: {
|
|
56
|
+
total: result.totalCount,
|
|
57
|
+
returned: result.items.length,
|
|
58
|
+
offset: 0,
|
|
59
|
+
hasMore: false,
|
|
60
|
+
nextOffset: null,
|
|
61
|
+
truncated: false,
|
|
62
|
+
truncatedCount: 0,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const responseJson = JSON.stringify(response, null, 2);
|
|
66
|
+
const actualTokens = Math.ceil(responseJson.length / tokenConfig.charsPerToken);
|
|
67
|
+
// The bug: even with limit: 50, the response can exceed token limits
|
|
68
|
+
if (actualTokens > tokenConfig.mcpMaxTokens) {
|
|
69
|
+
// BUG REPRODUCED: The response exceeds token limits
|
|
70
|
+
// Verify that checkTokenLimit correctly detected the issue
|
|
71
|
+
(0, globals_1.expect)(exceedsLimit).toBe(true);
|
|
72
|
+
(0, globals_1.expect)(safeItemCount).toBeLessThan(50);
|
|
73
|
+
(0, globals_1.expect)(actualTokens).toBeGreaterThan(tokenConfig.mcpMaxTokens);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
(0, globals_1.it)('should respect token limits over user-provided limit parameter', () => {
|
|
77
|
+
// Create 100 large context items (each ~800 chars)
|
|
78
|
+
const largeValue = 'y'.repeat(800);
|
|
79
|
+
for (let i = 0; i < 100; i++) {
|
|
80
|
+
repositories.contexts.save(sessionId, {
|
|
81
|
+
key: `item-${i}`,
|
|
82
|
+
value: largeValue,
|
|
83
|
+
channel: 'test-channel',
|
|
84
|
+
priority: 'normal',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// Query with limit: 50 (user expectation)
|
|
88
|
+
const result = repositories.contexts.queryEnhanced({
|
|
89
|
+
sessionId,
|
|
90
|
+
channel: 'test-channel',
|
|
91
|
+
limit: 50,
|
|
92
|
+
sort: 'created_desc',
|
|
93
|
+
includeMetadata: false,
|
|
94
|
+
});
|
|
95
|
+
// Simulate the context_get handler logic
|
|
96
|
+
const tokenConfig = (0, token_limits_js_1.getTokenConfig)();
|
|
97
|
+
const { exceedsLimit, safeItemCount } = (0, token_limits_js_1.checkTokenLimit)(result.items, false, tokenConfig);
|
|
98
|
+
let actualItems = result.items;
|
|
99
|
+
let wasTruncated = false;
|
|
100
|
+
if (exceedsLimit && safeItemCount < result.items.length) {
|
|
101
|
+
actualItems = result.items.slice(0, safeItemCount);
|
|
102
|
+
wasTruncated = true;
|
|
103
|
+
}
|
|
104
|
+
// Build response
|
|
105
|
+
const response = {
|
|
106
|
+
items: actualItems,
|
|
107
|
+
pagination: {
|
|
108
|
+
total: result.totalCount,
|
|
109
|
+
returned: actualItems.length,
|
|
110
|
+
offset: 0,
|
|
111
|
+
hasMore: wasTruncated || actualItems.length < result.totalCount,
|
|
112
|
+
nextOffset: wasTruncated ? actualItems.length : null,
|
|
113
|
+
truncated: wasTruncated,
|
|
114
|
+
truncatedCount: wasTruncated ? result.items.length - actualItems.length : 0,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
const responseJson = JSON.stringify(response, null, 2);
|
|
118
|
+
const actualTokens = Math.ceil(responseJson.length / tokenConfig.charsPerToken);
|
|
119
|
+
// Verify token limit is not exceeded
|
|
120
|
+
(0, globals_1.expect)(actualTokens).toBeLessThanOrEqual(tokenConfig.mcpMaxTokens);
|
|
121
|
+
// Verify truncation occurred if needed
|
|
122
|
+
if (exceedsLimit) {
|
|
123
|
+
(0, globals_1.expect)(wasTruncated).toBe(true);
|
|
124
|
+
(0, globals_1.expect)(actualItems.length).toBeLessThan(50);
|
|
125
|
+
(0, globals_1.expect)(actualItems.length).toBe(safeItemCount);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -55,6 +55,9 @@ describe('Project Directory Feature Tests', () => {
|
|
|
55
55
|
await git.init();
|
|
56
56
|
await git.addConfig('user.name', 'Test User');
|
|
57
57
|
await git.addConfig('user.email', 'test@example.com');
|
|
58
|
+
// Use repo-local hooks directory to prevent global hooks from interfering
|
|
59
|
+
const localHooksDir = path.join(tempProjectPath, '.git', 'hooks');
|
|
60
|
+
await git.addConfig('core.hooksPath', localHooksDir);
|
|
58
61
|
// Create initial commit
|
|
59
62
|
fs.writeFileSync(path.join(tempProjectPath, 'README.md'), '# Test Project');
|
|
60
63
|
await git.add('.');
|
|
@@ -242,6 +245,9 @@ describe('Project Directory Feature Tests', () => {
|
|
|
242
245
|
// Configure git for this test to avoid CI failures
|
|
243
246
|
await git.addConfig('user.name', 'Test User');
|
|
244
247
|
await git.addConfig('user.email', 'test@example.com');
|
|
248
|
+
// Use repo-local hooks directory to prevent global hooks from interfering
|
|
249
|
+
const localHooksDir = path.join(pathWithSpaces, '.git', 'hooks');
|
|
250
|
+
await git.addConfig('core.hooksPath', localHooksDir);
|
|
245
251
|
try {
|
|
246
252
|
fs.writeFileSync(path.join(pathWithSpaces, 'test.txt'), 'content');
|
|
247
253
|
await git.add('.');
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const globals_1 = require("@jest/globals");
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const tool_profiles_1 = require("../../utils/tool-profiles");
|
|
40
|
+
/**
|
|
41
|
+
* Drift-detection: extract tool names from the ListToolsRequestSchema handler
|
|
42
|
+
* in src/index.ts to verify ALL_TOOL_NAMES stays in sync with actual tool definitions.
|
|
43
|
+
*/
|
|
44
|
+
function extractToolNamesFromIndexTs() {
|
|
45
|
+
const indexPath = path.join(__dirname, '..', '..', 'index.ts');
|
|
46
|
+
const src = fs.readFileSync(indexPath, 'utf-8');
|
|
47
|
+
// Find the allTools array: starts after "const allTools" and ends at the matching "];"
|
|
48
|
+
// We look for tool name strings inside the ListToolsRequestSchema handler
|
|
49
|
+
const toolNameRegex = /^\s+name:\s+'(context_[a-z_]+)'/gm;
|
|
50
|
+
const names = [];
|
|
51
|
+
let match;
|
|
52
|
+
// Only capture tool names outside of block comments (skip commented-out tools)
|
|
53
|
+
// Split by block comment boundaries and only scan non-comment sections
|
|
54
|
+
const sections = src.split(/\/\*[\s\S]*?\*\//);
|
|
55
|
+
for (const section of sections) {
|
|
56
|
+
toolNameRegex.lastIndex = 0;
|
|
57
|
+
while ((match = toolNameRegex.exec(section)) !== null) {
|
|
58
|
+
names.push(match[1]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return names;
|
|
62
|
+
}
|
|
63
|
+
(0, globals_1.describe)('Tool Profile Integration Tests', () => {
|
|
64
|
+
const originalEnv = process.env.TOOL_PROFILE;
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
if (originalEnv !== undefined) {
|
|
67
|
+
process.env.TOOL_PROFILE = originalEnv;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
delete process.env.TOOL_PROFILE;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
(0, globals_1.describe)('Drift detection: ALL_TOOL_NAMES vs index.ts', () => {
|
|
74
|
+
(0, globals_1.it)('ALL_TOOL_NAMES should match active tools defined in index.ts', () => {
|
|
75
|
+
const indexToolNames = extractToolNamesFromIndexTs();
|
|
76
|
+
const allToolNamesArray = [...tool_profiles_1.ALL_TOOL_NAMES];
|
|
77
|
+
// Same count
|
|
78
|
+
(0, globals_1.expect)(allToolNamesArray.length).toBe(indexToolNames.length);
|
|
79
|
+
// Same set of names
|
|
80
|
+
(0, globals_1.expect)(new Set(allToolNamesArray)).toEqual(new Set(indexToolNames));
|
|
81
|
+
});
|
|
82
|
+
(0, globals_1.it)('should not include commented-out tools', () => {
|
|
83
|
+
// context_share and context_get_shared are commented out in index.ts
|
|
84
|
+
(0, globals_1.expect)(tool_profiles_1.ALL_TOOL_NAMES_SET.has('context_share')).toBe(false);
|
|
85
|
+
(0, globals_1.expect)(tool_profiles_1.ALL_TOOL_NAMES_SET.has('context_get_shared')).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
(0, globals_1.describe)('Profile filtering behavior', () => {
|
|
89
|
+
(0, globals_1.it)('minimal profile should include core tools and exclude advanced tools', () => {
|
|
90
|
+
process.env.TOOL_PROFILE = 'minimal';
|
|
91
|
+
const profile = (0, tool_profiles_1.resolveActiveProfile)('/nonexistent/config.json');
|
|
92
|
+
// Core tools present
|
|
93
|
+
(0, globals_1.expect)(profile.tools.has('context_save')).toBe(true);
|
|
94
|
+
(0, globals_1.expect)(profile.tools.has('context_get')).toBe(true);
|
|
95
|
+
(0, globals_1.expect)(profile.tools.has('context_search')).toBe(true);
|
|
96
|
+
(0, globals_1.expect)(profile.tools.has('context_checkpoint')).toBe(true);
|
|
97
|
+
// Advanced tools absent
|
|
98
|
+
(0, globals_1.expect)(profile.tools.has('context_analyze')).toBe(false);
|
|
99
|
+
(0, globals_1.expect)(profile.tools.has('context_visualize')).toBe(false);
|
|
100
|
+
(0, globals_1.expect)(profile.tools.has('context_delegate')).toBe(false);
|
|
101
|
+
(0, globals_1.expect)(profile.tools.has('context_semantic_search')).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
(0, globals_1.it)('default (no env var) should expose all tools with backwards-compatible behavior', () => {
|
|
104
|
+
delete process.env.TOOL_PROFILE;
|
|
105
|
+
const profile = (0, tool_profiles_1.resolveActiveProfile)('/nonexistent/config.json');
|
|
106
|
+
(0, globals_1.expect)(profile.tools.size).toBe(tool_profiles_1.ALL_TOOL_NAMES.length);
|
|
107
|
+
(0, globals_1.expect)(profile.profileName).toBe('full');
|
|
108
|
+
(0, globals_1.expect)(profile.source).toBe('default');
|
|
109
|
+
(0, globals_1.expect)(profile.warnings).toHaveLength(0);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
(0, globals_1.describe)('CallTool guard behavior', () => {
|
|
113
|
+
(0, globals_1.it)('disabled tool should be in ALL_TOOL_NAMES_SET but not in profile tools', () => {
|
|
114
|
+
process.env.TOOL_PROFILE = 'minimal';
|
|
115
|
+
const profile = (0, tool_profiles_1.resolveActiveProfile)('/nonexistent/config.json');
|
|
116
|
+
const disabledTool = 'context_analyze';
|
|
117
|
+
// The guard logic: known tool that is not enabled
|
|
118
|
+
const isKnown = tool_profiles_1.ALL_TOOL_NAMES_SET.has(disabledTool);
|
|
119
|
+
const isEnabled = profile.tools.has(disabledTool);
|
|
120
|
+
(0, globals_1.expect)(isKnown).toBe(true);
|
|
121
|
+
(0, globals_1.expect)(isEnabled).toBe(false);
|
|
122
|
+
// In index.ts: isKnown && !isEnabled → return isError: true
|
|
123
|
+
});
|
|
124
|
+
(0, globals_1.it)('unknown tool should not be in ALL_TOOL_NAMES_SET (falls through to default switch)', () => {
|
|
125
|
+
const unknownTool = 'non_existent_tool';
|
|
126
|
+
(0, globals_1.expect)(tool_profiles_1.ALL_TOOL_NAMES_SET.has(unknownTool)).toBe(false);
|
|
127
|
+
// In index.ts: !isKnown → falls through to default: throw new Error()
|
|
128
|
+
});
|
|
129
|
+
(0, globals_1.it)('enabled tool should pass both checks', () => {
|
|
130
|
+
process.env.TOOL_PROFILE = 'minimal';
|
|
131
|
+
const profile = (0, tool_profiles_1.resolveActiveProfile)('/nonexistent/config.json');
|
|
132
|
+
const enabledTool = 'context_save';
|
|
133
|
+
const isKnown = tool_profiles_1.ALL_TOOL_NAMES_SET.has(enabledTool);
|
|
134
|
+
const isEnabled = profile.tools.has(enabledTool);
|
|
135
|
+
(0, globals_1.expect)(isKnown).toBe(true);
|
|
136
|
+
(0, globals_1.expect)(isEnabled).toBe(true);
|
|
137
|
+
// In index.ts: isKnown && isEnabled → guard does not fire, proceeds to switch
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
(0, globals_1.describe)('TOOL_PROFILE_CONFIG support', () => {
|
|
141
|
+
(0, globals_1.it)('resolveActiveProfile accepts custom config path (used by TOOL_PROFILE_CONFIG)', () => {
|
|
142
|
+
// This tests the mechanism that index.ts uses:
|
|
143
|
+
// resolveActiveProfile(process.env.TOOL_PROFILE_CONFIG)
|
|
144
|
+
const result = (0, tool_profiles_1.resolveActiveProfile)('/nonexistent/custom/path/config.json');
|
|
145
|
+
// Missing file → no config → falls back to built-in 'full'
|
|
146
|
+
(0, globals_1.expect)(result.profileName).toBe('full');
|
|
147
|
+
(0, globals_1.expect)(result.tools.size).toBe(38);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -123,6 +123,9 @@ Tip: Initialize git with 'git init' to enable git tracking features.`;
|
|
|
123
123
|
await git.init();
|
|
124
124
|
await git.addConfig('user.name', 'Test User');
|
|
125
125
|
await git.addConfig('user.email', 'test@example.com');
|
|
126
|
+
// Use repo-local hooks directory to prevent global hooks from interfering
|
|
127
|
+
const localHooksDir = path.join(tempRepoPath, '.git', 'hooks');
|
|
128
|
+
await git.addConfig('core.hooksPath', localHooksDir);
|
|
126
129
|
});
|
|
127
130
|
afterEach(() => {
|
|
128
131
|
fs.rmSync(tempRepoPath, { recursive: true, force: true });
|