@compr/opscontext-mcp 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/LICENSE +83 -0
  3. package/README.md +470 -0
  4. package/defaults/learnings.json +146 -0
  5. package/dist/activation.d.ts +48 -0
  6. package/dist/activation.js +377 -0
  7. package/dist/adapters.d.ts +101 -0
  8. package/dist/adapters.js +171 -0
  9. package/dist/agents.d.ts +137 -0
  10. package/dist/agents.js +1638 -0
  11. package/dist/audit.d.ts +23 -0
  12. package/dist/audit.js +163 -0
  13. package/dist/cache.d.ts +15 -0
  14. package/dist/cache.js +117 -0
  15. package/dist/claude-integration.d.ts +95 -0
  16. package/dist/claude-integration.js +247 -0
  17. package/dist/cli.d.ts +18 -0
  18. package/dist/cli.js +1823 -0
  19. package/dist/code-chunker.d.ts +12 -0
  20. package/dist/code-chunker.js +270 -0
  21. package/dist/collectors.d.ts +63 -0
  22. package/dist/collectors.js +617 -0
  23. package/dist/config.d.ts +73 -0
  24. package/dist/config.js +239 -0
  25. package/dist/embeddings.d.ts +36 -0
  26. package/dist/embeddings.js +124 -0
  27. package/dist/firewall.d.ts +133 -0
  28. package/dist/firewall.js +631 -0
  29. package/dist/hooks.d.ts +76 -0
  30. package/dist/hooks.js +313 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.js +1081 -0
  33. package/dist/ingest.d.ts +32 -0
  34. package/dist/ingest.js +162 -0
  35. package/dist/learnings.d.ts +108 -0
  36. package/dist/learnings.js +714 -0
  37. package/dist/license-sig.d.ts +47 -0
  38. package/dist/license-sig.js +104 -0
  39. package/dist/policy.d.ts +131 -0
  40. package/dist/policy.js +182 -0
  41. package/dist/search.d.ts +11 -0
  42. package/dist/search.js +99 -0
  43. package/dist/sessions.d.ts +46 -0
  44. package/dist/sessions.js +153 -0
  45. package/examples/adapters/notion-adapter.js +108 -0
  46. package/examples/adapters/rss-adapter.js +76 -0
  47. package/package.json +87 -0
  48. package/skills/opscontext/SKILL.md +260 -0
