@cachly-dev/mcp-server 0.10.129 โ 0.10.130
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/README.md +2 -2
- package/dist/index.js +1 -7
- package/dist/packages/telegram-notify/client.js +2 -75
- package/dist/src/ambient-cli.js +7 -253
- package/dist/src/ambient-deps.js +1 -55
- package/dist/src/ambient-hooks.js +5 -278
- package/dist/src/ambient-ledger.js +6 -95
- package/dist/src/ambient-memory.js +1 -67
- package/dist/src/ambient-recall.js +1 -221
- package/dist/src/aussetzer.js +1 -225
- package/dist/src/auth.js +9 -140
- package/dist/src/autor-abzeichen.js +1 -56
- package/dist/src/autor.js +1 -136
- package/dist/src/bauabdruck.js +1 -93
- package/dist/src/bedeutung.js +2 -323
- package/dist/src/bench/cachly-bench.js +4 -249
- package/dist/src/bench/external-corpus.js +4 -135
- package/dist/src/bench/fixtures.js +1 -225
- package/dist/src/brain-export.js +16 -119
- package/dist/src/brain-watch-hook.js +8 -86
- package/dist/src/cachly-url.js +1 -21
- package/dist/src/ckg.js +1 -108
- package/dist/src/cls-hook.js +8 -94
- package/dist/src/confidence.js +1 -50
- package/dist/src/credentials.js +1 -85
- package/dist/src/deutsch.js +1 -223
- package/dist/src/doctor.js +2 -248
- package/dist/src/editor.js +1 -63
- package/dist/src/eingaenge-nachruesten.js +1 -290
- package/dist/src/eingaenge.js +2 -260
- package/dist/src/embeddings.js +13 -223
- package/dist/src/feldreparatur.js +3 -113
- package/dist/src/first-contact.js +4 -108
- package/dist/src/first-recall-proof.js +4 -41
- package/dist/src/funnel-milestones.js +2 -48
- package/dist/src/geliefert.js +1 -114
- package/dist/src/handlers/advanced.js +38 -537
- package/dist/src/handlers/brain.js +69 -4397
- package/dist/src/handlers/cache.js +59 -638
- package/dist/src/handlers/context.js +17 -182
- package/dist/src/handlers/fedbrain.js +40 -1436
- package/dist/src/handlers/instances.js +16 -201
- package/dist/src/handlers/roadmap.js +13 -175
- package/dist/src/handlers/share.js +19 -530
- package/dist/src/handlers/syndicate.js +38 -904
- package/dist/src/handlers/team.js +89 -1865
- package/dist/src/handlers/viz.js +3 -148
- package/dist/src/index.js +321 -4281
- package/dist/src/interferenz.js +1 -141
- package/dist/src/kostprobe.js +7 -125
- package/dist/src/lesson-preview.js +2 -133
- package/dist/src/namespace.js +1 -128
- package/dist/src/notifier.js +1 -6
- package/dist/src/rangfolge-stellschrauben.js +1 -0
- package/dist/src/rangfolge.js +1 -249
- package/dist/src/raumkorrektur.js +1 -197
- package/dist/src/recall-tiefe.js +1 -42
- package/dist/src/rerank.js +1 -154
- package/dist/src/resonanz.js +1 -140
- package/dist/src/search.js +1 -2104
- package/dist/src/seltenheitsbestand.js +1 -92
- package/dist/src/speicherlage.js +1 -84
- package/dist/src/spuren.js +1 -150
- package/dist/src/starter-corpus.js +1 -165
- package/dist/src/telemetry-types.js +0 -13
- package/dist/src/tools.js +10 -2465
- package/dist/src/toolspecs.js +1 -121
- package/dist/src/upgrade-nudge.js +1 -29
- package/dist/src/utils.js +1 -89
- package/dist/src/versuch.js +1 -264
- package/dist/src/vorspann.js +1 -146
- package/dist/src/wartung/vektoren-verdichten.js +1 -200
- package/dist/src/werkzeug-nutzung.js +1 -72
- package/dist/src/wertbeitrag.js +1 -203
- package/dist/src/wirkungsspur.js +1 -0
- package/glama.json +1 -1
- package/package.json +8 -7
- package/server.json +3 -3
- package/dist/index.d.ts +0 -1
- package/dist/index.js.map +0 -1
- package/dist/packages/telegram-notify/client.d.ts +0 -19
- package/dist/packages/telegram-notify/client.js.map +0 -1
- package/dist/src/ambient-cli.d.ts +0 -101
- package/dist/src/ambient-cli.js.map +0 -1
- package/dist/src/ambient-deps.d.ts +0 -26
- package/dist/src/ambient-deps.js.map +0 -1
- package/dist/src/ambient-hooks.d.ts +0 -137
- package/dist/src/ambient-hooks.js.map +0 -1
- package/dist/src/ambient-ledger.d.ts +0 -19
- package/dist/src/ambient-ledger.js.map +0 -1
- package/dist/src/ambient-memory.d.ts +0 -12
- package/dist/src/ambient-memory.js.map +0 -1
- package/dist/src/ambient-recall.d.ts +0 -127
- package/dist/src/ambient-recall.js.map +0 -1
- package/dist/src/aussetzer.d.ts +0 -156
- package/dist/src/aussetzer.js.map +0 -1
- package/dist/src/auth.d.ts +0 -43
- package/dist/src/auth.js.map +0 -1
- package/dist/src/autor-abzeichen.d.ts +0 -41
- package/dist/src/autor-abzeichen.js.map +0 -1
- package/dist/src/autor.d.ts +0 -101
- package/dist/src/autor.js.map +0 -1
- package/dist/src/bauabdruck-daten.generiert.json +0 -5
- package/dist/src/bauabdruck.d.ts +0 -38
- package/dist/src/bauabdruck.js.map +0 -1
- package/dist/src/bedeutung.d.ts +0 -168
- package/dist/src/bedeutung.js.map +0 -1
- package/dist/src/bench/cachly-bench.d.ts +0 -61
- package/dist/src/bench/cachly-bench.js.map +0 -1
- package/dist/src/bench/external-corpus.d.ts +0 -44
- package/dist/src/bench/external-corpus.js.map +0 -1
- package/dist/src/bench/fixtures.d.ts +0 -37
- package/dist/src/bench/fixtures.js.map +0 -1
- package/dist/src/brain-export.d.ts +0 -64
- package/dist/src/brain-export.js.map +0 -1
- package/dist/src/brain-watch-hook.d.ts +0 -21
- package/dist/src/brain-watch-hook.js.map +0 -1
- package/dist/src/cachly-url.d.ts +0 -68
- package/dist/src/cachly-url.js.map +0 -1
- package/dist/src/ckg.d.ts +0 -54
- package/dist/src/ckg.js.map +0 -1
- package/dist/src/cls-hook.d.ts +0 -15
- package/dist/src/cls-hook.js.map +0 -1
- package/dist/src/confidence.d.ts +0 -19
- package/dist/src/confidence.js.map +0 -1
- package/dist/src/credentials.d.ts +0 -37
- package/dist/src/credentials.js.map +0 -1
- package/dist/src/deutsch.d.ts +0 -98
- package/dist/src/deutsch.js.map +0 -1
- package/dist/src/doctor.d.ts +0 -47
- package/dist/src/doctor.js.map +0 -1
- package/dist/src/editor.d.ts +0 -9
- package/dist/src/editor.js.map +0 -1
- package/dist/src/eingaenge-nachruesten.d.ts +0 -41
- package/dist/src/eingaenge-nachruesten.js.map +0 -1
- package/dist/src/eingaenge.d.ts +0 -159
- package/dist/src/eingaenge.js.map +0 -1
- package/dist/src/embeddings.d.ts +0 -48
- package/dist/src/embeddings.js.map +0 -1
- package/dist/src/feldreparatur.d.ts +0 -58
- package/dist/src/feldreparatur.js.map +0 -1
- package/dist/src/first-contact.d.ts +0 -56
- package/dist/src/first-contact.js.map +0 -1
- package/dist/src/first-recall-proof.d.ts +0 -19
- package/dist/src/first-recall-proof.js.map +0 -1
- package/dist/src/funnel-milestones.d.ts +0 -5
- package/dist/src/funnel-milestones.js.map +0 -1
- package/dist/src/geliefert.d.ts +0 -63
- package/dist/src/geliefert.js.map +0 -1
- package/dist/src/handlers/advanced.d.ts +0 -6
- package/dist/src/handlers/advanced.js.map +0 -1
- package/dist/src/handlers/brain.d.ts +0 -41
- package/dist/src/handlers/brain.js.map +0 -1
- package/dist/src/handlers/cache.d.ts +0 -6
- package/dist/src/handlers/cache.js.map +0 -1
- package/dist/src/handlers/context.d.ts +0 -6
- package/dist/src/handlers/context.js.map +0 -1
- package/dist/src/handlers/fedbrain.d.ts +0 -18
- package/dist/src/handlers/fedbrain.js.map +0 -1
- package/dist/src/handlers/instances.d.ts +0 -33
- package/dist/src/handlers/instances.js.map +0 -1
- package/dist/src/handlers/roadmap.d.ts +0 -6
- package/dist/src/handlers/roadmap.js.map +0 -1
- package/dist/src/handlers/share.d.ts +0 -8
- package/dist/src/handlers/share.js.map +0 -1
- package/dist/src/handlers/syndicate.d.ts +0 -6
- package/dist/src/handlers/syndicate.js.map +0 -1
- package/dist/src/handlers/team.d.ts +0 -45
- package/dist/src/handlers/team.js.map +0 -1
- package/dist/src/handlers/viz.d.ts +0 -6
- package/dist/src/handlers/viz.js.map +0 -1
- package/dist/src/index.d.ts +0 -93
- package/dist/src/index.js.map +0 -1
- package/dist/src/interferenz.d.ts +0 -111
- package/dist/src/interferenz.js.map +0 -1
- package/dist/src/kostprobe.d.ts +0 -25
- package/dist/src/kostprobe.js.map +0 -1
- package/dist/src/lesson-preview.d.ts +0 -81
- package/dist/src/lesson-preview.js.map +0 -1
- package/dist/src/namespace.d.ts +0 -20
- package/dist/src/namespace.js.map +0 -1
- package/dist/src/notifier.d.ts +0 -5
- package/dist/src/notifier.js.map +0 -1
- package/dist/src/rangfolge.d.ts +0 -205
- package/dist/src/rangfolge.js.map +0 -1
- package/dist/src/raumkorrektur.d.ts +0 -100
- package/dist/src/raumkorrektur.js.map +0 -1
- package/dist/src/recall-tiefe.d.ts +0 -48
- package/dist/src/recall-tiefe.js.map +0 -1
- package/dist/src/rerank.d.ts +0 -48
- package/dist/src/rerank.js.map +0 -1
- package/dist/src/resonanz.d.ts +0 -85
- package/dist/src/resonanz.js.map +0 -1
- package/dist/src/search.d.ts +0 -153
- package/dist/src/search.js.map +0 -1
- package/dist/src/seltenheitsbestand.d.ts +0 -53
- package/dist/src/seltenheitsbestand.js.map +0 -1
- package/dist/src/speicherlage.d.ts +0 -51
- package/dist/src/speicherlage.js.map +0 -1
- package/dist/src/spuren.d.ts +0 -97
- package/dist/src/spuren.js.map +0 -1
- package/dist/src/starter-corpus.d.ts +0 -28
- package/dist/src/starter-corpus.js.map +0 -1
- package/dist/src/telemetry-types.d.ts +0 -87
- package/dist/src/telemetry-types.js.map +0 -1
- package/dist/src/tools.d.ts +0 -2933
- package/dist/src/tools.js.map +0 -1
- package/dist/src/toolspecs.d.ts +0 -43
- package/dist/src/toolspecs.js.map +0 -1
- package/dist/src/upgrade-nudge.d.ts +0 -15
- package/dist/src/upgrade-nudge.js.map +0 -1
- package/dist/src/utils.d.ts +0 -34
- package/dist/src/utils.js.map +0 -1
- package/dist/src/versuch.d.ts +0 -187
- package/dist/src/versuch.js.map +0 -1
- package/dist/src/vorspann.d.ts +0 -73
- package/dist/src/vorspann.js.map +0 -1
- package/dist/src/wartung/vektoren-verdichten.d.ts +0 -50
- package/dist/src/wartung/vektoren-verdichten.js.map +0 -1
- package/dist/src/werkzeug-nutzung.d.ts +0 -62
- package/dist/src/werkzeug-nutzung.js.map +0 -1
- package/dist/src/wertbeitrag.d.ts +0 -148
- package/dist/src/wertbeitrag.js.map +0 -1
|
@@ -1,1493 +1,57 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { calculateConfidence, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_VALUE, CONFIDENCE_WARN_DAYS, CONFIDENCE_STALE_DAYS } from '../confidence.js';
|
|
7
|
-
import { safeJsonParse } from '../utils.js';
|
|
8
|
-
import { ersparteMinuten, istStarterLektion, fmtStunden } from '../wertbeitrag.js';
|
|
9
|
-
import { cachlyUrl } from '../cachly-url.js';
|
|
10
|
-
export const TEAM_TOOL_NAMES = new Set([
|
|
11
|
-
'sync_file_changes', 'team_learn', 'team_confirm', 'team_recall', 'team_synthesize', 'memory_crystalize', 'team_crystallize',
|
|
12
|
-
'brain_doctor', 'brain_hygiene', 'recall_at', 'trace_dependency', 'global_learn', 'global_recall',
|
|
13
|
-
'publish_lesson', 'import_public_brain', 'setup_ai_memory',
|
|
14
|
-
'team_assign_role', 'team_whoami', 'team_roster',
|
|
15
|
-
'team_grant_scope', 'team_scopes', 'team_audit',
|
|
16
|
-
]);
|
|
17
|
-
const ROLE_RANK = {
|
|
18
|
-
admin: 4, reviewer: 3, contributor: 2, viewer: 1,
|
|
19
|
-
};
|
|
20
|
-
export const ROLE_BADGE = {
|
|
21
|
-
admin: '๐', reviewer: '๐ก๏ธ', contributor: 'โ๏ธ', viewer: '๐๏ธ',
|
|
22
|
-
};
|
|
23
|
-
const ROLE_CAPABILITIES = {
|
|
24
|
-
admin: 'assign roles ยท delete lessons ยท all reviewer + contributor actions',
|
|
25
|
-
reviewer: 'senior-review lessons (๐ก๏ธ) ยท all contributor actions',
|
|
26
|
-
contributor: 'store lessons ยท peer-review lessons (โ๏ธ) ยท recall',
|
|
27
|
-
viewer: 'recall only โ cannot store or review lessons',
|
|
28
|
-
};
|
|
29
|
-
export const ROLES_KEY = (instanceId) => `cachly:team:roles:${instanceId}`;
|
|
30
|
-
export async function getRole(redis, instanceId, handle) {
|
|
31
|
-
const raw = await redis.hget(ROLES_KEY(instanceId), handle.toLowerCase()).catch(() => null);
|
|
32
|
-
return raw;
|
|
33
|
-
}
|
|
34
|
-
/** True when `actorRole` is at least as powerful as `requiredRole`. */
|
|
35
|
-
export function hasPermission(actorRole, required) {
|
|
36
|
-
if (!actorRole)
|
|
37
|
-
return false;
|
|
38
|
-
return (ROLE_RANK[actorRole] ?? 0) >= (ROLE_RANK[required] ?? 0);
|
|
39
|
-
}
|
|
40
|
-
/** The review level a role warrants in team_confirm. */
|
|
41
|
-
export function roleToReviewLevel(role) {
|
|
42
|
-
return role === 'admin' || role === 'reviewer' ? 'senior' : 'peer';
|
|
43
|
-
}
|
|
44
|
-
// โโ RBAC enforcement (W6) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
45
|
-
// Governance "activates" once an admin is bootstrapped on a brain (the same
|
|
46
|
-
// signal team_assign_role uses). Before that, the team is open and every tool
|
|
47
|
-
// behaves exactly as before โ so existing single-user and small-team flows are
|
|
48
|
-
// untouched. Once an admin exists, write/review tools enforce role minimums.
|
|
49
|
-
/** True once at least one admin exists โ i.e. governance has been turned on. */
|
|
50
|
-
export async function rbacActive(redis, instanceId) {
|
|
51
|
-
const allRoles = await redis.hgetall(ROLES_KEY(instanceId)).catch(() => ({}));
|
|
52
|
-
return Object.values(allRoles).includes('admin');
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Enforce that `handle` holds at least `required` โ but only when governance is
|
|
56
|
-
* active. Returns null when the action is permitted, or a clear refusal message
|
|
57
|
-
* (never throws โ a denied write must not break the agent call). `action` is a
|
|
58
|
-
* short human phrase, e.g. "store team lessons".
|
|
59
|
-
*/
|
|
60
|
-
export async function requireRole(redis, instanceId, handle, required, action) {
|
|
61
|
-
if (!(await rbacActive(redis, instanceId)))
|
|
62
|
-
return null; // open team โ allow
|
|
63
|
-
const role = handle ? await getRole(redis, instanceId, handle) : null;
|
|
64
|
-
if (hasPermission(role, required))
|
|
65
|
-
return null;
|
|
66
|
-
const have = role ? `${ROLE_BADGE[role]} ${role}` : 'no role';
|
|
67
|
-
return [
|
|
68
|
-
`๐ **Permission denied** โ to ${action} you need at least \`${required}\` ${ROLE_BADGE[required]} (you have: ${have}).`,
|
|
69
|
-
``,
|
|
70
|
-
`Ask an admin to grant it:`,
|
|
71
|
-
`\`team_assign_role(instance_id="${instanceId}", handle="${handle || '<your-handle>'}", role="${required}", assigned_by="<admin>")\``,
|
|
72
|
-
`_Governance is active on this brain. Run \`team_whoami\` to check your role._`,
|
|
73
|
-
].join('\n');
|
|
74
|
-
}
|
|
75
|
-
/** Append an immutable governance-audit entry (role changes, gated actions). */
|
|
76
|
-
export async function auditLog(redis, instanceId, entry) {
|
|
77
|
-
const key = `cachly:team:audit:${instanceId}`;
|
|
78
|
-
const rec = JSON.stringify({ ...entry, ts: new Date().toISOString() });
|
|
79
|
-
await redis.rpush(key, rec).catch(() => { });
|
|
80
|
-
await redis.ltrim(key, -500, -1).catch(() => { });
|
|
81
|
-
await redis.expire(key, 365 * 2 * 86400).catch(() => { }); // 2-year retention
|
|
82
|
-
}
|
|
83
|
-
// โโ Lessons-Review workflow (P1-1) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
84
|
-
// When an org enables pending-mode (PUT /orgs/:id/lesson-review), the API
|
|
85
|
-
// mirrors the setting onto each org instance's Valkey as REVIEW_MODE_KEY = "1".
|
|
86
|
-
// Team-shared lesson writes then land under PROPOSED_LESSON_PREFIX instead of
|
|
87
|
-
// cachly:lesson:best:*, so every recall path (which reads best:*) automatically
|
|
88
|
-
// ignores them until an org owner/admin approves the proposal in the dashboard
|
|
89
|
-
// (Team โ Review). Key absent = direct writes, exactly as before.
|
|
90
|
-
export const REVIEW_MODE_KEY = 'cachly:team:review_mode';
|
|
91
|
-
export const PROPOSED_LESSON_PREFIX = 'cachly:lesson:proposed:';
|
|
92
|
-
// Proposals expire after 90 days (same window as the lesson history lists) so
|
|
93
|
-
// an abandoned review queue cleans itself up.
|
|
94
|
-
const PROPOSAL_TTL_SECONDS = 90 * 86400;
|
|
95
|
-
/** True when the org has turned on review-required mode for this brain. */
|
|
96
|
-
export async function reviewModeEnabled(redis, instanceId) {
|
|
97
|
-
void instanceId; // per-instance flag; id only kept for future API fallback
|
|
98
|
-
const raw = await redis.get(REVIEW_MODE_KEY).catch(() => null);
|
|
99
|
-
return raw === '1' || raw === 'true' || raw === 'on';
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Store a lesson as a pending proposal (one per topic, latest wins โ mirroring
|
|
103
|
-
* the upsert semantics of cachly:lesson:best:<topic>) and tell the caller what
|
|
104
|
-
* happens next. Never throws โ a queued lesson must not break the agent call.
|
|
105
|
-
*/
|
|
106
|
-
export async function storeLessonProposal(redis, topic, record) {
|
|
107
|
-
const proposal = { ...record, proposed_at: new Date().toISOString(), status: 'pending' };
|
|
108
|
-
await redis
|
|
109
|
-
.set(`${PROPOSED_LESSON_PREFIX}${topic}`, JSON.stringify(proposal), 'EX', PROPOSAL_TTL_SECONDS)
|
|
110
|
-
.catch(() => { });
|
|
111
|
-
const author = typeof record.author === 'string' && record.author ? ` by **${record.author}**` : '';
|
|
112
|
-
return [
|
|
113
|
-
`๐ **Lesson submitted for review** โ \`${topic}\`${author} is now a pending proposal.`,
|
|
114
|
-
``,
|
|
115
|
-
`This team requires review before knowledge enters the Team Brain (org setting).`,
|
|
116
|
-
`An org owner/admin can approve or reject it in the dashboard under **Team โ Review**.`,
|
|
117
|
-
`Until approved it will NOT surface in \`smart_recall\` / \`team_recall\`.`,
|
|
118
|
-
].join('\n');
|
|
119
|
-
}
|
|
120
|
-
// โโ Team-level visibility scopes (groups) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
121
|
-
// A named sub-team (e.g. "backend", "security"). Lessons can be scoped to a group
|
|
122
|
-
// via `group` on learn_from_attempts; they then only surface in recall for members
|
|
123
|
-
// of that group (admins see everything). Orthogonal to lesson-level `private`.
|
|
124
|
-
// cachly:team:groups:{instance} โ set of group names
|
|
125
|
-
// cachly:team:group:{instance}:{group} โ set of member handles
|
|
126
|
-
export const GROUPS_INDEX_KEY = (instanceId) => `cachly:team:groups:${instanceId}`;
|
|
127
|
-
export const GROUP_KEY = (instanceId, group) => `cachly:team:group:${instanceId}:${group.toLowerCase()}`;
|
|
128
|
-
/** Return the set of group names `handle` belongs to (lower-cased). */
|
|
129
|
-
export async function getScopes(redis, instanceId, handle) {
|
|
130
|
-
const groups = await redis.smembers(GROUPS_INDEX_KEY(instanceId)).catch(() => []);
|
|
131
|
-
const out = new Set();
|
|
132
|
-
for (const g of groups) {
|
|
133
|
-
const isMember = await redis.sismember(GROUP_KEY(instanceId, g), handle.toLowerCase()).catch(() => 0);
|
|
134
|
-
if (isMember)
|
|
135
|
-
out.add(g.toLowerCase());
|
|
136
|
-
}
|
|
137
|
-
return out;
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Can a requester (with `requesterScopes`, possibly admin) see a lesson scoped to
|
|
141
|
-
* `lessonGroup`? Team-wide lessons (no group) are always visible; group-scoped
|
|
142
|
-
* lessons require membership. Admins see everything. Pure + testable.
|
|
143
|
-
*/
|
|
144
|
-
export function lessonVisibleToScope(lessonGroup, requesterScopes, isAdmin) {
|
|
145
|
-
if (!lessonGroup)
|
|
146
|
-
return true;
|
|
147
|
-
if (isAdmin)
|
|
148
|
-
return true;
|
|
149
|
-
return requesterScopes.has(lessonGroup.toLowerCase());
|
|
150
|
-
}
|
|
151
|
-
export async function handleTeamTool(name, args, getConnection, apiFetch) {
|
|
152
|
-
switch (name) {
|
|
153
|
-
case 'sync_file_changes': {
|
|
154
|
-
const { instance_id, changed_files, git_diff_stat, commit_msg } = args;
|
|
155
|
-
const redis = await getConnection(instance_id);
|
|
156
|
-
// Store file change event in session history
|
|
157
|
-
const changeRecord = {
|
|
158
|
-
ts: new Date().toISOString(),
|
|
159
|
-
files: changed_files,
|
|
160
|
-
commit_msg,
|
|
161
|
-
diff_stat: git_diff_stat?.slice(0, 500),
|
|
162
|
-
};
|
|
163
|
-
await redis.lpush('cachly:session:file_changes', JSON.stringify(changeRecord));
|
|
164
|
-
await redis.ltrim('cachly:session:file_changes', 0, 99);
|
|
165
|
-
await redis.expire('cachly:session:file_changes', 30 * 86400);
|
|
166
|
-
// Find lessons relevant to the changed files
|
|
167
|
-
const lessonKeys = [];
|
|
168
|
-
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
169
|
-
await new Promise((resolve, reject) => {
|
|
170
|
-
lStream.on('data', (batch) => lessonKeys.push(...batch));
|
|
171
|
-
lStream.on('end', resolve);
|
|
172
|
-
lStream.on('error', reject);
|
|
173
|
-
});
|
|
174
|
-
const relevant = [];
|
|
175
|
-
const sfRaws = lessonKeys.length > 0 ? await redis.mget(...lessonKeys) : [];
|
|
176
|
-
for (const raw of sfRaws) {
|
|
177
|
-
if (!raw)
|
|
178
|
-
continue;
|
|
179
|
-
const lesson = safeJsonParse(raw, null);
|
|
180
|
-
if (!lesson)
|
|
181
|
-
continue;
|
|
182
|
-
// Match by file_paths stored in lesson OR by topic keywords matching file name
|
|
183
|
-
const topicWords = lesson.topic.toLowerCase().split(/[:\-_]/);
|
|
184
|
-
const fileMatches = changed_files.some(f => {
|
|
185
|
-
const fname = f.split('/').pop()?.replace(/\.[^.]+$/, '').toLowerCase() ?? '';
|
|
186
|
-
return topicWords.some(w => w.length > 3 && fname.includes(w))
|
|
187
|
-
|| (lesson.file_paths ?? []).some(lf => f.includes(lf) || lf.includes(f));
|
|
188
|
-
});
|
|
189
|
-
if (fileMatches) {
|
|
190
|
-
const emoji = lesson.outcome === 'success' ? 'โ
' : 'โ ๏ธ';
|
|
191
|
-
relevant.push(` ${emoji} \`${lesson.topic}\` โ ${lesson.what_worked.slice(0, 80)}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
const lines = [
|
|
195
|
-
`๐ **File sync recorded**: ${changed_files.length} files`,
|
|
196
|
-
commit_msg ? `๐ Commit: "${commit_msg}"` : '',
|
|
197
|
-
'',
|
|
198
|
-
`**Changed:** ${changed_files.slice(0, 8).map(f => `\`${f}\``).join(', ')}${changed_files.length > 8 ? ` +${changed_files.length - 8} more` : ''}`,
|
|
199
|
-
'',
|
|
200
|
-
];
|
|
201
|
-
if (relevant.length > 0) {
|
|
202
|
-
lines.push(`๐ง **Relevant brain lessons (${relevant.length}):**`, ...relevant);
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
lines.push(`๐ก No existing lessons match these files yet. Add them with \`learn_from_attempts\`.`);
|
|
206
|
-
}
|
|
207
|
-
return lines.filter(Boolean).join('\n');
|
|
208
|
-
}
|
|
209
|
-
// โโ team_learn โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
210
|
-
case 'team_learn': {
|
|
211
|
-
const { instance_id, author, topic, outcome, what_worked, what_failed, severity, file_paths, commands, tags } = args;
|
|
212
|
-
if (!author || !topic || !outcome || !what_worked) {
|
|
213
|
-
return 'โ Required: author, topic, outcome, what_worked';
|
|
214
|
-
}
|
|
215
|
-
const iid = instance_id;
|
|
216
|
-
if (!iid)
|
|
217
|
-
return 'โ instance_id required';
|
|
218
|
-
// W6 enforcement: once governance is active, a viewer (recall-only) cannot
|
|
219
|
-
// store team lessons โ contributor+ required. Open teams are unaffected.
|
|
220
|
-
const redisTL = await getConnection(iid);
|
|
221
|
-
const learnDenied = await requireRole(redisTL, iid, author, 'contributor', 'store team lessons');
|
|
222
|
-
if (learnDenied)
|
|
223
|
-
return learnDenied;
|
|
224
|
-
// Store with author attribution via the same learn_from_attempts Redis structure
|
|
225
|
-
const lesson = {
|
|
226
|
-
topic, outcome, what_worked,
|
|
227
|
-
what_failed: what_failed ?? '',
|
|
228
|
-
severity: severity ?? 'minor',
|
|
229
|
-
author,
|
|
230
|
-
file_paths: file_paths ?? [],
|
|
231
|
-
commands: commands ?? [],
|
|
232
|
-
tags: [...(tags ?? []), 'team'],
|
|
233
|
-
timestamp: new Date().toISOString(),
|
|
234
|
-
recall_count: 0,
|
|
235
|
-
version: 2,
|
|
236
|
-
};
|
|
237
|
-
// P1-1 Lessons-Review: when the org enabled pending-mode, the lesson
|
|
238
|
-
// becomes a proposal (cachly:lesson:proposed:<topic>) instead of landing
|
|
239
|
-
// in the recall keyspace. Default (flag absent) = direct write, as before.
|
|
240
|
-
if (await reviewModeEnabled(redisTL, iid)) {
|
|
241
|
-
return storeLessonProposal(redisTL, topic, lesson);
|
|
242
|
-
}
|
|
243
|
-
const key = `cachly:lessons:${topic}`;
|
|
244
|
-
await redisTL.rpush(key, JSON.stringify(lesson));
|
|
245
|
-
if (outcome === 'success') {
|
|
246
|
-
await redisTL.set(`cachly:lesson:best:${topic}`, JSON.stringify(lesson));
|
|
247
|
-
}
|
|
248
|
-
return `โ
Team lesson stored by **${author}**: \`${topic}\` (${outcome})\n๐ก ${what_worked.slice(0, 120)}`;
|
|
249
|
-
}
|
|
250
|
-
// โโ team_confirm โ knowledge governance (Phase 3) โโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
251
|
-
// A human reviewer endorses a stored lesson. The endorsement raises the
|
|
252
|
-
// lesson's recall ranking (see src/rerank.ts), so trusted, reviewed knowledge
|
|
253
|
-
// surfaces above unreviewed auto-learned entries.
|
|
254
|
-
case 'team_confirm': {
|
|
255
|
-
const { instance_id, topic, reviewer, level, note = '' } = args;
|
|
256
|
-
if (!instance_id)
|
|
257
|
-
return 'โ instance_id required';
|
|
258
|
-
if (!topic || !reviewer)
|
|
259
|
-
return 'โ Required: topic, reviewer';
|
|
260
|
-
const redis = await getConnection(instance_id);
|
|
261
|
-
// W6 enforcement: once governance is active, only reviewer+ may confirm
|
|
262
|
-
// lessons (vision: "nur reviewer dรผrfen team_confirm"). Open teams are
|
|
263
|
-
// unaffected. A denied confirm returns guidance, never throws.
|
|
264
|
-
const confirmDenied = await requireRole(redis, instance_id, reviewer, 'reviewer', 'confirm team lessons');
|
|
265
|
-
if (confirmDenied)
|
|
266
|
-
return confirmDenied;
|
|
267
|
-
// Role-aware review level: if the reviewer has an assigned role, that determines
|
|
268
|
-
// weight (admin/reviewer โ senior, contributor โ peer). The caller can still
|
|
269
|
-
// pass `level` explicitly to override (e.g. a guest reviewer without a role).
|
|
270
|
-
// This prevents self-promotion โ you can't claim senior by just passing level="senior".
|
|
271
|
-
const assignedRole = await getRole(redis, instance_id, reviewer);
|
|
272
|
-
const autoLevel = roleToReviewLevel(assignedRole);
|
|
273
|
-
// If the caller explicitly asked for 'peer' on a senior-role reviewer,
|
|
274
|
-
// honour that (downgrade is allowed; upgrade is not โ unless they have the role).
|
|
275
|
-
const reviewLevel = level === 'peer' ? 'peer'
|
|
276
|
-
: level === 'senior' && hasPermission(assignedRole, 'reviewer') ? 'senior'
|
|
277
|
-
: autoLevel;
|
|
278
|
-
const roleNote = assignedRole
|
|
279
|
-
? ` (role: ${ROLE_BADGE[assignedRole]} ${assignedRole})`
|
|
280
|
-
: ` (no role assigned โ peer weight applied)`;
|
|
281
|
-
const key = `cachly:lesson:best:${topic}`;
|
|
282
|
-
const raw = await redis.get(key);
|
|
283
|
-
const lesson = safeJsonParse(raw, null);
|
|
284
|
-
if (!lesson) {
|
|
285
|
-
return `๐ญ No best-solution lesson found for \`${topic}\`.\n\nA lesson must exist (store one with \`team_learn\` / \`learn_from_attempts\`) before it can be confirmed.`;
|
|
286
|
-
}
|
|
287
|
-
// Track distinct reviewers so endorsements can't be inflated by one person.
|
|
288
|
-
const endorsersKey = `cachly:lesson:endorsers:${topic}`;
|
|
289
|
-
const isNewEndorser = (await redis.sadd(endorsersKey, reviewer)) === 1;
|
|
290
|
-
await redis.expire(endorsersKey, 365 * 86400);
|
|
291
|
-
const endorsements = await redis.scard(endorsersKey);
|
|
292
|
-
// Senior beats peer; never downgrade an existing senior review to peer.
|
|
293
|
-
const prevLevel = lesson.review_level ?? '';
|
|
294
|
-
const effectiveLevel = prevLevel === 'senior' ? 'senior' : reviewLevel;
|
|
295
|
-
const updated = {
|
|
296
|
-
...lesson,
|
|
297
|
-
reviewed_by: reviewer,
|
|
298
|
-
review_level: effectiveLevel,
|
|
299
|
-
endorsements,
|
|
300
|
-
reviewed_at: new Date().toISOString(),
|
|
301
|
-
};
|
|
302
|
-
await redis.set(key, JSON.stringify(updated));
|
|
303
|
-
// Append an immutable review record for the audit trail.
|
|
304
|
-
const reviewLog = {
|
|
305
|
-
reviewer, level: reviewLevel, note: note.slice(0, 280), ts: new Date().toISOString(),
|
|
306
|
-
};
|
|
307
|
-
const logKey = `cachly:lesson:reviews:${topic}`;
|
|
308
|
-
await redis.rpush(logKey, JSON.stringify(reviewLog));
|
|
309
|
-
await redis.ltrim(logKey, -50, -1);
|
|
310
|
-
await redis.expire(logKey, 365 * 86400);
|
|
311
|
-
// Governance audit trail (W6): every confirm is recorded brain-wide so an
|
|
312
|
-
// admin can answer "who endorsed what, and when" for compliance.
|
|
313
|
-
await auditLog(redis, instance_id, {
|
|
314
|
-
action: 'team_confirm', actor: reviewer, topic, level: effectiveLevel,
|
|
315
|
-
});
|
|
316
|
-
const badge = effectiveLevel === 'senior' ? '๐ก๏ธ senior-reviewed' : 'โ๏ธ peer-reviewed';
|
|
317
|
-
const alreadyNote = isNewEndorser ? '' : ` (already endorsed by ${reviewer} โ no change to rank)`;
|
|
318
|
-
return `${badge} โ \`${topic}\` confirmed by **${reviewer}**${roleNote} (${endorsements} distinct endorsement${endorsements === 1 ? '' : 's'})${alreadyNote}.\n๐ This lesson now ranks higher in \`smart_recall\` / \`team_recall\`.${note ? `\n๐ ${note.slice(0, 200)}` : ''}`;
|
|
319
|
-
}
|
|
320
|
-
// โโ team_recall โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
321
|
-
case 'team_recall': {
|
|
322
|
-
const { instance_id, topic, author, limit = 10 } = args;
|
|
323
|
-
const redis = await getConnection(instance_id);
|
|
324
|
-
const lessonKeys = [];
|
|
325
|
-
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
326
|
-
await new Promise((resolve, reject) => {
|
|
327
|
-
lStream.on('data', (batch) => lessonKeys.push(...batch));
|
|
328
|
-
lStream.on('end', resolve);
|
|
329
|
-
lStream.on('error', reject);
|
|
330
|
-
});
|
|
331
|
-
let lessons = [];
|
|
332
|
-
if (lessonKeys.length > 0) {
|
|
333
|
-
const raws = await redis.mget(...lessonKeys);
|
|
334
|
-
for (const raw of raws) {
|
|
335
|
-
const l = safeJsonParse(raw ?? null, null);
|
|
336
|
-
if (l)
|
|
337
|
-
lessons.push(l);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
// Filter
|
|
341
|
-
if (topic) {
|
|
342
|
-
const t = topic.toLowerCase();
|
|
343
|
-
lessons = lessons.filter(l => l.topic.toLowerCase().includes(t) ||
|
|
344
|
-
(l.tags ?? []).some((tag) => tag.toLowerCase().includes(t)));
|
|
345
|
-
}
|
|
346
|
-
if (author) {
|
|
347
|
-
const a = author.toLowerCase();
|
|
348
|
-
lessons = lessons.filter(l => l.author?.toLowerCase().includes(a));
|
|
349
|
-
}
|
|
350
|
-
// Sort: reviewed lessons first (senior > peer), then by recall_count desc.
|
|
351
|
-
const reviewRank = (l) => l.reviewed_by ? (l.review_level === 'senior' ? 2 : 1) : 0;
|
|
352
|
-
lessons.sort((a, b) => (reviewRank(b) - reviewRank(a)) || ((b.recall_count ?? 0) - (a.recall_count ?? 0)));
|
|
353
|
-
lessons = lessons.slice(0, limit);
|
|
354
|
-
if (lessons.length === 0) {
|
|
355
|
-
return topic
|
|
356
|
-
? `๐ญ No team lessons found for \`${topic}\`.\n\nShared instance: add lessons with \`learn_from_attempts\` and include an \`author\` field.`
|
|
357
|
-
: `๐ญ No lessons in this brain yet.\n\nAll team members sharing this instance will see lessons here.`;
|
|
358
|
-
}
|
|
359
|
-
const lines = [`๐ฅ **Team Brain** โ ${lessons.length} lesson${lessons.length > 1 ? 's' : ''}`, ''];
|
|
360
|
-
for (const l of lessons) {
|
|
361
|
-
const emoji = l.outcome === 'success' ? 'โ
' : l.outcome === 'partial' ? 'โ ๏ธ' : 'โ';
|
|
362
|
-
const sev = l.severity === 'critical' ? '๐ด ' : l.severity === 'major' ? '๐ก ' : '';
|
|
363
|
-
const reviewBadge = l.reviewed_by
|
|
364
|
-
? (l.review_level === 'senior' ? ' ๐ก๏ธ' : ' โ๏ธ')
|
|
365
|
-
: '';
|
|
366
|
-
const authorStr = l.author ? ` ยท _by ${l.author}_` : '';
|
|
367
|
-
const reviewStr = l.reviewed_by
|
|
368
|
-
? ` ยท reviewed by ${l.reviewed_by}${(l.endorsements ?? 0) > 1 ? ` +${(l.endorsements ?? 0) - 1}` : ''}`
|
|
369
|
-
: '';
|
|
370
|
-
const recallStr = (l.recall_count ?? 0) > 0 ? ` ยท recalled ${l.recall_count}ร` : '';
|
|
371
|
-
const ago = Math.round((Date.now() - new Date(l.ts).getTime()) / 86400000);
|
|
372
|
-
const agoStr = ago === 0 ? 'today' : ago === 1 ? 'yesterday' : `${ago}d ago`;
|
|
373
|
-
lines.push(`${emoji} ${sev}**\`${l.topic}\`**${reviewBadge}${authorStr}${reviewStr}${recallStr} ยท ${agoStr}`);
|
|
374
|
-
lines.push(` ${l.what_worked.slice(0, 120)}`);
|
|
375
|
-
lines.push('');
|
|
376
|
-
}
|
|
377
|
-
return lines.join('\n');
|
|
378
|
-
}
|
|
379
|
-
// โโ team_synthesize โ Team Brain Synthesis โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
380
|
-
case 'team_synthesize': {
|
|
381
|
-
const { instance_id, topic } = args;
|
|
382
|
-
const redis = await getConnection(instance_id);
|
|
383
|
-
// Load history list for this topic (all authors' contributions)
|
|
384
|
-
const listKey = `cachly:lessons:${topic}`;
|
|
385
|
-
const all = await redis.lrange(listKey, 0, -1);
|
|
386
|
-
if (all.length < 2) {
|
|
387
|
-
return `๐ญ Need at least 2 entries for topic \`${topic}\` to synthesize.\n\nCurrently: ${all.length} entr${all.length === 1 ? 'y' : 'ies'}.\n\nHave team members store lessons via \`learn_from_attempts(topic="${topic}", ...)\`.`;
|
|
388
|
-
}
|
|
389
|
-
const entries = all.map(r => { try {
|
|
390
|
-
return JSON.parse(r);
|
|
391
|
-
}
|
|
392
|
-
catch {
|
|
393
|
-
return null;
|
|
394
|
-
} }).filter((e) => e !== null);
|
|
395
|
-
// Group by outcome
|
|
396
|
-
const successes = entries.filter(e => e.outcome === 'success');
|
|
397
|
-
const failures = entries.filter(e => e.outcome === 'failure');
|
|
398
|
-
const partials = entries.filter(e => e.outcome === 'partial');
|
|
399
|
-
const authors = [...new Set(entries.map(e => e.author).filter(Boolean))];
|
|
400
|
-
const hasMultiAuthor = authors.length > 1;
|
|
401
|
-
// Build canonical merged version
|
|
402
|
-
// what_worked: pick the most recent success, or longest for most detail
|
|
403
|
-
const bestSuccess = successes.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime())[0];
|
|
404
|
-
const whatWorkedCandidates = successes.map(e => e.what_worked).filter(w => w && w.length > 10);
|
|
405
|
-
const canonicalWorked = whatWorkedCandidates.sort((a, b) => b.length - a.length)[0] ?? bestSuccess?.what_worked ?? '';
|
|
406
|
-
// what_failed: union of all unique failure reasons
|
|
407
|
-
const allFailed = [...new Set([...failures, ...partials].map(e => e.what_failed).filter((w) => !!w && w.length > 5))];
|
|
408
|
-
const severities = entries.map(e => e.severity).filter(Boolean);
|
|
409
|
-
const canonicalSeverity = severities.includes('critical') ? 'critical' : severities.includes('major') ? 'major' : 'minor';
|
|
410
|
-
const lines = [
|
|
411
|
-
`๐งฌ **Team Brain Synthesis: \`${topic}\`**`,
|
|
412
|
-
`_${entries.length} entries from ${authors.length} author${authors.length === 1 ? '' : 's'}${hasMultiAuthor ? ` (${authors.join(', ')})` : ''} ยท ${successes.length} success ยท ${failures.length} failure ยท ${partials.length} partial_`,
|
|
413
|
-
'',
|
|
414
|
-
`**Canonical "what worked":**`,
|
|
415
|
-
`> ${canonicalWorked}`,
|
|
416
|
-
'',
|
|
417
|
-
allFailed.length > 0 ? `**Avoid (combined failures):**` : '',
|
|
418
|
-
...allFailed.map(f => `> โ ${f}`),
|
|
419
|
-
allFailed.length > 0 ? '' : '',
|
|
420
|
-
`**Suggested canonical lesson:**`,
|
|
421
|
-
'```',
|
|
422
|
-
`learn_from_attempts(`,
|
|
423
|
-
` topic = "${topic}",`,
|
|
424
|
-
` outcome = "success",`,
|
|
425
|
-
` what_worked = "${canonicalWorked.replace(/"/g, "'")}",`,
|
|
426
|
-
allFailed.length > 0 ? ` what_failed = "${allFailed[0].replace(/"/g, "'")}",` : '',
|
|
427
|
-
` severity = "${canonicalSeverity}",`,
|
|
428
|
-
`)`,
|
|
429
|
-
'```',
|
|
430
|
-
'',
|
|
431
|
-
hasMultiAuthor
|
|
432
|
-
? `๐ก _${authors.length} team members contributed to this synthesis. Store the canonical version to replace individual entries._`
|
|
433
|
-
: `๐ก _Single author โ more value when multiple team members contribute to the same topic._`,
|
|
434
|
-
].filter(l => l !== undefined).join('\n');
|
|
435
|
-
return lines;
|
|
436
|
-
}
|
|
437
|
-
// โโ brain_doctor โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
438
|
-
// โโ memory_crystalize โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
439
|
-
case 'memory_crystalize': {
|
|
440
|
-
const { instance_id, label: crystalLabel = '' } = args;
|
|
441
|
-
const redis = await getConnection(instance_id);
|
|
442
|
-
const now = new Date();
|
|
443
|
-
const week = `${now.getFullYear()}-W${String(Math.ceil((now.getDate() - now.getDay() + 10) / 7)).padStart(2, '0')}`;
|
|
444
|
-
const effectiveLabel = crystalLabel || `${now.toISOString().slice(0, 7)} Crystal`;
|
|
445
|
-
// Read session history
|
|
446
|
-
const sessionHistory = await redis.lrange('cachly:session:history', 0, 49);
|
|
447
|
-
// Read all auto-learned lessons
|
|
448
|
-
const allLessonKeys = [];
|
|
449
|
-
const ls = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
450
|
-
await new Promise((res, rej) => {
|
|
451
|
-
ls.on('data', (b) => allLessonKeys.push(...b));
|
|
452
|
-
ls.on('end', res);
|
|
453
|
-
ls.on('error', rej);
|
|
454
|
-
});
|
|
455
|
-
const allLessons = [];
|
|
456
|
-
if (allLessonKeys.length > 0) {
|
|
457
|
-
const raws = await redis.mget(...allLessonKeys);
|
|
458
|
-
for (const raw of raws) {
|
|
459
|
-
const l = safeJsonParse(raw ?? null, null);
|
|
460
|
-
if (l)
|
|
461
|
-
allLessons.push(l);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
// Group lessons by top-level category
|
|
465
|
-
const categoryMap = new Map();
|
|
466
|
-
for (const l of allLessons) {
|
|
467
|
-
const cat = l.topic.split(':')[0] || 'misc';
|
|
468
|
-
if (!categoryMap.has(cat))
|
|
469
|
-
categoryMap.set(cat, []);
|
|
470
|
-
categoryMap.get(cat).push(l);
|
|
471
|
-
}
|
|
472
|
-
// Build top patterns (most frequent categories with a representative insight)
|
|
473
|
-
const topPatterns = [];
|
|
474
|
-
for (const [cat, catLessons] of [...categoryMap.entries()].sort((a, b) => b[1].length - a[1].length).slice(0, 8)) {
|
|
475
|
-
const successLessons = catLessons.filter(l => l.outcome === 'success');
|
|
476
|
-
const best = successLessons[0] ?? catLessons[0];
|
|
477
|
-
if (!best)
|
|
478
|
-
continue;
|
|
479
|
-
topPatterns.push({
|
|
480
|
-
category: cat,
|
|
481
|
-
insight: best.what_worked.slice(0, 120),
|
|
482
|
-
count: catLessons.length,
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
const crystal = {
|
|
486
|
-
label: effectiveLabel,
|
|
487
|
-
ts: now.toISOString(),
|
|
488
|
-
session_count: sessionHistory.length,
|
|
489
|
-
lesson_count: allLessons.length,
|
|
490
|
-
top_patterns: topPatterns,
|
|
491
|
-
categories: [...categoryMap.keys()],
|
|
492
|
-
created_from: `${sessionHistory.length} sessions, ${allLessons.length} lessons`,
|
|
493
|
-
};
|
|
494
|
-
const crystalJson = JSON.stringify(crystal);
|
|
495
|
-
await redis.set('cachly:crystal:latest', crystalJson);
|
|
496
|
-
await redis.expire('cachly:crystal:latest', 90 * 86400);
|
|
497
|
-
await redis.set(`cachly:crystal:${week}`, crystalJson);
|
|
498
|
-
await redis.expire(`cachly:crystal:${week}`, 365 * 86400);
|
|
499
|
-
const lines = [
|
|
500
|
-
`๐ **Memory Crystal created: ${effectiveLabel}**`,
|
|
501
|
-
``,
|
|
502
|
-
`๐ Compressed: **${sessionHistory.length} sessions** + **${allLessons.length} lessons** โ ${topPatterns.length} top patterns`,
|
|
503
|
-
``,
|
|
504
|
-
`**Top patterns by category:**`,
|
|
505
|
-
...topPatterns.slice(0, 6).map(p => ` โข **${p.category}** (${p.count}ร): ${p.insight.slice(0, 90)}`),
|
|
506
|
-
``,
|
|
507
|
-
`๐ก This crystal will appear in every future \`session_start\` briefing.`,
|
|
508
|
-
`๐ก Re-run \`memory_crystalize\` monthly to keep it fresh.`,
|
|
509
|
-
];
|
|
510
|
-
return lines.join('\n');
|
|
511
|
-
}
|
|
512
|
-
// โโ team_crystallize (W8) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
513
|
-
// The team-wide, causal counter to per-user "Dreaming". Where memory_crystalize
|
|
514
|
-
// compresses ONE brain by category, team_crystallize surfaces what a single-user
|
|
515
|
-
// background process structurally cannot: which fixes solved structurally
|
|
516
|
-
// SIMILAR problems across MULTIPLE people. A pattern only crystallizes when โฅ2
|
|
517
|
-
// distinct authors converged on it โ that cross-person signal is the moat.
|
|
518
|
-
case 'team_crystallize': {
|
|
519
|
-
const { instance_id, min_authors = 2, label: tcLabel = '' } = args;
|
|
520
|
-
const redis = await getConnection(instance_id);
|
|
521
|
-
const now = new Date();
|
|
522
|
-
const minAuthors = Math.max(2, Number(min_authors) || 2);
|
|
523
|
-
// Load every best-lesson with its author + tags (the cross-person signal).
|
|
524
|
-
const keys = [];
|
|
525
|
-
const stream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
526
|
-
await new Promise((res, rej) => {
|
|
527
|
-
stream.on('data', (b) => keys.push(...b));
|
|
528
|
-
stream.on('end', res);
|
|
529
|
-
stream.on('error', rej);
|
|
530
|
-
});
|
|
531
|
-
const lessons = [];
|
|
532
|
-
if (keys.length > 0) {
|
|
533
|
-
const raws = await redis.mget(...keys);
|
|
534
|
-
for (const raw of raws) {
|
|
535
|
-
const l = safeJsonParse(raw ?? null, null);
|
|
536
|
-
if (l && l.outcome === 'success' && l.author)
|
|
537
|
-
lessons.push(l);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
// Derive a structural "concept key" for each lesson: the topic namespace plus
|
|
541
|
-
// its most significant problem token, so "db:pool-exhaustion" and
|
|
542
|
-
// "payments:connection-pool" can converge via the shared "pool" concept.
|
|
543
|
-
const STOP = new Set(['the', 'and', 'for', 'with', 'from', 'that', 'this', 'fix', 'bug', 'error', 'issue', 'use', 'via', 'add', 'set', 'team']);
|
|
544
|
-
const conceptOf = (l) => {
|
|
545
|
-
const [ns, ...rest] = l.topic.split(':');
|
|
546
|
-
// Tags are deliberate concept markers โ two people who both tag a lesson
|
|
547
|
-
// "pool" intend the same concept even when their topics differ. Prefer a
|
|
548
|
-
// shared, intentional tag over a topic-derived token. Alphabetically-first
|
|
549
|
-
// significant tag keeps the choice deterministic across authors.
|
|
550
|
-
const sigTags = (l.tags ?? [])
|
|
551
|
-
.map(t => t.toLowerCase().trim())
|
|
552
|
-
.filter(t => t.length > 3 && !STOP.has(t))
|
|
553
|
-
.sort();
|
|
554
|
-
if (sigTags.length > 0)
|
|
555
|
-
return sigTags[0];
|
|
556
|
-
// No usable tag โ fall back to the longest topic-tail token.
|
|
557
|
-
const tail = rest.join('-').toLowerCase();
|
|
558
|
-
const tokens = tail.replace(/[^a-z0-9\s-]/g, ' ').split(/[\s-]+/)
|
|
559
|
-
.filter(t => t.length > 3 && !STOP.has(t));
|
|
560
|
-
return tokens.sort((a, b) => b.length - a.length || a.localeCompare(b))[0] ?? ns;
|
|
561
|
-
};
|
|
562
|
-
const clusters = new Map();
|
|
563
|
-
for (const l of lessons) {
|
|
564
|
-
const concept = conceptOf(l);
|
|
565
|
-
let c = clusters.get(concept);
|
|
566
|
-
if (!c) {
|
|
567
|
-
c = { concept, authors: new Set(), lessons: [], namespaces: new Set(), bestFix: l };
|
|
568
|
-
clusters.set(concept, c);
|
|
569
|
-
}
|
|
570
|
-
c.authors.add(l.author.toLowerCase());
|
|
571
|
-
c.lessons.push(l);
|
|
572
|
-
c.namespaces.add(l.topic.split(':')[0] || 'misc');
|
|
573
|
-
// Best fix = highest community trust (endorsements > confirm_count), senior-reviewed wins ties.
|
|
574
|
-
const score = (x) => (x.endorsements ?? 0) * 10 + (x.confirm_count ?? 0) + (x.review_level === 'senior' ? 5 : 0);
|
|
575
|
-
if (score(l) > score(c.bestFix))
|
|
576
|
-
c.bestFix = l;
|
|
577
|
-
}
|
|
578
|
-
// A pattern crystallizes only when enough DISTINCT people converged on it.
|
|
579
|
-
const crossPerson = [...clusters.values()]
|
|
580
|
-
.filter(c => c.authors.size >= minAuthors)
|
|
581
|
-
.sort((a, b) => (b.authors.size - a.authors.size) || (b.lessons.length - a.lessons.length));
|
|
582
|
-
const patterns = crossPerson.slice(0, 10).map(c => ({
|
|
583
|
-
concept: c.concept,
|
|
584
|
-
author_count: c.authors.size,
|
|
585
|
-
authors: [...c.authors],
|
|
586
|
-
lesson_count: c.lessons.length,
|
|
587
|
-
spans: [...c.namespaces],
|
|
588
|
-
convergent_fix: c.bestFix.what_worked.slice(0, 200),
|
|
589
|
-
fix_topic: c.bestFix.topic,
|
|
590
|
-
}));
|
|
591
|
-
// Persist the team crystal alongside (not overwriting) the per-brain crystal.
|
|
592
|
-
const effectiveLabel = tcLabel || `${now.toISOString().slice(0, 7)} Team Crystal`;
|
|
593
|
-
const crystal = {
|
|
594
|
-
label: effectiveLabel, ts: now.toISOString(), kind: 'team',
|
|
595
|
-
contributors: new Set(lessons.map(l => l.author.toLowerCase())).size,
|
|
596
|
-
analysed: lessons.length, patterns,
|
|
597
|
-
};
|
|
598
|
-
await redis.set('cachly:crystal:team:latest', JSON.stringify(crystal));
|
|
599
|
-
await redis.expire('cachly:crystal:team:latest', 180 * 86400);
|
|
600
|
-
if (patterns.length === 0) {
|
|
601
|
-
return [
|
|
602
|
-
`๐ **Team Crystal โ not enough cross-person convergence yet**`,
|
|
603
|
-
``,
|
|
604
|
-
`Analysed **${lessons.length}** attributed success lessons from **${crystal.contributors}** contributor${crystal.contributors !== 1 ? 's' : ''}, but no concept had **${minAuthors}+ distinct authors** converge on it yet.`,
|
|
605
|
-
``,
|
|
606
|
-
`This is the one thing a per-user memory can't build โ it needs your team to accumulate shared, attributed lessons.`,
|
|
607
|
-
`Keep using \`learn_from_attempts(author="...")\` / \`team_learn\`; re-run as the team grows.`,
|
|
608
|
-
].join('\n');
|
|
609
|
-
}
|
|
610
|
-
const lines = [
|
|
611
|
-
`๐ **Team Crystal created: ${effectiveLabel}**`,
|
|
612
|
-
``,
|
|
613
|
-
`๐งฌ **${patterns.length} cross-person pattern${patterns.length !== 1 ? 's' : ''}** โ fixes that ${minAuthors}+ teammates independently converged on.`,
|
|
614
|
-
`_Compressed from ${lessons.length} attributed lessons across ${crystal.contributors} contributors. This is the team-wide, causal layer a per-user "Dreaming" process structurally cannot produce._`,
|
|
615
|
-
``,
|
|
616
|
-
];
|
|
617
|
-
for (const p of patterns.slice(0, 6)) {
|
|
618
|
-
const spanNote = p.spans.length > 1 ? ` ยท spans ${p.spans.map(s => `\`${s}\``).join(', ')}` : '';
|
|
619
|
-
lines.push(`### ๐งฉ \`${p.concept}\` โ ${p.author_count} people converged${spanNote}`, `**Convergent fix** (from \`${p.fix_topic}\`): ${p.convergent_fix}`, `**Who solved it together:** ${p.authors.map(a => `@${a}`).join(', ')} _(ask them โ they've each hit this)_`, ``);
|
|
620
|
-
}
|
|
621
|
-
lines.push(`---`, `๐ก Surfaces in \`crystal_view\` and onboarding. Re-run \`team_crystallize\` monthly.`);
|
|
622
|
-
return lines.join('\n');
|
|
623
|
-
}
|
|
624
|
-
case 'brain_doctor': {
|
|
625
|
-
const { instance_id, workspace_path: drWorkspacePath = '' } = args;
|
|
626
|
-
const redis = await getConnection(instance_id);
|
|
627
|
-
const issues = [];
|
|
628
|
-
const checks = [];
|
|
629
|
-
// Count lessons
|
|
630
|
-
const lessonKeys = [];
|
|
631
|
-
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
632
|
-
await new Promise((resolve, reject) => {
|
|
633
|
-
lStream.on('data', (batch) => lessonKeys.push(...batch));
|
|
634
|
-
lStream.on('end', resolve);
|
|
635
|
-
lStream.on('error', reject);
|
|
636
|
-
});
|
|
637
|
-
// Count context
|
|
638
|
-
let ctxCount = 0;
|
|
639
|
-
const ctxStream = redis.scanStream({ match: 'cachly:ctx:*', count: 200 });
|
|
640
|
-
await new Promise((resolve, reject) => {
|
|
641
|
-
ctxStream.on('data', (batch) => {
|
|
642
|
-
ctxCount += batch.filter((k) => !k.endsWith(':meta')).length;
|
|
643
|
-
});
|
|
644
|
-
ctxStream.on('end', resolve);
|
|
645
|
-
ctxStream.on('error', reject);
|
|
646
|
-
});
|
|
647
|
-
const lessons = [];
|
|
648
|
-
if (lessonKeys.length > 0) {
|
|
649
|
-
const raws = await redis.mget(...lessonKeys);
|
|
650
|
-
for (const raw of raws) {
|
|
651
|
-
const l = safeJsonParse(raw ?? null, null);
|
|
652
|
-
if (l)
|
|
653
|
-
lessons.push(l);
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
// Last session
|
|
657
|
-
const lastSessionRaw = await redis.get('cachly:session:last');
|
|
658
|
-
const lastSession = safeJsonParse(lastSessionRaw, null);
|
|
659
|
-
// Open failures
|
|
660
|
-
const openFailures = lessons.filter(l => l.outcome === 'failure' || l.outcome === 'partial');
|
|
661
|
-
// Unused lessons (never recalled)
|
|
662
|
-
const unusedLessons = lessons.filter(l => (l.recall_count ?? 0) === 0);
|
|
663
|
-
// Critical lessons
|
|
664
|
-
const criticalLessons = lessons.filter(l => l.severity === 'critical');
|
|
665
|
-
// Confidence decay analysis
|
|
666
|
-
const staleLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) < CONFIDENCE_STALE_VALUE);
|
|
667
|
-
const warnLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) >= CONFIDENCE_STALE_VALUE && calculateConfidence(l) < CONFIDENCE_WARN_VALUE);
|
|
668
|
-
const withAudit = lessons.filter(l => (l.audit_trail ?? []).length > 1);
|
|
669
|
-
const teamLessons = lessons.filter(l => l.author);
|
|
670
|
-
const uniqueAuthors = new Set(lessons.map(l => l.author).filter(Boolean));
|
|
671
|
-
// Effective IQ boost: total recalls / lessons (how much the brain actually helped)
|
|
672
|
-
const totalRecalls = lessons.reduce((sum, l) => sum + (l.recall_count ?? 0), 0);
|
|
673
|
-
const iqBoostPct = lessons.length > 0 ? Math.min(100, Math.round((totalRecalls / lessons.length) * 10)) : 0;
|
|
674
|
-
// โโ Deckung des Bedeutungsabgleichs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
675
|
-
//
|
|
676
|
-
// Gemessen am 20.08.2026 gegen die Produktion: `cachly:lesson:vec:*` war
|
|
677
|
-
// LEER โ 0 Vektoren bei 506 Lektionen. Damit lief der Bedeutungsabgleich
|
|
678
|
-
// dort nie: `brain.ts` steigt bei `vektorbestand.groesse === 0` aus und
|
|
679
|
-
// faellt auf den reinen Wortabgleich zurueck.
|
|
680
|
-
//
|
|
681
|
-
// Von aussen war dieser Zustand von "alles in Ordnung" nicht zu
|
|
682
|
-
// unterscheiden โ die Suche lieferte weiter Ergebnisse, nur schlechtere.
|
|
683
|
-
// Genau das ist die Fehlerklasse "Stille wird als gruen gebucht": es gab
|
|
684
|
-
// eine Zahl fuer alles Moegliche, aber keine fuer DAS.
|
|
685
|
-
const zaehleSchluessel = async (muster) => {
|
|
686
|
-
let n = 0;
|
|
687
|
-
const st = redis.scanStream({ match: muster, count: 200 });
|
|
688
|
-
await new Promise((resolve, reject) => {
|
|
689
|
-
st.on('data', (batch) => { n += batch.length; });
|
|
690
|
-
st.on('end', resolve);
|
|
691
|
-
st.on('error', reject);
|
|
692
|
-
});
|
|
693
|
-
return n;
|
|
694
|
-
};
|
|
695
|
-
const vektorZahl = await zaehleSchluessel('cachly:lesson:vec:*');
|
|
696
|
-
const namensZahl = await zaehleSchluessel('cachly:lesson:vecname:*');
|
|
697
|
-
const eingangZahl = await zaehleSchluessel('cachly:lesson:eing:*');
|
|
698
|
-
const pfadZahl = await zaehleSchluessel('cachly:lesson:pfad:*');
|
|
699
|
-
const deckung = lessonKeys.length > 0
|
|
700
|
-
? Math.round((vektorZahl / lessonKeys.length) * 100)
|
|
701
|
-
: 100;
|
|
702
|
-
const urteil = beurteileDeckung(lessonKeys.length, vektorZahl);
|
|
703
|
-
if (lessonKeys.length > 0) {
|
|
704
|
-
if (urteil === 'aus') {
|
|
705
|
-
issues.push('semantic search is OFF: not a single lesson has an embedding '
|
|
706
|
-
+ `(0 of ${lessonKeys.length}). Recall falls back to keyword matching only. `
|
|
707
|
-
+ 'Fix: run the backfill (sdk/mcp/src/eingaenge-nachruesten.ts).');
|
|
708
|
-
checks.push(`๐ด **Semantic search: OFF** โ 0 of ${lessonKeys.length} lessons have an embedding`);
|
|
709
|
-
}
|
|
710
|
-
else if (urteil === 'luecke') {
|
|
711
|
-
issues.push(`only ${deckung}% of lessons have an embedding (${vektorZahl} of ${lessonKeys.length}) โ `
|
|
712
|
-
+ 'the rest can never be found by meaning.');
|
|
713
|
-
checks.push(`๐ก **Semantic coverage: ${deckung}%** โ ${lessonKeys.length - vektorZahl} lessons without an embedding`);
|
|
714
|
-
}
|
|
715
|
-
else {
|
|
716
|
-
checks.push(`โ
**Semantic coverage: ${deckung}%** โ ${vektorZahl} full-text, `
|
|
717
|
-
+ `${namensZahl} topic-name, ${eingangZahl} with error-text entries`
|
|
718
|
-
+ (pfadZahl > 0 ? `, ${pfadZahl} with learned question paths` : ''));
|
|
719
|
-
// Die Trampelpfade sind das einzige, was MIT DER NUTZUNG waechst.
|
|
720
|
-
// Steht die Zahl auf null, obwohl schon lange gesucht wird, lernt
|
|
721
|
-
// das Gedaechtnis nichts dazu โ das ist eine eigene Auskunft.
|
|
722
|
-
if (pfadZahl === 0 && lessonKeys.length >= 20) {
|
|
723
|
-
checks.push('๐ค **No learned question paths yet** โ a question that finds the same '
|
|
724
|
-
+ 'lesson twice becomes its own entry. Needs repeated real use.');
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
// Quality score (0-100)
|
|
729
|
-
let score = 50;
|
|
730
|
-
if (lessonKeys.length >= 5)
|
|
731
|
-
score += 10;
|
|
732
|
-
if (lessonKeys.length >= 20)
|
|
733
|
-
score += 10;
|
|
734
|
-
if (ctxCount >= 3)
|
|
735
|
-
score += 10;
|
|
736
|
-
if (ctxCount >= 10)
|
|
737
|
-
score += 5;
|
|
738
|
-
if (lastSession)
|
|
739
|
-
score += 10;
|
|
740
|
-
if (openFailures.length === 0)
|
|
741
|
-
score += 5;
|
|
742
|
-
const unusedRatio = lessons.length > 0 ? unusedLessons.length / lessons.length : 0;
|
|
743
|
-
if (unusedRatio < 0.5)
|
|
744
|
-
score += 10;
|
|
745
|
-
if (staleLessons.length === 0)
|
|
746
|
-
score += 5;
|
|
747
|
-
if (uniqueAuthors.size >= 2)
|
|
748
|
-
score += 5; // team collaboration bonus
|
|
749
|
-
// Ohne Einbettungen laeuft die halbe Suche nicht โ das darf keine
|
|
750
|
-
// Randnotiz sein, sondern muss die Zahl druecken, die oben steht.
|
|
751
|
-
if (urteil !== 'gut')
|
|
752
|
-
score -= 20;
|
|
753
|
-
score = Math.max(0, Math.min(100, score));
|
|
754
|
-
const scoreEmoji = score >= 80 ? '๐ข' : score >= 50 ? '๐ก' : '๐ด';
|
|
755
|
-
const iqEmoji = iqBoostPct >= 50 ? '๐' : iqBoostPct >= 20 ? '๐' : '๐ค';
|
|
756
|
-
checks.push(`${scoreEmoji} **Brain Quality Score: ${score}/100**`);
|
|
757
|
-
checks.push(`${iqEmoji} **Effective IQ Boost: ${iqBoostPct}%** (${totalRecalls} recalls across ${lessons.length} lessons)`);
|
|
758
|
-
checks.push(`๐ **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical ยท ${withAudit.length} with audit trail ยท ${teamLessons.length} from team)`);
|
|
759
|
-
checks.push(`๐พ **Context entries:** ${ctxCount}`);
|
|
760
|
-
checks.push(`๐ฏ **Confidence:** ${lessons.length - staleLessons.length - warnLessons.length} fresh ยท ${warnLessons.length} warn ยท ${staleLessons.length} stale`);
|
|
761
|
-
checks.push(`โฑ๏ธ **Decay config:** warn after ${CONFIDENCE_WARN_DAYS}d ยท stale after ${CONFIDENCE_STALE_DAYS}d`);
|
|
762
|
-
checks.push(formatiereBauabdruck(bauabdruck));
|
|
763
|
-
// โโ Was tut der Speicher WIRKLICH โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
764
|
-
//
|
|
765
|
-
// Neu am 20.08.2026. Bis dahin meldete brain_doctor, wie viele Lektionen
|
|
766
|
-
// da sind โ aber nicht, ob der Speicher sie behaelt. Eine Instanz lief
|
|
767
|
-
// monatelang auf `allkeys-lru` und haette bei Platzmangel die aeltesten
|
|
768
|
-
// Lektionen still geloescht. Der Code war seit April richtig; ein
|
|
769
|
-
// Container behaelt aber den Startbefehl, mit dem er erzeugt wurde.
|
|
770
|
-
//
|
|
771
|
-
// Ein Test beweist die Absicht. Diese Zeilen messen den Zustand.
|
|
772
|
-
try {
|
|
773
|
-
const info = await redis.info('memory');
|
|
774
|
-
let richtlinie = null;
|
|
775
|
-
try {
|
|
776
|
-
const cfg = await redis.config('GET', 'maxmemory-policy');
|
|
777
|
-
richtlinie = Array.isArray(cfg) && cfg.length > 1 ? String(cfg[1]) : null;
|
|
778
|
-
}
|
|
779
|
-
catch {
|
|
780
|
-
// CONFIG GET ist auf manchen verwalteten Speichern gesperrt. Dann
|
|
781
|
-
// bleibt richtlinie null โ und beurteileSpeicher sagt "nicht
|
|
782
|
-
// gemessen" statt "in Ordnung".
|
|
783
|
-
richtlinie = null;
|
|
784
|
-
}
|
|
785
|
-
const lage = {
|
|
786
|
-
benutzt: zahlAusInfo(info, 'used_memory'),
|
|
787
|
-
grenze: zahlAusInfo(info, 'maxmemory'),
|
|
788
|
-
richtlinie,
|
|
789
|
-
};
|
|
790
|
-
for (const u of beurteileSpeicher(lage)) {
|
|
791
|
-
const zeichen = u.art === 'fehler' ? '๐ด' : u.art === 'hinweis' ? '๐ก' : u.art === 'ungemessen' ? 'โช' : '๐ข';
|
|
792
|
-
const zeile = `${zeichen} **Speicher:** ${u.text}`;
|
|
793
|
-
if (u.art === 'fehler' || u.art === 'ungemessen')
|
|
794
|
-
issues.push(zeile);
|
|
795
|
-
else
|
|
796
|
-
checks.push(zeile);
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
catch {
|
|
800
|
-
// Auch das Scheitern der Messung ist eine Nachricht, keine Stille.
|
|
801
|
-
issues.push('โช **Speicher:** Lage nicht abfragbar โ unbekannt, ob dieser Speicher bei Platzmangel still loescht.');
|
|
802
|
-
}
|
|
803
|
-
if (uniqueAuthors.size >= 2) {
|
|
804
|
-
checks.push(`๐ฅ **Team:** ${uniqueAuthors.size} contributors (${[...uniqueAuthors].join(', ')})`);
|
|
805
|
-
}
|
|
806
|
-
// Stale index detection
|
|
807
|
-
try {
|
|
808
|
-
const lastIndexRaw = await redis.get('cachly:index:last_run');
|
|
809
|
-
if (lastIndexRaw) {
|
|
810
|
-
const lastIndexAge = Math.round((Date.now() - new Date(lastIndexRaw).getTime()) / 86_400_000);
|
|
811
|
-
if (lastIndexAge > 7) {
|
|
812
|
-
issues.push(`๐ Index is ${lastIndexAge}d stale โ run \`index_project\` to re-sync semantic search`);
|
|
813
|
-
}
|
|
814
|
-
else {
|
|
815
|
-
checks.push(`๐๏ธ **Semantic index:** ${lastIndexAge}d old (fresh)`);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
else {
|
|
819
|
-
issues.push(`๐ก No semantic index โ run \`index_project(dir="<your-src>")\` to enable semantic search`);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
catch { /* non-critical */ }
|
|
823
|
-
// Memory crystal status
|
|
824
|
-
try {
|
|
825
|
-
const crystalRaw = await redis.get('cachly:crystal:latest');
|
|
826
|
-
if (crystalRaw) {
|
|
827
|
-
const crystal = JSON.parse(crystalRaw);
|
|
828
|
-
const crystalAge = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
|
|
829
|
-
checks.push(`๐ **Memory Crystal:** ${crystal.label} (${crystalAge}d ago)`);
|
|
830
|
-
if (crystalAge > 30)
|
|
831
|
-
issues.push(`๐ก Memory Crystal is ${crystalAge}d old โ re-run \`memory_crystalize\` to compress new sessions`);
|
|
832
|
-
}
|
|
833
|
-
else if (lessonKeys.length >= 10) {
|
|
834
|
-
issues.push(`๐ก ${lessonKeys.length} lessons but no Memory Crystal โ run \`memory_crystalize\` to compress wisdom`);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
catch { /* non-critical */ }
|
|
838
|
-
// openclaw cross-promo (check package.json in workspace)
|
|
839
|
-
if (drWorkspacePath) {
|
|
840
|
-
try {
|
|
841
|
-
const pkgPath = drWorkspacePath.replace(/\/$/, '') + '/package.json';
|
|
842
|
-
const pkgRaw = readFileSync(pkgPath, 'utf-8');
|
|
843
|
-
const pkg = JSON.parse(pkgRaw);
|
|
844
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
845
|
-
const hasLLMDep = ['openai', '@anthropic-ai/sdk', '@google/generative-ai', 'mistralai', 'cohere-ai'].some(d => d in allDeps);
|
|
846
|
-
const hasOpenclaw = '@cachly-dev/openclaw' in allDeps;
|
|
847
|
-
if (hasLLMDep && !hasOpenclaw) {
|
|
848
|
-
issues.push(`๐ก **openclaw missing:** you use LLM APIs (${Object.keys(allDeps).filter(d => ['openai', '@anthropic-ai/sdk'].includes(d)).join(', ')}) but not \`@cachly-dev/openclaw\``);
|
|
849
|
-
issues.push(` โ \`npm install @cachly-dev/openclaw\` cuts LLM costs 60โ90% with 3 lines of code`);
|
|
850
|
-
}
|
|
851
|
-
else if (hasOpenclaw) {
|
|
852
|
-
checks.push(`โ
**@cachly-dev/openclaw installed** (LLM cost caching active)`);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
catch { /* no package.json or unreadable */ }
|
|
856
|
-
}
|
|
857
|
-
if (lastSession) {
|
|
858
|
-
const ageMin = Math.round((Date.now() - new Date(lastSession.ts).getTime()) / 60000);
|
|
859
|
-
const ageStr = ageMin < 60 ? `${ageMin}m` : ageMin < 1440 ? `${Math.round(ageMin / 60)}h` : `${Math.round(ageMin / 1440)}d`;
|
|
860
|
-
checks.push(`๐ **Last session:** ${ageStr} ago`);
|
|
861
|
-
}
|
|
862
|
-
else {
|
|
863
|
-
issues.push('โ No session history โ call `session_start` + `session_end` to start tracking');
|
|
864
|
-
}
|
|
865
|
-
if (lessonKeys.length === 0) {
|
|
866
|
-
issues.push('โ No lessons โ call `learn_from_attempts` after solving bugs');
|
|
867
|
-
}
|
|
868
|
-
else if (lessonKeys.length < 5) {
|
|
869
|
-
issues.push(`๐ก Only ${lessonKeys.length} lessons โ add more after each problem solved`);
|
|
870
|
-
}
|
|
871
|
-
if (iqBoostPct === 0 && lessons.length >= 5) {
|
|
872
|
-
issues.push(`๐ค **IQ Boost is 0%** โ lessons exist but are never recalled. Use \`recall_best_solution\` BEFORE tasks.`);
|
|
873
|
-
}
|
|
874
|
-
if (ctxCount === 0) {
|
|
875
|
-
issues.push('๐ก No context โ use `remember_context` to cache architecture docs, ADRs, etc.');
|
|
876
|
-
}
|
|
877
|
-
if (openFailures.length > 0) {
|
|
878
|
-
issues.push(`โ ๏ธ ${openFailures.length} unresolved failure${openFailures.length > 1 ? 's' : ''}: ${openFailures.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
|
|
879
|
-
}
|
|
880
|
-
if (staleLessons.length > 0) {
|
|
881
|
-
issues.push(`๐ด ${staleLessons.length} STALE lesson${staleLessons.length > 1 ? 's' : ''} (>${CONFIDENCE_STALE_DAYS}d, confidence <${CONFIDENCE_STALE_VALUE * 100}%): ${staleLessons.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
|
|
882
|
-
issues.push(` โ Re-verify with \`recall_best_solution\` to reset confidence clock`);
|
|
883
|
-
}
|
|
884
|
-
if (warnLessons.length > 0) {
|
|
885
|
-
issues.push(`โ ๏ธ ${warnLessons.length} lesson${warnLessons.length > 1 ? 's' : ''} aging (>${CONFIDENCE_WARN_DAYS}d): ${warnLessons.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
|
|
886
|
-
}
|
|
887
|
-
if (unusedRatio > 0.7 && lessons.length > 5) {
|
|
888
|
-
issues.push(`๐ก ${unusedLessons.length} lessons never recalled โ verify topics match your workflow`);
|
|
889
|
-
}
|
|
890
|
-
const divider = 'โ'.repeat(48);
|
|
891
|
-
const lines = [
|
|
892
|
-
`๐ฉบ **Brain Doctor**`,
|
|
893
|
-
`\`${divider}\``,
|
|
894
|
-
'',
|
|
895
|
-
'**๐ Health Checks**',
|
|
896
|
-
...checks.map(c => ' ' + c),
|
|
897
|
-
'',
|
|
898
|
-
];
|
|
899
|
-
if (issues.length > 0) {
|
|
900
|
-
lines.push(`**๐ง Action Items** (${issues.length} issue${issues.length > 1 ? 's' : ''})`);
|
|
901
|
-
issues.forEach((item, idx) => lines.push(` ${idx + 1}. ${item}`));
|
|
902
|
-
lines.push('');
|
|
903
|
-
lines.push(`_Fix the action items above, then re-run \`brain_doctor\` to verify._`);
|
|
904
|
-
}
|
|
905
|
-
else {
|
|
906
|
-
lines.push('**โ
All checks passed** โ Brain is healthy!');
|
|
907
|
-
lines.push('');
|
|
908
|
-
lines.push('_Keep the brain sharp: `session_start` โ work โ `learn_from_attempts` โ `session_end`_');
|
|
909
|
-
}
|
|
910
|
-
return lines.join('\n');
|
|
911
|
-
}
|
|
912
|
-
// โโ brain_hygiene โ autonomous Brain maintenance sweep โโโโโโโโโโโโโโโโโโโโ
|
|
913
|
-
//
|
|
914
|
-
// Runs a full lesson sweep and applies three automated hygiene rules:
|
|
915
|
-
//
|
|
916
|
-
// 1. Decay flagging: lessons where confidence < PROVISIONAL_THRESHOLD
|
|
917
|
-
// (default 0.5) are marked state="provisional". They still appear in
|
|
918
|
-
// smart_recall with a โ ๏ธ badge but callers know to re-verify.
|
|
919
|
-
//
|
|
920
|
-
// 2. Archival: lessons that are provisional AND have recall_count < 2 AND
|
|
921
|
-
// are older than ARCHIVE_DAYS (default 30) are moved to state="archived".
|
|
922
|
-
// Archived lessons are excluded from smart_recall results but kept for
|
|
923
|
-
// audit_trail inspection.
|
|
924
|
-
//
|
|
925
|
-
// 3. Contradiction auto-resolution: when the same topic has both a failure
|
|
926
|
-
// and a success lesson, and the success has recall_count โฅ 3ร the
|
|
927
|
-
// failure's recall_count, the failure lesson is archived automatically.
|
|
928
|
-
// The resolution is recorded in both lessons' audit_trail.
|
|
929
|
-
//
|
|
930
|
-
// dry_run=true (default false) reports what would change without writing.
|
|
931
|
-
case 'brain_hygiene': {
|
|
932
|
-
const { instance_id, dry_run = false, provisional_threshold = 0.5, archive_days = 30, } = args;
|
|
933
|
-
const redis = await getConnection(instance_id);
|
|
934
|
-
const lessonKeys = [];
|
|
935
|
-
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
936
|
-
await new Promise((resolve, reject) => {
|
|
937
|
-
lStream.on('data', (batch) => lessonKeys.push(...batch));
|
|
938
|
-
lStream.on('end', resolve);
|
|
939
|
-
lStream.on('error', reject);
|
|
940
|
-
});
|
|
941
|
-
if (lessonKeys.length === 0) {
|
|
942
|
-
return '๐งน **Brain Hygiene** โ no lessons found. Nothing to sweep.';
|
|
943
|
-
}
|
|
944
|
-
const rawValues = await redis.mget(...lessonKeys);
|
|
945
|
-
const lessons = [];
|
|
946
|
-
for (let i = 0; i < lessonKeys.length; i++) {
|
|
947
|
-
const raw = rawValues[i];
|
|
948
|
-
const l = safeJsonParse(raw ?? null, null);
|
|
949
|
-
if (l)
|
|
950
|
-
lessons.push({ ...l, _key: lessonKeys[i] });
|
|
951
|
-
}
|
|
952
|
-
const nowMs = Date.now();
|
|
953
|
-
const provisionalKeys = [];
|
|
954
|
-
const archivedKeys = [];
|
|
955
|
-
const conflictResolutions = [];
|
|
956
|
-
const updates = new Map(); // key โ updated lesson
|
|
957
|
-
// โโ Rule 1 + 2: decay flagging + archival โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
958
|
-
for (const lesson of lessons) {
|
|
959
|
-
const currentState = lesson.state ?? 'active';
|
|
960
|
-
if (currentState === 'archived')
|
|
961
|
-
continue; // already done
|
|
962
|
-
const conf = calculateConfidence(lesson);
|
|
963
|
-
const refMs = new Date(lesson.verified_at ?? lesson.ts).getTime();
|
|
964
|
-
const ageDays = (nowMs - refMs) / 86400000;
|
|
965
|
-
if (conf <= provisional_threshold) {
|
|
966
|
-
const recallCount = lesson.recall_count ?? 0;
|
|
967
|
-
const shouldArchive = currentState === 'provisional' &&
|
|
968
|
-
recallCount < 2 &&
|
|
969
|
-
ageDays > archive_days;
|
|
970
|
-
if (shouldArchive) {
|
|
971
|
-
archivedKeys.push(lesson.topic);
|
|
972
|
-
if (!dry_run) {
|
|
973
|
-
const updated = {
|
|
974
|
-
...lesson,
|
|
975
|
-
state: 'archived',
|
|
976
|
-
audit_trail: [
|
|
977
|
-
...(lesson.audit_trail ?? []),
|
|
978
|
-
{ ts: new Date().toISOString(), action: 'archived', prev_state: 'provisional' },
|
|
979
|
-
],
|
|
980
|
-
};
|
|
981
|
-
updates.set(lesson._key, updated);
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
else if (currentState === 'active') {
|
|
985
|
-
provisionalKeys.push(lesson.topic);
|
|
986
|
-
if (!dry_run) {
|
|
987
|
-
const updated = {
|
|
988
|
-
...lesson,
|
|
989
|
-
state: 'provisional',
|
|
990
|
-
audit_trail: [
|
|
991
|
-
...(lesson.audit_trail ?? []),
|
|
992
|
-
{ ts: new Date().toISOString(), action: 'flagged_provisional', prev_state: 'active' },
|
|
993
|
-
],
|
|
994
|
-
};
|
|
995
|
-
updates.set(lesson._key, updated);
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
// โโ Rule 3: orphan-failure cleanup โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1001
|
-
// Each topic has exactly one `best` lesson (upsert-on-write model).
|
|
1002
|
-
// A failure lesson that was never recalled (recall_count === 0) and is
|
|
1003
|
-
// older than archive_days days represents an unresolved one-off error
|
|
1004
|
-
// that no one ever found useful. Archive it to keep the recall surface clean.
|
|
1005
|
-
for (const lesson of lessons) {
|
|
1006
|
-
const currentState = lesson.state ?? 'active';
|
|
1007
|
-
if (currentState === 'archived')
|
|
1008
|
-
continue;
|
|
1009
|
-
// Skip if Rule 1/2 already scheduled archival for this key.
|
|
1010
|
-
if (updates.has(lesson._key) && updates.get(lesson._key).state === 'archived')
|
|
1011
|
-
continue;
|
|
1012
|
-
if (lesson.outcome !== 'failure')
|
|
1013
|
-
continue;
|
|
1014
|
-
const recallCount = lesson.recall_count ?? 0;
|
|
1015
|
-
if (recallCount > 0)
|
|
1016
|
-
continue; // still useful
|
|
1017
|
-
const refMs = new Date(lesson.verified_at ?? lesson.ts).getTime();
|
|
1018
|
-
const ageDays = (nowMs - refMs) / 86400000;
|
|
1019
|
-
if (ageDays <= archive_days)
|
|
1020
|
-
continue;
|
|
1021
|
-
// Remove from provisionalKeys if Rule 1 already staged it โ Rule 3 wins with archived.
|
|
1022
|
-
const provIdx = provisionalKeys.indexOf(lesson.topic);
|
|
1023
|
-
if (provIdx !== -1)
|
|
1024
|
-
provisionalKeys.splice(provIdx, 1);
|
|
1025
|
-
conflictResolutions.push(`${lesson.topic} (failure, never recalled, ${Math.round(ageDays)}d old)`);
|
|
1026
|
-
if (!dry_run) {
|
|
1027
|
-
const base = updates.get(lesson._key) ?? lesson;
|
|
1028
|
-
const updated = {
|
|
1029
|
-
...base,
|
|
1030
|
-
state: 'archived',
|
|
1031
|
-
audit_trail: [
|
|
1032
|
-
...(base.audit_trail ?? []),
|
|
1033
|
-
{ ts: new Date().toISOString(), action: 'orphan_failure_archived', prev_state: currentState },
|
|
1034
|
-
],
|
|
1035
|
-
};
|
|
1036
|
-
updates.set(lesson._key, updated);
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
// โโ Regel 4: Startvorrat entwerten und Zeitersparnis neu rechnen โโโโโโ
|
|
1040
|
-
//
|
|
1041
|
-
// ANLASS (17.08.2026): Ein Brain mit 475 Lektionen zeigte 54 Tage
|
|
1042
|
-
// eingesparte Entwicklerzeit und 97.237,50 โฌ Gegenwert. Nachgerechnet kam
|
|
1043
|
-
// praktisch alles aus dem mitgelieferten Startvorrat โ `docker:layer-cache`
|
|
1044
|
-
// bei 973 Abrufen, `cache:stampede` bei 971, dahinter neun weitere bei je
|
|
1045
|
-
// rund 208. Die echten Lektionen standen bei zwei bis fรผnf.
|
|
1046
|
-
//
|
|
1047
|
-
// Ab jetzt zรคhlen Starter-Abrufe nicht mehr mit (wertbeitrag.ts). Das
|
|
1048
|
-
// stoppt aber nur das Weiterwachsen; die aufgelaufenen Zรคhler bleiben und
|
|
1049
|
-
// tragen die Zahl weiter. Deshalb hier zweierlei:
|
|
1050
|
-
//
|
|
1051
|
-
// 1. Starter-Zรคhler auf 0. Sie sind keine Leistung dieses Teams.
|
|
1052
|
-
// 2. time_saved_mins NEU BERECHNEN statt zu korrigieren. Die alte Summe
|
|
1053
|
-
// ist nicht rekonstruierbar (sie wurde im Juli schon einmal
|
|
1054
|
-
// zurรผckgesetzt und geht nicht mit der Summe der Einzelzรคhler auf).
|
|
1055
|
-
// Etwas davon abzuziehen ergรคbe eine Zahl, die genauso erfunden wรคre
|
|
1056
|
-
// wie die alte โ nur unauffรคlliger. Neu gerechnet ist sie prรผfbar.
|
|
1057
|
-
//
|
|
1058
|
-
// NACHTRAG 22.08.2026: Der Filter oben erwischte den grรถรten Posten
|
|
1059
|
-
// nicht โ `import_public_brain` schreibt `source: 'public_brain'`, und
|
|
1060
|
-
// verglichen wurde mit `=== 'starter'`. Und die Zeile darunter rechnete
|
|
1061
|
-
// ABRUFZAHL MAL STAFFEL. Beides zusammen machte das Aufrรคumen zu einer
|
|
1062
|
-
// Verschlimmerung: 1664 h โ 6445 h.
|
|
1063
|
-
//
|
|
1064
|
-
// Die Rechnung lautet jetzt: Summe รผber alle NICHT fremden Lektionen,
|
|
1065
|
-
// die mindestens einmal geholfen haben โ JEDE GENAU EINMAL. Recherchiert
|
|
1066
|
-
// wird einmal; der neunhundertste Blick auf dieselbe Lektion spart keine
|
|
1067
|
-
// neunhundertste Recherche. Damit ist die Zahl nach oben durch den
|
|
1068
|
-
// Wissensstand begrenzt statt durch die Nutzung.
|
|
1069
|
-
let starterZurueckgesetzt = 0;
|
|
1070
|
-
let minutenNeu = 0;
|
|
1071
|
-
const minutenAlt = parseFloat((await redis.get(`cachly:stats:time_saved_mins:${instance_id}`)) ?? '0') || 0;
|
|
1072
|
-
for (const lesson of lessons) {
|
|
1073
|
-
if (istStarterLektion(lesson)) {
|
|
1074
|
-
if ((lesson.recall_count ?? 0) > 0) {
|
|
1075
|
-
starterZurueckgesetzt++;
|
|
1076
|
-
const bisher = updates.get(lesson._key) ?? lesson;
|
|
1077
|
-
updates.set(lesson._key, { ...bisher, recall_count: 0 });
|
|
1078
|
-
}
|
|
1079
|
-
continue;
|
|
1080
|
-
}
|
|
1081
|
-
// NICHT mal Abrufzahl. Recherchiert wird einmal; der neunhundertste
|
|
1082
|
-
// Blick auf dieselbe Lektion spart keine neunhundertste Recherche.
|
|
1083
|
-
// Zaehlt also jede Lektion einmal, sobald sie ueberhaupt geholfen hat.
|
|
1084
|
-
if ((lesson.recall_count ?? 0) > 0)
|
|
1085
|
-
minutenNeu += ersparteMinuten(lesson);
|
|
1086
|
-
}
|
|
1087
|
-
if (!dry_run) {
|
|
1088
|
-
await redis.set(`cachly:stats:time_saved_mins:${instance_id}`, String(minutenNeu)).catch(() => { });
|
|
1089
|
-
}
|
|
1090
|
-
// โโ Flush updates โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1091
|
-
if (!dry_run && updates.size > 0) {
|
|
1092
|
-
const pipeline = redis.pipeline();
|
|
1093
|
-
for (const [key, updated] of updates) {
|
|
1094
|
-
const { _key: _unused, ...lessonData } = updated;
|
|
1095
|
-
void _unused;
|
|
1096
|
-
pipeline.set(key, JSON.stringify(lessonData));
|
|
1097
|
-
}
|
|
1098
|
-
await pipeline.exec();
|
|
1099
|
-
}
|
|
1100
|
-
// โโ Report โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1101
|
-
const prefix = dry_run ? '๐ **Brain Hygiene (dry run)**' : '๐งน **Brain Hygiene**';
|
|
1102
|
-
const lines = [
|
|
1103
|
-
`${prefix} โ scanned **${lessons.length}** lessons`,
|
|
1104
|
-
'',
|
|
1105
|
-
`| Action | Count |`,
|
|
1106
|
-
`|---|---|`,
|
|
1107
|
-
`| Flagged provisional (confidence < ${provisional_threshold}) | ${provisionalKeys.length} |`,
|
|
1108
|
-
`| Archived (stale + low-recall + age > ${archive_days}d) | ${archivedKeys.length} |`,
|
|
1109
|
-
`| Contradictions auto-resolved | ${conflictResolutions.length} |`,
|
|
1110
|
-
`| Startvorrat entwertet (Zรคhler auf 0) | ${starterZurueckgesetzt} |`,
|
|
1111
|
-
'',
|
|
1112
|
-
];
|
|
1113
|
-
if (Math.round(minutenAlt) !== Math.round(minutenNeu)) {
|
|
1114
|
-
// Die Zahl beim Namen nennen, nicht nur still korrigieren. Wer morgen
|
|
1115
|
-
// eine kleinere Ersparnis im Panel sieht, soll hier lesen koennen warum.
|
|
1116
|
-
lines.push(`**โฑ๏ธ Zeitersparnis neu gerechnet:** ${fmtStunden(minutenAlt)} โ **${fmtStunden(minutenNeu)}**`, `_Nur eigene Lektionen, und jede genau einmal. Der mitgelieferte Startvorrat ist Allgemeinwissen, kein gelerntes; und wer dieselbe Lektion zum zweiten Mal liest, spart keine zweite Recherche._`, '');
|
|
1117
|
-
}
|
|
1118
|
-
if (provisionalKeys.length > 0) {
|
|
1119
|
-
lines.push(`**โ ๏ธ Provisional (verify or re-learn):**`);
|
|
1120
|
-
provisionalKeys.slice(0, 10).forEach(t => lines.push(` - \`${t}\``));
|
|
1121
|
-
if (provisionalKeys.length > 10)
|
|
1122
|
-
lines.push(` - _โฆand ${provisionalKeys.length - 10} more_`);
|
|
1123
|
-
lines.push('');
|
|
1124
|
-
}
|
|
1125
|
-
if (archivedKeys.length > 0) {
|
|
1126
|
-
lines.push(`**๐ฆ Archived (excluded from recall):**`);
|
|
1127
|
-
archivedKeys.slice(0, 5).forEach(t => lines.push(` - \`${t}\``));
|
|
1128
|
-
if (archivedKeys.length > 5)
|
|
1129
|
-
lines.push(` - _โฆand ${archivedKeys.length - 5} more_`);
|
|
1130
|
-
lines.push('');
|
|
1131
|
-
}
|
|
1132
|
-
if (conflictResolutions.length > 0) {
|
|
1133
|
-
lines.push(`**โ๏ธ Contradictions resolved (failure archived, success kept):**`);
|
|
1134
|
-
conflictResolutions.slice(0, 5).forEach(r => lines.push(` - ${r}`));
|
|
1135
|
-
lines.push('');
|
|
1136
|
-
}
|
|
1137
|
-
if (dry_run) {
|
|
1138
|
-
lines.push(`_Run \`brain_hygiene(dry_run=false)\` to apply these changes._`);
|
|
1139
|
-
}
|
|
1140
|
-
else if (provisionalKeys.length + archivedKeys.length + conflictResolutions.length === 0) {
|
|
1141
|
-
lines.push(`โ
Brain is clean โ no hygiene actions needed.`);
|
|
1142
|
-
}
|
|
1143
|
-
else {
|
|
1144
|
-
lines.push(`_Changes applied. Run \`brain_doctor\` to verify the updated health score._`);
|
|
1145
|
-
}
|
|
1146
|
-
return lines.join('\n');
|
|
1147
|
-
}
|
|
1148
|
-
// โโ recall_at โ Brain Archaeology โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1149
|
-
case 'recall_at': {
|
|
1150
|
-
const { instance_id, topic, date } = args;
|
|
1151
|
-
const redis = await getConnection(instance_id);
|
|
1152
|
-
const cutoff = new Date(date).getTime();
|
|
1153
|
-
if (isNaN(cutoff))
|
|
1154
|
-
return `โ Invalid date "${date}". Use ISO format: "2026-01-15"`;
|
|
1155
|
-
const listKey = `cachly:lessons:${topic}`;
|
|
1156
|
-
const all = await redis.lrange(listKey, 0, -1);
|
|
1157
|
-
if (all.length === 0)
|
|
1158
|
-
return `๐ญ No history found for \`${topic}\`. Lessons are stored via \`learn_from_attempts\`.`;
|
|
1159
|
-
const before = all
|
|
1160
|
-
.map(raw => { try {
|
|
1161
|
-
return JSON.parse(raw);
|
|
1162
|
-
}
|
|
1163
|
-
catch {
|
|
1164
|
-
return null;
|
|
1165
|
-
} })
|
|
1166
|
-
.filter((l) => l !== null && new Date(l.ts).getTime() <= cutoff)
|
|
1167
|
-
.sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
|
|
1168
|
-
if (before.length === 0) {
|
|
1169
|
-
const first = safeJsonParse(all[0] ?? null, null);
|
|
1170
|
-
const earliest = first?.ts ? new Date(first.ts).toLocaleDateString('de-DE') : 'unknown';
|
|
1171
|
-
return `๐ญ No entries for \`${topic}\` found before **${date}**. Earliest entry: ${earliest}.`;
|
|
1172
|
-
}
|
|
1173
|
-
const lines = [
|
|
1174
|
-
`๐บ **Brain Archaeology: \`${topic}\` before ${date}**`,
|
|
1175
|
-
`_${before.length} of ${all.length} total entries shown_`,
|
|
1176
|
-
'',
|
|
1177
|
-
];
|
|
1178
|
-
for (const l of before.slice(-10)) {
|
|
1179
|
-
const emoji = l.outcome === 'success' ? 'โ
' : l.outcome === 'partial' ? 'โ ๏ธ' : 'โ';
|
|
1180
|
-
const d = new Date(l.ts).toLocaleDateString('de-DE');
|
|
1181
|
-
lines.push(`**${d}** ${emoji} ${l.outcome}`);
|
|
1182
|
-
if (l.what_worked)
|
|
1183
|
-
lines.push(` โ ${l.what_worked.slice(0, 100)}`);
|
|
1184
|
-
lines.push('');
|
|
1185
|
-
}
|
|
1186
|
-
lines.push(`_Full evolution: ${all.map(r => { try {
|
|
1187
|
-
const l = JSON.parse(r);
|
|
1188
|
-
return l.outcome === 'success' ? 'โ
' : l.outcome === 'partial' ? 'โ ๏ธ' : 'โ';
|
|
1189
|
-
}
|
|
1190
|
-
catch {
|
|
1191
|
-
return '?';
|
|
1192
|
-
} }).join(' โ ')}_`);
|
|
1193
|
-
return lines.join('\n');
|
|
1194
|
-
}
|
|
1195
|
-
// โโ trace_dependency โ Causal Chain โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1196
|
-
case 'trace_dependency': {
|
|
1197
|
-
const { instance_id, dependency, mark_review = false } = args;
|
|
1198
|
-
const redis = await getConnection(instance_id);
|
|
1199
|
-
const depKey = `cachly:dep:${dependency}`;
|
|
1200
|
-
const raw = await redis.get(depKey);
|
|
1201
|
-
if (!raw)
|
|
1202
|
-
return `๐ญ No lessons found that depend on \`${dependency}\`.\n\nAdd dependencies via: \`learn_from_attempts(..., depends_on=["${dependency}"])\``;
|
|
1203
|
-
const topics = safeJsonParse(raw, []);
|
|
1204
|
-
const lines = [
|
|
1205
|
-
`๐ **Causal Chain: \`${dependency}\`** โ ${topics.length} dependent lesson${topics.length === 1 ? '' : 's'}`,
|
|
1206
|
-
'',
|
|
1207
|
-
];
|
|
1208
|
-
for (const t of topics) {
|
|
1209
|
-
const lessonRaw = await redis.get(`cachly:lesson:best:${t}`);
|
|
1210
|
-
if (!lessonRaw) {
|
|
1211
|
-
lines.push(` โข \`${t}\` _(lesson deleted)_`);
|
|
1212
|
-
continue;
|
|
1213
|
-
}
|
|
1214
|
-
const lesson = safeJsonParse(lessonRaw, null);
|
|
1215
|
-
if (!lesson) {
|
|
1216
|
-
lines.push(` โข \`${t}\` _(lesson corrupted)_`);
|
|
1217
|
-
continue;
|
|
1218
|
-
}
|
|
1219
|
-
const emoji = lesson.outcome === 'success' ? 'โ
' : lesson.outcome === 'partial' ? 'โ ๏ธ' : 'โ';
|
|
1220
|
-
const reviewBadge = lesson.needs_review ? ' ๐ **needs_review**' : '';
|
|
1221
|
-
lines.push(` ${emoji} \`${t}\` (${lesson.severity ?? 'major'})${reviewBadge}`);
|
|
1222
|
-
if (mark_review) {
|
|
1223
|
-
const updated = { ...lesson, needs_review: true };
|
|
1224
|
-
await redis.set(`cachly:lesson:best:${t}`, JSON.stringify(updated));
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
if (mark_review) {
|
|
1228
|
-
lines.push('', `๐ All ${topics.length} lessons marked as **needs_review** โ verify they still work with the changed dependency.`);
|
|
1229
|
-
}
|
|
1230
|
-
else {
|
|
1231
|
-
lines.push('', `_Run with \`mark_review: true\` to flag all dependent lessons for re-verification._`);
|
|
1232
|
-
}
|
|
1233
|
-
return lines.join('\n');
|
|
1234
|
-
}
|
|
1235
|
-
// โโ global_learn โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1236
|
-
case 'global_learn': {
|
|
1237
|
-
const { instance_id, topic, lesson, severity = 'minor', tags = [] } = args;
|
|
1238
|
-
const redis = await getConnection(instance_id);
|
|
1239
|
-
const key = `cachly:global:lesson:${topic}`;
|
|
1240
|
-
const record = {
|
|
1241
|
-
topic,
|
|
1242
|
-
lesson,
|
|
1243
|
-
severity,
|
|
1244
|
-
tags,
|
|
1245
|
-
ts: new Date().toISOString(),
|
|
1246
|
-
scope: 'global',
|
|
1247
|
-
recall_count: 0,
|
|
1248
|
-
};
|
|
1249
|
-
// Preserve recall_count on update
|
|
1250
|
-
const existing = await redis.get(key);
|
|
1251
|
-
if (existing) {
|
|
1252
|
-
const prev = safeJsonParse(existing, null);
|
|
1253
|
-
record.recall_count = prev?.recall_count ?? 0;
|
|
1254
|
-
}
|
|
1255
|
-
await redis.set(key, JSON.stringify(record));
|
|
1256
|
-
return `๐ **Global lesson stored**: \`${topic}\`\n\n${lesson}\n\nRecallable from any project via \`global_recall(topic="${topic}")\`.`;
|
|
1257
|
-
}
|
|
1258
|
-
// โโ global_recall โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1259
|
-
case 'global_recall': {
|
|
1260
|
-
const { instance_id, topic } = args;
|
|
1261
|
-
const redis = await getConnection(instance_id);
|
|
1262
|
-
const keys = [];
|
|
1263
|
-
const gStream = redis.scanStream({ match: 'cachly:global:lesson:*', count: 200 });
|
|
1264
|
-
await new Promise((resolve, reject) => {
|
|
1265
|
-
gStream.on('data', (batch) => keys.push(...batch));
|
|
1266
|
-
gStream.on('end', resolve);
|
|
1267
|
-
gStream.on('error', reject);
|
|
1268
|
-
});
|
|
1269
|
-
let lessons = [];
|
|
1270
|
-
if (keys.length > 0) {
|
|
1271
|
-
const raws = await redis.mget(...keys);
|
|
1272
|
-
for (const raw of raws) {
|
|
1273
|
-
const l = safeJsonParse(raw ?? null, null);
|
|
1274
|
-
if (l)
|
|
1275
|
-
lessons.push(l);
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
if (topic) {
|
|
1279
|
-
const t = topic.toLowerCase();
|
|
1280
|
-
lessons = lessons.filter(l => l.topic.toLowerCase().includes(t));
|
|
1281
|
-
}
|
|
1282
|
-
if (lessons.length === 0) {
|
|
1283
|
-
return `๐ญ No global lessons${topic ? ` for \`${topic}\`` : ''}.\n\nAdd cross-project knowledge with \`global_learn(topic="...", lesson="...")\`.`;
|
|
1284
|
-
}
|
|
1285
|
-
// Increment recall_count + stamp last_recalled_at (recall-recency signal)
|
|
1286
|
-
for (const l of lessons) {
|
|
1287
|
-
const k = `cachly:global:lesson:${l.topic}`;
|
|
1288
|
-
const raw = await redis.get(k);
|
|
1289
|
-
if (raw) {
|
|
1290
|
-
const rec = safeJsonParse(raw, null);
|
|
1291
|
-
if (!rec)
|
|
1292
|
-
continue;
|
|
1293
|
-
rec.recall_count = (rec.recall_count ?? 0) + 1;
|
|
1294
|
-
rec.last_recalled_at = new Date().toISOString();
|
|
1295
|
-
await redis.set(k, JSON.stringify(rec));
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
const lines = [`๐ **Global Brain** โ ${lessons.length} lesson${lessons.length > 1 ? 's' : ''}`, ''];
|
|
1299
|
-
for (const l of lessons) {
|
|
1300
|
-
const sev = l.severity === 'critical' ? '๐ด ' : l.severity === 'major' ? '๐ก ' : '';
|
|
1301
|
-
lines.push(`${sev}**\`${l.topic}\`**`);
|
|
1302
|
-
lines.push(l.lesson.slice(0, 200));
|
|
1303
|
-
lines.push('');
|
|
1304
|
-
}
|
|
1305
|
-
return lines.join('\n');
|
|
1306
|
-
}
|
|
1307
|
-
// โโ publish_lesson โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1308
|
-
case 'publish_lesson': {
|
|
1309
|
-
const { instance_id, topic, lesson, framework = 'general', severity = 'minor' } = args;
|
|
1310
|
-
const redis = await getConnection(instance_id);
|
|
1311
|
-
// Strip potential PII patterns (emails, tokens, paths)
|
|
1312
|
-
const sanitized = lesson
|
|
1313
|
-
.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '[email]')
|
|
1314
|
-
.replace(/\b(sk-|cky_live_|Bearer\s)[A-Za-z0-9_-]{8,}/g, '[token]')
|
|
1315
|
-
.replace(/\/Users\/[^\s/]+/g, '/Users/[user]')
|
|
1316
|
-
.replace(/\/home\/[^\s/]+/g, '/home/[user]');
|
|
1317
|
-
const publicLesson = {
|
|
1318
|
-
topic,
|
|
1319
|
-
lesson: sanitized,
|
|
1320
|
-
framework,
|
|
1321
|
-
severity,
|
|
1322
|
-
ts: new Date().toISOString(),
|
|
1323
|
-
published: true,
|
|
1324
|
-
votes: 0,
|
|
1325
|
-
};
|
|
1326
|
-
// Store locally with public flag (future: sync to Cachly public API)
|
|
1327
|
-
const key = `cachly:public:lesson:${framework}:${topic}`;
|
|
1328
|
-
await redis.set(key, JSON.stringify(publicLesson), 'EX', 365 * 86400);
|
|
1329
|
-
return [
|
|
1330
|
-
`๐ข **Lesson published!**`,
|
|
1331
|
-
``,
|
|
1332
|
-
`**Topic:** \`${topic}\``,
|
|
1333
|
-
`**Framework:** ${framework}`,
|
|
1334
|
-
`**Content:** ${sanitized.slice(0, 200)}${sanitized.length > 200 ? 'โฆ' : ''}`,
|
|
1335
|
-
``,
|
|
1336
|
-
`This lesson is now available in the Public Brain for other developers.`,
|
|
1337
|
-
`Import it anywhere: \`import_public_brain(framework="${framework}")\``,
|
|
1338
|
-
].join('\n');
|
|
1339
|
-
}
|
|
1340
|
-
// โโ import_public_brain โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1341
|
-
case 'import_public_brain': {
|
|
1342
|
-
const { instance_id, framework, limit = 20 } = args;
|
|
1343
|
-
const redis = await getConnection(instance_id);
|
|
1344
|
-
// Community-curated lessons per framework
|
|
1345
|
-
const COMMUNITY_LESSONS = {
|
|
1346
|
-
nextjs: [
|
|
1347
|
-
{ topic: 'nextjs:image-layout', lesson: 'Use fill + relative parent instead of layout="fill" (deprecated since Next.js 13)', severity: 'major' },
|
|
1348
|
-
{ topic: 'nextjs:app-router-fetch', lesson: 'fetch() in Server Components is cached by default โ add {cache:"no-store"} for dynamic data', severity: 'major' },
|
|
1349
|
-
{ topic: 'nextjs:metadata-export', lesson: 'Export metadata const or generateMetadata() โ never both in same file', severity: 'minor' },
|
|
1350
|
-
{ topic: 'nextjs:client-boundary', lesson: '"use client" propagates down โ keep it at the lowest component, not at page level', severity: 'major' },
|
|
1351
|
-
{ topic: 'nextjs:env-prefix', lesson: 'Only NEXT_PUBLIC_* env vars are exposed to client โ others are server-only', severity: 'critical' },
|
|
1352
|
-
{ topic: 'nextjs:revalidate', lesson: 'export const revalidate = 0 disables caching for entire route; use revalidatePath() for on-demand', severity: 'minor' },
|
|
1353
|
-
],
|
|
1354
|
-
fastapi: [
|
|
1355
|
-
{ topic: 'fastapi:async-db', lesson: 'Use async session with asyncpg โ sync SQLAlchemy blocks the event loop', severity: 'critical' },
|
|
1356
|
-
{ topic: 'fastapi:pydantic-v2', lesson: 'Pydantic v2: use model_validate() instead of parse_obj(), .dict() โ .model_dump()', severity: 'major' },
|
|
1357
|
-
{ topic: 'fastapi:lifespan', lesson: 'Use lifespan context manager instead of deprecated on_event startup/shutdown', severity: 'minor' },
|
|
1358
|
-
{ topic: 'fastapi:background-tasks', lesson: 'BackgroundTasks run after response is sent โ not in a separate thread pool', severity: 'major' },
|
|
1359
|
-
{ topic: 'fastapi:cors-order', lesson: 'CORSMiddleware must be added before other middleware to work correctly', severity: 'critical' },
|
|
1360
|
-
],
|
|
1361
|
-
go: [
|
|
1362
|
-
{ topic: 'go:context-cancel', lesson: 'Always call cancel() from context.WithCancel โ leak goroutines if not cancelled', severity: 'critical' },
|
|
1363
|
-
{ topic: 'go:defer-in-loop', lesson: 'defer in a loop runs at function return, not loop iteration โ use IIFE or explicit close', severity: 'major' },
|
|
1364
|
-
{ topic: 'go:nil-interface', lesson: 'nil interface != interface containing nil pointer โ use explicit nil checks', severity: 'major' },
|
|
1365
|
-
{ topic: 'go:goroutine-leak', lesson: 'Goroutines with channel sends block forever if receiver is gone โ use select with done chan', severity: 'critical' },
|
|
1366
|
-
{ topic: 'go:embed-path', lesson: '//go:embed path must be relative and known at compile time โ no os.Getenv', severity: 'minor' },
|
|
1367
|
-
],
|
|
1368
|
-
docker: [
|
|
1369
|
-
{ topic: 'docker:layer-cache', lesson: 'Copy package.json before source code โ Docker caches layers, npm install only reruns on dep changes', severity: 'major' },
|
|
1370
|
-
{ topic: 'docker:non-root', lesson: 'Run as non-root user (USER 1001) โ some k8s clusters reject root containers by policy', severity: 'critical' },
|
|
1371
|
-
{ topic: 'docker:build-arg-secret', lesson: 'Never use ARG for secrets โ visible in image history. Use --secret mount instead', severity: 'critical' },
|
|
1372
|
-
{ topic: 'docker:entrypoint-exec', lesson: 'Use exec form ["cmd","arg"] not shell form "cmd arg" โ shell form ignores SIGTERM', severity: 'major' },
|
|
1373
|
-
{ topic: 'docker:multi-stage', lesson: 'Multi-stage builds: copy only built artifacts to final stage โ keep image small', severity: 'minor' },
|
|
1374
|
-
],
|
|
1375
|
-
kubernetes: [
|
|
1376
|
-
{ topic: 'k8s:resource-limits', lesson: 'Always set resource limits โ unbounded pods cause node evictions and OOMKill', severity: 'critical' },
|
|
1377
|
-
{ topic: 'k8s:liveness-vs-readiness', lesson: 'Liveness failures restart pod; Readiness failures remove from LB. Use different endpoints', severity: 'major' },
|
|
1378
|
-
{ topic: 'k8s:imagepullpolicy', lesson: 'imagePullPolicy: Always in production โ IfNotPresent can serve stale images', severity: 'major' },
|
|
1379
|
-
{ topic: 'k8s:configmap-env', lesson: 'ConfigMap changes don\'t restart pods โ use rollout restart or mount as volume', severity: 'critical' },
|
|
1380
|
-
{ topic: 'k8s:pdb', lesson: 'Set PodDisruptionBudget for stateful apps โ node drains kill all pods without it', severity: 'major' },
|
|
1381
|
-
],
|
|
1382
|
-
react: [
|
|
1383
|
-
{ topic: 'react:useeffect-deps', lesson: 'Omitting dependencies from useEffect deps array causes stale closure bugs โ use exhaustive-deps ESLint rule', severity: 'critical' },
|
|
1384
|
-
{ topic: 'react:key-index', lesson: 'Never use array index as key in lists โ causes subtle re-render bugs on reorder/delete', severity: 'major' },
|
|
1385
|
-
{ topic: 'react:setState-in-render', lesson: 'setState() during render causes infinite loop โ move to useEffect or event handler', severity: 'critical' },
|
|
1386
|
-
{ topic: 'react:memo-reference', lesson: 'Object/array literals in JSX recreate on every render โ useMemo for expensive derived values', severity: 'minor' },
|
|
1387
|
-
],
|
|
1388
|
-
typescript: [
|
|
1389
|
-
{ topic: 'ts:type-guard', lesson: 'Use "x is Type" return type for type guard functions โ not "boolean"', severity: 'minor' },
|
|
1390
|
-
{ topic: 'ts:strict-null', lesson: 'Enable strictNullChecks in tsconfig โ catches 90% of runtime null errors at compile time', severity: 'critical' },
|
|
1391
|
-
{ topic: 'ts:enum-avoid', lesson: 'Prefer union types ("a"|"b") over enum โ enums have surprising runtime behavior', severity: 'minor' },
|
|
1392
|
-
{ topic: 'ts:satisfies', lesson: 'Use "satisfies" operator to validate type without widening โ more precise than explicit annotation', severity: 'minor' },
|
|
1393
|
-
],
|
|
1394
|
-
python: [
|
|
1395
|
-
{ topic: 'python:mutable-default', lesson: 'Never use mutable default arguments (def f(x=[])) โ shared across all calls. Use None + guard', severity: 'critical' },
|
|
1396
|
-
{ topic: 'python:walrus-operator', lesson: ':= (walrus) assigns and returns โ useful in while/comprehensions but hard to read in complex expr', severity: 'minor' },
|
|
1397
|
-
{ topic: 'python:asyncio-run', lesson: 'asyncio.run() creates new event loop โ calling it inside an existing loop raises RuntimeError', severity: 'major' },
|
|
1398
|
-
{ topic: 'python:typing-optional', lesson: 'Optional[X] == Union[X, None] โ in Python 3.10+ use X | None syntax', severity: 'minor' },
|
|
1399
|
-
],
|
|
1400
|
-
};
|
|
1401
|
-
const fw = framework.toLowerCase();
|
|
1402
|
-
const lessons = COMMUNITY_LESSONS[fw];
|
|
1403
|
-
if (!lessons) {
|
|
1404
|
-
const available = Object.keys(COMMUNITY_LESSONS).join(', ');
|
|
1405
|
-
return `โ No public brain available for \`${framework}\`.\n\nAvailable: ${available}\n\nOr publish your own: \`publish_lesson(framework="${framework}", ...)\``;
|
|
1406
|
-
}
|
|
1407
|
-
const toImport = lessons.slice(0, limit);
|
|
1408
|
-
let importedCount = 0;
|
|
1409
|
-
for (const l of toImport) {
|
|
1410
|
-
const key = `cachly:lesson:best:${l.topic}`;
|
|
1411
|
-
const existing = await redis.get(key);
|
|
1412
|
-
if (!existing) {
|
|
1413
|
-
await redis.set(key, JSON.stringify({
|
|
1414
|
-
...l,
|
|
1415
|
-
what_worked: l.lesson,
|
|
1416
|
-
outcome: 'success',
|
|
1417
|
-
ts: new Date().toISOString(),
|
|
1418
|
-
recall_count: 0,
|
|
1419
|
-
source: 'public_brain',
|
|
1420
|
-
version: 2,
|
|
1421
|
-
}));
|
|
1422
|
-
importedCount++;
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
const lines = [
|
|
1426
|
-
`๐ฅ **Public Brain imported: ${framework}**`,
|
|
1427
|
-
``,
|
|
1428
|
-
`${importedCount} new lessons added (${toImport.length - importedCount} already existed)`,
|
|
1429
|
-
``,
|
|
1430
|
-
`**Imported topics:**`,
|
|
1431
|
-
...toImport.map(l => {
|
|
1432
|
-
const sev = l.severity === 'critical' ? '๐ด' : l.severity === 'major' ? '๐ก' : '๐ก';
|
|
1433
|
-
return ` ${sev} \`${l.topic}\``;
|
|
1434
|
-
}),
|
|
1435
|
-
``,
|
|
1436
|
-
`These lessons will now appear in \`session_start\` when relevant.`,
|
|
1437
|
-
`Recall any time: \`recall_best_solution(topic="${fw}:...")\``,
|
|
1438
|
-
];
|
|
1439
|
-
return lines.join('\n');
|
|
1440
|
-
}
|
|
1441
|
-
// โโ setup_ai_memory โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1442
|
-
case 'setup_ai_memory': {
|
|
1443
|
-
const { instance_id, project_dir, embed_provider: providerArg = 'openai', project_description = 'a software project', } = args;
|
|
1444
|
-
const inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
1445
|
-
// Provider-specific env var instructions
|
|
1446
|
-
const providerEnvMap = {
|
|
1447
|
-
openai: { key: 'OPENAI_API_KEY', hint: 'Get at: https://platform.openai.com/api-keys' },
|
|
1448
|
-
mistral: { key: 'MISTRAL_API_KEY', hint: 'Get at: https://console.mistral.ai/api-keys' },
|
|
1449
|
-
cohere: { key: 'COHERE_API_KEY', hint: 'Get at: https://dashboard.cohere.com/api-keys' },
|
|
1450
|
-
ollama: { key: 'OLLAMA_BASE_URL', hint: 'Run: brew install ollama && ollama serve (free, local, no key needed)' },
|
|
1451
|
-
gemini: { key: 'GEMINI_API_KEY', hint: 'Get at: https://aistudio.google.com/app/apikey' },
|
|
1452
|
-
};
|
|
1453
|
-
const provInfo = providerEnvMap[providerArg] ?? providerEnvMap['openai'];
|
|
1454
|
-
const hasVector = !!inst.vector_token;
|
|
1455
|
-
// Generate .mcp.json snippet
|
|
1456
|
-
const mcpJsonSnippet = JSON.stringify({
|
|
1457
|
-
mcpServers: {
|
|
1458
|
-
cachly: {
|
|
1459
|
-
command: 'npx',
|
|
1460
|
-
args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
1461
|
-
env: {
|
|
1462
|
-
CACHLY_JWT: 'your-api-token-from-cachly.dev/settings',
|
|
1463
|
-
[provInfo.key]: providerArg === 'ollama' ? 'http://localhost:11434' : 'your-key-here',
|
|
1464
|
-
...(providerArg !== 'openai' ? { CACHLY_EMBED_PROVIDER: providerArg } : {}),
|
|
1465
|
-
},
|
|
1466
|
-
},
|
|
1467
|
-
},
|
|
1468
|
-
}, null, 2);
|
|
1469
|
-
// Generate copilot-instructions.md content
|
|
1470
|
-
const tier = inst.tier.toUpperCase();
|
|
1471
|
-
const smartRecallNote = hasVector
|
|
1472
|
-
? '- `smart_recall("natural language query")` โ semantic search (finds by meaning)'
|
|
1473
|
-
: '- `recall_context("arch:*")` โ exact/glob key lookup (upgrade to Speed/Business for smart_recall)';
|
|
1474
|
-
const layerNote = hasVector
|
|
1475
|
-
? `Layer 3 (Semantic): smart_recall uses pgvector HNSW on your ${tier} instance`
|
|
1476
|
-
: `Layer 3 (Autopilot): this file โ upgrade to Speed/Business tier to unlock smart_recall`;
|
|
1477
|
-
const copilotInstructions = `# cachly AI Brain โ ${project_description}
|
|
1
|
+
import{readFileSync as le}from"node:fs";import{bauabdruck as ue,formatiereBauabdruck as de}from"../bauabdruck.js";import{beurteileSpeicher as he,zahlAusInfo as se}from"../speicherlage.js";import{beurteileDeckung as pe}from"../eingaenge.js";import{join as E}from"node:path";import{calculateConfidence as Y,CONFIDENCE_STALE_VALUE as X,CONFIDENCE_WARN_VALUE as me,CONFIDENCE_WARN_DAYS as ne,CONFIDENCE_STALE_DAYS as oe}from"../confidence.js";import{safeJsonParse as T}from"../utils.js";import{ersparteMinuten as fe,istStarterLektion as ge,fmtStunden as ae}from"../wertbeitrag.js";import{cachlyUrl as ye}from"../cachly-url.js";const Ie=new Set(["sync_file_changes","team_learn","team_confirm","team_recall","team_synthesize","memory_crystalize","team_crystallize","brain_doctor","brain_hygiene","recall_at","trace_dependency","global_learn","global_recall","publish_lesson","import_public_brain","setup_ai_memory","team_assign_role","team_whoami","team_roster","team_grant_scope","team_scopes","team_audit"]),G={admin:4,reviewer:3,contributor:2,viewer:1},F={admin:"\u{1F451}",reviewer:"\u{1F6E1}\uFE0F",contributor:"\u270F\uFE0F",viewer:"\u{1F441}\uFE0F"},Q={admin:"assign roles \xB7 delete lessons \xB7 all reviewer + contributor actions",reviewer:"senior-review lessons (\u{1F6E1}\uFE0F) \xB7 all contributor actions",contributor:"store lessons \xB7 peer-review lessons (\u2714\uFE0F) \xB7 recall",viewer:"recall only \u2014 cannot store or review lessons"},B=b=>`cachly:team:roles:${b}`;async function J(b,y,_){return await b.hget(B(y),_.toLowerCase()).catch(()=>null)}function H(b,y){return b?(G[b]??0)>=(G[y]??0):!1}function we(b){return b==="admin"||b==="reviewer"?"senior":"peer"}async function _e(b,y){const _=await b.hgetall(B(y)).catch(()=>({}));return Object.values(_).includes("admin")}async function ee(b,y,_,R,r){if(!await _e(b,y))return null;const e=_?await J(b,y,_):null;if(H(e,R))return null;const a=e?`${F[e]} ${e}`:"no role";return[`\u{1F512} **Permission denied** \u2014 to ${r} you need at least \`${R}\` ${F[R]} (you have: ${a}).`,"","Ask an admin to grant it:",`\`team_assign_role(instance_id="${y}", handle="${_||"<your-handle>"}", role="${R}", assigned_by="<admin>")\``,"_Governance is active on this brain. Run `team_whoami` to check your role._"].join(`
|
|
2
|
+
`)}async function ie(b,y,_){const R=`cachly:team:audit:${y}`,r=JSON.stringify({..._,ts:new Date().toISOString()});await b.rpush(R,r).catch(()=>{}),await b.ltrim(R,-500,-1).catch(()=>{}),await b.expire(R,365*2*86400).catch(()=>{})}const ve="cachly:team:review_mode",$e="cachly:lesson:proposed:",be=90*86400;async function ke(b,y){const _=await b.get(ve).catch(()=>null);return _==="1"||_==="true"||_==="on"}async function Se(b,y,_){const R={..._,proposed_at:new Date().toISOString(),status:"pending"};await b.set(`${$e}${y}`,JSON.stringify(R),"EX",be).catch(()=>{});const r=typeof _.author=="string"&&_.author?` by **${_.author}**`:"";return[`\u{1F4DD} **Lesson submitted for review** \u2014 \`${y}\`${r} is now a pending proposal.`,"","This team requires review before knowledge enters the Team Brain (org setting).","An org owner/admin can approve or reject it in the dashboard under **Team \u2192 Review**.","Until approved it will NOT surface in `smart_recall` / `team_recall`."].join(`
|
|
3
|
+
`)}const q=b=>`cachly:team:groups:${b}`,K=(b,y)=>`cachly:team:group:${b}:${y.toLowerCase()}`;async function xe(b,y,_){const R=await b.smembers(q(y)).catch(()=>[]),r=new Set;for(const e of R)await b.sismember(K(y,e),_.toLowerCase()).catch(()=>0)&&r.add(e.toLowerCase());return r}function Me(b,y,_){return!b||_?!0:y.has(b.toLowerCase())}async function Pe(b,y,_,R){switch(b){case"sync_file_changes":{const{instance_id:r,changed_files:e,git_diff_stat:a,commit_msg:o}=y,i=await _(r),t={ts:new Date().toISOString(),files:e,commit_msg:o,diff_stat:a?.slice(0,500)};await i.lpush("cachly:session:file_changes",JSON.stringify(t)),await i.ltrim("cachly:session:file_changes",0,99),await i.expire("cachly:session:file_changes",30*86400);const c=[],l=i.scanStream({match:"cachly:lesson:best:*",count:200});await new Promise((p,g)=>{l.on("data",v=>c.push(...v)),l.on("end",p),l.on("error",g)});const h=[],s=c.length>0?await i.mget(...c):[];for(const p of s){if(!p)continue;const g=T(p,null);if(!g)continue;const v=g.topic.toLowerCase().split(/[:\-_]/);if(e.some(w=>{const $=w.split("/").pop()?.replace(/\.[^.]+$/,"").toLowerCase()??"";return v.some(m=>m.length>3&&$.includes(m))||(g.file_paths??[]).some(m=>w.includes(m)||m.includes(w))})){const w=g.outcome==="success"?"\u2705":"\u26A0\uFE0F";h.push(` ${w} \`${g.topic}\` \u2014 ${g.what_worked.slice(0,80)}`)}}const n=[`\u{1F4C1} **File sync recorded**: ${e.length} files`,o?`\u{1F4DD} Commit: "${o}"`:"","",`**Changed:** ${e.slice(0,8).map(p=>`\`${p}\``).join(", ")}${e.length>8?` +${e.length-8} more`:""}`,""];return h.length>0?n.push(`\u{1F9E0} **Relevant brain lessons (${h.length}):**`,...h):n.push("\u{1F4A1} No existing lessons match these files yet. Add them with `learn_from_attempts`."),n.filter(Boolean).join(`
|
|
4
|
+
`)}case"team_learn":{const{instance_id:r,author:e,topic:a,outcome:o,what_worked:i,what_failed:t,severity:c,file_paths:l,commands:h,tags:s}=y;if(!e||!a||!o||!i)return"\u274C Required: author, topic, outcome, what_worked";const n=r;if(!n)return"\u274C instance_id required";const p=await _(n),g=await ee(p,n,e,"contributor","store team lessons");if(g)return g;const v={topic:a,outcome:o,what_worked:i,what_failed:t??"",severity:c??"minor",author:e,file_paths:l??[],commands:h??[],tags:[...s??[],"team"],timestamp:new Date().toISOString(),recall_count:0,version:2};if(await ke(p,n))return Se(p,a,v);const A=`cachly:lessons:${a}`;return await p.rpush(A,JSON.stringify(v)),o==="success"&&await p.set(`cachly:lesson:best:${a}`,JSON.stringify(v)),`\u2705 Team lesson stored by **${e}**: \`${a}\` (${o})
|
|
5
|
+
\u{1F4A1} ${i.slice(0,120)}`}case"team_confirm":{const{instance_id:r,topic:e,reviewer:a,level:o,note:i=""}=y;if(!r)return"\u274C instance_id required";if(!e||!a)return"\u274C Required: topic, reviewer";const t=await _(r),c=await ee(t,r,a,"reviewer","confirm team lessons");if(c)return c;const l=await J(t,r,a),h=we(l),s=o==="peer"?"peer":o==="senior"&&H(l,"reviewer")?"senior":h,n=l?` (role: ${F[l]} ${l})`:" (no role assigned \u2014 peer weight applied)",p=`cachly:lesson:best:${e}`,g=await t.get(p),v=T(g,null);if(!v)return`\u{1F4ED} No best-solution lesson found for \`${e}\`.
|
|
1478
6
|
|
|
1479
|
-
|
|
7
|
+
A lesson must exist (store one with \`team_learn\` / \`learn_from_attempts\`) before it can be confirmed.`;const A=`cachly:lesson:endorsers:${e}`,w=await t.sadd(A,a)===1;await t.expire(A,365*86400);const $=await t.scard(A),d=(v.review_level??"")==="senior"?"senior":s,u={...v,reviewed_by:a,review_level:d,endorsements:$,reviewed_at:new Date().toISOString()};await t.set(p,JSON.stringify(u));const S={reviewer:a,level:s,note:i.slice(0,280),ts:new Date().toISOString()},L=`cachly:lesson:reviews:${e}`;await t.rpush(L,JSON.stringify(S)),await t.ltrim(L,-50,-1),await t.expire(L,365*86400),await ie(t,r,{action:"team_confirm",actor:a,topic:e,level:d});const x=d==="senior"?"\u{1F6E1}\uFE0F senior-reviewed":"\u2714\uFE0F peer-reviewed",k=w?"":` (already endorsed by ${a} \u2014 no change to rank)`;return`${x} \u2014 \`${e}\` confirmed by **${a}**${n} (${$} distinct endorsement${$===1?"":"s"})${k}.
|
|
8
|
+
\u{1F4C8} This lesson now ranks higher in \`smart_recall\` / \`team_recall\`.${i?`
|
|
9
|
+
\u{1F4DD} ${i.slice(0,200)}`:""}`}case"team_recall":{const{instance_id:r,topic:e,author:a,limit:o=10}=y,i=await _(r),t=[],c=i.scanStream({match:"cachly:lesson:best:*",count:200});await new Promise((n,p)=>{c.on("data",g=>t.push(...g)),c.on("end",n),c.on("error",p)});let l=[];if(t.length>0){const n=await i.mget(...t);for(const p of n){const g=T(p??null,null);g&&l.push(g)}}if(e){const n=e.toLowerCase();l=l.filter(p=>p.topic.toLowerCase().includes(n)||(p.tags??[]).some(g=>g.toLowerCase().includes(n)))}if(a){const n=a.toLowerCase();l=l.filter(p=>p.author?.toLowerCase().includes(n))}const h=n=>n.reviewed_by?n.review_level==="senior"?2:1:0;if(l.sort((n,p)=>h(p)-h(n)||(p.recall_count??0)-(n.recall_count??0)),l=l.slice(0,o),l.length===0)return e?`\u{1F4ED} No team lessons found for \`${e}\`.
|
|
10
|
+
|
|
11
|
+
Shared instance: add lessons with \`learn_from_attempts\` and include an \`author\` field.`:`\u{1F4ED} No lessons in this brain yet.
|
|
12
|
+
|
|
13
|
+
All team members sharing this instance will see lessons here.`;const s=[`\u{1F465} **Team Brain** \u2014 ${l.length} lesson${l.length>1?"s":""}`,""];for(const n of l){const p=n.outcome==="success"?"\u2705":n.outcome==="partial"?"\u26A0\uFE0F":"\u274C",g=n.severity==="critical"?"\u{1F534} ":n.severity==="major"?"\u{1F7E1} ":"",v=n.reviewed_by?n.review_level==="senior"?" \u{1F6E1}\uFE0F":" \u2714\uFE0F":"",A=n.author?` \xB7 _by ${n.author}_`:"",w=n.reviewed_by?` \xB7 reviewed by ${n.reviewed_by}${(n.endorsements??0)>1?` +${(n.endorsements??0)-1}`:""}`:"",$=(n.recall_count??0)>0?` \xB7 recalled ${n.recall_count}\xD7`:"",m=Math.round((Date.now()-new Date(n.ts).getTime())/864e5),d=m===0?"today":m===1?"yesterday":`${m}d ago`;s.push(`${p} ${g}**\`${n.topic}\`**${v}${A}${w}${$} \xB7 ${d}`),s.push(` ${n.what_worked.slice(0,120)}`),s.push("")}return s.join(`
|
|
14
|
+
`)}case"team_synthesize":{const{instance_id:r,topic:e}=y,a=await _(r),o=`cachly:lessons:${e}`,i=await a.lrange(o,0,-1);if(i.length<2)return`\u{1F4ED} Need at least 2 entries for topic \`${e}\` to synthesize.
|
|
15
|
+
|
|
16
|
+
Currently: ${i.length} entr${i.length===1?"y":"ies"}.
|
|
17
|
+
|
|
18
|
+
Have team members store lessons via \`learn_from_attempts(topic="${e}", ...)\`.`;const t=i.map(d=>{try{return JSON.parse(d)}catch{return null}}).filter(d=>d!==null),c=t.filter(d=>d.outcome==="success"),l=t.filter(d=>d.outcome==="failure"),h=t.filter(d=>d.outcome==="partial"),s=[...new Set(t.map(d=>d.author).filter(Boolean))],n=s.length>1,p=c.sort((d,u)=>new Date(u.ts).getTime()-new Date(d.ts).getTime())[0],v=c.map(d=>d.what_worked).filter(d=>d&&d.length>10).sort((d,u)=>u.length-d.length)[0]??p?.what_worked??"",A=[...new Set([...l,...h].map(d=>d.what_failed).filter(d=>!!d&&d.length>5))],w=t.map(d=>d.severity).filter(Boolean),$=w.includes("critical")?"critical":w.includes("major")?"major":"minor";return[`\u{1F9EC} **Team Brain Synthesis: \`${e}\`**`,`_${t.length} entries from ${s.length} author${s.length===1?"":"s"}${n?` (${s.join(", ")})`:""} \xB7 ${c.length} success \xB7 ${l.length} failure \xB7 ${h.length} partial_`,"",'**Canonical "what worked":**',`> ${v}`,"",A.length>0?"**Avoid (combined failures):**":"",...A.map(d=>`> \u274C ${d}`),(A.length>0,""),"**Suggested canonical lesson:**","```","learn_from_attempts(",` topic = "${e}",`,' outcome = "success",',` what_worked = "${v.replace(/"/g,"'")}",`,A.length>0?` what_failed = "${A[0].replace(/"/g,"'")}",`:"",` severity = "${$}",`,")","```","",n?`\u{1F4A1} _${s.length} team members contributed to this synthesis. Store the canonical version to replace individual entries._`:"\u{1F4A1} _Single author \u2014 more value when multiple team members contribute to the same topic._"].filter(d=>d!==void 0).join(`
|
|
19
|
+
`)}case"memory_crystalize":{const{instance_id:r,label:e=""}=y,a=await _(r),o=new Date,i=`${o.getFullYear()}-W${String(Math.ceil((o.getDate()-o.getDay()+10)/7)).padStart(2,"0")}`,t=e||`${o.toISOString().slice(0,7)} Crystal`,c=await a.lrange("cachly:session:history",0,49),l=[],h=a.scanStream({match:"cachly:lesson:best:*",count:200});await new Promise((w,$)=>{h.on("data",m=>l.push(...m)),h.on("end",w),h.on("error",$)});const s=[];if(l.length>0){const w=await a.mget(...l);for(const $ of w){const m=T($??null,null);m&&s.push(m)}}const n=new Map;for(const w of s){const $=w.topic.split(":")[0]||"misc";n.has($)||n.set($,[]),n.get($).push(w)}const p=[];for(const[w,$]of[...n.entries()].sort((m,d)=>d[1].length-m[1].length).slice(0,8)){const d=$.filter(u=>u.outcome==="success")[0]??$[0];d&&p.push({category:w,insight:d.what_worked.slice(0,120),count:$.length})}const g={label:t,ts:o.toISOString(),session_count:c.length,lesson_count:s.length,top_patterns:p,categories:[...n.keys()],created_from:`${c.length} sessions, ${s.length} lessons`},v=JSON.stringify(g);return await a.set("cachly:crystal:latest",v),await a.expire("cachly:crystal:latest",90*86400),await a.set(`cachly:crystal:${i}`,v),await a.expire(`cachly:crystal:${i}`,365*86400),[`\u{1F48E} **Memory Crystal created: ${t}**`,"",`\u{1F4CA} Compressed: **${c.length} sessions** + **${s.length} lessons** \u2192 ${p.length} top patterns`,"","**Top patterns by category:**",...p.slice(0,6).map(w=>` \u2022 **${w.category}** (${w.count}\xD7): ${w.insight.slice(0,90)}`),"","\u{1F4A1} This crystal will appear in every future `session_start` briefing.","\u{1F4A1} Re-run `memory_crystalize` monthly to keep it fresh."].join(`
|
|
20
|
+
`)}case"team_crystallize":{const{instance_id:r,min_authors:e=2,label:a=""}=y,o=await _(r),i=new Date,t=Math.max(2,Number(e)||2),c=[],l=o.scanStream({match:"cachly:lesson:best:*",count:200});await new Promise((m,d)=>{l.on("data",u=>c.push(...u)),l.on("end",m),l.on("error",d)});const h=[];if(c.length>0){const m=await o.mget(...c);for(const d of m){const u=T(d??null,null);u&&u.outcome==="success"&&u.author&&h.push(u)}}const s=new Set(["the","and","for","with","from","that","this","fix","bug","error","issue","use","via","add","set","team"]),n=m=>{const[d,...u]=m.topic.split(":"),S=(m.tags??[]).map(k=>k.toLowerCase().trim()).filter(k=>k.length>3&&!s.has(k)).sort();return S.length>0?S[0]:u.join("-").toLowerCase().replace(/[^a-z0-9\s-]/g," ").split(/[\s-]+/).filter(k=>k.length>3&&!s.has(k)).sort((k,D)=>D.length-k.length||k.localeCompare(D))[0]??d},p=new Map;for(const m of h){const d=n(m);let u=p.get(d);u||(u={concept:d,authors:new Set,lessons:[],namespaces:new Set,bestFix:m},p.set(d,u)),u.authors.add(m.author.toLowerCase()),u.lessons.push(m),u.namespaces.add(m.topic.split(":")[0]||"misc");const S=L=>(L.endorsements??0)*10+(L.confirm_count??0)+(L.review_level==="senior"?5:0);S(m)>S(u.bestFix)&&(u.bestFix=m)}const v=[...p.values()].filter(m=>m.authors.size>=t).sort((m,d)=>d.authors.size-m.authors.size||d.lessons.length-m.lessons.length).slice(0,10).map(m=>({concept:m.concept,author_count:m.authors.size,authors:[...m.authors],lesson_count:m.lessons.length,spans:[...m.namespaces],convergent_fix:m.bestFix.what_worked.slice(0,200),fix_topic:m.bestFix.topic})),A=a||`${i.toISOString().slice(0,7)} Team Crystal`,w={label:A,ts:i.toISOString(),kind:"team",contributors:new Set(h.map(m=>m.author.toLowerCase())).size,analysed:h.length,patterns:v};if(await o.set("cachly:crystal:team:latest",JSON.stringify(w)),await o.expire("cachly:crystal:team:latest",180*86400),v.length===0)return["\u{1F4A0} **Team Crystal \u2014 not enough cross-person convergence yet**","",`Analysed **${h.length}** attributed success lessons from **${w.contributors}** contributor${w.contributors!==1?"s":""}, but no concept had **${t}+ distinct authors** converge on it yet.`,"","This is the one thing a per-user memory can't build \u2014 it needs your team to accumulate shared, attributed lessons.",'Keep using `learn_from_attempts(author="...")` / `team_learn`; re-run as the team grows.'].join(`
|
|
21
|
+
`);const $=[`\u{1F4A0} **Team Crystal created: ${A}**`,"",`\u{1F9EC} **${v.length} cross-person pattern${v.length!==1?"s":""}** \u2014 fixes that ${t}+ teammates independently converged on.`,`_Compressed from ${h.length} attributed lessons across ${w.contributors} contributors. This is the team-wide, causal layer a per-user "Dreaming" process structurally cannot produce._`,""];for(const m of v.slice(0,6)){const d=m.spans.length>1?` \xB7 spans ${m.spans.map(u=>`\`${u}\``).join(", ")}`:"";$.push(`### \u{1F9E9} \`${m.concept}\` \u2014 ${m.author_count} people converged${d}`,`**Convergent fix** (from \`${m.fix_topic}\`): ${m.convergent_fix}`,`**Who solved it together:** ${m.authors.map(u=>`@${u}`).join(", ")} _(ask them \u2014 they've each hit this)_`,"")}return $.push("---","\u{1F4A1} Surfaces in `crystal_view` and onboarding. Re-run `team_crystallize` monthly."),$.join(`
|
|
22
|
+
`)}case"brain_doctor":{const{instance_id:r,workspace_path:e=""}=y,a=await _(r),o=[],i=[],t=[],c=a.scanStream({match:"cachly:lesson:best:*",count:200});await new Promise((f,j)=>{c.on("data",O=>t.push(...O)),c.on("end",f),c.on("error",j)});let l=0;const h=a.scanStream({match:"cachly:ctx:*",count:200});await new Promise((f,j)=>{h.on("data",O=>{l+=O.filter(C=>!C.endsWith(":meta")).length}),h.on("end",f),h.on("error",j)});const s=[];if(t.length>0){const f=await a.mget(...t);for(const j of f){const O=T(j??null,null);O&&s.push(O)}}const n=await a.get("cachly:session:last"),p=T(n,null),g=s.filter(f=>f.outcome==="failure"||f.outcome==="partial"),v=s.filter(f=>(f.recall_count??0)===0),A=s.filter(f=>f.severity==="critical"),w=s.filter(f=>f.outcome==="success"&&Y(f)<X),$=s.filter(f=>f.outcome==="success"&&Y(f)>=X&&Y(f)<me),m=s.filter(f=>(f.audit_trail??[]).length>1),d=s.filter(f=>f.author),u=new Set(s.map(f=>f.author).filter(Boolean)),S=s.reduce((f,j)=>f+(j.recall_count??0),0),L=s.length>0?Math.min(100,Math.round(S/s.length*10)):0,x=async f=>{let j=0;const O=a.scanStream({match:f,count:200});return await new Promise((C,W)=>{O.on("data",z=>{j+=z.length}),O.on("end",C),O.on("error",W)}),j},k=await x("cachly:lesson:vec:*"),D=await x("cachly:lesson:vecname:*"),M=await x("cachly:lesson:eing:*"),I=await x("cachly:lesson:pfad:*"),U=t.length>0?Math.round(k/t.length*100):100,Z=pe(t.length,k);t.length>0&&(Z==="aus"?(o.push(`semantic search is OFF: not a single lesson has an embedding (0 of ${t.length}). Recall falls back to keyword matching only. Fix: run the backfill (sdk/mcp/src/eingaenge-nachruesten.ts).`),i.push(`\u{1F534} **Semantic search: OFF** \u2014 0 of ${t.length} lessons have an embedding`)):Z==="luecke"?(o.push(`only ${U}% of lessons have an embedding (${k} of ${t.length}) \u2014 the rest can never be found by meaning.`),i.push(`\u{1F7E1} **Semantic coverage: ${U}%** \u2014 ${t.length-k} lessons without an embedding`)):(i.push(`\u2705 **Semantic coverage: ${U}%** \u2014 ${k} full-text, ${D} topic-name, ${M} with error-text entries`+(I>0?`, ${I} with learned question paths`:"")),I===0&&t.length>=20&&i.push("\u{1F4A4} **No learned question paths yet** \u2014 a question that finds the same lesson twice becomes its own entry. Needs repeated real use.")));let N=50;t.length>=5&&(N+=10),t.length>=20&&(N+=10),l>=3&&(N+=10),l>=10&&(N+=5),p&&(N+=10),g.length===0&&(N+=5);const te=s.length>0?v.length/s.length:0;te<.5&&(N+=10),w.length===0&&(N+=5),u.size>=2&&(N+=5),Z!=="gut"&&(N-=20),N=Math.max(0,Math.min(100,N));const re=N>=80?"\u{1F7E2}":N>=50?"\u{1F7E1}":"\u{1F534}",ce=L>=50?"\u{1F680}":L>=20?"\u{1F4C8}":"\u{1F4A4}";i.push(`${re} **Brain Quality Score: ${N}/100**`),i.push(`${ce} **Effective IQ Boost: ${L}%** (${S} recalls across ${s.length} lessons)`),i.push(`\u{1F4DA} **Lessons:** ${t.length} (${A.length} critical \xB7 ${m.length} with audit trail \xB7 ${d.length} from team)`),i.push(`\u{1F4BE} **Context entries:** ${l}`),i.push(`\u{1F3AF} **Confidence:** ${s.length-w.length-$.length} fresh \xB7 ${$.length} warn \xB7 ${w.length} stale`),i.push(`\u23F1\uFE0F **Decay config:** warn after ${ne}d \xB7 stale after ${oe}d`),i.push(de(ue));try{const f=await a.info("memory");let j=null;try{const C=await a.config("GET","maxmemory-policy");j=Array.isArray(C)&&C.length>1?String(C[1]):null}catch{j=null}const O={benutzt:se(f,"used_memory"),grenze:se(f,"maxmemory"),richtlinie:j};for(const C of he(O)){const z=`${C.art==="fehler"?"\u{1F534}":C.art==="hinweis"?"\u{1F7E1}":C.art==="ungemessen"?"\u26AA":"\u{1F7E2}"} **Speicher:** ${C.text}`;C.art==="fehler"||C.art==="ungemessen"?o.push(z):i.push(z)}}catch{o.push("\u26AA **Speicher:** Lage nicht abfragbar \u2014 unbekannt, ob dieser Speicher bei Platzmangel still loescht.")}u.size>=2&&i.push(`\u{1F465} **Team:** ${u.size} contributors (${[...u].join(", ")})`);try{const f=await a.get("cachly:index:last_run");if(f){const j=Math.round((Date.now()-new Date(f).getTime())/864e5);j>7?o.push(`\u{1F504} Index is ${j}d stale \u2014 run \`index_project\` to re-sync semantic search`):i.push(`\u{1F5C2}\uFE0F **Semantic index:** ${j}d old (fresh)`)}else o.push('\u{1F4A1} No semantic index \u2014 run `index_project(dir="<your-src>")` to enable semantic search')}catch{}try{const f=await a.get("cachly:crystal:latest");if(f){const j=JSON.parse(f),O=Math.round((Date.now()-new Date(j.ts).getTime())/864e5);i.push(`\u{1F48E} **Memory Crystal:** ${j.label} (${O}d ago)`),O>30&&o.push(`\u{1F4A1} Memory Crystal is ${O}d old \u2014 re-run \`memory_crystalize\` to compress new sessions`)}else t.length>=10&&o.push(`\u{1F4A1} ${t.length} lessons but no Memory Crystal \u2014 run \`memory_crystalize\` to compress wisdom`)}catch{}if(e)try{const f=e.replace(/\/$/,"")+"/package.json",j=le(f,"utf-8"),O=JSON.parse(j),C={...O.dependencies,...O.devDependencies},W=["openai","@anthropic-ai/sdk","@google/generative-ai","mistralai","cohere-ai"].some(V=>V in C),z="@cachly-dev/openclaw"in C;W&&!z?(o.push(`\u{1F4A1} **openclaw missing:** you use LLM APIs (${Object.keys(C).filter(V=>["openai","@anthropic-ai/sdk"].includes(V)).join(", ")}) but not \`@cachly-dev/openclaw\``),o.push(" \u2192 `npm install @cachly-dev/openclaw` cuts LLM costs 60\u201390% with 3 lines of code")):z&&i.push("\u2705 **@cachly-dev/openclaw installed** (LLM cost caching active)")}catch{}if(p){const f=Math.round((Date.now()-new Date(p.ts).getTime())/6e4),j=f<60?`${f}m`:f<1440?`${Math.round(f/60)}h`:`${Math.round(f/1440)}d`;i.push(`\u{1F550} **Last session:** ${j} ago`)}else o.push("\u274C No session history \u2014 call `session_start` + `session_end` to start tracking");t.length===0?o.push("\u274C No lessons \u2014 call `learn_from_attempts` after solving bugs"):t.length<5&&o.push(`\u{1F4A1} Only ${t.length} lessons \u2014 add more after each problem solved`),L===0&&s.length>=5&&o.push("\u{1F4A4} **IQ Boost is 0%** \u2014 lessons exist but are never recalled. Use `recall_best_solution` BEFORE tasks."),l===0&&o.push("\u{1F4A1} No context \u2014 use `remember_context` to cache architecture docs, ADRs, etc."),g.length>0&&o.push(`\u26A0\uFE0F ${g.length} unresolved failure${g.length>1?"s":""}: ${g.slice(0,3).map(f=>`\`${f.topic}\``).join(", ")}`),w.length>0&&(o.push(`\u{1F534} ${w.length} STALE lesson${w.length>1?"s":""} (>${oe}d, confidence <${X*100}%): ${w.slice(0,3).map(f=>`\`${f.topic}\``).join(", ")}`),o.push(" \u2192 Re-verify with `recall_best_solution` to reset confidence clock")),$.length>0&&o.push(`\u26A0\uFE0F ${$.length} lesson${$.length>1?"s":""} aging (>${ne}d): ${$.slice(0,3).map(f=>`\`${f.topic}\``).join(", ")}`),te>.7&&s.length>5&&o.push(`\u{1F4A1} ${v.length} lessons never recalled \u2014 verify topics match your workflow`);const P=["\u{1FA7A} **Brain Doctor**",`\`${"\u2500".repeat(48)}\``,"","**\u{1F4CA} Health Checks**",...i.map(f=>" "+f),""];return o.length>0?(P.push(`**\u{1F527} Action Items** (${o.length} issue${o.length>1?"s":""})`),o.forEach((f,j)=>P.push(` ${j+1}. ${f}`)),P.push(""),P.push("_Fix the action items above, then re-run `brain_doctor` to verify._")):(P.push("**\u2705 All checks passed** \u2014 Brain is healthy!"),P.push(""),P.push("_Keep the brain sharp: `session_start` \u2192 work \u2192 `learn_from_attempts` \u2192 `session_end`_")),P.join(`
|
|
23
|
+
`)}case"brain_hygiene":{const{instance_id:r,dry_run:e=!1,provisional_threshold:a=.5,archive_days:o=30}=y,i=await _(r),t=[],c=i.scanStream({match:"cachly:lesson:best:*",count:200});if(await new Promise((u,S)=>{c.on("data",L=>t.push(...L)),c.on("end",u),c.on("error",S)}),t.length===0)return"\u{1F9F9} **Brain Hygiene** \u2014 no lessons found. Nothing to sweep.";const l=await i.mget(...t),h=[];for(let u=0;u<t.length;u++){const S=l[u],L=T(S??null,null);L&&h.push({...L,_key:t[u]})}const s=Date.now(),n=[],p=[],g=[],v=new Map;for(const u of h){const S=u.state??"active";if(S==="archived")continue;const L=Y(u),x=new Date(u.verified_at??u.ts).getTime(),k=(s-x)/864e5;if(L<=a){const D=u.recall_count??0;if(S==="provisional"&&D<2&&k>o){if(p.push(u.topic),!e){const I={...u,state:"archived",audit_trail:[...u.audit_trail??[],{ts:new Date().toISOString(),action:"archived",prev_state:"provisional"}]};v.set(u._key,I)}}else if(S==="active"&&(n.push(u.topic),!e)){const I={...u,state:"provisional",audit_trail:[...u.audit_trail??[],{ts:new Date().toISOString(),action:"flagged_provisional",prev_state:"active"}]};v.set(u._key,I)}}}for(const u of h){const S=u.state??"active";if(S==="archived"||v.has(u._key)&&v.get(u._key).state==="archived"||u.outcome!=="failure"||(u.recall_count??0)>0)continue;const x=new Date(u.verified_at??u.ts).getTime(),k=(s-x)/864e5;if(k<=o)continue;const D=n.indexOf(u.topic);if(D!==-1&&n.splice(D,1),g.push(`${u.topic} (failure, never recalled, ${Math.round(k)}d old)`),!e){const M=v.get(u._key)??u,I={...M,state:"archived",audit_trail:[...M.audit_trail??[],{ts:new Date().toISOString(),action:"orphan_failure_archived",prev_state:S}]};v.set(u._key,I)}}let A=0,w=0;const $=parseFloat(await i.get(`cachly:stats:time_saved_mins:${r}`)??"0")||0;for(const u of h){if(ge(u)){if((u.recall_count??0)>0){A++;const S=v.get(u._key)??u;v.set(u._key,{...S,recall_count:0})}continue}(u.recall_count??0)>0&&(w+=fe(u))}if(e||await i.set(`cachly:stats:time_saved_mins:${r}`,String(w)).catch(()=>{}),!e&&v.size>0){const u=i.pipeline();for(const[S,L]of v){const{_key:x,...k}=L;u.set(S,JSON.stringify(k))}await u.exec()}const d=[`${e?"\u{1F50D} **Brain Hygiene (dry run)**":"\u{1F9F9} **Brain Hygiene**"} \u2014 scanned **${h.length}** lessons`,"","| Action | Count |","|---|---|",`| Flagged provisional (confidence < ${a}) | ${n.length} |`,`| Archived (stale + low-recall + age > ${o}d) | ${p.length} |`,`| Contradictions auto-resolved | ${g.length} |`,`| Startvorrat entwertet (Z\xE4hler auf 0) | ${A} |`,""];return Math.round($)!==Math.round(w)&&d.push(`**\u23F1\uFE0F Zeitersparnis neu gerechnet:** ${ae($)} \u2192 **${ae(w)}**`,"_Nur eigene Lektionen, und jede genau einmal. Der mitgelieferte Startvorrat ist Allgemeinwissen, kein gelerntes; und wer dieselbe Lektion zum zweiten Mal liest, spart keine zweite Recherche._",""),n.length>0&&(d.push("**\u26A0\uFE0F Provisional (verify or re-learn):**"),n.slice(0,10).forEach(u=>d.push(` - \`${u}\``)),n.length>10&&d.push(` - _\u2026and ${n.length-10} more_`),d.push("")),p.length>0&&(d.push("**\u{1F4E6} Archived (excluded from recall):**"),p.slice(0,5).forEach(u=>d.push(` - \`${u}\``)),p.length>5&&d.push(` - _\u2026and ${p.length-5} more_`),d.push("")),g.length>0&&(d.push("**\u2694\uFE0F Contradictions resolved (failure archived, success kept):**"),g.slice(0,5).forEach(u=>d.push(` - ${u}`)),d.push("")),e?d.push("_Run `brain_hygiene(dry_run=false)` to apply these changes._"):n.length+p.length+g.length===0?d.push("\u2705 Brain is clean \u2014 no hygiene actions needed."):d.push("_Changes applied. Run `brain_doctor` to verify the updated health score._"),d.join(`
|
|
24
|
+
`)}case"recall_at":{const{instance_id:r,topic:e,date:a}=y,o=await _(r),i=new Date(a).getTime();if(isNaN(i))return`\u274C Invalid date "${a}". Use ISO format: "2026-01-15"`;const t=`cachly:lessons:${e}`,c=await o.lrange(t,0,-1);if(c.length===0)return`\u{1F4ED} No history found for \`${e}\`. Lessons are stored via \`learn_from_attempts\`.`;const l=c.map(s=>{try{return JSON.parse(s)}catch{return null}}).filter(s=>s!==null&&new Date(s.ts).getTime()<=i).sort((s,n)=>new Date(s.ts).getTime()-new Date(n.ts).getTime());if(l.length===0){const s=T(c[0]??null,null),n=s?.ts?new Date(s.ts).toLocaleDateString("de-DE"):"unknown";return`\u{1F4ED} No entries for \`${e}\` found before **${a}**. Earliest entry: ${n}.`}const h=[`\u{1F3FA} **Brain Archaeology: \`${e}\` before ${a}**`,`_${l.length} of ${c.length} total entries shown_`,""];for(const s of l.slice(-10)){const n=s.outcome==="success"?"\u2705":s.outcome==="partial"?"\u26A0\uFE0F":"\u274C",p=new Date(s.ts).toLocaleDateString("de-DE");h.push(`**${p}** ${n} ${s.outcome}`),s.what_worked&&h.push(` \u2192 ${s.what_worked.slice(0,100)}`),h.push("")}return h.push(`_Full evolution: ${c.map(s=>{try{const n=JSON.parse(s);return n.outcome==="success"?"\u2705":n.outcome==="partial"?"\u26A0\uFE0F":"\u274C"}catch{return"?"}}).join(" \u2192 ")}_`),h.join(`
|
|
25
|
+
`)}case"trace_dependency":{const{instance_id:r,dependency:e,mark_review:a=!1}=y,o=await _(r),i=`cachly:dep:${e}`,t=await o.get(i);if(!t)return`\u{1F4ED} No lessons found that depend on \`${e}\`.
|
|
26
|
+
|
|
27
|
+
Add dependencies via: \`learn_from_attempts(..., depends_on=["${e}"])\``;const c=T(t,[]),l=[`\u{1F517} **Causal Chain: \`${e}\`** \u2014 ${c.length} dependent lesson${c.length===1?"":"s"}`,""];for(const h of c){const s=await o.get(`cachly:lesson:best:${h}`);if(!s){l.push(` \u2022 \`${h}\` _(lesson deleted)_`);continue}const n=T(s,null);if(!n){l.push(` \u2022 \`${h}\` _(lesson corrupted)_`);continue}const p=n.outcome==="success"?"\u2705":n.outcome==="partial"?"\u26A0\uFE0F":"\u274C",g=n.needs_review?" \u{1F50D} **needs_review**":"";if(l.push(` ${p} \`${h}\` (${n.severity??"major"})${g}`),a){const v={...n,needs_review:!0};await o.set(`cachly:lesson:best:${h}`,JSON.stringify(v))}}return a?l.push("",`\u{1F50D} All ${c.length} lessons marked as **needs_review** \u2014 verify they still work with the changed dependency.`):l.push("","_Run with `mark_review: true` to flag all dependent lessons for re-verification._"),l.join(`
|
|
28
|
+
`)}case"global_learn":{const{instance_id:r,topic:e,lesson:a,severity:o="minor",tags:i=[]}=y,t=await _(r),c=`cachly:global:lesson:${e}`,l={topic:e,lesson:a,severity:o,tags:i,ts:new Date().toISOString(),scope:"global",recall_count:0},h=await t.get(c);if(h){const s=T(h,null);l.recall_count=s?.recall_count??0}return await t.set(c,JSON.stringify(l)),`\u{1F310} **Global lesson stored**: \`${e}\`
|
|
29
|
+
|
|
30
|
+
${a}
|
|
31
|
+
|
|
32
|
+
Recallable from any project via \`global_recall(topic="${e}")\`.`}case"global_recall":{const{instance_id:r,topic:e}=y,a=await _(r),o=[],i=a.scanStream({match:"cachly:global:lesson:*",count:200});await new Promise((l,h)=>{i.on("data",s=>o.push(...s)),i.on("end",l),i.on("error",h)});let t=[];if(o.length>0){const l=await a.mget(...o);for(const h of l){const s=T(h??null,null);s&&t.push(s)}}if(e){const l=e.toLowerCase();t=t.filter(h=>h.topic.toLowerCase().includes(l))}if(t.length===0)return`\u{1F4ED} No global lessons${e?` for \`${e}\``:""}.
|
|
33
|
+
|
|
34
|
+
Add cross-project knowledge with \`global_learn(topic="...", lesson="...")\`.`;for(const l of t){const h=`cachly:global:lesson:${l.topic}`,s=await a.get(h);if(s){const n=T(s,null);if(!n)continue;n.recall_count=(n.recall_count??0)+1,n.last_recalled_at=new Date().toISOString(),await a.set(h,JSON.stringify(n))}}const c=[`\u{1F310} **Global Brain** \u2014 ${t.length} lesson${t.length>1?"s":""}`,""];for(const l of t){const h=l.severity==="critical"?"\u{1F534} ":l.severity==="major"?"\u{1F7E1} ":"";c.push(`${h}**\`${l.topic}\`**`),c.push(l.lesson.slice(0,200)),c.push("")}return c.join(`
|
|
35
|
+
`)}case"publish_lesson":{const{instance_id:r,topic:e,lesson:a,framework:o="general",severity:i="minor"}=y,t=await _(r),c=a.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,"[email]").replace(/\b(sk-|cky_live_|Bearer\s)[A-Za-z0-9_-]{8,}/g,"[token]").replace(/\/Users\/[^\s/]+/g,"/Users/[user]").replace(/\/home\/[^\s/]+/g,"/home/[user]"),l={topic:e,lesson:c,framework:o,severity:i,ts:new Date().toISOString(),published:!0,votes:0},h=`cachly:public:lesson:${o}:${e}`;return await t.set(h,JSON.stringify(l),"EX",365*86400),["\u{1F4E2} **Lesson published!**","",`**Topic:** \`${e}\``,`**Framework:** ${o}`,`**Content:** ${c.slice(0,200)}${c.length>200?"\u2026":""}`,"","This lesson is now available in the Public Brain for other developers.",`Import it anywhere: \`import_public_brain(framework="${o}")\``].join(`
|
|
36
|
+
`)}case"import_public_brain":{const{instance_id:r,framework:e,limit:a=20}=y,o=await _(r),i={nextjs:[{topic:"nextjs:image-layout",lesson:'Use fill + relative parent instead of layout="fill" (deprecated since Next.js 13)',severity:"major"},{topic:"nextjs:app-router-fetch",lesson:'fetch() in Server Components is cached by default \u2014 add {cache:"no-store"} for dynamic data',severity:"major"},{topic:"nextjs:metadata-export",lesson:"Export metadata const or generateMetadata() \u2014 never both in same file",severity:"minor"},{topic:"nextjs:client-boundary",lesson:'"use client" propagates down \u2014 keep it at the lowest component, not at page level',severity:"major"},{topic:"nextjs:env-prefix",lesson:"Only NEXT_PUBLIC_* env vars are exposed to client \u2014 others are server-only",severity:"critical"},{topic:"nextjs:revalidate",lesson:"export const revalidate = 0 disables caching for entire route; use revalidatePath() for on-demand",severity:"minor"}],fastapi:[{topic:"fastapi:async-db",lesson:"Use async session with asyncpg \u2014 sync SQLAlchemy blocks the event loop",severity:"critical"},{topic:"fastapi:pydantic-v2",lesson:"Pydantic v2: use model_validate() instead of parse_obj(), .dict() \u2192 .model_dump()",severity:"major"},{topic:"fastapi:lifespan",lesson:"Use lifespan context manager instead of deprecated on_event startup/shutdown",severity:"minor"},{topic:"fastapi:background-tasks",lesson:"BackgroundTasks run after response is sent \u2014 not in a separate thread pool",severity:"major"},{topic:"fastapi:cors-order",lesson:"CORSMiddleware must be added before other middleware to work correctly",severity:"critical"}],go:[{topic:"go:context-cancel",lesson:"Always call cancel() from context.WithCancel \u2014 leak goroutines if not cancelled",severity:"critical"},{topic:"go:defer-in-loop",lesson:"defer in a loop runs at function return, not loop iteration \u2014 use IIFE or explicit close",severity:"major"},{topic:"go:nil-interface",lesson:"nil interface != interface containing nil pointer \u2014 use explicit nil checks",severity:"major"},{topic:"go:goroutine-leak",lesson:"Goroutines with channel sends block forever if receiver is gone \u2014 use select with done chan",severity:"critical"},{topic:"go:embed-path",lesson:"//go:embed path must be relative and known at compile time \u2014 no os.Getenv",severity:"minor"}],docker:[{topic:"docker:layer-cache",lesson:"Copy package.json before source code \u2014 Docker caches layers, npm install only reruns on dep changes",severity:"major"},{topic:"docker:non-root",lesson:"Run as non-root user (USER 1001) \u2014 some k8s clusters reject root containers by policy",severity:"critical"},{topic:"docker:build-arg-secret",lesson:"Never use ARG for secrets \u2014 visible in image history. Use --secret mount instead",severity:"critical"},{topic:"docker:entrypoint-exec",lesson:'Use exec form ["cmd","arg"] not shell form "cmd arg" \u2014 shell form ignores SIGTERM',severity:"major"},{topic:"docker:multi-stage",lesson:"Multi-stage builds: copy only built artifacts to final stage \u2014 keep image small",severity:"minor"}],kubernetes:[{topic:"k8s:resource-limits",lesson:"Always set resource limits \u2014 unbounded pods cause node evictions and OOMKill",severity:"critical"},{topic:"k8s:liveness-vs-readiness",lesson:"Liveness failures restart pod; Readiness failures remove from LB. Use different endpoints",severity:"major"},{topic:"k8s:imagepullpolicy",lesson:"imagePullPolicy: Always in production \u2014 IfNotPresent can serve stale images",severity:"major"},{topic:"k8s:configmap-env",lesson:"ConfigMap changes don't restart pods \u2014 use rollout restart or mount as volume",severity:"critical"},{topic:"k8s:pdb",lesson:"Set PodDisruptionBudget for stateful apps \u2014 node drains kill all pods without it",severity:"major"}],react:[{topic:"react:useeffect-deps",lesson:"Omitting dependencies from useEffect deps array causes stale closure bugs \u2014 use exhaustive-deps ESLint rule",severity:"critical"},{topic:"react:key-index",lesson:"Never use array index as key in lists \u2014 causes subtle re-render bugs on reorder/delete",severity:"major"},{topic:"react:setState-in-render",lesson:"setState() during render causes infinite loop \u2014 move to useEffect or event handler",severity:"critical"},{topic:"react:memo-reference",lesson:"Object/array literals in JSX recreate on every render \u2014 useMemo for expensive derived values",severity:"minor"}],typescript:[{topic:"ts:type-guard",lesson:'Use "x is Type" return type for type guard functions \u2014 not "boolean"',severity:"minor"},{topic:"ts:strict-null",lesson:"Enable strictNullChecks in tsconfig \u2014 catches 90% of runtime null errors at compile time",severity:"critical"},{topic:"ts:enum-avoid",lesson:'Prefer union types ("a"|"b") over enum \u2014 enums have surprising runtime behavior',severity:"minor"},{topic:"ts:satisfies",lesson:'Use "satisfies" operator to validate type without widening \u2014 more precise than explicit annotation',severity:"minor"}],python:[{topic:"python:mutable-default",lesson:"Never use mutable default arguments (def f(x=[])) \u2014 shared across all calls. Use None + guard",severity:"critical"},{topic:"python:walrus-operator",lesson:":= (walrus) assigns and returns \u2014 useful in while/comprehensions but hard to read in complex expr",severity:"minor"},{topic:"python:asyncio-run",lesson:"asyncio.run() creates new event loop \u2014 calling it inside an existing loop raises RuntimeError",severity:"major"},{topic:"python:typing-optional",lesson:"Optional[X] == Union[X, None] \u2014 in Python 3.10+ use X | None syntax",severity:"minor"}]},t=e.toLowerCase(),c=i[t];if(!c){const n=Object.keys(i).join(", ");return`\u274C No public brain available for \`${e}\`.
|
|
37
|
+
|
|
38
|
+
Available: ${n}
|
|
39
|
+
|
|
40
|
+
Or publish your own: \`publish_lesson(framework="${e}", ...)\``}const l=c.slice(0,a);let h=0;for(const n of l){const p=`cachly:lesson:best:${n.topic}`;await o.get(p)||(await o.set(p,JSON.stringify({...n,what_worked:n.lesson,outcome:"success",ts:new Date().toISOString(),recall_count:0,source:"public_brain",version:2})),h++)}return[`\u{1F4E5} **Public Brain imported: ${e}**`,"",`${h} new lessons added (${l.length-h} already existed)`,"","**Imported topics:**",...l.map(n=>` ${n.severity==="critical"?"\u{1F534}":n.severity==="major"?"\u{1F7E1}":"\u{1F4A1}"} \`${n.topic}\``),"","These lessons will now appear in `session_start` when relevant.",`Recall any time: \`recall_best_solution(topic="${t}:...")\``].join(`
|
|
41
|
+
`)}case"setup_ai_memory":{const{instance_id:r,project_dir:e,embed_provider:a="openai",project_description:o="a software project"}=y,i=await R(`/api/v1/instances/${r}`),t={openai:{key:"OPENAI_API_KEY",hint:"Get at: https://platform.openai.com/api-keys"},mistral:{key:"MISTRAL_API_KEY",hint:"Get at: https://console.mistral.ai/api-keys"},cohere:{key:"COHERE_API_KEY",hint:"Get at: https://dashboard.cohere.com/api-keys"},ollama:{key:"OLLAMA_BASE_URL",hint:"Run: brew install ollama && ollama serve (free, local, no key needed)"},gemini:{key:"GEMINI_API_KEY",hint:"Get at: https://aistudio.google.com/app/apikey"}},c=t[a]??t.openai,l=!!i.vector_token,h=JSON.stringify({mcpServers:{cachly:{command:"npx",args:["-y","@cachly-dev/mcp-server@latest"],env:{CACHLY_JWT:"your-api-token-from-cachly.dev/settings",[c.key]:a==="ollama"?"http://localhost:11434":"your-key-here",...a!=="openai"?{CACHLY_EMBED_PROVIDER:a}:{}}}}},null,2),s=i.tier.toUpperCase(),n=l?'- `smart_recall("natural language query")` \u2014 semantic search (finds by meaning)':'- `recall_context("arch:*")` \u2014 exact/glob key lookup (upgrade to Speed/Business for smart_recall)',p=l?`Layer 3 (Semantic): smart_recall uses pgvector HNSW on your ${s} instance`:"Layer 3 (Autopilot): this file \u2014 upgrade to Speed/Business tier to unlock smart_recall",g=`# cachly AI Brain \u2014 ${o}
|
|
42
|
+
|
|
43
|
+
> AI memory system powered by [cachly.dev](${ye("/","team-readme")}). Works with GitHub Copilot, Claude Code, Cursor, Windsurf, Continue.dev, and any MCP-compatible editor.
|
|
1480
44
|
|
|
1481
45
|
---
|
|
1482
46
|
|
|
1483
|
-
##
|
|
47
|
+
## \u{1F534} MANDATORY RULES \u2014 Always enforced, no exceptions
|
|
1484
48
|
|
|
1485
|
-
### Rule 1
|
|
49
|
+
### Rule 1 \u2014 Session Start (FIRST thing in every window)
|
|
1486
50
|
Call \`session_start\` **before reading any source files or making any changes**:
|
|
1487
51
|
|
|
1488
52
|
\`\`\`
|
|
1489
53
|
session_start(
|
|
1490
|
-
instance_id = "${
|
|
54
|
+
instance_id = "${r}",
|
|
1491
55
|
focus = "<what you plan to work on today>",
|
|
1492
56
|
)
|
|
1493
57
|
\`\`\`
|
|
@@ -1498,12 +62,12 @@ Returns: last session summary, handoff from previous window, recent lessons, ope
|
|
|
1498
62
|
1. \`smart_recall("recent lessons <topic>")\`
|
|
1499
63
|
2. \`recall_best_solution("<topic>")\`
|
|
1500
64
|
|
|
1501
|
-
### Rule 2
|
|
1502
|
-
After **every** bug fix, deployment, failed attempt, or discovery
|
|
65
|
+
### Rule 2 \u2014 Learn After Every Fix/Deploy/Discovery (AUTOMATIC \u2014 no prompting needed)
|
|
66
|
+
After **every** bug fix, deployment, failed attempt, or discovery \u2014 call \`learn_from_attempts\` immediately:
|
|
1503
67
|
|
|
1504
68
|
\`\`\`
|
|
1505
69
|
learn_from_attempts(
|
|
1506
|
-
instance_id = "${
|
|
70
|
+
instance_id = "${r}",
|
|
1507
71
|
topic = "category:keyword", # deploy:web | fix:auth | debug:db | infra:docker
|
|
1508
72
|
outcome = "success" | "failure" | "partial",
|
|
1509
73
|
what_worked = "exact approach or command that worked",
|
|
@@ -1515,17 +79,17 @@ learn_from_attempts(
|
|
|
1515
79
|
\`\`\`
|
|
1516
80
|
|
|
1517
81
|
**These events MUST automatically trigger \`learn_from_attempts\`:**
|
|
1518
|
-
-
|
|
1519
|
-
-
|
|
1520
|
-
-
|
|
1521
|
-
-
|
|
1522
|
-
-
|
|
1523
|
-
-
|
|
1524
|
-
|
|
1525
|
-
### Rule 3
|
|
82
|
+
- \u2705 Successfully deployed \u2192 \`topic="deploy:<component>", outcome="success"\`
|
|
83
|
+
- \u2705 Fixed a bug \u2192 \`topic="fix:<description>", outcome="success"\`
|
|
84
|
+
- \u2705 Discovered root cause \u2192 \`topic="debug:<area>", outcome="partial"\`
|
|
85
|
+
- \u2705 Tried something that failed \u2192 \`topic="<area>", outcome="failure", what_failed="..."\`
|
|
86
|
+
- \u2705 Found a config quirk \u2192 \`topic="infra:<topic>", outcome="success"\`
|
|
87
|
+
- \u2705 Ran a working command \u2192 store it in \`commands=[...]\`
|
|
88
|
+
|
|
89
|
+
### Rule 3 \u2014 Session Handoff (when closing window or task list > 3 items)
|
|
1526
90
|
\`\`\`
|
|
1527
91
|
session_handoff(
|
|
1528
|
-
instance_id = "${
|
|
92
|
+
instance_id = "${r}",
|
|
1529
93
|
completed_tasks = ["what was done"],
|
|
1530
94
|
remaining_tasks = ["what is still to do"],
|
|
1531
95
|
files_changed = [{"path": "src/...", "status": "modified", "description": "what changed"}],
|
|
@@ -1536,20 +100,20 @@ session_handoff(
|
|
|
1536
100
|
|
|
1537
101
|
The next \`session_start\` automatically includes this handoff. The new window sees exactly what's done, what remains, and what NOT to break.
|
|
1538
102
|
|
|
1539
|
-
### Rule 4
|
|
103
|
+
### Rule 4 \u2014 Session End (LAST thing before closing)
|
|
1540
104
|
\`\`\`
|
|
1541
105
|
session_end(
|
|
1542
|
-
instance_id = "${
|
|
106
|
+
instance_id = "${r}",
|
|
1543
107
|
summary = "What was accomplished (2-3 sentences with specifics)",
|
|
1544
108
|
files_changed = ["list", "of", "changed", "files"],
|
|
1545
109
|
lessons_learned = <number of learn_from_attempts calls made>,
|
|
1546
110
|
)
|
|
1547
111
|
\`\`\`
|
|
1548
112
|
|
|
1549
|
-
> \`session_end\` **automatically extracts and stores lessons** from the summary text
|
|
113
|
+
> \`session_end\` **automatically extracts and stores lessons** from the summary text \u2014 no manual calls needed.
|
|
1550
114
|
|
|
1551
|
-
### Rule 5
|
|
1552
|
-
- \`index_project\` tracks MD5 hashes per file
|
|
115
|
+
### Rule 5 \u2014 Code Index Invalidation (AUTOMATIC)
|
|
116
|
+
- \`index_project\` tracks MD5 hashes per file \u2014 **skips unchanged files automatically**
|
|
1553
117
|
- After code changes: next \`index_project\` run re-indexes only modified files
|
|
1554
118
|
- After delete/rename: run \`index_project\` again to sync
|
|
1555
119
|
- No manual invalidation needed
|
|
@@ -1572,14 +136,13 @@ session_end(
|
|
|
1572
136
|
|
|
1573
137
|
| Tool | When to use |
|
|
1574
138
|
|------|-------------|
|
|
1575
|
-
| \`session_start\` | **FIRST**
|
|
1576
|
-
| \`session_end\` | **LAST**
|
|
139
|
+
| \`session_start\` | **FIRST** \u2014 mandatory at start of every session |
|
|
140
|
+
| \`session_end\` | **LAST** \u2014 mandatory at end, auto-learns from summary |
|
|
1577
141
|
| \`session_handoff\` | When closing window with remaining tasks |
|
|
1578
142
|
| \`learn_from_attempts\` | **AUTOMATIC** after every fix/deploy/discovery |
|
|
1579
143
|
| \`recall_best_solution\` | Before any non-trivial task |
|
|
1580
|
-
| \`remember_context\` | After analyzing code
|
|
1581
|
-
${
|
|
1582
|
-
| \`recall_context\` | Get exact key (supports glob: \`arch:*\`, \`file:*\`) |
|
|
144
|
+
| \`remember_context\` | After analyzing code \u2014 save findings for future sessions |
|
|
145
|
+
${n?"| `smart_recall` | Search brain by meaning/keywords |\n":""}| \`recall_context\` | Get exact key (supports glob: \`arch:*\`, \`file:*\`) |
|
|
1583
146
|
| \`brain_search\` | BM25+ full-text search over all brain data |
|
|
1584
147
|
| \`auto_learn_session\` | Batch-learn from a list of observations (optional) |
|
|
1585
148
|
| \`index_project\` | Index source files (smart hash, skips unchanged files) |
|
|
@@ -1590,373 +153,34 @@ ${smartRecallNote ? `| \`smart_recall\` | Search brain by meaning/keywords |\n`
|
|
|
1590
153
|
|
|
1591
154
|
## Instance Details
|
|
1592
155
|
|
|
1593
|
-
- **Instance ID:** \`${
|
|
1594
|
-
- **Instance name:** ${
|
|
1595
|
-
- **Tier:** ${
|
|
1596
|
-
- **${
|
|
1597
|
-
- **Embedding provider:** ${
|
|
156
|
+
- **Instance ID:** \`${r}\`
|
|
157
|
+
- **Instance name:** ${i.name}
|
|
158
|
+
- **Tier:** ${s}
|
|
159
|
+
- **${p}**
|
|
160
|
+
- **Embedding provider:** ${a}
|
|
1598
161
|
|
|
1599
162
|
---
|
|
1600
163
|
|
|
1601
164
|
## How the 3-layer system works
|
|
1602
165
|
|
|
1603
166
|
\`\`\`
|
|
1604
|
-
Layer 1
|
|
1605
|
-
Layer 2
|
|
1606
|
-
Layer 3
|
|
167
|
+
Layer 1 \u2014 Storage: Your cachly Valkey instance (${i.name}) \u2014 persists forever
|
|
168
|
+
Layer 2 \u2014 Tools: learn_from_attempts \xB7 recall_best_solution \xB7 brain_search \xB7 session_start/end
|
|
169
|
+
Layer 3 \u2014 Autopilot: This file \u2014 AI reads it and runs tools automatically every session
|
|
1607
170
|
\`\`\`
|
|
1608
171
|
|
|
1609
|
-
Result: Your AI **never solves the same problem twice** and always picks up exactly where it left off.
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
`**Step 2 โ Set your ${providerArg} key:**`,
|
|
1625
|
-
`\`\`\`bash`,
|
|
1626
|
-
`export ${provInfo.key}="your-key-here"`,
|
|
1627
|
-
`\`\`\``,
|
|
1628
|
-
`_(${provInfo.hint})_`,
|
|
1629
|
-
``,
|
|
1630
|
-
`โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ`,
|
|
1631
|
-
`**Step 3 โ copilot-instructions.md (Layer 3 Autopilot):**`,
|
|
1632
|
-
``,
|
|
1633
|
-
...(project_dir
|
|
1634
|
-
? [`๐ Detecting IDEs in \`${project_dir}\`โฆ`]
|
|
1635
|
-
: [`Copy this to \`.github/copilot-instructions.md\` (Copilot), \`CLAUDE.md\` (Claude Code), or \`.cursor/rules\` (Cursor) in your project:`]),
|
|
1636
|
-
``,
|
|
1637
|
-
`\`\`\`markdown`,
|
|
1638
|
-
copilotInstructions,
|
|
1639
|
-
`\`\`\``,
|
|
1640
|
-
``,
|
|
1641
|
-
`โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ`,
|
|
1642
|
-
`**How the 3 layers work together:**`,
|
|
1643
|
-
` Layer 1 โ Your Valkey instance stores all lessons + context (persists forever)`,
|
|
1644
|
-
` Layer 2 โ MCP tools (learn_from_attempts, recall_best_solution, smart_recall) read/write it`,
|
|
1645
|
-
` Layer 3 โ copilot-instructions.md makes your AI run them automatically`,
|
|
1646
|
-
``,
|
|
1647
|
-
`Result: Your AI never solves the same problem twice. ๐`,
|
|
1648
|
-
];
|
|
1649
|
-
// โโ IDE auto-detection + file writing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1650
|
-
if (project_dir) {
|
|
1651
|
-
const { mkdir, writeFile, access } = await import('node:fs/promises');
|
|
1652
|
-
const { constants } = await import('node:fs');
|
|
1653
|
-
const exists = async (p) => access(p, constants.F_OK).then(() => true).catch(() => false);
|
|
1654
|
-
const targets = [];
|
|
1655
|
-
let stopHookWritten = false;
|
|
1656
|
-
// GitHub Copilot โ always write (universal fallback)
|
|
1657
|
-
targets.push({
|
|
1658
|
-
ide: 'GitHub Copilot',
|
|
1659
|
-
path: join(project_dir, '.github', 'copilot-instructions.md'),
|
|
1660
|
-
content: copilotInstructions,
|
|
1661
|
-
});
|
|
1662
|
-
// Claude Code โ CLAUDE.md in project root
|
|
1663
|
-
if (await exists(join(project_dir, 'CLAUDE.md')) || await exists(join(project_dir, '.claude'))) {
|
|
1664
|
-
targets.push({
|
|
1665
|
-
ide: 'Claude Code',
|
|
1666
|
-
path: join(project_dir, 'CLAUDE.md'),
|
|
1667
|
-
content: copilotInstructions,
|
|
1668
|
-
});
|
|
1669
|
-
// Claude Code Stop-Hook โ auto-saves checkpoint when Claude stops responding
|
|
1670
|
-
const claudeDir = join(project_dir, '.claude');
|
|
1671
|
-
await mkdir(claudeDir, { recursive: true });
|
|
1672
|
-
const stopHook = {
|
|
1673
|
-
hooks: {
|
|
1674
|
-
Stop: [
|
|
1675
|
-
{
|
|
1676
|
-
matcher: '',
|
|
1677
|
-
hooks: [
|
|
1678
|
-
{
|
|
1679
|
-
type: 'command',
|
|
1680
|
-
command: `npx --yes @cachly-dev/mcp-server@latest checkpoint --instance-id ${instance_id}`,
|
|
1681
|
-
},
|
|
1682
|
-
],
|
|
1683
|
-
},
|
|
1684
|
-
],
|
|
1685
|
-
},
|
|
1686
|
-
};
|
|
1687
|
-
const settingsPath = join(claudeDir, 'settings.json');
|
|
1688
|
-
let existingSettings = {};
|
|
1689
|
-
try {
|
|
1690
|
-
const { readFile: rf } = await import('node:fs/promises');
|
|
1691
|
-
existingSettings = JSON.parse(await rf(settingsPath, 'utf-8'));
|
|
1692
|
-
}
|
|
1693
|
-
catch { /* new file */ }
|
|
1694
|
-
const merged = { ...existingSettings, hooks: stopHook.hooks };
|
|
1695
|
-
await writeFile(settingsPath, JSON.stringify(merged, null, 2), 'utf-8');
|
|
1696
|
-
stopHookWritten = true;
|
|
1697
|
-
}
|
|
1698
|
-
// Cursor โ .cursor/rules (new format) or .cursorrules (legacy)
|
|
1699
|
-
if (await exists(join(project_dir, '.cursor')) ||
|
|
1700
|
-
await exists(join(project_dir, '.cursorrules'))) {
|
|
1701
|
-
const cursorDir = join(project_dir, '.cursor');
|
|
1702
|
-
await mkdir(cursorDir, { recursive: true });
|
|
1703
|
-
targets.push({
|
|
1704
|
-
ide: 'Cursor',
|
|
1705
|
-
path: join(cursorDir, 'rules'),
|
|
1706
|
-
content: copilotInstructions,
|
|
1707
|
-
});
|
|
1708
|
-
}
|
|
1709
|
-
// Windsurf โ .windsurfrules
|
|
1710
|
-
if (await exists(join(project_dir, '.windsurfrules')) ||
|
|
1711
|
-
await exists(join(project_dir, '.windsurf'))) {
|
|
1712
|
-
targets.push({
|
|
1713
|
-
ide: 'Windsurf',
|
|
1714
|
-
path: join(project_dir, '.windsurfrules'),
|
|
1715
|
-
content: copilotInstructions,
|
|
1716
|
-
});
|
|
1717
|
-
}
|
|
1718
|
-
// VS Code (Copilot) โ already covered by .github/copilot-instructions.md above
|
|
1719
|
-
// Continue.dev โ .continue/config.json is JSON, not markdown โ skip, copilot-instructions handles it
|
|
1720
|
-
const written = [];
|
|
1721
|
-
for (const target of targets) {
|
|
1722
|
-
const dir = target.path.substring(0, target.path.lastIndexOf('/'));
|
|
1723
|
-
await mkdir(dir, { recursive: true });
|
|
1724
|
-
await writeFile(target.path, target.content, 'utf-8');
|
|
1725
|
-
written.push(`โ
[${target.ide}] โ \`${target.path.replace(project_dir, '.')}\``);
|
|
1726
|
-
}
|
|
1727
|
-
if (stopHookWritten) {
|
|
1728
|
-
written.push(`โ
[Claude Code Stop-Hook] โ \`.claude/settings.json\` (auto-checkpoint on stop)`);
|
|
1729
|
-
}
|
|
1730
|
-
lines.push(...written);
|
|
1731
|
-
}
|
|
1732
|
-
return lines.join('\n');
|
|
1733
|
-
}
|
|
1734
|
-
// โโ team_assign_role โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1735
|
-
// Assign a role (admin | reviewer | contributor | viewer) to a team member.
|
|
1736
|
-
// Only an existing admin can upgrade another member to admin or reviewer.
|
|
1737
|
-
// First call with role="admin" is bootstrapped (no auth required) so the
|
|
1738
|
-
// team can establish governance from a fresh brain.
|
|
1739
|
-
case 'team_assign_role': {
|
|
1740
|
-
const { instance_id, handle, role, assigned_by = '' } = args;
|
|
1741
|
-
if (!instance_id || !handle || !role)
|
|
1742
|
-
return 'โ Required: instance_id, handle, role';
|
|
1743
|
-
const cleanRole = ['admin', 'reviewer', 'contributor', 'viewer'].includes(role)
|
|
1744
|
-
? role : null;
|
|
1745
|
-
if (!cleanRole)
|
|
1746
|
-
return `โ Unknown role \`${role}\`. Valid: admin ยท reviewer ยท contributor ยท viewer`;
|
|
1747
|
-
const redis = await getConnection(instance_id);
|
|
1748
|
-
// Bootstrap: if there are no admins yet, the first assignment is free.
|
|
1749
|
-
const allRoles = await redis.hgetall(ROLES_KEY(instance_id)).catch(() => ({}));
|
|
1750
|
-
const hasAdmin = Object.values(allRoles).includes('admin');
|
|
1751
|
-
if (hasAdmin) {
|
|
1752
|
-
// After bootstrap, only an admin can assign roles.
|
|
1753
|
-
const actorRole = assigned_by ? await getRole(redis, instance_id, assigned_by) : null;
|
|
1754
|
-
if (!hasPermission(actorRole, 'admin')) {
|
|
1755
|
-
return [
|
|
1756
|
-
`โ **Permission denied** โ only an \`admin\` can assign roles.`,
|
|
1757
|
-
``,
|
|
1758
|
-
`Ask an existing admin to run:`,
|
|
1759
|
-
`\`team_assign_role(instance_id="${instance_id}", handle="${handle}", role="${role}", assigned_by="<their-handle>")\``,
|
|
1760
|
-
].join('\n');
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
const prevRole = allRoles[handle.toLowerCase()] ?? null;
|
|
1764
|
-
await redis.hset(ROLES_KEY(instance_id), handle.toLowerCase(), cleanRole);
|
|
1765
|
-
await redis.expire(ROLES_KEY(instance_id), 365 * 2 * 86400); // 2-year TTL
|
|
1766
|
-
// Governance audit trail (W6): record every role change with who made it.
|
|
1767
|
-
await auditLog(redis, instance_id, {
|
|
1768
|
-
action: 'team_assign_role', actor: assigned_by || '(bootstrap)',
|
|
1769
|
-
handle: handle.toLowerCase(), from: prevRole, to: cleanRole,
|
|
1770
|
-
});
|
|
1771
|
-
const isBootstrap = !hasAdmin;
|
|
1772
|
-
const badge = ROLE_BADGE[cleanRole];
|
|
1773
|
-
return [
|
|
1774
|
-
`${badge} **Role assigned** โ **${handle}** is now a \`${cleanRole}\``,
|
|
1775
|
-
``,
|
|
1776
|
-
`**Capabilities:** ${ROLE_CAPABILITIES[cleanRole]}`,
|
|
1777
|
-
``,
|
|
1778
|
-
isBootstrap ? `_First admin bootstrapped โ governance is now active on this brain._` : `_Assigned by ${assigned_by || '(bootstrap)'}_`,
|
|
1779
|
-
``,
|
|
1780
|
-
`Run \`team_roster\` to see all current roles.`,
|
|
1781
|
-
].join('\n');
|
|
1782
|
-
}
|
|
1783
|
-
// โโ team_whoami โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1784
|
-
// Shows a team member's own role and what they can do.
|
|
1785
|
-
case 'team_whoami': {
|
|
1786
|
-
const { instance_id, handle } = args;
|
|
1787
|
-
if (!instance_id || !handle)
|
|
1788
|
-
return 'โ Required: instance_id, handle';
|
|
1789
|
-
const redis = await getConnection(instance_id);
|
|
1790
|
-
const role = await getRole(redis, instance_id, handle);
|
|
1791
|
-
if (!role) {
|
|
1792
|
-
const allRoles = await redis.hgetall(ROLES_KEY(instance_id)).catch(() => ({}));
|
|
1793
|
-
const hasAdmin = Object.values(allRoles).includes('admin');
|
|
1794
|
-
return [
|
|
1795
|
-
`๐๏ธ **${handle}** has no assigned role on this brain.`,
|
|
1796
|
-
``,
|
|
1797
|
-
hasAdmin
|
|
1798
|
-
? `Ask an admin to run \`team_assign_role(handle="${handle}", role="contributor")\` to add you.`
|
|
1799
|
-
: `This brain has no roles set up yet. Establish governance:\n\`team_assign_role(handle="${handle}", role="admin")\``,
|
|
1800
|
-
].join('\n');
|
|
1801
|
-
}
|
|
1802
|
-
const badge = ROLE_BADGE[role];
|
|
1803
|
-
return [
|
|
1804
|
-
`## ${badge} ${handle} โ \`${role}\``,
|
|
1805
|
-
``,
|
|
1806
|
-
`**You can:** ${ROLE_CAPABILITIES[role]}`,
|
|
1807
|
-
``,
|
|
1808
|
-
`Run \`team_roster\` to see all team members and their roles.`,
|
|
1809
|
-
].join('\n');
|
|
1810
|
-
}
|
|
1811
|
-
// โโ team_roster โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1812
|
-
// Full view of all assigned roles, with last-active info from person nodes.
|
|
1813
|
-
case 'team_roster': {
|
|
1814
|
-
const { instance_id } = args;
|
|
1815
|
-
if (!instance_id)
|
|
1816
|
-
return 'โ Required: instance_id';
|
|
1817
|
-
const redis = await getConnection(instance_id);
|
|
1818
|
-
const allRoles = await redis.hgetall(ROLES_KEY(instance_id)).catch(() => ({}));
|
|
1819
|
-
if (Object.keys(allRoles).length === 0) {
|
|
1820
|
-
return [
|
|
1821
|
-
`## ๐ฅ Team Roster`,
|
|
1822
|
-
``,
|
|
1823
|
-
`No roles assigned yet. Establish governance with:`,
|
|
1824
|
-
`\`team_assign_role(instance_id="${instance_id}", handle="<your-handle>", role="admin")\``,
|
|
1825
|
-
].join('\n');
|
|
1826
|
-
}
|
|
1827
|
-
// Sort by role rank desc, then handle alpha
|
|
1828
|
-
const members = Object.entries(allRoles)
|
|
1829
|
-
.map(([h, r]) => ({ handle: h, role: r }))
|
|
1830
|
-
.sort((a, b) => ((ROLE_RANK[b.role] ?? 0) - (ROLE_RANK[a.role] ?? 0)) || a.handle.localeCompare(b.handle));
|
|
1831
|
-
const lines = [
|
|
1832
|
-
`## ๐ฅ Team Roster (${members.length} member${members.length !== 1 ? 's' : ''})`,
|
|
1833
|
-
``,
|
|
1834
|
-
`| Role | Handle | Capabilities |`,
|
|
1835
|
-
`|---|---|---|`,
|
|
1836
|
-
];
|
|
1837
|
-
for (const m of members) {
|
|
1838
|
-
const badge = ROLE_BADGE[m.role] ?? '?';
|
|
1839
|
-
lines.push(`| ${badge} ${m.role} | **${m.handle}** | ${ROLE_CAPABILITIES[m.role].split(' ยท ').slice(0, 2).join(', ')} |`);
|
|
1840
|
-
}
|
|
1841
|
-
lines.push(``, `_Manage with \`team_assign_role\`. Any admin can add or change roles._`);
|
|
1842
|
-
return lines.join('\n');
|
|
1843
|
-
}
|
|
1844
|
-
// โโ team_audit โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1845
|
-
// Governance/compliance trail (W6): who changed roles, who confirmed which
|
|
1846
|
-
// lessons, and when. Admin-gated once governance is active โ the audit log is
|
|
1847
|
-
// sensitive (it reveals the team's review activity). Newest entries first.
|
|
1848
|
-
case 'team_audit': {
|
|
1849
|
-
const { instance_id, requester = '', limit = 50 } = args;
|
|
1850
|
-
if (!instance_id)
|
|
1851
|
-
return 'โ Required: instance_id';
|
|
1852
|
-
const redis = await getConnection(instance_id);
|
|
1853
|
-
const denied = await requireRole(redis, instance_id, requester, 'admin', 'view the governance audit log');
|
|
1854
|
-
if (denied)
|
|
1855
|
-
return denied;
|
|
1856
|
-
const max = Math.min(Math.max(1, Number(limit) || 50), 200);
|
|
1857
|
-
const raw = await redis.lrange(`cachly:team:audit:${instance_id}`, -max, -1).catch(() => []);
|
|
1858
|
-
if (raw.length === 0) {
|
|
1859
|
-
return [
|
|
1860
|
-
`## ๐ Governance Audit Log`,
|
|
1861
|
-
``,
|
|
1862
|
-
`No governance events recorded yet.`,
|
|
1863
|
-
`Events are logged automatically on \`team_assign_role\` and \`team_confirm\`.`,
|
|
1864
|
-
].join('\n');
|
|
1865
|
-
}
|
|
1866
|
-
const entries = raw
|
|
1867
|
-
.map(r => safeJsonParse(r, null))
|
|
1868
|
-
.filter((e) => e !== null)
|
|
1869
|
-
.reverse(); // newest first
|
|
1870
|
-
const fmt = (e) => {
|
|
1871
|
-
const when = e.ts ? new Date(e.ts).toISOString().replace('T', ' ').slice(0, 16) : '?';
|
|
1872
|
-
if (e.action === 'team_assign_role') {
|
|
1873
|
-
return `| ${when} | ๐ role | **${e.actor}** set \`${e.handle}\`: ${e.from ?? 'โ'} โ **${e.to}** |`;
|
|
1874
|
-
}
|
|
1875
|
-
if (e.action === 'team_confirm') {
|
|
1876
|
-
return `| ${when} | โ
confirm | **${e.actor}** confirmed \`${e.topic}\` (${e.level}) |`;
|
|
1877
|
-
}
|
|
1878
|
-
return `| ${when} | ${e.action ?? '?'} | ${e.actor ?? '?'} |`;
|
|
1879
|
-
};
|
|
1880
|
-
return [
|
|
1881
|
-
`## ๐ Governance Audit Log (${entries.length} event${entries.length !== 1 ? 's' : ''})`,
|
|
1882
|
-
``,
|
|
1883
|
-
`| When (UTC) | Type | Detail |`,
|
|
1884
|
-
`|---|---|---|`,
|
|
1885
|
-
...entries.map(fmt),
|
|
1886
|
-
``,
|
|
1887
|
-
`_Immutable trail ยท 2-year retention ยท last ${max} events. Admin-only._`,
|
|
1888
|
-
].join('\n');
|
|
1889
|
-
}
|
|
1890
|
-
// โโ team_grant_scope โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1891
|
-
// Add (or remove) a member to a named group/sub-team. Group-scoped lessons
|
|
1892
|
-
// only surface in recall for members of that group. Admin-gated after the
|
|
1893
|
-
// role model is bootstrapped (consistent with team_assign_role).
|
|
1894
|
-
case 'team_grant_scope': {
|
|
1895
|
-
const { instance_id, handle, group, action = 'add', assigned_by = '' } = args;
|
|
1896
|
-
if (!instance_id || !handle || !group)
|
|
1897
|
-
return 'โ Required: instance_id, handle, group';
|
|
1898
|
-
const grp = String(group).toLowerCase().trim();
|
|
1899
|
-
if (!grp)
|
|
1900
|
-
return 'โ `group` must be a non-empty name (e.g. "backend", "security")';
|
|
1901
|
-
const redis = await getConnection(instance_id);
|
|
1902
|
-
// Admin gate (skipped during bootstrap, i.e. when no admin exists yet).
|
|
1903
|
-
const allRoles = await redis.hgetall(ROLES_KEY(instance_id)).catch(() => ({}));
|
|
1904
|
-
const hasAdmin = Object.values(allRoles).includes('admin');
|
|
1905
|
-
if (hasAdmin) {
|
|
1906
|
-
const actorRole = assigned_by ? await getRole(redis, instance_id, assigned_by) : null;
|
|
1907
|
-
if (!hasPermission(actorRole, 'admin')) {
|
|
1908
|
-
return `โ **Permission denied** โ only an \`admin\` can manage group scopes. Ask an admin to set \`assigned_by\`.`;
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
if (action === 'remove') {
|
|
1912
|
-
await redis.srem(GROUP_KEY(instance_id, grp), handle.toLowerCase());
|
|
1913
|
-
return `๐ **${handle}** removed from group \`${grp}\`.`;
|
|
1914
|
-
}
|
|
1915
|
-
await redis.sadd(GROUPS_INDEX_KEY(instance_id), grp);
|
|
1916
|
-
await redis.sadd(GROUP_KEY(instance_id, grp), handle.toLowerCase());
|
|
1917
|
-
await redis.expire(GROUPS_INDEX_KEY(instance_id), 365 * 2 * 86400);
|
|
1918
|
-
await redis.expire(GROUP_KEY(instance_id, grp), 365 * 2 * 86400);
|
|
1919
|
-
return [
|
|
1920
|
-
`๐ **Scope granted** โ **${handle}** is now in group \`${grp}\`.`,
|
|
1921
|
-
``,
|
|
1922
|
-
`Lessons stored with \`group="${grp}"\` will surface in recall for ${handle} (and other \`${grp}\` members + admins) only.`,
|
|
1923
|
-
``,
|
|
1924
|
-
`Store a scoped lesson: \`learn_from_attempts(topic="...", group="${grp}", ...)\``,
|
|
1925
|
-
].join('\n');
|
|
1926
|
-
}
|
|
1927
|
-
// โโ team_scopes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
1928
|
-
// List all groups + members, or the groups a specific handle belongs to.
|
|
1929
|
-
case 'team_scopes': {
|
|
1930
|
-
const { instance_id, handle } = args;
|
|
1931
|
-
if (!instance_id)
|
|
1932
|
-
return 'โ Required: instance_id';
|
|
1933
|
-
const redis = await getConnection(instance_id);
|
|
1934
|
-
if (handle) {
|
|
1935
|
-
const scopes = await getScopes(redis, instance_id, handle);
|
|
1936
|
-
if (scopes.size === 0) {
|
|
1937
|
-
return `๐๏ธ **${handle}** is in no groups โ sees only team-wide + public lessons.`;
|
|
1938
|
-
}
|
|
1939
|
-
return `๐ **${handle}** belongs to: ${[...scopes].map(g => `\`${g}\``).join(', ')}`;
|
|
1940
|
-
}
|
|
1941
|
-
const groups = await redis.smembers(GROUPS_INDEX_KEY(instance_id)).catch(() => []);
|
|
1942
|
-
if (groups.length === 0) {
|
|
1943
|
-
return [
|
|
1944
|
-
`## ๐ Team Scopes`,
|
|
1945
|
-
``,
|
|
1946
|
-
`No groups defined yet. Create one by adding a member:`,
|
|
1947
|
-
`\`team_grant_scope(instance_id="${instance_id}", handle="alice", group="backend")\``,
|
|
1948
|
-
].join('\n');
|
|
1949
|
-
}
|
|
1950
|
-
const lines = [`## ๐ Team Scopes (${groups.length} group${groups.length !== 1 ? 's' : ''})`, ``];
|
|
1951
|
-
for (const g of groups.sort()) {
|
|
1952
|
-
const members = await redis.smembers(GROUP_KEY(instance_id, g)).catch(() => []);
|
|
1953
|
-
lines.push(`- **\`${g}\`** (${members.length}): ${members.length ? members.map(m => `**${m}**`).join(', ') : '_empty_'}`);
|
|
1954
|
-
}
|
|
1955
|
-
lines.push(``, `_Scope a lesson to a group with \`learn_from_attempts(group="...")\`. Members + admins see it; others don't._`);
|
|
1956
|
-
return lines.join('\n');
|
|
1957
|
-
}
|
|
1958
|
-
default:
|
|
1959
|
-
return null;
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
//# sourceMappingURL=team.js.map
|
|
172
|
+
Result: Your AI **never solves the same problem twice** and always picks up exactly where it left off. \u{1F680}
|
|
173
|
+
`,v=["\u{1F9E0} **cachly AI Memory Setup Complete**","",`**Instance:** ${i.name} (${s}) \xB7 ID: \`${r}\``,`**Embedding Provider:** ${a}`,`**Semantic Search:** ${l?"\u2705 pgvector HNSW available":"\u26A0\uFE0F Not available on "+s+" \u2014 upgrade to Speed/Business"}`,"","\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500","**Step 1 \u2014 Add to .mcp.json:**","```json",h,"```","",`**Step 2 \u2014 Set your ${a} key:**`,"```bash",`export ${c.key}="your-key-here"`,"```",`_(${c.hint})_`,"","\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500","**Step 3 \u2014 copilot-instructions.md (Layer 3 Autopilot):**","",...e?[`\u{1F50D} Detecting IDEs in \`${e}\`\u2026`]:["Copy this to `.github/copilot-instructions.md` (Copilot), `CLAUDE.md` (Claude Code), or `.cursor/rules` (Cursor) in your project:"],"","```markdown",g,"```","","\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500","**How the 3 layers work together:**"," Layer 1 \u2192 Your Valkey instance stores all lessons + context (persists forever)"," Layer 2 \u2192 MCP tools (learn_from_attempts, recall_best_solution, smart_recall) read/write it"," Layer 3 \u2192 copilot-instructions.md makes your AI run them automatically","","Result: Your AI never solves the same problem twice. \u{1F680}"];if(e){const{mkdir:A,writeFile:w,access:$}=await import("node:fs/promises"),{constants:m}=await import("node:fs"),d=async x=>$(x,m.F_OK).then(()=>!0).catch(()=>!1),u=[];let S=!1;if(u.push({ide:"GitHub Copilot",path:E(e,".github","copilot-instructions.md"),content:g}),await d(E(e,"CLAUDE.md"))||await d(E(e,".claude"))){u.push({ide:"Claude Code",path:E(e,"CLAUDE.md"),content:g});const x=E(e,".claude");await A(x,{recursive:!0});const k={hooks:{Stop:[{matcher:"",hooks:[{type:"command",command:`npx --yes @cachly-dev/mcp-server@latest checkpoint --instance-id ${r}`}]}]}},D=E(x,"settings.json");let M={};try{const{readFile:U}=await import("node:fs/promises");M=JSON.parse(await U(D,"utf-8"))}catch{}const I={...M,hooks:k.hooks};await w(D,JSON.stringify(I,null,2),"utf-8"),S=!0}if(await d(E(e,".cursor"))||await d(E(e,".cursorrules"))){const x=E(e,".cursor");await A(x,{recursive:!0}),u.push({ide:"Cursor",path:E(x,"rules"),content:g})}(await d(E(e,".windsurfrules"))||await d(E(e,".windsurf")))&&u.push({ide:"Windsurf",path:E(e,".windsurfrules"),content:g});const L=[];for(const x of u){const k=x.path.substring(0,x.path.lastIndexOf("/"));await A(k,{recursive:!0}),await w(x.path,x.content,"utf-8"),L.push(`\u2705 [${x.ide}] \u2192 \`${x.path.replace(e,".")}\``)}S&&L.push("\u2705 [Claude Code Stop-Hook] \u2192 `.claude/settings.json` (auto-checkpoint on stop)"),v.push(...L)}return v.join(`
|
|
174
|
+
`)}case"team_assign_role":{const{instance_id:r,handle:e,role:a,assigned_by:o=""}=y;if(!r||!e||!a)return"\u274C Required: instance_id, handle, role";const i=["admin","reviewer","contributor","viewer"].includes(a)?a:null;if(!i)return`\u274C Unknown role \`${a}\`. Valid: admin \xB7 reviewer \xB7 contributor \xB7 viewer`;const t=await _(r),c=await t.hgetall(B(r)).catch(()=>({})),l=Object.values(c).includes("admin");if(l){const p=o?await J(t,r,o):null;if(!H(p,"admin"))return["\u274C **Permission denied** \u2014 only an `admin` can assign roles.","","Ask an existing admin to run:",`\`team_assign_role(instance_id="${r}", handle="${e}", role="${a}", assigned_by="<their-handle>")\``].join(`
|
|
175
|
+
`)}const h=c[e.toLowerCase()]??null;await t.hset(B(r),e.toLowerCase(),i),await t.expire(B(r),365*2*86400),await ie(t,r,{action:"team_assign_role",actor:o||"(bootstrap)",handle:e.toLowerCase(),from:h,to:i});const s=!l;return[`${F[i]} **Role assigned** \u2014 **${e}** is now a \`${i}\``,"",`**Capabilities:** ${Q[i]}`,"",s?"_First admin bootstrapped \u2014 governance is now active on this brain._":`_Assigned by ${o||"(bootstrap)"}_`,"","Run `team_roster` to see all current roles."].join(`
|
|
176
|
+
`)}case"team_whoami":{const{instance_id:r,handle:e}=y;if(!r||!e)return"\u274C Required: instance_id, handle";const a=await _(r),o=await J(a,r,e);if(!o){const t=await a.hgetall(B(r)).catch(()=>({})),c=Object.values(t).includes("admin");return[`\u{1F441}\uFE0F **${e}** has no assigned role on this brain.`,"",c?`Ask an admin to run \`team_assign_role(handle="${e}", role="contributor")\` to add you.`:`This brain has no roles set up yet. Establish governance:
|
|
177
|
+
\`team_assign_role(handle="${e}", role="admin")\``].join(`
|
|
178
|
+
`)}return[`## ${F[o]} ${e} \u2014 \`${o}\``,"",`**You can:** ${Q[o]}`,"","Run `team_roster` to see all team members and their roles."].join(`
|
|
179
|
+
`)}case"team_roster":{const{instance_id:r}=y;if(!r)return"\u274C Required: instance_id";const a=await(await _(r)).hgetall(B(r)).catch(()=>({}));if(Object.keys(a).length===0)return["## \u{1F465} Team Roster","","No roles assigned yet. Establish governance with:",`\`team_assign_role(instance_id="${r}", handle="<your-handle>", role="admin")\``].join(`
|
|
180
|
+
`);const o=Object.entries(a).map(([t,c])=>({handle:t,role:c})).sort((t,c)=>(G[c.role]??0)-(G[t.role]??0)||t.handle.localeCompare(c.handle)),i=[`## \u{1F465} Team Roster (${o.length} member${o.length!==1?"s":""})`,"","| Role | Handle | Capabilities |","|---|---|---|"];for(const t of o){const c=F[t.role]??"?";i.push(`| ${c} ${t.role} | **${t.handle}** | ${Q[t.role].split(" \xB7 ").slice(0,2).join(", ")} |`)}return i.push("","_Manage with `team_assign_role`. Any admin can add or change roles._"),i.join(`
|
|
181
|
+
`)}case"team_audit":{const{instance_id:r,requester:e="",limit:a=50}=y;if(!r)return"\u274C Required: instance_id";const o=await _(r),i=await ee(o,r,e,"admin","view the governance audit log");if(i)return i;const t=Math.min(Math.max(1,Number(a)||50),200),c=await o.lrange(`cachly:team:audit:${r}`,-t,-1).catch(()=>[]);if(c.length===0)return["## \u{1F4DC} Governance Audit Log","","No governance events recorded yet.","Events are logged automatically on `team_assign_role` and `team_confirm`."].join(`
|
|
182
|
+
`);const l=c.map(s=>T(s,null)).filter(s=>s!==null).reverse(),h=s=>{const n=s.ts?new Date(s.ts).toISOString().replace("T"," ").slice(0,16):"?";return s.action==="team_assign_role"?`| ${n} | \u{1F451} role | **${s.actor}** set \`${s.handle}\`: ${s.from??"\u2014"} \u2192 **${s.to}** |`:s.action==="team_confirm"?`| ${n} | \u2705 confirm | **${s.actor}** confirmed \`${s.topic}\` (${s.level}) |`:`| ${n} | ${s.action??"?"} | ${s.actor??"?"} |`};return[`## \u{1F4DC} Governance Audit Log (${l.length} event${l.length!==1?"s":""})`,"","| When (UTC) | Type | Detail |","|---|---|---|",...l.map(h),"",`_Immutable trail \xB7 2-year retention \xB7 last ${t} events. Admin-only._`].join(`
|
|
183
|
+
`)}case"team_grant_scope":{const{instance_id:r,handle:e,group:a,action:o="add",assigned_by:i=""}=y;if(!r||!e||!a)return"\u274C Required: instance_id, handle, group";const t=String(a).toLowerCase().trim();if(!t)return'\u274C `group` must be a non-empty name (e.g. "backend", "security")';const c=await _(r),l=await c.hgetall(B(r)).catch(()=>({}));if(Object.values(l).includes("admin")){const s=i?await J(c,r,i):null;if(!H(s,"admin"))return"\u274C **Permission denied** \u2014 only an `admin` can manage group scopes. Ask an admin to set `assigned_by`."}return o==="remove"?(await c.srem(K(r,t),e.toLowerCase()),`\u{1F513} **${e}** removed from group \`${t}\`.`):(await c.sadd(q(r),t),await c.sadd(K(r,t),e.toLowerCase()),await c.expire(q(r),365*2*86400),await c.expire(K(r,t),365*2*86400),[`\u{1F510} **Scope granted** \u2014 **${e}** is now in group \`${t}\`.`,"",`Lessons stored with \`group="${t}"\` will surface in recall for ${e} (and other \`${t}\` members + admins) only.`,"",`Store a scoped lesson: \`learn_from_attempts(topic="...", group="${t}", ...)\``].join(`
|
|
184
|
+
`))}case"team_scopes":{const{instance_id:r,handle:e}=y;if(!r)return"\u274C Required: instance_id";const a=await _(r);if(e){const t=await xe(a,r,e);return t.size===0?`\u{1F441}\uFE0F **${e}** is in no groups \u2014 sees only team-wide + public lessons.`:`\u{1F510} **${e}** belongs to: ${[...t].map(c=>`\`${c}\``).join(", ")}`}const o=await a.smembers(q(r)).catch(()=>[]);if(o.length===0)return["## \u{1F510} Team Scopes","","No groups defined yet. Create one by adding a member:",`\`team_grant_scope(instance_id="${r}", handle="alice", group="backend")\``].join(`
|
|
185
|
+
`);const i=[`## \u{1F510} Team Scopes (${o.length} group${o.length!==1?"s":""})`,""];for(const t of o.sort()){const c=await a.smembers(K(r,t)).catch(()=>[]);i.push(`- **\`${t}\`** (${c.length}): ${c.length?c.map(l=>`**${l}**`).join(", "):"_empty_"}`)}return i.push("",'_Scope a lesson to a group with `learn_from_attempts(group="...")`. Members + admins see it; others don\'t._'),i.join(`
|
|
186
|
+
`)}default:return null}}export{q as GROUPS_INDEX_KEY,K as GROUP_KEY,$e as PROPOSED_LESSON_PREFIX,ve as REVIEW_MODE_KEY,B as ROLES_KEY,F as ROLE_BADGE,Ie as TEAM_TOOL_NAMES,ie as auditLog,J as getRole,xe as getScopes,Pe as handleTeamTool,H as hasPermission,Me as lessonVisibleToScope,_e as rbacActive,ee as requireRole,ke as reviewModeEnabled,we as roleToReviewLevel,Se as storeLessonProposal};
|