@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
@@ -0,0 +1,32 @@
1
+ import { KnowledgeSource } from "./config.js";
2
+ /**
3
+ * A chunk of text extracted from a knowledge source,
4
+ * suitable for embedding or keyword search.
5
+ */
6
+ export interface Chunk {
7
+ /** Which source file this came from */
8
+ source: string;
9
+ /** Section heading path (e.g. "## Architecture > ### Docker") */
10
+ section: string;
11
+ /** The actual text content */
12
+ content: string;
13
+ /** Starting line number in the original file (1-based) */
14
+ lineStart: number;
15
+ /** Ending line number in the original file (1-based) */
16
+ lineEnd: number;
17
+ /** SHA-256 hash of content for deduplication */
18
+ contentHash?: string;
19
+ /** Timestamp when chunk was indexed (ISO string) */
20
+ indexedAt?: string;
21
+ /** True if chunk contains a lock marker (LOCKED / ALREADY IMPLEMENTED) — signals agents should not re-audit */
22
+ locked?: boolean;
23
+ }
24
+ /** Check if a chunk's content contains a lock marker */
25
+ export declare function hasLockMarker(text: string): boolean;
26
+ /**
27
+ * Ingest all configured knowledge sources into chunks.
28
+ * Skips files that don't exist (with a warning to stderr).
29
+ * Deduplicates chunks by content hash.
30
+ */
31
+ export declare function ingestSources(sources: KnowledgeSource[]): Chunk[];
32
+ //# sourceMappingURL=ingest.d.ts.map
package/dist/ingest.js ADDED
@@ -0,0 +1,162 @@
1
+ import { readFileSync, existsSync, statSync } from "fs";
2
+ import { basename } from "path";
3
+ import { createHash } from "crypto";
4
+ /**
5
+ * Number of overlap lines to carry over from the end of the previous chunk
6
+ * to the beginning of the next chunk. Provides context continuity at
7
+ * section boundaries (inspired by OpenClaw's 80-token overlap strategy).
8
+ */
9
+ const OVERLAP_LINES = 4;
10
+ /**
11
+ * Lock marker patterns that signal verified/audited content.
12
+ * Agents should NOT re-audit chunks containing these markers.
13
+ * Supports code comments (// LOCKED, /* LOCKED, # LOCKED),
14
+ * HTML comments (<!-- LOCKED -->), and markdown headings (## ALREADY IMPLEMENTED).
15
+ */
16
+ const LOCK_PATTERNS = [
17
+ /\/\/\s*LOCKED/i,
18
+ /\/\*\s*LOCKED/i,
19
+ /#\s*LOCKED/i,
20
+ /<!--\s*LOCKED/i,
21
+ /LOCKED\s*[—–-]\s*verified/i,
22
+ /DO\s*NOT\s*RE-?AUDIT/i,
23
+ /ALREADY\s+IMPLEMENTED/i,
24
+ /VERIFIED\s*[—–-]\s*DO\s*NOT/i,
25
+ ];
26
+ /** Check if a chunk's content contains a lock marker */
27
+ export function hasLockMarker(text) {
28
+ return LOCK_PATTERNS.some(p => p.test(text));
29
+ }
30
+ /**
31
+ * Compute SHA-256 hash of a string for content deduplication.
32
+ */
33
+ function hashContent(content) {
34
+ return createHash("sha256").update(content).digest("hex").slice(0, 16);
35
+ }
36
+ /**
37
+ * Parse a markdown file into chunks, splitting on headings.
38
+ * Each chunk captures the heading hierarchy for context.
39
+ *
40
+ * v1.10: Adds overlap lines from the end of each chunk to the start
41
+ * of the next chunk, providing context continuity at heading boundaries.
42
+ * Also computes SHA-256 content hashes for deduplication.
43
+ */
44
+ function parseMarkdown(filePath, sourceName) {
45
+ let mtime;
46
+ try {
47
+ mtime = statSync(filePath).mtime.toISOString();
48
+ }
49
+ catch {
50
+ mtime = new Date().toISOString();
51
+ }
52
+ const text = readFileSync(filePath, "utf-8");
53
+ const lines = text.split("\n");
54
+ const rawChunks = [];
55
+ // Track heading hierarchy
56
+ const headingStack = [];
57
+ let currentContent = [];
58
+ let chunkStartLine = 1;
59
+ for (let i = 0; i < lines.length; i++) {
60
+ const line = lines[i];
61
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)/);
62
+ if (headingMatch) {
63
+ // Flush previous chunk
64
+ if (currentContent.length > 0) {
65
+ const content = currentContent.join("\n").trim();
66
+ if (content.length > 0) {
67
+ rawChunks.push({
68
+ section: headingStack.join(" > ") || basename(filePath),
69
+ contentLines: [...currentContent],
70
+ startLine: chunkStartLine,
71
+ endLine: i,
72
+ });
73
+ }
74
+ }
75
+ const level = headingMatch[1].length;
76
+ const title = headingMatch[2].trim();
77
+ // Pop headings at same or deeper level
78
+ while (headingStack.length >= level) {
79
+ headingStack.pop();
80
+ }
81
+ headingStack.push(`${"#".repeat(level)} ${title}`);
82
+ currentContent = [];
83
+ chunkStartLine = i + 1; // 1-based
84
+ }
85
+ else {
86
+ currentContent.push(line);
87
+ }
88
+ }
89
+ // Flush last chunk
90
+ if (currentContent.length > 0) {
91
+ const content = currentContent.join("\n").trim();
92
+ if (content.length > 0) {
93
+ rawChunks.push({
94
+ section: headingStack.join(" > ") || basename(filePath),
95
+ contentLines: [...currentContent],
96
+ startLine: chunkStartLine,
97
+ endLine: lines.length,
98
+ });
99
+ }
100
+ }
101
+ // Build final chunks with overlap
102
+ const chunks = [];
103
+ for (let i = 0; i < rawChunks.length; i++) {
104
+ const raw = rawChunks[i];
105
+ let finalLines = raw.contentLines;
106
+ // Add overlap from previous chunk's tail (if not the first chunk)
107
+ if (i > 0 && OVERLAP_LINES > 0) {
108
+ const prevLines = rawChunks[i - 1].contentLines;
109
+ const overlapCount = Math.min(OVERLAP_LINES, prevLines.length);
110
+ const overlap = prevLines.slice(-overlapCount);
111
+ finalLines = [...overlap, "---", ...raw.contentLines];
112
+ }
113
+ const content = finalLines.join("\n").trim();
114
+ if (content.length > 0) {
115
+ const locked = hasLockMarker(content) || hasLockMarker(raw.section);
116
+ chunks.push({
117
+ source: sourceName,
118
+ section: raw.section,
119
+ content,
120
+ lineStart: raw.startLine,
121
+ lineEnd: raw.endLine,
122
+ contentHash: hashContent(content),
123
+ indexedAt: mtime,
124
+ ...(locked && { locked: true }),
125
+ });
126
+ }
127
+ }
128
+ return chunks;
129
+ }
130
+ /**
131
+ * Ingest all configured knowledge sources into chunks.
132
+ * Skips files that don't exist (with a warning to stderr).
133
+ * Deduplicates chunks by content hash.
134
+ */
135
+ export function ingestSources(sources) {
136
+ const allChunks = [];
137
+ const seenHashes = new Set();
138
+ let dupCount = 0;
139
+ for (const source of sources) {
140
+ if (!existsSync(source.path)) {
141
+ console.error(`[ContextEngine] ⚠ Skipping missing: ${source.path}`);
142
+ continue;
143
+ }
144
+ const chunks = parseMarkdown(source.path, source.name);
145
+ for (const chunk of chunks) {
146
+ if (chunk.contentHash && seenHashes.has(chunk.contentHash)) {
147
+ dupCount++;
148
+ continue;
149
+ }
150
+ if (chunk.contentHash)
151
+ seenHashes.add(chunk.contentHash);
152
+ allChunks.push(chunk);
153
+ }
154
+ console.error(`[ContextEngine] ✅ Indexed: ${source.name} (${chunks.length} chunks)`);
155
+ }
156
+ if (dupCount > 0) {
157
+ console.error(`[ContextEngine] 🔁 Deduplicated: ${dupCount} duplicate chunks removed`);
158
+ }
159
+ console.error(`[ContextEngine] 📦 Total: ${allChunks.length} chunks from ${sources.length} sources`);
160
+ return allChunks;
161
+ }
162
+ //# sourceMappingURL=ingest.js.map
@@ -0,0 +1,108 @@
1
+ import { Chunk } from "./ingest.js";
2
+ export interface Learning {
3
+ id: string;
4
+ category: string;
5
+ rule: string;
6
+ context: string;
7
+ project?: string;
8
+ tags: string[];
9
+ created: string;
10
+ updated: string;
11
+ }
12
+ export interface LearningsStore {
13
+ version: number;
14
+ count: number;
15
+ learnings: Learning[];
16
+ }
17
+ /** Valid categories for learnings */
18
+ export declare const LEARNING_CATEGORIES: readonly ["deployment", "api", "database", "frontend", "backend", "devops", "security", "performance", "testing", "debugging", "tooling", "git", "dependencies", "architecture", "data", "infrastructure", "mobile", "other"];
19
+ export type LearningCategory = (typeof LEARNING_CATEGORIES)[number];
20
+ /**
21
+ * Save a new learning. Returns the created learning with ID.
22
+ * Rejects rules shorter than MIN_RULE_LENGTH and auto-corrects "other" category.
23
+ */
24
+ export declare function saveLearning(category: string, rule: string, context: string, project?: string): Learning;
25
+ /**
26
+ * Search learnings by keyword. Returns matches sorted by relevance.
27
+ */
28
+ export declare function searchLearnings(query: string): Learning[];
29
+ /**
30
+ * Get learnings, optionally filtered by category and/or project.
31
+ *
32
+ * When `projects` is provided, only returns learnings that:
33
+ * - match one of the given project names (case-insensitive), OR
34
+ * - have no project set (universal learnings)
35
+ *
36
+ * This prevents cross-project IP leakage — e.g. CROWLR learnings
37
+ * won't appear when working on VOILA.
38
+ */
39
+ export declare function listLearnings(category?: string, projects?: string[]): Learning[];
40
+ /**
41
+ * Delete a learning by ID.
42
+ */
43
+ export declare function deleteLearning(id: string): boolean;
44
+ /**
45
+ * Import learnings from a Markdown file.
46
+ * Parses headings and bullet points to extract rules.
47
+ *
48
+ * Supported formats:
49
+ * 1. **Structured Markdown** — H2 = category, H3 = rule, bullet = context
50
+ * ```md
51
+ * ## deployment
52
+ * ### Never docker build | tee
53
+ * - Pipeline signals kill builds. Use nohup > /tmp/log 2>&1 &
54
+ * ```
55
+ *
56
+ * 2. **Bullet-list Markdown** — Each bullet with "→" or "—" separator
57
+ * ```md
58
+ * - [deployment] Never docker build | tee → Pipeline signals kill builds
59
+ * ```
60
+ *
61
+ * 3. **JSON array** — Direct Learning[] import
62
+ */
63
+ export interface ImportResult {
64
+ imported: number;
65
+ updated: number;
66
+ skipped: number;
67
+ errors: string[];
68
+ }
69
+ export declare function importLearningsFromFile(filePath: string, defaultCategory?: string, defaultProject?: string): ImportResult;
70
+ /**
71
+ * Convert learnings to Chunks so they can be included in search_context.
72
+ * This is the key integration — learnings auto-surface in hybrid search.
73
+ *
74
+ * When `projects` is provided, only includes learnings for those projects
75
+ * (+ universal learnings with no project). This prevents cross-project
76
+ * IP leakage — CROWLR secrets won't appear when searching in VOILA.
77
+ */
78
+ export declare function learningsToChunks(projects?: string[]): Chunk[];
79
+ /**
80
+ * Auto-import learnings from discovered knowledge source files.
81
+ *
82
+ * Called during reindex — scans all source markdown files and extracts
83
+ * rules into the permanent learning store. Deduplication is built-in,
84
+ * so calling repeatedly on the same files is safe (no duplicates).
85
+ *
86
+ * This ensures documentation rules become searchable learnings without
87
+ * requiring the user or agent to manually trigger `import_learnings`.
88
+ */
89
+ export declare function autoImportFromSources(sources: Array<{
90
+ path: string;
91
+ name: string;
92
+ }>): {
93
+ total: number;
94
+ imported: number;
95
+ updated: number;
96
+ };
97
+ /**
98
+ * Get the store stats.
99
+ */
100
+ export declare function learningsStats(): {
101
+ total: number;
102
+ categories: Record<string, number>;
103
+ };
104
+ /**
105
+ * Format learnings for display.
106
+ */
107
+ export declare function formatLearnings(learnings: Learning[]): string;
108
+ //# sourceMappingURL=learnings.d.ts.map