@compr/opscontext-mcp 2.8.1 → 2.8.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 +10 -0
- package/dist/cli.js +4 -0
- package/dist/index.js +4 -2
- package/dist/source-summary.d.ts +9 -0
- package/dist/source-summary.js +150 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ 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.8.2] 2026-09-08: every source says what it is
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **One-line summary per source in `list_sources` and `contextengine list-sources`**
|
|
12
|
+
(`src/source-summary.ts`): frontmatter `description` when the file has one, else H1 title
|
|
13
|
+
plus first prose line, else the module docstring or header comment for code. Read from the
|
|
14
|
+
first 4 KB of each file only. Why: with 800+ sources an agent opened two or three whole
|
|
15
|
+
documents to find the right one; the summary lets it pick once.
|
|
16
|
+
|
|
7
17
|
## [2.8.1] 2026-09-07: a push is not done until its CI is read
|
|
8
18
|
|
|
9
19
|
### Added
|
package/dist/cli.js
CHANGED
|
@@ -641,6 +641,7 @@ async function runInit() {
|
|
|
641
641
|
// ---------------------------------------------------------------------------
|
|
642
642
|
import { loadSources, loadProjectDirs, loadConfig, resolveProjectDir, findProjectRoot, looksLikePath } from "./config.js";
|
|
643
643
|
import { ingestSources } from "./ingest.js";
|
|
644
|
+
import { summarizeSource } from "./source-summary.js";
|
|
644
645
|
import { searchChunks } from "./search.js";
|
|
645
646
|
import { SERVER_COMMANDS, suggestCommands } from "./cli-commands.js";
|
|
646
647
|
import { collectProjectOps, collectSystemOps } from "./collectors.js";
|
|
@@ -743,6 +744,9 @@ async function cliListSources() {
|
|
|
743
744
|
const count = chunks.filter((c) => c.source === s.name).length;
|
|
744
745
|
const status = exists ? `✅ ${count} chunks` : "⚠ file not found";
|
|
745
746
|
console.log(` ${s.name}: ${status}`);
|
|
747
|
+
const summary = exists ? summarizeSource(s) : "";
|
|
748
|
+
if (summary)
|
|
749
|
+
console.log(` ${summary}`);
|
|
746
750
|
console.log(` ${s.path}`);
|
|
747
751
|
}
|
|
748
752
|
console.log("");
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { loadSources, loadProjectDirs, loadConfig, resolveProjectDir } from "./config.js";
|
|
6
6
|
import { ingestSources } from "./ingest.js";
|
|
7
|
+
import { summarizeSource } from "./source-summary.js";
|
|
7
8
|
import { searchChunks } from "./search.js";
|
|
8
9
|
import { initEmbeddings, embedChunks, embedKeyOf, vectorSearch, isEmbeddingsReady, } from "./embeddings.js";
|
|
9
10
|
import { collectProjectOps, collectSystemOps } from "./collectors.js";
|
|
@@ -575,7 +576,7 @@ server.tool("search_context", "Search across all indexed project knowledge (copi
|
|
|
575
576
|
// ---------------------------------------------------------------------------
|
|
576
577
|
// Tool: list_sources
|
|
577
578
|
// ---------------------------------------------------------------------------
|
|
578
|
-
server.tool("list_sources", "List all knowledge sources indexed by ContextEngine, with
|
|
579
|
+
server.tool("list_sources", "List all knowledge sources indexed by ContextEngine, each with a one-line summary (from the file's own head: frontmatter description, title plus first sentence, or module docstring), status (found/missing) and chunk counts. Read the summary to pick the right source before calling read_source.", {}, async () => {
|
|
579
580
|
const lines = sources.map((s) => {
|
|
580
581
|
const exists = existsSync(s.path);
|
|
581
582
|
const count = chunks.filter((c) => c.source === s.name).length;
|
|
@@ -583,7 +584,8 @@ server.tool("list_sources", "List all knowledge sources indexed by ContextEngine
|
|
|
583
584
|
const status = exists
|
|
584
585
|
? `✅ ${count} chunks${embeddedCount > 0 ? ` (${embeddedCount} embedded)` : ""}`
|
|
585
586
|
: "⚠ file not found";
|
|
586
|
-
|
|
587
|
+
const summary = exists ? summarizeSource(s) : "";
|
|
588
|
+
return `${s.name}: ${status}${summary ? `\n ${summary}` : ""}\n ${s.path}`;
|
|
587
589
|
});
|
|
588
590
|
const embStatus = isEmbeddingsReady()
|
|
589
591
|
? `✅ ${embeddedChunks.length} vectors`
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { KnowledgeSource } from "./config.js";
|
|
2
|
+
export declare const HEAD_BYTES = 4096;
|
|
3
|
+
export declare const SUMMARY_MAX = 110;
|
|
4
|
+
/** Read at most `bytes` from the start of a file. Empty string on any error. */
|
|
5
|
+
export declare function readHead(path: string, bytes?: number): string;
|
|
6
|
+
/** Summary for a configured source; "" when the file is unreadable or says nothing. */
|
|
7
|
+
export declare function summarizeSource(source: KnowledgeSource): string;
|
|
8
|
+
export declare function summarizeText(text: string, type: "markdown" | "code"): string;
|
|
9
|
+
//# sourceMappingURL=source-summary.d.ts.map
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-line summary per knowledge source, for list_sources (MCP) and
|
|
3
|
+
* `contextengine list-sources` (CLI).
|
|
4
|
+
*
|
|
5
|
+
* Why: with 800+ sources, an agent reading a bare name plus a path opens two
|
|
6
|
+
* or three files to find the right one. Each open puts a whole document into
|
|
7
|
+
* its context (see CLAUDE.md, multi-agent cost). A summary line derived from
|
|
8
|
+
* the first few KB of the file lets it pick once.
|
|
9
|
+
*
|
|
10
|
+
* Reads only the head of the file (HEAD_BYTES), never the whole document, so
|
|
11
|
+
* the cost is bounded no matter how many sources are configured.
|
|
12
|
+
*/
|
|
13
|
+
import { openSync, readSync, closeSync } from "fs";
|
|
14
|
+
export const HEAD_BYTES = 4096;
|
|
15
|
+
export const SUMMARY_MAX = 110;
|
|
16
|
+
/** Read at most `bytes` from the start of a file. Empty string on any error. */
|
|
17
|
+
export function readHead(path, bytes = HEAD_BYTES) {
|
|
18
|
+
let fd;
|
|
19
|
+
try {
|
|
20
|
+
fd = openSync(path, "r");
|
|
21
|
+
const buf = Buffer.alloc(bytes);
|
|
22
|
+
const n = readSync(fd, buf, 0, bytes, 0);
|
|
23
|
+
return buf.subarray(0, n).toString("utf-8");
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
if (fd !== undefined)
|
|
30
|
+
closeSync(fd);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Summary for a configured source; "" when the file is unreadable or says nothing. */
|
|
34
|
+
export function summarizeSource(source) {
|
|
35
|
+
return summarizeText(readHead(source.path), source.type);
|
|
36
|
+
}
|
|
37
|
+
export function summarizeText(text, type) {
|
|
38
|
+
if (!text)
|
|
39
|
+
return "";
|
|
40
|
+
return clip(type === "code" ? summarizeCode(text) : summarizeMarkdown(text));
|
|
41
|
+
}
|
|
42
|
+
function clip(s) {
|
|
43
|
+
const one = s.replace(/\s+/g, " ").trim();
|
|
44
|
+
if (one.length <= SUMMARY_MAX)
|
|
45
|
+
return one;
|
|
46
|
+
return one.slice(0, SUMMARY_MAX - 3).trimEnd() + "...";
|
|
47
|
+
}
|
|
48
|
+
/** Strip markdown decoration so the line reads as plain text. */
|
|
49
|
+
function plain(line) {
|
|
50
|
+
return line
|
|
51
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, "") // images
|
|
52
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1") // links -> text
|
|
53
|
+
.replace(/`([^`]*)`/g, "$1") // inline code
|
|
54
|
+
.replace(/\*\*|__/g, "") // bold
|
|
55
|
+
.replace(/^\s*(?:[-*+]|\d+[.)])\s+/, "") // list bullet
|
|
56
|
+
.replace(/^\s*>\s?/, "") // blockquote
|
|
57
|
+
.trim();
|
|
58
|
+
}
|
|
59
|
+
/** Header metadata lines that say nothing about the content. */
|
|
60
|
+
const META_LINE = /^(updated|last updated|created|date|status|author|owner|version|scope|audience)\b\s*[:\-]/i;
|
|
61
|
+
function summarizeMarkdown(text) {
|
|
62
|
+
let body = text;
|
|
63
|
+
// YAML frontmatter: prefer its description, it IS a one-line summary.
|
|
64
|
+
if (body.startsWith("---\n") || body.startsWith("---\r\n")) {
|
|
65
|
+
const end = body.indexOf("\n---", 4);
|
|
66
|
+
if (end !== -1) {
|
|
67
|
+
const fm = body.slice(4, end);
|
|
68
|
+
const desc = fm.match(/^description:\s*(.+)$/m);
|
|
69
|
+
if (desc)
|
|
70
|
+
return desc[1].trim().replace(/^["']|["']$/g, "");
|
|
71
|
+
body = body.slice(end + 4);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
let title = "";
|
|
75
|
+
let prose = "";
|
|
76
|
+
let inFence = false;
|
|
77
|
+
for (const raw of body.split("\n")) {
|
|
78
|
+
const line = raw.trim();
|
|
79
|
+
if (line.startsWith("```") || line.startsWith("~~~")) {
|
|
80
|
+
inFence = !inFence;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (inFence || !line)
|
|
84
|
+
continue;
|
|
85
|
+
if (line.startsWith("<!--") || line.startsWith("|") || line.startsWith("![") || line.startsWith("[!"))
|
|
86
|
+
continue;
|
|
87
|
+
if (/^[-=*_]{3,}$/.test(line))
|
|
88
|
+
continue;
|
|
89
|
+
if (line.startsWith("#")) {
|
|
90
|
+
if (!title)
|
|
91
|
+
title = plain(line.replace(/^#+\s*/, ""));
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const p = plain(line);
|
|
95
|
+
if (p.length < 12)
|
|
96
|
+
continue; // lone dates, "v2.1", etc.
|
|
97
|
+
if (META_LINE.test(p))
|
|
98
|
+
continue; // "Updated: 2026-03-13", "Status: draft"
|
|
99
|
+
prose = p;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
if (title && prose)
|
|
103
|
+
return `${title}: ${prose}`;
|
|
104
|
+
return title || prose;
|
|
105
|
+
}
|
|
106
|
+
const CODE_SKIP = /^(#!|\/\/\s*eslint|\/\*\s*eslint|@ts-|#\s*-\*-|#\s*coding[:=]|SPDX|Copyright|\[LOCK|\[LOCKED\]|\[NEVER\]|WHY:|FIX:|import |from |export |const |let |var |use strict|"use strict")/i;
|
|
107
|
+
function summarizeCode(text) {
|
|
108
|
+
const lines = text.split("\n");
|
|
109
|
+
let inBlock = false;
|
|
110
|
+
for (const raw of lines) {
|
|
111
|
+
let line = raw.trim();
|
|
112
|
+
if (!line)
|
|
113
|
+
continue;
|
|
114
|
+
if (!inBlock) {
|
|
115
|
+
const opensBlock = line.startsWith("/*") || line.startsWith('"""') || line.startsWith("'''");
|
|
116
|
+
const closesOnSameLine = (line.startsWith("/*") && /\*\/$/.test(line)) ||
|
|
117
|
+
(/^("""|''')/.test(line) && line.length > 3 && /("""|''')$/.test(line));
|
|
118
|
+
if (CODE_SKIP.test(line)) {
|
|
119
|
+
// Pragma, shebang or marker on the raw line: skip it. A one-line
|
|
120
|
+
// block comment ("/* eslint-disable */") must not leave us in a block.
|
|
121
|
+
if (opensBlock && !closesOnSameLine)
|
|
122
|
+
inBlock = true;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (opensBlock) {
|
|
126
|
+
inBlock = !closesOnSameLine;
|
|
127
|
+
line = line.replace(/^(\/\*+|"""|''')\s*/, "").replace(/(\*\/|"""|''')\s*$/, "").trim();
|
|
128
|
+
}
|
|
129
|
+
else if (line.startsWith("//") || line.startsWith("#")) {
|
|
130
|
+
line = line.replace(/^(\/\/|#)+\s*/, "").trim();
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// Code before any comment: nothing worth saying about this file.
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
if (/(\*\/|"""|''')$/.test(line))
|
|
139
|
+
inBlock = false;
|
|
140
|
+
line = line.replace(/^\*+\s*/, "").replace(/(\*\/|"""|''')\s*$/, "").trim();
|
|
141
|
+
}
|
|
142
|
+
if (!line || /^[-=*_#]{3,}$/.test(line) || CODE_SKIP.test(line))
|
|
143
|
+
continue;
|
|
144
|
+
if (line.length < 12)
|
|
145
|
+
continue;
|
|
146
|
+
return line;
|
|
147
|
+
}
|
|
148
|
+
return "";
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=source-summary.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@compr/opscontext-mcp",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.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",
|