package/dist/config.js ADDED
@@ -0,0 +1,239 @@
1
+ import { resolve, join } from "path";
2
+ import { homedir } from "os";
3
+ import { readFileSync, existsSync, readdirSync, statSync } from "fs";
4
+ import { discoverClaudeMemory } from "./claude-integration.js";
5
+ const DEFAULT_PATTERNS = [
6
+ // GitHub Copilot
7
+ ".github/copilot-instructions.md",
8
+ ".github/instructions/copilot-instructions.md",
9
+ ".github/SKILLS.md",
10
+ // Claude Code
11
+ "CLAUDE.md",
12
+ // Cursor
13
+ ".cursorrules",
14
+ ".cursor/rules",
15
+ // Codex / multi-agent
16
+ "AGENTS.md",
17
+ // Context engineering
18
+ "CONTEXT_MAP.md",
19
+ ];
20
+ /**
21
+ * Look for contextengine.json in standard locations.
22
+ * Priority: env var > CWD > home dir
23
+ */
24
+ function findConfigFile() {
25
+ const candidates = [];
26
+ const envPath = process.env.CONTEXTENGINE_CONFIG;
27
+ if (envPath) {
28
+ candidates.push(resolve(envPath));
29
+ }
30
+ candidates.push(resolve(process.cwd(), "contextengine.json"), resolve(homedir(), ".contextengine.json"));
31
+ for (const c of candidates) {
32
+ if (existsSync(c))
33
+ return c;
34
+ }
35
+ return null;
36
+ }
37
+ /**
38
+ * Auto-discover knowledge files by scanning directories for known patterns.
39
+ * Scans one level deep (each subdirectory = a project).
40
+ */
41
+ function discoverSources(dirs, patterns) {
42
+ const sources = [];
43
+ for (const dir of dirs) {
44
+ const absDir = resolve(dir.replace(/^~/, homedir()));
45
+ if (!existsSync(absDir))
46
+ continue;
47
+ // Check patterns at this level
48
+ for (const pattern of patterns) {
49
+ const filePath = join(absDir, pattern);
50
+ if (existsSync(filePath)) {
51
+ const dirName = absDir.split("/").pop() || absDir;
52
+ const fileName = pattern.split("/").pop() || pattern;
53
+ sources.push({
54
+ name: `${dirName} — ${fileName}`,
55
+ path: filePath,
56
+ type: "markdown",
57
+ });
58
+ }
59
+ }
60
+ // Scan one level deep (subdirectories = projects)
61
+ try {
62
+ for (const entry of readdirSync(absDir)) {
63
+ if (entry.startsWith(".") || entry === "node_modules")
64
+ continue;
65
+ const subDir = join(absDir, entry);
66
+ try {
67
+ if (!statSync(subDir).isDirectory())
68
+ continue;
69
+ }
70
+ catch {
71
+ continue;
72
+ }
73
+ for (const pattern of patterns) {
74
+ const filePath = join(subDir, pattern);
75
+ if (existsSync(filePath)) {
76
+ const fileName = pattern.split("/").pop() || pattern;
77
+ sources.push({
78
+ name: `${entry} — ${fileName}`,
79
+ path: filePath,
80
+ type: "markdown",
81
+ });
82
+ }
83
+ }
84
+ }
85
+ }
86
+ catch {
87
+ // Permission denied — skip
88
+ }
89
+ }
90
+ return sources;
91
+ }
92
+ /**
93
+ * Load knowledge sources.
94
+ *
95
+ * Resolution order:
96
+ * 1. Config file (CONTEXTENGINE_CONFIG env, ./contextengine.json, ~/.contextengine.json)
97
+ * 2. CONTEXTENGINE_WORKSPACES env var (colon-separated paths)
98
+ * 3. Auto-discover from ~/Projects
99
+ *
100
+ * Claude Code auto-memory (~/.claude/projects/<slug>/memory/*.md) is added
101
+ * to every resolution path unless OPSCONTEXT_SKIP_CLAUDE_MEMORY=1.
102
+ */
103
+ export function loadSources() {
104
+ const configPath = findConfigFile();
105
+ if (configPath) {
106
+ console.error(`[ContextEngine] 📄 Config: ${configPath}`);
107
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
108
+ const sources = [];
109
+ // Explicit sources
110
+ if (config.sources) {
111
+ for (const s of config.sources) {
112
+ const absPath = resolve(configPath, "..", s.path.replace(/^~/, homedir()));
113
+ if (existsSync(absPath)) {
114
+ sources.push({ name: s.name, path: absPath, type: "markdown" });
115
+ }
116
+ else {
117
+ console.error(`[ContextEngine] ⚠ Not found: ${absPath}`);
118
+ }
119
+ }
120
+ }
121
+ // Workspace auto-discovery
122
+ if (config.workspaces) {
123
+ const patterns = config.patterns || DEFAULT_PATTERNS;
124
+ const resolved = config.workspaces.map((w) => resolve(configPath, "..", w.replace(/^~/, homedir())));
125
+ sources.push(...discoverSources(resolved, patterns));
126
+ }
127
+ sources.push(...claudeMemorySources());
128
+ return dedup(sources);
129
+ }
130
+ // Env var fallback
131
+ const envWorkspaces = process.env.CONTEXTENGINE_WORKSPACES;
132
+ if (envWorkspaces) {
133
+ console.error(`[ContextEngine] 🔍 Discovering from CONTEXTENGINE_WORKSPACES`);
134
+ const dirs = envWorkspaces.split(":").filter(Boolean);
135
+ const found = discoverSources(dirs, DEFAULT_PATTERNS);
136
+ return dedup([...found, ...claudeMemorySources()]);
137
+ }
138
+ // Auto-discover from ~/Projects
139
+ const projectsDir = resolve(homedir(), "Projects");
140
+ if (existsSync(projectsDir)) {
141
+ console.error(`[ContextEngine] 🔍 Auto-discovering from ~/Projects`);
142
+ const found = discoverSources([projectsDir], DEFAULT_PATTERNS);
143
+ return dedup([...found, ...claudeMemorySources()]);
144
+ }
145
+ console.error(`[ContextEngine] ⚠ No sources found. Create contextengine.json or set CONTEXTENGINE_WORKSPACES.`);
146
+ // Claude memory may still exist even without project workspaces — surface it.
147
+ return claudeMemorySources();
148
+ }
149
+ /**
150
+ * Pull Claude Code auto-memory into the source list (read-only). Skips when
151
+ * OPSCONTEXT_SKIP_CLAUDE_MEMORY=1 (escape hatch for tests + air-gapped runs
152
+ * where ~/.claude/ contents may not be indexable).
153
+ */
154
+ function claudeMemorySources() {
155
+ if (process.env.OPSCONTEXT_SKIP_CLAUDE_MEMORY === "1")
156
+ return [];
157
+ try {
158
+ return discoverClaudeMemory();
159
+ }
160
+ catch {
161
+ return [];
162
+ }
163
+ }
164
+ /** Remove duplicate paths */
165
+ function dedup(sources) {
166
+ const seen = new Set();
167
+ return sources.filter((s) => {
168
+ if (seen.has(s.path))
169
+ return false;
170
+ seen.add(s.path);
171
+ return true;
172
+ });
173
+ }
174
+ /**
175
+ * Discover project directories from workspaces.
176
+ * Returns one entry per top-level project found.
177
+ */
178
+ export function loadProjectDirs() {
179
+ const configPath = findConfigFile();
180
+ const dirs = [];
181
+ let workspaceDirs = [];
182
+ if (configPath) {
183
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
184
+ if (config.collectOps === false)
185
+ return []; // opted out
186
+ if (config.workspaces) {
187
+ workspaceDirs = config.workspaces.map((w) => resolve(configPath, "..", w.replace(/^~/, homedir())));
188
+ }
189
+ }
190
+ // Env var fallback
191
+ if (workspaceDirs.length === 0) {
192
+ const envWorkspaces = process.env.CONTEXTENGINE_WORKSPACES;
193
+ if (envWorkspaces) {
194
+ workspaceDirs = envWorkspaces.split(":").filter(Boolean);
195
+ }
196
+ }
197
+ // Auto-discover fallback
198
+ if (workspaceDirs.length === 0) {
199
+ const projectsDir = resolve(homedir(), "Projects");
200
+ if (existsSync(projectsDir)) {
201
+ workspaceDirs = [projectsDir];
202
+ }
203
+ }
204
+ for (const wsDir of workspaceDirs) {
205
+ const absDir = resolve(wsDir.replace(/^~/, homedir()));
206
+ if (!existsSync(absDir))
207
+ continue;
208
+ try {
209
+ for (const entry of readdirSync(absDir)) {
210
+ if (entry.startsWith(".") || entry === "node_modules")
211
+ continue;
212
+ const subDir = join(absDir, entry);
213
+ try {
214
+ if (!statSync(subDir).isDirectory())
215
+ continue;
216
+ }
217
+ catch {
218
+ continue;
219
+ }
220
+ dirs.push({ name: entry, path: subDir });
221
+ }
222
+ }
223
+ catch {
224
+ // Permission denied — skip
225
+ }
226
+ }
227
+ return dirs;
228
+ }
229
+ /**
230
+ * Load the raw config (for checking flags like collectSystemOps).
231
+ */
232
+ export function loadConfig() {
233
+ const configPath = findConfigFile();
234
+ if (configPath) {
235
+ return JSON.parse(readFileSync(configPath, "utf-8"));
236
+ }
237
+ return {};
238
+ }
239
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,36 @@
1
+ import type { Chunk } from "./ingest.js";
2
+ /**
3
+ * Initialize the embedding pipeline (downloads model on first run, ~22MB).
4
+ * Subsequent calls use cached model.
5
+ *
6
+ * @huggingface/transformers is an optional dependency (~250MB transitive,
7
+ * native onnxruntime binaries). If it's absent (npm install --no-optional,
8
+ * air-gapped network at install time, or locked-down corp proxy), we silently
9
+ * fall back to BM25 keyword search — `search_context` still works.
10
+ */
11
+ export declare function initEmbeddings(): Promise<boolean>;
12
+ /**
13
+ * An embedded chunk with its vector.
14
+ */
15
+ export interface EmbeddedChunk {
16
+ chunk: Chunk;
17
+ vector: Float32Array;
18
+ }
19
+ /**
20
+ * Embed all chunks. Returns the chunks with their vectors.
21
+ * Shows progress on stderr.
22
+ */
23
+ export declare function embedChunks(chunks: Chunk[]): Promise<EmbeddedChunk[]>;
24
+ export interface VectorSearchResult {
25
+ chunk: Chunk;
26
+ score: number;
27
+ }
28
+ /**
29
+ * Semantic search: embed the query, then find most similar chunks.
30
+ */
31
+ export declare function vectorSearch(query: string, embeddedChunks: EmbeddedChunk[], topK?: number): Promise<VectorSearchResult[]>;
32
+ /**
33
+ * Check if embeddings are available.
34
+ */
35
+ export declare function isEmbeddingsReady(): boolean;
36
+ //# sourceMappingURL=embeddings.d.ts.map
@@ -0,0 +1,124 @@
1
+ // LOCKED — verified March 3 2026 — Xenova all-MiniLM-L6-v2 local CPU embeddings + disk cache
2
+ // DO NOT RE-AUDIT — stable since v1.0, no API keys, no data leaves machine
3
+ //
4
+ // 🔒 LOCKED [OPTIONAL-HF] — 2026-06-10
5
+ // ⛔ NEVER convert `await import("@huggingface/transformers")` to a static
6
+ // import. HF is in optionalDependencies — a static import breaks installs
7
+ // that ran with --omit=optional (locked-down npm proxies, air-gapped CI).
8
+ // ⛔ NEVER remove the try/catch around the dynamic import or the
9
+ // isMissingDep detection branch — both keep the MCP server alive on
10
+ // fresh installs where HF didn't download.
11
+ // WHY: The package was failing to install on enterprise environments because
12
+ // HF transitively pulls 427 MB of onnxruntime native binaries. Making
13
+ // HF optional dropped cold install from 547 MB to 120 MB and unblocked
14
+ // a whole class of buyers. A static import would silently re-break this.
15
+ // FIX: If you need to take a dependency on a transformer feature, add it
16
+ // behind the same dynamic-import + isEmbeddingsReady() check pattern.
17
+ // We dynamically import @huggingface/transformers to keep startup fast
18
+ // and handle the case where it fails gracefully.
19
+ let embedPipeline = null;
20
+ const MODEL_NAME = "Xenova/all-MiniLM-L6-v2";
21
+ /**
22
+ * Initialize the embedding pipeline (downloads model on first run, ~22MB).
23
+ * Subsequent calls use cached model.
24
+ *
25
+ * @huggingface/transformers is an optional dependency (~250MB transitive,
26
+ * native onnxruntime binaries). If it's absent (npm install --no-optional,
27
+ * air-gapped network at install time, or locked-down corp proxy), we silently
28
+ * fall back to BM25 keyword search — `search_context` still works.
29
+ */
30
+ export async function initEmbeddings() {
31
+ try {
32
+ console.error(`[ContextEngine] 🧠 Loading embedding model: ${MODEL_NAME}...`);
33
+ const { pipeline } = await import("@huggingface/transformers");
34
+ embedPipeline = await pipeline("feature-extraction", MODEL_NAME, {
35
+ dtype: "fp32",
36
+ });
37
+ console.error(`[ContextEngine] ✅ Embedding model loaded`);
38
+ return true;
39
+ }
40
+ catch (err) {
41
+ const msg = err.message;
42
+ const isMissingDep = msg.includes("Cannot find package") ||
43
+ msg.includes("ERR_MODULE_NOT_FOUND") ||
44
+ msg.includes("@huggingface/transformers");
45
+ if (isMissingDep) {
46
+ console.error(`[ContextEngine] ⚠ Semantic search disabled — @huggingface/transformers not installed.\n` +
47
+ ` BM25 keyword search still works. To enable semantic search:\n` +
48
+ ` npm install @huggingface/transformers\n` +
49
+ ` (~250MB download; CPU-only embeddings via Xenova/all-MiniLM-L6-v2, no data leaves the machine).`);
50
+ }
51
+ else {
52
+ console.error(`[ContextEngine] ⚠ Embeddings unavailable (keyword search only):`, msg);
53
+ }
54
+ return false;
55
+ }
56
+ }
57
+ /**
58
+ * Embed a single text string → float32 vector (384 dimensions).
59
+ */
60
+ async function embedText(text) {
61
+ const output = await embedPipeline(text, {
62
+ pooling: "mean",
63
+ normalize: true,
64
+ });
65
+ // output.data is a flat typed array
66
+ return new Float32Array(output.data);
67
+ }
68
+ /**
69
+ * Cosine similarity between two vectors.
70
+ * Both must be normalized (which MiniLM + normalize:true guarantees),
71
+ * so dot product = cosine similarity.
72
+ */
73
+ function cosineSimilarity(a, b) {
74
+ let dot = 0;
75
+ for (let i = 0; i < a.length; i++) {
76
+ dot += a[i] * b[i];
77
+ }
78
+ return dot;
79
+ }
80
+ /**
81
+ * Embed all chunks. Returns the chunks with their vectors.
82
+ * Shows progress on stderr.
83
+ */
84
+ export async function embedChunks(chunks) {
85
+ const results = [];
86
+ const total = chunks.length;
87
+ const batchSize = 10;
88
+ for (let i = 0; i < total; i += batchSize) {
89
+ const batch = chunks.slice(i, i + batchSize);
90
+ const batchResults = await Promise.all(batch.map(async (chunk) => {
91
+ // Embed section + content together for context
92
+ const text = `${chunk.section}\n${chunk.content}`.slice(0, 512);
93
+ const vector = await embedText(text);
94
+ return { chunk, vector };
95
+ }));
96
+ results.push(...batchResults);
97
+ const done = Math.min(i + batchSize, total);
98
+ if (done % 50 === 0 || done === total) {
99
+ console.error(`[ContextEngine] 📊 Embedded ${done}/${total} chunks`);
100
+ }
101
+ }
102
+ return results;
103
+ }
104
+ /**
105
+ * Semantic search: embed the query, then find most similar chunks.
106
+ */
107
+ export async function vectorSearch(query, embeddedChunks, topK = 10) {
108
+ if (!embedPipeline || embeddedChunks.length === 0)
109
+ return [];
110
+ const queryVector = await embedText(query);
111
+ const scored = embeddedChunks.map((ec) => ({
112
+ chunk: ec.chunk,
113
+ score: cosineSimilarity(queryVector, ec.vector),
114
+ }));
115
+ scored.sort((a, b) => b.score - a.score);
116
+ return scored.slice(0, topK);
117
+ }
118
+ /**
119
+ * Check if embeddings are available.
120
+ */
121
+ export function isEmbeddingsReady() {
122
+ return embedPipeline !== null;
123
+ }
124
+ //# sourceMappingURL=embeddings.js.map
@@ -0,0 +1,133 @@
1
+ export interface Obligation {
2
+ id: string;
3
+ label: string;
4
+ status: "ok" | "warn" | "fail";
5
+ detail: string;
6
+ }
7
+ /**
8
+ * Callback type for searching learnings — avoids circular import.
9
+ * Returns top matches with rule text + optional project scope.
10
+ */
11
+ export type LearningSearchFn = (query: string, projects?: string[]) => Array<{
12
+ rule: string;
13
+ project?: string;
14
+ category: string;
15
+ }>;
16
+ export declare class ProtocolFirewall {
17
+ private toolCalls;
18
+ private learningsSaved;
19
+ private sessionSaved;
20
+ private readonly startTime;
21
+ private nudgesIssued;
22
+ private searchRecalls;
23
+ private truncations;
24
+ private lastNonExemptCall;
25
+ private round;
26
+ private roundAtLastSave;
27
+ private roundsSinceSessionSave;
28
+ private lastSessionSaveTime;
29
+ private statsFlushTimer;
30
+ private static readonly STATS_FLUSH_MS;
31
+ private static readonly STATS_FILE;
32
+ private learningSearchFn;
33
+ private activeProjects;
34
+ private injectionCache;
35
+ private injectionCacheRound;
36
+ private learningsInjected;
37
+ private gitCache;
38
+ private docCache;
39
+ private projectDirs;
40
+ constructor(opts?: {
41
+ skipRestore?: boolean;
42
+ });
43
+ /**
44
+ * Resume enforcement from a prior session if it crashed recently.
45
+ * Reads session-stats.json and restores round counters so a crashed
46
+ * window doesn't reset enforcement back to silent.
47
+ */
48
+ private loadPriorState;
49
+ /**
50
+ * Update project directories (call during reindex).
51
+ */
52
+ setProjectDirs(dirs: Array<{
53
+ path: string;
54
+ name: string;
55
+ }>): void;
56
+ /**
57
+ * Register the learning search function.
58
+ * Call once at startup to enable auto-injection without circular imports.
59
+ */
60
+ setLearningSearchFn(fn: LearningSearchFn): void;
61
+ /**
62
+ * Wrap a tool response with protocol status + learning injection.
63
+ * This is the ONLY public API. Call on every tool response.
64
+ *
65
+ * - Exempt tools (save_learning, etc.) pass through unmodified
66
+ * - Silent phase (first 10 calls or 0 obligations): no change
67
+ * - Footer/Header: status block appended/prepended
68
+ * - Degraded: response TRUNCATED + status block
69
+ *
70
+ * @param toolName MCP tool name
71
+ * @param responseText Original tool response text
72
+ * @param contextHint Optional query/args string for learning injection
73
+ */
74
+ wrap(toolName: string, responseText: string, contextHint?: string): string;
75
+ /**
76
+ * Get current state for diagnostics / testing.
77
+ */
78
+ getState(): {
79
+ toolCalls: number;
80
+ learningsSaved: number;
81
+ sessionSaved: boolean;
82
+ uptimeMinutes: number;
83
+ nudgesIssued: number;
84
+ searchRecalls: number;
85
+ truncations: number;
86
+ timeSavedMinutes: number;
87
+ round: number;
88
+ roundsSinceSessionSave: number;
89
+ learningsInjected: number;
90
+ sessionOverdue: boolean;
91
+ };
92
+ /**
93
+ * Record that N learnings were surfaced in a search result.
94
+ * Call from search_context handler after counting learning-sourced results.
95
+ */
96
+ recordSearchRecalls(count: number): void;
97
+ /**
98
+ * Search and format relevant learnings for injection into tool response.
99
+ * Returns null if no relevant learnings or no search function registered.
100
+ * Results are cached per round to avoid repeated searches for same hint.
101
+ */
102
+ private buildLearningInjection;
103
+ /**
104
+ * Estimate minutes saved by ContextEngine this session.
105
+ * Only counts genuine value events — not overhead like nudges or auto-injection.
106
+ * - Each explicit search recall ≈ 2 min (avoids re-discovery / googling)
107
+ * - Each auto-injected learning ≈ 1 min (proactive context, less than explicit)
108
+ * - Each learning saved ≈ 1 min (future sessions benefit)
109
+ * - Session save ≈ 3 min (avoids cold-start next session)
110
+ * Note: nudges removed (they're enforcement overhead, not time saved).
111
+ */
112
+ private estimateTimeSaved;
113
+ private scheduleStatsFlush;
114
+ private flushStats;
115
+ private recordCompliance;
116
+ /**
117
+ * Check if session save is overdue (>10 minutes since last save,
118
+ * or >10 minutes of activity without ever saving).
119
+ * Only triggers after the warmup period (first 10 minutes of session).
120
+ */
121
+ private isSessionOverdue;
122
+ /**
123
+ * Build an urgent session reminder block.
124
+ * This is injected at the TOP of every tool response when overdue.
125
+ */
126
+ private buildSessionUrgentBlock;
127
+ private evaluate;
128
+ private checkGit;
129
+ private checkDocs;
130
+ private computeLevel;
131
+ private formatBlock;
132
+ }
133
+ //# sourceMappingURL=firewall.d.ts.map