@senoldogann/context-manager 0.1.21 → 0.1.24
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 +18 -1
- package/bin/ccm.js +142 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
### Installation
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
# 1. Install and configure for Claude Desktop, Antigravity,
|
|
25
|
+
# 1. Install and configure for Codex, Cursor, Claude Desktop, Antigravity, etc.
|
|
26
26
|
npx @senoldogann/context-manager install
|
|
27
27
|
|
|
28
28
|
# 2. Index your project
|
|
@@ -156,6 +156,23 @@ Enable `CCM_EMBED_DATA_FILES=1` to include them in semantic search.
|
|
|
156
156
|
|
|
157
157
|
## 📝 Changelog
|
|
158
158
|
|
|
159
|
+
### v0.1.24
|
|
160
|
+
- ✅ Native `protoc` installation in GitHub Actions release builds
|
|
161
|
+
- ✅ Updated release workflow actions for newer runner compatibility
|
|
162
|
+
|
|
163
|
+
### v0.1.23
|
|
164
|
+
- ✅ JSON-RPC request size limit for safer MCP stdio transport
|
|
165
|
+
- ✅ Sensitive value redaction in MCP debug logs
|
|
166
|
+
- ✅ Hardened path normalization for MCP graph and context tools
|
|
167
|
+
- ✅ GitHub release redirect allowlist for binary downloads
|
|
168
|
+
- ✅ Unused core dependency cleanup
|
|
169
|
+
|
|
170
|
+
### v0.1.22
|
|
171
|
+
- ✅ Codex installer support via `codex mcp add`
|
|
172
|
+
- ✅ Cursor config support via `~/.cursor/mcp.json`
|
|
173
|
+
- ✅ Evaluation bootstraps missing indexes before scoring
|
|
174
|
+
- ✅ Golden tasks refreshed for the current repo layout
|
|
175
|
+
|
|
159
176
|
### v0.1.21
|
|
160
177
|
- ✅ Release checksums (`checksums.txt`) for binary integrity
|
|
161
178
|
- ✅ MCP allowlist with optional strict enforcement
|
package/bin/ccm.js
CHANGED
|
@@ -1,47 +1,59 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { spawn } = require('child_process');
|
|
3
|
+
const { spawn, spawnSync } = require('child_process');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const https = require('https');
|
|
8
8
|
const crypto = require('crypto');
|
|
9
9
|
|
|
10
|
-
const VERSION = "0.1.
|
|
10
|
+
const VERSION = "0.1.24";
|
|
11
11
|
const REPO = 'senoldogann/LLM-Context-Manager';
|
|
12
12
|
const BIN_DIR = path.join(os.homedir(), '.ccm', 'bin');
|
|
13
13
|
const CHECKSUMS_FILE = 'checksums.txt';
|
|
14
14
|
let checksumCache = null;
|
|
15
15
|
|
|
16
|
+
const MCP_SERVER_NAME = 'context-manager';
|
|
17
|
+
const MCP_COMMAND = 'npx';
|
|
18
|
+
const MCP_ARGS = ['-y', '@senoldogann/context-manager', 'mcp'];
|
|
19
|
+
const MCP_ENV = {
|
|
20
|
+
RUST_LOG: 'info'
|
|
21
|
+
};
|
|
22
|
+
const ALLOWED_REDIRECT_HOSTS = new Set([
|
|
23
|
+
'github.com',
|
|
24
|
+
'objects.githubusercontent.com',
|
|
25
|
+
'release-assets.githubusercontent.com'
|
|
26
|
+
]);
|
|
27
|
+
|
|
16
28
|
function allowUnverifiedBinaries() {
|
|
17
29
|
const raw = process.env.CCM_ALLOW_UNVERIFIED_BINARIES || process.env.CCM_SKIP_CHECKSUM || '';
|
|
18
30
|
return ['1', 'true', 'yes'].includes(raw.toLowerCase());
|
|
19
31
|
}
|
|
20
32
|
|
|
21
33
|
async function installMcp() {
|
|
22
|
-
const configPaths = [];
|
|
23
34
|
const home = os.homedir();
|
|
35
|
+
const jsonTargets = [];
|
|
24
36
|
|
|
25
37
|
if (os.platform() === 'darwin') {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
jsonTargets.push(path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'));
|
|
39
|
+
jsonTargets.push(path.join(home, '.gemini', 'antigravity', 'mcp_config.json'));
|
|
40
|
+
jsonTargets.push(path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'));
|
|
41
|
+
jsonTargets.push(path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'cline_mcp_settings.json'));
|
|
30
42
|
} else if (os.platform() === 'win32') {
|
|
31
43
|
const appData = process.env.APPDATA || '';
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
jsonTargets.push(path.join(appData, 'Claude', 'claude_desktop_config.json'));
|
|
45
|
+
jsonTargets.push(path.join(process.env.USERPROFILE || '', 'AppData', 'Roaming', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'));
|
|
34
46
|
} else if (os.platform() === 'linux') {
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
jsonTargets.push(path.join(home, '.config', 'Claude', 'claude_desktop_config.json'));
|
|
48
|
+
jsonTargets.push(path.join(home, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'));
|
|
37
49
|
}
|
|
38
50
|
|
|
51
|
+
jsonTargets.push(path.join(home, '.cursor', 'mcp.json'));
|
|
52
|
+
|
|
39
53
|
const mcpConfig = {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"RUST_LOG": "info"
|
|
44
|
-
}
|
|
54
|
+
command: MCP_COMMAND,
|
|
55
|
+
args: MCP_ARGS,
|
|
56
|
+
env: MCP_ENV
|
|
45
57
|
};
|
|
46
58
|
|
|
47
59
|
console.log("[CCM] Pre-downloading binaries for all tools...");
|
|
@@ -49,38 +61,104 @@ async function installMcp() {
|
|
|
49
61
|
await getBinaryFor('ccm-mcp');
|
|
50
62
|
|
|
51
63
|
let installedCount = 0;
|
|
52
|
-
for (const configPath of configPaths) {
|
|
53
|
-
const dir = path.dirname(configPath);
|
|
54
|
-
if (fs.existsSync(dir)) {
|
|
55
|
-
let config = { mcpServers: {} };
|
|
56
|
-
if (fs.existsSync(configPath)) {
|
|
57
|
-
try {
|
|
58
|
-
const content = fs.readFileSync(configPath, 'utf8');
|
|
59
|
-
config = JSON.parse(content);
|
|
60
|
-
fs.copyFileSync(configPath, `${configPath}.bak`);
|
|
61
|
-
} catch (e) {
|
|
62
|
-
console.warn(`[CCM] Could not parse ${configPath}, creating backup and starting fresh section.`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (!config.mcpServers) config.mcpServers = {};
|
|
67
|
-
config.mcpServers["context-manager"] = mcpConfig;
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
for (const configPath of jsonTargets) {
|
|
66
|
+
if (installJsonConfig(configPath, mcpConfig)) {
|
|
71
67
|
installedCount++;
|
|
72
68
|
}
|
|
73
69
|
}
|
|
74
70
|
|
|
71
|
+
if (installCodexConfig()) {
|
|
72
|
+
installedCount++;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
75
|
if (installedCount === 0) {
|
|
76
76
|
console.log("[CCM] No supported MCP config directories found.");
|
|
77
|
-
console.log("[CCM]
|
|
78
|
-
console.log(JSON.stringify({
|
|
77
|
+
console.log("[CCM] Add this server manually:");
|
|
78
|
+
console.log(JSON.stringify({ [MCP_SERVER_NAME]: mcpConfig }, null, 2));
|
|
79
|
+
console.log("[CCM] Codex example:");
|
|
80
|
+
console.log(`codex mcp add ${MCP_SERVER_NAME} --env RUST_LOG=info -- ${MCP_COMMAND} ${MCP_ARGS.join(' ')}`);
|
|
79
81
|
} else {
|
|
80
82
|
console.log("[CCM] Installation complete! Restart your AI editor to see the changes.");
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
function installJsonConfig(configPath, mcpConfig) {
|
|
87
|
+
const dir = path.dirname(configPath);
|
|
88
|
+
|
|
89
|
+
if (!fs.existsSync(dir)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let config = { mcpServers: {} };
|
|
94
|
+
if (fs.existsSync(configPath)) {
|
|
95
|
+
try {
|
|
96
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
97
|
+
config = JSON.parse(content);
|
|
98
|
+
fs.copyFileSync(configPath, `${configPath}.bak`);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.warn(`[CCM] Could not parse ${configPath}, creating backup and starting fresh section.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!config.mcpServers || typeof config.mcpServers !== 'object') {
|
|
105
|
+
config.mcpServers = {};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
config.mcpServers[MCP_SERVER_NAME] = mcpConfig;
|
|
109
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
110
|
+
console.log(`[CCM] ✓ Successfully updated: ${configPath}`);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function installCodexConfig() {
|
|
115
|
+
const listResult = spawnSync('codex', ['mcp', 'list', '--json'], {
|
|
116
|
+
encoding: 'utf8'
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (listResult.error) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (listResult.status !== 0) {
|
|
124
|
+
console.warn(`[CCM] Codex MCP inspection failed: ${listResult.stderr.trim()}`);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let existingServers = [];
|
|
129
|
+
try {
|
|
130
|
+
existingServers = JSON.parse(listResult.stdout);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.warn('[CCM] Could not parse Codex MCP list output.');
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const existing = existingServers.find((server) => server.name === MCP_SERVER_NAME);
|
|
137
|
+
if (existing) {
|
|
138
|
+
const removeResult = spawnSync('codex', ['mcp', 'remove', MCP_SERVER_NAME], {
|
|
139
|
+
encoding: 'utf8'
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (removeResult.status !== 0) {
|
|
143
|
+
console.warn(`[CCM] Codex MCP removal failed: ${removeResult.stderr.trim()}`);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const addArgs = ['mcp', 'add', MCP_SERVER_NAME, '--env', 'RUST_LOG=info', '--', MCP_COMMAND, ...MCP_ARGS];
|
|
149
|
+
const addResult = spawnSync('codex', addArgs, {
|
|
150
|
+
encoding: 'utf8'
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (addResult.status !== 0) {
|
|
154
|
+
console.warn(`[CCM] Codex MCP install failed: ${addResult.stderr.trim()}`);
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log('[CCM] ✓ Successfully updated: ~/.codex/config.toml');
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
84
162
|
async function getBinaryFor(commandName) {
|
|
85
163
|
const platform = os.platform();
|
|
86
164
|
const arch = os.arch();
|
|
@@ -152,7 +230,12 @@ function downloadFile(url, dest) {
|
|
|
152
230
|
return new Promise((resolve, reject) => {
|
|
153
231
|
https.get(url, (response) => {
|
|
154
232
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
155
|
-
|
|
233
|
+
try {
|
|
234
|
+
const redirectUrl = resolveRedirectUrl(url, response.headers.location);
|
|
235
|
+
return downloadFile(redirectUrl, dest).then(resolve).catch(reject);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return reject(error);
|
|
238
|
+
}
|
|
156
239
|
}
|
|
157
240
|
if (response.statusCode !== 200) {
|
|
158
241
|
return reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
@@ -177,7 +260,12 @@ function downloadText(url) {
|
|
|
177
260
|
return new Promise((resolve, reject) => {
|
|
178
261
|
https.get(url, (response) => {
|
|
179
262
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
180
|
-
|
|
263
|
+
try {
|
|
264
|
+
const redirectUrl = resolveRedirectUrl(url, response.headers.location);
|
|
265
|
+
return downloadText(redirectUrl).then(resolve).catch(reject);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return reject(error);
|
|
268
|
+
}
|
|
181
269
|
}
|
|
182
270
|
if (response.statusCode !== 200) {
|
|
183
271
|
return reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
@@ -205,6 +293,22 @@ async function getChecksums() {
|
|
|
205
293
|
}
|
|
206
294
|
}
|
|
207
295
|
|
|
296
|
+
function resolveRedirectUrl(sourceUrl, location) {
|
|
297
|
+
if (!location) {
|
|
298
|
+
throw new Error('Redirect response did not include a Location header');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const resolved = new URL(location, sourceUrl);
|
|
302
|
+
if (resolved.protocol !== 'https:') {
|
|
303
|
+
throw new Error(`Blocked redirect to non-HTTPS URL: ${resolved.href}`);
|
|
304
|
+
}
|
|
305
|
+
if (!ALLOWED_REDIRECT_HOSTS.has(resolved.hostname)) {
|
|
306
|
+
throw new Error(`Blocked redirect to unexpected host: ${resolved.hostname}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return resolved.toString();
|
|
310
|
+
}
|
|
311
|
+
|
|
208
312
|
function parseChecksums(text) {
|
|
209
313
|
const map = new Map();
|
|
210
314
|
for (const line of text.split('\n')) {
|