@compr/opscontext-mcp 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +313 -0
- package/LICENSE +83 -0
- package/README.md +470 -0
- package/defaults/learnings.json +146 -0
- package/dist/activation.d.ts +48 -0
- package/dist/activation.js +377 -0
- package/dist/adapters.d.ts +101 -0
- package/dist/adapters.js +171 -0
- package/dist/agents.d.ts +137 -0
- package/dist/agents.js +1638 -0
- package/dist/audit.d.ts +23 -0
- package/dist/audit.js +163 -0
- package/dist/cache.d.ts +15 -0
- package/dist/cache.js +117 -0
- package/dist/claude-integration.d.ts +95 -0
- package/dist/claude-integration.js +247 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +1823 -0
- package/dist/code-chunker.d.ts +12 -0
- package/dist/code-chunker.js +270 -0
- package/dist/collectors.d.ts +63 -0
- package/dist/collectors.js +617 -0
- package/dist/config.d.ts +73 -0
- package/dist/config.js +239 -0
- package/dist/embeddings.d.ts +36 -0
- package/dist/embeddings.js +124 -0
- package/dist/firewall.d.ts +133 -0
- package/dist/firewall.js +631 -0
- package/dist/hooks.d.ts +76 -0
- package/dist/hooks.js +313 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1081 -0
- package/dist/ingest.d.ts +32 -0
- package/dist/ingest.js +162 -0
- package/dist/learnings.d.ts +108 -0
- package/dist/learnings.js +714 -0
- package/dist/license-sig.d.ts +47 -0
- package/dist/license-sig.js +104 -0
- package/dist/policy.d.ts +131 -0
- package/dist/policy.js +182 -0
- package/dist/search.d.ts +11 -0
- package/dist/search.js +99 -0
- package/dist/sessions.d.ts +46 -0
- package/dist/sessions.js +153 -0
- package/examples/adapters/notion-adapter.js +108 -0
- package/examples/adapters/rss-adapter.js +76 -0
- package/package.json +87 -0
- package/skills/opscontext/SKILL.md +260 -0
package/dist/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
|