@compr/opscontext-mcp 2.1.1 → 2.2.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 +117 -2
- package/README.md +73 -8
- package/dist/activation.js +29 -7
- package/dist/agents.d.ts +27 -1
- package/dist/agents.js +234 -43
- package/dist/audit.d.ts +1 -1
- package/dist/audit.js +133 -14
- package/dist/cli.js +333 -17
- package/dist/community-export.d.ts +84 -0
- package/dist/community-export.js +400 -0
- package/dist/community-sync.d.ts +100 -0
- package/dist/community-sync.js +506 -0
- package/dist/config.js +1 -0
- package/dist/hooks.d.ts +64 -0
- package/dist/hooks.js +240 -0
- package/dist/index.js +35 -2
- package/dist/install-autostart.js +66 -10
- package/dist/policy.d.ts +42 -0
- package/dist/policy.js +40 -0
- package/dist/tools-manifest.d.ts +37 -0
- package/dist/tools-manifest.js +64 -0
- package/package.json +4 -3
- package/skills/opscontext/SKILL.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -503,6 +503,12 @@ async function runInit() {
|
|
|
503
503
|
console.log(" 2. Start a Copilot chat — ContextEngine tools are now available");
|
|
504
504
|
console.log(" 3. Run `contextengine score` to get your AI-readiness baseline");
|
|
505
505
|
console.log("");
|
|
506
|
+
// Browser-capture nudge — fires once, never blocks init
|
|
507
|
+
const extensionSecretPath = join(homedir(), ".contextengine", "extension-secret");
|
|
508
|
+
if (!existsSync(extensionSecretPath)) {
|
|
509
|
+
console.log(" 💡 Next: run `opscontext init-extension-secret` to enable browser capture from Claude.ai and ChatGPT.");
|
|
510
|
+
console.log("");
|
|
511
|
+
}
|
|
506
512
|
}
|
|
507
513
|
finally {
|
|
508
514
|
rl.close();
|
|
@@ -516,13 +522,14 @@ import { ingestSources } from "./ingest.js";
|
|
|
516
522
|
import { searchChunks } from "./search.js";
|
|
517
523
|
import { collectProjectOps, collectSystemOps } from "./collectors.js";
|
|
518
524
|
import { scanCodeDir } from "./code-chunker.js";
|
|
519
|
-
import { listProjects, runComplianceAudit, formatProjectList, formatPlan, scoreProject, formatScoreReport, generateScoreHTML, generateProjectScoreMD, } from "./agents.js";
|
|
525
|
+
import { listProjects, runComplianceAudit, formatProjectList, formatPlan, scoreProject, runScoreCanary, formatScoreReport, generateScoreHTML, generateProjectScoreMD, } from "./agents.js";
|
|
520
526
|
import { listLearnings, learningsToChunks, learningsStats, formatLearnings, saveLearning, deleteLearning, importLearningsFromFile, autoImportFromSources, LEARNING_CATEGORIES, } from "./learnings.js";
|
|
521
527
|
import { saveSession, loadSession, listSessions, deleteSession, formatSession, formatSessionList, } from "./sessions.js";
|
|
522
528
|
import { activate, deactivate, getActivationStatus, gateCheck, } from "./activation.js";
|
|
529
|
+
import { syncTierA, syncTierB, loadCommunityStore, communityRulesToChunks, mergeWithDedup, STORE_PATH as COMMUNITY_STORE_PATH, } from "./community-sync.js";
|
|
523
530
|
import { readAuditLog, verifyChain, filterByRange, toCsv, } from "./audit.js";
|
|
524
531
|
import { loadRepoPolicy, parsePolicy, formatPolicySummary, formatValidationErrors, repoPolicyPath, } from "./policy.js";
|
|
525
|
-
import { getStagedFiles, runSecretScan, runDocCoverage, formatSecretViolations, formatDocCoverageViolations, formatSecretViolationsJson, formatDocCoverageViolationsJson, } from "./hooks.js";
|
|
532
|
+
import { getStagedFiles, runSecretScan, runDocCoverage, runCommitMessageRequired, formatSecretViolations, formatDocCoverageViolations, formatSecretViolationsJson, formatDocCoverageViolationsJson, formatCommitMessageViolations, formatCommitMessageViolationsJson, } from "./hooks.js";
|
|
526
533
|
import { safeAppend } from "./audit.js";
|
|
527
534
|
import { installSkill, locateBundledSkill, buildManagedBlock, syncClaudeMd, } from "./claude-integration.js";
|
|
528
535
|
import { fileURLToPath } from "url";
|
|
@@ -537,7 +544,7 @@ const isNonInteractive = !process.stdin.isTTY || process.argv.includes("--yes")
|
|
|
537
544
|
*/
|
|
538
545
|
async function initEngine() {
|
|
539
546
|
const sources = loadSources();
|
|
540
|
-
|
|
547
|
+
let chunks = ingestSources(sources);
|
|
541
548
|
const config = loadConfig();
|
|
542
549
|
const projectDirs = loadProjectDirs();
|
|
543
550
|
// Collect operational data
|
|
@@ -567,6 +574,16 @@ async function initEngine() {
|
|
|
567
574
|
const projectNames = projectDirs.map((d) => d.name);
|
|
568
575
|
const learningChunks = learningsToChunks(projectNames);
|
|
569
576
|
chunks.push(...learningChunks);
|
|
577
|
+
// Inject community rules (Tier A public + Tier B Pro), deduped against
|
|
578
|
+
// the local Learnings Store so identical content doesn't double-emit.
|
|
579
|
+
// Network is NOT touched here — only the cached store at
|
|
580
|
+
// ~/.contextengine/community-learnings.json is read. Use the
|
|
581
|
+
// `sync-community-rules` subcommand to refresh the cache.
|
|
582
|
+
const communityChunks = communityRulesToChunks();
|
|
583
|
+
if (communityChunks.length > 0) {
|
|
584
|
+
const localChunks = chunks;
|
|
585
|
+
chunks = mergeWithDedup(localChunks, communityChunks);
|
|
586
|
+
}
|
|
570
587
|
return { sources, chunks };
|
|
571
588
|
}
|
|
572
589
|
// ---------------------------------------------------------------------------
|
|
@@ -686,6 +703,19 @@ async function cliScore(project, html = false, save = true) {
|
|
|
686
703
|
console.error(gate);
|
|
687
704
|
process.exit(1);
|
|
688
705
|
}
|
|
706
|
+
// [SCORE-CANARY] — every health signal must read exactly as pinned before we are allowed to
|
|
707
|
+
// write a single SCORE.md. A drifting scorer that silently rewrites 37 reports is the failure
|
|
708
|
+
// this blocks; it does not test for one known bug, it refuses to proceed on ANY deviation.
|
|
709
|
+
const canary = runScoreCanary();
|
|
710
|
+
if (!canary.ok) {
|
|
711
|
+
console.error("\n🚨 Scoring canary FAILED — refusing to write any SCORE.md.\n");
|
|
712
|
+
for (const d of canary.deviations)
|
|
713
|
+
console.error(` • ${d}`);
|
|
714
|
+
console.error(canary.inconclusive
|
|
715
|
+
? "\nThe canary fixture could not be built, so the scorer is unverified. This is an unknown, not a pass.\n"
|
|
716
|
+
: "\nThe scorer no longer behaves as pinned. Fix the deviation or update the pin deliberately.\n");
|
|
717
|
+
process.exit(1);
|
|
718
|
+
}
|
|
689
719
|
const projectDirs = loadProjectDirs();
|
|
690
720
|
let scores;
|
|
691
721
|
if (project) {
|
|
@@ -828,6 +858,9 @@ async function cliExportLearnings(args) {
|
|
|
828
858
|
let category;
|
|
829
859
|
let format = "json";
|
|
830
860
|
let universalToo = false;
|
|
861
|
+
let tier;
|
|
862
|
+
let outputPath;
|
|
863
|
+
let review = false;
|
|
831
864
|
for (let i = 0; i < args.length; i++) {
|
|
832
865
|
const a = args[i];
|
|
833
866
|
if ((a === "--project" || a === "-p") && args[i + 1]) {
|
|
@@ -851,25 +884,114 @@ async function cliExportLearnings(args) {
|
|
|
851
884
|
universalToo = true;
|
|
852
885
|
continue;
|
|
853
886
|
}
|
|
887
|
+
if (a === "--tier" && args[i + 1]) {
|
|
888
|
+
const t = args[++i];
|
|
889
|
+
if (t !== "A" && t !== "B") {
|
|
890
|
+
console.error(`Unknown tier: ${t}. Supported: A, B.`);
|
|
891
|
+
process.exit(1);
|
|
892
|
+
}
|
|
893
|
+
tier = t;
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
if ((a === "--output" || a === "-o") && args[i + 1]) {
|
|
897
|
+
outputPath = args[++i];
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
if (a === "--review") {
|
|
901
|
+
review = true;
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
854
904
|
if (a === "-h" || a === "--help") {
|
|
855
|
-
console.log(`Usage: contextengine export-learnings [--
|
|
905
|
+
console.log(`Usage: contextengine export-learnings [--tier A|B] [--output PATH] [--review]
|
|
906
|
+
[--project NAME] [--category CAT]
|
|
907
|
+
[--format json|markdown] [--include-universal]
|
|
908
|
+
|
|
909
|
+
TWO MODES:
|
|
910
|
+
|
|
911
|
+
1) Legacy raw export — no --tier flag. Writes the raw, unredacted store
|
|
912
|
+
filtered by --project / --category. Use only for personal backups or
|
|
913
|
+
when sharing inside a trusted team. NOT safe for public distribution.
|
|
914
|
+
|
|
915
|
+
2) Tier-aware redacted export — pass --tier A or --tier B.
|
|
916
|
+
|
|
917
|
+
--tier A MIT-publishable subset. Pipeline:
|
|
918
|
+
a) Reuses the secret/PII patterns from
|
|
919
|
+
chrome-extension/src/content/shared/redact.ts
|
|
920
|
+
(cloud-vendor credential shapes, JWT, bearer
|
|
921
|
+
tokens, SSH private-key blocks, generic
|
|
922
|
+
credential-assignment patterns).
|
|
923
|
+
b) Strips PII: emails → [EMAIL], phone digits →
|
|
924
|
+
[PHONE], credit-card-shape → [CC].
|
|
925
|
+
c) Strips personal identifiers ("yannick", "yan",
|
|
926
|
+
"compr.ch/.fr") and project brand names
|
|
927
|
+
(CROWLR / KONIVE / INVOC / PLANK / COMPR / FASTPROD)
|
|
928
|
+
→ [project]. Local /Users/yan/Projects/* paths
|
|
929
|
+
become /workspace/*. api.compr.ch → [SERVER].
|
|
930
|
+
d) Filters to allow-list categories
|
|
931
|
+
(debugging / tooling / git / frontend / testing /
|
|
932
|
+
dependencies / performance / other). Drops
|
|
933
|
+
security / deployment / infrastructure / devops
|
|
934
|
+
unless the entry is tagged 'safe' (manual vet).
|
|
935
|
+
e) Hashes IDs and project names — co-clustering
|
|
936
|
+
survives but local UUIDs and brand names do not.
|
|
937
|
+
Safety contract enforced by the LOCK comment block at
|
|
938
|
+
the top of src/community-export.ts. Do NOT relax the
|
|
939
|
+
redactor without re-running the full test suite.
|
|
940
|
+
|
|
941
|
+
--tier B Full corpus for PRO subscribers (api.compr.ch heartbeat).
|
|
942
|
+
Still redacted for secrets + PII (the redactor handles
|
|
943
|
+
that), but security/deployment learnings are INCLUDED
|
|
944
|
+
and original IDs are preserved (authenticated surface).
|
|
856
945
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
946
|
+
--output PATH Where to write the JSON. Default:
|
|
947
|
+
./learnings-export-tierA.json / -tierB.json.
|
|
948
|
+
--review Open \$EDITOR (or vi) on the export JSON for a
|
|
949
|
+
manual trim before final write. Skipped if stdin
|
|
950
|
+
is not a TTY.
|
|
860
951
|
|
|
861
|
-
|
|
952
|
+
Legacy flags (no --tier):
|
|
953
|
+
--project NAME Only learnings tagged with this project
|
|
862
954
|
--category CAT Only this category (deployment, security, etc.)
|
|
863
955
|
--format json|markdown Output format (default: json)
|
|
864
|
-
--include-universal Also include unscoped learnings
|
|
865
|
-
alongside the project-filtered ones
|
|
956
|
+
--include-universal Also include unscoped learnings
|
|
866
957
|
|
|
867
|
-
Cross-client confidentiality: without --project, this
|
|
868
|
-
Always use --
|
|
869
|
-
|
|
958
|
+
Cross-client confidentiality: without --tier and without --project, this
|
|
959
|
+
exports the FULL raw store. Always use --tier A for public sharing, or
|
|
960
|
+
--project NAME for trusted-team sharing inside one engagement.`);
|
|
870
961
|
return;
|
|
871
962
|
}
|
|
872
963
|
}
|
|
964
|
+
// Tier-aware sanitized export path.
|
|
965
|
+
if (tier) {
|
|
966
|
+
const { exportLearnings, reviewLoop } = await import("./community-export.js");
|
|
967
|
+
const out = outputPath || join(process.cwd(), `learnings-export-tier${tier}.json`);
|
|
968
|
+
let result = exportLearnings({ tier, outputPath: out, review });
|
|
969
|
+
if (review) {
|
|
970
|
+
const edited = await reviewLoop(result.rules);
|
|
971
|
+
const payload = {
|
|
972
|
+
version: 1,
|
|
973
|
+
tier,
|
|
974
|
+
generatedAt: new Date().toISOString(),
|
|
975
|
+
count: edited.length,
|
|
976
|
+
dropped: result.dropped + (result.count - edited.length),
|
|
977
|
+
rules: edited,
|
|
978
|
+
};
|
|
979
|
+
writeFileSync(out, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
980
|
+
result = { ...result, count: edited.length, rules: edited };
|
|
981
|
+
}
|
|
982
|
+
console.log(`✅ Tier ${tier} export written to ${out}`);
|
|
983
|
+
console.log(` Included: ${result.count}`);
|
|
984
|
+
console.log(` Dropped: ${result.dropped}`);
|
|
985
|
+
if (tier === "A") {
|
|
986
|
+
console.log(` Note: Tier A is MIT-publishable. Safety guarantees enforced by`);
|
|
987
|
+
console.log(` the LOCK [COMMUNITY-EXPORT-SAFETY] block in src/community-export.ts.`);
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
console.log(` Note: Tier B is PRO-only. Distribute via the api.compr.ch`);
|
|
991
|
+
console.log(` license heartbeat, never via a public mirror.`);
|
|
992
|
+
}
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
873
995
|
let all = listLearnings(category);
|
|
874
996
|
if (project) {
|
|
875
997
|
const lower = project.toLowerCase();
|
|
@@ -941,7 +1063,7 @@ async function cliAuditExport(args) {
|
|
|
941
1063
|
continue;
|
|
942
1064
|
}
|
|
943
1065
|
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:
|
|
1066
|
+
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: produces evidence aligned with SOC 2 CC7.2 + ISO 27001 A.12.4.1\n(evidence artifacts — OpsContext is not itself certified; see docs/compliance/).`);
|
|
945
1067
|
return;
|
|
946
1068
|
}
|
|
947
1069
|
}
|
|
@@ -1118,6 +1240,23 @@ Subcommands:
|
|
|
1118
1240
|
doc-coverage For each policy.doc_coverage rule, check whether the
|
|
1119
1241
|
commit touches matching source paths AND the required
|
|
1120
1242
|
doc section is staged. Exit 1 on blocking violations.
|
|
1243
|
+
commit-message-required [MSG_FILE]
|
|
1244
|
+
For each policy.commit_message_required rule, check
|
|
1245
|
+
whether the commit touches matching source paths AND
|
|
1246
|
+
the commit message matches the required pattern. Exit 1
|
|
1247
|
+
on blocking violations. Bypass: include
|
|
1248
|
+
\`--skip-multi-agent-reason: <reason>\` in the commit
|
|
1249
|
+
body (≥ 20 chars, must contain whitespace, must be at
|
|
1250
|
+
the start of its own line) — bypass is recorded as a
|
|
1251
|
+
policy.skipped audit event (no exit-1).
|
|
1252
|
+
|
|
1253
|
+
MSG_FILE: path to the commit message file. Passed by the
|
|
1254
|
+
commit-msg git hook as \$1. Lookup order:
|
|
1255
|
+
1. positional arg (preferred, what commit-msg passes)
|
|
1256
|
+
2. COMMIT_MSG_FILE env (test injection)
|
|
1257
|
+
3. .git/COMMIT_EDITMSG (legacy fallback — note this is
|
|
1258
|
+
UNRELIABLE from pre-commit; use the commit-msg
|
|
1259
|
+
hook lifecycle).
|
|
1121
1260
|
|
|
1122
1261
|
Env:
|
|
1123
1262
|
CE_JSON=1 Emit one-line JSON per check instead of human-readable
|
|
@@ -1209,6 +1348,69 @@ tamper-evident audit log at ~/.contextengine/audit.log.`);
|
|
|
1209
1348
|
process.exit(1);
|
|
1210
1349
|
return;
|
|
1211
1350
|
}
|
|
1351
|
+
if (sub === "commit-message-required") {
|
|
1352
|
+
// Locate the commit-message file. Order:
|
|
1353
|
+
// 1. CLI positional arg (`contextengine hook commit-message-required
|
|
1354
|
+
// <path>`) — this is what the commit-msg git hook passes via $1.
|
|
1355
|
+
// Verifier P0 fix (2026-06-26): the previous implementation read
|
|
1356
|
+
// .git/COMMIT_EDITMSG from inside the PRE-COMMIT hook, but git
|
|
1357
|
+
// does not populate that file until AFTER pre-commit returns.
|
|
1358
|
+
// The check now runs from commit-msg, which gets the actual
|
|
1359
|
+
// message file path as its first argument.
|
|
1360
|
+
// 2. COMMIT_MSG_FILE env (for test injection / scripted callers).
|
|
1361
|
+
// 3. .git/COMMIT_EDITMSG in the repo root — backward-compat
|
|
1362
|
+
// fallback ONLY for legacy invocations. Will be empty / stale
|
|
1363
|
+
// when called from pre-commit; that's the documented footgun
|
|
1364
|
+
// this fix retires.
|
|
1365
|
+
const commitMsgFile = args[1] ||
|
|
1366
|
+
process.env.COMMIT_MSG_FILE ||
|
|
1367
|
+
join(repoRoot, ".git", "COMMIT_EDITMSG");
|
|
1368
|
+
let commitMessage = "";
|
|
1369
|
+
if (existsSync(commitMsgFile)) {
|
|
1370
|
+
try {
|
|
1371
|
+
commitMessage = readFileSync(commitMsgFile, "utf-8");
|
|
1372
|
+
}
|
|
1373
|
+
catch {
|
|
1374
|
+
// Fall through — empty message will fail any rule that fires,
|
|
1375
|
+
// surfacing the description to the user.
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
const violations = runCommitMessageRequired(policy, stagedFiles, commitMessage);
|
|
1379
|
+
if (jsonMode) {
|
|
1380
|
+
process.stdout.write(formatCommitMessageViolationsJson(violations) + "\n");
|
|
1381
|
+
}
|
|
1382
|
+
else {
|
|
1383
|
+
console.log(formatCommitMessageViolations(violations));
|
|
1384
|
+
}
|
|
1385
|
+
let exitBlock = false;
|
|
1386
|
+
for (const v of violations) {
|
|
1387
|
+
if (v.kind === "bypass") {
|
|
1388
|
+
// Bypass IS the auditable opt-out. Record the reason; let the
|
|
1389
|
+
// commit proceed.
|
|
1390
|
+
safeAppend("policy.skipped", {
|
|
1391
|
+
check: "commit-message-required",
|
|
1392
|
+
rule_id: v.ruleId,
|
|
1393
|
+
matched_files: v.matchedFiles,
|
|
1394
|
+
pattern: v.pattern,
|
|
1395
|
+
bypass_reason: v.bypassReason,
|
|
1396
|
+
});
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
// missing-pattern
|
|
1400
|
+
safeAppend("hook.block", {
|
|
1401
|
+
check: "commit-message-required",
|
|
1402
|
+
rule_id: v.ruleId,
|
|
1403
|
+
matched_files: v.matchedFiles,
|
|
1404
|
+
pattern: v.pattern,
|
|
1405
|
+
reason: "commit-message-pattern-not-matched",
|
|
1406
|
+
});
|
|
1407
|
+
if (v.severity === "block")
|
|
1408
|
+
exitBlock = true;
|
|
1409
|
+
}
|
|
1410
|
+
if (exitBlock)
|
|
1411
|
+
process.exit(1);
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1212
1414
|
console.error(`Unknown hook subcommand: ${sub}. Try 'contextengine hook --help'.`);
|
|
1213
1415
|
process.exit(1);
|
|
1214
1416
|
}
|
|
@@ -1716,6 +1918,107 @@ async function cliImportLearnings(args) {
|
|
|
1716
1918
|
}
|
|
1717
1919
|
}
|
|
1718
1920
|
// ---------------------------------------------------------------------------
|
|
1921
|
+
// CLI: sync-community-rules — fetch + cache community learnings
|
|
1922
|
+
// ---------------------------------------------------------------------------
|
|
1923
|
+
async function cliSyncCommunityRules(args) {
|
|
1924
|
+
let force = false;
|
|
1925
|
+
let tier = "all";
|
|
1926
|
+
for (let i = 0; i < args.length; i++) {
|
|
1927
|
+
const a = args[i];
|
|
1928
|
+
if (a === "--force" || a === "-f") {
|
|
1929
|
+
force = true;
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
if (a === "--tier" && args[i + 1]) {
|
|
1933
|
+
const t = args[++i];
|
|
1934
|
+
if (t !== "A" && t !== "B" && t !== "all") {
|
|
1935
|
+
console.error(`Unknown tier: ${t}. Try A | B | all.`);
|
|
1936
|
+
process.exit(1);
|
|
1937
|
+
}
|
|
1938
|
+
tier = t;
|
|
1939
|
+
continue;
|
|
1940
|
+
}
|
|
1941
|
+
if (a === "-h" || a === "--help") {
|
|
1942
|
+
console.log(`Usage: opscontext sync-community-rules [--force] [--tier A|B|all]
|
|
1943
|
+
|
|
1944
|
+
Fetches community-contributed learnings into the local cache at
|
|
1945
|
+
${COMMUNITY_STORE_PATH}.
|
|
1946
|
+
|
|
1947
|
+
Two tiers:
|
|
1948
|
+
Tier A (public) raw.githubusercontent.com — no license required
|
|
1949
|
+
Tier B (pro) api.compr.ch — requires an activated Pro license
|
|
1950
|
+
|
|
1951
|
+
Behavior:
|
|
1952
|
+
- Daily run recommended. Cron / launchd / periodic CI all fine — the
|
|
1953
|
+
sync is idempotent and ETag-guarded (304 = cheap no-op).
|
|
1954
|
+
- Network failures NEVER crash the search engine. On any HTTP error
|
|
1955
|
+
(timeout, DNS, 5xx, malformed JSON), the cached store is preserved
|
|
1956
|
+
and search continues to work offline.
|
|
1957
|
+
- Tier B is best-effort: with no license loaded (free tier) it is
|
|
1958
|
+
skipped silently with a note on stderr. On HTTP 401 / 403 the
|
|
1959
|
+
error is logged but no exception is thrown — your existing search
|
|
1960
|
+
keeps working.
|
|
1961
|
+
- Tier B responses are Ed25519-signed; an invalid signature causes
|
|
1962
|
+
the fetched payload to be discarded.
|
|
1963
|
+
|
|
1964
|
+
Flags:
|
|
1965
|
+
--force, -f Bypass the ETag cache; always re-fetch.
|
|
1966
|
+
--tier A|B|all Restrict the sync to one tier. Default: all.
|
|
1967
|
+
|
|
1968
|
+
After sync, the new rules are picked up automatically by search_context
|
|
1969
|
+
the next time the MCP server (re)indexes. Restart the server or run
|
|
1970
|
+
'opscontext reindex' to force an immediate pickup.`);
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
console.log(`\n🌐 Sync community rules (tier=${tier}, force=${force})\n`);
|
|
1975
|
+
if (tier === "A" || tier === "all") {
|
|
1976
|
+
const r = await syncTierA({ force });
|
|
1977
|
+
if (r.cached) {
|
|
1978
|
+
console.log(` Tier A — cached (no changes since last fetch)`);
|
|
1979
|
+
}
|
|
1980
|
+
else {
|
|
1981
|
+
console.log(` Tier A — fetched ${r.fetched} rule(s)`);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
if (tier === "B" || tier === "all") {
|
|
1985
|
+
// Resolve license through the activation module to keep auth shape
|
|
1986
|
+
// identical to /heartbeat — same machineId, same key.
|
|
1987
|
+
const licenseFile = join(homedir(), ".contextengine", "license.json");
|
|
1988
|
+
let token = null;
|
|
1989
|
+
if (existsSync(licenseFile)) {
|
|
1990
|
+
try {
|
|
1991
|
+
const data = JSON.parse(readFileSync(licenseFile, "utf-8"));
|
|
1992
|
+
token = typeof data.key === "string" ? data.key : null;
|
|
1993
|
+
}
|
|
1994
|
+
catch { /* malformed — treat as no license */ }
|
|
1995
|
+
}
|
|
1996
|
+
if (!token) {
|
|
1997
|
+
if (tier === "B") {
|
|
1998
|
+
console.error(` Tier B — no license loaded. Activate with: opscontext activate <key> <email>`);
|
|
1999
|
+
}
|
|
2000
|
+
else {
|
|
2001
|
+
console.log(` Tier B — skipped (no license loaded)`);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
else {
|
|
2005
|
+
const r = await syncTierB(token, { force });
|
|
2006
|
+
if (r.cached) {
|
|
2007
|
+
console.log(` Tier B — cached (no changes since last fetch)`);
|
|
2008
|
+
}
|
|
2009
|
+
else if (r.fetched === 0) {
|
|
2010
|
+
console.log(` Tier B — 0 rules (auth rejected or empty payload — see stderr)`);
|
|
2011
|
+
}
|
|
2012
|
+
else {
|
|
2013
|
+
console.log(` Tier B — fetched ${r.fetched} rule(s)`);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
const store = loadCommunityStore();
|
|
2018
|
+
console.log(`\n Store now holds ${store.rules.length} total community rule(s).`);
|
|
2019
|
+
console.log(` Path: ${COMMUNITY_STORE_PATH}\n`);
|
|
2020
|
+
}
|
|
2021
|
+
// ---------------------------------------------------------------------------
|
|
1719
2022
|
// CLI: stats — show live session stats from MCP server
|
|
1720
2023
|
// ---------------------------------------------------------------------------
|
|
1721
2024
|
function cliStats() {
|
|
@@ -1770,15 +2073,18 @@ Usage:
|
|
|
1770
2073
|
contextengine save-learning <text> -c <category> Save a learning
|
|
1771
2074
|
contextengine delete-learning <id> Delete a learning by ID
|
|
1772
2075
|
contextengine import-learnings <file> [-c cat] [-p project] Bulk-import learnings
|
|
1773
|
-
contextengine export-learnings [--project NAME] [--category CAT] [--format json|markdown] [--include-universal]
|
|
1774
|
-
Export learnings
|
|
2076
|
+
contextengine export-learnings [--tier A|B] [--output PATH] [--review] [--project NAME] [--category CAT] [--format json|markdown] [--include-universal]
|
|
2077
|
+
Export learnings. --tier A → MIT-publishable redacted subset (LOCKed safety pipeline).
|
|
2078
|
+
--tier B → full PRO corpus (still redacted; original IDs preserved).
|
|
2079
|
+
(no --tier) → legacy raw export, NOT safe for public distribution.
|
|
1775
2080
|
contextengine save-session <name> <key> <value> Save session context
|
|
1776
2081
|
contextengine load-session <name> Restore session context
|
|
1777
2082
|
contextengine list-sessions List all saved sessions
|
|
1778
2083
|
contextengine delete-session <name> Delete a saved session
|
|
1779
2084
|
contextengine end-session Pre-flight checklist (uncommitted changes, doc freshness)
|
|
1780
2085
|
contextengine audit-export [--since DATE] [--until DATE] [--format jsonl|csv]
|
|
1781
|
-
Export hash-chained audit log (
|
|
2086
|
+
Export hash-chained audit log (evidence aligned with
|
|
2087
|
+
SOC 2 CC7.2 + ISO 27001 A.12.4.1 — not a certification)
|
|
1782
2088
|
contextengine audit-verify Verify audit log chain integrity (tamper detection)
|
|
1783
2089
|
contextengine policy <validate|show> [args]
|
|
1784
2090
|
Author + validate the declarative .contextengine/policy.json
|
|
@@ -1801,6 +2107,10 @@ Usage:
|
|
|
1801
2107
|
contextengine sync-claude-md [--path CLAUDE.md] [--dry-run]
|
|
1802
2108
|
Refresh the OpsContext-managed block in CLAUDE.md
|
|
1803
2109
|
(top learnings + policy summary + recent hook blocks)
|
|
2110
|
+
contextengine sync-community-rules [--force] [--tier A|B|all]
|
|
2111
|
+
Fetch community-contributed learnings (Tier A = GitHub
|
|
2112
|
+
public, Tier B = api.compr.ch Pro). Daily run recommended.
|
|
2113
|
+
Network failures fall back to cached store.
|
|
1804
2114
|
contextengine score [project] [--html] [--no-save] AI-readiness score (Pro, writes SCORE.md)
|
|
1805
2115
|
contextengine audit Run compliance audit (Pro)
|
|
1806
2116
|
contextengine activate <key> <email> Activate a Pro license
|
|
@@ -2039,6 +2349,12 @@ else if (command === "uninstall-claude-hook") {
|
|
|
2039
2349
|
process.exit(1);
|
|
2040
2350
|
});
|
|
2041
2351
|
}
|
|
2352
|
+
else if (command === "sync-community-rules") {
|
|
2353
|
+
cliSyncCommunityRules(process.argv.slice(3)).catch((err) => {
|
|
2354
|
+
console.error("Error:", err);
|
|
2355
|
+
process.exit(1);
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2042
2358
|
else if (command === "stats") {
|
|
2043
2359
|
cliStats();
|
|
2044
2360
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An ExportedRule is the sanitized, publishable shape of a Learning.
|
|
3
|
+
*
|
|
4
|
+
* Differences from Learning:
|
|
5
|
+
* - `id` is NOT the user's local UUID. For Tier A it's sha256(originalId, salt)
|
|
6
|
+
* so it's stable across exports but does not link back to the user's store.
|
|
7
|
+
* For Tier B it can preserve the original id (PRO subscribers are
|
|
8
|
+
* authenticated; not a public artifact).
|
|
9
|
+
* - `rule` and `context` are passed through `redactRule` — secrets, PII, and
|
|
10
|
+
* personal/project identifiers replaced with token markers.
|
|
11
|
+
* - `project` is a sha256 prefix when present — co-clustering survives
|
|
12
|
+
* ("these 3 learnings came from the same project") but the human-readable
|
|
13
|
+
* brand is gone.
|
|
14
|
+
* - `tags` are preserved verbatim (they're already low-entropy and useful
|
|
15
|
+
* for the recipient agent's relevance ranking).
|
|
16
|
+
* - `created` / `updated` are intentionally OMITTED — timestamps narrow the
|
|
17
|
+
* anonymity set, and downstream consumers don't need them.
|
|
18
|
+
*/
|
|
19
|
+
export interface ExportedRule {
|
|
20
|
+
id: string;
|
|
21
|
+
category: string;
|
|
22
|
+
rule: string;
|
|
23
|
+
context: string;
|
|
24
|
+
project?: string;
|
|
25
|
+
tags: string[];
|
|
26
|
+
}
|
|
27
|
+
export interface ExportResult {
|
|
28
|
+
tier: "A" | "B";
|
|
29
|
+
outputPath: string;
|
|
30
|
+
count: number;
|
|
31
|
+
dropped: number;
|
|
32
|
+
rules: ExportedRule[];
|
|
33
|
+
}
|
|
34
|
+
export interface ExportOptions {
|
|
35
|
+
tier: "A" | "B";
|
|
36
|
+
outputPath: string;
|
|
37
|
+
review?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Run the full redaction pipeline on a single string.
|
|
41
|
+
*
|
|
42
|
+
* Returns the redacted text, or an empty string if the input is too short /
|
|
43
|
+
* empty / one-word (callers should treat empty-return as "drop this rule").
|
|
44
|
+
*
|
|
45
|
+
* Deterministic: identical input → identical output, no timestamps, no
|
|
46
|
+
* randomness.
|
|
47
|
+
*/
|
|
48
|
+
export declare function redactRule(text: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* Hash a local learning ID into a stable, public-safe Tier-A ID.
|
|
51
|
+
*
|
|
52
|
+
* Deterministic: same input → same hash → idempotent re-exports.
|
|
53
|
+
* Doesn't reveal the local UUID format (e.g. timestamp-based prefix).
|
|
54
|
+
*/
|
|
55
|
+
declare function hashPublicId(localId: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Hash a project name into a short prefix. Co-clustering survives (same
|
|
58
|
+
* project name → same prefix) but the brand is gone.
|
|
59
|
+
*/
|
|
60
|
+
declare function hashProject(name: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* Build the sanitized export for the given tier and write it to disk.
|
|
63
|
+
*
|
|
64
|
+
* Tier A: MIT-publishable. Filters to allow-list categories + 'safe' tag.
|
|
65
|
+
* IDs are sha256(localId)-derived (stable but not traceable).
|
|
66
|
+
* Tier B: PRO-only. Full corpus (still redacted for secrets/PII). IDs are
|
|
67
|
+
* the original UUIDs (PRO users are authenticated).
|
|
68
|
+
*/
|
|
69
|
+
export declare function exportLearnings(opts: ExportOptions): ExportResult;
|
|
70
|
+
/**
|
|
71
|
+
* Open the export in $EDITOR (or vi) for a manual review pass. The user can
|
|
72
|
+
* delete entries or hand-edit text; the parsed JSON is returned. In a
|
|
73
|
+
* non-interactive shell (no TTY) the rules are returned unchanged.
|
|
74
|
+
*/
|
|
75
|
+
export declare function reviewLoop(rules: ExportedRule[]): Promise<ExportedRule[]>;
|
|
76
|
+
export declare const __testing: {
|
|
77
|
+
hashPublicId: typeof hashPublicId;
|
|
78
|
+
hashProject: typeof hashProject;
|
|
79
|
+
PUBLIC_ID_SALT: string;
|
|
80
|
+
TIER_A_ALLOWED_CATEGORIES: Set<string>;
|
|
81
|
+
TIER_A_DENIED_CATEGORIES: Set<string>;
|
|
82
|
+
};
|
|
83
|
+
export {};
|
|
84
|
+
//# sourceMappingURL=community-export.d.ts.map
|