@senoldogann/context-manager 0.1.31 → 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 +201 -70
- package/package.json +10 -3
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,11 +35,13 @@ 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
|
-
const MCP_ARGS = ['-y',
|
|
44
|
+
const MCP_ARGS = ['-y', `@senoldogann/context-manager@${VERSION}`, 'mcp'];
|
|
41
45
|
const MCP_ENV = {
|
|
42
46
|
RUST_LOG: 'info'
|
|
43
47
|
};
|
|
@@ -47,6 +51,11 @@ const ALLOWED_REDIRECT_HOSTS = new Set([
|
|
|
47
51
|
'release-assets.githubusercontent.com'
|
|
48
52
|
]);
|
|
49
53
|
|
|
54
|
+
function positiveInteger(raw, fallback) {
|
|
55
|
+
const value = Number(raw);
|
|
56
|
+
return Number.isSafeInteger(value) && value > 0 ? value : fallback;
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
function allowUnverifiedBinaries() {
|
|
51
60
|
const raw = process.env.CCM_ALLOW_UNVERIFIED_BINARIES || process.env.CCM_SKIP_CHECKSUM || '';
|
|
52
61
|
return ['1', 'true', 'yes'].includes(raw.toLowerCase());
|
|
@@ -114,12 +123,15 @@ function installJsonConfig(configPath, mcpConfig) {
|
|
|
114
123
|
|
|
115
124
|
let config = { mcpServers: {} };
|
|
116
125
|
if (fs.existsSync(configPath)) {
|
|
126
|
+
const backupPath = `${configPath}.bak`;
|
|
127
|
+
fs.copyFileSync(configPath, backupPath);
|
|
117
128
|
try {
|
|
118
129
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
119
130
|
config = JSON.parse(content);
|
|
120
|
-
fs.copyFileSync(configPath, `${configPath}.bak`);
|
|
121
131
|
} catch (e) {
|
|
122
|
-
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Could not parse ${configPath}. The original file was preserved and copied to ${backupPath}.`
|
|
134
|
+
);
|
|
123
135
|
}
|
|
124
136
|
}
|
|
125
137
|
|
|
@@ -128,79 +140,114 @@ function installJsonConfig(configPath, mcpConfig) {
|
|
|
128
140
|
}
|
|
129
141
|
|
|
130
142
|
config.mcpServers[MCP_SERVER_NAME] = mcpConfig;
|
|
131
|
-
|
|
143
|
+
writeJsonAtomic(configPath, config);
|
|
132
144
|
console.log(`[CCM] ✓ Successfully updated: ${configPath}`);
|
|
133
145
|
return true;
|
|
134
146
|
}
|
|
135
147
|
|
|
136
|
-
function
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
function writeJsonAtomic(configPath, value) {
|
|
149
|
+
const tempPath = `${configPath}.${process.pid}.${Date.now()}.tmp`;
|
|
150
|
+
try {
|
|
151
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, {
|
|
152
|
+
encoding: 'utf8',
|
|
153
|
+
mode: 0o600
|
|
154
|
+
});
|
|
155
|
+
fs.renameSync(tempPath, configPath);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (fs.existsSync(tempPath)) {
|
|
158
|
+
fs.unlinkSync(tempPath);
|
|
159
|
+
}
|
|
160
|
+
throw error;
|
|
143
161
|
}
|
|
162
|
+
}
|
|
144
163
|
|
|
145
|
-
|
|
146
|
-
|
|
164
|
+
function installCodexConfig() {
|
|
165
|
+
const codexDirectory = path.join(os.homedir(), '.codex');
|
|
166
|
+
if (!fs.existsSync(codexDirectory)) {
|
|
147
167
|
return false;
|
|
148
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
|
+
}
|
|
149
174
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return false;
|
|
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`);
|
|
156
180
|
}
|
|
157
181
|
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
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);
|
|
167
193
|
}
|
|
168
194
|
}
|
|
169
195
|
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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);
|
|
182
212
|
}
|
|
183
213
|
|
|
184
214
|
function createUniqueTmpPath(binPath) {
|
|
185
215
|
return `${binPath}.${process.pid}.${Date.now()}.tmp`;
|
|
186
216
|
}
|
|
187
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
|
+
|
|
188
229
|
async function getBinaryFor(commandName) {
|
|
189
230
|
const platform = os.platform();
|
|
190
231
|
const arch = os.arch();
|
|
191
232
|
|
|
192
|
-
let target
|
|
233
|
+
let target;
|
|
193
234
|
if (platform === 'darwin') {
|
|
194
|
-
|
|
235
|
+
if (arch === 'arm64') target = 'aarch64-apple-darwin';
|
|
236
|
+
if (arch === 'x64') target = 'x86_64-apple-darwin';
|
|
195
237
|
} else if (platform === 'linux') {
|
|
196
|
-
target = '
|
|
238
|
+
if (arch === 'arm64') target = 'aarch64-unknown-linux-gnu';
|
|
239
|
+
if (arch === 'x64') target = 'x86_64-unknown-linux-gnu';
|
|
197
240
|
} else if (platform === 'win32') {
|
|
198
|
-
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}`);
|
|
199
245
|
}
|
|
200
246
|
|
|
201
247
|
const binFilename = `${commandName}-v${VERSION}-${target}`;
|
|
202
248
|
const binPath = path.join(BIN_DIR, binFilename);
|
|
203
249
|
const remoteFilename = `${commandName}-${target}`;
|
|
250
|
+
const compressedFilename = `${remoteFilename}.gz`;
|
|
204
251
|
|
|
205
252
|
// If file exists, ensure it is executable
|
|
206
253
|
if (fs.existsSync(binPath)) {
|
|
@@ -218,12 +265,29 @@ async function getBinaryFor(commandName) {
|
|
|
218
265
|
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
219
266
|
}
|
|
220
267
|
|
|
221
|
-
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${commandName}-${target}`;
|
|
222
268
|
const tmpPath = createUniqueTmpPath(binPath);
|
|
269
|
+
const compressedPath = `${binPath}.gz.download`;
|
|
270
|
+
const rawPath = `${binPath}.download`;
|
|
223
271
|
|
|
224
272
|
try {
|
|
225
|
-
|
|
226
|
-
|
|
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
|
+
}
|
|
227
291
|
fs.chmodSync(tmpPath, '755');
|
|
228
292
|
if (fs.existsSync(binPath)) {
|
|
229
293
|
fs.unlinkSync(tmpPath);
|
|
@@ -232,12 +296,24 @@ async function getBinaryFor(commandName) {
|
|
|
232
296
|
fs.renameSync(tmpPath, binPath);
|
|
233
297
|
} catch (err) {
|
|
234
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
|
+
}
|
|
235
303
|
throw err;
|
|
236
304
|
}
|
|
237
305
|
|
|
238
306
|
return binPath;
|
|
239
307
|
}
|
|
240
308
|
|
|
309
|
+
async function extractGzip(source, destination) {
|
|
310
|
+
await pipeline(
|
|
311
|
+
fs.createReadStream(source),
|
|
312
|
+
zlib.createGunzip(),
|
|
313
|
+
fs.createWriteStream(destination)
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
241
317
|
async function getBinary() {
|
|
242
318
|
// Detect which tool to run
|
|
243
319
|
let commandName = 'ccm-cli';
|
|
@@ -256,50 +332,87 @@ async function getBinary() {
|
|
|
256
332
|
return await getBinaryFor(commandName);
|
|
257
333
|
}
|
|
258
334
|
|
|
259
|
-
function downloadFile(url, dest) {
|
|
335
|
+
function downloadFile(url, dest, redirectsRemaining = 5) {
|
|
260
336
|
return new Promise((resolve, reject) => {
|
|
261
337
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
262
|
-
|
|
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) => {
|
|
263
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
|
+
}
|
|
264
346
|
try {
|
|
265
347
|
const redirectUrl = resolveRedirectUrl(url, response.headers.location);
|
|
266
|
-
return downloadFile(redirectUrl, dest)
|
|
348
|
+
return downloadFile(redirectUrl, dest, redirectsRemaining - 1)
|
|
349
|
+
.then(resolve)
|
|
350
|
+
.catch(reject);
|
|
267
351
|
} catch (error) {
|
|
268
352
|
return reject(error);
|
|
269
353
|
}
|
|
270
354
|
}
|
|
271
|
-
if (response.statusCode
|
|
272
|
-
|
|
355
|
+
if (response.statusCode === 416 && existingBytes > 0) {
|
|
356
|
+
response.resume();
|
|
357
|
+
return resolve();
|
|
273
358
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
})
|
|
284
|
-
|
|
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) => {
|
|
285
373
|
reject(err);
|
|
286
374
|
});
|
|
287
375
|
});
|
|
288
376
|
}
|
|
289
377
|
|
|
290
|
-
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) {
|
|
291
395
|
return new Promise((resolve, reject) => {
|
|
292
|
-
https.get(url, (response) => {
|
|
396
|
+
const request = https.get(url, (response) => {
|
|
293
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
|
+
}
|
|
294
402
|
try {
|
|
295
403
|
const redirectUrl = resolveRedirectUrl(url, response.headers.location);
|
|
296
|
-
return downloadText(redirectUrl)
|
|
404
|
+
return downloadText(redirectUrl, redirectsRemaining - 1)
|
|
405
|
+
.then(resolve)
|
|
406
|
+
.catch(reject);
|
|
297
407
|
} catch (error) {
|
|
298
408
|
return reject(error);
|
|
299
409
|
}
|
|
300
410
|
}
|
|
301
411
|
if (response.statusCode !== 200) {
|
|
302
|
-
|
|
412
|
+
response.resume();
|
|
413
|
+
const error = new Error(`Failed to download: ${response.statusCode}`);
|
|
414
|
+
error.statusCode = response.statusCode;
|
|
415
|
+
return reject(error);
|
|
303
416
|
}
|
|
304
417
|
let data = '';
|
|
305
418
|
response.setEncoding('utf8');
|
|
@@ -307,7 +420,11 @@ function downloadText(url) {
|
|
|
307
420
|
data += chunk;
|
|
308
421
|
});
|
|
309
422
|
response.on('end', () => resolve(data));
|
|
310
|
-
})
|
|
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);
|
|
311
428
|
});
|
|
312
429
|
}
|
|
313
430
|
|
|
@@ -415,4 +532,18 @@ async function main() {
|
|
|
415
532
|
}
|
|
416
533
|
}
|
|
417
534
|
|
|
418
|
-
main
|
|
535
|
+
if (require.main === module) {
|
|
536
|
+
main();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
module.exports = {
|
|
540
|
+
MCP_ARGS,
|
|
541
|
+
MCP_ENV,
|
|
542
|
+
extractGzip,
|
|
543
|
+
installCodexTomlConfig,
|
|
544
|
+
installJsonConfig,
|
|
545
|
+
parseChecksums,
|
|
546
|
+
resolveRedirectUrl,
|
|
547
|
+
verifyChecksum,
|
|
548
|
+
writeJsonAtomic
|
|
549
|
+
};
|
package/package.json
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
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"
|
|
14
21
|
},
|
|
15
22
|
"scripts": {
|
|
16
|
-
"test": "
|
|
23
|
+
"test": "node --test test/*.test.js"
|
|
17
24
|
},
|
|
18
25
|
"keywords": [
|
|
19
26
|
"mcp",
|