@senoldogann/context-manager 0.1.30 → 0.2.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/README.md CHANGED
@@ -104,13 +104,14 @@ CCM uses a **Local-First** architecture:
104
104
 
105
105
  ### Environment Variables
106
106
 
107
- Create `~/.ccm/.env`:
107
+ Create `~/.ccm/.env` (or start from the repository's `.env.example`):
108
108
 
109
109
  ```ini
110
110
  # Local (Recommended)
111
111
  EMBEDDING_PROVIDER=ollama
112
112
  EMBEDDING_HOST=http://127.0.0.1:11434
113
113
  EMBEDDING_MODEL=mxbai-embed-large
114
+ EMBEDDING_API_KEY=ollama
114
115
 
115
116
  # Cloud (Optional)
116
117
  EMBEDDING_PROVIDER=openai
@@ -139,6 +140,12 @@ CCM_EMBED_DATA_FILES=0
139
140
  CCM_ALLOW_UNVERIFIED_BINARIES=0
140
141
  ```
141
142
 
143
+ Advanced overrides:
144
+ - `CCM_PROJECT_ROOT` pins the default project root used by the wrapper and MCP fallback engine.
145
+ - `CCM_DB_PATH` overrides the default MCP vector DB location.
146
+ - `.env.example` in the repository contains chunking, batch-size, hybrid-weight, and compatibility-alias settings such as `OPENAI_API_KEY`, `CCM_SKIP_CHECKSUM`, `CCM_MCP_REQUIRE_ALLOWED_ROOTS`, `CCM_EMBED_DATA`, and `EMBEDDING_DISABLED`.
147
+ - Hybrid scoring defaults and tuning notes live in [`docs/hybrid-ranking.md`](https://github.com/senoldogann/LLM-Context-Manager/blob/main/docs/hybrid-ranking.md).
148
+
142
149
  **Production Tip:** Set `CCM_ALLOWED_ROOTS` and enable `CCM_REQUIRE_ALLOWED_ROOTS=1` to restrict MCP access.
143
150
 
144
151
  ---
@@ -169,7 +176,7 @@ This package handles:
169
176
  - The npm wrapper verifies checksums before using downloaded binaries
170
177
  - Redirects are restricted to approved GitHub release hosts
171
178
  - Release builds run for Linux, macOS, and Windows before assets are attached
172
- - npm publishing uses GitHub Actions trusted publishing with OIDC
179
+ - npm publication is a manual step from `npm/` after the GitHub Release assets are attached
173
180
  - The same smoke path is documented here and in the main repository README
174
181
 
175
182
  ### ✅ Binary Integrity
@@ -190,6 +197,11 @@ Enable `CCM_EMBED_DATA_FILES=1` to include them in semantic search.
190
197
 
191
198
  ## 📝 Changelog
192
199
 
200
+ ### v0.1.31
201
+ - ✅ `index_project` is idempotent again and ignores CCM's own generated index files
202
+ - ✅ `search_code` falls back cleanly when embeddings are disabled
203
+ - ✅ Added a real-world Guardian MCP end-to-end test for all 9 tools
204
+
193
205
  ### v0.1.27
194
206
  - ✅ `search_code` now returns node IDs and location metadata, with configurable limits
195
207
  - ✅ New `find_nodes` MCP tool for graph node discovery before `read_graph`
package/bin/ccm.js CHANGED
@@ -37,9 +37,12 @@ let checksumCache = null;
37
37
 
38
38
  const MCP_SERVER_NAME = 'context-manager';
39
39
  const MCP_COMMAND = 'npx';
40
- const MCP_ARGS = ['-y', '@senoldogann/context-manager', 'mcp'];
40
+ const MCP_ARGS = ['-y', `@senoldogann/context-manager@${VERSION}`, 'mcp'];
41
41
  const MCP_ENV = {
42
- RUST_LOG: 'info'
42
+ RUST_LOG: 'info',
43
+ CCM_PROJECT_ROOT: process.cwd(),
44
+ CCM_ALLOWED_ROOTS: process.cwd(),
45
+ CCM_REQUIRE_ALLOWED_ROOTS: '1'
43
46
  };
44
47
  const ALLOWED_REDIRECT_HOSTS = new Set([
45
48
  'github.com',
@@ -114,12 +117,15 @@ function installJsonConfig(configPath, mcpConfig) {
114
117
 
115
118
  let config = { mcpServers: {} };
116
119
  if (fs.existsSync(configPath)) {
120
+ const backupPath = `${configPath}.bak`;
121
+ fs.copyFileSync(configPath, backupPath);
117
122
  try {
118
123
  const content = fs.readFileSync(configPath, 'utf8');
119
124
  config = JSON.parse(content);
120
- fs.copyFileSync(configPath, `${configPath}.bak`);
121
125
  } catch (e) {
122
- console.warn(`[CCM] Could not parse ${configPath}, creating backup and starting fresh section.`);
126
+ throw new Error(
127
+ `Could not parse ${configPath}. The original file was preserved and copied to ${backupPath}.`
128
+ );
123
129
  }
124
130
  }
125
131
 
@@ -128,11 +134,27 @@ function installJsonConfig(configPath, mcpConfig) {
128
134
  }
129
135
 
130
136
  config.mcpServers[MCP_SERVER_NAME] = mcpConfig;
131
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
137
+ writeJsonAtomic(configPath, config);
132
138
  console.log(`[CCM] ✓ Successfully updated: ${configPath}`);
133
139
  return true;
134
140
  }
135
141
 
142
+ function writeJsonAtomic(configPath, value) {
143
+ const tempPath = `${configPath}.${process.pid}.${Date.now()}.tmp`;
144
+ try {
145
+ fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, {
146
+ encoding: 'utf8',
147
+ mode: 0o600
148
+ });
149
+ fs.renameSync(tempPath, configPath);
150
+ } catch (error) {
151
+ if (fs.existsSync(tempPath)) {
152
+ fs.unlinkSync(tempPath);
153
+ }
154
+ throw error;
155
+ }
156
+ }
157
+
136
158
  function installCodexConfig() {
137
159
  const listResult = spawnSync('codex', ['mcp', 'list', '--json'], {
138
160
  encoding: 'utf8'
@@ -157,17 +179,26 @@ function installCodexConfig() {
157
179
 
158
180
  const existing = existingServers.find((server) => server.name === MCP_SERVER_NAME);
159
181
  if (existing) {
160
- const removeResult = spawnSync('codex', ['mcp', 'remove', MCP_SERVER_NAME], {
161
- encoding: 'utf8'
162
- });
163
-
164
- if (removeResult.status !== 0) {
165
- console.warn(`[CCM] Codex MCP removal failed: ${removeResult.stderr.trim()}`);
166
- return false;
167
- }
182
+ console.log('[CCM] Codex MCP entry already exists; leaving it unchanged.');
183
+ return true;
168
184
  }
169
185
 
170
- const addArgs = ['mcp', 'add', MCP_SERVER_NAME, '--env', 'RUST_LOG=info', '--', MCP_COMMAND, ...MCP_ARGS];
186
+ const addArgs = [
187
+ 'mcp',
188
+ 'add',
189
+ MCP_SERVER_NAME,
190
+ '--env',
191
+ 'RUST_LOG=info',
192
+ '--env',
193
+ `CCM_PROJECT_ROOT=${process.cwd()}`,
194
+ '--env',
195
+ `CCM_ALLOWED_ROOTS=${process.cwd()}`,
196
+ '--env',
197
+ 'CCM_REQUIRE_ALLOWED_ROOTS=1',
198
+ '--',
199
+ MCP_COMMAND,
200
+ ...MCP_ARGS
201
+ ];
171
202
  const addResult = spawnSync('codex', addArgs, {
172
203
  encoding: 'utf8'
173
204
  });
@@ -415,4 +446,16 @@ async function main() {
415
446
  }
416
447
  }
417
448
 
418
- main();
449
+ if (require.main === module) {
450
+ main();
451
+ }
452
+
453
+ module.exports = {
454
+ MCP_ARGS,
455
+ MCP_ENV,
456
+ installJsonConfig,
457
+ parseChecksums,
458
+ resolveRedirectUrl,
459
+ verifyChecksum,
460
+ writeJsonAtomic
461
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senoldogann/context-manager",
3
- "version": "0.1.30",
3
+ "version": "0.2.0",
4
4
  "description": "LLM Context Manager MCP Server & CLI wrapper using npx",
5
5
  "repository": "https://github.com/senoldogann/LLM-Context-Manager",
6
6
  "homepage": "https://github.com/senoldogann/LLM-Context-Manager",
@@ -13,7 +13,7 @@
13
13
  "ccm-mcp": "bin/ccm.js"
14
14
  },
15
15
  "scripts": {
16
- "test": "echo \"Error: no test specified\" && exit 1"
16
+ "test": "node --test test/*.test.js"
17
17
  },
18
18
  "keywords": [
19
19
  "mcp",
@@ -0,0 +1,69 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('node:fs');
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+ const test = require('node:test');
6
+
7
+ const {
8
+ MCP_ARGS,
9
+ MCP_ENV,
10
+ installJsonConfig,
11
+ writeJsonAtomic
12
+ } = require('../bin/ccm.js');
13
+
14
+ function tempConfig() {
15
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ccm-installer-'));
16
+ return {
17
+ directory,
18
+ configPath: path.join(directory, 'mcp.json')
19
+ };
20
+ }
21
+
22
+ test('installer merges MCP config and preserves existing settings', () => {
23
+ const { configPath } = tempConfig();
24
+ fs.writeFileSync(
25
+ configPath,
26
+ JSON.stringify({ theme: 'dark', mcpServers: { existing: { command: 'safe' } } })
27
+ );
28
+
29
+ const changed = installJsonConfig(configPath, {
30
+ command: 'npx',
31
+ args: MCP_ARGS,
32
+ env: MCP_ENV
33
+ });
34
+
35
+ assert.equal(changed, true);
36
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
37
+ assert.equal(config.theme, 'dark');
38
+ assert.equal(config.mcpServers.existing.command, 'safe');
39
+ assert.equal(config.mcpServers['context-manager'].env.CCM_REQUIRE_ALLOWED_ROOTS, '1');
40
+ assert.ok(fs.existsSync(`${configPath}.bak`));
41
+ });
42
+
43
+ test('installer never overwrites malformed JSON', () => {
44
+ const { configPath } = tempConfig();
45
+ const malformed = '{"mcpServers":';
46
+ fs.writeFileSync(configPath, malformed);
47
+
48
+ assert.throws(
49
+ () => installJsonConfig(configPath, { command: 'npx', args: [], env: {} }),
50
+ /Could not parse/
51
+ );
52
+ assert.equal(fs.readFileSync(configPath, 'utf8'), malformed);
53
+ assert.equal(fs.readFileSync(`${configPath}.bak`, 'utf8'), malformed);
54
+ });
55
+
56
+ test('atomic writer leaves valid JSON and no temporary file', () => {
57
+ const { directory, configPath } = tempConfig();
58
+ writeJsonAtomic(configPath, { ok: true });
59
+
60
+ assert.deepEqual(JSON.parse(fs.readFileSync(configPath, 'utf8')), { ok: true });
61
+ assert.deepEqual(fs.readdirSync(directory), ['mcp.json']);
62
+ });
63
+
64
+ test('generated MCP command pins package version and narrows project access', () => {
65
+ assert.match(MCP_ARGS[1], /^@senoldogann\/context-manager@\d+\.\d+\.\d+$/);
66
+ assert.equal(MCP_ENV.CCM_REQUIRE_ALLOWED_ROOTS, '1');
67
+ assert.equal(MCP_ENV.CCM_ALLOWED_ROOTS, process.cwd());
68
+ assert.equal(MCP_ENV.CCM_PROJECT_ROOT, process.cwd());
69
+ });