@senoldogann/context-manager 0.2.0 → 0.3.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/README.md +31 -2
- package/bin/ccm.js +168 -80
- package/package.json +9 -2
- package/test/installer.test.js +0 -69
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
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
6
|
|
|
7
|
+
**v0.2.1** fixes UTF-8 indexing crashes, stale Codex MCP entries, interrupted binary downloads, and missing cross-file class/import usages.
|
|
8
|
+
|
|
7
9
|
[](https://www.npmjs.com/package/@senoldogann/context-manager)
|
|
8
10
|
[](https://modelcontextprotocol.io/)
|
|
9
11
|
[](https://www.rust-lang.org/)
|
|
@@ -45,6 +47,8 @@ npx @senoldogann/context-manager mcp
|
|
|
45
47
|
|
|
46
48
|
If the wrapper is healthy, the query command should return code context and the MCP process should stay open waiting for stdio messages.
|
|
47
49
|
|
|
50
|
+
The first index is complete; subsequent runs are incremental and process only added, changed, renamed, or deleted files. An unchanged project returns a clean no-op.
|
|
51
|
+
|
|
48
52
|
---
|
|
49
53
|
|
|
50
54
|
## 📖 What is CCM?
|
|
@@ -67,6 +71,7 @@ The npm wrapper downloads pre-built binaries and passes commands through:
|
|
|
67
71
|
| `npx @senoldogann/context-manager index --path <dir>` | Index a project |
|
|
68
72
|
| `npx @senoldogann/context-manager query --text "..."` | Search codebase |
|
|
69
73
|
| `npx @senoldogann/context-manager mcp` | Run MCP server directly |
|
|
74
|
+
| `npx @senoldogann/context-manager doctor --path <dir>` | Diagnose config and index health |
|
|
70
75
|
| `npx @senoldogann/context-manager eval --tasks <file>` | Run evaluation tasks |
|
|
71
76
|
|
|
72
77
|
### Supported Hosts
|
|
@@ -138,6 +143,10 @@ CCM_EMBED_DATA_FILES=0
|
|
|
138
143
|
|
|
139
144
|
# Binary checksum verification (0 = enforce, 1 = bypass)
|
|
140
145
|
CCM_ALLOW_UNVERIFIED_BINARIES=0
|
|
146
|
+
|
|
147
|
+
# Optional binary download tuning
|
|
148
|
+
CCM_DOWNLOAD_TIMEOUT_MS=120000
|
|
149
|
+
CCM_DOWNLOAD_ATTEMPTS=3
|
|
141
150
|
```
|
|
142
151
|
|
|
143
152
|
Advanced overrides:
|
|
@@ -167,16 +176,27 @@ Once configured, ask your AI agent:
|
|
|
167
176
|
This package handles:
|
|
168
177
|
|
|
169
178
|
1. OS/architecture detection
|
|
170
|
-
2.
|
|
179
|
+
2. Resumable, retrying compressed binary download from GitHub Releases
|
|
171
180
|
3. Global persistence in `~/.ccm`
|
|
172
181
|
|
|
182
|
+
For a manual maintainer release, run the commands from this `npm/` directory:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npm test
|
|
186
|
+
npm pack --dry-run
|
|
187
|
+
npm publish --access public --provenance
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Running `npm publish` from the repository root fails because the root intentionally has no `package.json`.
|
|
191
|
+
|
|
173
192
|
### ✅ Release Reliability
|
|
174
193
|
|
|
175
194
|
- GitHub Releases publish platform binaries plus `checksums.txt`
|
|
176
195
|
- The npm wrapper verifies checksums before using downloaded binaries
|
|
196
|
+
- Interrupted downloads resume from their partial file and use a bounded timeout/retry policy
|
|
177
197
|
- Redirects are restricted to approved GitHub release hosts
|
|
178
198
|
- Release builds run for Linux, macOS, and Windows before assets are attached
|
|
179
|
-
-
|
|
199
|
+
- Codex configuration is updated directly and atomically, even when the `codex` wrapper is broken
|
|
180
200
|
- The same smoke path is documented here and in the main repository README
|
|
181
201
|
|
|
182
202
|
### ✅ Binary Integrity
|
|
@@ -197,6 +217,15 @@ Enable `CCM_EMBED_DATA_FILES=1` to include them in semantic search.
|
|
|
197
217
|
|
|
198
218
|
## 📝 Changelog
|
|
199
219
|
|
|
220
|
+
### v0.2.1
|
|
221
|
+
- ✅ UTF-8-safe semantic chunking prevents panics on Turkish, Finnish, and emoji content
|
|
222
|
+
- ✅ Cross-file imports, constructors, and type annotations contribute to usage and impact analysis
|
|
223
|
+
- ✅ `get_context` returns the enclosing function/class instead of only a leaf assignment
|
|
224
|
+
- ✅ Incremental `diff_context` includes staged, unstaged, and untracked worktree files
|
|
225
|
+
- ✅ Compressed release binaries, download resume/retry/timeout, Linux ARM64 detection
|
|
226
|
+
- ✅ Codex MCP entries are repaired and enabled without depending on the Codex CLI binary
|
|
227
|
+
- ✅ Release tags run tests, lint, npm tests, indexing, and golden tasks v3 before publishing assets
|
|
228
|
+
|
|
200
229
|
### v0.1.31
|
|
201
230
|
- ✅ `index_project` is idempotent again and ignores CCM's own generated index files
|
|
202
231
|
- ✅ `search_code` falls back cleanly when embeddings are disabled
|
package/bin/ccm.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { spawn
|
|
3
|
+
const { spawn } = 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
|
+
const zlib = require('zlib');
|
|
10
|
+
const { pipeline } = require('stream/promises');
|
|
9
11
|
|
|
10
12
|
function resolvePackageVersion() {
|
|
11
13
|
if (process.env.CCM_BINARY_VERSION && process.env.CCM_BINARY_VERSION.trim() !== '') {
|
|
@@ -33,16 +35,15 @@ const VERSION = resolvePackageVersion();
|
|
|
33
35
|
const REPO = 'senoldogann/LLM-Context-Manager';
|
|
34
36
|
const BIN_DIR = path.join(os.homedir(), '.ccm', 'bin');
|
|
35
37
|
const CHECKSUMS_FILE = 'checksums.txt';
|
|
38
|
+
const DOWNLOAD_TIMEOUT_MS = positiveInteger(process.env.CCM_DOWNLOAD_TIMEOUT_MS, 120_000);
|
|
39
|
+
const DOWNLOAD_ATTEMPTS = positiveInteger(process.env.CCM_DOWNLOAD_ATTEMPTS, 3);
|
|
36
40
|
let checksumCache = null;
|
|
37
41
|
|
|
38
42
|
const MCP_SERVER_NAME = 'context-manager';
|
|
39
43
|
const MCP_COMMAND = 'npx';
|
|
40
44
|
const MCP_ARGS = ['-y', `@senoldogann/context-manager@${VERSION}`, 'mcp'];
|
|
41
45
|
const MCP_ENV = {
|
|
42
|
-
RUST_LOG: 'info'
|
|
43
|
-
CCM_PROJECT_ROOT: process.cwd(),
|
|
44
|
-
CCM_ALLOWED_ROOTS: process.cwd(),
|
|
45
|
-
CCM_REQUIRE_ALLOWED_ROOTS: '1'
|
|
46
|
+
RUST_LOG: 'info'
|
|
46
47
|
};
|
|
47
48
|
const ALLOWED_REDIRECT_HOSTS = new Set([
|
|
48
49
|
'github.com',
|
|
@@ -50,6 +51,11 @@ const ALLOWED_REDIRECT_HOSTS = new Set([
|
|
|
50
51
|
'release-assets.githubusercontent.com'
|
|
51
52
|
]);
|
|
52
53
|
|
|
54
|
+
function positiveInteger(raw, fallback) {
|
|
55
|
+
const value = Number(raw);
|
|
56
|
+
return Number.isSafeInteger(value) && value > 0 ? value : fallback;
|
|
57
|
+
}
|
|
58
|
+
|
|
53
59
|
function allowUnverifiedBinaries() {
|
|
54
60
|
const raw = process.env.CCM_ALLOW_UNVERIFIED_BINARIES || process.env.CCM_SKIP_CHECKSUM || '';
|
|
55
61
|
return ['1', 'true', 'yes'].includes(raw.toLowerCase());
|
|
@@ -156,82 +162,92 @@ function writeJsonAtomic(configPath, value) {
|
|
|
156
162
|
}
|
|
157
163
|
|
|
158
164
|
function installCodexConfig() {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
if (listResult.error) {
|
|
164
|
-
return false;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (listResult.status !== 0) {
|
|
168
|
-
console.warn(`[CCM] Codex MCP inspection failed: ${listResult.stderr.trim()}`);
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
let existingServers = [];
|
|
173
|
-
try {
|
|
174
|
-
existingServers = JSON.parse(listResult.stdout);
|
|
175
|
-
} catch (error) {
|
|
176
|
-
console.warn('[CCM] Could not parse Codex MCP list output.');
|
|
165
|
+
const codexDirectory = path.join(os.homedir(), '.codex');
|
|
166
|
+
if (!fs.existsSync(codexDirectory)) {
|
|
177
167
|
return false;
|
|
178
168
|
}
|
|
169
|
+
const configPath = path.join(codexDirectory, 'config.toml');
|
|
170
|
+
installCodexTomlConfig(configPath, process.cwd(), VERSION);
|
|
171
|
+
console.log('[CCM] ✓ Successfully updated: ~/.codex/config.toml');
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
179
174
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
175
|
+
function installCodexTomlConfig(configPath, projectRoot, version) {
|
|
176
|
+
let content = '';
|
|
177
|
+
if (fs.existsSync(configPath)) {
|
|
178
|
+
content = fs.readFileSync(configPath, 'utf8');
|
|
179
|
+
fs.copyFileSync(configPath, `${configPath}.bak`);
|
|
184
180
|
}
|
|
185
181
|
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
'--',
|
|
199
|
-
MCP_COMMAND,
|
|
200
|
-
...MCP_ARGS
|
|
201
|
-
];
|
|
202
|
-
const addResult = spawnSync('codex', addArgs, {
|
|
203
|
-
encoding: 'utf8'
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
if (addResult.status !== 0) {
|
|
207
|
-
console.warn(`[CCM] Codex MCP install failed: ${addResult.stderr.trim()}`);
|
|
208
|
-
return false;
|
|
182
|
+
const sectionPrefix = `mcp_servers.${MCP_SERVER_NAME}`;
|
|
183
|
+
const lines = content.split(/\r?\n/);
|
|
184
|
+
const preserved = [];
|
|
185
|
+
let removing = false;
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
const header = line.trim().match(/^\[([^\]]+)\]$/);
|
|
188
|
+
if (header) {
|
|
189
|
+
removing = header[1] === sectionPrefix || header[1].startsWith(`${sectionPrefix}.`);
|
|
190
|
+
}
|
|
191
|
+
if (!removing) {
|
|
192
|
+
preserved.push(line);
|
|
193
|
+
}
|
|
209
194
|
}
|
|
210
195
|
|
|
211
|
-
|
|
212
|
-
|
|
196
|
+
const quote = (value) => JSON.stringify(String(value));
|
|
197
|
+
const block = [
|
|
198
|
+
`[${sectionPrefix}]`,
|
|
199
|
+
`command = ${quote(MCP_COMMAND)}`,
|
|
200
|
+
`args = [${['-y', `@senoldogann/context-manager@${version}`, 'mcp'].map(quote).join(', ')}]`,
|
|
201
|
+
'enabled = true',
|
|
202
|
+
'',
|
|
203
|
+
`[${sectionPrefix}.env]`,
|
|
204
|
+
`RUST_LOG = ${quote('info')}`,
|
|
205
|
+
`CCM_PROJECT_ROOT = ${quote(projectRoot)}`,
|
|
206
|
+
`CCM_ALLOWED_ROOTS = ${quote(projectRoot)}`,
|
|
207
|
+
`CCM_REQUIRE_ALLOWED_ROOTS = ${quote('1')}`
|
|
208
|
+
].join('\n');
|
|
209
|
+
|
|
210
|
+
const next = `${preserved.join('\n').trimEnd()}\n\n${block}\n`.replace(/^\n+/, '');
|
|
211
|
+
writeTextAtomic(configPath, next);
|
|
213
212
|
}
|
|
214
213
|
|
|
215
214
|
function createUniqueTmpPath(binPath) {
|
|
216
215
|
return `${binPath}.${process.pid}.${Date.now()}.tmp`;
|
|
217
216
|
}
|
|
218
217
|
|
|
218
|
+
function writeTextAtomic(filePath, content) {
|
|
219
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
220
|
+
try {
|
|
221
|
+
fs.writeFileSync(tempPath, content, { encoding: 'utf8', mode: 0o600 });
|
|
222
|
+
fs.renameSync(tempPath, filePath);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
219
229
|
async function getBinaryFor(commandName) {
|
|
220
230
|
const platform = os.platform();
|
|
221
231
|
const arch = os.arch();
|
|
222
232
|
|
|
223
|
-
let target
|
|
233
|
+
let target;
|
|
224
234
|
if (platform === 'darwin') {
|
|
225
|
-
|
|
235
|
+
if (arch === 'arm64') target = 'aarch64-apple-darwin';
|
|
236
|
+
if (arch === 'x64') target = 'x86_64-apple-darwin';
|
|
226
237
|
} else if (platform === 'linux') {
|
|
227
|
-
target = '
|
|
238
|
+
if (arch === 'arm64') target = 'aarch64-unknown-linux-gnu';
|
|
239
|
+
if (arch === 'x64') target = 'x86_64-unknown-linux-gnu';
|
|
228
240
|
} else if (platform === 'win32') {
|
|
229
|
-
target = 'x86_64-pc-windows-msvc.exe';
|
|
241
|
+
if (arch === 'x64') target = 'x86_64-pc-windows-msvc.exe';
|
|
242
|
+
}
|
|
243
|
+
if (!target) {
|
|
244
|
+
throw new Error(`Unsupported platform: ${platform}/${arch}`);
|
|
230
245
|
}
|
|
231
246
|
|
|
232
247
|
const binFilename = `${commandName}-v${VERSION}-${target}`;
|
|
233
248
|
const binPath = path.join(BIN_DIR, binFilename);
|
|
234
249
|
const remoteFilename = `${commandName}-${target}`;
|
|
250
|
+
const compressedFilename = `${remoteFilename}.gz`;
|
|
235
251
|
|
|
236
252
|
// If file exists, ensure it is executable
|
|
237
253
|
if (fs.existsSync(binPath)) {
|
|
@@ -249,12 +265,29 @@ async function getBinaryFor(commandName) {
|
|
|
249
265
|
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
250
266
|
}
|
|
251
267
|
|
|
252
|
-
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${commandName}-${target}`;
|
|
253
268
|
const tmpPath = createUniqueTmpPath(binPath);
|
|
269
|
+
const compressedPath = `${binPath}.gz.download`;
|
|
270
|
+
const rawPath = `${binPath}.download`;
|
|
254
271
|
|
|
255
272
|
try {
|
|
256
|
-
|
|
257
|
-
|
|
273
|
+
const compressedUrl =
|
|
274
|
+
`https://github.com/${REPO}/releases/download/v${VERSION}/${compressedFilename}`;
|
|
275
|
+
try {
|
|
276
|
+
await downloadFileWithRetry(compressedUrl, compressedPath);
|
|
277
|
+
await verifyChecksum(compressedPath, [compressedFilename]);
|
|
278
|
+
await extractGzip(compressedPath, tmpPath);
|
|
279
|
+
fs.unlinkSync(compressedPath);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (error.statusCode !== 404) {
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
if (fs.existsSync(compressedPath)) fs.unlinkSync(compressedPath);
|
|
285
|
+
const rawUrl =
|
|
286
|
+
`https://github.com/${REPO}/releases/download/v${VERSION}/${remoteFilename}`;
|
|
287
|
+
await downloadFileWithRetry(rawUrl, rawPath);
|
|
288
|
+
await verifyChecksum(rawPath, [remoteFilename, binFilename]);
|
|
289
|
+
fs.renameSync(rawPath, tmpPath);
|
|
290
|
+
}
|
|
258
291
|
fs.chmodSync(tmpPath, '755');
|
|
259
292
|
if (fs.existsSync(binPath)) {
|
|
260
293
|
fs.unlinkSync(tmpPath);
|
|
@@ -263,12 +296,24 @@ async function getBinaryFor(commandName) {
|
|
|
263
296
|
fs.renameSync(tmpPath, binPath);
|
|
264
297
|
} catch (err) {
|
|
265
298
|
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
|
|
299
|
+
if (/checksum|gzip|header|unexpected end/i.test(err.message || '')) {
|
|
300
|
+
if (fs.existsSync(compressedPath)) fs.unlinkSync(compressedPath);
|
|
301
|
+
if (fs.existsSync(rawPath)) fs.unlinkSync(rawPath);
|
|
302
|
+
}
|
|
266
303
|
throw err;
|
|
267
304
|
}
|
|
268
305
|
|
|
269
306
|
return binPath;
|
|
270
307
|
}
|
|
271
308
|
|
|
309
|
+
async function extractGzip(source, destination) {
|
|
310
|
+
await pipeline(
|
|
311
|
+
fs.createReadStream(source),
|
|
312
|
+
zlib.createGunzip(),
|
|
313
|
+
fs.createWriteStream(destination)
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
272
317
|
async function getBinary() {
|
|
273
318
|
// Detect which tool to run
|
|
274
319
|
let commandName = 'ccm-cli';
|
|
@@ -287,50 +332,87 @@ async function getBinary() {
|
|
|
287
332
|
return await getBinaryFor(commandName);
|
|
288
333
|
}
|
|
289
334
|
|
|
290
|
-
function downloadFile(url, dest) {
|
|
335
|
+
function downloadFile(url, dest, redirectsRemaining = 5) {
|
|
291
336
|
return new Promise((resolve, reject) => {
|
|
292
337
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
293
|
-
|
|
338
|
+
const existingBytes = fs.existsSync(dest) ? fs.statSync(dest).size : 0;
|
|
339
|
+
const headers = existingBytes > 0 ? { Range: `bytes=${existingBytes}-` } : {};
|
|
340
|
+
const request = https.get(url, { headers }, (response) => {
|
|
294
341
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
342
|
+
response.resume();
|
|
343
|
+
if (redirectsRemaining <= 0) {
|
|
344
|
+
return reject(new Error('Too many redirects while downloading binary'));
|
|
345
|
+
}
|
|
295
346
|
try {
|
|
296
347
|
const redirectUrl = resolveRedirectUrl(url, response.headers.location);
|
|
297
|
-
return downloadFile(redirectUrl, dest)
|
|
348
|
+
return downloadFile(redirectUrl, dest, redirectsRemaining - 1)
|
|
349
|
+
.then(resolve)
|
|
350
|
+
.catch(reject);
|
|
298
351
|
} catch (error) {
|
|
299
352
|
return reject(error);
|
|
300
353
|
}
|
|
301
354
|
}
|
|
302
|
-
if (response.statusCode
|
|
303
|
-
|
|
355
|
+
if (response.statusCode === 416 && existingBytes > 0) {
|
|
356
|
+
response.resume();
|
|
357
|
+
return resolve();
|
|
304
358
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
})
|
|
315
|
-
|
|
359
|
+
if (response.statusCode !== 200 && response.statusCode !== 206) {
|
|
360
|
+
response.resume();
|
|
361
|
+
const error = new Error(`Failed to download: ${response.statusCode}`);
|
|
362
|
+
error.statusCode = response.statusCode;
|
|
363
|
+
return reject(error);
|
|
364
|
+
}
|
|
365
|
+
const append = response.statusCode === 206 && existingBytes > 0;
|
|
366
|
+
const file = fs.createWriteStream(dest, { flags: append ? 'a' : 'w' });
|
|
367
|
+
pipeline(response, file).then(resolve).catch(reject);
|
|
368
|
+
});
|
|
369
|
+
request.setTimeout(DOWNLOAD_TIMEOUT_MS, () => {
|
|
370
|
+
request.destroy(new Error(`Download timed out after ${DOWNLOAD_TIMEOUT_MS}ms`));
|
|
371
|
+
});
|
|
372
|
+
request.on('error', (err) => {
|
|
316
373
|
reject(err);
|
|
317
374
|
});
|
|
318
375
|
});
|
|
319
376
|
}
|
|
320
377
|
|
|
321
|
-
function
|
|
378
|
+
async function downloadFileWithRetry(url, dest, attempts = DOWNLOAD_ATTEMPTS) {
|
|
379
|
+
let lastError;
|
|
380
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
381
|
+
try {
|
|
382
|
+
await downloadFile(url, dest);
|
|
383
|
+
return;
|
|
384
|
+
} catch (error) {
|
|
385
|
+
lastError = error;
|
|
386
|
+
if (error.statusCode === 404 || attempt === attempts) break;
|
|
387
|
+
console.warn(`[CCM] Download interrupted; retrying (${attempt}/${attempts})...`);
|
|
388
|
+
await new Promise((resolve) => setTimeout(resolve, attempt * 500));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
throw lastError;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function downloadText(url, redirectsRemaining = 5) {
|
|
322
395
|
return new Promise((resolve, reject) => {
|
|
323
|
-
https.get(url, (response) => {
|
|
396
|
+
const request = https.get(url, (response) => {
|
|
324
397
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
398
|
+
response.resume();
|
|
399
|
+
if (redirectsRemaining <= 0) {
|
|
400
|
+
return reject(new Error('Too many redirects while downloading checksums'));
|
|
401
|
+
}
|
|
325
402
|
try {
|
|
326
403
|
const redirectUrl = resolveRedirectUrl(url, response.headers.location);
|
|
327
|
-
return downloadText(redirectUrl)
|
|
404
|
+
return downloadText(redirectUrl, redirectsRemaining - 1)
|
|
405
|
+
.then(resolve)
|
|
406
|
+
.catch(reject);
|
|
328
407
|
} catch (error) {
|
|
329
408
|
return reject(error);
|
|
330
409
|
}
|
|
331
410
|
}
|
|
332
411
|
if (response.statusCode !== 200) {
|
|
333
|
-
|
|
412
|
+
response.resume();
|
|
413
|
+
const error = new Error(`Failed to download: ${response.statusCode}`);
|
|
414
|
+
error.statusCode = response.statusCode;
|
|
415
|
+
return reject(error);
|
|
334
416
|
}
|
|
335
417
|
let data = '';
|
|
336
418
|
response.setEncoding('utf8');
|
|
@@ -338,7 +420,11 @@ function downloadText(url) {
|
|
|
338
420
|
data += chunk;
|
|
339
421
|
});
|
|
340
422
|
response.on('end', () => resolve(data));
|
|
341
|
-
})
|
|
423
|
+
});
|
|
424
|
+
request.setTimeout(DOWNLOAD_TIMEOUT_MS, () => {
|
|
425
|
+
request.destroy(new Error(`Download timed out after ${DOWNLOAD_TIMEOUT_MS}ms`));
|
|
426
|
+
});
|
|
427
|
+
request.on('error', reject);
|
|
342
428
|
});
|
|
343
429
|
}
|
|
344
430
|
|
|
@@ -453,6 +539,8 @@ if (require.main === module) {
|
|
|
453
539
|
module.exports = {
|
|
454
540
|
MCP_ARGS,
|
|
455
541
|
MCP_ENV,
|
|
542
|
+
extractGzip,
|
|
543
|
+
installCodexTomlConfig,
|
|
456
544
|
installJsonConfig,
|
|
457
545
|
parseChecksums,
|
|
458
546
|
resolveRedirectUrl,
|
package/package.json
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@senoldogann/context-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "LLM Context Manager MCP Server & CLI wrapper using npx",
|
|
5
|
-
"repository":
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/senoldogann/LLM-Context-Manager.git"
|
|
8
|
+
},
|
|
6
9
|
"homepage": "https://github.com/senoldogann/LLM-Context-Manager",
|
|
7
10
|
"bugs": {
|
|
8
11
|
"url": "https://github.com/senoldogann/LLM-Context-Manager/issues"
|
|
9
12
|
},
|
|
10
13
|
"main": "bin/ccm.js",
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
11
18
|
"bin": {
|
|
12
19
|
"ccm-cli": "bin/ccm.js",
|
|
13
20
|
"ccm-mcp": "bin/ccm.js"
|
package/test/installer.test.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
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
|
-
});
|