@compr/opscontext-mcp 2.4.0 → 2.4.1
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/CHANGELOG.md +58 -0
- package/README.md +23 -2
- package/dist/agents.js +120 -70
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +62 -8
- package/dist/config.d.ts +37 -0
- package/dist/config.js +85 -1
- package/dist/index.js +6 -4
- package/package.json +1 -1
- package/skills/opscontext/SKILL.md +17 -15
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to OpsContext for AI Agents (previously ContextEngine — MCP server + CLI) are documented here.
|
|
4
4
|
|
|
5
|
+
> Entries for 2.2.0 through 2.4.0 were not backfilled here; see `docs/sessions/SESSION_19` through `SESSION_21` for those releases.
|
|
6
|
+
|
|
7
|
+
## [2.4.1] — 2026-08-16 — `score` scope is now explicit: current project by default, `--all` to opt into the fleet
|
|
8
|
+
|
|
9
|
+
### Changed (behaviour — read this before upgrading)
|
|
10
|
+
|
|
11
|
+
- **`score` with no argument now scores only the current project**, walking up from the working
|
|
12
|
+
directory to the enclosing repo root. It previously scored all discovered projects **and wrote
|
|
13
|
+
a `SCORE.md` into every one of them** — which dirtied 26 repositories on the author's machine,
|
|
14
|
+
several of them auto-pushing on commit. Fleet-wide scoring now requires `--all`.
|
|
15
|
+
Locked as `[SCORE-FLEET-IS-OPT-IN]`.
|
|
16
|
+
- `score --all` prints how many projects it is about to write into before doing it, and
|
|
17
|
+
`--all` combined with a project argument is now a hard error rather than a silently ignored flag.
|
|
18
|
+
- The MCP `score_project` tool keeps its fleet-wide default — it never writes `SCORE.md`, so it
|
|
19
|
+
does not carry the hazard. The write is what must be asked for, not the scan.
|
|
20
|
+
|
|
21
|
+
- **`score` with no argument now refuses to run outside a project** instead of scoring whatever
|
|
22
|
+
directory it is in. Run from `~/Projects` — a *container* of projects — it previously scored
|
|
23
|
+
the container and wrote a `SCORE.md` claiming `Projects: 27/100 (F)`; from `/` it would do the
|
|
24
|
+
same to the filesystem root. It now exits 1 and names the three ways to proceed.
|
|
25
|
+
Locked as `[SCORE-CWD-MUST-BE-A-PROJECT]`.
|
|
26
|
+
|
|
27
|
+
### Fixed (documentation that shipped a working footgun)
|
|
28
|
+
|
|
29
|
+
- **`skills/opscontext/SKILL.md` told agents to run `npx @compr/contextengine-mcp`** — a package
|
|
30
|
+
name npm freezes at **1.23.1**. Following the shipped skill downloaded a June scorer and ran a
|
|
31
|
+
fleet-wide write with a 94-point rubric that reports root-level `copilot-instructions.md` as
|
|
32
|
+
`Missing`. Ten call sites corrected, including three MCP-config blocks that would have
|
|
33
|
+
installed the frozen version as a long-running server. `skills/` now has zero references to it.
|
|
34
|
+
- Same correction to the `.claude/settings.json` post-push reminder.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **`score` accepts a path as well as a project name** — `score ~/Projects/PLANK.io`,
|
|
39
|
+
`score ../PLANK.io`, or an absolute path. Paths outside configured workspaces work too.
|
|
40
|
+
Previously `score /Users/yan/Projects/PLANK.io` failed with "Project not found" while listing
|
|
41
|
+
`PLANK.io` among the available projects — the lookup only ever compared basenames, so the
|
|
42
|
+
directory was never inspected. Locked as `[SCORE-ACCEPTS-PATH]`.
|
|
43
|
+
- Same path support in the MCP `score_project` tool (strictly more accepting; no default changed).
|
|
44
|
+
- `tests/project-resolution.test.ts` — 11 regression tests covering name/path/symlink/
|
|
45
|
+
file-not-a-directory/case-insensitivity/dotted names, and `findProjectRoot` termination.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- Polyglot repositories are scored against the languages they actually use (released here; committed
|
|
50
|
+
in 2.4.0's wake): test directories are aggregated across ecosystems rather than the first match
|
|
51
|
+
winning, `.dart` tests are counted, and Dart tooling (`analysis_options.yaml`) satisfies the
|
|
52
|
+
type-checking check instead of the project being marked down for a missing `tsconfig.json`.
|
|
53
|
+
PLANK.io 98%; KONIVE.com correctly reports 102 test files across two directories.
|
|
54
|
+
|
|
55
|
+
### Investigated — no change
|
|
56
|
+
|
|
57
|
+
- A report that the generated pre-commit hook regenerates and re-stages `SCORE.md` mid-commit
|
|
58
|
+
**does not reproduce**. `generatePreCommitHook()` only stats mtimes and greps staged files for
|
|
59
|
+
secrets; there is no score invocation and no `git add` in it, nor in the older custom hook the
|
|
60
|
+
reporting repo actually has installed. The observed empty commit is consistent with the
|
|
61
|
+
fleet-wide-write default above, which this release removes.
|
|
62
|
+
|
|
5
63
|
## [2.1.3] — 2026-06-26 — Tool manifest as single source of truth + server-meta.json for VS Code extension
|
|
6
64
|
|
|
7
65
|
Tactical fix for a class of silent display drift: the VS Code info panel hardcoded "Active on all 17 MCP tools" while the npm package was at 21 tools. The README was at 20. None of these were tied to the actual `server.tool(...)` registrations, so adding a tool (e.g. `drift_status` in 2.1.0) left all displays stale.
|
package/README.md
CHANGED
|
@@ -248,9 +248,14 @@ npx @compr/opscontext-mcp list-sources
|
|
|
248
248
|
# Discover and analyze all projects
|
|
249
249
|
npx @compr/opscontext-mcp list-projects
|
|
250
250
|
|
|
251
|
-
# AI-readiness score
|
|
251
|
+
# AI-readiness score — no argument scores the CURRENT project only
|
|
252
252
|
npx @compr/opscontext-mcp score
|
|
253
|
-
npx @compr/opscontext-mcp score ContextEngine
|
|
253
|
+
npx @compr/opscontext-mcp score ContextEngine # by project name
|
|
254
|
+
npx @compr/opscontext-mcp score ~/Projects/PLANK.io # or by path
|
|
255
|
+
|
|
256
|
+
# Score every discovered project (writes a SCORE.md into each — opt in explicitly)
|
|
257
|
+
npx @compr/opscontext-mcp score --all
|
|
258
|
+
npx @compr/opscontext-mcp score --all --no-save # scan without writing
|
|
254
259
|
|
|
255
260
|
# Visual HTML report (opens in browser)
|
|
256
261
|
npx @compr/opscontext-mcp score --html
|
|
@@ -436,6 +441,22 @@ The `score` command evaluates project AI-readiness across **documentation, infra
|
|
|
436
441
|
|
|
437
442
|
**Grade scale:** A+ (90%+) · A (80%+) · B (70%+) · C (60%+) · D (50%+) · F (<50%)
|
|
438
443
|
|
|
444
|
+
### What gets scored, and what gets written
|
|
445
|
+
|
|
446
|
+
`score` writes a `SCORE.md` into each project it scores. Because that is a write into your
|
|
447
|
+
repositories, the scope is never inferred:
|
|
448
|
+
|
|
449
|
+
| Command | Scores | Writes `SCORE.md` to |
|
|
450
|
+
|---|---|---|
|
|
451
|
+
| `score` | the project you are standing in (walks up to the repo root) | that one project |
|
|
452
|
+
| `score <name>` / `score <path>` | that one project | that one project |
|
|
453
|
+
| `score --all` | every discovered project | **every** discovered project |
|
|
454
|
+
| any of the above `--no-save` | as above | nothing |
|
|
455
|
+
|
|
456
|
+
A project argument may be a **name** (`PLANK.io`) or a **path** (`~/Projects/PLANK.io`,
|
|
457
|
+
`../PLANK.io`, or an absolute path). A path also works for projects outside your configured
|
|
458
|
+
workspaces.
|
|
459
|
+
|
|
439
460
|
### Project Naming & Structure Tips
|
|
440
461
|
|
|
441
462
|
The scorer discovers projects from your configured `workspaces` directories (default: `~/Projects`).
|
package/dist/agents.js
CHANGED
|
@@ -86,22 +86,48 @@ const AGENT_DOC_TOPICS = [
|
|
|
86
86
|
function matchDocTopics(content, topics) {
|
|
87
87
|
return topics.filter(t => t.patterns.test(content)).map(t => t.label);
|
|
88
88
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
89
|
+
/**
|
|
90
|
+
* ALL languages present, root and one level down — not just a winner.
|
|
91
|
+
*
|
|
92
|
+
* A single "primary language" is the wrong model for this fleet. PLANK.io is a Flutter app with a
|
|
93
|
+
* Node backend: picking one meant scoring a Dart codebase against `tsconfig.json` and calling its
|
|
94
|
+
* analyzer config missing. Returning the set lets each tooling check pass on whichever ecosystem
|
|
95
|
+
* actually configures it. Part of [SCORE-LANGUAGE-AWARE].
|
|
96
|
+
*/
|
|
97
|
+
const LANGUAGE_MARKERS = [
|
|
98
|
+
{ lang: "dart", files: ["pubspec.yaml"] },
|
|
99
|
+
{ lang: "python", files: ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "__manifest__.py"] },
|
|
100
|
+
{ lang: "js", files: ["package.json"] },
|
|
101
|
+
{ lang: "php", files: ["composer.json"] },
|
|
102
|
+
];
|
|
103
|
+
function detectLanguages(p) {
|
|
104
|
+
const found = new Set();
|
|
105
|
+
const scan = (base) => {
|
|
106
|
+
for (const { lang, files } of LANGUAGE_MARKERS) {
|
|
107
|
+
if (files.some(f => existsSync(join(base, f))))
|
|
108
|
+
found.add(lang);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
scan(p);
|
|
112
|
+
for (const sub of safeSubdirs(p))
|
|
113
|
+
scan(join(p, sub));
|
|
114
|
+
if (found.size === 0)
|
|
115
|
+
found.add("other");
|
|
116
|
+
return found;
|
|
117
|
+
}
|
|
118
|
+
/** First existing path among `candidates`, searched at the root and one level down. */
|
|
119
|
+
function findConfig(p, candidates) {
|
|
120
|
+
for (const c of candidates) {
|
|
121
|
+
if (existsSync(join(p, c)))
|
|
122
|
+
return c;
|
|
123
|
+
}
|
|
98
124
|
for (const sub of safeSubdirs(p)) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
125
|
+
for (const c of candidates) {
|
|
126
|
+
if (existsSync(join(p, sub, c)))
|
|
127
|
+
return `${sub}/${c}`;
|
|
128
|
+
}
|
|
103
129
|
}
|
|
104
|
-
return
|
|
130
|
+
return null;
|
|
105
131
|
}
|
|
106
132
|
/** Immediate subdirectories worth searching — skips vendored, hidden and build output. */
|
|
107
133
|
function safeSubdirs(p) {
|
|
@@ -184,9 +210,15 @@ function countTestFiles(dirPath, depth = 0) {
|
|
|
184
210
|
count += countTestFiles(fullPath, depth + 1);
|
|
185
211
|
}
|
|
186
212
|
else if (entry.isFile()) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
213
|
+
// 🔒 [SCORE-LANGUAGE-AWARE] — the extension list is part of the language assumption.
|
|
214
|
+
// `dart` was absent, so PLANK.io's 68 Flutter tests in plank_app/test/ counted as ZERO:
|
|
215
|
+
// the directory was found, every file was skipped, and the row credited the Node backend
|
|
216
|
+
// alone. A test counter that silently ignores a language reports "no tests" for a suite
|
|
217
|
+
// that runs on every build. Add the extension when adding a language, not after someone
|
|
218
|
+
// notices their tests are invisible.
|
|
219
|
+
if (/\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py|php|dart)$/.test(entry.name) ||
|
|
220
|
+
/_(test|spec)\.(py|dart|go|rb)$/.test(entry.name) ||
|
|
221
|
+
entry.name.startsWith("test_")) {
|
|
190
222
|
count++;
|
|
191
223
|
}
|
|
192
224
|
}
|
|
@@ -1112,7 +1144,7 @@ export function scoreProject(dir) {
|
|
|
1112
1144
|
const checks = [];
|
|
1113
1145
|
const p = dir.path;
|
|
1114
1146
|
// Language decides which tooling checks apply at all — see [SCORE-LANGUAGE-AWARE].
|
|
1115
|
-
const
|
|
1147
|
+
const langs = detectLanguages(p);
|
|
1116
1148
|
// --- Documentation (30 points max) ---
|
|
1117
1149
|
// copilot-instructions.md (6 pts) — scored on CONTENT, not length.
|
|
1118
1150
|
// 🔒 LOCKED [SCORE-CONTENT-NOT-LENGTH] — 2026-08-14
|
|
@@ -1427,74 +1459,86 @@ export function scoreProject(dir) {
|
|
|
1427
1459
|
];
|
|
1428
1460
|
const foundTests = testDirs.filter(td => existsSync(join(p, td)));
|
|
1429
1461
|
if (foundTests.length > 0) {
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1462
|
+
// Count across EVERY test directory, not just the first match. PLANK.io has 68 Flutter tests
|
|
1463
|
+
// in plank_app/test/ and 38 backend tests in backend/__tests__/; taking foundTests[0] credited
|
|
1464
|
+
// the backend alone and rendered the larger codebase invisible. Any polyglot or multi-package
|
|
1465
|
+
// repo hit this — the bug was the `[0]`, not the search.
|
|
1466
|
+
const perDir = foundTests.map(td => ({ rel: td, count: countTestFiles(join(p, td)), symlink: isSymlink(join(p, td)) }));
|
|
1467
|
+
const testFileCount = perDir.reduce((sum, d) => sum + d.count, 0);
|
|
1468
|
+
const contributing = perDir.filter(d => d.count > 0);
|
|
1469
|
+
const where = (contributing.length > 0 ? contributing : perDir)
|
|
1470
|
+
.map(d => `${d.rel}/ (${d.count})`)
|
|
1471
|
+
.slice(0, 3)
|
|
1472
|
+
.join(", ") + (perDir.length > 3 ? `, +${perDir.length - 3} more` : "");
|
|
1473
|
+
const allSymlinks = perDir.every(d => d.symlink);
|
|
1474
|
+
if (allSymlinks) {
|
|
1475
|
+
checks.push({ name: "Tests", category: "Code Quality", points: 3, maxPoints: 8, status: "partial", detail: `${where} — symlinked, should be real test directories` });
|
|
1435
1476
|
}
|
|
1436
1477
|
else if (testFileCount >= RUBRIC.testsFull) {
|
|
1437
|
-
checks.push({ name: "Tests", category: "Code Quality", points: 8, maxPoints: 8, status: "pass", detail: `${
|
|
1478
|
+
checks.push({ name: "Tests", category: "Code Quality", points: 8, maxPoints: 8, status: "pass", detail: `${testFileCount} test files across ${contributing.length} dir(s): ${where}` });
|
|
1438
1479
|
}
|
|
1439
1480
|
else if (testFileCount > RUBRIC.testsPartial) {
|
|
1440
|
-
checks.push({ name: "Tests", category: "Code Quality", points: 5, maxPoints: 8, status: "partial", detail:
|
|
1481
|
+
checks.push({ name: "Tests", category: "Code Quality", points: 5, maxPoints: 8, status: "partial", detail: `only ${testFileCount} test files: ${where}` });
|
|
1441
1482
|
}
|
|
1442
1483
|
else {
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1484
|
+
let hasAnyFiles = false;
|
|
1485
|
+
for (const d of perDir) {
|
|
1486
|
+
try {
|
|
1487
|
+
if (readdirSync(join(p, d.rel)).length > 0) {
|
|
1488
|
+
hasAnyFiles = true;
|
|
1489
|
+
break;
|
|
1490
|
+
}
|
|
1450
1491
|
}
|
|
1492
|
+
catch { /* unreadable dir counts as empty */ }
|
|
1451
1493
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1494
|
+
checks.push(hasAnyFiles
|
|
1495
|
+
? { name: "Tests", category: "Code Quality", points: 4, maxPoints: 8, status: "partial", detail: `${where} has files but no standard test files detected` }
|
|
1496
|
+
: { name: "Tests", category: "Code Quality", points: 1, maxPoints: 8, status: "partial", detail: `${where} exists but empty` });
|
|
1455
1497
|
}
|
|
1456
1498
|
}
|
|
1457
1499
|
else {
|
|
1458
1500
|
checks.push({ name: "Tests", category: "Code Quality", points: 0, maxPoints: 8, status: "fail", detail: "No test directory" });
|
|
1459
1501
|
}
|
|
1460
|
-
// TypeScript / type checking (5 pts)
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
const
|
|
1465
|
-
|
|
1466
|
-
const
|
|
1467
|
-
|
|
1468
|
-
|
|
1502
|
+
// TypeScript / type checking (5 pts) — see [SCORE-LANGUAGE-AWARE].
|
|
1503
|
+
// Checks every ecosystem the repo actually uses, so a Flutter app with a Node backend is not
|
|
1504
|
+
// told its Dart analyzer config is a missing tsconfig.
|
|
1505
|
+
{
|
|
1506
|
+
const tsCfg = findConfig(p, ["tsconfig.json"]);
|
|
1507
|
+
const dartCfg = langs.has("dart") ? findConfig(p, ["analysis_options.yaml"]) : null;
|
|
1508
|
+
const pyCfg = langs.has("python") ? findConfig(p, ["mypy.ini", ".mypy.ini", "pyrightconfig.json"]) : null;
|
|
1509
|
+
const pyprojectPath = findConfig(p, ["pyproject.toml"]);
|
|
1510
|
+
const pyproject = pyprojectPath ? readFileSync(join(p, pyprojectPath), "utf-8") : "";
|
|
1511
|
+
const pyInline = langs.has("python") && /\[tool\.(mypy|pyright)\]/.test(pyproject);
|
|
1512
|
+
if (tsCfg) {
|
|
1513
|
+
const content = readFileSync(join(p, tsCfg), "utf-8").trim();
|
|
1514
|
+
const substantive = content.length > RUBRIC.tsconfigSubstantive && (content.includes('"compilerOptions"') || content.includes('"extends"'));
|
|
1515
|
+
if (isSymlink(join(p, tsCfg))) {
|
|
1516
|
+
checks.push({ name: "TypeScript", category: "Code Quality", points: 2, maxPoints: 5, status: "partial", detail: `${tsCfg} is a symlink — create a real config` });
|
|
1517
|
+
}
|
|
1518
|
+
else if (substantive) {
|
|
1519
|
+
checks.push({ name: "TypeScript", category: "Code Quality", points: 5, maxPoints: 5, status: "pass", detail: `${tsCfg} present` });
|
|
1520
|
+
}
|
|
1521
|
+
else {
|
|
1522
|
+
checks.push({ name: "TypeScript", category: "Code Quality", points: 3, maxPoints: 5, status: "partial", detail: `${tsCfg} is minimal — add compilerOptions for full type safety` });
|
|
1523
|
+
}
|
|
1469
1524
|
}
|
|
1470
|
-
else if (
|
|
1471
|
-
checks.push({ name: "
|
|
1525
|
+
else if (dartCfg) {
|
|
1526
|
+
checks.push({ name: "Type checking", category: "Code Quality", points: 5, maxPoints: 5, status: "pass", detail: `${dartCfg} — Dart analyzer configured` });
|
|
1472
1527
|
}
|
|
1473
|
-
else {
|
|
1474
|
-
checks.push({ name: "
|
|
1528
|
+
else if (pyCfg || pyInline) {
|
|
1529
|
+
checks.push({ name: "Type checking", category: "Code Quality", points: 5, maxPoints: 5, status: "pass", detail: `${pyCfg ?? pyprojectPath} — static type checking configured` });
|
|
1475
1530
|
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
const pyTypeMarkers = ["mypy.ini", ".mypy.ini", "pyrightconfig.json"];
|
|
1483
|
-
const foundPyType = pyTypeMarkers.filter(m => existsSync(join(p, m)));
|
|
1484
|
-
const pyproject = existsSync(join(p, "pyproject.toml")) ? readFileSync(join(p, "pyproject.toml"), "utf-8") : "";
|
|
1485
|
-
if (foundPyType.length > 0 || /\[tool\.(mypy|pyright)\]/.test(pyproject)) {
|
|
1486
|
-
checks.push({ name: "Type checking", category: "Code Quality", points: 5, maxPoints: 5, status: "pass", detail: `${foundPyType[0] ?? "pyproject.toml"} — static type checking configured` });
|
|
1531
|
+
else if (existsSync(join(p, "jsconfig.json"))) {
|
|
1532
|
+
checks.push({ name: "Type checking", category: "Code Quality", points: 2, maxPoints: 5, status: "partial", detail: "jsconfig.json only" });
|
|
1533
|
+
}
|
|
1534
|
+
else if (langs.has("js") || langs.has("dart") || langs.has("python")) {
|
|
1535
|
+
const want = [langs.has("js") && "tsconfig.json", langs.has("dart") && "analysis_options.yaml", langs.has("python") && "mypy/pyright"].filter(Boolean).join(" or ");
|
|
1536
|
+
checks.push({ name: "Type checking", category: "Code Quality", points: 0, maxPoints: 5, status: "fail", detail: `No ${want} found — add static type checking` });
|
|
1487
1537
|
}
|
|
1488
1538
|
else {
|
|
1489
|
-
checks.push({ name: "Type checking", category: "Code Quality", points: 0, maxPoints: 5, status: "
|
|
1539
|
+
checks.push({ name: "Type checking", category: "Code Quality", points: 0, maxPoints: 5, status: "unknown", detail: "❔ No type-checking convention known for this project type — not assessed" });
|
|
1490
1540
|
}
|
|
1491
1541
|
}
|
|
1492
|
-
else if (lang === "php" || lang === "other") {
|
|
1493
|
-
checks.push({ name: "Type checking", category: "Code Quality", points: 0, maxPoints: 5, status: "unknown", detail: `❔ No type-checking convention known for this project type (${lang}) — not assessed` });
|
|
1494
|
-
}
|
|
1495
|
-
else {
|
|
1496
|
-
checks.push({ name: "Type checking", category: "Code Quality", points: 0, maxPoints: 5, status: "fail", detail: "No tsconfig/jsconfig" });
|
|
1497
|
-
}
|
|
1498
1542
|
// Linting config (4 pts) — verifies linting tools are installed, not just config
|
|
1499
1543
|
const lintConfigs = [".eslintrc.js", ".eslintrc.json", ".eslintrc.yml", "eslint.config.js", "eslint.config.mjs", ".prettierrc", "phpcs.xml"];
|
|
1500
1544
|
const foundLint = lintConfigs.filter(l => existsSync(join(p, l)));
|
|
@@ -1511,10 +1555,16 @@ export function scoreProject(dir) {
|
|
|
1511
1555
|
checks.push({ name: "Linting", category: "Code Quality", points: 4, maxPoints: 4, status: "pass", detail: foundLint.join(", ") });
|
|
1512
1556
|
}
|
|
1513
1557
|
}
|
|
1514
|
-
else if (
|
|
1558
|
+
else if (langs.has("dart") && findConfig(p, ["analysis_options.yaml"])) {
|
|
1559
|
+
// Dart's analyzer IS the linter — analysis_options.yaml is enforced on every build.
|
|
1560
|
+
checks.push({ name: "Linting", category: "Code Quality", points: 4, maxPoints: 4, status: "pass", detail: `${findConfig(p, ["analysis_options.yaml"])} — Dart analyzer lints` });
|
|
1561
|
+
}
|
|
1562
|
+
else if (langs.has("python")) {
|
|
1515
1563
|
// See [SCORE-LANGUAGE-AWARE]. Python linting is ruff/flake8/pylint, not eslint.
|
|
1516
|
-
const
|
|
1517
|
-
const
|
|
1564
|
+
const pyLintCfg = findConfig(p, ["ruff.toml", ".ruff.toml", ".flake8", ".pylintrc", "tox.ini", "setup.cfg"]);
|
|
1565
|
+
const pyLint = pyLintCfg ? [pyLintCfg] : [];
|
|
1566
|
+
const pyprojectRel = findConfig(p, ["pyproject.toml"]);
|
|
1567
|
+
const pyproject = pyprojectRel ? readFileSync(join(p, pyprojectRel), "utf-8") : "";
|
|
1518
1568
|
if (pyLint.length > 0 || /\[tool\.(ruff|flake8|pylint|black)\]/.test(pyproject)) {
|
|
1519
1569
|
checks.push({ name: "Linting", category: "Code Quality", points: 4, maxPoints: 4, status: "pass", detail: pyLint[0] ?? "pyproject.toml" });
|
|
1520
1570
|
}
|
|
@@ -1522,7 +1572,7 @@ export function scoreProject(dir) {
|
|
|
1522
1572
|
checks.push({ name: "Linting", category: "Code Quality", points: 0, maxPoints: 4, status: "fail", detail: "Python project with no ruff/flake8/pylint config — add one" });
|
|
1523
1573
|
}
|
|
1524
1574
|
}
|
|
1525
|
-
else if (
|
|
1575
|
+
else if (langs.has("other") && langs.size === 1) {
|
|
1526
1576
|
checks.push({ name: "Linting", category: "Code Quality", points: 0, maxPoints: 4, status: "unknown", detail: "❔ No linting convention known for this project type — not assessed" });
|
|
1527
1577
|
}
|
|
1528
1578
|
else {
|
package/dist/cli.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* contextengine list-projects Discover and analyze all projects
|
|
11
11
|
* contextengine list-learnings List all permanent learnings
|
|
12
12
|
* contextengine save-learning Save a learning (terminal fallback for MCP)
|
|
13
|
-
* contextengine score [project]
|
|
13
|
+
* contextengine score [project|path] AI-readiness score for one project (default: cwd; --all for fleet)
|
|
14
14
|
* contextengine audit Run compliance audit across all projects
|
|
15
15
|
* contextengine help Show this message
|
|
16
16
|
*/
|
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* contextengine list-projects Discover and analyze all projects
|
|
11
11
|
* contextengine list-learnings List all permanent learnings
|
|
12
12
|
* contextengine save-learning Save a learning (terminal fallback for MCP)
|
|
13
|
-
* contextengine score [project]
|
|
13
|
+
* contextengine score [project|path] AI-readiness score for one project (default: cwd; --all for fleet)
|
|
14
14
|
* contextengine audit Run compliance audit across all projects
|
|
15
15
|
* contextengine help Show this message
|
|
16
16
|
*/
|
|
@@ -517,7 +517,7 @@ async function runInit() {
|
|
|
517
517
|
// ---------------------------------------------------------------------------
|
|
518
518
|
// CLI Engine — shared initialization for all CLI subcommands
|
|
519
519
|
// ---------------------------------------------------------------------------
|
|
520
|
-
import { loadSources, loadProjectDirs, loadConfig } from "./config.js";
|
|
520
|
+
import { loadSources, loadProjectDirs, loadConfig, resolveProjectDir, findProjectRoot } from "./config.js";
|
|
521
521
|
import { ingestSources } from "./ingest.js";
|
|
522
522
|
import { searchChunks } from "./search.js";
|
|
523
523
|
import { collectProjectOps, collectSystemOps } from "./collectors.js";
|
|
@@ -697,12 +697,34 @@ async function cliDeleteLearning(id) {
|
|
|
697
697
|
process.exit(1);
|
|
698
698
|
}
|
|
699
699
|
}
|
|
700
|
-
|
|
700
|
+
/**
|
|
701
|
+
* 🔒 LOCKED [SCORE-FLEET-IS-OPT-IN] — 2026-08-16
|
|
702
|
+
* ⛔ NEVER make a bare `contextengine score` iterate the whole fleet again, and never
|
|
703
|
+
* let the no-argument path write SCORE.md anywhere but the resolved project root.
|
|
704
|
+
* WHY: `score` with no argument scored all 37 discovered projects AND wrote a SCORE.md
|
|
705
|
+
* into every one of them. Three separate agent sessions ran it expecting to score
|
|
706
|
+
* the repo they were standing in, and dirtied 26 unrelated repositories — several of
|
|
707
|
+
* which auto-push to Google Drive on commit, so the noise propagated off-machine
|
|
708
|
+
* before anyone noticed. The blast radius of the default was the entire fleet while
|
|
709
|
+
* the intent behind typing it was almost always a single project.
|
|
710
|
+
* FIX: the destructive-at-scale operation must be *asked for*. `--all` opts in; no
|
|
711
|
+
* argument scores the current working directory's project root. A fleet-wide write
|
|
712
|
+
* is a deliberate choice, never a default the user backs into.
|
|
713
|
+
* Note the MCP `score_project` tool is read-only (it never writes SCORE.md), which
|
|
714
|
+
* is why its fleet-wide default is left alone — the hazard here is the write, not
|
|
715
|
+
* the scan.
|
|
716
|
+
*/
|
|
717
|
+
async function cliScore(project, html = false, save = true, all = false) {
|
|
701
718
|
const gate = gateCheck("score_project");
|
|
702
719
|
if (gate) {
|
|
703
720
|
console.error(gate);
|
|
704
721
|
process.exit(1);
|
|
705
722
|
}
|
|
723
|
+
if (project && all) {
|
|
724
|
+
console.error(`❌ Cannot combine --all with a project argument ("${project}").`);
|
|
725
|
+
console.error(` Use --all for the whole fleet, or name one project/path.`);
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
706
728
|
// [SCORE-CANARY] — every health signal must read exactly as pinned before we are allowed to
|
|
707
729
|
// write a single SCORE.md. A drifting scorer that silently rewrites 37 reports is the failure
|
|
708
730
|
// this blocks; it does not test for one known bug, it refuses to proceed on ANY deviation.
|
|
@@ -718,17 +740,44 @@ async function cliScore(project, html = false, save = true) {
|
|
|
718
740
|
}
|
|
719
741
|
const projectDirs = loadProjectDirs();
|
|
720
742
|
let scores;
|
|
721
|
-
if (
|
|
722
|
-
|
|
743
|
+
if (all) {
|
|
744
|
+
if (save) {
|
|
745
|
+
console.error(`⚠️ --all: scoring ${projectDirs.length} projects and writing a SCORE.md into each.`);
|
|
746
|
+
console.error(` Use --no-save to scan without writing.\n`);
|
|
747
|
+
}
|
|
748
|
+
scores = projectDirs.map((d) => scoreProject(d));
|
|
749
|
+
}
|
|
750
|
+
else if (project) {
|
|
751
|
+
// [SCORE-ACCEPTS-PATH] — a path is as valid an identifier as a name.
|
|
752
|
+
const dir = resolveProjectDir(project, projectDirs);
|
|
723
753
|
if (!dir) {
|
|
724
754
|
console.error(`❌ Project not found: "${project}"`);
|
|
755
|
+
console.error(` Not a known project name, and not an existing directory.`);
|
|
725
756
|
console.error(`Available: ${projectDirs.map((d) => d.name).join(", ")}`);
|
|
726
757
|
process.exit(1);
|
|
727
758
|
}
|
|
728
759
|
scores = [scoreProject(dir)];
|
|
729
760
|
}
|
|
730
761
|
else {
|
|
731
|
-
|
|
762
|
+
// [SCORE-FLEET-IS-OPT-IN] — no argument means "the project I am standing in",
|
|
763
|
+
// never "every project on this machine".
|
|
764
|
+
// [SCORE-CWD-MUST-BE-A-PROJECT] — and if I am not standing in a project, say so
|
|
765
|
+
// rather than scoring whatever directory happens to be here.
|
|
766
|
+
const root = findProjectRoot(process.cwd());
|
|
767
|
+
if (!root) {
|
|
768
|
+
console.error(`❌ Not inside a project: ${process.cwd()}`);
|
|
769
|
+
console.error(` No .git or package.json found here or in any parent directory.`);
|
|
770
|
+
console.error(` Do one of:`);
|
|
771
|
+
console.error(` • cd into a project, then run: contextengine score`);
|
|
772
|
+
console.error(` • name it: contextengine score <name|path>`);
|
|
773
|
+
console.error(` • score the whole fleet: contextengine score --all`);
|
|
774
|
+
process.exit(1);
|
|
775
|
+
}
|
|
776
|
+
const known = projectDirs.find((d) => resolve(d.path) === resolve(root));
|
|
777
|
+
const dir = known ?? { name: basename(root), path: root };
|
|
778
|
+
console.error(`📍 Scoring current project: ${dir.name} (${dir.path})`);
|
|
779
|
+
console.error(` Use --all to score every discovered project.\n`);
|
|
780
|
+
scores = [scoreProject(dir)];
|
|
732
781
|
}
|
|
733
782
|
if (html) {
|
|
734
783
|
const htmlContent = generateScoreHTML(scores);
|
|
@@ -2111,7 +2160,11 @@ Usage:
|
|
|
2111
2160
|
Fetch community-contributed learnings (Tier A = GitHub
|
|
2112
2161
|
public, Tier B = api.compr.ch Pro). Daily run recommended.
|
|
2113
2162
|
Network failures fall back to cached store.
|
|
2114
|
-
contextengine score [project] [--html] [--no-save]
|
|
2163
|
+
contextengine score [project|path] [--all] [--html] [--no-save]
|
|
2164
|
+
AI-readiness score (Pro, writes SCORE.md).
|
|
2165
|
+
No argument scores the CURRENT project only.
|
|
2166
|
+
Accepts a project name or a directory path.
|
|
2167
|
+
--all scores every discovered project (writes to each).
|
|
2115
2168
|
contextengine audit Run compliance audit (Pro)
|
|
2116
2169
|
contextengine activate <key> <email> Activate a Pro license
|
|
2117
2170
|
contextengine deactivate Remove license and premium modules
|
|
@@ -2195,8 +2248,9 @@ else if (command === "score") {
|
|
|
2195
2248
|
const args = process.argv.slice(3);
|
|
2196
2249
|
const htmlFlag = args.includes("--html");
|
|
2197
2250
|
const noSaveFlag = args.includes("--no-save");
|
|
2251
|
+
const allFlag = args.includes("--all");
|
|
2198
2252
|
const project = args.filter(a => !a.startsWith("--"))[0];
|
|
2199
|
-
cliScore(project, htmlFlag, !noSaveFlag).catch((err) => {
|
|
2253
|
+
cliScore(project, htmlFlag, !noSaveFlag, allFlag).catch((err) => {
|
|
2200
2254
|
console.error("Error:", err);
|
|
2201
2255
|
process.exit(1);
|
|
2202
2256
|
});
|
package/dist/config.d.ts
CHANGED
|
@@ -66,6 +66,43 @@ export declare function loadSources(): KnowledgeSource[];
|
|
|
66
66
|
* Returns one entry per top-level project found.
|
|
67
67
|
*/
|
|
68
68
|
export declare function loadProjectDirs(): ProjectDirectory[];
|
|
69
|
+
/**
|
|
70
|
+
* 🔒 LOCKED [SCORE-ACCEPTS-PATH] — 2026-08-16
|
|
71
|
+
* ⛔ NEVER narrow this back to `dirs.find(d => d.name === token)` alone.
|
|
72
|
+
* WHY: `contextengine score /Users/yan/Projects/PLANK.io` failed with
|
|
73
|
+
* "Project not found: /Users/yan/Projects/PLANK.io" while listing PLANK.io
|
|
74
|
+
* among the available projects. A path is the natural first guess for a
|
|
75
|
+
* tool that prints absolute paths in its own output, and the error named
|
|
76
|
+
* the one thing the user had clearly just given it. The directory was
|
|
77
|
+
* never inspected — the lookup only ever compared basenames, so this was
|
|
78
|
+
* [ABSENCE-IS-NOT-A-VERDICT] at the argument-parsing layer: "not in my
|
|
79
|
+
* name index" was reported as "does not exist".
|
|
80
|
+
* FIX: resolve names AND paths. A path that exists is a project, whether or not
|
|
81
|
+
* it sits under a configured workspace — that is what makes the tool usable
|
|
82
|
+
* outside `~/Projects`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function resolveProjectDir(token: string, dirs: ProjectDirectory[]): ProjectDirectory | null;
|
|
85
|
+
/**
|
|
86
|
+
* 🔒 LOCKED [SCORE-CWD-MUST-BE-A-PROJECT] — 2026-08-16
|
|
87
|
+
* ⛔ NEVER fall back to returning `start` when no project marker is found. A directory
|
|
88
|
+
* that is not a project must produce null, and the caller must refuse to score it.
|
|
89
|
+
* WHY: the first cut of this returned `start` on failure, reasoning that "an un-versioned
|
|
90
|
+
* directory is still scoreable — it just scores badly." That is exactly the
|
|
91
|
+
* absence-as-verdict mistake this codebase keeps relearning. Running `score` from
|
|
92
|
+
* `~/Projects` — a CONTAINER of 37 projects, not a project — walked to the filesystem
|
|
93
|
+
* root, found nothing, fell back, scored the container as though it were a project,
|
|
94
|
+
* and wrote `~/Projects/SCORE.md` claiming "Projects: 27/100 (F)". Run from `/` it
|
|
95
|
+
* would do the same to the filesystem root. "I cannot tell which project you mean" is
|
|
96
|
+
* an unknown, and the safe response to an unknown scope is to ask, never to write.
|
|
97
|
+
* FIX: return null and let the caller error out with the three things the user can do
|
|
98
|
+
* instead (cd into a project, name one, or --all). Found by an adversarial review
|
|
99
|
+
* agent that ran the real CLI from `/` and `~/Projects`.
|
|
100
|
+
*
|
|
101
|
+
* Walk up from `start` to the enclosing project root. Stops at the first directory
|
|
102
|
+
* holding a `.git` or a `package.json`. Returns null when neither is found anywhere
|
|
103
|
+
* above `start`.
|
|
104
|
+
*/
|
|
105
|
+
export declare function findProjectRoot(start: string): string | null;
|
|
69
106
|
/**
|
|
70
107
|
* Load the raw config (for checking flags like collectSystemOps).
|
|
71
108
|
*/
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolve, join } from "path";
|
|
1
|
+
import { resolve, join, basename, dirname, sep } from "path";
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { readFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
4
4
|
import { discoverClaudeMemory } from "./claude-integration.js";
|
|
@@ -227,6 +227,90 @@ export function loadProjectDirs() {
|
|
|
227
227
|
}
|
|
228
228
|
return dirs;
|
|
229
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Does this token look like a filesystem path rather than a bare project name?
|
|
232
|
+
*
|
|
233
|
+
* Deliberately conservative: only strings that CANNOT be a directory basename
|
|
234
|
+
* (they contain a separator, or start with `~`/`.`) are treated as
|
|
235
|
+
* path-only. Everything else stays eligible for name lookup first, so
|
|
236
|
+
* `score KONIVE.com` keeps resolving exactly as it did before this existed.
|
|
237
|
+
*/
|
|
238
|
+
function looksLikePath(token) {
|
|
239
|
+
return (token.includes("/") ||
|
|
240
|
+
token.includes(sep) ||
|
|
241
|
+
token.startsWith("~") ||
|
|
242
|
+
token === "." ||
|
|
243
|
+
token === "..");
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* 🔒 LOCKED [SCORE-ACCEPTS-PATH] — 2026-08-16
|
|
247
|
+
* ⛔ NEVER narrow this back to `dirs.find(d => d.name === token)` alone.
|
|
248
|
+
* WHY: `contextengine score /Users/yan/Projects/PLANK.io` failed with
|
|
249
|
+
* "Project not found: /Users/yan/Projects/PLANK.io" while listing PLANK.io
|
|
250
|
+
* among the available projects. A path is the natural first guess for a
|
|
251
|
+
* tool that prints absolute paths in its own output, and the error named
|
|
252
|
+
* the one thing the user had clearly just given it. The directory was
|
|
253
|
+
* never inspected — the lookup only ever compared basenames, so this was
|
|
254
|
+
* [ABSENCE-IS-NOT-A-VERDICT] at the argument-parsing layer: "not in my
|
|
255
|
+
* name index" was reported as "does not exist".
|
|
256
|
+
* FIX: resolve names AND paths. A path that exists is a project, whether or not
|
|
257
|
+
* it sits under a configured workspace — that is what makes the tool usable
|
|
258
|
+
* outside `~/Projects`.
|
|
259
|
+
*/
|
|
260
|
+
export function resolveProjectDir(token, dirs) {
|
|
261
|
+
// Name lookup first — preserves pre-existing behaviour exactly.
|
|
262
|
+
if (!looksLikePath(token)) {
|
|
263
|
+
const byName = dirs.find((d) => d.name.toLowerCase() === token.toLowerCase());
|
|
264
|
+
if (byName)
|
|
265
|
+
return byName;
|
|
266
|
+
}
|
|
267
|
+
// Path resolution — absolute, relative, or `~`-prefixed.
|
|
268
|
+
const abs = resolve(token.replace(/^~/, homedir()));
|
|
269
|
+
try {
|
|
270
|
+
if (statSync(abs).isDirectory()) {
|
|
271
|
+
// Prefer the configured entry when the path points at a known project,
|
|
272
|
+
// so the reported name matches the rest of the fleet output.
|
|
273
|
+
const known = dirs.find((d) => resolve(d.path) === abs);
|
|
274
|
+
return known ?? { name: basename(abs), path: abs };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// ENOENT / EACCES — not a usable directory. Fall through to null.
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* 🔒 LOCKED [SCORE-CWD-MUST-BE-A-PROJECT] — 2026-08-16
|
|
284
|
+
* ⛔ NEVER fall back to returning `start` when no project marker is found. A directory
|
|
285
|
+
* that is not a project must produce null, and the caller must refuse to score it.
|
|
286
|
+
* WHY: the first cut of this returned `start` on failure, reasoning that "an un-versioned
|
|
287
|
+
* directory is still scoreable — it just scores badly." That is exactly the
|
|
288
|
+
* absence-as-verdict mistake this codebase keeps relearning. Running `score` from
|
|
289
|
+
* `~/Projects` — a CONTAINER of 37 projects, not a project — walked to the filesystem
|
|
290
|
+
* root, found nothing, fell back, scored the container as though it were a project,
|
|
291
|
+
* and wrote `~/Projects/SCORE.md` claiming "Projects: 27/100 (F)". Run from `/` it
|
|
292
|
+
* would do the same to the filesystem root. "I cannot tell which project you mean" is
|
|
293
|
+
* an unknown, and the safe response to an unknown scope is to ask, never to write.
|
|
294
|
+
* FIX: return null and let the caller error out with the three things the user can do
|
|
295
|
+
* instead (cd into a project, name one, or --all). Found by an adversarial review
|
|
296
|
+
* agent that ran the real CLI from `/` and `~/Projects`.
|
|
297
|
+
*
|
|
298
|
+
* Walk up from `start` to the enclosing project root. Stops at the first directory
|
|
299
|
+
* holding a `.git` or a `package.json`. Returns null when neither is found anywhere
|
|
300
|
+
* above `start`.
|
|
301
|
+
*/
|
|
302
|
+
export function findProjectRoot(start) {
|
|
303
|
+
let dir = resolve(start);
|
|
304
|
+
for (;;) {
|
|
305
|
+
if (existsSync(join(dir, ".git")) || existsSync(join(dir, "package.json"))) {
|
|
306
|
+
return dir;
|
|
307
|
+
}
|
|
308
|
+
const parent = dirname(dir);
|
|
309
|
+
if (parent === dir)
|
|
310
|
+
return null; // reached filesystem root, no marker seen
|
|
311
|
+
dir = parent;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
230
314
|
/**
|
|
231
315
|
* Load the raw config (for checking flags like collectSystemOps).
|
|
232
316
|
*/
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { loadSources, loadProjectDirs, loadConfig } from "./config.js";
|
|
5
|
+
import { loadSources, loadProjectDirs, loadConfig, resolveProjectDir } from "./config.js";
|
|
6
6
|
import { ingestSources } from "./ingest.js";
|
|
7
7
|
import { searchChunks } from "./search.js";
|
|
8
8
|
import { initEmbeddings, embedChunks, vectorSearch, isEmbeddingsReady, } from "./embeddings.js";
|
|
@@ -474,7 +474,7 @@ server.tool("score_project", "Score one or all projects on AI-readiness (0-100%)
|
|
|
474
474
|
project: z
|
|
475
475
|
.string()
|
|
476
476
|
.optional()
|
|
477
|
-
.describe("Project name to score. Omit to score all projects."),
|
|
477
|
+
.describe("Project name OR absolute directory path to score. Omit to score all projects."),
|
|
478
478
|
}, async ({ project }) => {
|
|
479
479
|
const gate = gateCheck("score_project");
|
|
480
480
|
if (gate)
|
|
@@ -482,13 +482,15 @@ server.tool("score_project", "Score one or all projects on AI-readiness (0-100%)
|
|
|
482
482
|
const projectDirs = loadProjectDirs();
|
|
483
483
|
let scores;
|
|
484
484
|
if (project) {
|
|
485
|
-
|
|
485
|
+
// [SCORE-ACCEPTS-PATH] — resolve names and paths alike. This tool never writes
|
|
486
|
+
// SCORE.md, so unlike the CLI its fleet-wide default is harmless and is kept.
|
|
487
|
+
const dir = resolveProjectDir(project, projectDirs);
|
|
486
488
|
if (!dir) {
|
|
487
489
|
return {
|
|
488
490
|
content: [
|
|
489
491
|
{
|
|
490
492
|
type: "text",
|
|
491
|
-
text: `Project "${project}" not found. Available: ${projectDirs.map((d) => d.name).join(", ")}`,
|
|
493
|
+
text: `Project "${project}" not found — not a known project name, and not an existing directory. Available: ${projectDirs.map((d) => d.name).join(", ")}`,
|
|
492
494
|
},
|
|
493
495
|
],
|
|
494
496
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@compr/opscontext-mcp",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "OpsContext for AI Agents — read-only fleet visibility (PM2/nginx/Docker/git/cron) + tamper-evident audit log + policy-as-code hooks. The ops + compliance layer Claude Code can't grow natively.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,7 +14,7 @@ ContextEngine turns your project documentation into a **queryable knowledge base
|
|
|
14
14
|
### 1. Initialize (one-time per project)
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npx @compr/
|
|
17
|
+
npx @compr/opscontext-mcp init
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Creates `contextengine.json` config + `.github/copilot-instructions.md` template in the current directory.
|
|
@@ -39,14 +39,16 @@ ContextEngine auto-discovers documentation files from 7 common patterns:
|
|
|
39
39
|
ContextEngine also works as a standalone CLI tool — no MCP client needed:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
npx @compr/
|
|
43
|
-
npx @compr/
|
|
44
|
-
npx @compr/
|
|
45
|
-
npx @compr/
|
|
46
|
-
npx @compr/
|
|
47
|
-
npx @compr/
|
|
48
|
-
npx @compr/
|
|
49
|
-
npx @compr/
|
|
42
|
+
npx @compr/opscontext-mcp search "docker nginx" # Search knowledge base
|
|
43
|
+
npx @compr/opscontext-mcp list-sources # Show indexed sources
|
|
44
|
+
npx @compr/opscontext-mcp list-projects # Discover all projects
|
|
45
|
+
npx @compr/opscontext-mcp score # Score the CURRENT project (writes its SCORE.md)
|
|
46
|
+
npx @compr/opscontext-mcp score ~/Projects/PLANK.io # Score one project by name or path
|
|
47
|
+
npx @compr/opscontext-mcp score --all --no-save # Score every project, write nothing
|
|
48
|
+
npx @compr/opscontext-mcp score --html # Visual HTML report
|
|
49
|
+
npx @compr/opscontext-mcp list-learnings security # List learnings by category
|
|
50
|
+
npx @compr/opscontext-mcp audit # Compliance audit
|
|
51
|
+
npx @compr/opscontext-mcp help # Show all commands
|
|
50
52
|
```
|
|
51
53
|
|
|
52
54
|
## MCP Server Setup
|
|
@@ -62,7 +64,7 @@ Add to `.vscode/mcp.json` in your project root:
|
|
|
62
64
|
"servers": {
|
|
63
65
|
"contextengine": {
|
|
64
66
|
"command": "npx",
|
|
65
|
-
"args": ["-y", "@compr/
|
|
67
|
+
"args": ["-y", "@compr/opscontext-mcp"],
|
|
66
68
|
"env": {
|
|
67
69
|
"CONTEXTENGINE_WORKSPACES": "/path/to/your/projects"
|
|
68
70
|
}
|
|
@@ -80,7 +82,7 @@ Add to Claude Desktop MCP config:
|
|
|
80
82
|
"mcpServers": {
|
|
81
83
|
"contextengine": {
|
|
82
84
|
"command": "npx",
|
|
83
|
-
"args": ["-y", "@compr/
|
|
85
|
+
"args": ["-y", "@compr/opscontext-mcp"],
|
|
84
86
|
"env": {
|
|
85
87
|
"CONTEXTENGINE_WORKSPACES": "/path/to/your/projects"
|
|
86
88
|
}
|
|
@@ -98,7 +100,7 @@ Add to your OpenClaw `openclaw.json` MCP servers section:
|
|
|
98
100
|
"mcpServers": {
|
|
99
101
|
"contextengine": {
|
|
100
102
|
"command": "npx",
|
|
101
|
-
"args": ["-y", "@compr/
|
|
103
|
+
"args": ["-y", "@compr/opscontext-mcp"],
|
|
102
104
|
"env": {
|
|
103
105
|
"CONTEXTENGINE_WORKSPACES": "/path/to/your/projects"
|
|
104
106
|
}
|
|
@@ -186,7 +188,7 @@ load_session --name "project-x"
|
|
|
186
188
|
|
|
187
189
|
## Configuration
|
|
188
190
|
|
|
189
|
-
Create `contextengine.json` in your project root (or run `npx @compr/
|
|
191
|
+
Create `contextengine.json` in your project root (or run `npx @compr/opscontext-mcp init`):
|
|
190
192
|
|
|
191
193
|
```json
|
|
192
194
|
{
|
|
@@ -220,14 +222,14 @@ Create `contextengine.json` in your project root (or run `npx @compr/contextengi
|
|
|
220
222
|
- First run downloads the embedding model (~22MB) — subsequent runs use cache
|
|
221
223
|
- Keyword search is available instantly at startup; semantic search becomes available once the model loads
|
|
222
224
|
- License: BSL-1.1 (Business Source License)
|
|
223
|
-
- npm: `@compr/
|
|
225
|
+
- npm: `@compr/opscontext-mcp`
|
|
224
226
|
|
|
225
227
|
### Post-Commit Verification
|
|
226
228
|
|
|
227
229
|
After every `git push`, call `end_session` to verify nothing was missed:
|
|
228
230
|
|
|
229
231
|
- **MCP tool**: `end_session` — use when ContextEngine MCP server is connected
|
|
230
|
-
- **CLI fallback**: `npx @compr/
|
|
232
|
+
- **CLI fallback**: `npx @compr/opscontext-mcp end-session` — use when MCP is not connected (Cursor, Copilot, terminal sessions)
|
|
231
233
|
|
|
232
234
|
Resolve any FAIL items before finishing. Workflow: `commit → push → end_session → verify → fix anything it catches → re-commit if needed`.
|
|
233
235
|
|