@compr/opscontext-mcp 2.1.1 → 2.1.3
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/audit.d.ts +1 -1
- package/dist/audit.js +133 -14
- package/dist/cli.js +319 -16
- 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/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();
|
|
@@ -520,9 +526,10 @@ import { listProjects, runComplianceAudit, formatProjectList, formatPlan, scoreP
|
|
|
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
|
// ---------------------------------------------------------------------------
|
|
@@ -828,6 +845,9 @@ async function cliExportLearnings(args) {
|
|
|
828
845
|
let category;
|
|
829
846
|
let format = "json";
|
|
830
847
|
let universalToo = false;
|
|
848
|
+
let tier;
|
|
849
|
+
let outputPath;
|
|
850
|
+
let review = false;
|
|
831
851
|
for (let i = 0; i < args.length; i++) {
|
|
832
852
|
const a = args[i];
|
|
833
853
|
if ((a === "--project" || a === "-p") && args[i + 1]) {
|
|
@@ -851,25 +871,114 @@ async function cliExportLearnings(args) {
|
|
|
851
871
|
universalToo = true;
|
|
852
872
|
continue;
|
|
853
873
|
}
|
|
874
|
+
if (a === "--tier" && args[i + 1]) {
|
|
875
|
+
const t = args[++i];
|
|
876
|
+
if (t !== "A" && t !== "B") {
|
|
877
|
+
console.error(`Unknown tier: ${t}. Supported: A, B.`);
|
|
878
|
+
process.exit(1);
|
|
879
|
+
}
|
|
880
|
+
tier = t;
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
if ((a === "--output" || a === "-o") && args[i + 1]) {
|
|
884
|
+
outputPath = args[++i];
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
if (a === "--review") {
|
|
888
|
+
review = true;
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
854
891
|
if (a === "-h" || a === "--help") {
|
|
855
|
-
console.log(`Usage: contextengine export-learnings [--
|
|
892
|
+
console.log(`Usage: contextengine export-learnings [--tier A|B] [--output PATH] [--review]
|
|
893
|
+
[--project NAME] [--category CAT]
|
|
894
|
+
[--format json|markdown] [--include-universal]
|
|
895
|
+
|
|
896
|
+
TWO MODES:
|
|
856
897
|
|
|
857
|
-
|
|
858
|
-
project
|
|
859
|
-
|
|
898
|
+
1) Legacy raw export — no --tier flag. Writes the raw, unredacted store
|
|
899
|
+
filtered by --project / --category. Use only for personal backups or
|
|
900
|
+
when sharing inside a trusted team. NOT safe for public distribution.
|
|
860
901
|
|
|
861
|
-
|
|
902
|
+
2) Tier-aware redacted export — pass --tier A or --tier B.
|
|
903
|
+
|
|
904
|
+
--tier A MIT-publishable subset. Pipeline:
|
|
905
|
+
a) Reuses the secret/PII patterns from
|
|
906
|
+
chrome-extension/src/content/shared/redact.ts
|
|
907
|
+
(cloud-vendor credential shapes, JWT, bearer
|
|
908
|
+
tokens, SSH private-key blocks, generic
|
|
909
|
+
credential-assignment patterns).
|
|
910
|
+
b) Strips PII: emails → [EMAIL], phone digits →
|
|
911
|
+
[PHONE], credit-card-shape → [CC].
|
|
912
|
+
c) Strips personal identifiers ("yannick", "yan",
|
|
913
|
+
"compr.ch/.fr") and project brand names
|
|
914
|
+
(CROWLR / KONIVE / INVOC / PLANK / COMPR / FASTPROD)
|
|
915
|
+
→ [project]. Local /Users/yan/Projects/* paths
|
|
916
|
+
become /workspace/*. api.compr.ch → [SERVER].
|
|
917
|
+
d) Filters to allow-list categories
|
|
918
|
+
(debugging / tooling / git / frontend / testing /
|
|
919
|
+
dependencies / performance / other). Drops
|
|
920
|
+
security / deployment / infrastructure / devops
|
|
921
|
+
unless the entry is tagged 'safe' (manual vet).
|
|
922
|
+
e) Hashes IDs and project names — co-clustering
|
|
923
|
+
survives but local UUIDs and brand names do not.
|
|
924
|
+
Safety contract enforced by the LOCK comment block at
|
|
925
|
+
the top of src/community-export.ts. Do NOT relax the
|
|
926
|
+
redactor without re-running the full test suite.
|
|
927
|
+
|
|
928
|
+
--tier B Full corpus for PRO subscribers (api.compr.ch heartbeat).
|
|
929
|
+
Still redacted for secrets + PII (the redactor handles
|
|
930
|
+
that), but security/deployment learnings are INCLUDED
|
|
931
|
+
and original IDs are preserved (authenticated surface).
|
|
932
|
+
|
|
933
|
+
--output PATH Where to write the JSON. Default:
|
|
934
|
+
./learnings-export-tierA.json / -tierB.json.
|
|
935
|
+
--review Open \$EDITOR (or vi) on the export JSON for a
|
|
936
|
+
manual trim before final write. Skipped if stdin
|
|
937
|
+
is not a TTY.
|
|
938
|
+
|
|
939
|
+
Legacy flags (no --tier):
|
|
940
|
+
--project NAME Only learnings tagged with this project
|
|
862
941
|
--category CAT Only this category (deployment, security, etc.)
|
|
863
942
|
--format json|markdown Output format (default: json)
|
|
864
|
-
--include-universal Also include unscoped learnings
|
|
865
|
-
alongside the project-filtered ones
|
|
943
|
+
--include-universal Also include unscoped learnings
|
|
866
944
|
|
|
867
|
-
Cross-client confidentiality: without --project, this
|
|
868
|
-
Always use --
|
|
869
|
-
|
|
945
|
+
Cross-client confidentiality: without --tier and without --project, this
|
|
946
|
+
exports the FULL raw store. Always use --tier A for public sharing, or
|
|
947
|
+
--project NAME for trusted-team sharing inside one engagement.`);
|
|
870
948
|
return;
|
|
871
949
|
}
|
|
872
950
|
}
|
|
951
|
+
// Tier-aware sanitized export path.
|
|
952
|
+
if (tier) {
|
|
953
|
+
const { exportLearnings, reviewLoop } = await import("./community-export.js");
|
|
954
|
+
const out = outputPath || join(process.cwd(), `learnings-export-tier${tier}.json`);
|
|
955
|
+
let result = exportLearnings({ tier, outputPath: out, review });
|
|
956
|
+
if (review) {
|
|
957
|
+
const edited = await reviewLoop(result.rules);
|
|
958
|
+
const payload = {
|
|
959
|
+
version: 1,
|
|
960
|
+
tier,
|
|
961
|
+
generatedAt: new Date().toISOString(),
|
|
962
|
+
count: edited.length,
|
|
963
|
+
dropped: result.dropped + (result.count - edited.length),
|
|
964
|
+
rules: edited,
|
|
965
|
+
};
|
|
966
|
+
writeFileSync(out, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
967
|
+
result = { ...result, count: edited.length, rules: edited };
|
|
968
|
+
}
|
|
969
|
+
console.log(`✅ Tier ${tier} export written to ${out}`);
|
|
970
|
+
console.log(` Included: ${result.count}`);
|
|
971
|
+
console.log(` Dropped: ${result.dropped}`);
|
|
972
|
+
if (tier === "A") {
|
|
973
|
+
console.log(` Note: Tier A is MIT-publishable. Safety guarantees enforced by`);
|
|
974
|
+
console.log(` the LOCK [COMMUNITY-EXPORT-SAFETY] block in src/community-export.ts.`);
|
|
975
|
+
}
|
|
976
|
+
else {
|
|
977
|
+
console.log(` Note: Tier B is PRO-only. Distribute via the api.compr.ch`);
|
|
978
|
+
console.log(` license heartbeat, never via a public mirror.`);
|
|
979
|
+
}
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
873
982
|
let all = listLearnings(category);
|
|
874
983
|
if (project) {
|
|
875
984
|
const lower = project.toLowerCase();
|
|
@@ -941,7 +1050,7 @@ async function cliAuditExport(args) {
|
|
|
941
1050
|
continue;
|
|
942
1051
|
}
|
|
943
1052
|
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:
|
|
1053
|
+
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
1054
|
return;
|
|
946
1055
|
}
|
|
947
1056
|
}
|
|
@@ -1118,6 +1227,23 @@ Subcommands:
|
|
|
1118
1227
|
doc-coverage For each policy.doc_coverage rule, check whether the
|
|
1119
1228
|
commit touches matching source paths AND the required
|
|
1120
1229
|
doc section is staged. Exit 1 on blocking violations.
|
|
1230
|
+
commit-message-required [MSG_FILE]
|
|
1231
|
+
For each policy.commit_message_required rule, check
|
|
1232
|
+
whether the commit touches matching source paths AND
|
|
1233
|
+
the commit message matches the required pattern. Exit 1
|
|
1234
|
+
on blocking violations. Bypass: include
|
|
1235
|
+
\`--skip-multi-agent-reason: <reason>\` in the commit
|
|
1236
|
+
body (≥ 20 chars, must contain whitespace, must be at
|
|
1237
|
+
the start of its own line) — bypass is recorded as a
|
|
1238
|
+
policy.skipped audit event (no exit-1).
|
|
1239
|
+
|
|
1240
|
+
MSG_FILE: path to the commit message file. Passed by the
|
|
1241
|
+
commit-msg git hook as \$1. Lookup order:
|
|
1242
|
+
1. positional arg (preferred, what commit-msg passes)
|
|
1243
|
+
2. COMMIT_MSG_FILE env (test injection)
|
|
1244
|
+
3. .git/COMMIT_EDITMSG (legacy fallback — note this is
|
|
1245
|
+
UNRELIABLE from pre-commit; use the commit-msg
|
|
1246
|
+
hook lifecycle).
|
|
1121
1247
|
|
|
1122
1248
|
Env:
|
|
1123
1249
|
CE_JSON=1 Emit one-line JSON per check instead of human-readable
|
|
@@ -1209,6 +1335,69 @@ tamper-evident audit log at ~/.contextengine/audit.log.`);
|
|
|
1209
1335
|
process.exit(1);
|
|
1210
1336
|
return;
|
|
1211
1337
|
}
|
|
1338
|
+
if (sub === "commit-message-required") {
|
|
1339
|
+
// Locate the commit-message file. Order:
|
|
1340
|
+
// 1. CLI positional arg (`contextengine hook commit-message-required
|
|
1341
|
+
// <path>`) — this is what the commit-msg git hook passes via $1.
|
|
1342
|
+
// Verifier P0 fix (2026-06-26): the previous implementation read
|
|
1343
|
+
// .git/COMMIT_EDITMSG from inside the PRE-COMMIT hook, but git
|
|
1344
|
+
// does not populate that file until AFTER pre-commit returns.
|
|
1345
|
+
// The check now runs from commit-msg, which gets the actual
|
|
1346
|
+
// message file path as its first argument.
|
|
1347
|
+
// 2. COMMIT_MSG_FILE env (for test injection / scripted callers).
|
|
1348
|
+
// 3. .git/COMMIT_EDITMSG in the repo root — backward-compat
|
|
1349
|
+
// fallback ONLY for legacy invocations. Will be empty / stale
|
|
1350
|
+
// when called from pre-commit; that's the documented footgun
|
|
1351
|
+
// this fix retires.
|
|
1352
|
+
const commitMsgFile = args[1] ||
|
|
1353
|
+
process.env.COMMIT_MSG_FILE ||
|
|
1354
|
+
join(repoRoot, ".git", "COMMIT_EDITMSG");
|
|
1355
|
+
let commitMessage = "";
|
|
1356
|
+
if (existsSync(commitMsgFile)) {
|
|
1357
|
+
try {
|
|
1358
|
+
commitMessage = readFileSync(commitMsgFile, "utf-8");
|
|
1359
|
+
}
|
|
1360
|
+
catch {
|
|
1361
|
+
// Fall through — empty message will fail any rule that fires,
|
|
1362
|
+
// surfacing the description to the user.
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
const violations = runCommitMessageRequired(policy, stagedFiles, commitMessage);
|
|
1366
|
+
if (jsonMode) {
|
|
1367
|
+
process.stdout.write(formatCommitMessageViolationsJson(violations) + "\n");
|
|
1368
|
+
}
|
|
1369
|
+
else {
|
|
1370
|
+
console.log(formatCommitMessageViolations(violations));
|
|
1371
|
+
}
|
|
1372
|
+
let exitBlock = false;
|
|
1373
|
+
for (const v of violations) {
|
|
1374
|
+
if (v.kind === "bypass") {
|
|
1375
|
+
// Bypass IS the auditable opt-out. Record the reason; let the
|
|
1376
|
+
// commit proceed.
|
|
1377
|
+
safeAppend("policy.skipped", {
|
|
1378
|
+
check: "commit-message-required",
|
|
1379
|
+
rule_id: v.ruleId,
|
|
1380
|
+
matched_files: v.matchedFiles,
|
|
1381
|
+
pattern: v.pattern,
|
|
1382
|
+
bypass_reason: v.bypassReason,
|
|
1383
|
+
});
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
// missing-pattern
|
|
1387
|
+
safeAppend("hook.block", {
|
|
1388
|
+
check: "commit-message-required",
|
|
1389
|
+
rule_id: v.ruleId,
|
|
1390
|
+
matched_files: v.matchedFiles,
|
|
1391
|
+
pattern: v.pattern,
|
|
1392
|
+
reason: "commit-message-pattern-not-matched",
|
|
1393
|
+
});
|
|
1394
|
+
if (v.severity === "block")
|
|
1395
|
+
exitBlock = true;
|
|
1396
|
+
}
|
|
1397
|
+
if (exitBlock)
|
|
1398
|
+
process.exit(1);
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1212
1401
|
console.error(`Unknown hook subcommand: ${sub}. Try 'contextengine hook --help'.`);
|
|
1213
1402
|
process.exit(1);
|
|
1214
1403
|
}
|
|
@@ -1716,6 +1905,107 @@ async function cliImportLearnings(args) {
|
|
|
1716
1905
|
}
|
|
1717
1906
|
}
|
|
1718
1907
|
// ---------------------------------------------------------------------------
|
|
1908
|
+
// CLI: sync-community-rules — fetch + cache community learnings
|
|
1909
|
+
// ---------------------------------------------------------------------------
|
|
1910
|
+
async function cliSyncCommunityRules(args) {
|
|
1911
|
+
let force = false;
|
|
1912
|
+
let tier = "all";
|
|
1913
|
+
for (let i = 0; i < args.length; i++) {
|
|
1914
|
+
const a = args[i];
|
|
1915
|
+
if (a === "--force" || a === "-f") {
|
|
1916
|
+
force = true;
|
|
1917
|
+
continue;
|
|
1918
|
+
}
|
|
1919
|
+
if (a === "--tier" && args[i + 1]) {
|
|
1920
|
+
const t = args[++i];
|
|
1921
|
+
if (t !== "A" && t !== "B" && t !== "all") {
|
|
1922
|
+
console.error(`Unknown tier: ${t}. Try A | B | all.`);
|
|
1923
|
+
process.exit(1);
|
|
1924
|
+
}
|
|
1925
|
+
tier = t;
|
|
1926
|
+
continue;
|
|
1927
|
+
}
|
|
1928
|
+
if (a === "-h" || a === "--help") {
|
|
1929
|
+
console.log(`Usage: opscontext sync-community-rules [--force] [--tier A|B|all]
|
|
1930
|
+
|
|
1931
|
+
Fetches community-contributed learnings into the local cache at
|
|
1932
|
+
${COMMUNITY_STORE_PATH}.
|
|
1933
|
+
|
|
1934
|
+
Two tiers:
|
|
1935
|
+
Tier A (public) raw.githubusercontent.com — no license required
|
|
1936
|
+
Tier B (pro) api.compr.ch — requires an activated Pro license
|
|
1937
|
+
|
|
1938
|
+
Behavior:
|
|
1939
|
+
- Daily run recommended. Cron / launchd / periodic CI all fine — the
|
|
1940
|
+
sync is idempotent and ETag-guarded (304 = cheap no-op).
|
|
1941
|
+
- Network failures NEVER crash the search engine. On any HTTP error
|
|
1942
|
+
(timeout, DNS, 5xx, malformed JSON), the cached store is preserved
|
|
1943
|
+
and search continues to work offline.
|
|
1944
|
+
- Tier B is best-effort: with no license loaded (free tier) it is
|
|
1945
|
+
skipped silently with a note on stderr. On HTTP 401 / 403 the
|
|
1946
|
+
error is logged but no exception is thrown — your existing search
|
|
1947
|
+
keeps working.
|
|
1948
|
+
- Tier B responses are Ed25519-signed; an invalid signature causes
|
|
1949
|
+
the fetched payload to be discarded.
|
|
1950
|
+
|
|
1951
|
+
Flags:
|
|
1952
|
+
--force, -f Bypass the ETag cache; always re-fetch.
|
|
1953
|
+
--tier A|B|all Restrict the sync to one tier. Default: all.
|
|
1954
|
+
|
|
1955
|
+
After sync, the new rules are picked up automatically by search_context
|
|
1956
|
+
the next time the MCP server (re)indexes. Restart the server or run
|
|
1957
|
+
'opscontext reindex' to force an immediate pickup.`);
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
console.log(`\n🌐 Sync community rules (tier=${tier}, force=${force})\n`);
|
|
1962
|
+
if (tier === "A" || tier === "all") {
|
|
1963
|
+
const r = await syncTierA({ force });
|
|
1964
|
+
if (r.cached) {
|
|
1965
|
+
console.log(` Tier A — cached (no changes since last fetch)`);
|
|
1966
|
+
}
|
|
1967
|
+
else {
|
|
1968
|
+
console.log(` Tier A — fetched ${r.fetched} rule(s)`);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
if (tier === "B" || tier === "all") {
|
|
1972
|
+
// Resolve license through the activation module to keep auth shape
|
|
1973
|
+
// identical to /heartbeat — same machineId, same key.
|
|
1974
|
+
const licenseFile = join(homedir(), ".contextengine", "license.json");
|
|
1975
|
+
let token = null;
|
|
1976
|
+
if (existsSync(licenseFile)) {
|
|
1977
|
+
try {
|
|
1978
|
+
const data = JSON.parse(readFileSync(licenseFile, "utf-8"));
|
|
1979
|
+
token = typeof data.key === "string" ? data.key : null;
|
|
1980
|
+
}
|
|
1981
|
+
catch { /* malformed — treat as no license */ }
|
|
1982
|
+
}
|
|
1983
|
+
if (!token) {
|
|
1984
|
+
if (tier === "B") {
|
|
1985
|
+
console.error(` Tier B — no license loaded. Activate with: opscontext activate <key> <email>`);
|
|
1986
|
+
}
|
|
1987
|
+
else {
|
|
1988
|
+
console.log(` Tier B — skipped (no license loaded)`);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
else {
|
|
1992
|
+
const r = await syncTierB(token, { force });
|
|
1993
|
+
if (r.cached) {
|
|
1994
|
+
console.log(` Tier B — cached (no changes since last fetch)`);
|
|
1995
|
+
}
|
|
1996
|
+
else if (r.fetched === 0) {
|
|
1997
|
+
console.log(` Tier B — 0 rules (auth rejected or empty payload — see stderr)`);
|
|
1998
|
+
}
|
|
1999
|
+
else {
|
|
2000
|
+
console.log(` Tier B — fetched ${r.fetched} rule(s)`);
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
const store = loadCommunityStore();
|
|
2005
|
+
console.log(`\n Store now holds ${store.rules.length} total community rule(s).`);
|
|
2006
|
+
console.log(` Path: ${COMMUNITY_STORE_PATH}\n`);
|
|
2007
|
+
}
|
|
2008
|
+
// ---------------------------------------------------------------------------
|
|
1719
2009
|
// CLI: stats — show live session stats from MCP server
|
|
1720
2010
|
// ---------------------------------------------------------------------------
|
|
1721
2011
|
function cliStats() {
|
|
@@ -1770,15 +2060,18 @@ Usage:
|
|
|
1770
2060
|
contextengine save-learning <text> -c <category> Save a learning
|
|
1771
2061
|
contextengine delete-learning <id> Delete a learning by ID
|
|
1772
2062
|
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
|
|
2063
|
+
contextengine export-learnings [--tier A|B] [--output PATH] [--review] [--project NAME] [--category CAT] [--format json|markdown] [--include-universal]
|
|
2064
|
+
Export learnings. --tier A → MIT-publishable redacted subset (LOCKed safety pipeline).
|
|
2065
|
+
--tier B → full PRO corpus (still redacted; original IDs preserved).
|
|
2066
|
+
(no --tier) → legacy raw export, NOT safe for public distribution.
|
|
1775
2067
|
contextengine save-session <name> <key> <value> Save session context
|
|
1776
2068
|
contextengine load-session <name> Restore session context
|
|
1777
2069
|
contextengine list-sessions List all saved sessions
|
|
1778
2070
|
contextengine delete-session <name> Delete a saved session
|
|
1779
2071
|
contextengine end-session Pre-flight checklist (uncommitted changes, doc freshness)
|
|
1780
2072
|
contextengine audit-export [--since DATE] [--until DATE] [--format jsonl|csv]
|
|
1781
|
-
Export hash-chained audit log (
|
|
2073
|
+
Export hash-chained audit log (evidence aligned with
|
|
2074
|
+
SOC 2 CC7.2 + ISO 27001 A.12.4.1 — not a certification)
|
|
1782
2075
|
contextengine audit-verify Verify audit log chain integrity (tamper detection)
|
|
1783
2076
|
contextengine policy <validate|show> [args]
|
|
1784
2077
|
Author + validate the declarative .contextengine/policy.json
|
|
@@ -1801,6 +2094,10 @@ Usage:
|
|
|
1801
2094
|
contextengine sync-claude-md [--path CLAUDE.md] [--dry-run]
|
|
1802
2095
|
Refresh the OpsContext-managed block in CLAUDE.md
|
|
1803
2096
|
(top learnings + policy summary + recent hook blocks)
|
|
2097
|
+
contextengine sync-community-rules [--force] [--tier A|B|all]
|
|
2098
|
+
Fetch community-contributed learnings (Tier A = GitHub
|
|
2099
|
+
public, Tier B = api.compr.ch Pro). Daily run recommended.
|
|
2100
|
+
Network failures fall back to cached store.
|
|
1804
2101
|
contextengine score [project] [--html] [--no-save] AI-readiness score (Pro, writes SCORE.md)
|
|
1805
2102
|
contextengine audit Run compliance audit (Pro)
|
|
1806
2103
|
contextengine activate <key> <email> Activate a Pro license
|
|
@@ -2039,6 +2336,12 @@ else if (command === "uninstall-claude-hook") {
|
|
|
2039
2336
|
process.exit(1);
|
|
2040
2337
|
});
|
|
2041
2338
|
}
|
|
2339
|
+
else if (command === "sync-community-rules") {
|
|
2340
|
+
cliSyncCommunityRules(process.argv.slice(3)).catch((err) => {
|
|
2341
|
+
console.error("Error:", err);
|
|
2342
|
+
process.exit(1);
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2042
2345
|
else if (command === "stats") {
|
|
2043
2346
|
cliStats();
|
|
2044
2347
|
}
|
|
@@ -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
|