@compr/opscontext-mcp 2.8.1 → 2.8.3
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 +26 -0
- package/dist/cli.js +4 -0
- package/dist/index.js +4 -2
- package/dist/install-claude-hook.d.ts +23 -0
- package/dist/install-claude-hook.js +84 -6
- 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,32 @@ 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.3] 2026-09-16: one hook per event, verified
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`install-claude-hook` no longer registers the emit hooks twice** (`src/install-claude-hook.ts`,
|
|
12
|
+
LOCKs `[HOOKS-COMPARED-BY-EXPANDED-PATH]`, `[INSTALL-VERIFIES-BY-COUNT]`). Its "already
|
|
13
|
+
installed?" check compared command text literally, so hooks written as
|
|
14
|
+
`$HOME/.claude/hooks/opscontext-emit.sh` looked absent and a second set was added: on the
|
|
15
|
+
author's machine every Claude Code prompt and tool call reached the audit log twice from
|
|
16
|
+
2026-09-06 to 2026-09-15, doubling the inputs of the `stuck` and `silent_failure` heuristics.
|
|
17
|
+
The installer now compares script paths with `$HOME`, `${HOME}` and `~` expanded, removes
|
|
18
|
+
extra copies of its own commands (other hooks untouched), and after writing re-reads
|
|
19
|
+
`settings.json` and exits 1 unless each event runs its script exactly once. Running
|
|
20
|
+
`install-claude-hook` again repairs an affected machine; the audit log is left as it is (it is
|
|
21
|
+
hash-chained).
|
|
22
|
+
|
|
23
|
+
## [2.8.2] 2026-09-08: every source says what it is
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **One-line summary per source in `list_sources` and `contextengine list-sources`**
|
|
28
|
+
(`src/source-summary.ts`): frontmatter `description` when the file has one, else H1 title
|
|
29
|
+
plus first prose line, else the module docstring or header comment for code. Read from the
|
|
30
|
+
first 4 KB of each file only. Why: with 800+ sources an agent opened two or three whole
|
|
31
|
+
documents to find the right one; the summary lets it pick once.
|
|
32
|
+
|
|
7
33
|
## [2.8.1] 2026-09-07: a push is not done until its CI is read
|
|
8
34
|
|
|
9
35
|
### 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`
|
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
export interface HookCommand {
|
|
2
|
+
type: string;
|
|
3
|
+
command: string;
|
|
4
|
+
timeout?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface HookEntry {
|
|
7
|
+
matcher?: string;
|
|
8
|
+
hooks: HookCommand[];
|
|
9
|
+
}
|
|
10
|
+
export interface Settings {
|
|
11
|
+
hooks?: Record<string, HookEntry[]>;
|
|
12
|
+
[k: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
/** The script path of a hook command, with $HOME, ${HOME} or a leading ~ expanded. */
|
|
15
|
+
export declare function hookScriptPath(command: string, home?: string): string;
|
|
16
|
+
/** Removes repeated registrations of our scripts under the same matcher. Keeps the first copy
|
|
17
|
+
* and every hook that is not ours; drops an entry only when that leaves it empty. */
|
|
18
|
+
export declare function dropDuplicateHooks(entries: HookEntry[], ourScripts: string[], home?: string): {
|
|
19
|
+
entries: HookEntry[];
|
|
20
|
+
removed: number;
|
|
21
|
+
};
|
|
22
|
+
/** How many times each event runs `script`. A correct install has exactly 1 everywhere. */
|
|
23
|
+
export declare function countOurHooks(settings: Settings, events: readonly string[], script: string, home?: string): Record<string, number>;
|
|
1
24
|
export declare function cliInstallClaudeHook(args: string[]): Promise<void>;
|
|
2
25
|
export declare function cliUninstallClaudeHook(args: string[]): Promise<void>;
|
|
3
26
|
//# sourceMappingURL=install-claude-hook.d.ts.map
|
|
@@ -50,10 +50,59 @@ function backupSettings() {
|
|
|
50
50
|
copyFileSync(SETTINGS_FILE, backup);
|
|
51
51
|
return backup;
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
// [LOCKED] [HOOKS-COMPARED-BY-EXPANDED-PATH] - 2026-09-15
|
|
54
|
+
// [NEVER] compare a hook command with startsWith or any other literal text match again.
|
|
55
|
+
// WHY: settings.json held the emit hooks as "$HOME/.claude/hooks/opscontext-emit.sh <Kind>",
|
|
56
|
+
// hand-wired on 2026-06-23 before this installer existed. On 2026-09-06 the 2.7.0 rollout
|
|
57
|
+
// ran install-claude-hook; its startsWith(absolute path) check did not see them, printed
|
|
58
|
+
// "4 hook entries added, 0 already present" and wrote a second set. From 2026-09-06
|
|
59
|
+
// 08:20:21Z every Claude Code event reached the audit log twice (0 doubled events in the 8
|
|
60
|
+
// days before, 99.5 to 100 percent every day after), doubling the stuck and silent_failure
|
|
61
|
+
// inputs that [OPSCONTEXT-CC-HOOK] protects.
|
|
62
|
+
// FIX: compare the script path after expanding $HOME, ${HOME} and a leading ~; installing also
|
|
63
|
+
// removes extra copies of our own commands under the same matcher, and nothing else.
|
|
64
|
+
/** The script path of a hook command, with $HOME, ${HOME} or a leading ~ expanded. */
|
|
65
|
+
export function hookScriptPath(command, home = homedir()) {
|
|
66
|
+
const m = /^\s*(?:"([^"]*)"|'([^']*)'|(\S+))/.exec(command ?? "");
|
|
67
|
+
const path = m ? (m[1] ?? m[2] ?? m[3]) : "";
|
|
68
|
+
return path.replace(/^(?:\$HOME|\$\{HOME\}|~)(?=\/)/, home);
|
|
69
|
+
}
|
|
70
|
+
/** The command with its script path expanded, so two spellings of one call compare equal. */
|
|
71
|
+
function normalizedCommand(command, home) {
|
|
72
|
+
const args = (command ?? "").trim().replace(/^(?:"[^"]*"|'[^']*'|\S+)/, "").trim();
|
|
73
|
+
return `${hookScriptPath(command, home)} ${args}`.trim();
|
|
74
|
+
}
|
|
75
|
+
function hookAlreadyWired(entries, hookScript, home = homedir()) {
|
|
76
|
+
return (entries ?? []).some((e) => e.hooks?.some((h) => hookScriptPath(h.command, home) === hookScript));
|
|
77
|
+
}
|
|
78
|
+
/** Removes repeated registrations of our scripts under the same matcher. Keeps the first copy
|
|
79
|
+
* and every hook that is not ours; drops an entry only when that leaves it empty. */
|
|
80
|
+
export function dropDuplicateHooks(entries, ourScripts, home = homedir()) {
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
let removed = 0;
|
|
83
|
+
const kept = [];
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
const before = entry.hooks ?? [];
|
|
86
|
+
const hooks = before.filter((h) => {
|
|
87
|
+
if (!ourScripts.includes(hookScriptPath(h.command, home)))
|
|
88
|
+
return true;
|
|
89
|
+
const key = `${entry.matcher ?? ""}\u0000${normalizedCommand(h.command, home)}`;
|
|
90
|
+
if (seen.has(key)) {
|
|
91
|
+
removed++;
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
seen.add(key);
|
|
95
|
+
return true;
|
|
96
|
+
});
|
|
97
|
+
if (hooks.length > 0 || before.length === 0)
|
|
98
|
+
kept.push({ ...entry, hooks });
|
|
99
|
+
}
|
|
100
|
+
return { entries: kept, removed };
|
|
101
|
+
}
|
|
102
|
+
/** How many times each event runs `script`. A correct install has exactly 1 everywhere. */
|
|
103
|
+
export function countOurHooks(settings, events, script, home = homedir()) {
|
|
104
|
+
const count = (ev) => (settings.hooks?.[ev] ?? []).flatMap((e) => e.hooks ?? []).filter((h) => hookScriptPath(h.command, home) === script).length;
|
|
105
|
+
return Object.fromEntries(events.map((ev) => [ev, count(ev)]));
|
|
57
106
|
}
|
|
58
107
|
/** Path to the reference hook script bundled with this package. */
|
|
59
108
|
function bundledHookSource() {
|
|
@@ -129,7 +178,17 @@ Run: opscontext install-autostart
|
|
|
129
178
|
if (backup)
|
|
130
179
|
console.log(`✅ Backed up settings.json → ${backup}`);
|
|
131
180
|
settings.hooks ??= {};
|
|
132
|
-
const hookCmdPrefix = `${HOOK_SCRIPT}`; //
|
|
181
|
+
const hookCmdPrefix = `${HOOK_SCRIPT}`; // compared by expanded path, [HOOKS-COMPARED-BY-EXPANDED-PATH]
|
|
182
|
+
// [LOCK] [HOOKS-COMPARED-BY-EXPANDED-PATH]: remove extra copies before deciding what to add.
|
|
183
|
+
let deduped = 0;
|
|
184
|
+
for (const kind of [...EVENT_KINDS, "Stop"]) {
|
|
185
|
+
const entries = settings.hooks[kind];
|
|
186
|
+
if (!entries)
|
|
187
|
+
continue;
|
|
188
|
+
const r = dropDuplicateHooks(entries, [HOOK_SCRIPT, GATE_SCRIPT]);
|
|
189
|
+
settings.hooks[kind] = r.entries;
|
|
190
|
+
deduped += r.removed;
|
|
191
|
+
}
|
|
133
192
|
let added = 0;
|
|
134
193
|
let skipped = 0;
|
|
135
194
|
for (const kind of EVENT_KINDS) {
|
|
@@ -169,7 +228,26 @@ Run: opscontext install-autostart
|
|
|
169
228
|
}
|
|
170
229
|
console.log(`✅ Installed session gate: ${GATE_SCRIPT}`);
|
|
171
230
|
writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + "\n");
|
|
172
|
-
|
|
231
|
+
const removedNote = deduped ? `, ${deduped} duplicate registrations removed` : "";
|
|
232
|
+
console.log(`✅ ${added} hook entries added, ${skipped} already present${removedNote}.`);
|
|
233
|
+
// [LOCKED] [INSTALL-VERIFIES-BY-COUNT] - 2026-09-15
|
|
234
|
+
// [NEVER] treat the added/present counters above as proof of a correct install.
|
|
235
|
+
// WHY: on 2026-09-06 this command printed "0 already present" over three existing hooks, and
|
|
236
|
+
// the session that ran it recorded "they were not there"; the doubled audit events then
|
|
237
|
+
// went unseen for nine days.
|
|
238
|
+
// FIX: re-read settings.json from disk and require exactly one registration per event.
|
|
239
|
+
const written = readSettings();
|
|
240
|
+
const counts = {
|
|
241
|
+
...countOurHooks(written, EVENT_KINDS, HOOK_SCRIPT),
|
|
242
|
+
...countOurHooks(written, ["Stop"], GATE_SCRIPT),
|
|
243
|
+
};
|
|
244
|
+
const wrong = Object.entries(counts).filter(([, n]) => n !== 1);
|
|
245
|
+
if (wrong.length > 0) {
|
|
246
|
+
const detail = wrong.map(([ev, n]) => `${ev}=${n}`).join(", ");
|
|
247
|
+
console.error(`❌ settings.json must hold exactly one OpsContext hook per event, found ${detail}. Backup: ${backup || "none"}`);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
console.log(`✅ Verified in settings.json: exactly one registration for ${Object.keys(counts).join(", ")}.`);
|
|
173
251
|
console.log(``);
|
|
174
252
|
console.log(`Test live:`);
|
|
175
253
|
console.log(` 1. Open a NEW VS Code terminal (settings.json is read at session start).`);
|
|
@@ -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.3",
|
|
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",
|