@cachly-dev/mcp-server 0.9.19 → 0.9.21
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/.github/workflows/publish.yml +42 -0
- package/LICENSE +202 -0
- package/dist/handlers/advanced.d.ts +6 -0
- package/dist/handlers/advanced.js +586 -0
- package/dist/handlers/advanced.js.map +1 -0
- package/dist/handlers/cache.d.ts +6 -0
- package/dist/handlers/cache.js +520 -0
- package/dist/handlers/cache.js.map +1 -0
- package/dist/handlers/context.d.ts +6 -0
- package/dist/handlers/context.js +177 -0
- package/dist/handlers/context.js.map +1 -0
- package/dist/handlers/fedbrain.d.ts +6 -0
- package/dist/handlers/fedbrain.js +1078 -0
- package/dist/handlers/fedbrain.js.map +1 -0
- package/dist/handlers/instances.d.ts +9 -0
- package/dist/handlers/instances.js +153 -0
- package/dist/handlers/instances.js.map +1 -0
- package/dist/handlers/roadmap.d.ts +6 -0
- package/dist/handlers/roadmap.js +165 -0
- package/dist/handlers/roadmap.js.map +1 -0
- package/dist/handlers/syndicate.d.ts +6 -0
- package/dist/handlers/syndicate.js +339 -0
- package/dist/handlers/syndicate.js.map +1 -0
- package/dist/handlers/team.d.ts +6 -0
- package/dist/handlers/team.js +1048 -0
- package/dist/handlers/team.js.map +1 -0
- package/dist/index.js +38 -4018
- package/dist/index.js.map +1 -1
- package/glama.json +6 -0
- package/package.json +1 -1
- package/src/handlers/advanced.ts +637 -0
- package/src/handlers/cache.ts +634 -0
- package/src/handlers/context.ts +218 -0
- package/src/handlers/fedbrain.ts +1143 -0
- package/src/handlers/instances.ts +180 -0
- package/src/handlers/roadmap.ts +192 -0
- package/src/handlers/syndicate.ts +451 -0
- package/src/handlers/team.ts +1163 -0
- package/src/index.ts +44 -4465
|
@@ -0,0 +1,1048 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { calculateConfidence, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_VALUE, CONFIDENCE_WARN_DAYS, CONFIDENCE_STALE_DAYS } from '../confidence.js';
|
|
4
|
+
export const TEAM_TOOL_NAMES = new Set([
|
|
5
|
+
'sync_file_changes', 'team_learn', 'team_recall', 'team_synthesize', 'memory_crystalize',
|
|
6
|
+
'brain_doctor', 'recall_at', 'trace_dependency', 'global_learn', 'global_recall',
|
|
7
|
+
'publish_lesson', 'import_public_brain', 'setup_ai_memory',
|
|
8
|
+
]);
|
|
9
|
+
export async function handleTeamTool(name, args, getConnection, apiFetch) {
|
|
10
|
+
switch (name) {
|
|
11
|
+
case 'sync_file_changes': {
|
|
12
|
+
const { instance_id, changed_files, git_diff_stat, commit_msg } = args;
|
|
13
|
+
const redis = await getConnection(instance_id);
|
|
14
|
+
// Store file change event in session history
|
|
15
|
+
const changeRecord = {
|
|
16
|
+
ts: new Date().toISOString(),
|
|
17
|
+
files: changed_files,
|
|
18
|
+
commit_msg,
|
|
19
|
+
diff_stat: git_diff_stat?.slice(0, 500),
|
|
20
|
+
};
|
|
21
|
+
await redis.lpush('cachly:session:file_changes', JSON.stringify(changeRecord));
|
|
22
|
+
await redis.ltrim('cachly:session:file_changes', 0, 99);
|
|
23
|
+
await redis.expire('cachly:session:file_changes', 30 * 86400);
|
|
24
|
+
// Find lessons relevant to the changed files
|
|
25
|
+
const lessonKeys = [];
|
|
26
|
+
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
27
|
+
await new Promise((resolve, reject) => {
|
|
28
|
+
lStream.on('data', (batch) => lessonKeys.push(...batch));
|
|
29
|
+
lStream.on('end', resolve);
|
|
30
|
+
lStream.on('error', reject);
|
|
31
|
+
});
|
|
32
|
+
const relevant = [];
|
|
33
|
+
for (const k of lessonKeys) {
|
|
34
|
+
const raw = await redis.get(k);
|
|
35
|
+
if (!raw)
|
|
36
|
+
continue;
|
|
37
|
+
const lesson = JSON.parse(raw);
|
|
38
|
+
// Match by file_paths stored in lesson OR by topic keywords matching file name
|
|
39
|
+
const topicWords = lesson.topic.toLowerCase().split(/[:\-_]/);
|
|
40
|
+
const fileMatches = changed_files.some(f => {
|
|
41
|
+
const fname = f.split('/').pop()?.replace(/\.[^.]+$/, '').toLowerCase() ?? '';
|
|
42
|
+
return topicWords.some(w => w.length > 3 && fname.includes(w))
|
|
43
|
+
|| (lesson.file_paths ?? []).some(lf => f.includes(lf) || lf.includes(f));
|
|
44
|
+
});
|
|
45
|
+
if (fileMatches) {
|
|
46
|
+
const emoji = lesson.outcome === 'success' ? '✅' : '⚠️';
|
|
47
|
+
relevant.push(` ${emoji} \`${lesson.topic}\` — ${lesson.what_worked.slice(0, 80)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const lines = [
|
|
51
|
+
`📁 **File sync recorded**: ${changed_files.length} files`,
|
|
52
|
+
commit_msg ? `📝 Commit: "${commit_msg}"` : '',
|
|
53
|
+
'',
|
|
54
|
+
`**Changed:** ${changed_files.slice(0, 8).map(f => `\`${f}\``).join(', ')}${changed_files.length > 8 ? ` +${changed_files.length - 8} more` : ''}`,
|
|
55
|
+
'',
|
|
56
|
+
];
|
|
57
|
+
if (relevant.length > 0) {
|
|
58
|
+
lines.push(`🧠 **Relevant brain lessons (${relevant.length}):**`, ...relevant);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
lines.push(`💡 No existing lessons match these files yet. Add them with \`learn_from_attempts\`.`);
|
|
62
|
+
}
|
|
63
|
+
return lines.filter(Boolean).join('\n');
|
|
64
|
+
}
|
|
65
|
+
// ── team_learn ────────────────────────────────────────────────────────────
|
|
66
|
+
case 'team_learn': {
|
|
67
|
+
const { instance_id, author, topic, outcome, what_worked, what_failed, severity, file_paths, commands, tags } = args;
|
|
68
|
+
if (!author || !topic || !outcome || !what_worked) {
|
|
69
|
+
return '❌ Required: author, topic, outcome, what_worked';
|
|
70
|
+
}
|
|
71
|
+
const iid = instance_id;
|
|
72
|
+
if (!iid)
|
|
73
|
+
return '❌ instance_id required';
|
|
74
|
+
// Store with author attribution via the same learn_from_attempts Redis structure
|
|
75
|
+
const lesson = {
|
|
76
|
+
topic, outcome, what_worked,
|
|
77
|
+
what_failed: what_failed ?? '',
|
|
78
|
+
severity: severity ?? 'minor',
|
|
79
|
+
author,
|
|
80
|
+
file_paths: file_paths ?? [],
|
|
81
|
+
commands: commands ?? [],
|
|
82
|
+
tags: [...(tags ?? []), 'team'],
|
|
83
|
+
timestamp: new Date().toISOString(),
|
|
84
|
+
recall_count: 0,
|
|
85
|
+
version: 2,
|
|
86
|
+
};
|
|
87
|
+
const redis = await getConnection(iid);
|
|
88
|
+
const key = `cachly:lessons:${topic}`;
|
|
89
|
+
await redis.rpush(key, JSON.stringify(lesson));
|
|
90
|
+
if (outcome === 'success') {
|
|
91
|
+
await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(lesson));
|
|
92
|
+
}
|
|
93
|
+
return `✅ Team lesson stored by **${author}**: \`${topic}\` (${outcome})\n💡 ${what_worked.slice(0, 120)}`;
|
|
94
|
+
}
|
|
95
|
+
// ── team_recall ───────────────────────────────────────────────────────────
|
|
96
|
+
case 'team_recall': {
|
|
97
|
+
const { instance_id, topic, author, limit = 10 } = args;
|
|
98
|
+
const redis = await getConnection(instance_id);
|
|
99
|
+
const lessonKeys = [];
|
|
100
|
+
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
101
|
+
await new Promise((resolve, reject) => {
|
|
102
|
+
lStream.on('data', (batch) => lessonKeys.push(...batch));
|
|
103
|
+
lStream.on('end', resolve);
|
|
104
|
+
lStream.on('error', reject);
|
|
105
|
+
});
|
|
106
|
+
let lessons = [];
|
|
107
|
+
for (const k of lessonKeys) {
|
|
108
|
+
const raw = await redis.get(k);
|
|
109
|
+
if (!raw)
|
|
110
|
+
continue;
|
|
111
|
+
try {
|
|
112
|
+
lessons.push(JSON.parse(raw));
|
|
113
|
+
}
|
|
114
|
+
catch { /* skip */ }
|
|
115
|
+
}
|
|
116
|
+
// Filter
|
|
117
|
+
if (topic) {
|
|
118
|
+
const t = topic.toLowerCase();
|
|
119
|
+
lessons = lessons.filter(l => l.topic.toLowerCase().includes(t) ||
|
|
120
|
+
(l.tags ?? []).some((tag) => tag.toLowerCase().includes(t)));
|
|
121
|
+
}
|
|
122
|
+
if (author) {
|
|
123
|
+
const a = author.toLowerCase();
|
|
124
|
+
lessons = lessons.filter(l => l.author?.toLowerCase().includes(a));
|
|
125
|
+
}
|
|
126
|
+
// Sort by recall_count desc
|
|
127
|
+
lessons.sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0));
|
|
128
|
+
lessons = lessons.slice(0, limit);
|
|
129
|
+
if (lessons.length === 0) {
|
|
130
|
+
return topic
|
|
131
|
+
? `📭 No team lessons found for \`${topic}\`.\n\nShared instance: add lessons with \`learn_from_attempts\` and include an \`author\` field.`
|
|
132
|
+
: `📭 No lessons in this brain yet.\n\nAll team members sharing this instance will see lessons here.`;
|
|
133
|
+
}
|
|
134
|
+
const lines = [`👥 **Team Brain** — ${lessons.length} lesson${lessons.length > 1 ? 's' : ''}`, ''];
|
|
135
|
+
for (const l of lessons) {
|
|
136
|
+
const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
|
|
137
|
+
const sev = l.severity === 'critical' ? '🔴 ' : l.severity === 'major' ? '🟡 ' : '';
|
|
138
|
+
const authorStr = l.author ? ` · _by ${l.author}_` : '';
|
|
139
|
+
const recallStr = (l.recall_count ?? 0) > 0 ? ` · recalled ${l.recall_count}×` : '';
|
|
140
|
+
const ago = Math.round((Date.now() - new Date(l.ts).getTime()) / 86400000);
|
|
141
|
+
const agoStr = ago === 0 ? 'today' : ago === 1 ? 'yesterday' : `${ago}d ago`;
|
|
142
|
+
lines.push(`${emoji} ${sev}**\`${l.topic}\`**${authorStr}${recallStr} · ${agoStr}`);
|
|
143
|
+
lines.push(` ${l.what_worked.slice(0, 120)}`);
|
|
144
|
+
lines.push('');
|
|
145
|
+
}
|
|
146
|
+
return lines.join('\n');
|
|
147
|
+
}
|
|
148
|
+
// ── team_synthesize — Team Brain Synthesis ────────────────────────────────
|
|
149
|
+
case 'team_synthesize': {
|
|
150
|
+
const { instance_id, topic } = args;
|
|
151
|
+
const redis = await getConnection(instance_id);
|
|
152
|
+
// Load history list for this topic (all authors' contributions)
|
|
153
|
+
const listKey = `cachly:lessons:${topic}`;
|
|
154
|
+
const all = await redis.lrange(listKey, 0, -1);
|
|
155
|
+
if (all.length < 2) {
|
|
156
|
+
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}", ...)\`.`;
|
|
157
|
+
}
|
|
158
|
+
const entries = all.map(r => { try {
|
|
159
|
+
return JSON.parse(r);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
} }).filter((e) => e !== null);
|
|
164
|
+
// Group by outcome
|
|
165
|
+
const successes = entries.filter(e => e.outcome === 'success');
|
|
166
|
+
const failures = entries.filter(e => e.outcome === 'failure');
|
|
167
|
+
const partials = entries.filter(e => e.outcome === 'partial');
|
|
168
|
+
const authors = [...new Set(entries.map(e => e.author).filter(Boolean))];
|
|
169
|
+
const hasMultiAuthor = authors.length > 1;
|
|
170
|
+
// Build canonical merged version
|
|
171
|
+
// what_worked: pick the most recent success, or longest for most detail
|
|
172
|
+
const bestSuccess = successes.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime())[0];
|
|
173
|
+
const whatWorkedCandidates = successes.map(e => e.what_worked).filter(w => w && w.length > 10);
|
|
174
|
+
const canonicalWorked = whatWorkedCandidates.sort((a, b) => b.length - a.length)[0] ?? bestSuccess?.what_worked ?? '';
|
|
175
|
+
// what_failed: union of all unique failure reasons
|
|
176
|
+
const allFailed = [...new Set([...failures, ...partials].map(e => e.what_failed).filter((w) => !!w && w.length > 5))];
|
|
177
|
+
const severities = entries.map(e => e.severity).filter(Boolean);
|
|
178
|
+
const canonicalSeverity = severities.includes('critical') ? 'critical' : severities.includes('major') ? 'major' : 'minor';
|
|
179
|
+
const lines = [
|
|
180
|
+
`🧬 **Team Brain Synthesis: \`${topic}\`**`,
|
|
181
|
+
`_${entries.length} entries from ${authors.length} author${authors.length === 1 ? '' : 's'}${hasMultiAuthor ? ` (${authors.join(', ')})` : ''} · ${successes.length} success · ${failures.length} failure · ${partials.length} partial_`,
|
|
182
|
+
'',
|
|
183
|
+
`**Canonical "what worked":**`,
|
|
184
|
+
`> ${canonicalWorked}`,
|
|
185
|
+
'',
|
|
186
|
+
allFailed.length > 0 ? `**Avoid (combined failures):**` : '',
|
|
187
|
+
...allFailed.map(f => `> ❌ ${f}`),
|
|
188
|
+
allFailed.length > 0 ? '' : '',
|
|
189
|
+
`**Suggested canonical lesson:**`,
|
|
190
|
+
'```',
|
|
191
|
+
`learn_from_attempts(`,
|
|
192
|
+
` topic = "${topic}",`,
|
|
193
|
+
` outcome = "success",`,
|
|
194
|
+
` what_worked = "${canonicalWorked.replace(/"/g, "'")}",`,
|
|
195
|
+
allFailed.length > 0 ? ` what_failed = "${allFailed[0].replace(/"/g, "'")}",` : '',
|
|
196
|
+
` severity = "${canonicalSeverity}",`,
|
|
197
|
+
`)`,
|
|
198
|
+
'```',
|
|
199
|
+
'',
|
|
200
|
+
hasMultiAuthor
|
|
201
|
+
? `💡 _${authors.length} team members contributed to this synthesis. Store the canonical version to replace individual entries._`
|
|
202
|
+
: `💡 _Single author — more value when multiple team members contribute to the same topic._`,
|
|
203
|
+
].filter(l => l !== undefined).join('\n');
|
|
204
|
+
return lines;
|
|
205
|
+
}
|
|
206
|
+
// ── brain_doctor ──────────────────────────────────────────────────────────
|
|
207
|
+
// ── memory_crystalize ─────────────────────────────────────────────────────
|
|
208
|
+
case 'memory_crystalize': {
|
|
209
|
+
const { instance_id, label: crystalLabel = '' } = args;
|
|
210
|
+
const redis = await getConnection(instance_id);
|
|
211
|
+
const now = new Date();
|
|
212
|
+
const week = `${now.getFullYear()}-W${String(Math.ceil((now.getDate() - now.getDay() + 10) / 7)).padStart(2, '0')}`;
|
|
213
|
+
const effectiveLabel = crystalLabel || `${now.toISOString().slice(0, 7)} Crystal`;
|
|
214
|
+
// Read session history
|
|
215
|
+
const sessionHistory = await redis.lrange('cachly:session:history', 0, 49);
|
|
216
|
+
// Read all auto-learned lessons
|
|
217
|
+
const allLessonKeys = [];
|
|
218
|
+
const ls = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
219
|
+
await new Promise((res, rej) => {
|
|
220
|
+
ls.on('data', (b) => allLessonKeys.push(...b));
|
|
221
|
+
ls.on('end', res);
|
|
222
|
+
ls.on('error', rej);
|
|
223
|
+
});
|
|
224
|
+
const allLessons = [];
|
|
225
|
+
for (const k of allLessonKeys) {
|
|
226
|
+
const raw = await redis.get(k);
|
|
227
|
+
if (!raw)
|
|
228
|
+
continue;
|
|
229
|
+
try {
|
|
230
|
+
allLessons.push(JSON.parse(raw));
|
|
231
|
+
}
|
|
232
|
+
catch { /* skip */ }
|
|
233
|
+
}
|
|
234
|
+
// Group lessons by top-level category
|
|
235
|
+
const categoryMap = new Map();
|
|
236
|
+
for (const l of allLessons) {
|
|
237
|
+
const cat = l.topic.split(':')[0] || 'misc';
|
|
238
|
+
if (!categoryMap.has(cat))
|
|
239
|
+
categoryMap.set(cat, []);
|
|
240
|
+
categoryMap.get(cat).push(l);
|
|
241
|
+
}
|
|
242
|
+
// Build top patterns (most frequent categories with a representative insight)
|
|
243
|
+
const topPatterns = [];
|
|
244
|
+
for (const [cat, catLessons] of [...categoryMap.entries()].sort((a, b) => b[1].length - a[1].length).slice(0, 8)) {
|
|
245
|
+
const successLessons = catLessons.filter(l => l.outcome === 'success');
|
|
246
|
+
const best = successLessons[0] ?? catLessons[0];
|
|
247
|
+
if (!best)
|
|
248
|
+
continue;
|
|
249
|
+
topPatterns.push({
|
|
250
|
+
category: cat,
|
|
251
|
+
insight: best.what_worked.slice(0, 120),
|
|
252
|
+
count: catLessons.length,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const crystal = {
|
|
256
|
+
label: effectiveLabel,
|
|
257
|
+
ts: now.toISOString(),
|
|
258
|
+
session_count: sessionHistory.length,
|
|
259
|
+
lesson_count: allLessons.length,
|
|
260
|
+
top_patterns: topPatterns,
|
|
261
|
+
categories: [...categoryMap.keys()],
|
|
262
|
+
created_from: `${sessionHistory.length} sessions, ${allLessons.length} lessons`,
|
|
263
|
+
};
|
|
264
|
+
const crystalJson = JSON.stringify(crystal);
|
|
265
|
+
await redis.set('cachly:crystal:latest', crystalJson);
|
|
266
|
+
await redis.expire('cachly:crystal:latest', 90 * 86400);
|
|
267
|
+
await redis.set(`cachly:crystal:${week}`, crystalJson);
|
|
268
|
+
await redis.expire(`cachly:crystal:${week}`, 365 * 86400);
|
|
269
|
+
const lines = [
|
|
270
|
+
`💎 **Memory Crystal created: ${effectiveLabel}**`,
|
|
271
|
+
``,
|
|
272
|
+
`📊 Compressed: **${sessionHistory.length} sessions** + **${allLessons.length} lessons** → ${topPatterns.length} top patterns`,
|
|
273
|
+
``,
|
|
274
|
+
`**Top patterns by category:**`,
|
|
275
|
+
...topPatterns.slice(0, 6).map(p => ` • **${p.category}** (${p.count}×): ${p.insight.slice(0, 90)}`),
|
|
276
|
+
``,
|
|
277
|
+
`💡 This crystal will appear in every future \`session_start\` briefing.`,
|
|
278
|
+
`💡 Re-run \`memory_crystalize\` monthly to keep it fresh.`,
|
|
279
|
+
];
|
|
280
|
+
return lines.join('\n');
|
|
281
|
+
}
|
|
282
|
+
case 'brain_doctor': {
|
|
283
|
+
const { instance_id, workspace_path: drWorkspacePath = '' } = args;
|
|
284
|
+
const redis = await getConnection(instance_id);
|
|
285
|
+
const issues = [];
|
|
286
|
+
const checks = [];
|
|
287
|
+
// Count lessons
|
|
288
|
+
const lessonKeys = [];
|
|
289
|
+
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
290
|
+
await new Promise((resolve, reject) => {
|
|
291
|
+
lStream.on('data', (batch) => lessonKeys.push(...batch));
|
|
292
|
+
lStream.on('end', resolve);
|
|
293
|
+
lStream.on('error', reject);
|
|
294
|
+
});
|
|
295
|
+
// Count context
|
|
296
|
+
let ctxCount = 0;
|
|
297
|
+
const ctxStream = redis.scanStream({ match: 'cachly:ctx:*', count: 200 });
|
|
298
|
+
await new Promise((resolve, reject) => {
|
|
299
|
+
ctxStream.on('data', (batch) => {
|
|
300
|
+
ctxCount += batch.filter((k) => !k.endsWith(':meta')).length;
|
|
301
|
+
});
|
|
302
|
+
ctxStream.on('end', resolve);
|
|
303
|
+
ctxStream.on('error', reject);
|
|
304
|
+
});
|
|
305
|
+
const lessons = [];
|
|
306
|
+
for (const k of lessonKeys) {
|
|
307
|
+
const raw = await redis.get(k);
|
|
308
|
+
if (!raw)
|
|
309
|
+
continue;
|
|
310
|
+
try {
|
|
311
|
+
lessons.push(JSON.parse(raw));
|
|
312
|
+
}
|
|
313
|
+
catch { /* skip */ }
|
|
314
|
+
}
|
|
315
|
+
// Last session
|
|
316
|
+
const lastSessionRaw = await redis.get('cachly:session:last');
|
|
317
|
+
let lastSession = null;
|
|
318
|
+
if (lastSessionRaw) {
|
|
319
|
+
try {
|
|
320
|
+
lastSession = JSON.parse(lastSessionRaw);
|
|
321
|
+
}
|
|
322
|
+
catch { /* ignore */ }
|
|
323
|
+
}
|
|
324
|
+
// Open failures
|
|
325
|
+
const openFailures = lessons.filter(l => l.outcome === 'failure' || l.outcome === 'partial');
|
|
326
|
+
// Unused lessons (never recalled)
|
|
327
|
+
const unusedLessons = lessons.filter(l => (l.recall_count ?? 0) === 0);
|
|
328
|
+
// Critical lessons
|
|
329
|
+
const criticalLessons = lessons.filter(l => l.severity === 'critical');
|
|
330
|
+
// Confidence decay analysis
|
|
331
|
+
const staleLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) < CONFIDENCE_STALE_VALUE);
|
|
332
|
+
const warnLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) >= CONFIDENCE_STALE_VALUE && calculateConfidence(l) < CONFIDENCE_WARN_VALUE);
|
|
333
|
+
const withAudit = lessons.filter(l => (l.audit_trail ?? []).length > 1);
|
|
334
|
+
const teamLessons = lessons.filter(l => l.author);
|
|
335
|
+
const uniqueAuthors = new Set(lessons.map(l => l.author).filter(Boolean));
|
|
336
|
+
// Effective IQ boost: total recalls / lessons (how much the brain actually helped)
|
|
337
|
+
const totalRecalls = lessons.reduce((sum, l) => sum + (l.recall_count ?? 0), 0);
|
|
338
|
+
const iqBoostPct = lessons.length > 0 ? Math.min(100, Math.round((totalRecalls / lessons.length) * 10)) : 0;
|
|
339
|
+
// Quality score (0-100)
|
|
340
|
+
let score = 50;
|
|
341
|
+
if (lessonKeys.length >= 5)
|
|
342
|
+
score += 10;
|
|
343
|
+
if (lessonKeys.length >= 20)
|
|
344
|
+
score += 10;
|
|
345
|
+
if (ctxCount >= 3)
|
|
346
|
+
score += 10;
|
|
347
|
+
if (ctxCount >= 10)
|
|
348
|
+
score += 5;
|
|
349
|
+
if (lastSession)
|
|
350
|
+
score += 10;
|
|
351
|
+
if (openFailures.length === 0)
|
|
352
|
+
score += 5;
|
|
353
|
+
const unusedRatio = lessons.length > 0 ? unusedLessons.length / lessons.length : 0;
|
|
354
|
+
if (unusedRatio < 0.5)
|
|
355
|
+
score += 10;
|
|
356
|
+
if (staleLessons.length === 0)
|
|
357
|
+
score += 5;
|
|
358
|
+
if (uniqueAuthors.size >= 2)
|
|
359
|
+
score += 5; // team collaboration bonus
|
|
360
|
+
const scoreEmoji = score >= 80 ? '🟢' : score >= 50 ? '🟡' : '🔴';
|
|
361
|
+
const iqEmoji = iqBoostPct >= 50 ? '🚀' : iqBoostPct >= 20 ? '📈' : '💤';
|
|
362
|
+
checks.push(`${scoreEmoji} **Brain Quality Score: ${score}/100**`);
|
|
363
|
+
checks.push(`${iqEmoji} **Effective IQ Boost: ${iqBoostPct}%** (${totalRecalls} recalls across ${lessons.length} lessons)`);
|
|
364
|
+
checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical · ${withAudit.length} with audit trail · ${teamLessons.length} from team)`);
|
|
365
|
+
checks.push(`💾 **Context entries:** ${ctxCount}`);
|
|
366
|
+
checks.push(`🎯 **Confidence:** ${lessons.length - staleLessons.length - warnLessons.length} fresh · ${warnLessons.length} warn · ${staleLessons.length} stale`);
|
|
367
|
+
checks.push(`⏱️ **Decay config:** warn after ${CONFIDENCE_WARN_DAYS}d · stale after ${CONFIDENCE_STALE_DAYS}d`);
|
|
368
|
+
if (uniqueAuthors.size >= 2) {
|
|
369
|
+
checks.push(`👥 **Team:** ${uniqueAuthors.size} contributors (${[...uniqueAuthors].join(', ')})`);
|
|
370
|
+
}
|
|
371
|
+
// Stale index detection
|
|
372
|
+
try {
|
|
373
|
+
const lastIndexRaw = await redis.get('cachly:index:last_run');
|
|
374
|
+
if (lastIndexRaw) {
|
|
375
|
+
const lastIndexAge = Math.round((Date.now() - new Date(lastIndexRaw).getTime()) / 86_400_000);
|
|
376
|
+
if (lastIndexAge > 7) {
|
|
377
|
+
issues.push(`🔄 Index is ${lastIndexAge}d stale — run \`index_project\` to re-sync semantic search`);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
checks.push(`🗂️ **Semantic index:** ${lastIndexAge}d old (fresh)`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
issues.push(`💡 No semantic index — run \`index_project(dir="<your-src>")\` to enable semantic search`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch { /* non-critical */ }
|
|
388
|
+
// Memory crystal status
|
|
389
|
+
try {
|
|
390
|
+
const crystalRaw = await redis.get('cachly:crystal:latest');
|
|
391
|
+
if (crystalRaw) {
|
|
392
|
+
const crystal = JSON.parse(crystalRaw);
|
|
393
|
+
const crystalAge = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
|
|
394
|
+
checks.push(`💎 **Memory Crystal:** ${crystal.label} (${crystalAge}d ago)`);
|
|
395
|
+
if (crystalAge > 30)
|
|
396
|
+
issues.push(`💡 Memory Crystal is ${crystalAge}d old — re-run \`memory_crystalize\` to compress new sessions`);
|
|
397
|
+
}
|
|
398
|
+
else if (lessonKeys.length >= 10) {
|
|
399
|
+
issues.push(`💡 ${lessonKeys.length} lessons but no Memory Crystal — run \`memory_crystalize\` to compress wisdom`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
catch { /* non-critical */ }
|
|
403
|
+
// openclaw cross-promo (check package.json in workspace)
|
|
404
|
+
if (drWorkspacePath) {
|
|
405
|
+
try {
|
|
406
|
+
const pkgPath = drWorkspacePath.replace(/\/$/, '') + '/package.json';
|
|
407
|
+
const pkgRaw = readFileSync(pkgPath, 'utf-8');
|
|
408
|
+
const pkg = JSON.parse(pkgRaw);
|
|
409
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
410
|
+
const hasLLMDep = ['openai', '@anthropic-ai/sdk', '@google/generative-ai', 'mistralai', 'cohere-ai'].some(d => d in allDeps);
|
|
411
|
+
const hasOpenclaw = '@cachly-dev/openclaw' in allDeps;
|
|
412
|
+
if (hasLLMDep && !hasOpenclaw) {
|
|
413
|
+
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\``);
|
|
414
|
+
issues.push(` → \`npm install @cachly-dev/openclaw\` cuts LLM costs 60–90% with 3 lines of code`);
|
|
415
|
+
}
|
|
416
|
+
else if (hasOpenclaw) {
|
|
417
|
+
checks.push(`✅ **@cachly-dev/openclaw installed** (LLM cost caching active)`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch { /* no package.json or unreadable */ }
|
|
421
|
+
}
|
|
422
|
+
if (lastSession) {
|
|
423
|
+
const ageMin = Math.round((Date.now() - new Date(lastSession.ts).getTime()) / 60000);
|
|
424
|
+
const ageStr = ageMin < 60 ? `${ageMin}m` : ageMin < 1440 ? `${Math.round(ageMin / 60)}h` : `${Math.round(ageMin / 1440)}d`;
|
|
425
|
+
checks.push(`🕐 **Last session:** ${ageStr} ago`);
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
issues.push('❌ No session history — call `session_start` + `session_end` to start tracking');
|
|
429
|
+
}
|
|
430
|
+
if (lessonKeys.length === 0) {
|
|
431
|
+
issues.push('❌ No lessons — call `learn_from_attempts` after solving bugs');
|
|
432
|
+
}
|
|
433
|
+
else if (lessonKeys.length < 5) {
|
|
434
|
+
issues.push(`💡 Only ${lessonKeys.length} lessons — add more after each problem solved`);
|
|
435
|
+
}
|
|
436
|
+
if (iqBoostPct === 0 && lessons.length >= 5) {
|
|
437
|
+
issues.push(`💤 **IQ Boost is 0%** — lessons exist but are never recalled. Use \`recall_best_solution\` BEFORE tasks.`);
|
|
438
|
+
}
|
|
439
|
+
if (ctxCount === 0) {
|
|
440
|
+
issues.push('💡 No context — use `remember_context` to cache architecture docs, ADRs, etc.');
|
|
441
|
+
}
|
|
442
|
+
if (openFailures.length > 0) {
|
|
443
|
+
issues.push(`⚠️ ${openFailures.length} unresolved failure${openFailures.length > 1 ? 's' : ''}: ${openFailures.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
|
|
444
|
+
}
|
|
445
|
+
if (staleLessons.length > 0) {
|
|
446
|
+
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(', ')}`);
|
|
447
|
+
issues.push(` → Re-verify with \`recall_best_solution\` to reset confidence clock`);
|
|
448
|
+
}
|
|
449
|
+
if (warnLessons.length > 0) {
|
|
450
|
+
issues.push(`⚠️ ${warnLessons.length} lesson${warnLessons.length > 1 ? 's' : ''} aging (>${CONFIDENCE_WARN_DAYS}d): ${warnLessons.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
|
|
451
|
+
}
|
|
452
|
+
if (unusedRatio > 0.7 && lessons.length > 5) {
|
|
453
|
+
issues.push(`💡 ${unusedLessons.length} lessons never recalled — verify topics match your workflow`);
|
|
454
|
+
}
|
|
455
|
+
const lines = ['🩺 **Brain Doctor Report**', '', ...checks.map(c => ' ' + c), ''];
|
|
456
|
+
if (issues.length > 0) {
|
|
457
|
+
lines.push('**Issues to fix:**');
|
|
458
|
+
for (const i of issues)
|
|
459
|
+
lines.push(' ' + i);
|
|
460
|
+
lines.push('');
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
lines.push(' 🎉 Brain looks healthy! Keep calling session_start/session_end.');
|
|
464
|
+
}
|
|
465
|
+
return lines.join('\n');
|
|
466
|
+
}
|
|
467
|
+
// ── recall_at — Brain Archaeology ────────────────────────────────────────
|
|
468
|
+
case 'recall_at': {
|
|
469
|
+
const { instance_id, topic, date } = args;
|
|
470
|
+
const redis = await getConnection(instance_id);
|
|
471
|
+
const cutoff = new Date(date).getTime();
|
|
472
|
+
if (isNaN(cutoff))
|
|
473
|
+
return `❌ Invalid date "${date}". Use ISO format: "2026-01-15"`;
|
|
474
|
+
const listKey = `cachly:lessons:${topic}`;
|
|
475
|
+
const all = await redis.lrange(listKey, 0, -1);
|
|
476
|
+
if (all.length === 0)
|
|
477
|
+
return `📭 No history found for \`${topic}\`. Lessons are stored via \`learn_from_attempts\`.`;
|
|
478
|
+
const before = all
|
|
479
|
+
.map(raw => { try {
|
|
480
|
+
return JSON.parse(raw);
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
return null;
|
|
484
|
+
} })
|
|
485
|
+
.filter((l) => l !== null && new Date(l.ts).getTime() <= cutoff)
|
|
486
|
+
.sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
|
|
487
|
+
if (before.length === 0)
|
|
488
|
+
return `📭 No entries for \`${topic}\` found before **${date}**. Earliest entry: ${new Date(JSON.parse(all[0]).ts).toLocaleDateString('de-DE')}.`;
|
|
489
|
+
const lines = [
|
|
490
|
+
`🏺 **Brain Archaeology: \`${topic}\` before ${date}**`,
|
|
491
|
+
`_${before.length} of ${all.length} total entries shown_`,
|
|
492
|
+
'',
|
|
493
|
+
];
|
|
494
|
+
for (const l of before.slice(-10)) {
|
|
495
|
+
const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
|
|
496
|
+
const d = new Date(l.ts).toLocaleDateString('de-DE');
|
|
497
|
+
lines.push(`**${d}** ${emoji} ${l.outcome}`);
|
|
498
|
+
if (l.what_worked)
|
|
499
|
+
lines.push(` → ${l.what_worked.slice(0, 100)}`);
|
|
500
|
+
lines.push('');
|
|
501
|
+
}
|
|
502
|
+
lines.push(`_Full evolution: ${all.map(r => { try {
|
|
503
|
+
const l = JSON.parse(r);
|
|
504
|
+
return l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
return '?';
|
|
508
|
+
} }).join(' → ')}_`);
|
|
509
|
+
return lines.join('\n');
|
|
510
|
+
}
|
|
511
|
+
// ── trace_dependency — Causal Chain ──────────────────────────────────────
|
|
512
|
+
case 'trace_dependency': {
|
|
513
|
+
const { instance_id, dependency, mark_review = false } = args;
|
|
514
|
+
const redis = await getConnection(instance_id);
|
|
515
|
+
const depKey = `cachly:dep:${dependency}`;
|
|
516
|
+
const raw = await redis.get(depKey);
|
|
517
|
+
if (!raw)
|
|
518
|
+
return `📭 No lessons found that depend on \`${dependency}\`.\n\nAdd dependencies via: \`learn_from_attempts(..., depends_on=["${dependency}"])\``;
|
|
519
|
+
const topics = JSON.parse(raw);
|
|
520
|
+
const lines = [
|
|
521
|
+
`🔗 **Causal Chain: \`${dependency}\`** — ${topics.length} dependent lesson${topics.length === 1 ? '' : 's'}`,
|
|
522
|
+
'',
|
|
523
|
+
];
|
|
524
|
+
for (const t of topics) {
|
|
525
|
+
const lessonRaw = await redis.get(`cachly:lesson:best:${t}`);
|
|
526
|
+
if (!lessonRaw) {
|
|
527
|
+
lines.push(` • \`${t}\` _(lesson deleted)_`);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const lesson = JSON.parse(lessonRaw);
|
|
531
|
+
const emoji = lesson.outcome === 'success' ? '✅' : lesson.outcome === 'partial' ? '⚠️' : '❌';
|
|
532
|
+
const reviewBadge = lesson.needs_review ? ' 🔍 **needs_review**' : '';
|
|
533
|
+
lines.push(` ${emoji} \`${t}\` (${lesson.severity ?? 'major'})${reviewBadge}`);
|
|
534
|
+
if (mark_review) {
|
|
535
|
+
const updated = { ...lesson, needs_review: true };
|
|
536
|
+
await redis.set(`cachly:lesson:best:${t}`, JSON.stringify(updated));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (mark_review) {
|
|
540
|
+
lines.push('', `🔍 All ${topics.length} lessons marked as **needs_review** — verify they still work with the changed dependency.`);
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
lines.push('', `_Run with \`mark_review: true\` to flag all dependent lessons for re-verification._`);
|
|
544
|
+
}
|
|
545
|
+
return lines.join('\n');
|
|
546
|
+
}
|
|
547
|
+
// ── global_learn ──────────────────────────────────────────────────────────
|
|
548
|
+
case 'global_learn': {
|
|
549
|
+
const { instance_id, topic, lesson, severity = 'minor', tags = [] } = args;
|
|
550
|
+
const redis = await getConnection(instance_id);
|
|
551
|
+
const key = `cachly:global:lesson:${topic}`;
|
|
552
|
+
const record = {
|
|
553
|
+
topic,
|
|
554
|
+
lesson,
|
|
555
|
+
severity,
|
|
556
|
+
tags,
|
|
557
|
+
ts: new Date().toISOString(),
|
|
558
|
+
scope: 'global',
|
|
559
|
+
recall_count: 0,
|
|
560
|
+
};
|
|
561
|
+
// Preserve recall_count on update
|
|
562
|
+
const existing = await redis.get(key);
|
|
563
|
+
if (existing) {
|
|
564
|
+
const prev = JSON.parse(existing);
|
|
565
|
+
record.recall_count = prev.recall_count ?? 0;
|
|
566
|
+
}
|
|
567
|
+
await redis.set(key, JSON.stringify(record));
|
|
568
|
+
return `🌐 **Global lesson stored**: \`${topic}\`\n\n${lesson}\n\nRecallable from any project via \`global_recall(topic="${topic}")\`.`;
|
|
569
|
+
}
|
|
570
|
+
// ── global_recall ─────────────────────────────────────────────────────────
|
|
571
|
+
case 'global_recall': {
|
|
572
|
+
const { instance_id, topic } = args;
|
|
573
|
+
const redis = await getConnection(instance_id);
|
|
574
|
+
const keys = [];
|
|
575
|
+
const gStream = redis.scanStream({ match: 'cachly:global:lesson:*', count: 200 });
|
|
576
|
+
await new Promise((resolve, reject) => {
|
|
577
|
+
gStream.on('data', (batch) => keys.push(...batch));
|
|
578
|
+
gStream.on('end', resolve);
|
|
579
|
+
gStream.on('error', reject);
|
|
580
|
+
});
|
|
581
|
+
let lessons = [];
|
|
582
|
+
for (const k of keys) {
|
|
583
|
+
const raw = await redis.get(k);
|
|
584
|
+
if (!raw)
|
|
585
|
+
continue;
|
|
586
|
+
try {
|
|
587
|
+
lessons.push(JSON.parse(raw));
|
|
588
|
+
}
|
|
589
|
+
catch { /* skip */ }
|
|
590
|
+
}
|
|
591
|
+
if (topic) {
|
|
592
|
+
const t = topic.toLowerCase();
|
|
593
|
+
lessons = lessons.filter(l => l.topic.toLowerCase().includes(t));
|
|
594
|
+
}
|
|
595
|
+
if (lessons.length === 0) {
|
|
596
|
+
return `📭 No global lessons${topic ? ` for \`${topic}\`` : ''}.\n\nAdd cross-project knowledge with \`global_learn(topic="...", lesson="...")\`.`;
|
|
597
|
+
}
|
|
598
|
+
// Increment recall_count
|
|
599
|
+
for (const l of lessons) {
|
|
600
|
+
const k = `cachly:global:lesson:${l.topic}`;
|
|
601
|
+
const raw = await redis.get(k);
|
|
602
|
+
if (raw) {
|
|
603
|
+
const rec = JSON.parse(raw);
|
|
604
|
+
rec.recall_count = (rec.recall_count ?? 0) + 1;
|
|
605
|
+
await redis.set(k, JSON.stringify(rec));
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const lines = [`🌐 **Global Brain** — ${lessons.length} lesson${lessons.length > 1 ? 's' : ''}`, ''];
|
|
609
|
+
for (const l of lessons) {
|
|
610
|
+
const sev = l.severity === 'critical' ? '🔴 ' : l.severity === 'major' ? '🟡 ' : '';
|
|
611
|
+
lines.push(`${sev}**\`${l.topic}\`**`);
|
|
612
|
+
lines.push(l.lesson.slice(0, 200));
|
|
613
|
+
lines.push('');
|
|
614
|
+
}
|
|
615
|
+
return lines.join('\n');
|
|
616
|
+
}
|
|
617
|
+
// ── publish_lesson ────────────────────────────────────────────────────────
|
|
618
|
+
case 'publish_lesson': {
|
|
619
|
+
const { instance_id, topic, lesson, framework = 'general', severity = 'minor' } = args;
|
|
620
|
+
const redis = await getConnection(instance_id);
|
|
621
|
+
// Strip potential PII patterns (emails, tokens, paths)
|
|
622
|
+
const sanitized = lesson
|
|
623
|
+
.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '[email]')
|
|
624
|
+
.replace(/\b(sk-|cky_live_|Bearer\s)[A-Za-z0-9_\-]{8,}/g, '[token]')
|
|
625
|
+
.replace(/\/Users\/[^\s/]+/g, '/Users/[user]')
|
|
626
|
+
.replace(/\/home\/[^\s/]+/g, '/home/[user]');
|
|
627
|
+
const publicLesson = {
|
|
628
|
+
topic,
|
|
629
|
+
lesson: sanitized,
|
|
630
|
+
framework,
|
|
631
|
+
severity,
|
|
632
|
+
ts: new Date().toISOString(),
|
|
633
|
+
published: true,
|
|
634
|
+
votes: 0,
|
|
635
|
+
};
|
|
636
|
+
// Store locally with public flag (future: sync to Cachly public API)
|
|
637
|
+
const key = `cachly:public:lesson:${framework}:${topic}`;
|
|
638
|
+
await redis.set(key, JSON.stringify(publicLesson), 'EX', 365 * 86400);
|
|
639
|
+
return [
|
|
640
|
+
`📢 **Lesson published!**`,
|
|
641
|
+
``,
|
|
642
|
+
`**Topic:** \`${topic}\``,
|
|
643
|
+
`**Framework:** ${framework}`,
|
|
644
|
+
`**Content:** ${sanitized.slice(0, 200)}${sanitized.length > 200 ? '…' : ''}`,
|
|
645
|
+
``,
|
|
646
|
+
`This lesson is now available in the Public Brain for other developers.`,
|
|
647
|
+
`Import it anywhere: \`import_public_brain(framework="${framework}")\``,
|
|
648
|
+
].join('\n');
|
|
649
|
+
}
|
|
650
|
+
// ── import_public_brain ───────────────────────────────────────────────────
|
|
651
|
+
case 'import_public_brain': {
|
|
652
|
+
const { instance_id, framework, limit = 20 } = args;
|
|
653
|
+
const redis = await getConnection(instance_id);
|
|
654
|
+
// Community-curated lessons per framework
|
|
655
|
+
const COMMUNITY_LESSONS = {
|
|
656
|
+
nextjs: [
|
|
657
|
+
{ topic: 'nextjs:image-layout', lesson: 'Use fill + relative parent instead of layout="fill" (deprecated since Next.js 13)', severity: 'major' },
|
|
658
|
+
{ topic: 'nextjs:app-router-fetch', lesson: 'fetch() in Server Components is cached by default — add {cache:"no-store"} for dynamic data', severity: 'major' },
|
|
659
|
+
{ topic: 'nextjs:metadata-export', lesson: 'Export metadata const or generateMetadata() — never both in same file', severity: 'minor' },
|
|
660
|
+
{ topic: 'nextjs:client-boundary', lesson: '"use client" propagates down — keep it at the lowest component, not at page level', severity: 'major' },
|
|
661
|
+
{ topic: 'nextjs:env-prefix', lesson: 'Only NEXT_PUBLIC_* env vars are exposed to client — others are server-only', severity: 'critical' },
|
|
662
|
+
{ topic: 'nextjs:revalidate', lesson: 'export const revalidate = 0 disables caching for entire route; use revalidatePath() for on-demand', severity: 'minor' },
|
|
663
|
+
],
|
|
664
|
+
fastapi: [
|
|
665
|
+
{ topic: 'fastapi:async-db', lesson: 'Use async session with asyncpg — sync SQLAlchemy blocks the event loop', severity: 'critical' },
|
|
666
|
+
{ topic: 'fastapi:pydantic-v2', lesson: 'Pydantic v2: use model_validate() instead of parse_obj(), .dict() → .model_dump()', severity: 'major' },
|
|
667
|
+
{ topic: 'fastapi:lifespan', lesson: 'Use lifespan context manager instead of deprecated on_event startup/shutdown', severity: 'minor' },
|
|
668
|
+
{ topic: 'fastapi:background-tasks', lesson: 'BackgroundTasks run after response is sent — not in a separate thread pool', severity: 'major' },
|
|
669
|
+
{ topic: 'fastapi:cors-order', lesson: 'CORSMiddleware must be added before other middleware to work correctly', severity: 'critical' },
|
|
670
|
+
],
|
|
671
|
+
go: [
|
|
672
|
+
{ topic: 'go:context-cancel', lesson: 'Always call cancel() from context.WithCancel — leak goroutines if not cancelled', severity: 'critical' },
|
|
673
|
+
{ topic: 'go:defer-in-loop', lesson: 'defer in a loop runs at function return, not loop iteration — use IIFE or explicit close', severity: 'major' },
|
|
674
|
+
{ topic: 'go:nil-interface', lesson: 'nil interface != interface containing nil pointer — use explicit nil checks', severity: 'major' },
|
|
675
|
+
{ topic: 'go:goroutine-leak', lesson: 'Goroutines with channel sends block forever if receiver is gone — use select with done chan', severity: 'critical' },
|
|
676
|
+
{ topic: 'go:embed-path', lesson: '//go:embed path must be relative and known at compile time — no os.Getenv', severity: 'minor' },
|
|
677
|
+
],
|
|
678
|
+
docker: [
|
|
679
|
+
{ topic: 'docker:layer-cache', lesson: 'Copy package.json before source code — Docker caches layers, npm install only reruns on dep changes', severity: 'major' },
|
|
680
|
+
{ topic: 'docker:non-root', lesson: 'Run as non-root user (USER 1001) — some k8s clusters reject root containers by policy', severity: 'critical' },
|
|
681
|
+
{ topic: 'docker:build-arg-secret', lesson: 'Never use ARG for secrets — visible in image history. Use --secret mount instead', severity: 'critical' },
|
|
682
|
+
{ topic: 'docker:entrypoint-exec', lesson: 'Use exec form ["cmd","arg"] not shell form "cmd arg" — shell form ignores SIGTERM', severity: 'major' },
|
|
683
|
+
{ topic: 'docker:multi-stage', lesson: 'Multi-stage builds: copy only built artifacts to final stage — keep image small', severity: 'minor' },
|
|
684
|
+
],
|
|
685
|
+
kubernetes: [
|
|
686
|
+
{ topic: 'k8s:resource-limits', lesson: 'Always set resource limits — unbounded pods cause node evictions and OOMKill', severity: 'critical' },
|
|
687
|
+
{ topic: 'k8s:liveness-vs-readiness', lesson: 'Liveness failures restart pod; Readiness failures remove from LB. Use different endpoints', severity: 'major' },
|
|
688
|
+
{ topic: 'k8s:imagepullpolicy', lesson: 'imagePullPolicy: Always in production — IfNotPresent can serve stale images', severity: 'major' },
|
|
689
|
+
{ topic: 'k8s:configmap-env', lesson: 'ConfigMap changes don\'t restart pods — use rollout restart or mount as volume', severity: 'critical' },
|
|
690
|
+
{ topic: 'k8s:pdb', lesson: 'Set PodDisruptionBudget for stateful apps — node drains kill all pods without it', severity: 'major' },
|
|
691
|
+
],
|
|
692
|
+
react: [
|
|
693
|
+
{ topic: 'react:useeffect-deps', lesson: 'Omitting dependencies from useEffect deps array causes stale closure bugs — use exhaustive-deps ESLint rule', severity: 'critical' },
|
|
694
|
+
{ topic: 'react:key-index', lesson: 'Never use array index as key in lists — causes subtle re-render bugs on reorder/delete', severity: 'major' },
|
|
695
|
+
{ topic: 'react:setState-in-render', lesson: 'setState() during render causes infinite loop — move to useEffect or event handler', severity: 'critical' },
|
|
696
|
+
{ topic: 'react:memo-reference', lesson: 'Object/array literals in JSX recreate on every render — useMemo for expensive derived values', severity: 'minor' },
|
|
697
|
+
],
|
|
698
|
+
typescript: [
|
|
699
|
+
{ topic: 'ts:type-guard', lesson: 'Use "x is Type" return type for type guard functions — not "boolean"', severity: 'minor' },
|
|
700
|
+
{ topic: 'ts:strict-null', lesson: 'Enable strictNullChecks in tsconfig — catches 90% of runtime null errors at compile time', severity: 'critical' },
|
|
701
|
+
{ topic: 'ts:enum-avoid', lesson: 'Prefer union types ("a"|"b") over enum — enums have surprising runtime behavior', severity: 'minor' },
|
|
702
|
+
{ topic: 'ts:satisfies', lesson: 'Use "satisfies" operator to validate type without widening — more precise than explicit annotation', severity: 'minor' },
|
|
703
|
+
],
|
|
704
|
+
python: [
|
|
705
|
+
{ topic: 'python:mutable-default', lesson: 'Never use mutable default arguments (def f(x=[])) — shared across all calls. Use None + guard', severity: 'critical' },
|
|
706
|
+
{ topic: 'python:walrus-operator', lesson: ':= (walrus) assigns and returns — useful in while/comprehensions but hard to read in complex expr', severity: 'minor' },
|
|
707
|
+
{ topic: 'python:asyncio-run', lesson: 'asyncio.run() creates new event loop — calling it inside an existing loop raises RuntimeError', severity: 'major' },
|
|
708
|
+
{ topic: 'python:typing-optional', lesson: 'Optional[X] == Union[X, None] — in Python 3.10+ use X | None syntax', severity: 'minor' },
|
|
709
|
+
],
|
|
710
|
+
};
|
|
711
|
+
const fw = framework.toLowerCase();
|
|
712
|
+
const lessons = COMMUNITY_LESSONS[fw];
|
|
713
|
+
if (!lessons) {
|
|
714
|
+
const available = Object.keys(COMMUNITY_LESSONS).join(', ');
|
|
715
|
+
return `❌ No public brain available for \`${framework}\`.\n\nAvailable: ${available}\n\nOr publish your own: \`publish_lesson(framework="${framework}", ...)\``;
|
|
716
|
+
}
|
|
717
|
+
const toImport = lessons.slice(0, limit);
|
|
718
|
+
let importedCount = 0;
|
|
719
|
+
for (const l of toImport) {
|
|
720
|
+
const key = `cachly:lesson:best:${l.topic}`;
|
|
721
|
+
const existing = await redis.get(key);
|
|
722
|
+
if (!existing) {
|
|
723
|
+
await redis.set(key, JSON.stringify({
|
|
724
|
+
...l,
|
|
725
|
+
what_worked: l.lesson,
|
|
726
|
+
outcome: 'success',
|
|
727
|
+
ts: new Date().toISOString(),
|
|
728
|
+
recall_count: 0,
|
|
729
|
+
source: 'public_brain',
|
|
730
|
+
version: 2,
|
|
731
|
+
}));
|
|
732
|
+
importedCount++;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const lines = [
|
|
736
|
+
`📥 **Public Brain imported: ${framework}**`,
|
|
737
|
+
``,
|
|
738
|
+
`${importedCount} new lessons added (${toImport.length - importedCount} already existed)`,
|
|
739
|
+
``,
|
|
740
|
+
`**Imported topics:**`,
|
|
741
|
+
...toImport.map(l => {
|
|
742
|
+
const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '💡';
|
|
743
|
+
return ` ${sev} \`${l.topic}\``;
|
|
744
|
+
}),
|
|
745
|
+
``,
|
|
746
|
+
`These lessons will now appear in \`session_start\` when relevant.`,
|
|
747
|
+
`Recall any time: \`recall_best_solution(topic="${fw}:...")\``,
|
|
748
|
+
];
|
|
749
|
+
return lines.join('\n');
|
|
750
|
+
}
|
|
751
|
+
// ── setup_ai_memory ───────────────────────────────────────────────────────
|
|
752
|
+
case 'setup_ai_memory': {
|
|
753
|
+
const { instance_id, project_dir, embed_provider: providerArg = 'openai', project_description = 'a software project', } = args;
|
|
754
|
+
const inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
755
|
+
// Provider-specific env var instructions
|
|
756
|
+
const providerEnvMap = {
|
|
757
|
+
openai: { key: 'OPENAI_API_KEY', hint: 'Get at: https://platform.openai.com/api-keys' },
|
|
758
|
+
mistral: { key: 'MISTRAL_API_KEY', hint: 'Get at: https://console.mistral.ai/api-keys' },
|
|
759
|
+
cohere: { key: 'COHERE_API_KEY', hint: 'Get at: https://dashboard.cohere.com/api-keys' },
|
|
760
|
+
ollama: { key: 'OLLAMA_BASE_URL', hint: 'Run: brew install ollama && ollama serve (free, local, no key needed)' },
|
|
761
|
+
gemini: { key: 'GEMINI_API_KEY', hint: 'Get at: https://aistudio.google.com/app/apikey' },
|
|
762
|
+
};
|
|
763
|
+
const provInfo = providerEnvMap[providerArg] ?? providerEnvMap['openai'];
|
|
764
|
+
const hasVector = !!inst.vector_token;
|
|
765
|
+
// Generate .mcp.json snippet
|
|
766
|
+
const mcpJsonSnippet = JSON.stringify({
|
|
767
|
+
mcpServers: {
|
|
768
|
+
cachly: {
|
|
769
|
+
command: 'npx',
|
|
770
|
+
args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
771
|
+
env: {
|
|
772
|
+
CACHLY_JWT: 'your-api-token-from-cachly.dev/settings',
|
|
773
|
+
[provInfo.key]: providerArg === 'ollama' ? 'http://localhost:11434' : 'your-key-here',
|
|
774
|
+
...(providerArg !== 'openai' ? { CACHLY_EMBED_PROVIDER: providerArg } : {}),
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
}, null, 2);
|
|
779
|
+
// Generate copilot-instructions.md content
|
|
780
|
+
const tier = inst.tier.toUpperCase();
|
|
781
|
+
const smartRecallNote = hasVector
|
|
782
|
+
? '- `smart_recall("natural language query")` — semantic search (finds by meaning)'
|
|
783
|
+
: '- `recall_context("arch:*")` — exact/glob key lookup (upgrade to Speed/Business for smart_recall)';
|
|
784
|
+
const layerNote = hasVector
|
|
785
|
+
? `Layer 3 (Semantic): smart_recall uses pgvector HNSW on your ${tier} instance`
|
|
786
|
+
: `Layer 3 (Autopilot): this file — upgrade to Speed/Business tier to unlock smart_recall`;
|
|
787
|
+
const copilotInstructions = `# cachly AI Brain — ${project_description}
|
|
788
|
+
|
|
789
|
+
> AI memory system powered by [cachly.dev](https://cachly.dev). Works with GitHub Copilot, Claude Code, Cursor, Windsurf, Continue.dev, and any MCP-compatible editor.
|
|
790
|
+
|
|
791
|
+
---
|
|
792
|
+
|
|
793
|
+
## 🔴 MANDATORY RULES — Always enforced, no exceptions
|
|
794
|
+
|
|
795
|
+
### Rule 1 — Session Start (FIRST thing in every window)
|
|
796
|
+
Call \`session_start\` **before reading any source files or making any changes**:
|
|
797
|
+
|
|
798
|
+
\`\`\`
|
|
799
|
+
session_start(
|
|
800
|
+
instance_id = "${instance_id}",
|
|
801
|
+
focus = "<what you plan to work on today>",
|
|
802
|
+
)
|
|
803
|
+
\`\`\`
|
|
804
|
+
|
|
805
|
+
Returns: last session summary, handoff from previous window, recent lessons, open failures, brain health.
|
|
806
|
+
|
|
807
|
+
**If \`session_start\` is unavailable**, fall back:
|
|
808
|
+
1. \`smart_recall("recent lessons <topic>")\`
|
|
809
|
+
2. \`recall_best_solution("<topic>")\`
|
|
810
|
+
|
|
811
|
+
### Rule 2 — Learn After Every Fix/Deploy/Discovery (AUTOMATIC — no prompting needed)
|
|
812
|
+
After **every** bug fix, deployment, failed attempt, or discovery — call \`learn_from_attempts\` immediately:
|
|
813
|
+
|
|
814
|
+
\`\`\`
|
|
815
|
+
learn_from_attempts(
|
|
816
|
+
instance_id = "${instance_id}",
|
|
817
|
+
topic = "category:keyword", # deploy:web | fix:auth | debug:db | infra:docker
|
|
818
|
+
outcome = "success" | "failure" | "partial",
|
|
819
|
+
what_worked = "exact approach or command that worked",
|
|
820
|
+
what_failed = "what did NOT work (prevents future wasted time)",
|
|
821
|
+
severity = "critical" | "major" | "minor",
|
|
822
|
+
commands = ["exact command that worked"],
|
|
823
|
+
file_paths = ["path/to/changed/file"],
|
|
824
|
+
)
|
|
825
|
+
\`\`\`
|
|
826
|
+
|
|
827
|
+
**These events MUST automatically trigger \`learn_from_attempts\`:**
|
|
828
|
+
- ✅ Successfully deployed → \`topic="deploy:<component>", outcome="success"\`
|
|
829
|
+
- ✅ Fixed a bug → \`topic="fix:<description>", outcome="success"\`
|
|
830
|
+
- ✅ Discovered root cause → \`topic="debug:<area>", outcome="partial"\`
|
|
831
|
+
- ✅ Tried something that failed → \`topic="<area>", outcome="failure", what_failed="..."\`
|
|
832
|
+
- ✅ Found a config quirk → \`topic="infra:<topic>", outcome="success"\`
|
|
833
|
+
- ✅ Ran a working command → store it in \`commands=[...]\`
|
|
834
|
+
|
|
835
|
+
### Rule 3 — Session Handoff (when closing window or task list > 3 items)
|
|
836
|
+
\`\`\`
|
|
837
|
+
session_handoff(
|
|
838
|
+
instance_id = "${instance_id}",
|
|
839
|
+
completed_tasks = ["what was done"],
|
|
840
|
+
remaining_tasks = ["what is still to do"],
|
|
841
|
+
files_changed = [{"path": "src/...", "status": "modified", "description": "what changed"}],
|
|
842
|
+
instructions = "IMPORTANT: what the next window MUST know to not break things",
|
|
843
|
+
context_summary = "current state of the system",
|
|
844
|
+
)
|
|
845
|
+
\`\`\`
|
|
846
|
+
|
|
847
|
+
The next \`session_start\` automatically includes this handoff. The new window sees exactly what's done, what remains, and what NOT to break.
|
|
848
|
+
|
|
849
|
+
### Rule 4 — Session End (LAST thing before closing)
|
|
850
|
+
\`\`\`
|
|
851
|
+
session_end(
|
|
852
|
+
instance_id = "${instance_id}",
|
|
853
|
+
summary = "What was accomplished (2-3 sentences with specifics)",
|
|
854
|
+
files_changed = ["list", "of", "changed", "files"],
|
|
855
|
+
lessons_learned = <number of learn_from_attempts calls made>,
|
|
856
|
+
)
|
|
857
|
+
\`\`\`
|
|
858
|
+
|
|
859
|
+
> \`session_end\` **automatically extracts and stores lessons** from the summary text — no manual calls needed.
|
|
860
|
+
|
|
861
|
+
### Rule 5 — Code Index Invalidation (AUTOMATIC)
|
|
862
|
+
- \`index_project\` tracks MD5 hashes per file — **skips unchanged files automatically**
|
|
863
|
+
- After code changes: next \`index_project\` run re-indexes only modified files
|
|
864
|
+
- After delete/rename: run \`index_project\` again to sync
|
|
865
|
+
- No manual invalidation needed
|
|
866
|
+
|
|
867
|
+
---
|
|
868
|
+
|
|
869
|
+
## Task-type trigger table
|
|
870
|
+
|
|
871
|
+
| You're about to... | Call BEFORE | Call AFTER |
|
|
872
|
+
|---|---|---|
|
|
873
|
+
| Deploy anything | \`recall_best_solution("deploy:<component>")\` | \`learn_from_attempts(topic="deploy:...")\` |
|
|
874
|
+
| Fix a bug | \`recall_best_solution("fix:<area>")\` | \`learn_from_attempts(topic="fix:...")\` |
|
|
875
|
+
| Add a feature | \`session_start(focus="feat:<area>")\` | \`learn_from_attempts(topic="feat:...")\` |
|
|
876
|
+
| Infra/server work | \`recall_best_solution("infra:<topic>")\` | \`learn_from_attempts(topic="infra:...")\` |
|
|
877
|
+
| Debug an issue | \`smart_recall("<error message or symptom>")\` | \`learn_from_attempts(topic="debug:...")\` |
|
|
878
|
+
|
|
879
|
+
---
|
|
880
|
+
|
|
881
|
+
## Available Brain Tools
|
|
882
|
+
|
|
883
|
+
| Tool | When to use |
|
|
884
|
+
|------|-------------|
|
|
885
|
+
| \`session_start\` | **FIRST** — mandatory at start of every session |
|
|
886
|
+
| \`session_end\` | **LAST** — mandatory at end, auto-learns from summary |
|
|
887
|
+
| \`session_handoff\` | When closing window with remaining tasks |
|
|
888
|
+
| \`learn_from_attempts\` | **AUTOMATIC** after every fix/deploy/discovery |
|
|
889
|
+
| \`recall_best_solution\` | Before any non-trivial task |
|
|
890
|
+
| \`remember_context\` | After analyzing code — save findings for future sessions |
|
|
891
|
+
${smartRecallNote ? `| \`smart_recall\` | Search brain by meaning/keywords |\n` : ''}\
|
|
892
|
+
| \`recall_context\` | Get exact key (supports glob: \`arch:*\`, \`file:*\`) |
|
|
893
|
+
| \`brain_search\` | BM25+ full-text search over all brain data |
|
|
894
|
+
| \`auto_learn_session\` | Batch-learn from a list of observations (optional) |
|
|
895
|
+
| \`index_project\` | Index source files (smart hash, skips unchanged files) |
|
|
896
|
+
| \`list_remembered\` | See what's cached in the brain |
|
|
897
|
+
| \`forget_context\` | Remove stale context |
|
|
898
|
+
|
|
899
|
+
---
|
|
900
|
+
|
|
901
|
+
## Instance Details
|
|
902
|
+
|
|
903
|
+
- **Instance ID:** \`${instance_id}\`
|
|
904
|
+
- **Instance name:** ${inst.name}
|
|
905
|
+
- **Tier:** ${tier}
|
|
906
|
+
- **${layerNote}**
|
|
907
|
+
- **Embedding provider:** ${providerArg}
|
|
908
|
+
|
|
909
|
+
---
|
|
910
|
+
|
|
911
|
+
## How the 3-layer system works
|
|
912
|
+
|
|
913
|
+
\`\`\`
|
|
914
|
+
Layer 1 — Storage: Your cachly Valkey instance (${inst.name}) — persists forever
|
|
915
|
+
Layer 2 — Tools: learn_from_attempts · recall_best_solution · brain_search · session_start/end
|
|
916
|
+
Layer 3 — Autopilot: This file — AI reads it and runs tools automatically every session
|
|
917
|
+
\`\`\`
|
|
918
|
+
|
|
919
|
+
Result: Your AI **never solves the same problem twice** and always picks up exactly where it left off. 🚀
|
|
920
|
+
`;
|
|
921
|
+
const lines = [
|
|
922
|
+
`🧠 **cachly AI Memory Setup Complete**`,
|
|
923
|
+
``,
|
|
924
|
+
`**Instance:** ${inst.name} (${tier}) · ID: \`${instance_id}\``,
|
|
925
|
+
`**Embedding Provider:** ${providerArg}`,
|
|
926
|
+
`**Semantic Search:** ${hasVector ? '✅ pgvector HNSW available' : '⚠️ Not available on ' + tier + ' — upgrade to Speed/Business'}`,
|
|
927
|
+
``,
|
|
928
|
+
`─────────────────────────────────────────────`,
|
|
929
|
+
`**Step 1 — Add to .mcp.json:**`,
|
|
930
|
+
`\`\`\`json`,
|
|
931
|
+
mcpJsonSnippet,
|
|
932
|
+
`\`\`\``,
|
|
933
|
+
``,
|
|
934
|
+
`**Step 2 — Set your ${providerArg} key:**`,
|
|
935
|
+
`\`\`\`bash`,
|
|
936
|
+
`export ${provInfo.key}="your-key-here"`,
|
|
937
|
+
`\`\`\``,
|
|
938
|
+
`_(${provInfo.hint})_`,
|
|
939
|
+
``,
|
|
940
|
+
`─────────────────────────────────────────────`,
|
|
941
|
+
`**Step 3 — copilot-instructions.md (Layer 3 Autopilot):**`,
|
|
942
|
+
``,
|
|
943
|
+
...(project_dir
|
|
944
|
+
? [`🔍 Detecting IDEs in \`${project_dir}\`…`]
|
|
945
|
+
: [`Copy this to \`.github/copilot-instructions.md\` (Copilot), \`CLAUDE.md\` (Claude Code), or \`.cursor/rules\` (Cursor) in your project:`]),
|
|
946
|
+
``,
|
|
947
|
+
`\`\`\`markdown`,
|
|
948
|
+
copilotInstructions,
|
|
949
|
+
`\`\`\``,
|
|
950
|
+
``,
|
|
951
|
+
`─────────────────────────────────────────────`,
|
|
952
|
+
`**How the 3 layers work together:**`,
|
|
953
|
+
` Layer 1 → Your Valkey instance stores all lessons + context (persists forever)`,
|
|
954
|
+
` Layer 2 → MCP tools (learn_from_attempts, recall_best_solution, smart_recall) read/write it`,
|
|
955
|
+
` Layer 3 → copilot-instructions.md makes your AI run them automatically`,
|
|
956
|
+
``,
|
|
957
|
+
`Result: Your AI never solves the same problem twice. 🚀`,
|
|
958
|
+
];
|
|
959
|
+
// ── IDE auto-detection + file writing ────────────────────────────────
|
|
960
|
+
if (project_dir) {
|
|
961
|
+
const { mkdir, writeFile, access } = await import('node:fs/promises');
|
|
962
|
+
const { constants } = await import('node:fs');
|
|
963
|
+
const exists = async (p) => access(p, constants.F_OK).then(() => true).catch(() => false);
|
|
964
|
+
const targets = [];
|
|
965
|
+
let stopHookWritten = false;
|
|
966
|
+
// GitHub Copilot — always write (universal fallback)
|
|
967
|
+
targets.push({
|
|
968
|
+
ide: 'GitHub Copilot',
|
|
969
|
+
path: join(project_dir, '.github', 'copilot-instructions.md'),
|
|
970
|
+
content: copilotInstructions,
|
|
971
|
+
});
|
|
972
|
+
// Claude Code — CLAUDE.md in project root
|
|
973
|
+
if (await exists(join(project_dir, 'CLAUDE.md')) || await exists(join(project_dir, '.claude'))) {
|
|
974
|
+
targets.push({
|
|
975
|
+
ide: 'Claude Code',
|
|
976
|
+
path: join(project_dir, 'CLAUDE.md'),
|
|
977
|
+
content: copilotInstructions,
|
|
978
|
+
});
|
|
979
|
+
// Claude Code Stop-Hook — auto-saves checkpoint when Claude stops responding
|
|
980
|
+
const claudeDir = join(project_dir, '.claude');
|
|
981
|
+
await mkdir(claudeDir, { recursive: true });
|
|
982
|
+
const stopHook = {
|
|
983
|
+
hooks: {
|
|
984
|
+
Stop: [
|
|
985
|
+
{
|
|
986
|
+
matcher: '',
|
|
987
|
+
hooks: [
|
|
988
|
+
{
|
|
989
|
+
type: 'command',
|
|
990
|
+
command: `npx --yes @cachly-dev/mcp-server@latest checkpoint --instance-id ${instance_id}`,
|
|
991
|
+
},
|
|
992
|
+
],
|
|
993
|
+
},
|
|
994
|
+
],
|
|
995
|
+
},
|
|
996
|
+
};
|
|
997
|
+
const settingsPath = join(claudeDir, 'settings.json');
|
|
998
|
+
let existingSettings = {};
|
|
999
|
+
try {
|
|
1000
|
+
const { readFile: rf } = await import('node:fs/promises');
|
|
1001
|
+
existingSettings = JSON.parse(await rf(settingsPath, 'utf-8'));
|
|
1002
|
+
}
|
|
1003
|
+
catch { /* new file */ }
|
|
1004
|
+
const merged = { ...existingSettings, hooks: stopHook.hooks };
|
|
1005
|
+
await writeFile(settingsPath, JSON.stringify(merged, null, 2), 'utf-8');
|
|
1006
|
+
stopHookWritten = true;
|
|
1007
|
+
}
|
|
1008
|
+
// Cursor — .cursor/rules (new format) or .cursorrules (legacy)
|
|
1009
|
+
if (await exists(join(project_dir, '.cursor')) ||
|
|
1010
|
+
await exists(join(project_dir, '.cursorrules'))) {
|
|
1011
|
+
const cursorDir = join(project_dir, '.cursor');
|
|
1012
|
+
await mkdir(cursorDir, { recursive: true });
|
|
1013
|
+
targets.push({
|
|
1014
|
+
ide: 'Cursor',
|
|
1015
|
+
path: join(cursorDir, 'rules'),
|
|
1016
|
+
content: copilotInstructions,
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
// Windsurf — .windsurfrules
|
|
1020
|
+
if (await exists(join(project_dir, '.windsurfrules')) ||
|
|
1021
|
+
await exists(join(project_dir, '.windsurf'))) {
|
|
1022
|
+
targets.push({
|
|
1023
|
+
ide: 'Windsurf',
|
|
1024
|
+
path: join(project_dir, '.windsurfrules'),
|
|
1025
|
+
content: copilotInstructions,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
// VS Code (Copilot) — already covered by .github/copilot-instructions.md above
|
|
1029
|
+
// Continue.dev — .continue/config.json is JSON, not markdown — skip, copilot-instructions handles it
|
|
1030
|
+
const written = [];
|
|
1031
|
+
for (const target of targets) {
|
|
1032
|
+
const dir = target.path.substring(0, target.path.lastIndexOf('/'));
|
|
1033
|
+
await mkdir(dir, { recursive: true });
|
|
1034
|
+
await writeFile(target.path, target.content, 'utf-8');
|
|
1035
|
+
written.push(`✅ [${target.ide}] → \`${target.path.replace(project_dir, '.')}\``);
|
|
1036
|
+
}
|
|
1037
|
+
if (stopHookWritten) {
|
|
1038
|
+
written.push(`✅ [Claude Code Stop-Hook] → \`.claude/settings.json\` (auto-checkpoint on stop)`);
|
|
1039
|
+
}
|
|
1040
|
+
lines.push(...written);
|
|
1041
|
+
}
|
|
1042
|
+
return lines.join('\n');
|
|
1043
|
+
}
|
|
1044
|
+
default:
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
//# sourceMappingURL=team.js.map
|