@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,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
@@ -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