@senoldogann/context-manager 0.1.20 → 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 +159 -51
- package/bin/ccm.js +236 -37
- package/package.json +3 -5
package/README.md
CHANGED
|
@@ -2,81 +2,189 @@
|
|
|
2
2
|
|
|
3
3
|
> 🧠 The Neural Backbone for Autonomous AI Agents
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Node.js wrapper for Cognitive Codebase Matrix (CCM)** - Enables AI agents to understand and navigate your codebase with surgical precision.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@senoldogann/context-manager)
|
|
8
|
+
[](https://modelcontextprotocol.io/)
|
|
9
|
+
[](https://www.rust-lang.org/)
|
|
10
|
+
|
|
11
|
+
---
|
|
6
12
|
|
|
7
13
|
## 🚀 Quick Start
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
### Prerequisites
|
|
16
|
+
|
|
17
|
+
1. **Node.js** 16+ installed
|
|
18
|
+
2. **Ollama** installed and running (for local embeddings)
|
|
19
|
+
- Download: https://ollama.com
|
|
20
|
+
- Pull required model: `ollama pull mxbai-embed-large`
|
|
12
21
|
|
|
13
|
-
###
|
|
14
|
-
CCM uses a **Local-First** architecture by default. This means:
|
|
15
|
-
* Your code is **never** sent to 3rd party servers (OpenAI, Anthropic, etc.).
|
|
16
|
-
* All vector operations (Embedding) happen on your local machine.
|
|
17
|
-
* You can safely use it for internal or confidential projects.
|
|
22
|
+
### Installation
|
|
18
23
|
|
|
19
|
-
The easiest way to get started. This will automatically add CCM to your Claude or Antigravity configuration:
|
|
20
24
|
```bash
|
|
25
|
+
# 1. Install and configure for Codex, Cursor, Claude Desktop, Antigravity, etc.
|
|
21
26
|
npx @senoldogann/context-manager install
|
|
27
|
+
|
|
28
|
+
# 2. Index your project
|
|
29
|
+
npx @senoldogann/context-manager index --path .
|
|
22
30
|
```
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
**That's it!** Restart your AI editor and start asking questions about your code.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 📖 What is CCM?
|
|
37
|
+
|
|
38
|
+
CCM transforms static source code into a dynamic, queryable Knowledge Graph:
|
|
39
|
+
|
|
40
|
+
- **🔍 Semantic Search** - Find code by meaning ("where is auth logic?")
|
|
41
|
+
- **🧠 Graph Navigation** - Understand relationships ("who calls this function?")
|
|
42
|
+
- **📍 Cursor Context** - Get relevant code based on your position
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 🔧 Commands
|
|
47
|
+
|
|
48
|
+
The npm wrapper downloads pre-built binaries and passes commands through:
|
|
49
|
+
|
|
50
|
+
| Command | Description |
|
|
51
|
+
|---------|-------------|
|
|
52
|
+
| `npx @senoldogann/context-manager install` | Auto-configure MCP for editors |
|
|
53
|
+
| `npx @senoldogann/context-manager index --path <dir>` | Index a project |
|
|
54
|
+
| `npx @senoldogann/context-manager query --text "..."` | Search codebase |
|
|
55
|
+
| `npx @senoldogann/context-manager mcp` | Run MCP server directly |
|
|
56
|
+
| `npx @senoldogann/context-manager eval --tasks <file>` | Run evaluation tasks |
|
|
57
|
+
|
|
58
|
+
### Options
|
|
26
59
|
|
|
27
|
-
If you still want to index manually:
|
|
28
60
|
```bash
|
|
29
|
-
|
|
30
|
-
|
|
61
|
+
# Watch mode - auto-reindex on file changes
|
|
62
|
+
npx @senoldogann/context-manager index --path . --watch
|
|
31
63
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
If you prefer to configure your AI editor manually without the `install` command, add this to your `mcp_config.json`:
|
|
35
|
-
|
|
36
|
-
```json
|
|
37
|
-
{
|
|
38
|
-
"mcpServers": {
|
|
39
|
-
"context-manager": {
|
|
40
|
-
"command": "npx",
|
|
41
|
-
"args": ["-y", "@senoldogann/context-manager", "mcp"],
|
|
42
|
-
"env": {
|
|
43
|
-
"RUST_LOG": "info"
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
64
|
+
# Custom database path
|
|
65
|
+
npx @senoldogann/context-manager index --path . --db-path /custom/path
|
|
48
66
|
```
|
|
49
67
|
|
|
50
|
-
|
|
51
|
-
To ensure your AI agent (Claude, Cursor, etc.) always uses CCM for deep analysis, add this to your **Custom Instructions** or **System Prompt**:
|
|
68
|
+
---
|
|
52
69
|
|
|
53
|
-
|
|
70
|
+
## 🔒 Privacy by Default
|
|
71
|
+
|
|
72
|
+
CCM uses a **Local-First** architecture:
|
|
73
|
+
|
|
74
|
+
- ✅ Your code **never** leaves your machine
|
|
75
|
+
- ✅ All embeddings run locally via Ollama
|
|
76
|
+
- ✅ No external API calls (unless you configure OpenAI)
|
|
54
77
|
|
|
55
78
|
---
|
|
56
79
|
|
|
57
|
-
##
|
|
80
|
+
## ⚙️ Configuration
|
|
58
81
|
|
|
59
|
-
|
|
82
|
+
### Environment Variables
|
|
60
83
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
84
|
+
Create `~/.ccm/.env`:
|
|
85
|
+
|
|
86
|
+
```ini
|
|
87
|
+
# Local (Recommended)
|
|
88
|
+
EMBEDDING_PROVIDER=ollama
|
|
89
|
+
EMBEDDING_HOST=http://127.0.0.1:11434
|
|
90
|
+
EMBEDDING_MODEL=mxbai-embed-large
|
|
91
|
+
|
|
92
|
+
# Cloud (Optional)
|
|
93
|
+
EMBEDDING_PROVIDER=openai
|
|
94
|
+
EMBEDDING_API_KEY=sk-your-key
|
|
95
|
+
EMBEDDING_MODEL=text-embedding-3-small
|
|
96
|
+
|
|
97
|
+
# Networking & Limits
|
|
98
|
+
EMBEDDING_TIMEOUT_SECS=30
|
|
99
|
+
CCM_MAX_FILE_BYTES=2097152
|
|
100
|
+
|
|
101
|
+
# MCP Security
|
|
102
|
+
CCM_ALLOWED_ROOTS=/Users/you/projects:/Users/you/sandbox
|
|
103
|
+
CCM_REQUIRE_ALLOWED_ROOTS=0
|
|
104
|
+
|
|
105
|
+
# MCP Runtime
|
|
106
|
+
CCM_MCP_ENGINE_CACHE_SIZE=8
|
|
107
|
+
CCM_MCP_DEBUG=0
|
|
108
|
+
|
|
109
|
+
# Optional: disable embeddings entirely (semantic search disabled)
|
|
110
|
+
CCM_DISABLE_EMBEDDER=0
|
|
111
|
+
|
|
112
|
+
# Optional: embed data files (md/json/yaml) into vector search
|
|
113
|
+
CCM_EMBED_DATA_FILES=0
|
|
114
|
+
|
|
115
|
+
# Binary checksum verification (0 = enforce, 1 = bypass)
|
|
116
|
+
CCM_ALLOW_UNVERIFIED_BINARIES=0
|
|
65
117
|
```
|
|
66
118
|
|
|
119
|
+
**Production Tip:** Set `CCM_ALLOWED_ROOTS` and enable `CCM_REQUIRE_ALLOWED_ROOTS=1` to restrict MCP access.
|
|
120
|
+
|
|
67
121
|
---
|
|
68
122
|
|
|
69
|
-
##
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
123
|
+
## 🤖 Usage in AI
|
|
124
|
+
|
|
125
|
+
Once configured, ask your AI agent:
|
|
126
|
+
|
|
127
|
+
> "Search for the authentication flow in this codebase."
|
|
74
128
|
|
|
75
|
-
|
|
129
|
+
> "Read the graph for `UserService` and show me its callers."
|
|
130
|
+
|
|
131
|
+
> "What functions call `parse_config`?"
|
|
132
|
+
|
|
133
|
+
---
|
|
76
134
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
135
|
+
## 📦 For Developers
|
|
136
|
+
|
|
137
|
+
This package handles:
|
|
138
|
+
|
|
139
|
+
1. OS/architecture detection
|
|
140
|
+
2. Binary download from GitHub Releases
|
|
141
|
+
3. Global persistence in `~/.ccm`
|
|
142
|
+
|
|
143
|
+
### ✅ Binary Integrity
|
|
144
|
+
|
|
145
|
+
Downloads are verified against `checksums.txt` from the GitHub Release.
|
|
146
|
+
If the manifest is missing or a mismatch occurs, you can set `CCM_ALLOW_UNVERIFIED_BINARIES=1` to bypass verification (not recommended).
|
|
147
|
+
|
|
148
|
+
### 📄 Data Files in Search
|
|
149
|
+
|
|
150
|
+
By default, data files (`.md`, `.json`, `.yaml`) are indexed but not embedded.
|
|
151
|
+
Enable `CCM_EMBED_DATA_FILES=1` to include them in semantic search.
|
|
152
|
+
|
|
153
|
+
**Source:** https://github.com/senoldogann/LLM-Context-Manager
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 📝 Changelog
|
|
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
|
+
|
|
176
|
+
### v0.1.21
|
|
177
|
+
- ✅ Release checksums (`checksums.txt`) for binary integrity
|
|
178
|
+
- ✅ MCP allowlist with optional strict enforcement
|
|
179
|
+
- ✅ Data file embedding is optional (`CCM_EMBED_DATA_FILES`)
|
|
180
|
+
- ✅ CLI/MCP integration tests
|
|
181
|
+
|
|
182
|
+
### v0.1.20
|
|
183
|
+
- ✅ 100% evaluation pass rate
|
|
184
|
+
- ✅ Hybrid scoring (graph + semantic)
|
|
185
|
+
- ✅ Lazy indexing support
|
|
186
|
+
- ✅ Watch mode for auto-reindexing
|
|
187
|
+
|
|
188
|
+
---
|
|
81
189
|
|
|
82
|
-
Built with ❤️ in **Rust
|
|
190
|
+
Built with ❤️ in **Rust**
|
package/bin/ccm.js
CHANGED
|
@@ -1,39 +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
|
+
const crypto = require('crypto');
|
|
8
9
|
|
|
9
|
-
const VERSION = "0.1.
|
|
10
|
+
const VERSION = "0.1.24";
|
|
10
11
|
const REPO = 'senoldogann/LLM-Context-Manager';
|
|
11
12
|
const BIN_DIR = path.join(os.homedir(), '.ccm', 'bin');
|
|
13
|
+
const CHECKSUMS_FILE = 'checksums.txt';
|
|
14
|
+
let checksumCache = null;
|
|
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
|
+
|
|
28
|
+
function allowUnverifiedBinaries() {
|
|
29
|
+
const raw = process.env.CCM_ALLOW_UNVERIFIED_BINARIES || process.env.CCM_SKIP_CHECKSUM || '';
|
|
30
|
+
return ['1', 'true', 'yes'].includes(raw.toLowerCase());
|
|
31
|
+
}
|
|
12
32
|
|
|
13
33
|
async function installMcp() {
|
|
14
|
-
const configPaths = [];
|
|
15
34
|
const home = os.homedir();
|
|
35
|
+
const jsonTargets = [];
|
|
16
36
|
|
|
17
37
|
if (os.platform() === 'darwin') {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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'));
|
|
22
42
|
} else if (os.platform() === 'win32') {
|
|
23
43
|
const appData = process.env.APPDATA || '';
|
|
24
|
-
|
|
25
|
-
|
|
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'));
|
|
26
46
|
} else if (os.platform() === 'linux') {
|
|
27
|
-
|
|
28
|
-
|
|
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'));
|
|
29
49
|
}
|
|
30
50
|
|
|
51
|
+
jsonTargets.push(path.join(home, '.cursor', 'mcp.json'));
|
|
52
|
+
|
|
31
53
|
const mcpConfig = {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"RUST_LOG": "info"
|
|
36
|
-
}
|
|
54
|
+
command: MCP_COMMAND,
|
|
55
|
+
args: MCP_ARGS,
|
|
56
|
+
env: MCP_ENV
|
|
37
57
|
};
|
|
38
58
|
|
|
39
59
|
console.log("[CCM] Pre-downloading binaries for all tools...");
|
|
@@ -41,38 +61,104 @@ async function installMcp() {
|
|
|
41
61
|
await getBinaryFor('ccm-mcp');
|
|
42
62
|
|
|
43
63
|
let installedCount = 0;
|
|
44
|
-
for (const configPath of configPaths) {
|
|
45
|
-
const dir = path.dirname(configPath);
|
|
46
|
-
if (fs.existsSync(dir)) {
|
|
47
|
-
let config = { mcpServers: {} };
|
|
48
|
-
if (fs.existsSync(configPath)) {
|
|
49
|
-
try {
|
|
50
|
-
const content = fs.readFileSync(configPath, 'utf8');
|
|
51
|
-
config = JSON.parse(content);
|
|
52
|
-
fs.copyFileSync(configPath, `${configPath}.bak`);
|
|
53
|
-
} catch (e) {
|
|
54
|
-
console.warn(`[CCM] Could not parse ${configPath}, creating backup and starting fresh section.`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (!config.mcpServers) config.mcpServers = {};
|
|
59
|
-
config.mcpServers["context-manager"] = mcpConfig;
|
|
60
64
|
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
for (const configPath of jsonTargets) {
|
|
66
|
+
if (installJsonConfig(configPath, mcpConfig)) {
|
|
63
67
|
installedCount++;
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
if (installCodexConfig()) {
|
|
72
|
+
installedCount++;
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
if (installedCount === 0) {
|
|
68
76
|
console.log("[CCM] No supported MCP config directories found.");
|
|
69
|
-
console.log("[CCM]
|
|
70
|
-
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(' ')}`);
|
|
71
81
|
} else {
|
|
72
82
|
console.log("[CCM] Installation complete! Restart your AI editor to see the changes.");
|
|
73
83
|
}
|
|
74
84
|
}
|
|
75
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
|
+
|
|
76
162
|
async function getBinaryFor(commandName) {
|
|
77
163
|
const platform = os.platform();
|
|
78
164
|
const arch = os.arch();
|
|
@@ -88,6 +174,7 @@ async function getBinaryFor(commandName) {
|
|
|
88
174
|
|
|
89
175
|
const binFilename = `${commandName}-v${VERSION}-${target}`;
|
|
90
176
|
const binPath = path.join(BIN_DIR, binFilename);
|
|
177
|
+
const remoteFilename = `${commandName}-${target}`;
|
|
91
178
|
|
|
92
179
|
// If file exists, ensure it is executable
|
|
93
180
|
if (fs.existsSync(binPath)) {
|
|
@@ -110,6 +197,7 @@ async function getBinaryFor(commandName) {
|
|
|
110
197
|
|
|
111
198
|
try {
|
|
112
199
|
await downloadFile(url, tmpPath);
|
|
200
|
+
await verifyChecksum(tmpPath, [remoteFilename, binFilename]);
|
|
113
201
|
fs.chmodSync(tmpPath, '755');
|
|
114
202
|
fs.renameSync(tmpPath, binPath);
|
|
115
203
|
} catch (err) {
|
|
@@ -142,7 +230,12 @@ function downloadFile(url, dest) {
|
|
|
142
230
|
return new Promise((resolve, reject) => {
|
|
143
231
|
https.get(url, (response) => {
|
|
144
232
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
145
|
-
|
|
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
|
+
}
|
|
146
239
|
}
|
|
147
240
|
if (response.statusCode !== 200) {
|
|
148
241
|
return reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
@@ -163,6 +256,112 @@ function downloadFile(url, dest) {
|
|
|
163
256
|
});
|
|
164
257
|
}
|
|
165
258
|
|
|
259
|
+
function downloadText(url) {
|
|
260
|
+
return new Promise((resolve, reject) => {
|
|
261
|
+
https.get(url, (response) => {
|
|
262
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
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
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (response.statusCode !== 200) {
|
|
271
|
+
return reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
272
|
+
}
|
|
273
|
+
let data = '';
|
|
274
|
+
response.setEncoding('utf8');
|
|
275
|
+
response.on('data', (chunk) => {
|
|
276
|
+
data += chunk;
|
|
277
|
+
});
|
|
278
|
+
response.on('end', () => resolve(data));
|
|
279
|
+
}).on('error', reject);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function getChecksums() {
|
|
284
|
+
if (checksumCache) return checksumCache;
|
|
285
|
+
|
|
286
|
+
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${CHECKSUMS_FILE}`;
|
|
287
|
+
try {
|
|
288
|
+
const text = await downloadText(url);
|
|
289
|
+
checksumCache = parseChecksums(text);
|
|
290
|
+
return checksumCache;
|
|
291
|
+
} catch (err) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
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
|
+
|
|
312
|
+
function parseChecksums(text) {
|
|
313
|
+
const map = new Map();
|
|
314
|
+
for (const line of text.split('\n')) {
|
|
315
|
+
const trimmed = line.trim();
|
|
316
|
+
if (!trimmed) continue;
|
|
317
|
+
const parts = trimmed.split(/\s+/);
|
|
318
|
+
if (parts.length < 2) continue;
|
|
319
|
+
const hash = parts[0].toLowerCase();
|
|
320
|
+
const filename = parts[parts.length - 1].replace(/^\*/, '');
|
|
321
|
+
map.set(filename, hash);
|
|
322
|
+
}
|
|
323
|
+
return map;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function sha256File(filePath) {
|
|
327
|
+
return new Promise((resolve, reject) => {
|
|
328
|
+
const hash = crypto.createHash('sha256');
|
|
329
|
+
const stream = fs.createReadStream(filePath);
|
|
330
|
+
stream.on('error', reject);
|
|
331
|
+
stream.on('data', (data) => hash.update(data));
|
|
332
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function verifyChecksum(filePath, candidates) {
|
|
337
|
+
const checksums = await getChecksums();
|
|
338
|
+
if (!checksums) {
|
|
339
|
+
if (allowUnverifiedBinaries()) {
|
|
340
|
+
console.warn('[CCM] Checksum manifest not found. Proceeding without verification.');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
throw new Error('Checksum manifest not found. Set CCM_ALLOW_UNVERIFIED_BINARIES=1 to bypass.');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const expected = candidates
|
|
347
|
+
.map((name) => [name, `${name}.exe`])
|
|
348
|
+
.flat()
|
|
349
|
+
.map((name) => checksums.get(name))
|
|
350
|
+
.find(Boolean);
|
|
351
|
+
if (!expected) {
|
|
352
|
+
if (allowUnverifiedBinaries()) {
|
|
353
|
+
console.warn('[CCM] Checksum not found for binary. Proceeding without verification.');
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
throw new Error('Checksum not found for binary. Set CCM_ALLOW_UNVERIFIED_BINARIES=1 to bypass.');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const actual = await sha256File(filePath);
|
|
360
|
+
if (actual !== expected) {
|
|
361
|
+
throw new Error(`Checksum mismatch. Expected ${expected}, got ${actual}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
166
365
|
async function main() {
|
|
167
366
|
try {
|
|
168
367
|
const binPath = await getBinary();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@senoldogann/context-manager",
|
|
3
|
-
"version": "0.1.
|
|
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": {
|
|
@@ -20,10 +20,8 @@
|
|
|
20
20
|
],
|
|
21
21
|
"author": "dogan",
|
|
22
22
|
"license": "MIT",
|
|
23
|
-
"dependencies": {
|
|
24
|
-
"node-fetch": "^3.3.1"
|
|
25
|
-
},
|
|
23
|
+
"dependencies": {},
|
|
26
24
|
"engines": {
|
|
27
25
|
"node": ">=16.0.0"
|
|
28
26
|
}
|
|
29
|
-
}
|
|
27
|
+
}
|