@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,76 @@
1
+ import type { Policy } from "./policy.js";
2
+ /**
3
+ * Convert a glob with `*`, `**`, `?` into an anchored RegExp.
4
+ * Supports the subset of globs that policy.json paths use in practice:
5
+ * - `**\/` recursively matches directories
6
+ * - `*` matches any char except `/`
7
+ * - `?` matches a single char except `/`
8
+ * - Literal path separators and dots
9
+ */
10
+ export declare function globToRegExp(glob: string): RegExp;
11
+ export declare function matchesAnyGlob(path: string, globs: string[]): boolean;
12
+ export interface StagedFile {
13
+ path: string;
14
+ /** Added lines only (the `+` lines from --unified=0, with the leading `+` stripped) */
15
+ addedLines: Array<{
16
+ lineNumber: number;
17
+ content: string;
18
+ }>;
19
+ }
20
+ /**
21
+ * Read the staged diff as a structured list. Uses git CLI directly — no
22
+ * surprises about index state, no library to keep in sync with git.
23
+ *
24
+ * Errors from git (not a repo, no staged changes, etc.) are RE-THROWN, not
25
+ * swallowed. The caller decides whether "no staged changes" is fatal.
26
+ */
27
+ export declare function getStagedFiles(repoRoot: string): StagedFile[];
28
+ export interface SecretViolation {
29
+ patternId: string;
30
+ severity: "block" | "warn";
31
+ file: string;
32
+ lineNumber: number;
33
+ /** Pattern source for audit attribution. Never the matched value. */
34
+ patternSource: string;
35
+ }
36
+ /**
37
+ * Apply policy.secret_patterns to a list of staged files. Returns one
38
+ * violation per matched line. Honors the `paths` glob scoping per pattern.
39
+ *
40
+ * IMPORTANT: We never return the matched secret value. Only the location
41
+ * + pattern id. This is the redaction contract.
42
+ */
43
+ export declare function runSecretScan(policy: Policy, files: StagedFile[]): SecretViolation[];
44
+ export interface DocCoverageViolation {
45
+ severity: "block" | "warn";
46
+ sourcePaths: string[];
47
+ matchedFiles: string[];
48
+ requiresSection: string;
49
+ reason: "doc-section-not-found" | "doc-not-staged-and-section-unchanged";
50
+ }
51
+ /**
52
+ * For each doc_coverage rule, check:
53
+ * - did this commit touch any of the rule's source paths?
54
+ * - if yes, is the required doc-section file either (a) also in the staged
55
+ * diff, or (b) present at the expected path?
56
+ *
57
+ * "Section unchanged" detection requires reading the doc; we treat presence
58
+ * of the file as the floor (a real next-iteration improvement: hash the
59
+ * section under the anchor and require staged change to the anchor section
60
+ * when triggered. For now, presence + warn=non-staged is the contract).
61
+ */
62
+ export declare function runDocCoverage(policy: Policy, files: StagedFile[], repoRoot: string): DocCoverageViolation[];
63
+ /**
64
+ * Compute a stable SHA-256 of the section content under a markdown anchor.
65
+ * "Section" = lines from `## Anchor` (or `### Anchor` etc.) up to the next
66
+ * heading at the same or higher level. Used by the next-iteration v2 check.
67
+ *
68
+ * Exposed now so the CLI can surface a stable hash for compliance evidence
69
+ * (e.g., "as of this commit, the firewall section is hash X").
70
+ */
71
+ export declare function hashDocSection(filePath: string, anchor: string): string | null;
72
+ export declare function formatSecretViolations(violations: SecretViolation[]): string;
73
+ export declare function formatDocCoverageViolations(violations: DocCoverageViolation[]): string;
74
+ export declare function formatSecretViolationsJson(violations: SecretViolation[]): string;
75
+ export declare function formatDocCoverageViolationsJson(violations: DocCoverageViolation[]): string;
76
+ //# sourceMappingURL=hooks.d.ts.map
package/dist/hooks.js ADDED
@@ -0,0 +1,313 @@
1
+ // 🔒 LOCKED [HOOK-CHECKERS] — 2026-06-10
2
+ // ⛔ NEVER print the matched secret value in violation output. Print pattern
3
+ // id + file + line + redaction only. Leaking secrets via "helpful" error
4
+ // messages was a classic regression in v1.x of similar tools.
5
+ // ⛔ NEVER swallow git errors silently — if git isn't available, surface the
6
+ // failure so the user knows the gate is not actually running.
7
+ // WHY: This is the production enforcement path. A check that silently
8
+ // passes when broken is worse than no check — it ships false
9
+ // compliance evidence. Loud failure ≫ silent skip.
10
+ // FIX: To add a new gate, add a runXxx() function here, expose via the
11
+ // `hook` CLI subcommand, never widen the secret-redaction contract.
12
+ //
13
+ // Hook checkers — TypeScript implementations of the policy gates that the
14
+ // pre-commit hook (or any other PreToolUse / CI surface) invokes.
15
+ //
16
+ // Inputs are fed by helpers that read the actual git working state.
17
+ // Each runner returns a list of violations + a summary; the CLI maps that
18
+ // onto exit codes + audit events + human/machine output.
19
+ import { execSync } from "child_process";
20
+ import { existsSync, readFileSync } from "fs";
21
+ import { createHash } from "crypto";
22
+ import { join } from "path";
23
+ // ---------------------------------------------------------------------------
24
+ // Glob matching (tiny, no dep)
25
+ // ---------------------------------------------------------------------------
26
+ /**
27
+ * Convert a glob with `*`, `**`, `?` into an anchored RegExp.
28
+ * Supports the subset of globs that policy.json paths use in practice:
29
+ * - `**\/` recursively matches directories
30
+ * - `*` matches any char except `/`
31
+ * - `?` matches a single char except `/`
32
+ * - Literal path separators and dots
33
+ */
34
+ export function globToRegExp(glob) {
35
+ let re = "^";
36
+ let i = 0;
37
+ while (i < glob.length) {
38
+ const c = glob[i];
39
+ if (c === "*") {
40
+ if (glob[i + 1] === "*") {
41
+ // `**` — match across directory separators (including zero dirs)
42
+ // `**/` consumes the trailing slash if present
43
+ if (glob[i + 2] === "/") {
44
+ re += "(?:.*/)?";
45
+ i += 3;
46
+ }
47
+ else {
48
+ re += ".*";
49
+ i += 2;
50
+ }
51
+ }
52
+ else {
53
+ re += "[^/]*";
54
+ i++;
55
+ }
56
+ }
57
+ else if (c === "?") {
58
+ re += "[^/]";
59
+ i++;
60
+ }
61
+ else if (".+^$()|{}[]\\".includes(c)) {
62
+ re += "\\" + c;
63
+ i++;
64
+ }
65
+ else {
66
+ re += c;
67
+ i++;
68
+ }
69
+ }
70
+ re += "$";
71
+ return new RegExp(re);
72
+ }
73
+ export function matchesAnyGlob(path, globs) {
74
+ return globs.some((g) => globToRegExp(g).test(path));
75
+ }
76
+ /**
77
+ * Read the staged diff as a structured list. Uses git CLI directly — no
78
+ * surprises about index state, no library to keep in sync with git.
79
+ *
80
+ * Errors from git (not a repo, no staged changes, etc.) are RE-THROWN, not
81
+ * swallowed. The caller decides whether "no staged changes" is fatal.
82
+ */
83
+ export function getStagedFiles(repoRoot) {
84
+ const nameOutput = execSync("git diff --cached --name-only --diff-filter=ACMR", {
85
+ cwd: repoRoot,
86
+ encoding: "utf-8",
87
+ }).trim();
88
+ if (!nameOutput)
89
+ return [];
90
+ const fileNames = nameOutput.split("\n");
91
+ const result = [];
92
+ for (const file of fileNames) {
93
+ let diff;
94
+ try {
95
+ diff = execSync(`git diff --cached --unified=0 -- "${file.replace(/"/g, '\\"')}"`, {
96
+ cwd: repoRoot,
97
+ encoding: "utf-8",
98
+ });
99
+ }
100
+ catch {
101
+ // Binary file, deleted, or path that doesn't roundtrip — skip safely.
102
+ continue;
103
+ }
104
+ const addedLines = [];
105
+ let currentLine = 0;
106
+ for (const line of diff.split("\n")) {
107
+ // Hunk header: @@ -a,b +c,d @@
108
+ const hunk = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
109
+ if (hunk) {
110
+ currentLine = parseInt(hunk[1], 10);
111
+ continue;
112
+ }
113
+ if (line.startsWith("+++"))
114
+ continue;
115
+ if (line.startsWith("+")) {
116
+ addedLines.push({ lineNumber: currentLine, content: line.slice(1) });
117
+ currentLine++;
118
+ }
119
+ else if (line.startsWith(" ") || line.startsWith("---")) {
120
+ // Context or header — should not appear with --unified=0 but be safe
121
+ }
122
+ else if (line.startsWith("-")) {
123
+ // Removed lines don't advance the new-file line counter
124
+ }
125
+ else {
126
+ currentLine++;
127
+ }
128
+ }
129
+ result.push({ path: file, addedLines });
130
+ }
131
+ return result;
132
+ }
133
+ /**
134
+ * Apply policy.secret_patterns to a list of staged files. Returns one
135
+ * violation per matched line. Honors the `paths` glob scoping per pattern.
136
+ *
137
+ * IMPORTANT: We never return the matched secret value. Only the location
138
+ * + pattern id. This is the redaction contract.
139
+ */
140
+ export function runSecretScan(policy, files) {
141
+ const violations = [];
142
+ for (const pattern of policy.secret_patterns) {
143
+ const re = new RegExp(pattern.pattern);
144
+ for (const file of files) {
145
+ if (pattern.paths?.length && !matchesAnyGlob(file.path, pattern.paths))
146
+ continue;
147
+ for (const line of file.addedLines) {
148
+ if (re.test(line.content)) {
149
+ violations.push({
150
+ patternId: pattern.id,
151
+ severity: pattern.severity,
152
+ file: file.path,
153
+ lineNumber: line.lineNumber,
154
+ patternSource: pattern.pattern,
155
+ });
156
+ }
157
+ }
158
+ }
159
+ }
160
+ return violations;
161
+ }
162
+ /**
163
+ * For each doc_coverage rule, check:
164
+ * - did this commit touch any of the rule's source paths?
165
+ * - if yes, is the required doc-section file either (a) also in the staged
166
+ * diff, or (b) present at the expected path?
167
+ *
168
+ * "Section unchanged" detection requires reading the doc; we treat presence
169
+ * of the file as the floor (a real next-iteration improvement: hash the
170
+ * section under the anchor and require staged change to the anchor section
171
+ * when triggered. For now, presence + warn=non-staged is the contract).
172
+ */
173
+ export function runDocCoverage(policy, files, repoRoot) {
174
+ const stagedSet = new Set(files.map((f) => f.path));
175
+ const violations = [];
176
+ for (const rule of policy.doc_coverage) {
177
+ const matchedFiles = files
178
+ .map((f) => f.path)
179
+ .filter((p) => matchesAnyGlob(p, rule.paths));
180
+ if (matchedFiles.length === 0)
181
+ continue; // rule did not fire
182
+ const [docPath /*, anchor*/] = rule.requires_section.split("#");
183
+ const absoluteDocPath = join(repoRoot, docPath);
184
+ if (!existsSync(absoluteDocPath)) {
185
+ violations.push({
186
+ severity: rule.severity,
187
+ sourcePaths: rule.paths,
188
+ matchedFiles,
189
+ requiresSection: rule.requires_section,
190
+ reason: "doc-section-not-found",
191
+ });
192
+ continue;
193
+ }
194
+ // If the doc file IS staged, the commit author is updating it — pass.
195
+ if (stagedSet.has(docPath))
196
+ continue;
197
+ // Doc exists but is not in this commit. v1 contract: warn-or-block based
198
+ // on severity. Next iteration: hash the section under the anchor and
199
+ // require staged change to that section's lines specifically.
200
+ violations.push({
201
+ severity: rule.severity,
202
+ sourcePaths: rule.paths,
203
+ matchedFiles,
204
+ requiresSection: rule.requires_section,
205
+ reason: "doc-not-staged-and-section-unchanged",
206
+ });
207
+ }
208
+ return violations;
209
+ }
210
+ // ---------------------------------------------------------------------------
211
+ // Doc section hash (foundation for future v2 staged-section-change check)
212
+ // ---------------------------------------------------------------------------
213
+ /**
214
+ * Compute a stable SHA-256 of the section content under a markdown anchor.
215
+ * "Section" = lines from `## Anchor` (or `### Anchor` etc.) up to the next
216
+ * heading at the same or higher level. Used by the next-iteration v2 check.
217
+ *
218
+ * Exposed now so the CLI can surface a stable hash for compliance evidence
219
+ * (e.g., "as of this commit, the firewall section is hash X").
220
+ */
221
+ export function hashDocSection(filePath, anchor) {
222
+ if (!existsSync(filePath))
223
+ return null;
224
+ const lines = readFileSync(filePath, "utf-8").split("\n");
225
+ const slug = (s) => s.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
226
+ let inSection = false;
227
+ let sectionLevel = -1;
228
+ const section = [];
229
+ for (const line of lines) {
230
+ const heading = line.match(/^(#+)\s+(.+?)\s*$/);
231
+ if (heading) {
232
+ const level = heading[1].length;
233
+ const headSlug = slug(heading[2]);
234
+ if (!inSection && headSlug === slug(anchor)) {
235
+ inSection = true;
236
+ sectionLevel = level;
237
+ continue;
238
+ }
239
+ if (inSection && level <= sectionLevel) {
240
+ // Hit a sibling or higher heading — section ended
241
+ break;
242
+ }
243
+ }
244
+ if (inSection)
245
+ section.push(line);
246
+ }
247
+ if (!inSection)
248
+ return null;
249
+ return createHash("sha256").update(section.join("\n")).digest("hex");
250
+ }
251
+ // ---------------------------------------------------------------------------
252
+ // Formatters
253
+ // ---------------------------------------------------------------------------
254
+ export function formatSecretViolations(violations) {
255
+ if (violations.length === 0)
256
+ return "✅ No policy secret patterns matched.";
257
+ const lines = [];
258
+ const blocking = violations.filter((v) => v.severity === "block").length;
259
+ const warning = violations.filter((v) => v.severity === "warn").length;
260
+ lines.push(`🔒 SECRET POLICY: ${violations.length} violation(s) — ${blocking} blocking, ${warning} warning(s).`);
261
+ for (const v of violations) {
262
+ lines.push(` [${v.severity}] ${v.patternId} at ${v.file}:${v.lineNumber}`);
263
+ }
264
+ return lines.join("\n");
265
+ }
266
+ export function formatDocCoverageViolations(violations) {
267
+ if (violations.length === 0)
268
+ return "✅ All doc-coverage rules satisfied.";
269
+ const lines = [];
270
+ const blocking = violations.filter((v) => v.severity === "block").length;
271
+ const warning = violations.filter((v) => v.severity === "warn").length;
272
+ lines.push(`📄 DOC COVERAGE: ${violations.length} violation(s) — ${blocking} blocking, ${warning} warning(s).`);
273
+ for (const v of violations) {
274
+ const reason = v.reason === "doc-section-not-found"
275
+ ? "doc file does not exist"
276
+ : "doc not staged in this commit (and section content unchanged)";
277
+ lines.push(` [${v.severity}] ${v.matchedFiles.join(", ")} → ${v.requiresSection} (${reason})`);
278
+ }
279
+ return lines.join("\n");
280
+ }
281
+ // ---------------------------------------------------------------------------
282
+ // Machine-readable output (for CI pipelines)
283
+ // ---------------------------------------------------------------------------
284
+ export function formatSecretViolationsJson(violations) {
285
+ return JSON.stringify({
286
+ check: "secret-scan",
287
+ violations_total: violations.length,
288
+ blocking: violations.filter((v) => v.severity === "block").length,
289
+ warnings: violations.filter((v) => v.severity === "warn").length,
290
+ violations: violations.map((v) => ({
291
+ pattern_id: v.patternId,
292
+ severity: v.severity,
293
+ file: v.file,
294
+ line: v.lineNumber,
295
+ })),
296
+ });
297
+ }
298
+ export function formatDocCoverageViolationsJson(violations) {
299
+ return JSON.stringify({
300
+ check: "doc-coverage",
301
+ violations_total: violations.length,
302
+ blocking: violations.filter((v) => v.severity === "block").length,
303
+ warnings: violations.filter((v) => v.severity === "warn").length,
304
+ violations: violations.map((v) => ({
305
+ severity: v.severity,
306
+ source_paths: v.sourcePaths,
307
+ matched_files: v.matchedFiles,
308
+ requires_section: v.requiresSection,
309
+ reason: v.reason,
310
+ })),
311
+ });
312
+ }
313
+ //# sourceMappingURL=hooks.js.map
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map