@compr/opscontext-mcp 2.4.1 → 2.4.2
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 +47 -0
- package/README.md +22 -2
- package/dist/cli.js +29 -4
- package/dist/config.d.ts +9 -33
- package/dist/config.js +97 -27
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,53 @@ All notable changes to OpsContext for AI Agents (previously ContextEngine — MC
|
|
|
4
4
|
|
|
5
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
6
|
|
|
7
|
+
## [2.4.2] — 2026-08-16 — Close the three holes 2.4.1 left in "a directory is not a project"
|
|
8
|
+
|
|
9
|
+
2.4.1 made `score`'s scope explicit but enforced it on only one of three entry points. An
|
|
10
|
+
adversarial review (30 agents, sandboxed) raised 26 findings, refuted 18, and confirmed 8 —
|
|
11
|
+
clustering into the fixes below. Every repro was reproduced against the shipped build before
|
|
12
|
+
being fixed, and re-run after.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **`score .` re-created the exact incident 2.4.1's LOCK forbids.** `cd ~/Projects && score .`
|
|
17
|
+
wrote `~/Projects/SCORE.md` — *"Projects: 27/100 (F), Not a git repo"* — into the container of
|
|
18
|
+
37 repositories. The identical command *without* the `.` was correctly refused: the guard
|
|
19
|
+
existed, and the explicit-path entry point walked straight past it. Same for `score ~/Projects`,
|
|
20
|
+
`score ..` and `score /`. A path must now carry a build/VCS marker
|
|
21
|
+
(`.git`, `package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`, `Makefile`, …) to be
|
|
22
|
+
scoreable. Locked as `[RESOLVE-PATH-MUST-BE-A-PROJECT]`.
|
|
23
|
+
- **A mistyped project name silently scored a subdirectory.** `score src`, `score dist`,
|
|
24
|
+
`score docs` no longer errored with the available-projects list — the bare token fell through
|
|
25
|
+
to path resolution against the cwd, scored the subdirectory, and wrote a `SCORE.md` into it.
|
|
26
|
+
In this repo `score dist` wrote `dist/SCORE.md`, which then ships inside the npm tarball. A
|
|
27
|
+
bare name now resolves against the fleet index **only**; the error names the directory it
|
|
28
|
+
found and offers `score ./src` as the explicit escape hatch.
|
|
29
|
+
- **`score` in a subpackage reported "Not a git repo" from inside a git repo.** `cd server &&
|
|
30
|
+
score` stopped at the nearest `package.json` and reported *"server — 32% (F), Not a git repo,
|
|
31
|
+
No CI pipeline, README.md Missing"* — every claim false about the project the user was standing
|
|
32
|
+
in. `.git` is now the project boundary and wins over any nearer build file; a genuinely
|
|
33
|
+
standalone package still resolves to itself. Locked as `[GIT-ROOT-IS-THE-PROJECT-BOUNDARY]`.
|
|
34
|
+
- **`CONTEXTENGINE_WORKSPACES` was read and ignored.** It was a fallback that applied only when
|
|
35
|
+
the config file defined no `workspaces` — so on the documented setup it did nothing, including
|
|
36
|
+
in every MCP config block this project ships. Precedence is now env var > config file >
|
|
37
|
+
auto-discovery. Locked as `[ENV-WORKSPACES-WINS]`.
|
|
38
|
+
- Error messages no longer say "not an existing directory" about a directory that exists. Each
|
|
39
|
+
of the four failure modes (unknown name / no such path / not a directory / not a project) now
|
|
40
|
+
names the actual reason.
|
|
41
|
+
|
|
42
|
+
### Documentation
|
|
43
|
+
|
|
44
|
+
- README's "Config resolution order" table documented the pre-fix precedence and could not
|
|
45
|
+
describe both surfaces at once. Now split: config-file lookup, project-fleet lookup (env wins),
|
|
46
|
+
and an explicit note that the *search corpus* still prefers the config file.
|
|
47
|
+
- `CLAUDE.md` still told every agent in this repo that `npx . score` scores all projects.
|
|
48
|
+
|
|
49
|
+
### Tests
|
|
50
|
+
|
|
51
|
+
373 pass (8 new). One pre-existing test asserted the old behaviour — a marker-less directory
|
|
52
|
+
resolving as a project — and was corrected rather than deleted.
|
|
53
|
+
|
|
7
54
|
## [2.4.1] — 2026-08-16 — `score` scope is now explicit: current project by default, `--all` to opt into the fleet
|
|
8
55
|
|
|
9
56
|
### Changed (behaviour — read this before upgrading)
|
package/README.md
CHANGED
|
@@ -348,13 +348,33 @@ For full control, create a `contextengine.json`:
|
|
|
348
348
|
|
|
349
349
|
### Config resolution order
|
|
350
350
|
|
|
351
|
+
Which **config file** is read (both the search corpus and the project fleet):
|
|
352
|
+
|
|
351
353
|
| Priority | Source |
|
|
352
354
|
|----------|--------|
|
|
353
355
|
| 1 | `CONTEXTENGINE_CONFIG` env var |
|
|
354
356
|
| 2 | `./contextengine.json` |
|
|
355
357
|
| 3 | `~/.contextengine.json` |
|
|
356
|
-
|
|
357
|
-
|
|
358
|
+
|
|
359
|
+
Which **project fleet** is scanned — this is what `score --all`, `audit`, `list_projects`
|
|
360
|
+
and `check_ports` operate on:
|
|
361
|
+
|
|
362
|
+
| Priority | Source |
|
|
363
|
+
|----------|--------|
|
|
364
|
+
| 1 | `CONTEXTENGINE_WORKSPACES` env var (colon-separated) |
|
|
365
|
+
| 2 | `workspaces` in the config file |
|
|
366
|
+
| 3 | `~/Projects` auto-discover |
|
|
367
|
+
|
|
368
|
+
**The env var wins.** It is set per-invocation, so it is the most specific statement of
|
|
369
|
+
intent — and it is what the MCP config blocks in this README set. Use it to scope a run:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
CONTEXTENGINE_WORKSPACES=/tmp/sandbox npx @compr/opscontext-mcp score --all
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
> Note: the **search corpus** (`search`, `reindex`, `list-sources`) still prefers the config
|
|
376
|
+
> file's `workspaces` over the env var. If you rely on the env var to scope indexing, set
|
|
377
|
+
> `CONTEXTENGINE_CONFIG` to a config without `workspaces`, or unset `workspaces` there.
|
|
358
378
|
|
|
359
379
|
## Plugin Adapters
|
|
360
380
|
|
package/dist/cli.js
CHANGED
|
@@ -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, resolveProjectDir, findProjectRoot } from "./config.js";
|
|
520
|
+
import { loadSources, loadProjectDirs, loadConfig, resolveProjectDir, findProjectRoot, looksLikePath } 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";
|
|
@@ -751,9 +751,34 @@ async function cliScore(project, html = false, save = true, all = false) {
|
|
|
751
751
|
// [SCORE-ACCEPTS-PATH] — a path is as valid an identifier as a name.
|
|
752
752
|
const dir = resolveProjectDir(project, projectDirs);
|
|
753
753
|
if (!dir) {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
754
|
+
// Say which of the three failures actually happened. "Not found" for a
|
|
755
|
+
// directory that plainly exists is the same absence-as-verdict mistake
|
|
756
|
+
// [SCORE-ACCEPTS-PATH] was written to fix — do not reintroduce it here.
|
|
757
|
+
const abs = resolve(project.replace(/^~/, homedir()));
|
|
758
|
+
const exists = existsSync(abs);
|
|
759
|
+
const isDir = exists && statSync(abs).isDirectory();
|
|
760
|
+
if (!looksLikePath(project)) {
|
|
761
|
+
// A bare name that missed the fleet index. Deliberately NOT resolved as a
|
|
762
|
+
// relative directory — that is what silently scored ./src and ./dist.
|
|
763
|
+
console.error(`❌ Project not found: "${project}"`);
|
|
764
|
+
if (isDir) {
|
|
765
|
+
console.error(` A directory named "${project}" exists here, but it is not a`);
|
|
766
|
+
console.error(` project in your fleet. To score it anyway: contextengine score ./${project}`);
|
|
767
|
+
}
|
|
768
|
+
console.error(`Available: ${projectDirs.map((d) => d.name).join(", ")}`);
|
|
769
|
+
}
|
|
770
|
+
else if (!exists) {
|
|
771
|
+
console.error(`❌ No such directory: ${abs}`);
|
|
772
|
+
}
|
|
773
|
+
else if (!isDir) {
|
|
774
|
+
console.error(`❌ Not a directory: ${abs}`);
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
console.error(`❌ Not a project: ${abs}`);
|
|
778
|
+
console.error(` The directory exists, but has no .git, package.json, pyproject.toml`);
|
|
779
|
+
console.error(` or other build file — so it looks like a container, not a project.`);
|
|
780
|
+
console.error(` If you meant the projects INSIDE it, use: contextengine score --all`);
|
|
781
|
+
}
|
|
757
782
|
process.exit(1);
|
|
758
783
|
}
|
|
759
784
|
scores = [scoreProject(dir)];
|
package/dist/config.d.ts
CHANGED
|
@@ -67,41 +67,17 @@ export declare function loadSources(): KnowledgeSource[];
|
|
|
67
67
|
*/
|
|
68
68
|
export declare function loadProjectDirs(): ProjectDirectory[];
|
|
69
69
|
/**
|
|
70
|
-
*
|
|
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`.
|
|
70
|
+
* Does this token look like a filesystem path rather than a bare project name?
|
|
100
71
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
72
|
+
* Deliberately conservative: only strings that CANNOT be a directory basename
|
|
73
|
+
* (they contain a separator, or start with `~`/`.`) are treated as
|
|
74
|
+
* path-only. Everything else stays eligible for name lookup first, so
|
|
75
|
+
* `score KONIVE.com` keeps resolving exactly as it did before this existed.
|
|
104
76
|
*/
|
|
77
|
+
export declare function looksLikePath(token: string): boolean;
|
|
78
|
+
/** Does this directory carry any build/VCS marker that makes it a project? */
|
|
79
|
+
export declare function hasProjectMarker(dir: string): boolean;
|
|
80
|
+
export declare function resolveProjectDir(token: string, dirs: ProjectDirectory[]): ProjectDirectory | null;
|
|
105
81
|
export declare function findProjectRoot(start: string): string | null;
|
|
106
82
|
/**
|
|
107
83
|
* Load the raw config (for checking flags like collectSystemOps).
|
package/dist/config.js
CHANGED
|
@@ -188,12 +188,27 @@ export function loadProjectDirs() {
|
|
|
188
188
|
workspaceDirs = config.workspaces.map((w) => resolve(configPath, "..", w.replace(/^~/, homedir())));
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
191
|
+
/**
|
|
192
|
+
* 🔒 LOCKED [ENV-WORKSPACES-WINS] — 2026-08-16
|
|
193
|
+
* ⛔ NEVER demote CONTEXTENGINE_WORKSPACES back to a fallback that only applies when the
|
|
194
|
+
* config file happens not to define `workspaces`.
|
|
195
|
+
* WHY: it WAS such a fallback (`if (workspaceDirs.length === 0)`), so on any machine with a
|
|
196
|
+
* contextengine.json defining workspaces — which is the documented setup — the env var
|
|
197
|
+
* was read, ignored, and never reported. Every MCP config block we ship in
|
|
198
|
+
* skills/opscontext/SKILL.md sets `env: { CONTEXTENGINE_WORKSPACES: ... }`, so our own
|
|
199
|
+
* documented integration silently did nothing.
|
|
200
|
+
* Caught the hard way: an attempt to sandbox a review agent by pointing this variable at
|
|
201
|
+
* a scratch directory was ignored, and `score --all` wrote SCORE.md into 28 real
|
|
202
|
+
* repositories instead. The sandbox reported success because the variable was accepted
|
|
203
|
+
* without complaint — absence of an error read as confirmation.
|
|
204
|
+
* FIX: standard precedence — an explicit env var beats a config file beats auto-discovery.
|
|
205
|
+
* It is set per-invocation and is therefore the most specific statement of intent.
|
|
206
|
+
*/
|
|
207
|
+
const envWorkspaces = process.env.CONTEXTENGINE_WORKSPACES;
|
|
208
|
+
if (envWorkspaces) {
|
|
209
|
+
const fromEnv = envWorkspaces.split(":").filter(Boolean);
|
|
210
|
+
if (fromEnv.length > 0)
|
|
211
|
+
workspaceDirs = fromEnv;
|
|
197
212
|
}
|
|
198
213
|
// Auto-discover fallback
|
|
199
214
|
if (workspaceDirs.length === 0) {
|
|
@@ -235,7 +250,7 @@ export function loadProjectDirs() {
|
|
|
235
250
|
* path-only. Everything else stays eligible for name lookup first, so
|
|
236
251
|
* `score KONIVE.com` keeps resolving exactly as it did before this existed.
|
|
237
252
|
*/
|
|
238
|
-
function looksLikePath(token) {
|
|
253
|
+
export function looksLikePath(token) {
|
|
239
254
|
return (token.includes("/") ||
|
|
240
255
|
token.includes(sep) ||
|
|
241
256
|
token.startsWith("~") ||
|
|
@@ -253,31 +268,60 @@ function looksLikePath(token) {
|
|
|
253
268
|
* never inspected — the lookup only ever compared basenames, so this was
|
|
254
269
|
* [ABSENCE-IS-NOT-A-VERDICT] at the argument-parsing layer: "not in my
|
|
255
270
|
* name index" was reported as "does not exist".
|
|
256
|
-
* FIX: resolve names AND paths. A path that exists
|
|
257
|
-
* it sits under a configured workspace — that is what makes
|
|
258
|
-
* outside `~/Projects`.
|
|
271
|
+
* FIX: resolve names AND paths. A path that exists AND carries a project marker is a
|
|
272
|
+
* project, whether or not it sits under a configured workspace — that is what makes
|
|
273
|
+
* the tool usable outside `~/Projects`.
|
|
274
|
+
*
|
|
275
|
+
* 🔒 LOCKED [RESOLVE-PATH-MUST-BE-A-PROJECT] — 2026-08-16
|
|
276
|
+
* ⛔ NEVER accept "it is a directory that exists" as proof that a path is a project, and
|
|
277
|
+
* NEVER let a bare name that missed the index fall through to path resolution.
|
|
278
|
+
* WHY: the first cut did both, and an adversarial review reproduced three consequences.
|
|
279
|
+
* 1. `cd ~/Projects && score .` wrote `~/Projects/SCORE.md` — "Projects: 27/100 (F),
|
|
280
|
+
* Not a git repo" — into the CONTAINER of 37 repositories. The identical command
|
|
281
|
+
* WITHOUT the `.` was correctly refused, so the guard existed and one entry point
|
|
282
|
+
* walked straight past it. Same for `score ~/Projects`, `score ..`, and `score /`.
|
|
283
|
+
* 2. `score src`, `score dist`, `score docs` — a typo or a half-remembered name — no
|
|
284
|
+
* longer errored with "Available: …". The bare token fell through to
|
|
285
|
+
* `resolve(token)` against the cwd, so it silently scored a SUBDIRECTORY and wrote
|
|
286
|
+
* a SCORE.md into it. In this repo `score dist` writes `dist/SCORE.md`, which then
|
|
287
|
+
* ships inside the npm tarball.
|
|
288
|
+
* 3. It made the sibling LOCK a half-truth: [SCORE-CWD-MUST-BE-A-PROJECT] promises a
|
|
289
|
+
* non-project is never scored, but enforced it on the no-argument path only.
|
|
290
|
+
* FIX: a directory must carry a build/VCS marker to be scoreable, and a bare name resolves
|
|
291
|
+
* against the fleet index ONLY. Configured projects always pass — they are the fleet
|
|
292
|
+
* by definition. Absence of a marker is a measurement, not permission to write.
|
|
259
293
|
*/
|
|
294
|
+
const PROJECT_MARKERS = [
|
|
295
|
+
".git", "package.json", "pyproject.toml", "requirements.txt", "setup.py",
|
|
296
|
+
"composer.json", "pubspec.yaml", "go.mod", "Cargo.toml", "Gemfile",
|
|
297
|
+
"pom.xml", "build.gradle", "Makefile", "CMakeLists.txt",
|
|
298
|
+
];
|
|
299
|
+
/** Does this directory carry any build/VCS marker that makes it a project? */
|
|
300
|
+
export function hasProjectMarker(dir) {
|
|
301
|
+
return PROJECT_MARKERS.some((m) => existsSync(join(dir, m)));
|
|
302
|
+
}
|
|
260
303
|
export function resolveProjectDir(token, dirs) {
|
|
261
|
-
//
|
|
304
|
+
// A bare name resolves against the fleet index ONLY. It must never silently
|
|
305
|
+
// become a cwd-relative directory — that is how `score src` wrote src/SCORE.md
|
|
306
|
+
// instead of printing "Project not found. Available: …". Use `./src` to mean a path.
|
|
262
307
|
if (!looksLikePath(token)) {
|
|
263
|
-
|
|
264
|
-
if (byName)
|
|
265
|
-
return byName;
|
|
308
|
+
return (dirs.find((d) => d.name.toLowerCase() === token.toLowerCase()) ?? null);
|
|
266
309
|
}
|
|
267
310
|
// Path resolution — absolute, relative, or `~`-prefixed.
|
|
268
311
|
const abs = resolve(token.replace(/^~/, homedir()));
|
|
269
312
|
try {
|
|
270
|
-
if (statSync(abs).isDirectory())
|
|
271
|
-
|
|
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
|
-
}
|
|
313
|
+
if (!statSync(abs).isDirectory())
|
|
314
|
+
return null;
|
|
276
315
|
}
|
|
277
316
|
catch {
|
|
278
|
-
// ENOENT / EACCES — not a usable directory.
|
|
317
|
+
return null; // ENOENT / EACCES — not a usable directory.
|
|
279
318
|
}
|
|
280
|
-
|
|
319
|
+
// A configured project is a project by definition, marker or not.
|
|
320
|
+
const known = dirs.find((d) => resolve(d.path) === abs);
|
|
321
|
+
if (known)
|
|
322
|
+
return known;
|
|
323
|
+
// Otherwise it must look like a project. `~/Projects` and `/` do not.
|
|
324
|
+
return hasProjectMarker(abs) ? { name: basename(abs), path: abs } : null;
|
|
281
325
|
}
|
|
282
326
|
/**
|
|
283
327
|
* 🔒 LOCKED [SCORE-CWD-MUST-BE-A-PROJECT] — 2026-08-16
|
|
@@ -295,21 +339,47 @@ export function resolveProjectDir(token, dirs) {
|
|
|
295
339
|
* instead (cd into a project, name one, or --all). Found by an adversarial review
|
|
296
340
|
* agent that ran the real CLI from `/` and `~/Projects`.
|
|
297
341
|
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
* above
|
|
342
|
+
* 🔒 LOCKED [GIT-ROOT-IS-THE-PROJECT-BOUNDARY] — 2026-08-16
|
|
343
|
+
* ⛔ NEVER return the nearest `package.json` directory without first checking whether a
|
|
344
|
+
* `.git` sits above it.
|
|
345
|
+
* WHY: stopping at the nearest marker meant `cd ContextEngine/server && score` reported
|
|
346
|
+
* **"Scoring current project: server ... 32% (F) — Not a git repo, No CI pipeline,
|
|
347
|
+
* README.md Missing"** and wrote `server/SCORE.md`. Every one of those statements is
|
|
348
|
+
* false about the project the user is standing in: the repo root has `.git`, CI, and
|
|
349
|
+
* a README. A build file marks a *package*; `.git` marks the *project*. Reporting
|
|
350
|
+
* "Not a git repo" from inside a git repo is the scorer describing a boundary it
|
|
351
|
+
* invented — absence-as-verdict again, this time about where the project ends.
|
|
352
|
+
* FIX: `.git` wins. Walk up looking for it, remembering the nearest build file on the way,
|
|
353
|
+
* and fall back to that remembered directory only if no `.git` exists anywhere above.
|
|
354
|
+
* A genuinely standalone package (no git anywhere) still resolves to itself.
|
|
355
|
+
*
|
|
356
|
+
* Walk up from `start` to the enclosing project root. Returns null when nothing is
|
|
357
|
+
* found anywhere above `start`.
|
|
301
358
|
*/
|
|
359
|
+
const BUILD_FILE_MARKERS = [
|
|
360
|
+
"package.json", "pyproject.toml", "requirements.txt", "setup.py",
|
|
361
|
+
"composer.json", "pubspec.yaml", "go.mod", "Cargo.toml", "Gemfile",
|
|
362
|
+
"pom.xml", "build.gradle", "Makefile", "CMakeLists.txt",
|
|
363
|
+
];
|
|
302
364
|
export function findProjectRoot(start) {
|
|
303
365
|
let dir = resolve(start);
|
|
366
|
+
let nearestBuildFile = null;
|
|
304
367
|
for (;;) {
|
|
305
|
-
|
|
368
|
+
// .git is the project boundary and always wins, however far up it sits.
|
|
369
|
+
if (existsSync(join(dir, ".git")))
|
|
306
370
|
return dir;
|
|
371
|
+
if (nearestBuildFile === null &&
|
|
372
|
+
BUILD_FILE_MARKERS.some((m) => existsSync(join(dir, m)))) {
|
|
373
|
+
nearestBuildFile = dir;
|
|
307
374
|
}
|
|
308
375
|
const parent = dirname(dir);
|
|
309
376
|
if (parent === dir)
|
|
310
|
-
|
|
377
|
+
break; // reached filesystem root
|
|
311
378
|
dir = parent;
|
|
312
379
|
}
|
|
380
|
+
// No .git anywhere above — a standalone package resolves to itself; a plain
|
|
381
|
+
// directory (a container, or /) resolves to nothing at all.
|
|
382
|
+
return nearestBuildFile;
|
|
313
383
|
}
|
|
314
384
|
/**
|
|
315
385
|
* Load the raw config (for checking flags like collectSystemOps).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@compr/opscontext-mcp",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
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",
|