@compr/opscontext-mcp 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/LICENSE +83 -0
  3. package/README.md +470 -0
  4. package/defaults/learnings.json +146 -0
  5. package/dist/activation.d.ts +48 -0
  6. package/dist/activation.js +377 -0
  7. package/dist/adapters.d.ts +101 -0
  8. package/dist/adapters.js +171 -0
  9. package/dist/agents.d.ts +137 -0
  10. package/dist/agents.js +1638 -0
  11. package/dist/audit.d.ts +23 -0
  12. package/dist/audit.js +163 -0
  13. package/dist/cache.d.ts +15 -0
  14. package/dist/cache.js +117 -0
  15. package/dist/claude-integration.d.ts +95 -0
  16. package/dist/claude-integration.js +247 -0
  17. package/dist/cli.d.ts +18 -0
  18. package/dist/cli.js +1823 -0
  19. package/dist/code-chunker.d.ts +12 -0
  20. package/dist/code-chunker.js +270 -0
  21. package/dist/collectors.d.ts +63 -0
  22. package/dist/collectors.js +617 -0
  23. package/dist/config.d.ts +73 -0
  24. package/dist/config.js +239 -0
  25. package/dist/embeddings.d.ts +36 -0
  26. package/dist/embeddings.js +124 -0
  27. package/dist/firewall.d.ts +133 -0
  28. package/dist/firewall.js +631 -0
  29. package/dist/hooks.d.ts +76 -0
  30. package/dist/hooks.js +313 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.js +1081 -0
  33. package/dist/ingest.d.ts +32 -0
  34. package/dist/ingest.js +162 -0
  35. package/dist/learnings.d.ts +108 -0
  36. package/dist/learnings.js +714 -0
  37. package/dist/license-sig.d.ts +47 -0
  38. package/dist/license-sig.js +104 -0
  39. package/dist/policy.d.ts +131 -0
  40. package/dist/policy.js +182 -0
  41. package/dist/search.d.ts +11 -0
  42. package/dist/search.js +99 -0
  43. package/dist/sessions.d.ts +46 -0
  44. package/dist/sessions.js +153 -0
  45. package/examples/adapters/notion-adapter.js +108 -0
  46. package/examples/adapters/rss-adapter.js +76 -0
  47. package/package.json +87 -0
  48. package/skills/opscontext/SKILL.md +260 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1823 @@
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
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from "fs";
18
+ import { join, basename, resolve } from "path";
19
+ import { createInterface } from "readline";
20
+ import { tmpdir, homedir } from "os";
21
+ import { execSync } from "child_process";
22
+ function detectProject(dir) {
23
+ const name = basename(dir);
24
+ const result = {
25
+ language: "unknown",
26
+ framework: null,
27
+ hasGit: existsSync(join(dir, ".git")),
28
+ hasGitHub: existsSync(join(dir, ".github")),
29
+ hasSrc: existsSync(join(dir, "src")),
30
+ hasTests: false,
31
+ projectName: name,
32
+ suggestedCodeDirs: [],
33
+ suggestedPatterns: [],
34
+ };
35
+ // Detect language + framework
36
+ if (existsSync(join(dir, "package.json"))) {
37
+ result.language = "javascript/typescript";
38
+ try {
39
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
40
+ if (pkg.dependencies?.next || pkg.devDependencies?.next)
41
+ result.framework = "Next.js";
42
+ else if (pkg.dependencies?.react || pkg.devDependencies?.react)
43
+ result.framework = "React";
44
+ else if (pkg.dependencies?.vue || pkg.devDependencies?.vue)
45
+ result.framework = "Vue";
46
+ else if (pkg.dependencies?.express || pkg.devDependencies?.express)
47
+ result.framework = "Express";
48
+ else if (pkg.dependencies?.["@modelcontextprotocol/sdk"])
49
+ result.framework = "MCP";
50
+ }
51
+ catch { /* ignore */ }
52
+ }
53
+ if (existsSync(join(dir, "composer.json")))
54
+ result.language = "php";
55
+ if (existsSync(join(dir, "Cargo.toml")))
56
+ result.language = "rust";
57
+ if (existsSync(join(dir, "go.mod")))
58
+ result.language = "go";
59
+ if (existsSync(join(dir, "requirements.txt")) || existsSync(join(dir, "pyproject.toml")))
60
+ result.language = "python";
61
+ // Detect framework specifics
62
+ if (existsSync(join(dir, "artisan")))
63
+ result.framework = "Laravel";
64
+ if (existsSync(join(dir, "manage.py")))
65
+ result.framework = "Django";
66
+ if (existsSync(join(dir, "Gemfile"))) {
67
+ result.language = "ruby";
68
+ result.framework = "Rails";
69
+ }
70
+ if (existsSync(join(dir, "pubspec.yaml"))) {
71
+ result.language = "dart";
72
+ result.framework = "Flutter";
73
+ }
74
+ // Detect tests
75
+ result.hasTests =
76
+ existsSync(join(dir, "tests")) ||
77
+ existsSync(join(dir, "test")) ||
78
+ existsSync(join(dir, "__tests__")) ||
79
+ existsSync(join(dir, "spec"));
80
+ // Suggest code dirs
81
+ for (const d of ["src", "app", "lib", "scripts"]) {
82
+ if (existsSync(join(dir, d)))
83
+ result.suggestedCodeDirs.push(d);
84
+ }
85
+ // Suggest patterns (always include these defaults)
86
+ result.suggestedPatterns = [
87
+ ".github/copilot-instructions.md",
88
+ "CLAUDE.md",
89
+ ".cursorrules",
90
+ "AGENTS.md",
91
+ ];
92
+ return result;
93
+ }
94
+ // ---------------------------------------------------------------------------
95
+ // Template for copilot-instructions.md
96
+ // ---------------------------------------------------------------------------
97
+ function generateCopilotInstructions(det) {
98
+ const lines = [];
99
+ lines.push(`# Copilot Instructions — ${det.projectName}\n`);
100
+ lines.push("## Project Context");
101
+ lines.push(`- **Language**: ${det.language}`);
102
+ if (det.framework)
103
+ lines.push(`- **Framework**: ${det.framework}`);
104
+ lines.push(`- **Branch**: \`main\``);
105
+ lines.push("");
106
+ lines.push("## Architecture");
107
+ lines.push("<!-- Describe your project architecture, key files, and data flow -->");
108
+ lines.push("");
109
+ lines.push("## Critical Rules");
110
+ lines.push("1. <!-- Rule 1: e.g., ES Modules only, all imports use .js extension -->");
111
+ lines.push("2. <!-- Rule 2: e.g., PHP 8.2 compatibility required -->");
112
+ lines.push("3. <!-- Rule 3: e.g., Never modify production .env without permission -->");
113
+ lines.push("");
114
+ lines.push("## Key Files");
115
+ lines.push("| File | Purpose |");
116
+ lines.push("|------|---------|");
117
+ lines.push("| <!-- path --> | <!-- description --> |");
118
+ lines.push("");
119
+ lines.push("## Related");
120
+ lines.push("- <!-- Links to related projects, docs, or resources -->");
121
+ lines.push("");
122
+ lines.push("## End-of-Session Protocol");
123
+ lines.push("Before ending ANY coding session, the AI agent MUST:");
124
+ lines.push("1. Update this file (`copilot-instructions.md`) with any new rules, architecture changes, or version bumps");
125
+ lines.push("2. Git commit + push all changed repositories");
126
+ lines.push("3. <!-- Optional: Update SKILLS.md, session logs, or other tracking docs -->");
127
+ lines.push("");
128
+ return lines.join("\n");
129
+ }
130
+ // ---------------------------------------------------------------------------
131
+ // Template for SKILLS.md
132
+ // ---------------------------------------------------------------------------
133
+ function generateSkillsMd(det) {
134
+ const lines = [];
135
+ lines.push(`# SKILLS.md — ${det.projectName}\n`);
136
+ lines.push("## What This Agent Can Do");
137
+ lines.push("<!-- List the key capabilities this project's AI agent should have -->");
138
+ lines.push("");
139
+ lines.push("## Key Patterns");
140
+ lines.push("<!-- Document reusable patterns agents should follow -->");
141
+ lines.push("");
142
+ lines.push("## What NOT to Do");
143
+ lines.push("<!-- Document anti-patterns and known pitfalls -->");
144
+ lines.push("");
145
+ return lines.join("\n");
146
+ }
147
+ // ---------------------------------------------------------------------------
148
+ // Template for SCORE.md
149
+ // ---------------------------------------------------------------------------
150
+ function generateScoreMd(det) {
151
+ const lines = [];
152
+ lines.push(`# SCORE.md — ${det.projectName}\n`);
153
+ lines.push("## AI-Readiness Score");
154
+ lines.push("<!-- Run `contextengine score` to generate this section -->");
155
+ lines.push("");
156
+ lines.push("## History");
157
+ lines.push(`| Date | Score | Notes |`);
158
+ lines.push(`|------|-------|-------|`);
159
+ lines.push(`| ${new Date().toISOString().split("T")[0]} | -- | Initial scaffold |`);
160
+ lines.push("");
161
+ return lines.join("\n");
162
+ }
163
+ // ---------------------------------------------------------------------------
164
+ // Template for CLAUDE.md
165
+ // ---------------------------------------------------------------------------
166
+ function generateClaudeMd(det) {
167
+ const lines = [];
168
+ lines.push(`# CLAUDE.md — ${det.projectName}\n`);
169
+ lines.push("## What This Is");
170
+ lines.push(`<!-- Brief description of ${det.projectName} -->`);
171
+ lines.push("");
172
+ lines.push("## Critical Rules");
173
+ lines.push("");
174
+ lines.push("1. <!-- Rule 1 -->");
175
+ lines.push("2. <!-- Rule 2 -->");
176
+ lines.push("");
177
+ lines.push("## Key Commands");
178
+ lines.push("```bash");
179
+ if (det.language === "javascript/typescript") {
180
+ lines.push("npm run build # Compile");
181
+ lines.push("npm test # Run tests");
182
+ lines.push("npm start # Start");
183
+ }
184
+ else if (det.language === "python") {
185
+ lines.push("python -m pytest # Run tests");
186
+ lines.push("python main.py # Start");
187
+ }
188
+ else if (det.language === "php") {
189
+ lines.push("composer install # Install deps");
190
+ lines.push("php artisan serve # Start (Laravel)");
191
+ }
192
+ else {
193
+ lines.push("# Add your key commands here");
194
+ }
195
+ lines.push("```");
196
+ lines.push("");
197
+ return lines.join("\n");
198
+ }
199
+ // ---------------------------------------------------------------------------
200
+ // Template for .vscode/mcp.json
201
+ // ---------------------------------------------------------------------------
202
+ function generateMcpJson() {
203
+ // Detect absolute node path for nvm compatibility
204
+ let nodePath = "node";
205
+ try {
206
+ nodePath = execSync("which node", { encoding: "utf-8" }).trim();
207
+ }
208
+ catch { /* fallback to bare node */ }
209
+ // Detect npx path for the args
210
+ let npxPath = "npx";
211
+ try {
212
+ npxPath = execSync("which npx", { encoding: "utf-8" }).trim();
213
+ }
214
+ catch { /* fallback to bare npx */ }
215
+ return {
216
+ servers: {
217
+ contextengine: {
218
+ type: "stdio",
219
+ command: npxPath,
220
+ args: ["-y", "@compr/contextengine-mcp"],
221
+ },
222
+ },
223
+ };
224
+ }
225
+ // ---------------------------------------------------------------------------
226
+ // Template for pre-commit hook (CE doc freshness + secret scanner)
227
+ // ---------------------------------------------------------------------------
228
+ function generatePreCommitHook() {
229
+ const lines = [];
230
+ lines.push("#!/bin/zsh");
231
+ lines.push("# ContextEngine — Pre-commit CE Compliance Check");
232
+ lines.push("# Blocks commits when CE docs are stale (>4h) or missing.");
233
+ lines.push("# Override: git commit --no-verify");
234
+ lines.push("#");
235
+ lines.push("# Checks: copilot-instructions.md, SKILLS.md, SCORE.md freshness");
236
+ lines.push("# Also scans for accidentally committed secrets.");
237
+ lines.push("");
238
+ lines.push("violations=0");
239
+ lines.push("");
240
+ lines.push("# Check CE doc freshness (4 hours = 14400 seconds)");
241
+ lines.push("now=$(date +%s)");
242
+ lines.push("max_age=14400");
243
+ lines.push("");
244
+ lines.push('for candidate_path in ".github/copilot-instructions.md" "SKILLS.md" "SCORE.md"; do');
245
+ lines.push(' if [[ -f "$candidate_path" ]]; then');
246
+ lines.push(' mod=$(stat -f %m "$candidate_path" 2>/dev/null || stat -c %Y "$candidate_path" 2>/dev/null)');
247
+ lines.push(' age=$((now - mod))');
248
+ lines.push(' if (( age > max_age )); then');
249
+ lines.push(' hours=$((age / 3600))');
250
+ lines.push(' echo "āš ļø CE: $candidate_path — last updated ${hours}h ago (not in this commit)"');
251
+ lines.push(" violations=$((violations + 1))");
252
+ lines.push(" fi");
253
+ lines.push(" fi");
254
+ lines.push("done");
255
+ lines.push("");
256
+ lines.push("# Secret scanning — block known secret patterns in staged files");
257
+ lines.push('secret_patterns=(');
258
+ lines.push(' "sk_live_" "sk_test_" "pk_live_" "pk_test_"');
259
+ lines.push(' "ghp_[A-Za-z0-9]" "glpat-[A-Za-z0-9]"');
260
+ lines.push(' "gsk_[A-Za-z0-9]" "xoxb-" "xoxp-"');
261
+ lines.push(' "AKIA[A-Z0-9]{16}" "SG\\.[A-Za-z0-9]"');
262
+ lines.push(' "sshpass.*-p"');
263
+ lines.push(")");
264
+ lines.push("");
265
+ lines.push("staged_files=$(git diff --cached --name-only --diff-filter=ACM)");
266
+ lines.push('for file in $staged_files; do');
267
+ lines.push(' # Skip known safe files');
268
+ lines.push(' case "$file" in');
269
+ lines.push(' .env|.env.*|.copilot-credentials.md|*/pre-commit*) continue ;;');
270
+ lines.push(" esac");
271
+ lines.push(' for pattern in "${secret_patterns[@]}"; do');
272
+ lines.push(' if grep -qE "$pattern" "$file" 2>/dev/null; then');
273
+ lines.push(' echo "šŸ”“ SECRET DETECTED in $file (pattern: $pattern)"');
274
+ lines.push(" violations=$((violations + 1))");
275
+ lines.push(" fi");
276
+ lines.push(" done");
277
+ lines.push("done");
278
+ lines.push("");
279
+ lines.push("if (( violations > 0 )); then");
280
+ lines.push(' echo ""');
281
+ lines.push(' echo "╔═══════════════════════════════════════════════════════╗"');
282
+ lines.push(' echo "ā•‘ ContextEngine: ${violations} CE compliance violation(s) ā•‘"');
283
+ lines.push(' echo "ā•‘ Code changed but CE docs are stale or missing. ā•‘"');
284
+ lines.push(' echo "ā•‘ Update: copilot-instructions, SKILLS, SCORE ā•‘"');
285
+ lines.push(' echo "ā•‘ ā•‘"');
286
+ lines.push(' echo "ā•‘ āŒ COMMIT BLOCKED — update docs first ā•‘"');
287
+ lines.push(' echo "ā•‘ Override: git commit --no-verify ā•‘"');
288
+ lines.push(' echo "ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•"');
289
+ lines.push(' echo ""');
290
+ lines.push(" exit 1");
291
+ lines.push("fi");
292
+ lines.push("");
293
+ lines.push("exit 0");
294
+ lines.push("");
295
+ return lines.join("\n");
296
+ }
297
+ // ---------------------------------------------------------------------------
298
+ // Template for post-commit hook (auto-push)
299
+ // ---------------------------------------------------------------------------
300
+ function generatePostCommitHook() {
301
+ const lines = [];
302
+ lines.push("#!/bin/zsh");
303
+ lines.push("# ContextEngine — Post-commit auto-push");
304
+ lines.push("# Pushes to origin (and gdrive if configured) in background.");
305
+ lines.push("# Runs async so commits return instantly.");
306
+ lines.push("");
307
+ lines.push("(");
308
+ lines.push(' git push origin "$(git branch --show-current)" 2>/dev/null &');
309
+ lines.push(' git push gdrive "$(git branch --show-current)" 2>/dev/null &');
310
+ lines.push(" wait");
311
+ lines.push(") &");
312
+ lines.push("");
313
+ return lines.join("\n");
314
+ }
315
+ // ---------------------------------------------------------------------------
316
+ // Template for contextengine.json
317
+ // ---------------------------------------------------------------------------
318
+ function generateConfig(det, cwd) {
319
+ const config = {
320
+ sources: [],
321
+ workspaces: [],
322
+ patterns: det.suggestedPatterns,
323
+ };
324
+ // Add existing instruction files as explicit sources
325
+ const existingFiles = [
326
+ { path: ".github/copilot-instructions.md", name: `${det.projectName} — Copilot Instructions` },
327
+ { path: "CLAUDE.md", name: `${det.projectName} — CLAUDE` },
328
+ { path: ".cursorrules", name: `${det.projectName} — Cursor Rules` },
329
+ { path: "AGENTS.md", name: `${det.projectName} — AGENTS` },
330
+ { path: "README.md", name: `${det.projectName} — README` },
331
+ ];
332
+ const sources = [];
333
+ for (const f of existingFiles) {
334
+ if (existsSync(join(cwd, f.path))) {
335
+ sources.push({ name: f.name, path: f.path });
336
+ }
337
+ }
338
+ config.sources = sources;
339
+ if (det.suggestedCodeDirs.length > 0) {
340
+ config.codeDirs = det.suggestedCodeDirs;
341
+ }
342
+ return config;
343
+ }
344
+ // ---------------------------------------------------------------------------
345
+ // Interactive prompts
346
+ // ---------------------------------------------------------------------------
347
+ function ask(rl, question) {
348
+ return new Promise((resolve) => rl.question(question, resolve));
349
+ }
350
+ async function runInit() {
351
+ const cwd = process.cwd();
352
+ console.log("\nšŸš€ ContextEngine Init\n");
353
+ console.log(`Initializing in: ${cwd}\n`);
354
+ // Detect project
355
+ const det = detectProject(cwd);
356
+ console.log(` Detected: ${det.language}${det.framework ? ` (${det.framework})` : ""}`);
357
+ console.log(` Git: ${det.hasGit ? "āœ…" : "āŒ"} GitHub: ${det.hasGitHub ? "āœ…" : "āŒ"} Tests: ${det.hasTests ? "āœ…" : "āŒ"}`);
358
+ if (det.suggestedCodeDirs.length > 0) {
359
+ console.log(` Code dirs: ${det.suggestedCodeDirs.join(", ")}`);
360
+ }
361
+ console.log("");
362
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
363
+ let created = 0;
364
+ let skipped = 0;
365
+ try {
366
+ // -----------------------------------------------------------------------
367
+ // Tier 1: Required — .vscode/mcp.json
368
+ // -----------------------------------------------------------------------
369
+ console.log(" šŸ“¦ Tier 1: Required");
370
+ const mcpPath = join(cwd, ".vscode", "mcp.json");
371
+ if (existsSync(mcpPath)) {
372
+ console.log(" ā­ .vscode/mcp.json already exists — skipping");
373
+ skipped++;
374
+ }
375
+ else {
376
+ const answer = isNonInteractive ? "y" : await ask(rl, " Create .vscode/mcp.json (MCP connectivity)? [Y/n] ");
377
+ if (answer.toLowerCase() !== "n") {
378
+ mkdirSync(join(cwd, ".vscode"), { recursive: true });
379
+ writeFileSync(mcpPath, JSON.stringify(generateMcpJson(), null, 2) + "\n");
380
+ console.log(" āœ… Created .vscode/mcp.json");
381
+ created++;
382
+ }
383
+ }
384
+ console.log("");
385
+ // -----------------------------------------------------------------------
386
+ // Tier 2: Strongly Recommended
387
+ // -----------------------------------------------------------------------
388
+ console.log(" šŸ“‹ Tier 2: Strongly Recommended");
389
+ // contextengine.json
390
+ const configPath = join(cwd, "contextengine.json");
391
+ if (existsSync(configPath)) {
392
+ console.log(" ā­ contextengine.json already exists — skipping");
393
+ skipped++;
394
+ }
395
+ else {
396
+ const createConfig = isNonInteractive ? "y" : await ask(rl, " Create contextengine.json? [Y/n] ");
397
+ if (createConfig.toLowerCase() !== "n") {
398
+ const config = generateConfig(det, cwd);
399
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
400
+ console.log(" āœ… Created contextengine.json");
401
+ created++;
402
+ }
403
+ }
404
+ // .github/copilot-instructions.md
405
+ const copilotPath = join(cwd, ".github", "copilot-instructions.md");
406
+ if (existsSync(copilotPath)) {
407
+ console.log(" ā­ .github/copilot-instructions.md already exists — skipping");
408
+ skipped++;
409
+ }
410
+ else {
411
+ const createCopilot = isNonInteractive ? "y" : await ask(rl, " Create .github/copilot-instructions.md? [Y/n] ");
412
+ if (createCopilot.toLowerCase() !== "n") {
413
+ mkdirSync(join(cwd, ".github"), { recursive: true });
414
+ writeFileSync(copilotPath, generateCopilotInstructions(det));
415
+ console.log(" āœ… Created .github/copilot-instructions.md");
416
+ created++;
417
+ }
418
+ }
419
+ // SKILLS.md
420
+ const skillsPath = join(cwd, "SKILLS.md");
421
+ if (existsSync(skillsPath)) {
422
+ console.log(" ā­ SKILLS.md already exists — skipping");
423
+ skipped++;
424
+ }
425
+ else {
426
+ const answer = isNonInteractive ? "y" : await ask(rl, " Create SKILLS.md (agent skill tracking)? [Y/n] ");
427
+ if (answer.toLowerCase() !== "n") {
428
+ writeFileSync(skillsPath, generateSkillsMd(det));
429
+ console.log(" āœ… Created SKILLS.md");
430
+ created++;
431
+ }
432
+ }
433
+ // SCORE.md
434
+ const scorePath = join(cwd, "SCORE.md");
435
+ if (existsSync(scorePath)) {
436
+ console.log(" ā­ SCORE.md already exists — skipping");
437
+ skipped++;
438
+ }
439
+ else {
440
+ const answer = isNonInteractive ? "y" : await ask(rl, " Create SCORE.md (AI-readiness tracking)? [Y/n] ");
441
+ if (answer.toLowerCase() !== "n") {
442
+ writeFileSync(scorePath, generateScoreMd(det));
443
+ console.log(" āœ… Created SCORE.md");
444
+ created++;
445
+ }
446
+ }
447
+ // Git hooks (only if .git exists)
448
+ if (det.hasGit) {
449
+ const hooksDir = join(cwd, ".git", "hooks");
450
+ const preCommitDest = join(hooksDir, "pre-commit");
451
+ const postCommitDest = join(hooksDir, "post-commit");
452
+ if (existsSync(preCommitDest)) {
453
+ console.log(" ā­ .git/hooks/pre-commit already exists — skipping");
454
+ skipped++;
455
+ }
456
+ else {
457
+ const answer = isNonInteractive ? "y" : await ask(rl, " Install pre-commit hook (doc freshness + secret scan)? [Y/n] ");
458
+ if (answer.toLowerCase() !== "n") {
459
+ mkdirSync(hooksDir, { recursive: true });
460
+ writeFileSync(preCommitDest, generatePreCommitHook(), { mode: 0o755 });
461
+ console.log(" āœ… Installed .git/hooks/pre-commit");
462
+ created++;
463
+ }
464
+ }
465
+ if (existsSync(postCommitDest)) {
466
+ console.log(" ā­ .git/hooks/post-commit already exists — skipping");
467
+ skipped++;
468
+ }
469
+ else {
470
+ const answer = isNonInteractive ? "y" : await ask(rl, " Install post-commit hook (auto-push)? [Y/n] ");
471
+ if (answer.toLowerCase() !== "n") {
472
+ mkdirSync(hooksDir, { recursive: true });
473
+ writeFileSync(postCommitDest, generatePostCommitHook(), { mode: 0o755 });
474
+ console.log(" āœ… Installed .git/hooks/post-commit");
475
+ created++;
476
+ }
477
+ }
478
+ }
479
+ console.log("");
480
+ // -----------------------------------------------------------------------
481
+ // Tier 3: Optional
482
+ // -----------------------------------------------------------------------
483
+ console.log(" šŸ’” Tier 3: Optional");
484
+ // CLAUDE.md
485
+ const claudePath = join(cwd, "CLAUDE.md");
486
+ if (existsSync(claudePath)) {
487
+ console.log(" ā­ CLAUDE.md already exists — skipping");
488
+ skipped++;
489
+ }
490
+ else {
491
+ const answer = isNonInteractive ? "y" : await ask(rl, " Create CLAUDE.md (Claude-specific instructions)? [Y/n] ");
492
+ if (answer.toLowerCase() !== "n") {
493
+ writeFileSync(claudePath, generateClaudeMd(det));
494
+ console.log(" āœ… Created CLAUDE.md");
495
+ created++;
496
+ }
497
+ }
498
+ // Summary
499
+ console.log(`\n✨ Done! Created ${created} files, skipped ${skipped} (already exist).`);
500
+ console.log("");
501
+ console.log(" Next steps:");
502
+ console.log(" 1. Edit .github/copilot-instructions.md with your project details");
503
+ console.log(" 2. Start a Copilot chat — ContextEngine tools are now available");
504
+ console.log(" 3. Run `contextengine score` to get your AI-readiness baseline");
505
+ console.log("");
506
+ }
507
+ finally {
508
+ rl.close();
509
+ }
510
+ }
511
+ // ---------------------------------------------------------------------------
512
+ // CLI Engine — shared initialization for all CLI subcommands
513
+ // ---------------------------------------------------------------------------
514
+ import { loadSources, loadProjectDirs, loadConfig } from "./config.js";
515
+ import { ingestSources } from "./ingest.js";
516
+ import { searchChunks } from "./search.js";
517
+ import { collectProjectOps, collectSystemOps } from "./collectors.js";
518
+ import { scanCodeDir } from "./code-chunker.js";
519
+ import { listProjects, runComplianceAudit, formatProjectList, formatPlan, scoreProject, formatScoreReport, generateScoreHTML, generateProjectScoreMD, } from "./agents.js";
520
+ import { listLearnings, learningsToChunks, learningsStats, formatLearnings, saveLearning, deleteLearning, importLearningsFromFile, autoImportFromSources, LEARNING_CATEGORIES, } from "./learnings.js";
521
+ import { saveSession, loadSession, listSessions, deleteSession, formatSession, formatSessionList, } from "./sessions.js";
522
+ import { activate, deactivate, getActivationStatus, gateCheck, } from "./activation.js";
523
+ import { readAuditLog, verifyChain, filterByRange, toCsv, } from "./audit.js";
524
+ import { loadRepoPolicy, parsePolicy, formatPolicySummary, formatValidationErrors, repoPolicyPath, } from "./policy.js";
525
+ import { getStagedFiles, runSecretScan, runDocCoverage, formatSecretViolations, formatDocCoverageViolations, formatSecretViolationsJson, formatDocCoverageViolationsJson, } from "./hooks.js";
526
+ import { safeAppend } from "./audit.js";
527
+ import { installSkill, locateBundledSkill, buildManagedBlock, syncClaudeMd, } from "./claude-integration.js";
528
+ import { fileURLToPath } from "url";
529
+ // ---------------------------------------------------------------------------
530
+ // Non-interactive mode: skip prompts when piped or --yes flag
531
+ // ---------------------------------------------------------------------------
532
+ const isNonInteractive = !process.stdin.isTTY || process.argv.includes("--yes") || process.argv.includes("-y");
533
+ /**
534
+ * Initialize the engine (load sources, ingest, collect ops, learnings).
535
+ * This is the same logic as index.ts main() but WITHOUT MCP server or embeddings.
536
+ * Keyword search is instant and sufficient for CLI usage.
537
+ */
538
+ async function initEngine() {
539
+ const sources = loadSources();
540
+ const chunks = ingestSources(sources);
541
+ const config = loadConfig();
542
+ const projectDirs = loadProjectDirs();
543
+ // Collect operational data
544
+ if (config.collectOps !== false) {
545
+ for (const dir of projectDirs) {
546
+ const ops = collectProjectOps(dir.path, dir.name);
547
+ chunks.push(...ops);
548
+ }
549
+ }
550
+ if (config.collectSystemOps !== false) {
551
+ const sysOps = collectSystemOps();
552
+ chunks.push(...sysOps);
553
+ }
554
+ // Scan code files
555
+ if (config.codeDirs && config.codeDirs.length > 0) {
556
+ for (const dir of projectDirs) {
557
+ for (const codeDir of config.codeDirs) {
558
+ const codePath = join(dir.path, codeDir);
559
+ if (existsSync(codePath)) {
560
+ const codeResults = scanCodeDir(codePath, dir.name);
561
+ chunks.push(...codeResults);
562
+ }
563
+ }
564
+ }
565
+ }
566
+ // Inject learnings (project-scoped to prevent cross-project IP leakage)
567
+ const projectNames = projectDirs.map((d) => d.name);
568
+ const learningChunks = learningsToChunks(projectNames);
569
+ chunks.push(...learningChunks);
570
+ return { sources, chunks };
571
+ }
572
+ // ---------------------------------------------------------------------------
573
+ // CLI Subcommands
574
+ // ---------------------------------------------------------------------------
575
+ async function cliSearch(query, topK) {
576
+ const { chunks } = await initEngine();
577
+ const results = searchChunks(chunks, query, topK);
578
+ if (results.length === 0) {
579
+ console.log(`No results found for: "${query}"`);
580
+ return;
581
+ }
582
+ console.log(`\nšŸ” Search: "${query}" | ${results.length} results (keyword/BM25)\n`);
583
+ for (let i = 0; i < results.length; i++) {
584
+ const r = results[i];
585
+ console.log(`--- Result ${i + 1} (score: ${r.score.toFixed(3)}) ---`);
586
+ console.log(`Source: ${r.chunk.source}`);
587
+ console.log(`Section: ${r.chunk.section}`);
588
+ console.log(`Lines: ${r.chunk.lineStart}-${r.chunk.lineEnd}`);
589
+ console.log("");
590
+ console.log(r.chunk.content);
591
+ console.log("");
592
+ }
593
+ }
594
+ async function cliListSources() {
595
+ const { sources, chunks } = await initEngine();
596
+ console.log(`\nšŸ“š ContextEngine — ${sources.length} sources | ${chunks.length} chunks\n`);
597
+ for (const s of sources) {
598
+ const exists = existsSync(s.path);
599
+ const count = chunks.filter((c) => c.source === s.name).length;
600
+ const status = exists ? `āœ… ${count} chunks` : "⚠ file not found";
601
+ console.log(` ${s.name}: ${status}`);
602
+ console.log(` ${s.path}`);
603
+ }
604
+ console.log("");
605
+ }
606
+ async function cliListProjects() {
607
+ const gate = gateCheck("list_projects");
608
+ if (gate) {
609
+ console.error(gate);
610
+ process.exit(1);
611
+ }
612
+ const projectDirs = loadProjectDirs();
613
+ const projects = listProjects(projectDirs);
614
+ const text = formatProjectList(projects);
615
+ console.log(`\n${text}`);
616
+ }
617
+ async function cliListLearnings(category) {
618
+ // Project-scoped: only show learnings for workspace projects + universal
619
+ const projectDirs = loadProjectDirs();
620
+ const projectNames = projectDirs.map((d) => d.name);
621
+ const learnings = listLearnings(category, projectNames);
622
+ const text = formatLearnings(learnings);
623
+ console.log(`\n${text}`);
624
+ }
625
+ async function cliSaveLearning(args) {
626
+ // Parse: save-learning "rule text" -c category [-p project] [--context "..."]
627
+ let rule = "";
628
+ let category = "";
629
+ let project;
630
+ let context = "";
631
+ for (let i = 0; i < args.length; i++) {
632
+ if ((args[i] === "-c" || args[i] === "--category") && args[i + 1]) {
633
+ category = args[++i];
634
+ }
635
+ else if ((args[i] === "-p" || args[i] === "--project") && args[i + 1]) {
636
+ project = args[++i];
637
+ }
638
+ else if (args[i] === "--context" && args[i + 1]) {
639
+ context = args[++i];
640
+ }
641
+ else if (!rule) {
642
+ rule = args[i];
643
+ }
644
+ else {
645
+ // Additional words become part of the rule if not quoted
646
+ rule += " " + args[i];
647
+ }
648
+ }
649
+ if (!rule || !category) {
650
+ console.error("Usage: contextengine save-learning \"<rule text>\" -c <category> [-p project] [--context \"...\"]");
651
+ console.error(`\nCategories: ${LEARNING_CATEGORIES.join(", ")}`);
652
+ process.exit(1);
653
+ }
654
+ if (!LEARNING_CATEGORIES.includes(category)) {
655
+ console.error(`āŒ Invalid category: "${category}"`);
656
+ console.error(`Valid: ${LEARNING_CATEGORIES.join(", ")}`);
657
+ process.exit(1);
658
+ }
659
+ const learning = saveLearning(category, rule, context, project);
660
+ console.log(`āœ… Learning saved: ${learning.id}`);
661
+ console.log(` Category: ${learning.category}`);
662
+ console.log(` Rule: ${learning.rule}`);
663
+ if (learning.project)
664
+ console.log(` Project: ${learning.project}`);
665
+ if (learning.context)
666
+ console.log(` Context: ${learning.context}`);
667
+ console.log(` Tags: ${learning.tags.join(", ")}`);
668
+ }
669
+ async function cliDeleteLearning(id) {
670
+ if (!id) {
671
+ console.error("Usage: contextengine delete-learning <id>");
672
+ process.exit(1);
673
+ }
674
+ const deleted = deleteLearning(id);
675
+ if (deleted) {
676
+ console.log(`āœ… Learning ${id} deleted.`);
677
+ }
678
+ else {
679
+ console.error(`āŒ Learning not found: ${id}`);
680
+ process.exit(1);
681
+ }
682
+ }
683
+ async function cliScore(project, html = false, save = true) {
684
+ const gate = gateCheck("score_project");
685
+ if (gate) {
686
+ console.error(gate);
687
+ process.exit(1);
688
+ }
689
+ const projectDirs = loadProjectDirs();
690
+ let scores;
691
+ if (project) {
692
+ const dir = projectDirs.find((d) => d.name.toLowerCase() === project.toLowerCase());
693
+ if (!dir) {
694
+ console.error(`āŒ Project not found: "${project}"`);
695
+ console.error(`Available: ${projectDirs.map((d) => d.name).join(", ")}`);
696
+ process.exit(1);
697
+ }
698
+ scores = [scoreProject(dir)];
699
+ }
700
+ else {
701
+ scores = projectDirs.map((d) => scoreProject(d));
702
+ }
703
+ if (html) {
704
+ const htmlContent = generateScoreHTML(scores);
705
+ const tmpPath = join(tmpdir(), "contextengine-score.html");
706
+ writeFileSync(tmpPath, htmlContent, "utf-8");
707
+ console.log(`\nšŸ“Š HTML report written to: ${tmpPath}`);
708
+ // Open in default browser
709
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
710
+ try {
711
+ execSync(`${openCmd} "${tmpPath}"`);
712
+ console.log("🌐 Opened in browser\n");
713
+ }
714
+ catch {
715
+ console.log(`Open manually: file://${tmpPath}\n`);
716
+ }
717
+ }
718
+ else {
719
+ const text = formatScoreReport(scores);
720
+ console.log(`\n${text}`);
721
+ }
722
+ // Auto-write SCORE.md to each project root
723
+ if (save) {
724
+ for (const s of scores) {
725
+ const scorePath = join(s.path, "SCORE.md");
726
+ const md = generateProjectScoreMD(s);
727
+ writeFileSync(scorePath, md, "utf-8");
728
+ console.log(`šŸ“ SCORE.md written to ${scorePath}`);
729
+ }
730
+ }
731
+ }
732
+ async function cliAudit() {
733
+ const gate = gateCheck("run_audit");
734
+ if (gate) {
735
+ console.error(gate);
736
+ process.exit(1);
737
+ }
738
+ const projectDirs = loadProjectDirs();
739
+ const plan = runComplianceAudit(projectDirs);
740
+ const text = formatPlan(plan);
741
+ console.log(`\n${text}`);
742
+ }
743
+ // ---------------------------------------------------------------------------
744
+ // CLI: Session management
745
+ // ---------------------------------------------------------------------------
746
+ async function cliSaveSession(args) {
747
+ // save-session <name> <key> <value>
748
+ // or: save-session <name> <key> --stdin (reads value from stdin)
749
+ let name = "";
750
+ let key = "";
751
+ let value = "";
752
+ let fromStdin = false;
753
+ const positional = [];
754
+ for (let i = 0; i < args.length; i++) {
755
+ if (args[i] === "--stdin") {
756
+ fromStdin = true;
757
+ }
758
+ else {
759
+ positional.push(args[i]);
760
+ }
761
+ }
762
+ name = positional[0] || "";
763
+ key = positional[1] || "";
764
+ value = positional.slice(2).join(" ");
765
+ if (!name || !key) {
766
+ console.error("Usage: contextengine save-session <name> <key> <value>");
767
+ console.error(" contextengine save-session <name> <key> --stdin");
768
+ console.error("\nExamples:");
769
+ console.error(' contextengine save-session my-project summary "Fixed auth bug, deployed to staging"');
770
+ console.error(' contextengine save-session my-project active_tasks "1. Deploy 2. Test 3. Monitor"');
771
+ console.error(' cat notes.md | contextengine save-session my-project notes --stdin');
772
+ process.exit(1);
773
+ }
774
+ if (fromStdin && !value) {
775
+ // Read from stdin
776
+ const chunks = [];
777
+ for await (const chunk of process.stdin) {
778
+ chunks.push(Buffer.from(chunk));
779
+ }
780
+ value = Buffer.concat(chunks).toString("utf-8").trim();
781
+ }
782
+ if (!value) {
783
+ console.error("Error: No value provided. Pass as argument or use --stdin.");
784
+ process.exit(1);
785
+ }
786
+ const session = saveSession(name, key, value);
787
+ console.log(`āœ… Session "${name}" updated — key "${key}" saved (${session.entries.length} total entries)`);
788
+ }
789
+ async function cliLoadSession(name) {
790
+ if (!name) {
791
+ console.error("Usage: contextengine load-session <name>");
792
+ process.exit(1);
793
+ }
794
+ const session = loadSession(name);
795
+ if (!session) {
796
+ console.error(`āŒ Session not found: "${name}"`);
797
+ const sessions = listSessions();
798
+ if (sessions.length > 0) {
799
+ console.error(`\nAvailable sessions: ${sessions.map(s => s.name).join(", ")}`);
800
+ }
801
+ process.exit(1);
802
+ }
803
+ console.log(formatSession(session));
804
+ }
805
+ async function cliListSessions() {
806
+ const sessions = listSessions();
807
+ console.log(`\n${formatSessionList(sessions)}`);
808
+ }
809
+ async function cliDeleteSession(name) {
810
+ if (!name) {
811
+ console.error("Usage: contextengine delete-session <name>");
812
+ process.exit(1);
813
+ }
814
+ const ok = deleteSession(name);
815
+ if (ok) {
816
+ console.log(`āœ… Deleted session "${name}".`);
817
+ return;
818
+ }
819
+ console.error(`āŒ Session not found: "${name}"`);
820
+ const available = listSessions();
821
+ if (available.length > 0) {
822
+ console.error(`\nAvailable sessions: ${available.map((s) => s.name).join(", ")}`);
823
+ }
824
+ process.exit(1);
825
+ }
826
+ async function cliExportLearnings(args) {
827
+ let project;
828
+ let category;
829
+ let format = "json";
830
+ let universalToo = false;
831
+ for (let i = 0; i < args.length; i++) {
832
+ const a = args[i];
833
+ if ((a === "--project" || a === "-p") && args[i + 1]) {
834
+ project = args[++i];
835
+ continue;
836
+ }
837
+ if ((a === "--category" || a === "-c") && args[i + 1]) {
838
+ category = args[++i];
839
+ continue;
840
+ }
841
+ if (a === "--format" && args[i + 1]) {
842
+ const f = args[++i];
843
+ if (f !== "json" && f !== "markdown") {
844
+ console.error(`Unknown format: ${f}. Supported: json, markdown.`);
845
+ process.exit(1);
846
+ }
847
+ format = f;
848
+ continue;
849
+ }
850
+ if (a === "--include-universal") {
851
+ universalToo = true;
852
+ continue;
853
+ }
854
+ if (a === "-h" || a === "--help") {
855
+ console.log(`Usage: contextengine export-learnings [--project NAME] [--category CAT] [--format json|markdown] [--include-universal]
856
+
857
+ Exports learnings as JSON or Markdown. Use --project to scope to a single
858
+ project's learnings (essential for consultants who share artifacts with
859
+ clients — the universal store mixes all projects together by default).
860
+
861
+ --project NAME Only learnings tagged with this project (case-insensitive)
862
+ --category CAT Only this category (deployment, security, etc.)
863
+ --format json|markdown Output format (default: json)
864
+ --include-universal Also include unscoped learnings (project=undefined)
865
+ alongside the project-filtered ones
866
+
867
+ Cross-client confidentiality: without --project, this exports the FULL store.
868
+ Always use --project NAME when sharing exported learnings with anyone outside
869
+ the owning project's team.`);
870
+ return;
871
+ }
872
+ }
873
+ let all = listLearnings(category);
874
+ if (project) {
875
+ const lower = project.toLowerCase();
876
+ all = all.filter((l) => {
877
+ const matchProject = l.project && l.project.toLowerCase() === lower;
878
+ const matchUniversal = universalToo && !l.project;
879
+ return matchProject || matchUniversal;
880
+ });
881
+ }
882
+ if (format === "markdown") {
883
+ if (all.length === 0) {
884
+ process.stdout.write(`# Learnings export\n\n_No learnings matched the filter._\n`);
885
+ return;
886
+ }
887
+ const grouped = {};
888
+ for (const l of all) {
889
+ (grouped[l.category] ||= []).push(l);
890
+ }
891
+ const scope = project
892
+ ? `project ${project}${universalToo ? " + universal" : ""}`
893
+ : "ALL projects (warning: cross-project IP)";
894
+ process.stdout.write(`# Learnings export — ${scope}\n\n`);
895
+ process.stdout.write(`_Exported ${all.length} learning(s) on ${new Date().toISOString()}_\n\n`);
896
+ for (const cat of Object.keys(grouped).sort()) {
897
+ process.stdout.write(`## ${cat}\n\n`);
898
+ for (const l of grouped[cat]) {
899
+ process.stdout.write(`### ${l.rule}\n\n`);
900
+ if (l.context)
901
+ process.stdout.write(`${l.context}\n\n`);
902
+ if (l.project)
903
+ process.stdout.write(`_Project: ${l.project} Ā· Updated: ${l.updated}_\n\n`);
904
+ }
905
+ }
906
+ return;
907
+ }
908
+ // JSON format
909
+ const payload = {
910
+ version: 1,
911
+ exported_at: new Date().toISOString(),
912
+ scope: project
913
+ ? { project, include_universal: universalToo }
914
+ : { project: null, include_universal: true },
915
+ count: all.length,
916
+ learnings: all,
917
+ };
918
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
919
+ }
920
+ async function cliAuditExport(args) {
921
+ let since;
922
+ let until;
923
+ let format = "jsonl";
924
+ for (let i = 0; i < args.length; i++) {
925
+ const a = args[i];
926
+ if (a === "--since" && args[i + 1]) {
927
+ since = args[++i];
928
+ continue;
929
+ }
930
+ if (a === "--until" && args[i + 1]) {
931
+ until = args[++i];
932
+ continue;
933
+ }
934
+ if (a === "--format" && args[i + 1]) {
935
+ const f = args[++i];
936
+ if (f !== "jsonl" && f !== "csv") {
937
+ console.error(`Unknown format: ${f}. Supported: jsonl, csv.`);
938
+ process.exit(1);
939
+ }
940
+ format = f;
941
+ continue;
942
+ }
943
+ if (a === "-h" || a === "--help") {
944
+ console.log(`Usage: contextengine audit-export [--since ISO_DATE] [--until ISO_DATE] [--format jsonl|csv]\n\nExports the hash-chained audit log from ~/.contextengine/audit.log.\nCompliance use: SOC2 CC7.2, ISO 27001 A.12.4.1.`);
945
+ return;
946
+ }
947
+ }
948
+ let records;
949
+ try {
950
+ records = readAuditLog();
951
+ }
952
+ catch (e) {
953
+ console.error(`Audit log unreadable: ${e instanceof Error ? e.message : String(e)}`);
954
+ process.exit(1);
955
+ }
956
+ const filtered = filterByRange(records, since, until);
957
+ if (format === "csv") {
958
+ process.stdout.write(toCsv(filtered) + "\n");
959
+ }
960
+ else {
961
+ for (const r of filtered)
962
+ process.stdout.write(JSON.stringify(r) + "\n");
963
+ }
964
+ }
965
+ async function cliInstallSkill(args) {
966
+ let scope;
967
+ let force = false;
968
+ for (let i = 0; i < args.length; i++) {
969
+ const a = args[i];
970
+ if (a === "--global") {
971
+ scope = "global";
972
+ continue;
973
+ }
974
+ if (a === "--project") {
975
+ scope = "project";
976
+ continue;
977
+ }
978
+ if (a === "--force" || a === "-f") {
979
+ force = true;
980
+ continue;
981
+ }
982
+ if (a === "-h" || a === "--help") {
983
+ console.log(`Usage: contextengine install-skill [--global | --project] [--force]
984
+
985
+ Copies the bundled OpsContext skill into Claude Code's skills directory so
986
+ Claude Code's native skills system discovers it.
987
+
988
+ --global Install at ~/.claude/skills/opscontext/ (recommended for personal use)
989
+ --project Install at <cwd>/.claude/skills/opscontext/ (per-repo opt-in)
990
+ --force Overwrite an existing installation
991
+
992
+ Default scope: --project if <cwd>/.claude/ exists, otherwise --global.
993
+
994
+ After installation, Claude Code surfaces the skill via its native skills
995
+ loading. MCP tools are discovered the normal way through .vscode/mcp.json
996
+ (or your client's MCP config).`);
997
+ return;
998
+ }
999
+ }
1000
+ const distDir = fileURLToPath(new URL(".", import.meta.url));
1001
+ const bundled = locateBundledSkill(distDir);
1002
+ const result = installSkill(bundled, { scope, force });
1003
+ if (!result.ok) {
1004
+ console.error(result.message);
1005
+ process.exit(1);
1006
+ }
1007
+ console.log(result.message);
1008
+ if (result.alreadyInstalled) {
1009
+ console.log("(Pass --force to overwrite.)");
1010
+ }
1011
+ }
1012
+ async function cliSyncClaudeMd(args) {
1013
+ let targetPath;
1014
+ let dryRun = false;
1015
+ for (let i = 0; i < args.length; i++) {
1016
+ const a = args[i];
1017
+ if (a === "--path" && args[i + 1]) {
1018
+ targetPath = args[++i];
1019
+ continue;
1020
+ }
1021
+ if (a === "--dry-run") {
1022
+ dryRun = true;
1023
+ continue;
1024
+ }
1025
+ if (a === "-h" || a === "--help") {
1026
+ console.log(`Usage: contextengine sync-claude-md [--path CLAUDE.md] [--dry-run]
1027
+
1028
+ Maintains a managed block inside CLAUDE.md with the OpsContext snapshot for
1029
+ this project: top operational rules + active policy gates + recent hook
1030
+ blocks from the audit log.
1031
+
1032
+ The block is delimited by:
1033
+ <!-- BEGIN: managed by OpsContext (...) -->
1034
+ ...
1035
+ <!-- END: managed by OpsContext -->
1036
+
1037
+ Run on every commit (or in a pre-commit hook) to keep the snapshot current.
1038
+ Since Claude Code loads CLAUDE.md natively at every session start, the
1039
+ snapshot reaches the agent's context without any MCP call.
1040
+
1041
+ --path FILE Target CLAUDE.md path (default: <cwd>/CLAUDE.md)
1042
+ --dry-run Print the block to stdout, don't touch the file`);
1043
+ return;
1044
+ }
1045
+ }
1046
+ const cwd = process.cwd();
1047
+ const filePath = targetPath ? resolve(targetPath) : join(cwd, "CLAUDE.md");
1048
+ const projectName = basename(cwd);
1049
+ // Top learnings — scope to this project + universal
1050
+ const allLearnings = listLearnings(undefined, [projectName]);
1051
+ // Sort by updated desc and take 5; map to the slim shape
1052
+ const topLearnings = [...allLearnings]
1053
+ .sort((a, b) => (b.updated > a.updated ? 1 : -1))
1054
+ .slice(0, 5)
1055
+ .map((l) => ({ id: l.id, category: l.category, rule: l.rule, project: l.project }));
1056
+ // Policy summary
1057
+ const policyResult = loadRepoPolicy(cwd);
1058
+ let policySummary = null;
1059
+ if (policyResult && policyResult.ok) {
1060
+ const p = policyResult.policy;
1061
+ policySummary = {
1062
+ secretPatternCount: p.secret_patterns.length,
1063
+ secretPatternIds: p.secret_patterns.map((s) => s.id),
1064
+ docCoverageCount: p.doc_coverage.length,
1065
+ deployVerifyHostCount: p.deploy_verify_hosts.length,
1066
+ bypassTokenCount: p.bypass_tokens.length,
1067
+ };
1068
+ }
1069
+ // Recent hook.block events (last 3)
1070
+ let recentBlocks = [];
1071
+ try {
1072
+ const all = readAuditLog();
1073
+ const blocks = all.filter((r) => r.event === "hook.block");
1074
+ recentBlocks = blocks.slice(-3).reverse().map((r) => ({
1075
+ ts: r.ts,
1076
+ check: String(r.payload.check ?? "unknown"),
1077
+ reason: shortBlockReason(r.payload),
1078
+ }));
1079
+ }
1080
+ catch {
1081
+ // Audit log unreadable or absent — skip silently; managed block omits the section
1082
+ }
1083
+ const block = buildManagedBlock({
1084
+ projectName,
1085
+ topLearnings,
1086
+ policySummary,
1087
+ recentBlocks,
1088
+ generatedAt: new Date().toISOString().slice(0, 19) + "Z",
1089
+ });
1090
+ if (dryRun) {
1091
+ process.stdout.write(block + "\n");
1092
+ return;
1093
+ }
1094
+ const result = syncClaudeMd(filePath, block);
1095
+ console.log(`āœ… ${result.mode} — ${result.filePath} (${result.bytesWritten} bytes)`);
1096
+ }
1097
+ function shortBlockReason(payload) {
1098
+ if (payload.pattern_id)
1099
+ return `secret pattern ${payload.pattern_id} at ${payload.file}:${payload.line}`;
1100
+ if (payload.matched_files) {
1101
+ const files = Array.isArray(payload.matched_files) ? payload.matched_files.join(", ") : "files";
1102
+ return `doc-coverage on ${files} → ${payload.requires_section ?? "?"} (${payload.reason ?? "?"})`;
1103
+ }
1104
+ return JSON.stringify(payload).slice(0, 120);
1105
+ }
1106
+ async function cliHook(args) {
1107
+ const sub = args[0];
1108
+ const jsonMode = process.env.CE_JSON === "1";
1109
+ if (!sub || sub === "-h" || sub === "--help") {
1110
+ console.log(`Usage: contextengine hook <kind>
1111
+
1112
+ Run policy-driven pre-commit checks against the staged git diff.
1113
+
1114
+ Subcommands:
1115
+ secret-scan Apply policy.secret_patterns to added lines. Exit 1 on
1116
+ any blocking violation, 0 otherwise. Warnings print but
1117
+ don't fail.
1118
+ doc-coverage For each policy.doc_coverage rule, check whether the
1119
+ commit touches matching source paths AND the required
1120
+ doc section is staged. Exit 1 on blocking violations.
1121
+
1122
+ Env:
1123
+ CE_JSON=1 Emit one-line JSON per check instead of human-readable
1124
+ output (for CI logs). Exit codes unchanged.
1125
+
1126
+ Reads .contextengine/policy.json from the current git toplevel. If no
1127
+ policy file exists, both checks exit 0 (no-op — the legacy hook layer
1128
+ runs anyway).
1129
+
1130
+ Every blocking violation also appends a hook.block record to the
1131
+ tamper-evident audit log at ~/.contextengine/audit.log.`);
1132
+ return;
1133
+ }
1134
+ // Find repo root via git
1135
+ let repoRoot;
1136
+ try {
1137
+ repoRoot = execSync("git rev-parse --show-toplevel", {
1138
+ encoding: "utf-8",
1139
+ }).trim();
1140
+ }
1141
+ catch {
1142
+ console.error("Error: not inside a git repository.");
1143
+ process.exit(1);
1144
+ }
1145
+ const policyResult = loadRepoPolicy(repoRoot);
1146
+ if (policyResult === null) {
1147
+ // No policy file — no-op (legacy hooks still run inline patterns).
1148
+ if (jsonMode) {
1149
+ process.stdout.write(JSON.stringify({ check: sub, skipped: "no_policy_file" }) + "\n");
1150
+ }
1151
+ return;
1152
+ }
1153
+ if (!policyResult.ok) {
1154
+ console.error(formatValidationErrors(policyResult.errors));
1155
+ console.error(`\nFix .contextengine/policy.json before commits will pass.`);
1156
+ process.exit(1);
1157
+ }
1158
+ const policy = policyResult.policy;
1159
+ let stagedFiles;
1160
+ try {
1161
+ stagedFiles = getStagedFiles(repoRoot);
1162
+ }
1163
+ catch (e) {
1164
+ console.error(`Error reading staged diff: ${e instanceof Error ? e.message : String(e)}`);
1165
+ process.exit(1);
1166
+ }
1167
+ if (stagedFiles.length === 0)
1168
+ return; // nothing to scan
1169
+ if (sub === "secret-scan") {
1170
+ const violations = runSecretScan(policy, stagedFiles);
1171
+ if (jsonMode) {
1172
+ process.stdout.write(formatSecretViolationsJson(violations) + "\n");
1173
+ }
1174
+ else {
1175
+ console.log(formatSecretViolations(violations));
1176
+ }
1177
+ const blocking = violations.filter((v) => v.severity === "block");
1178
+ for (const v of blocking) {
1179
+ safeAppend("hook.block", {
1180
+ check: "secret-scan",
1181
+ pattern_id: v.patternId,
1182
+ file: v.file,
1183
+ line: v.lineNumber,
1184
+ });
1185
+ }
1186
+ if (blocking.length > 0)
1187
+ process.exit(1);
1188
+ return;
1189
+ }
1190
+ if (sub === "doc-coverage") {
1191
+ const violations = runDocCoverage(policy, stagedFiles, repoRoot);
1192
+ if (jsonMode) {
1193
+ process.stdout.write(formatDocCoverageViolationsJson(violations) + "\n");
1194
+ }
1195
+ else {
1196
+ console.log(formatDocCoverageViolations(violations));
1197
+ }
1198
+ const blocking = violations.filter((v) => v.severity === "block");
1199
+ for (const v of blocking) {
1200
+ safeAppend("hook.block", {
1201
+ check: "doc-coverage",
1202
+ source_paths: v.sourcePaths,
1203
+ matched_files: v.matchedFiles,
1204
+ requires_section: v.requiresSection,
1205
+ reason: v.reason,
1206
+ });
1207
+ }
1208
+ if (blocking.length > 0)
1209
+ process.exit(1);
1210
+ return;
1211
+ }
1212
+ console.error(`Unknown hook subcommand: ${sub}. Try 'contextengine hook --help'.`);
1213
+ process.exit(1);
1214
+ }
1215
+ async function cliPolicy(args) {
1216
+ const sub = args[0];
1217
+ if (!sub || sub === "-h" || sub === "--help") {
1218
+ console.log(`Usage: contextengine policy <subcommand>
1219
+
1220
+ Subcommands:
1221
+ validate <file> Validate a policy.json file against the v1 schema.
1222
+ Exit 0 on valid, exit 1 with field-level errors otherwise.
1223
+ show Load and pretty-print the active repo policy
1224
+ (.contextengine/policy.json in the current working tree).
1225
+
1226
+ The policy file is the declarative contract that replaces inline-bash hook
1227
+ logic. Schema fields:
1228
+ - secret_patterns Regex rules for the pre-commit secret scanner
1229
+ - doc_coverage Source-subtree → doc-section coverage requirements
1230
+ - deploy_verify_hosts Hosts that require a verification probe post-push
1231
+ - bypass_tokens Documented escape hatches with reason + TTL
1232
+
1233
+ Enforcement integration is shipping in the next sprint. This release
1234
+ ships the schema + loader + validator + CLI so policies can be authored,
1235
+ reviewed, and validated in PR ahead of the hook wiring.`);
1236
+ return;
1237
+ }
1238
+ if (sub === "validate") {
1239
+ const filePath = args[1];
1240
+ if (!filePath) {
1241
+ console.error("Usage: contextengine policy validate <file>");
1242
+ process.exit(1);
1243
+ }
1244
+ let contents;
1245
+ try {
1246
+ contents = readFileSync(filePath, "utf-8");
1247
+ }
1248
+ catch (e) {
1249
+ console.error(`Cannot read ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
1250
+ process.exit(1);
1251
+ }
1252
+ const result = parsePolicy(contents);
1253
+ if (!result.ok) {
1254
+ console.error(formatValidationErrors(result.errors));
1255
+ process.exit(1);
1256
+ }
1257
+ console.log(`āœ… Policy valid (v${result.policy.version}).`);
1258
+ console.log(formatPolicySummary(result.policy));
1259
+ return;
1260
+ }
1261
+ if (sub === "show") {
1262
+ const cwd = process.cwd();
1263
+ const result = loadRepoPolicy(cwd);
1264
+ if (result === null) {
1265
+ console.error(`No policy found at ${repoPolicyPath(cwd)}.`);
1266
+ console.error(`To create one, write a JSON file with at minimum:`);
1267
+ console.error(` { "version": 1 }`);
1268
+ process.exit(1);
1269
+ }
1270
+ if (!result.ok) {
1271
+ console.error(formatValidationErrors(result.errors));
1272
+ process.exit(1);
1273
+ }
1274
+ console.log(formatPolicySummary(result.policy));
1275
+ return;
1276
+ }
1277
+ console.error(`Unknown subcommand: ${sub}. Try 'contextengine policy --help'.`);
1278
+ process.exit(1);
1279
+ }
1280
+ async function cliAuditVerify() {
1281
+ const report = verifyChain();
1282
+ if (report.ok) {
1283
+ console.log(`āœ… Audit chain verified — ${report.total} record(s), hash chain intact.`);
1284
+ return;
1285
+ }
1286
+ console.error(`āŒ Audit chain BROKEN at index ${report.breakAtIndex} (of ${report.total}).`);
1287
+ console.error(` Reason: ${report.breakReason}`);
1288
+ console.error(`\nA broken chain means the log was either edited after the fact, or a record was`);
1289
+ console.error(`partially written during a crash. For compliance-graded evidence, treat all`);
1290
+ console.error(`records from the break onward as unverified.`);
1291
+ process.exit(2);
1292
+ }
1293
+ async function cliEndSession() {
1294
+ const projectDirs = loadProjectDirs();
1295
+ const checks = [];
1296
+ let passCount = 0;
1297
+ let failCount = 0;
1298
+ checks.push("═══════════════════════════════════════");
1299
+ checks.push(" ContextEngine — End-of-Session Checklist");
1300
+ checks.push("═══════════════════════════════════════\n");
1301
+ // --- Check 1: Uncommitted changes across all repos ---
1302
+ checks.push("## 1. Git Status\n");
1303
+ const reposChecked = new Set();
1304
+ for (const dir of projectDirs) {
1305
+ try {
1306
+ const gitRoot = execSync("git rev-parse --show-toplevel", {
1307
+ cwd: dir.path, encoding: "utf-8", timeout: 5000,
1308
+ }).trim();
1309
+ if (reposChecked.has(gitRoot))
1310
+ continue;
1311
+ reposChecked.add(gitRoot);
1312
+ const status = execSync("git status --porcelain", {
1313
+ cwd: gitRoot, encoding: "utf-8", timeout: 5000,
1314
+ }).trim();
1315
+ const repoName = basename(gitRoot);
1316
+ // Get current branch
1317
+ let branch = "unknown";
1318
+ try {
1319
+ branch = execSync("git branch --show-current", {
1320
+ cwd: gitRoot, encoding: "utf-8", timeout: 5000,
1321
+ }).trim();
1322
+ }
1323
+ catch { /* ignore */ }
1324
+ if (status) {
1325
+ const fileCount = status.split("\n").length;
1326
+ checks.push(`- āŒ FAIL — ${repoName} (${branch}) has ${fileCount} uncommitted file(s)`);
1327
+ const files = status.split("\n").slice(0, 5);
1328
+ for (const f of files) {
1329
+ checks.push(` - ${f.trim()}`);
1330
+ }
1331
+ if (fileCount > 5)
1332
+ checks.push(` - ... and ${fileCount - 5} more`);
1333
+ failCount++;
1334
+ }
1335
+ else {
1336
+ checks.push(`- āœ… PASS — ${repoName} (${branch}) — clean`);
1337
+ passCount++;
1338
+ }
1339
+ }
1340
+ catch {
1341
+ // Not a git repo
1342
+ }
1343
+ }
1344
+ // Also check common doc repos
1345
+ const home = process.env.HOME || "";
1346
+ const extraRepoPaths = [join(home, "FASTPROD")];
1347
+ for (const repoPath of extraRepoPaths) {
1348
+ if (!existsSync(repoPath) || reposChecked.has(repoPath))
1349
+ continue;
1350
+ try {
1351
+ const gitRoot = execSync("git rev-parse --show-toplevel", {
1352
+ cwd: repoPath, encoding: "utf-8", timeout: 5000,
1353
+ }).trim();
1354
+ if (reposChecked.has(gitRoot))
1355
+ continue;
1356
+ reposChecked.add(gitRoot);
1357
+ const status = execSync("git status --porcelain", {
1358
+ cwd: gitRoot, encoding: "utf-8", timeout: 5000,
1359
+ }).trim();
1360
+ const repoName = basename(gitRoot);
1361
+ if (status) {
1362
+ const fileCount = status.split("\n").length;
1363
+ checks.push(`- āŒ FAIL — ${repoName} has ${fileCount} uncommitted file(s)`);
1364
+ failCount++;
1365
+ }
1366
+ else {
1367
+ checks.push(`- āœ… PASS — ${repoName} is clean`);
1368
+ passCount++;
1369
+ }
1370
+ }
1371
+ catch { /* Not a git repo */ }
1372
+ }
1373
+ checks.push("");
1374
+ // --- Check 2: Documentation freshness ---
1375
+ checks.push("## 2. Documentation Freshness\n");
1376
+ const now = Date.now();
1377
+ const SESSION_THRESHOLD_MS = 4 * 60 * 60 * 1000; // 4 hours
1378
+ for (const dir of projectDirs) {
1379
+ const copilotPath = join(dir.path, ".github", "copilot-instructions.md");
1380
+ if (existsSync(copilotPath)) {
1381
+ const stat = statSync(copilotPath);
1382
+ const ageMs = now - stat.mtimeMs;
1383
+ if (ageMs < SESSION_THRESHOLD_MS) {
1384
+ const mins = Math.round(ageMs / 60000);
1385
+ checks.push(`- āœ… PASS — ${dir.name}/copilot-instructions.md updated ${mins}m ago`);
1386
+ passCount++;
1387
+ }
1388
+ else {
1389
+ const hours = Math.round(ageMs / 3600000);
1390
+ checks.push(`- āš ļø STALE — ${dir.name}/copilot-instructions.md last modified ${hours}h ago`);
1391
+ failCount++;
1392
+ }
1393
+ }
1394
+ else {
1395
+ checks.push(`- āŒ MISSING — ${dir.name}/.github/copilot-instructions.md`);
1396
+ failCount++;
1397
+ }
1398
+ // Check SKILLS.md
1399
+ const skillsPath = join(dir.path, "SKILLS.md");
1400
+ if (existsSync(skillsPath)) {
1401
+ const stat = statSync(skillsPath);
1402
+ const ageMs = now - stat.mtimeMs;
1403
+ const hours = Math.round(ageMs / 3600000);
1404
+ if (ageMs < SESSION_THRESHOLD_MS) {
1405
+ checks.push(`- āœ… PASS — ${dir.name}/SKILLS.md updated ${Math.round(ageMs / 60000)}m ago`);
1406
+ passCount++;
1407
+ }
1408
+ else {
1409
+ checks.push(`- āš ļø STALE — ${dir.name}/SKILLS.md last modified ${hours}h ago`);
1410
+ failCount++;
1411
+ }
1412
+ }
1413
+ // Check SCORE.md
1414
+ const scorePath = join(dir.path, "SCORE.md");
1415
+ if (existsSync(scorePath)) {
1416
+ checks.push(`- āœ… EXISTS — ${dir.name}/SCORE.md`);
1417
+ passCount++;
1418
+ }
1419
+ }
1420
+ // Check session doc
1421
+ const sessionDocPath = join(home, "FASTPROD", "docs", "CROWLR_COMPR_APPS_SESSION.md");
1422
+ if (existsSync(sessionDocPath)) {
1423
+ const stat = statSync(sessionDocPath);
1424
+ const ageMs = now - stat.mtimeMs;
1425
+ if (ageMs < SESSION_THRESHOLD_MS) {
1426
+ const mins = Math.round(ageMs / 60000);
1427
+ checks.push(`- āœ… PASS — SESSION.md updated ${mins}m ago`);
1428
+ passCount++;
1429
+ }
1430
+ else {
1431
+ const hours = Math.round(ageMs / 3600000);
1432
+ checks.push(`- āš ļø STALE — SESSION.md last modified ${hours}h ago — append session summary`);
1433
+ failCount++;
1434
+ }
1435
+ }
1436
+ checks.push("");
1437
+ // --- Auto-import learnings from docs before checking stats ---
1438
+ const docSources = loadSources().map((s) => ({ path: s.path, name: s.name }));
1439
+ const autoImport = autoImportFromSources(docSources);
1440
+ if (autoImport.imported > 0) {
1441
+ checks.push(`šŸ“„ Auto-imported ${autoImport.imported} new learnings from ${autoImport.total} doc sources\n`);
1442
+ }
1443
+ // --- Check 3: Learnings Store ---
1444
+ checks.push("## 3. Learnings Store\n");
1445
+ const stats = learningsStats();
1446
+ checks.push(`- šŸ“Š **${stats.total} learnings** across **${Object.keys(stats.categories).length} categories**`);
1447
+ // Show category breakdown
1448
+ const sortedCategories = Object.entries(stats.categories).sort((a, b) => b[1] - a[1]);
1449
+ for (const [cat, count] of sortedCategories) {
1450
+ checks.push(` - ${cat}: ${count}`);
1451
+ }
1452
+ // Show project-scoped count for current workspace
1453
+ const projectNames = projectDirs.map((d) => d.name);
1454
+ const scopedLearnings = listLearnings(undefined, projectNames);
1455
+ const otherCount = stats.total - scopedLearnings.length;
1456
+ checks.push(`- šŸ”’ **${scopedLearnings.length}** visible to current workspace (${otherCount} scoped to other projects)`);
1457
+ passCount++;
1458
+ checks.push("");
1459
+ // --- Check 4: Sessions ---
1460
+ checks.push("## 4. Sessions\n");
1461
+ const sessions = listSessions();
1462
+ if (sessions.length > 0) {
1463
+ checks.push(`- šŸ“ **${sessions.length} saved sessions**`);
1464
+ // Show 3 most recent
1465
+ const recent = sessions.slice(0, 3);
1466
+ for (const s of recent) {
1467
+ const age = Math.round((now - new Date(s.updated).getTime()) / 3600000);
1468
+ checks.push(` - ${s.name} (${s.entries} entries, ${age}h ago)`);
1469
+ }
1470
+ if (sessions.length > 3)
1471
+ checks.push(` - ... and ${sessions.length - 3} more`);
1472
+ passCount++;
1473
+ }
1474
+ else {
1475
+ checks.push(`- āš ļø No sessions saved — run \`save_session\` before ending`);
1476
+ failCount++;
1477
+ }
1478
+ checks.push("");
1479
+ // --- Summary ---
1480
+ checks.push("═══════════════════════════════════════");
1481
+ checks.push("## Summary\n");
1482
+ const total = passCount + failCount;
1483
+ if (failCount === 0) {
1484
+ checks.push(`āœ… ALL CLEAR — ${passCount}/${total} checks passed. Safe to end session.`);
1485
+ }
1486
+ else {
1487
+ checks.push(`āš ļø ${failCount} item(s) need attention — ${passCount}/${total} passed.`);
1488
+ checks.push("");
1489
+ checks.push("Before ending this session:");
1490
+ checks.push("1. Commit and push all uncommitted changes");
1491
+ checks.push("2. Update copilot-instructions.md with new facts");
1492
+ checks.push("3. Save session with `save_session`");
1493
+ checks.push("4. Save learnings with `save_learning` for each reusable pattern");
1494
+ checks.push("5. Run `contextengine end-session` again to verify");
1495
+ }
1496
+ console.log(checks.join("\n"));
1497
+ process.exit(failCount > 0 ? 1 : 0);
1498
+ }
1499
+ async function cliImportLearnings(args) {
1500
+ // import-learnings <file> [-c category] [-p project]
1501
+ let filePath = "";
1502
+ let category = "other";
1503
+ let project;
1504
+ for (let i = 0; i < args.length; i++) {
1505
+ if ((args[i] === "-c" || args[i] === "--category") && args[i + 1]) {
1506
+ category = args[++i];
1507
+ }
1508
+ else if ((args[i] === "-p" || args[i] === "--project") && args[i + 1]) {
1509
+ project = args[++i];
1510
+ }
1511
+ else if (!filePath) {
1512
+ filePath = args[i];
1513
+ }
1514
+ }
1515
+ if (!filePath) {
1516
+ console.error("Usage: contextengine import-learnings <file.md|file.json> [-c category] [-p project]");
1517
+ process.exit(1);
1518
+ }
1519
+ const result = importLearningsFromFile(filePath, category, project);
1520
+ console.log(`\nšŸ“„ Import Results:`);
1521
+ console.log(` Imported: ${result.imported}`);
1522
+ console.log(` Updated: ${result.updated}`);
1523
+ console.log(` Skipped: ${result.skipped}`);
1524
+ if (result.errors.length > 0) {
1525
+ console.log(` Errors:`);
1526
+ for (const err of result.errors) {
1527
+ console.log(` - ${err}`);
1528
+ }
1529
+ }
1530
+ }
1531
+ // ---------------------------------------------------------------------------
1532
+ // CLI: stats — show live session stats from MCP server
1533
+ // ---------------------------------------------------------------------------
1534
+ function cliStats() {
1535
+ const statsFile = join(homedir(), ".contextengine", "session-stats.json");
1536
+ if (!existsSync(statsFile)) {
1537
+ console.log("\nšŸ“Š No active session stats found.");
1538
+ console.log(" Stats are written by the MCP server during active sessions.");
1539
+ console.log(" Start a session with your AI agent to see stats here.\n");
1540
+ return;
1541
+ }
1542
+ try {
1543
+ const raw = readFileSync(statsFile, "utf-8");
1544
+ const stats = JSON.parse(raw);
1545
+ console.log("\nšŸ“Š ContextEngine Session Stats\n");
1546
+ console.log(` ā± Uptime: ${stats.uptimeMinutes ?? 0} min`);
1547
+ console.log(` šŸ”§ Tool calls: ${stats.toolCalls ?? 0}`);
1548
+ console.log(` 🧠 Learnings saved: ${stats.learningsSaved ?? 0}`);
1549
+ console.log(` šŸ” Search recalls: ${stats.searchRecalls ?? 0} (learnings surfaced)`);
1550
+ console.log(` šŸ“‹ Nudges issued: ${stats.nudgesIssued ?? 0}`);
1551
+ console.log(` ā›” Truncations: ${stats.truncations ?? 0}`);
1552
+ console.log(` šŸ’¾ Session saved: ${stats.sessionSaved ? "āœ…" : "āŒ"}`);
1553
+ console.log(` ā± Time saved: ~${stats.timeSavedMinutes ?? 0} min`);
1554
+ console.log(` šŸ• Started: ${stats.startedAt ?? "unknown"}`);
1555
+ console.log(` šŸ”„ Last update: ${stats.updatedAt ?? "unknown"}`);
1556
+ console.log("");
1557
+ }
1558
+ catch {
1559
+ console.error("Error reading session stats.");
1560
+ }
1561
+ }
1562
+ // ---------------------------------------------------------------------------
1563
+ // Main — route to init, CLI subcommand, or MCP server
1564
+ // ---------------------------------------------------------------------------
1565
+ const command = process.argv[2];
1566
+ if (command === "init") {
1567
+ runInit().catch((err) => {
1568
+ console.error("Error:", err);
1569
+ process.exit(1);
1570
+ });
1571
+ }
1572
+ else if (command === "help" || command === "--help" || command === "-h") {
1573
+ console.log(`
1574
+ ContextEngine — queryable knowledge base for AI coding agents
1575
+
1576
+ Usage:
1577
+ contextengine Start MCP server (stdio transport)
1578
+ contextengine init Scaffold project (mcp.json, docs, hooks, config)
1579
+ contextengine search <query> [-n N] Search indexed knowledge (default: top 5)
1580
+ contextengine list-sources Show all indexed sources with chunk counts
1581
+ contextengine list-projects Discover and analyze all projects (Pro)
1582
+ contextengine list-learnings [cat] List all learnings (optional: filter by category)
1583
+ contextengine save-learning <text> -c <category> Save a learning
1584
+ contextengine delete-learning <id> Delete a learning by ID
1585
+ contextengine import-learnings <file> [-c cat] [-p project] Bulk-import learnings
1586
+ contextengine export-learnings [--project NAME] [--category CAT] [--format json|markdown] [--include-universal]
1587
+ Export learnings (scope to one project for safe sharing)
1588
+ contextengine save-session <name> <key> <value> Save session context
1589
+ contextengine load-session <name> Restore session context
1590
+ contextengine list-sessions List all saved sessions
1591
+ contextengine delete-session <name> Delete a saved session
1592
+ contextengine end-session Pre-flight checklist (uncommitted changes, doc freshness)
1593
+ contextengine audit-export [--since DATE] [--until DATE] [--format jsonl|csv]
1594
+ Export hash-chained audit log (SOC2/ISO27001 evidence)
1595
+ contextengine audit-verify Verify audit log chain integrity (tamper detection)
1596
+ contextengine policy <validate|show> [args]
1597
+ Author + validate the declarative .contextengine/policy.json
1598
+ contextengine hook <secret-scan|doc-coverage>
1599
+ Run policy-driven pre-commit checks against staged diff
1600
+ (exit 1 on blocking violation; CE_JSON=1 for CI output)
1601
+ contextengine install-skill [--global | --project] [--force]
1602
+ Install bundled OpsContext skill into Claude Code's skills dir
1603
+ contextengine sync-claude-md [--path CLAUDE.md] [--dry-run]
1604
+ Refresh the OpsContext-managed block in CLAUDE.md
1605
+ (top learnings + policy summary + recent hook blocks)
1606
+ contextengine score [project] [--html] [--no-save] AI-readiness score (Pro, writes SCORE.md)
1607
+ contextengine audit Run compliance audit (Pro)
1608
+ contextengine activate <key> <email> Activate a Pro license
1609
+ contextengine deactivate Remove license and premium modules
1610
+ contextengine stats Show live MCP session stats (value meter)
1611
+ contextengine status Show license status
1612
+ contextengine help Show this message
1613
+
1614
+ Flags:
1615
+ --yes, -y Skip all interactive prompts (auto-accept defaults)
1616
+
1617
+ Examples:
1618
+ npx @compr/contextengine-mcp search "docker nginx"
1619
+ npx @compr/contextengine-mcp score ContextEngine
1620
+ npx @compr/contextengine-mcp score --html
1621
+ npx @compr/contextengine-mcp save-session my-project summary "Deployed v2, fixed auth"
1622
+ npx @compr/contextengine-mcp load-session my-project
1623
+ npx @compr/contextengine-mcp end-session
1624
+ npx @compr/contextengine-mcp import-learnings rules.md -c deployment
1625
+ npx @compr/contextengine-mcp init --yes
1626
+ echo "value" | npx @compr/contextengine-mcp save-session my-project notes --stdin
1627
+
1628
+ npm: https://www.npmjs.com/package/@compr/contextengine-mcp
1629
+ `);
1630
+ }
1631
+ else if (command === "search") {
1632
+ const queryParts = [];
1633
+ let topK = 5;
1634
+ const args = process.argv.slice(3);
1635
+ for (let i = 0; i < args.length; i++) {
1636
+ if ((args[i] === "-n" || args[i] === "--top") && args[i + 1]) {
1637
+ topK = parseInt(args[i + 1], 10) || 5;
1638
+ i++; // skip next
1639
+ }
1640
+ else {
1641
+ queryParts.push(args[i]);
1642
+ }
1643
+ }
1644
+ const query = queryParts.join(" ");
1645
+ if (!query) {
1646
+ console.error("Usage: contextengine search <query> [-n N]");
1647
+ process.exit(1);
1648
+ }
1649
+ cliSearch(query, topK).catch((err) => {
1650
+ console.error("Error:", err);
1651
+ process.exit(1);
1652
+ });
1653
+ }
1654
+ else if (command === "list-sources") {
1655
+ cliListSources().catch((err) => {
1656
+ console.error("Error:", err);
1657
+ process.exit(1);
1658
+ });
1659
+ }
1660
+ else if (command === "list-projects") {
1661
+ cliListProjects().catch((err) => {
1662
+ console.error("Error:", err);
1663
+ process.exit(1);
1664
+ });
1665
+ }
1666
+ else if (command === "list-learnings") {
1667
+ const category = process.argv[3];
1668
+ cliListLearnings(category).catch((err) => {
1669
+ console.error("Error:", err);
1670
+ process.exit(1);
1671
+ });
1672
+ }
1673
+ else if (command === "save-learning") {
1674
+ cliSaveLearning(process.argv.slice(3)).catch((err) => {
1675
+ console.error("Error:", err);
1676
+ process.exit(1);
1677
+ });
1678
+ }
1679
+ else if (command === "delete-learning") {
1680
+ const id = process.argv[3];
1681
+ cliDeleteLearning(id).catch((err) => {
1682
+ console.error("Error:", err);
1683
+ process.exit(1);
1684
+ });
1685
+ }
1686
+ else if (command === "score") {
1687
+ const args = process.argv.slice(3);
1688
+ const htmlFlag = args.includes("--html");
1689
+ const noSaveFlag = args.includes("--no-save");
1690
+ const project = args.filter(a => !a.startsWith("--"))[0];
1691
+ cliScore(project, htmlFlag, !noSaveFlag).catch((err) => {
1692
+ console.error("Error:", err);
1693
+ process.exit(1);
1694
+ });
1695
+ }
1696
+ else if (command === "audit") {
1697
+ cliAudit().catch((err) => {
1698
+ console.error("Error:", err);
1699
+ process.exit(1);
1700
+ });
1701
+ }
1702
+ else if (command === "save-session") {
1703
+ cliSaveSession(process.argv.slice(3)).catch((err) => {
1704
+ console.error("Error:", err);
1705
+ process.exit(1);
1706
+ });
1707
+ }
1708
+ else if (command === "load-session") {
1709
+ cliLoadSession(process.argv[3] || "").catch((err) => {
1710
+ console.error("Error:", err);
1711
+ process.exit(1);
1712
+ });
1713
+ }
1714
+ else if (command === "list-sessions") {
1715
+ cliListSessions().catch((err) => {
1716
+ console.error("Error:", err);
1717
+ process.exit(1);
1718
+ });
1719
+ }
1720
+ else if (command === "delete-session") {
1721
+ cliDeleteSession(process.argv[3] || "").catch((err) => {
1722
+ console.error("Error:", err);
1723
+ process.exit(1);
1724
+ });
1725
+ }
1726
+ else if (command === "export-learnings") {
1727
+ cliExportLearnings(process.argv.slice(3)).catch((err) => {
1728
+ console.error("Error:", err);
1729
+ process.exit(1);
1730
+ });
1731
+ }
1732
+ else if (command === "audit-export") {
1733
+ cliAuditExport(process.argv.slice(3)).catch((err) => {
1734
+ console.error("Error:", err);
1735
+ process.exit(1);
1736
+ });
1737
+ }
1738
+ else if (command === "policy") {
1739
+ cliPolicy(process.argv.slice(3)).catch((err) => {
1740
+ console.error("Error:", err);
1741
+ process.exit(1);
1742
+ });
1743
+ }
1744
+ else if (command === "hook") {
1745
+ cliHook(process.argv.slice(3)).catch((err) => {
1746
+ console.error("Error:", err);
1747
+ process.exit(1);
1748
+ });
1749
+ }
1750
+ else if (command === "install-skill") {
1751
+ cliInstallSkill(process.argv.slice(3)).catch((err) => {
1752
+ console.error("Error:", err);
1753
+ process.exit(1);
1754
+ });
1755
+ }
1756
+ else if (command === "sync-claude-md") {
1757
+ cliSyncClaudeMd(process.argv.slice(3)).catch((err) => {
1758
+ console.error("Error:", err);
1759
+ process.exit(1);
1760
+ });
1761
+ }
1762
+ else if (command === "audit-verify") {
1763
+ cliAuditVerify().catch((err) => {
1764
+ console.error("Error:", err);
1765
+ process.exit(1);
1766
+ });
1767
+ }
1768
+ else if (command === "end-session") {
1769
+ cliEndSession().catch((err) => {
1770
+ console.error("Error:", err);
1771
+ process.exit(1);
1772
+ });
1773
+ }
1774
+ else if (command === "import-learnings") {
1775
+ cliImportLearnings(process.argv.slice(3)).catch((err) => {
1776
+ console.error("Error:", err);
1777
+ process.exit(1);
1778
+ });
1779
+ }
1780
+ else if (command === "activate") {
1781
+ const key = process.argv[3];
1782
+ const email = process.argv[4];
1783
+ if (!key || !email) {
1784
+ console.error("Usage: contextengine activate <license-key> <email>");
1785
+ console.error("Get a license: https://compr.ch/contextengine/pricing");
1786
+ process.exit(1);
1787
+ }
1788
+ activate(key, email).then((result) => {
1789
+ console.log(result.message);
1790
+ process.exit(result.success ? 0 : 1);
1791
+ }).catch((err) => {
1792
+ console.error("Activation error:", err);
1793
+ process.exit(1);
1794
+ });
1795
+ }
1796
+ else if (command === "stats") {
1797
+ cliStats();
1798
+ }
1799
+ else if (command === "deactivate") {
1800
+ deactivate();
1801
+ console.log("āœ… License removed. Premium features disabled.");
1802
+ }
1803
+ else if (command === "status") {
1804
+ const status = getActivationStatus();
1805
+ console.log(`\nšŸ”‘ ContextEngine License Status\n`);
1806
+ console.log(` Activated: ${status.activated ? "āœ… Yes" : "āŒ No"}`);
1807
+ console.log(` Plan: ${status.plan}`);
1808
+ console.log(` Expires: ${status.expiresAt}`);
1809
+ console.log(` Delta version: ${status.deltaVersion}`);
1810
+ console.log(` Machine ID: ${status.machineId}`);
1811
+ if (status.premiumTools.length > 0) {
1812
+ console.log(`\n šŸ”“ Premium tools: ${status.premiumTools.join(", ")}`);
1813
+ }
1814
+ else {
1815
+ console.log(`\n šŸ”’ Premium tools locked. Activate: contextengine activate <key> <email>`);
1816
+ }
1817
+ console.log("");
1818
+ }
1819
+ else {
1820
+ // Default: start MCP server
1821
+ import("./index.js");
1822
+ }
1823
+ //# sourceMappingURL=cli.js.map