@senoldogann/context-manager 0.1.31 → 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/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.31",
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
+ });