@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.
- package/CHANGELOG.md +313 -0
- package/LICENSE +83 -0
- package/README.md +470 -0
- package/defaults/learnings.json +146 -0
- package/dist/activation.d.ts +48 -0
- package/dist/activation.js +377 -0
- package/dist/adapters.d.ts +101 -0
- package/dist/adapters.js +171 -0
- package/dist/agents.d.ts +137 -0
- package/dist/agents.js +1638 -0
- package/dist/audit.d.ts +23 -0
- package/dist/audit.js +163 -0
- package/dist/cache.d.ts +15 -0
- package/dist/cache.js +117 -0
- package/dist/claude-integration.d.ts +95 -0
- package/dist/claude-integration.js +247 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +1823 -0
- package/dist/code-chunker.d.ts +12 -0
- package/dist/code-chunker.js +270 -0
- package/dist/collectors.d.ts +63 -0
- package/dist/collectors.js +617 -0
- package/dist/config.d.ts +73 -0
- package/dist/config.js +239 -0
- package/dist/embeddings.d.ts +36 -0
- package/dist/embeddings.js +124 -0
- package/dist/firewall.d.ts +133 -0
- package/dist/firewall.js +631 -0
- package/dist/hooks.d.ts +76 -0
- package/dist/hooks.js +313 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1081 -0
- package/dist/ingest.d.ts +32 -0
- package/dist/ingest.js +162 -0
- package/dist/learnings.d.ts +108 -0
- package/dist/learnings.js +714 -0
- package/dist/license-sig.d.ts +47 -0
- package/dist/license-sig.js +104 -0
- package/dist/policy.d.ts +131 -0
- package/dist/policy.js +182 -0
- package/dist/search.d.ts +11 -0
- package/dist/search.js +99 -0
- package/dist/sessions.d.ts +46 -0
- package/dist/sessions.js +153 -0
- package/examples/adapters/notion-adapter.js +108 -0
- package/examples/adapters/rss-adapter.js +76 -0
- package/package.json +87 -0
- package/skills/opscontext/SKILL.md +260 -0
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type AuditEvent = "learning.save" | "learning.delete" | "learning.import" | "session.save" | "session.delete" | "activation.activate" | "activation.deactivate" | "activation.heartbeat" | "activation.signature_reject" | "activation.legacy_signature" | "firewall.escalate" | "hook.block" | "hook.bypass";
|
|
2
|
+
export interface AuditRecord {
|
|
3
|
+
ts: string;
|
|
4
|
+
event: AuditEvent;
|
|
5
|
+
actor: string;
|
|
6
|
+
payload: Record<string, unknown>;
|
|
7
|
+
prev_hash: string;
|
|
8
|
+
hash: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function appendAudit(event: AuditEvent, payload: Record<string, unknown>, actor?: string): AuditRecord;
|
|
11
|
+
export declare function readAuditLog(): AuditRecord[];
|
|
12
|
+
export interface IntegrityReport {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
total: number;
|
|
15
|
+
breakAtIndex: number | null;
|
|
16
|
+
breakReason: string | null;
|
|
17
|
+
}
|
|
18
|
+
export declare function verifyChain(): IntegrityReport;
|
|
19
|
+
export declare function filterByRange(records: AuditRecord[], since?: string, until?: string): AuditRecord[];
|
|
20
|
+
export declare function toCsv(records: AuditRecord[]): string;
|
|
21
|
+
export declare function resetCacheForTest(): void;
|
|
22
|
+
export declare function safeAppend(event: AuditEvent, payload: Record<string, unknown>, actor?: string): void;
|
|
23
|
+
//# sourceMappingURL=audit.d.ts.map
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// 🔒 LOCKED [AUDIT-CHAIN] — 2026-06-10
|
|
2
|
+
// ⛔ NEVER change the canonical serialization in computeHash() — key order,
|
|
3
|
+
// field names, JSON.stringify behavior, or genesis hash value. Any change
|
|
4
|
+
// breaks verification of every audit log written by an older client.
|
|
5
|
+
// â›” NEVER swap SHA-256 for a different hash without a migration path.
|
|
6
|
+
// ⛔ NEVER catch errors inside appendAudit() — silent failures defeat the
|
|
7
|
+
// entire compliance story. Use safeAppend() at call sites if you need
|
|
8
|
+
// failure isolation; appendAudit() must surface problems loudly.
|
|
9
|
+
// WHY: This is the SOC2 CC7.2 / ISO 27001 A.12.4.1 compliance bedrock. The
|
|
10
|
+
// audit log is the foundation that licence-signature verification,
|
|
11
|
+
// compliance reporting, and enforcement telemetry all build on. Any
|
|
12
|
+
// silent break here destroys evidence value across years of records.
|
|
13
|
+
// FIX: If you need to evolve the record format, version the chain
|
|
14
|
+
// (add a "v":2 field) and keep verifyChain() backward-compatible by
|
|
15
|
+
// dispatching on the v field. Don't mutate the v=1 contract.
|
|
16
|
+
//
|
|
17
|
+
// Tamper-evident audit log — hash-chained JSONL at ~/.contextengine/audit.log.
|
|
18
|
+
//
|
|
19
|
+
// Compliance basis: SOC2 CC7.2 (audit logging), ISO 27001 A.12.4.1 (event logs).
|
|
20
|
+
//
|
|
21
|
+
// Records every state-changing operation. Each line carries the SHA-256 hash
|
|
22
|
+
// of the previous line's canonical content, so mutation of any historical
|
|
23
|
+
// record breaks chain verification at that index.
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, appendFileSync } from "fs";
|
|
25
|
+
import { join } from "path";
|
|
26
|
+
import { homedir } from "os";
|
|
27
|
+
import { createHash } from "crypto";
|
|
28
|
+
const GENESIS_HASH = "0".repeat(64);
|
|
29
|
+
function auditDir() {
|
|
30
|
+
// CONTEXTENGINE_HOME lets tests run against a temp dir without touching ~/.contextengine
|
|
31
|
+
return process.env.CONTEXTENGINE_HOME || join(homedir(), ".contextengine");
|
|
32
|
+
}
|
|
33
|
+
function auditPath() {
|
|
34
|
+
return join(auditDir(), "audit.log");
|
|
35
|
+
}
|
|
36
|
+
function ensureDir() {
|
|
37
|
+
const dir = auditDir();
|
|
38
|
+
if (!existsSync(dir)) {
|
|
39
|
+
mkdirSync(dir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function readLastHash() {
|
|
43
|
+
const path = auditPath();
|
|
44
|
+
if (!existsSync(path))
|
|
45
|
+
return GENESIS_HASH;
|
|
46
|
+
const data = readFileSync(path, "utf-8");
|
|
47
|
+
const lines = data.split("\n").filter(Boolean);
|
|
48
|
+
if (lines.length === 0)
|
|
49
|
+
return GENESIS_HASH;
|
|
50
|
+
try {
|
|
51
|
+
const last = JSON.parse(lines[lines.length - 1]);
|
|
52
|
+
return last.hash;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return GENESIS_HASH;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function computeHash(prevHash, ts, event, actor, payload) {
|
|
59
|
+
// Canonical serialization — keys in fixed order so independent verifiers get
|
|
60
|
+
// the same bytes regardless of how the record object was originally built.
|
|
61
|
+
const canonical = JSON.stringify({ prev_hash: prevHash, ts, event, actor, payload });
|
|
62
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
63
|
+
}
|
|
64
|
+
let cachedLastHash = null;
|
|
65
|
+
export function appendAudit(event, payload, actor = "system") {
|
|
66
|
+
ensureDir();
|
|
67
|
+
if (cachedLastHash === null)
|
|
68
|
+
cachedLastHash = readLastHash();
|
|
69
|
+
const ts = new Date().toISOString();
|
|
70
|
+
const hash = computeHash(cachedLastHash, ts, event, actor, payload);
|
|
71
|
+
const record = { ts, event, actor, payload, prev_hash: cachedLastHash, hash };
|
|
72
|
+
appendFileSync(auditPath(), JSON.stringify(record) + "\n");
|
|
73
|
+
cachedLastHash = hash;
|
|
74
|
+
return record;
|
|
75
|
+
}
|
|
76
|
+
export function readAuditLog() {
|
|
77
|
+
const path = auditPath();
|
|
78
|
+
if (!existsSync(path))
|
|
79
|
+
return [];
|
|
80
|
+
const data = readFileSync(path, "utf-8");
|
|
81
|
+
return data
|
|
82
|
+
.split("\n")
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.map((line, i) => {
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(line);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw new Error(`Corrupt audit line ${i + 1}: not valid JSON`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
export function verifyChain() {
|
|
94
|
+
let records;
|
|
95
|
+
try {
|
|
96
|
+
records = readAuditLog();
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
total: 0,
|
|
102
|
+
breakAtIndex: null,
|
|
103
|
+
breakReason: e instanceof Error ? e.message : String(e),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
let prev = GENESIS_HASH;
|
|
107
|
+
for (let i = 0; i < records.length; i++) {
|
|
108
|
+
const r = records[i];
|
|
109
|
+
if (r.prev_hash !== prev) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
total: records.length,
|
|
113
|
+
breakAtIndex: i,
|
|
114
|
+
breakReason: `prev_hash mismatch at index ${i}`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const expected = computeHash(prev, r.ts, r.event, r.actor, r.payload);
|
|
118
|
+
if (r.hash !== expected) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
total: records.length,
|
|
122
|
+
breakAtIndex: i,
|
|
123
|
+
breakReason: `hash mismatch at index ${i} (record tampered)`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
prev = r.hash;
|
|
127
|
+
}
|
|
128
|
+
return { ok: true, total: records.length, breakAtIndex: null, breakReason: null };
|
|
129
|
+
}
|
|
130
|
+
export function filterByRange(records, since, until) {
|
|
131
|
+
return records.filter((r) => {
|
|
132
|
+
if (since && r.ts < since)
|
|
133
|
+
return false;
|
|
134
|
+
if (until && r.ts > until)
|
|
135
|
+
return false;
|
|
136
|
+
return true;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
export function toCsv(records) {
|
|
140
|
+
const header = "ts,event,actor,payload,prev_hash,hash";
|
|
141
|
+
const rows = records.map((r) => {
|
|
142
|
+
const payload = JSON.stringify(r.payload).replace(/"/g, '""');
|
|
143
|
+
return `${r.ts},${r.event},${r.actor},"${payload}",${r.prev_hash},${r.hash}`;
|
|
144
|
+
});
|
|
145
|
+
return [header, ...rows].join("\n");
|
|
146
|
+
}
|
|
147
|
+
// Test-only — flush in-memory chain cache so a fresh path is re-read.
|
|
148
|
+
export function resetCacheForTest() {
|
|
149
|
+
cachedLastHash = null;
|
|
150
|
+
}
|
|
151
|
+
// Safe wrapper that never throws into hot paths. Use this from production
|
|
152
|
+
// call sites so a failed audit append cannot break a learning save or
|
|
153
|
+
// session write.
|
|
154
|
+
export function safeAppend(event, payload, actor = "system") {
|
|
155
|
+
try {
|
|
156
|
+
appendAudit(event, payload, actor);
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
// Last-resort surface — stderr only, never throw upward.
|
|
160
|
+
process.stderr.write(`[ContextEngine] audit append failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=audit.js.map
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Chunk } from "./ingest.js";
|
|
2
|
+
import type { EmbeddedChunk } from "./embeddings.js";
|
|
3
|
+
/**
|
|
4
|
+
* Try to load cached embeddings. Returns null if cache is stale or missing.
|
|
5
|
+
*/
|
|
6
|
+
export declare function loadCache(chunks: Chunk[]): EmbeddedChunk[] | null;
|
|
7
|
+
/**
|
|
8
|
+
* Save embeddings to disk cache.
|
|
9
|
+
*/
|
|
10
|
+
export declare function saveCache(chunks: Chunk[], embedded: EmbeddedChunk[]): void;
|
|
11
|
+
/**
|
|
12
|
+
* Clear the embedding cache.
|
|
13
|
+
*/
|
|
14
|
+
export declare function clearCache(): boolean;
|
|
15
|
+
//# sourceMappingURL=cache.d.ts.map
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync } from "fs";
|
|
2
|
+
import { resolve, join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { createHash } from "crypto";
|
|
5
|
+
/**
|
|
6
|
+
* Embedding cache — persists vectors to disk so restart is instant.
|
|
7
|
+
*
|
|
8
|
+
* Cache key: SHA-256 hash of all chunk contents (sorted).
|
|
9
|
+
* If chunks haven't changed, we skip the entire embedding step.
|
|
10
|
+
*
|
|
11
|
+
* File: ~/.contextengine/embedding-cache.json
|
|
12
|
+
* Format: { hash, vectors: [[...float32 values]], chunks: [{source,section,lineStart,lineEnd}] }
|
|
13
|
+
*
|
|
14
|
+
* On a typical 555-chunk dataset:
|
|
15
|
+
* - First run: ~15s (model load + embed)
|
|
16
|
+
* - Cached restart: ~200ms (read JSON + reconstruct Float32Arrays)
|
|
17
|
+
*/
|
|
18
|
+
const CACHE_DIR = resolve(homedir(), ".contextengine");
|
|
19
|
+
const CACHE_FILE = join(CACHE_DIR, "embedding-cache.json");
|
|
20
|
+
const CACHE_VERSION = 2;
|
|
21
|
+
/**
|
|
22
|
+
* Compute a hash of all chunk contents to detect changes.
|
|
23
|
+
* Uses source+section+content concatenated.
|
|
24
|
+
*/
|
|
25
|
+
function computeChunkHash(chunks) {
|
|
26
|
+
const hasher = createHash("sha256");
|
|
27
|
+
for (const c of chunks) {
|
|
28
|
+
hasher.update(`${c.source}\0${c.section}\0${c.content}\0`);
|
|
29
|
+
}
|
|
30
|
+
return hasher.digest("hex");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Try to load cached embeddings. Returns null if cache is stale or missing.
|
|
34
|
+
*/
|
|
35
|
+
export function loadCache(chunks) {
|
|
36
|
+
if (!existsSync(CACHE_FILE))
|
|
37
|
+
return null;
|
|
38
|
+
try {
|
|
39
|
+
const raw = readFileSync(CACHE_FILE, "utf-8");
|
|
40
|
+
const cache = JSON.parse(raw);
|
|
41
|
+
// Version check
|
|
42
|
+
if (cache.version !== CACHE_VERSION) {
|
|
43
|
+
console.error("[ContextEngine] 💾 Cache version mismatch — will re-embed");
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
// Hash check — have chunks changed?
|
|
47
|
+
const currentHash = computeChunkHash(chunks);
|
|
48
|
+
if (cache.hash !== currentHash) {
|
|
49
|
+
console.error("[ContextEngine] 💾 Cache stale (chunks changed) — will re-embed");
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
// Count check
|
|
53
|
+
if (cache.vectors.length !== chunks.length) {
|
|
54
|
+
console.error("[ContextEngine] 💾 Cache count mismatch — will re-embed");
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
// Reconstruct EmbeddedChunk[] from cache
|
|
58
|
+
const result = [];
|
|
59
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
60
|
+
result.push({
|
|
61
|
+
chunk: chunks[i],
|
|
62
|
+
vector: new Float32Array(cache.vectors[i]),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
console.error(`[ContextEngine] 💾 Loaded ${result.length} cached embeddings (instant!)`);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
console.error("[ContextEngine] âš Cache read failed:", err.message);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Save embeddings to disk cache.
|
|
75
|
+
*/
|
|
76
|
+
export function saveCache(chunks, embedded) {
|
|
77
|
+
try {
|
|
78
|
+
if (!existsSync(CACHE_DIR)) {
|
|
79
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
80
|
+
}
|
|
81
|
+
const cache = {
|
|
82
|
+
hash: computeChunkHash(chunks),
|
|
83
|
+
version: CACHE_VERSION,
|
|
84
|
+
chunkCount: chunks.length,
|
|
85
|
+
timestamp: new Date().toISOString(),
|
|
86
|
+
chunkKeys: chunks.map((c) => ({
|
|
87
|
+
source: c.source,
|
|
88
|
+
section: c.section,
|
|
89
|
+
lineStart: c.lineStart,
|
|
90
|
+
lineEnd: c.lineEnd,
|
|
91
|
+
})),
|
|
92
|
+
vectors: embedded.map((ec) => Array.from(ec.vector)),
|
|
93
|
+
};
|
|
94
|
+
writeFileSync(CACHE_FILE, JSON.stringify(cache));
|
|
95
|
+
const sizeKB = Math.round(statSync(CACHE_FILE).size / 1024);
|
|
96
|
+
console.error(`[ContextEngine] 💾 Saved ${embedded.length} embeddings to cache (${sizeKB} KB)`);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.error("[ContextEngine] âš Cache write failed:", err.message);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Clear the embedding cache.
|
|
104
|
+
*/
|
|
105
|
+
export function clearCache() {
|
|
106
|
+
try {
|
|
107
|
+
if (existsSync(CACHE_FILE)) {
|
|
108
|
+
unlinkSync(CACHE_FILE);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { KnowledgeSource } from "./config.js";
|
|
2
|
+
export type SkillInstallScope = "global" | "project";
|
|
3
|
+
export interface InstallSkillOpts {
|
|
4
|
+
scope?: SkillInstallScope;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
force?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface InstallSkillResult {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
installedTo: string;
|
|
11
|
+
source: string;
|
|
12
|
+
message: string;
|
|
13
|
+
alreadyInstalled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Locate the bundled skill directory regardless of how the package was
|
|
17
|
+
* installed. Walks up from the running CLI to find the package root.
|
|
18
|
+
*/
|
|
19
|
+
export declare function locateBundledSkill(cliDistDir: string): string;
|
|
20
|
+
export declare function installSkill(bundledSkillDir: string, opts?: InstallSkillOpts): InstallSkillResult;
|
|
21
|
+
export interface ManagedBlockInput {
|
|
22
|
+
projectName: string;
|
|
23
|
+
topLearnings: Array<{
|
|
24
|
+
id: string;
|
|
25
|
+
category: string;
|
|
26
|
+
rule: string;
|
|
27
|
+
project?: string;
|
|
28
|
+
}>;
|
|
29
|
+
policySummary: {
|
|
30
|
+
secretPatternCount: number;
|
|
31
|
+
secretPatternIds: string[];
|
|
32
|
+
docCoverageCount: number;
|
|
33
|
+
deployVerifyHostCount: number;
|
|
34
|
+
bypassTokenCount: number;
|
|
35
|
+
} | null;
|
|
36
|
+
recentBlocks: Array<{
|
|
37
|
+
ts: string;
|
|
38
|
+
check: string;
|
|
39
|
+
reason: string;
|
|
40
|
+
}>;
|
|
41
|
+
generatedAt: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build the managed-block content. Pure function — easy to test, never
|
|
45
|
+
* touches the filesystem.
|
|
46
|
+
*
|
|
47
|
+
* The format is intentionally compact: Claude Code's CLAUDE.md is loaded
|
|
48
|
+
* into every session's context, so each byte costs token budget. Aim for
|
|
49
|
+
* <500 tokens (~2000 chars) of content between the markers.
|
|
50
|
+
*/
|
|
51
|
+
export declare function buildManagedBlock(input: ManagedBlockInput): string;
|
|
52
|
+
/**
|
|
53
|
+
* Write the managed block into CLAUDE.md. Idempotent: if markers exist,
|
|
54
|
+
* replace the content between them. Otherwise append at end.
|
|
55
|
+
*
|
|
56
|
+
* Returns the modified file content (for testing) AND writes it to disk.
|
|
57
|
+
*/
|
|
58
|
+
export interface SyncResult {
|
|
59
|
+
ok: boolean;
|
|
60
|
+
mode: "replaced-existing-block" | "appended-new-block" | "created-new-file";
|
|
61
|
+
filePath: string;
|
|
62
|
+
bytesWritten: number;
|
|
63
|
+
}
|
|
64
|
+
export declare function syncClaudeMd(filePath: string, blockContent: string): SyncResult;
|
|
65
|
+
export declare const _markers: {
|
|
66
|
+
BEGIN_MARKER: string;
|
|
67
|
+
END_MARKER: string;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Walk Claude Code's per-project auto-memory directories and surface every
|
|
71
|
+
* .md file as a knowledge source. The structure is:
|
|
72
|
+
* ~/.claude/projects/<project-slug>/memory/MEMORY.md
|
|
73
|
+
* ~/.claude/projects/<project-slug>/memory/feedback_*.md
|
|
74
|
+
* ~/.claude/projects/<project-slug>/memory/project_*.md
|
|
75
|
+
* ... etc
|
|
76
|
+
*
|
|
77
|
+
* The <project-slug> is Claude Code's URL-safe encoding of an absolute path
|
|
78
|
+
* (slashes become hyphens, leading hyphen). We display the decoded path for
|
|
79
|
+
* readability in source names.
|
|
80
|
+
*
|
|
81
|
+
* Read-only: this function never modifies anything under ~/.claude/.
|
|
82
|
+
*/
|
|
83
|
+
export declare function discoverClaudeMemory(opts?: {
|
|
84
|
+
home?: string;
|
|
85
|
+
}): KnowledgeSource[];
|
|
86
|
+
/**
|
|
87
|
+
* Decode Claude Code's project slug back into a readable path.
|
|
88
|
+
* Encoding: leading hyphen, slashes → hyphens. e.g.
|
|
89
|
+
* -Users-yan-Projects-INVOK-fr → /Users/yan/Projects/INVOK-fr
|
|
90
|
+
*
|
|
91
|
+
* Not perfectly reversible (a dir literally named "foo-bar" looks the same
|
|
92
|
+
* as a path /foo/bar) but good enough for the display label.
|
|
93
|
+
*/
|
|
94
|
+
export declare function decodeClaudeProjectSlug(slug: string): string;
|
|
95
|
+
//# sourceMappingURL=claude-integration.d.ts.map
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// 🔒 LOCKED [CLAUDE-INTEGRATION] — 2026-06-11
|
|
2
|
+
// â›” NEVER change the BEGIN/END managed-block markers without a migration
|
|
3
|
+
// path. Users will have these markers committed in their CLAUDE.md
|
|
4
|
+
// files; changing the format silently doubles the block (old marker
|
|
5
|
+
// stays in place, new marker appended fresh).
|
|
6
|
+
// â›” NEVER include rule content that could be sensitive (raw command lines,
|
|
7
|
+
// file paths with secrets, etc.) in the managed block — CLAUDE.md is
|
|
8
|
+
// committed to git. Emit IDs + categories + short descriptions only.
|
|
9
|
+
// WHY: OpsContext-managed sections in CLAUDE.md are the highest-leverage
|
|
10
|
+
// integration with Claude Code — CLAUDE.md is loaded at every session
|
|
11
|
+
// start natively, so OpsContext's most-relevant intel lands in the
|
|
12
|
+
// agent's context with zero MCP roundtrip.
|
|
13
|
+
// FIX: If the managed-block format needs to evolve, add a version line
|
|
14
|
+
// inside the marker and have the rewriter accept both old and new
|
|
15
|
+
// versions during a deprecation window.
|
|
16
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "fs";
|
|
17
|
+
import { join, dirname, resolve } from "path";
|
|
18
|
+
import { homedir } from "os";
|
|
19
|
+
/**
|
|
20
|
+
* Locate the bundled skill directory regardless of how the package was
|
|
21
|
+
* installed. Walks up from the running CLI to find the package root.
|
|
22
|
+
*/
|
|
23
|
+
export function locateBundledSkill(cliDistDir) {
|
|
24
|
+
// dist/cli.js → ../skills/opscontext/
|
|
25
|
+
return resolve(cliDistDir, "..", "skills", "opscontext");
|
|
26
|
+
}
|
|
27
|
+
export function installSkill(bundledSkillDir, opts = {}) {
|
|
28
|
+
const scope = opts.scope ?? defaultScope(opts.cwd ?? process.cwd());
|
|
29
|
+
const target = scope === "global"
|
|
30
|
+
? join(homedir(), ".claude", "skills", "opscontext")
|
|
31
|
+
: join(opts.cwd ?? process.cwd(), ".claude", "skills", "opscontext");
|
|
32
|
+
const sourceSkillFile = join(bundledSkillDir, "SKILL.md");
|
|
33
|
+
const targetSkillFile = join(target, "SKILL.md");
|
|
34
|
+
if (!existsSync(sourceSkillFile)) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
installedTo: target,
|
|
38
|
+
source: bundledSkillDir,
|
|
39
|
+
message: `Bundled skill not found at ${sourceSkillFile}. Reinstall @compr/opscontext-mcp.`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (existsSync(targetSkillFile) && !opts.force) {
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
installedTo: target,
|
|
46
|
+
source: bundledSkillDir,
|
|
47
|
+
alreadyInstalled: true,
|
|
48
|
+
message: `Skill already installed at ${target}. Pass --force to overwrite.`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
mkdirSync(target, { recursive: true });
|
|
52
|
+
writeFileSync(targetSkillFile, readFileSync(sourceSkillFile, "utf-8"));
|
|
53
|
+
return {
|
|
54
|
+
ok: true,
|
|
55
|
+
installedTo: target,
|
|
56
|
+
source: bundledSkillDir,
|
|
57
|
+
message: `✅ Installed opscontext skill → ${targetSkillFile}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function defaultScope(cwd) {
|
|
61
|
+
// If the project has its own .claude/ dir, install there. Otherwise global.
|
|
62
|
+
return existsSync(join(cwd, ".claude")) ? "project" : "global";
|
|
63
|
+
}
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// B. CLAUDE.md auto-managed section
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
const BEGIN_MARKER = "<!-- BEGIN: managed by OpsContext (regenerated by `opscontext sync-claude-md`; manual edits to this block are overwritten) -->";
|
|
68
|
+
const END_MARKER = "<!-- END: managed by OpsContext -->";
|
|
69
|
+
/**
|
|
70
|
+
* Build the managed-block content. Pure function — easy to test, never
|
|
71
|
+
* touches the filesystem.
|
|
72
|
+
*
|
|
73
|
+
* The format is intentionally compact: Claude Code's CLAUDE.md is loaded
|
|
74
|
+
* into every session's context, so each byte costs token budget. Aim for
|
|
75
|
+
* <500 tokens (~2000 chars) of content between the markers.
|
|
76
|
+
*/
|
|
77
|
+
export function buildManagedBlock(input) {
|
|
78
|
+
const lines = [];
|
|
79
|
+
lines.push(BEGIN_MARKER);
|
|
80
|
+
lines.push(`## OpsContext snapshot for ${input.projectName}`);
|
|
81
|
+
lines.push("");
|
|
82
|
+
lines.push(`_Regenerated ${input.generatedAt}. Run \`opscontext sync-claude-md\` to refresh._`);
|
|
83
|
+
lines.push("");
|
|
84
|
+
// Top learnings — the recall hits the agent's context every session
|
|
85
|
+
if (input.topLearnings.length > 0) {
|
|
86
|
+
lines.push("### Top operational rules for this project");
|
|
87
|
+
for (const l of input.topLearnings) {
|
|
88
|
+
// Short ID + category prefix so the agent can cite it back; rule is the
|
|
89
|
+
// user-facing memory. Limit rule to one line to keep token budget low.
|
|
90
|
+
const rule = l.rule.replace(/\s+/g, " ").trim().slice(0, 180);
|
|
91
|
+
lines.push(`- [\`${l.id}\` · ${l.category}] ${rule}`);
|
|
92
|
+
}
|
|
93
|
+
lines.push("");
|
|
94
|
+
}
|
|
95
|
+
// Active policy gates — what the pre-commit hook will block
|
|
96
|
+
if (input.policySummary) {
|
|
97
|
+
lines.push("### Active policy gates (`.contextengine/policy.json`)");
|
|
98
|
+
const p = input.policySummary;
|
|
99
|
+
const parts = [];
|
|
100
|
+
if (p.secretPatternCount > 0) {
|
|
101
|
+
parts.push(`${p.secretPatternCount} secret pattern(s): ${p.secretPatternIds.slice(0, 5).join(", ")}${p.secretPatternIds.length > 5 ? "…" : ""}`);
|
|
102
|
+
}
|
|
103
|
+
if (p.docCoverageCount > 0)
|
|
104
|
+
parts.push(`${p.docCoverageCount} doc-coverage rule(s)`);
|
|
105
|
+
if (p.deployVerifyHostCount > 0)
|
|
106
|
+
parts.push(`${p.deployVerifyHostCount} deploy-verify host(s)`);
|
|
107
|
+
if (p.bypassTokenCount > 0)
|
|
108
|
+
parts.push(`${p.bypassTokenCount} bypass token(s)`);
|
|
109
|
+
if (parts.length === 0)
|
|
110
|
+
parts.push("policy.json present but no active rules");
|
|
111
|
+
for (const p2 of parts)
|
|
112
|
+
lines.push(`- ${p2}`);
|
|
113
|
+
lines.push("");
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
lines.push("### Policy");
|
|
117
|
+
lines.push("- No `.contextengine/policy.json` in this repo. Run `opscontext policy validate <file>` to author one.");
|
|
118
|
+
lines.push("");
|
|
119
|
+
}
|
|
120
|
+
// Recent compliance events — show the agent what was blocked
|
|
121
|
+
if (input.recentBlocks.length > 0) {
|
|
122
|
+
lines.push("### Recent hook blocks (last 3, from `~/.contextengine/audit.log`)");
|
|
123
|
+
for (const b of input.recentBlocks) {
|
|
124
|
+
const ts = b.ts.split("T")[0];
|
|
125
|
+
lines.push(`- ${ts} — \`${b.check}\` blocked: ${b.reason}`);
|
|
126
|
+
}
|
|
127
|
+
lines.push("");
|
|
128
|
+
}
|
|
129
|
+
lines.push(END_MARKER);
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
132
|
+
export function syncClaudeMd(filePath, blockContent) {
|
|
133
|
+
let mode;
|
|
134
|
+
let nextContent;
|
|
135
|
+
if (!existsSync(filePath)) {
|
|
136
|
+
mode = "created-new-file";
|
|
137
|
+
nextContent = `# CLAUDE.md\n\n${blockContent}\n`;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
const current = readFileSync(filePath, "utf-8");
|
|
141
|
+
const beginIdx = current.indexOf(BEGIN_MARKER);
|
|
142
|
+
const endIdx = current.indexOf(END_MARKER);
|
|
143
|
+
if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
|
|
144
|
+
// Replace the block + its trailing newline if present
|
|
145
|
+
const afterEnd = endIdx + END_MARKER.length;
|
|
146
|
+
const trailingNewline = current[afterEnd] === "\n" ? 1 : 0;
|
|
147
|
+
nextContent =
|
|
148
|
+
current.slice(0, beginIdx) +
|
|
149
|
+
blockContent +
|
|
150
|
+
current.slice(afterEnd + trailingNewline) +
|
|
151
|
+
// ensure we don't end up with stray blanks at boundary
|
|
152
|
+
(current[beginIdx - 1] === "\n" ? "" : "\n");
|
|
153
|
+
mode = "replaced-existing-block";
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// Append at end, with a leading newline if file doesn't end in one
|
|
157
|
+
const sep = current.endsWith("\n") ? "" : "\n";
|
|
158
|
+
nextContent = current + sep + "\n" + blockContent + "\n";
|
|
159
|
+
mode = "appended-new-block";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Always ensure the file ends with a single newline (POSIX convention) so
|
|
163
|
+
// a repeat sync with the same input is a true no-op.
|
|
164
|
+
if (!nextContent.endsWith("\n"))
|
|
165
|
+
nextContent += "\n";
|
|
166
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
167
|
+
writeFileSync(filePath, nextContent);
|
|
168
|
+
return {
|
|
169
|
+
ok: true,
|
|
170
|
+
mode,
|
|
171
|
+
filePath,
|
|
172
|
+
bytesWritten: Buffer.byteLength(nextContent),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// Exported for tests
|
|
176
|
+
export const _markers = { BEGIN_MARKER, END_MARKER };
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// C. Claude Code auto-memory discovery
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
/**
|
|
181
|
+
* Walk Claude Code's per-project auto-memory directories and surface every
|
|
182
|
+
* .md file as a knowledge source. The structure is:
|
|
183
|
+
* ~/.claude/projects/<project-slug>/memory/MEMORY.md
|
|
184
|
+
* ~/.claude/projects/<project-slug>/memory/feedback_*.md
|
|
185
|
+
* ~/.claude/projects/<project-slug>/memory/project_*.md
|
|
186
|
+
* ... etc
|
|
187
|
+
*
|
|
188
|
+
* The <project-slug> is Claude Code's URL-safe encoding of an absolute path
|
|
189
|
+
* (slashes become hyphens, leading hyphen). We display the decoded path for
|
|
190
|
+
* readability in source names.
|
|
191
|
+
*
|
|
192
|
+
* Read-only: this function never modifies anything under ~/.claude/.
|
|
193
|
+
*/
|
|
194
|
+
export function discoverClaudeMemory(opts = {}) {
|
|
195
|
+
const home = opts.home ?? homedir();
|
|
196
|
+
const root = join(home, ".claude", "projects");
|
|
197
|
+
if (!existsSync(root))
|
|
198
|
+
return [];
|
|
199
|
+
const sources = [];
|
|
200
|
+
let entries;
|
|
201
|
+
try {
|
|
202
|
+
entries = readdirSync(root);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
for (const slug of entries) {
|
|
208
|
+
const memDir = join(root, slug, "memory");
|
|
209
|
+
if (!existsSync(memDir))
|
|
210
|
+
continue;
|
|
211
|
+
let files;
|
|
212
|
+
try {
|
|
213
|
+
files = readdirSync(memDir);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const displayPath = decodeClaudeProjectSlug(slug);
|
|
219
|
+
for (const f of files) {
|
|
220
|
+
if (!f.endsWith(".md"))
|
|
221
|
+
continue;
|
|
222
|
+
const filePath = join(memDir, f);
|
|
223
|
+
sources.push({
|
|
224
|
+
name: `Claude memory — ${displayPath} — ${f}`,
|
|
225
|
+
path: filePath,
|
|
226
|
+
type: "markdown",
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return sources;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Decode Claude Code's project slug back into a readable path.
|
|
234
|
+
* Encoding: leading hyphen, slashes → hyphens. e.g.
|
|
235
|
+
* -Users-yan-Projects-INVOK-fr → /Users/yan/Projects/INVOK-fr
|
|
236
|
+
*
|
|
237
|
+
* Not perfectly reversible (a dir literally named "foo-bar" looks the same
|
|
238
|
+
* as a path /foo/bar) but good enough for the display label.
|
|
239
|
+
*/
|
|
240
|
+
export function decodeClaudeProjectSlug(slug) {
|
|
241
|
+
if (!slug.startsWith("-"))
|
|
242
|
+
return slug;
|
|
243
|
+
// Replace the leading hyphen with a slash, leave the rest as-is for
|
|
244
|
+
// the display label.
|
|
245
|
+
return "/" + slug.slice(1);
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=claude-integration.js.map
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ContextEngine CLI — standalone tool access + MCP server.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* contextengine Start MCP server (stdio transport)
|
|
7
|
+
* contextengine init Scaffold project for ContextEngine (mcp.json, docs, hooks)
|
|
8
|
+
* contextengine search <query> Search across all indexed knowledge
|
|
9
|
+
* contextengine list-sources Show all indexed sources with chunk counts
|
|
10
|
+
* contextengine list-projects Discover and analyze all projects
|
|
11
|
+
* contextengine list-learnings List all permanent learnings
|
|
12
|
+
* contextengine save-learning Save a learning (terminal fallback for MCP)
|
|
13
|
+
* contextengine score [project] AI-readiness score (writes SCORE.md to each project)
|
|
14
|
+
* contextengine audit Run compliance audit across all projects
|
|
15
|
+
* contextengine help Show this message
|
|
16
|
+
*/
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=cli.d.ts.map
|