@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/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
- const chunks = ingestSources(sources);
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 [--project NAME] [--category CAT] [--format json|markdown] [--include-universal]
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
- 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).
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
- --project NAME Only learnings tagged with this project (case-insensitive)
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 (project=undefined)
865
- alongside the project-filtered ones
956
+ --include-universal Also include unscoped learnings
866
957
 
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.`);
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: SOC2 CC7.2, ISO 27001 A.12.4.1.`);
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 (scope to one project for safe sharing)
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 (SOC2/ISO27001 evidence)
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