@compr/opscontext-mcp 2.5.4 → 2.5.5
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 +31 -6
- package/dist/index.js +14 -3
- package/dist/learnings.d.ts +14 -1
- package/dist/learnings.js +67 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -646,7 +646,7 @@ import { SERVER_COMMANDS, suggestCommands } from "./cli-commands.js";
|
|
|
646
646
|
import { collectProjectOps, collectSystemOps } from "./collectors.js";
|
|
647
647
|
import { scanCodeDir } from "./code-chunker.js";
|
|
648
648
|
import { listProjects, runComplianceAudit, formatProjectList, formatPlan, scoreProject, runScoreCanary, formatScoreReport, generateScoreHTML, generateProjectScoreMD, } from "./agents.js";
|
|
649
|
-
import { listLearnings, learningsToChunks, learningsStats, formatLearnings, saveLearning, deleteLearning, importLearningsFromFile, autoImportFromSources, LEARNING_CATEGORIES, } from "./learnings.js";
|
|
649
|
+
import { listLearnings, parseSince, learningsToChunks, learningsStats, formatLearnings, saveLearning, deleteLearning, importLearningsFromFile, autoImportFromSources, LEARNING_CATEGORIES, } from "./learnings.js";
|
|
650
650
|
import { saveSession, loadSession, listSessions, deleteSession, formatSession, formatSessionList, } from "./sessions.js";
|
|
651
651
|
import { activate, deactivate, getActivationStatus, gateCheck, } from "./activation.js";
|
|
652
652
|
import { syncTierA, syncTierB, loadCommunityStore, communityRulesToChunks, mergeWithDedup, STORE_PATH as COMMUNITY_STORE_PATH, } from "./community-sync.js";
|
|
@@ -755,12 +755,37 @@ async function cliListProjects() {
|
|
|
755
755
|
const text = formatProjectList(projects);
|
|
756
756
|
console.log(`\n${text}`);
|
|
757
757
|
}
|
|
758
|
-
async function cliListLearnings(
|
|
758
|
+
async function cliListLearnings(args) {
|
|
759
|
+
// list-learnings [category] [--since today|yesterday|ISO]. [LOCK] [LEARNINGS-LIST-SHOWS-CREATED]
|
|
760
|
+
let category;
|
|
761
|
+
let sinceSpec;
|
|
762
|
+
for (let i = 0; i < args.length; i++) {
|
|
763
|
+
if (args[i] === "--since") {
|
|
764
|
+
sinceSpec = args[i + 1];
|
|
765
|
+
if (!sinceSpec) {
|
|
766
|
+
console.error("--since needs a value: today, yesterday, or an ISO date");
|
|
767
|
+
process.exit(1);
|
|
768
|
+
}
|
|
769
|
+
i++;
|
|
770
|
+
}
|
|
771
|
+
else if (!category) {
|
|
772
|
+
category = args[i];
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
let since;
|
|
776
|
+
if (sinceSpec) {
|
|
777
|
+
const parsed = parseSince(sinceSpec);
|
|
778
|
+
if (!parsed) {
|
|
779
|
+
console.error(`--since "${sinceSpec}" is not today, yesterday, or an ISO date. Refusing to answer with a zero.`);
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
since = parsed;
|
|
783
|
+
}
|
|
759
784
|
// Project-scoped: only show learnings for workspace projects + universal
|
|
760
785
|
const projectDirs = loadProjectDirs();
|
|
761
786
|
const projectNames = projectDirs.map((d) => d.name);
|
|
762
787
|
const learnings = listLearnings(category, projectNames);
|
|
763
|
-
const text = formatLearnings(learnings);
|
|
788
|
+
const text = formatLearnings(learnings, { since, sinceSpec });
|
|
764
789
|
console.log(`\n${text}`);
|
|
765
790
|
}
|
|
766
791
|
async function cliSaveLearning(args) {
|
|
@@ -2458,7 +2483,8 @@ Usage:
|
|
|
2458
2483
|
contextengine search <query> [-n N] Search indexed knowledge (default: top 5)
|
|
2459
2484
|
contextengine list-sources Show all indexed sources with chunk counts
|
|
2460
2485
|
contextengine list-projects Discover and analyze all projects (Pro)
|
|
2461
|
-
contextengine list-learnings [cat]
|
|
2486
|
+
contextengine list-learnings [cat] [--since today|yesterday|ISO]
|
|
2487
|
+
List learnings with their created instant (UTC + Europe/Zurich)
|
|
2462
2488
|
contextengine save-learning <text> -c <category> Save a learning
|
|
2463
2489
|
contextengine delete-learning <id> Delete a learning by ID
|
|
2464
2490
|
contextengine import-learnings <file> [-c cat] [-p project] Bulk-import learnings
|
|
@@ -2579,8 +2605,7 @@ else if (command === "list-projects") {
|
|
|
2579
2605
|
});
|
|
2580
2606
|
}
|
|
2581
2607
|
else if (command === "list-learnings") {
|
|
2582
|
-
|
|
2583
|
-
cliListLearnings(category).catch((err) => {
|
|
2608
|
+
cliListLearnings(process.argv.slice(3)).catch((err) => {
|
|
2584
2609
|
console.error("Error:", err);
|
|
2585
2610
|
process.exit(1);
|
|
2586
2611
|
});
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { verifyChain, readAuditLog, filterByRange, autoRotateAuditLog } from "./
|
|
|
14
14
|
import { startEventIngestServer } from "./http-server.js";
|
|
15
15
|
import { detect } from "./detector.js";
|
|
16
16
|
import { buildCostReport } from "./cost-report.js";
|
|
17
|
-
import { saveLearning, searchLearnings, listLearnings, deleteLearning, learningsToChunks, learningsStats, formatLearnings, importLearningsFromFile, autoImportFromSources, LEARNING_CATEGORIES, } from "./learnings.js";
|
|
17
|
+
import { saveLearning, searchLearnings, listLearnings, deleteLearning, learningsToChunks, learningsStats, formatLearnings, importLearningsFromFile, autoImportFromSources, LEARNING_CATEGORIES, parseSince } from "./learnings.js";
|
|
18
18
|
import { communityRulesToChunks, mergeWithDedup, loadCommunityStore, } from "./community-sync.js";
|
|
19
19
|
import { readFileSync, existsSync, watch, statSync, writeFileSync, mkdirSync } from "fs";
|
|
20
20
|
import { basename, join, dirname } from "path";
|
|
@@ -919,10 +919,21 @@ server.tool("list_learnings", "List all permanent learnings, optionally filtered
|
|
|
919
919
|
.string()
|
|
920
920
|
.optional()
|
|
921
921
|
.describe("Filter by category (deployment, api, database, etc.). Omit to show all."),
|
|
922
|
-
|
|
922
|
+
since: z
|
|
923
|
+
.string()
|
|
924
|
+
.optional()
|
|
925
|
+
.describe("Only learnings created at or after this boundary: 'today', 'yesterday' (Europe/Zurich calendar days) or an ISO date/instant. Every entry shows its created instant, UTC plus Europe/Zurich."),
|
|
926
|
+
}, async ({ category, since }) => {
|
|
923
927
|
// Project-scoped: only show learnings for active workspace projects + universal (no project)
|
|
928
|
+
let sinceDate;
|
|
929
|
+
if (since) {
|
|
930
|
+
const parsed = parseSince(since);
|
|
931
|
+
if (!parsed)
|
|
932
|
+
return respond("list_learnings", `❌ since="${since}" is not today, yesterday, or an ISO date. No list rendered, so this is not a zero.`);
|
|
933
|
+
sinceDate = parsed;
|
|
934
|
+
}
|
|
924
935
|
const learnings = listLearnings(category, activeProjectNames);
|
|
925
|
-
const text = formatLearnings(learnings);
|
|
936
|
+
const text = formatLearnings(learnings, { since: sinceDate, sinceSpec: since });
|
|
926
937
|
return respond("list_learnings", text);
|
|
927
938
|
});
|
|
928
939
|
// ---------------------------------------------------------------------------
|
package/dist/learnings.d.ts
CHANGED
|
@@ -104,5 +104,18 @@ export declare function learningsStats(): {
|
|
|
104
104
|
/**
|
|
105
105
|
* Format learnings for display.
|
|
106
106
|
*/
|
|
107
|
-
export declare
|
|
107
|
+
export declare const LEARNINGS_LOCAL_TZ = "Europe/Zurich";
|
|
108
|
+
/** `2026-09-05 10:30Z (12:30 CEST)`; `undated` when the record carries no usable instant. */
|
|
109
|
+
export declare function formatLearnedAt(iso: string | undefined, tz?: string): string;
|
|
110
|
+
/** `today` | `yesterday` (calendar days in `tz`) | any ISO date or instant. `null` when unparseable. */
|
|
111
|
+
export declare function parseSince(spec: string, now?: Date, tz?: string): Date | null;
|
|
112
|
+
/** Records created at or after `since`, oldest first. Undated records are excluded and counted by the caller. */
|
|
113
|
+
export declare function filterSince(learnings: Learning[], since: Date): Learning[];
|
|
114
|
+
export interface FormatLearningsOptions {
|
|
115
|
+
/** Already-parsed boundary; the caller resolves the spec so an invalid one errors before rendering. */
|
|
116
|
+
since?: Date;
|
|
117
|
+
/** The spec as typed, echoed in the header so the reader sees which boundary applied. */
|
|
118
|
+
sinceSpec?: string;
|
|
119
|
+
}
|
|
120
|
+
export declare function formatLearnings(learnings: Learning[], opts?: FormatLearningsOptions): string;
|
|
108
121
|
//# sourceMappingURL=learnings.d.ts.map
|
package/dist/learnings.js
CHANGED
|
@@ -680,12 +680,76 @@ export function learningsStats() {
|
|
|
680
680
|
/**
|
|
681
681
|
* Format learnings for display.
|
|
682
682
|
*/
|
|
683
|
-
|
|
683
|
+
// [LOCKED] [LEARNINGS-LIST-SHOWS-CREATED] 2026-09-05
|
|
684
|
+
// [NEVER] print a learning without its `created` instant, and [NEVER] answer "what was
|
|
685
|
+
// saved since X" by probing learnings.json with a hand-written key.
|
|
686
|
+
// WHY: on 2026-09-05 an agent read a date field the records do not have (`createdAt`),
|
|
687
|
+
// got an empty string for every record, and answered "0 saved today" with full
|
|
688
|
+
// confidence; a second agent made the same mistake the same morning. A probe on a
|
|
689
|
+
// missing key returns a confident zero, never an error. The store had 22 records
|
|
690
|
+
// from that day, under `created`. Until then the listing showed the date only,
|
|
691
|
+
// so "today" was also ambiguous around midnight between UTC and Yan's clock.
|
|
692
|
+
// FIX: one renderer shows `created` as the UTC instant plus the Europe/Zurich wall
|
|
693
|
+
// time, and `--since today|yesterday|ISO` is a first-class filter whose empty
|
|
694
|
+
// result names the boundary it applied. An unparseable spec is an error, not zero.
|
|
695
|
+
export const LEARNINGS_LOCAL_TZ = "Europe/Zurich";
|
|
696
|
+
/** `2026-09-05 10:30Z (12:30 CEST)`; `undated` when the record carries no usable instant. */
|
|
697
|
+
export function formatLearnedAt(iso, tz = LEARNINGS_LOCAL_TZ) {
|
|
698
|
+
if (!iso)
|
|
699
|
+
return "undated";
|
|
700
|
+
const d = new Date(iso);
|
|
701
|
+
if (Number.isNaN(d.getTime()))
|
|
702
|
+
return "undated";
|
|
703
|
+
const utc = d.toISOString().slice(0, 16).replace("T", " ") + "Z";
|
|
704
|
+
const local = new Intl.DateTimeFormat("en-GB", {
|
|
705
|
+
timeZone: tz, hour: "2-digit", minute: "2-digit", hour12: false, timeZoneName: "short",
|
|
706
|
+
}).format(d);
|
|
707
|
+
return `${utc} (${local})`;
|
|
708
|
+
}
|
|
709
|
+
/** Midnight of the given calendar day in `tz`, as a UTC instant. Offset read from Intl, never guessed. */
|
|
710
|
+
function localMidnightUtc(y, m, d, tz) {
|
|
711
|
+
const guess = new Date(Date.UTC(y, m - 1, d, 0, 0, 0));
|
|
712
|
+
const off = new Intl.DateTimeFormat("en-US", { timeZone: tz, timeZoneName: "longOffset" })
|
|
713
|
+
.formatToParts(guess).find((p) => p.type === "timeZoneName")?.value ?? "GMT";
|
|
714
|
+
const mm = /GMT([+-])(\d{2}):(\d{2})/.exec(off);
|
|
715
|
+
const minutes = mm ? (mm[1] === "-" ? -1 : 1) * (parseInt(mm[2], 10) * 60 + parseInt(mm[3], 10)) : 0;
|
|
716
|
+
return new Date(guess.getTime() - minutes * 60_000);
|
|
717
|
+
}
|
|
718
|
+
/** `today` | `yesterday` (calendar days in `tz`) | any ISO date or instant. `null` when unparseable. */
|
|
719
|
+
export function parseSince(spec, now = new Date(), tz = LEARNINGS_LOCAL_TZ) {
|
|
720
|
+
const s = spec.trim().toLowerCase();
|
|
721
|
+
if (s === "today" || s === "yesterday") {
|
|
722
|
+
const parts = new Intl.DateTimeFormat("en-CA", { timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit" })
|
|
723
|
+
.formatToParts(now);
|
|
724
|
+
const get = (t) => parseInt(parts.find((p) => p.type === t)?.value ?? "0", 10);
|
|
725
|
+
const midnight = localMidnightUtc(get("year"), get("month"), get("day"), tz);
|
|
726
|
+
return s === "today" ? midnight : new Date(midnight.getTime() - 86_400_000);
|
|
727
|
+
}
|
|
728
|
+
if (!/^\d{4}-\d{2}-\d{2}/.test(spec.trim()))
|
|
729
|
+
return null;
|
|
730
|
+
const d = new Date(spec.trim());
|
|
731
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
732
|
+
}
|
|
733
|
+
/** Records created at or after `since`, oldest first. Undated records are excluded and counted by the caller. */
|
|
734
|
+
export function filterSince(learnings, since) {
|
|
735
|
+
return learnings
|
|
736
|
+
.filter((l) => l.created && !Number.isNaN(new Date(l.created).getTime()) && new Date(l.created).getTime() >= since.getTime())
|
|
737
|
+
.sort((a, b) => new Date(a.created).getTime() - new Date(b.created).getTime());
|
|
738
|
+
}
|
|
739
|
+
export function formatLearnings(learnings, opts = {}) {
|
|
740
|
+
let sinceNote = "";
|
|
741
|
+
if (opts.since) {
|
|
742
|
+
learnings = filterSince(learnings, opts.since);
|
|
743
|
+
sinceNote = ` since ${opts.sinceSpec ?? opts.since.toISOString()} = ${formatLearnedAt(opts.since.toISOString())}`;
|
|
744
|
+
}
|
|
684
745
|
if (learnings.length === 0) {
|
|
746
|
+
if (opts.since) {
|
|
747
|
+
return `0 learnings${sinceNote}. The boundary above is the one that was applied; if that looks wrong, the store is at ~/.contextengine/learnings.json and its date field is \`created\`.`;
|
|
748
|
+
}
|
|
685
749
|
return "No learnings stored yet. Use `save_learning` to add operational rules.";
|
|
686
750
|
}
|
|
687
751
|
const lines = [];
|
|
688
|
-
lines.push(`# 💡 Learnings Store (${learnings.length} rules)\n`);
|
|
752
|
+
lines.push(`# 💡 Learnings Store (${learnings.length} rules${sinceNote})\n`);
|
|
689
753
|
// Group by category
|
|
690
754
|
const byCategory = new Map();
|
|
691
755
|
for (const l of learnings) {
|
|
@@ -704,8 +768,7 @@ export function formatLearnings(learnings) {
|
|
|
704
768
|
lines.push(`- **Context:** ${l.context}`);
|
|
705
769
|
if (l.tags?.length)
|
|
706
770
|
lines.push(`- **Tags:** ${l.tags.join(", ")}`);
|
|
707
|
-
|
|
708
|
-
lines.push(`- **Learned:** ${l.created.split("T")[0]}`);
|
|
771
|
+
lines.push(`- **Learned:** ${formatLearnedAt(l.created)}`);
|
|
709
772
|
lines.push("");
|
|
710
773
|
}
|
|
711
774
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@compr/opscontext-mcp",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.5",
|
|
4
4
|
"description": "OpsContext for AI Agents — read-only fleet visibility (PM2/nginx/Docker/git/cron) + tamper-evident audit log + policy-as-code hooks. The ops + compliance layer Claude Code can't grow natively.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|