@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 +58 -15
- package/package.json +2 -2
- package/test/installer.test.js +69 -0
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',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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 = [
|
|
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.
|
|
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": "
|
|
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
|
+
});
|