@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.
Files changed (3) hide show
  1. package/README.md +31 -2
  2. package/bin/ccm.js +201 -70
  3. 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
  [![npm](https://img.shields.io/npm/v/@senoldogann/context-manager?color=orange)](https://www.npmjs.com/package/@senoldogann/context-manager)
8
10
  [![MCP](https://img.shields.io/badge/MCP-Compatible-blue)](https://modelcontextprotocol.io/)
9
11
  [![Rust](https://img.shields.io/badge/Built%20With-Rust-orange.svg)](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. Binary download from GitHub Releases
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
- - npm publication is a manual step from `npm/` after the GitHub Release assets are attached
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, spawnSync } = require('child_process');
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', '@senoldogann/context-manager', 'mcp'];
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
- console.warn(`[CCM] Could not parse ${configPath}, creating backup and starting fresh section.`);
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
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
143
+ writeJsonAtomic(configPath, config);
132
144
  console.log(`[CCM] ✓ Successfully updated: ${configPath}`);
133
145
  return true;
134
146
  }
135
147
 
136
- function installCodexConfig() {
137
- const listResult = spawnSync('codex', ['mcp', 'list', '--json'], {
138
- encoding: 'utf8'
139
- });
140
-
141
- if (listResult.error) {
142
- return false;
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
- if (listResult.status !== 0) {
146
- console.warn(`[CCM] Codex MCP inspection failed: ${listResult.stderr.trim()}`);
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
- let existingServers = [];
151
- try {
152
- existingServers = JSON.parse(listResult.stdout);
153
- } catch (error) {
154
- console.warn('[CCM] Could not parse Codex MCP list output.');
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 existing = existingServers.find((server) => server.name === MCP_SERVER_NAME);
159
- if (existing) {
160
- const removeResult = spawnSync('codex', ['mcp', 'remove', MCP_SERVER_NAME], {
161
- encoding: 'utf8'
162
- });
163
-
164
- if (removeResult.status !== 0) {
165
- console.warn(`[CCM] Codex MCP removal failed: ${removeResult.stderr.trim()}`);
166
- 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);
167
193
  }
168
194
  }
169
195
 
170
- const addArgs = ['mcp', 'add', MCP_SERVER_NAME, '--env', 'RUST_LOG=info', '--', MCP_COMMAND, ...MCP_ARGS];
171
- const addResult = spawnSync('codex', addArgs, {
172
- encoding: 'utf8'
173
- });
174
-
175
- if (addResult.status !== 0) {
176
- console.warn(`[CCM] Codex MCP install failed: ${addResult.stderr.trim()}`);
177
- return false;
178
- }
179
-
180
- console.log('[CCM] Successfully updated: ~/.codex/config.toml');
181
- return true;
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
- target = arch === 'arm64' ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin';
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 = 'x86_64-unknown-linux-gnu';
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
- await downloadFile(url, tmpPath);
226
- await verifyChecksum(tmpPath, [remoteFilename, binFilename]);
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
- https.get(url, (response) => {
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).then(resolve).catch(reject);
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 !== 200) {
272
- return reject(new Error(`Failed to download: ${response.statusCode}`));
355
+ if (response.statusCode === 416 && existingBytes > 0) {
356
+ response.resume();
357
+ return resolve();
273
358
  }
274
- const file = fs.createWriteStream(dest);
275
- response.pipe(file);
276
- file.on('finish', () => {
277
- file.close(resolve);
278
- });
279
- file.on('error', (err) => {
280
- fs.unlink(dest, () => { });
281
- reject(err);
282
- });
283
- }).on('error', (err) => {
284
- fs.unlink(dest, () => { });
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 downloadText(url) {
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).then(resolve).catch(reject);
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
- return reject(new Error(`Failed to download: ${response.statusCode}`));
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
- }).on('error', reject);
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.1.31",
3
+ "version": "0.3.0",
4
4
  "description": "LLM Context Manager MCP Server & CLI wrapper using npx",
5
- "repository": "https://github.com/senoldogann/LLM-Context-Manager",
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": "echo \"Error: no test specified\" && exit 1"
23
+ "test": "node --test test/*.test.js"
17
24
  },
18
25
  "keywords": [
19
26
  "mcp",