@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.
Files changed (3) hide show
  1. package/README.md +18 -1
  2. package/bin/ccm.js +142 -38
  3. 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, Cursor, etc.
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.21";
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
- configPaths.push(path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'));
27
- configPaths.push(path.join(home, '.gemini', 'antigravity', 'mcp_config.json'));
28
- configPaths.push(path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'));
29
- configPaths.push(path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'cline_mcp_settings.json'));
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
- configPaths.push(path.join(appData, 'Claude', 'claude_desktop_config.json'));
33
- configPaths.push(path.join(process.env.USERPROFILE || '', 'AppData', 'Roaming', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'));
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
- configPaths.push(path.join(home, '.config', 'Claude', 'claude_desktop_config.json'));
36
- configPaths.push(path.join(home, '.config', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'));
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
- "command": "npx",
41
- "args": ["-y", "@senoldogann/context-manager", "mcp"],
42
- "env": {
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
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
70
- console.log(`[CCM] Successfully updated: ${configPath}`);
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] Please add this to your mcp_config.json manually:");
78
- console.log(JSON.stringify({ "context-manager": mcpConfig }, null, 2));
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
- return downloadFile(response.headers.location, dest).then(resolve).catch(reject);
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
- return downloadText(response.headers.location).then(resolve).catch(reject);
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')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senoldogann/context-manager",
3
- "version": "0.1.21",
3
+ "version": "0.1.24",
4
4
  "description": "LLM Context Manager MCP Server & CLI wrapper using npx",
5
5
  "main": "bin/ccm.js",
6
6
  "bin": {