@cachly-dev/mcp-server 0.5.36 → 0.5.38
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 +28 -0
- package/Dockerfile +15 -12
- package/LICENSE +202 -0
- package/README.md +2 -8
- package/dist/index.d.ts +2 -7
- package/dist/index.js +319 -2096
- package/dist/index.js.map +1 -1
- package/glama.json +6 -0
- package/package.json +1 -1
- package/smithery.yaml +5 -10
- package/src/index.ts +328 -2202
- package/CHANGELOG.md +0 -65
- package/dist/__tests__/namespace.test.d.ts +0 -1
- package/dist/__tests__/namespace.test.js +0 -114
- package/dist/__tests__/namespace.test.js.map +0 -1
- package/run-mcp.sh +0 -57
- package/scripts/postinstall.js +0 -36
- package/src/__tests__/namespace.test.ts +0 -126
- package/test-features.cjs +0 -23
package/dist/index.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { createHash } from 'node:crypto';
|
|
4
3
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
5
|
-
import { readFileSync } from 'node:fs';
|
|
6
|
-
import { execSync } from 'node:child_process';
|
|
7
4
|
import { join, relative, extname } from 'node:path';
|
|
8
5
|
/**
|
|
9
|
-
* cachly MCP Server v0.
|
|
6
|
+
* cachly MCP Server v0.5.38
|
|
10
7
|
*
|
|
11
8
|
* Exposes cachly.dev as MCP tools so any AI assistant
|
|
12
9
|
* (GitHub Copilot, Claude, Cursor, Windsurf, Continue.dev …) can:
|
|
@@ -34,20 +31,20 @@ import { join, relative, extname } from 'node:path';
|
|
|
34
31
|
*
|
|
35
32
|
* Configuration (env vars):
|
|
36
33
|
* CACHLY_API_URL – default https://api.cachly.dev
|
|
34
|
+
* CACHLY_AUTH_URL – default https://auth.cachly.dev (Keycloak base URL for health checks)
|
|
37
35
|
* CACHLY_JWT – your JWT (Keycloak access token)
|
|
38
36
|
* CACHLY_EMBED_PROVIDER – embedding backend: openai (default), gemini, mistral, cohere, ollama, cachly (server fallback)
|
|
39
37
|
* CACHLY_EMBED_MODEL – override embedding model (optional)
|
|
40
38
|
*/
|
|
41
39
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
42
40
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
43
|
-
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
44
41
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
45
42
|
import { Redis } from 'ioredis';
|
|
46
43
|
// ── Config ────────────────────────────────────────────────────────────────────
|
|
47
44
|
const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
|
|
45
|
+
const AUTH_URL = process.env.CACHLY_AUTH_URL ?? 'https://auth.cachly.dev';
|
|
48
46
|
const JWT = process.env.CACHLY_JWT ?? '';
|
|
49
47
|
const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
|
|
50
|
-
const CURRENT_VERSION = '0.5.26';
|
|
51
48
|
/**
|
|
52
49
|
* ┌──────────────────────────────────────────────────────────────────────┐
|
|
53
50
|
* │ EMBEDDING PROVIDER — pluggable, client-side first │
|
|
@@ -66,43 +63,6 @@ const CURRENT_VERSION = '0.5.26';
|
|
|
66
63
|
* └──────────────────────────────────────────────────────────────────────┘
|
|
67
64
|
*/
|
|
68
65
|
const EMBED_PROVIDER = (process.env.CACHLY_EMBED_PROVIDER ?? detectEmbedProvider()).toLowerCase();
|
|
69
|
-
// ── Confidence Decay Config (all values configurable via env) ─────────────────
|
|
70
|
-
const CONFIDENCE_WARN_DAYS = Number(process.env.CACHLY_CONFIDENCE_WARN_DAYS ?? 5); // → 0.7
|
|
71
|
-
const CONFIDENCE_STALE_DAYS = Number(process.env.CACHLY_CONFIDENCE_STALE_DAYS ?? 10); // → 0.5
|
|
72
|
-
const CONFIDENCE_WARN_VALUE = 0.7;
|
|
73
|
-
const CONFIDENCE_STALE_VALUE = 0.5;
|
|
74
|
-
/** Calculate current confidence for a lesson based on how long since last verified. */
|
|
75
|
-
function calculateConfidence(lesson) {
|
|
76
|
-
const ref = lesson.verified_at ?? lesson.ts;
|
|
77
|
-
const ageMs = Date.now() - new Date(ref).getTime();
|
|
78
|
-
const ageDays = ageMs / 86400000;
|
|
79
|
-
if (ageDays >= CONFIDENCE_STALE_DAYS)
|
|
80
|
-
return CONFIDENCE_STALE_VALUE;
|
|
81
|
-
if (ageDays >= CONFIDENCE_WARN_DAYS)
|
|
82
|
-
return CONFIDENCE_WARN_VALUE;
|
|
83
|
-
// Linear interpolation between fresh (1.0) and warn threshold
|
|
84
|
-
return 1.0 - (ageDays / CONFIDENCE_WARN_DAYS) * (1.0 - CONFIDENCE_WARN_VALUE);
|
|
85
|
-
}
|
|
86
|
-
/** Render a confidence badge string. */
|
|
87
|
-
function confidenceBadge(confidence, ageDays) {
|
|
88
|
-
if (confidence >= 0.9)
|
|
89
|
-
return '✅';
|
|
90
|
-
if (confidence >= 0.7)
|
|
91
|
-
return `⚠️ (${Math.round(ageDays)}d old, confidence ${(confidence * 100).toFixed(0)}% — verify before applying)`;
|
|
92
|
-
return `🔴 STALE (${Math.round(ageDays)}d old, confidence ${(confidence * 100).toFixed(0)}% — likely outdated!)`;
|
|
93
|
-
}
|
|
94
|
-
/** Category-specific required fields for structured lessons. */
|
|
95
|
-
const STRUCTURED_TEMPLATES = {
|
|
96
|
-
'deploy': { required: ['commands'], hint: 'deploy:* needs commands[]' },
|
|
97
|
-
'bash': { required: ['commands'], hint: 'bash:* needs commands[]' },
|
|
98
|
-
'infra': { required: ['commands'], hint: 'infra:* needs commands[] + verified on real system' },
|
|
99
|
-
'pricing': { required: [], hint: 'pricing:* — add context with source (e.g. "Stripe Dashboard Apr 2026")' },
|
|
100
|
-
'stripe': { required: [], hint: 'stripe:* — add context with Stripe API version' },
|
|
101
|
-
};
|
|
102
|
-
/** Fast content hash for index invalidation (not cryptographic, just change detection) */
|
|
103
|
-
function simpleHash(text) {
|
|
104
|
-
return createHash('md5').update(text).digest('hex').slice(0, 12);
|
|
105
|
-
}
|
|
106
66
|
function detectEmbedProvider() {
|
|
107
67
|
// Client-side keys first (direct, fast, no server roundtrip)
|
|
108
68
|
if (process.env.OPENAI_API_KEY)
|
|
@@ -288,14 +248,12 @@ function embedProviderHint() {
|
|
|
288
248
|
// • Recency boost — newer entries rank higher (exponential decay, 7-day half-life)
|
|
289
249
|
// • Multi-query splitting — numbered lists, semicolons, conjunctions
|
|
290
250
|
// • Fuzzy matching with Levenshtein distance ≤ 2
|
|
291
|
-
// • Multilingual stopwords (EN, DE, FR, ES, IT, PT
|
|
292
|
-
// • CJK character bigram extraction — handles Chinese, Japanese, Korean
|
|
251
|
+
// • Multilingual stopwords (EN, DE, FR, ES, IT, PT)
|
|
293
252
|
// • Pipeline Redis reads for performance
|
|
294
253
|
//
|
|
295
254
|
/**
|
|
296
255
|
* Stopwords — filtered out during tokenization.
|
|
297
|
-
* Covers: English, German, French, Spanish, Italian, Portuguese
|
|
298
|
-
* Chinese (Simplified + Traditional), Japanese, Korean.
|
|
256
|
+
* Covers: English, German, French, Spanish, Italian, Portuguese.
|
|
299
257
|
* Keeps the index small and scores meaningful.
|
|
300
258
|
*/
|
|
301
259
|
const STOPWORDS = new Set([
|
|
@@ -369,61 +327,14 @@ const STOPWORDS = new Set([
|
|
|
369
327
|
'eu', 'tu', 'ele', 'ela', 'nós', 'eles', 'elas', 'você', 'vocês',
|
|
370
328
|
'não', 'já', 'sim', 'bem', 'muito', 'também', 'ainda', 'sempre',
|
|
371
329
|
'todo', 'toda', 'todos', 'cada', 'outro', 'outra', 'quando', 'porque',
|
|
372
|
-
// ── Chinese (Simplified + Traditional) — high-frequency function characters ──
|
|
373
|
-
'的', '了', '是', '在', '不', '和', '我', '他', '这', '中', '大', '为',
|
|
374
|
-
'上', '个', '国', '以', '要', '就', '出', '说', '们', '有', '来', '到',
|
|
375
|
-
'时', '地', '年', '得', '着', '那', '过', '后', '还', '与', '也', '可',
|
|
376
|
-
'于', '从', '但', '而', '被', '把', '让', '使', '对', '很', '都', '一',
|
|
377
|
-
'会', '没', '人', '它', '这个', '那个', '什么', '如果', '因为', '所以',
|
|
378
|
-
'已经', '可以', '这些', '那些', '我们', '他们', '她们', '它们',
|
|
379
|
-
// ── Japanese — common hiragana particles and auxiliary verbs ──
|
|
380
|
-
'の', 'は', 'が', 'を', 'に', 'で', 'と', 'も', 'や', 'か', 'な', 'ね',
|
|
381
|
-
'よ', 'わ', 'て', 'い', 'う', 'え', 'お', 'き', 'く', 'け', 'こ', 'さ',
|
|
382
|
-
'し', 'す', 'せ', 'そ', 'た', 'ち', 'つ', 'ぬ', 'ん', 'から', 'まで',
|
|
383
|
-
'より', 'へ', 'です', 'ます', 'ない', 'ある', 'いる', 'する', 'こと',
|
|
384
|
-
'もの', 'ので', 'では', 'には', 'との', 'への', 'から', 'まで',
|
|
385
|
-
// ── Korean — common particles and auxiliary forms ──
|
|
386
|
-
'이', '가', '은', '는', '을', '를', '의', '에', '와', '과', '도',
|
|
387
|
-
'로', '에서', '한', '하', '있', '없', '것', '수', '않', '들',
|
|
388
|
-
'이다', '하다', '이고', '이며', '라고', '에게', '에서', '으로',
|
|
389
330
|
]);
|
|
390
|
-
|
|
391
|
-
const CJK_RE = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]/;
|
|
392
|
-
const SEGMENT_RE = /([\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]+|[^\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]+)/g;
|
|
393
|
-
/**
|
|
394
|
-
* Tokenize text into meaningful keywords.
|
|
395
|
-
* Handles accented Latin characters and CJK (Chinese, Japanese, Korean).
|
|
396
|
-
* CJK text is split into character bigrams (the standard for indexing logographic languages).
|
|
397
|
-
*/
|
|
331
|
+
/** Tokenize text into meaningful keywords. Handles unicode (accented chars). */
|
|
398
332
|
function tokenize(text) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
for (let i = 0; i < seg.length - 1; i++) {
|
|
405
|
-
const bigram = seg[i] + seg[i + 1];
|
|
406
|
-
if (!STOPWORDS.has(seg[i]) && !STOPWORDS.has(bigram)) {
|
|
407
|
-
tokens.push(bigram);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
// Trailing single character (for 1-char CJK terms)
|
|
411
|
-
if (seg.length >= 1) {
|
|
412
|
-
const last = seg[seg.length - 1];
|
|
413
|
-
if (!STOPWORDS.has(last))
|
|
414
|
-
tokens.push(last);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
else {
|
|
418
|
-
// Latin / other — existing logic
|
|
419
|
-
const words = seg
|
|
420
|
-
.replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœ0-9:_\-./]/g, ' ')
|
|
421
|
-
.split(/\s+/)
|
|
422
|
-
.filter(w => w.length > 1 && !STOPWORDS.has(w));
|
|
423
|
-
tokens.push(...words);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
return tokens;
|
|
333
|
+
return text
|
|
334
|
+
.toLowerCase()
|
|
335
|
+
.replace(/[^a-záàâãäåæçéèêëíìîïñóòôõöúùûüýÿßœ0-9:_\-./]/g, ' ')
|
|
336
|
+
.split(/\s+/)
|
|
337
|
+
.filter(w => w.length > 1 && !STOPWORDS.has(w));
|
|
427
338
|
}
|
|
428
339
|
/**
|
|
429
340
|
* Split a multi-topic query into sub-queries.
|
|
@@ -762,6 +673,14 @@ async function apiFetch(path, options = {}) {
|
|
|
762
673
|
},
|
|
763
674
|
});
|
|
764
675
|
if (!res.ok) {
|
|
676
|
+
if (res.status === 401 || res.status === 403) {
|
|
677
|
+
throw new McpError(ErrorCode.InvalidRequest, `cachly API: authentication failed (${res.status}) — your JWT may be expired or invalid.\n` +
|
|
678
|
+
`Get a new API token at https://cachly.dev/settings or run \`get_api_status\` to check token expiry.`);
|
|
679
|
+
}
|
|
680
|
+
if (res.status === 503 || res.status === 502) {
|
|
681
|
+
throw new McpError(ErrorCode.InternalError, `cachly API: auth service unavailable (${res.status}) — the authentication service may be temporarily down.\n` +
|
|
682
|
+
`Please try again in a moment or contact support@cachly.dev`);
|
|
683
|
+
}
|
|
765
684
|
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
766
685
|
throw new McpError(ErrorCode.InternalError, `cachly API error ${res.status}: ${body.error ?? res.statusText}`);
|
|
767
686
|
}
|
|
@@ -782,8 +701,8 @@ const TOOLS = [
|
|
|
782
701
|
name: 'create_instance',
|
|
783
702
|
description: 'Create a new managed Valkey/Redis cache instance on cachly.dev. ' +
|
|
784
703
|
'Free tier provisions in ~30 seconds. Paid tiers return a Stripe checkout URL. ' +
|
|
785
|
-
'Available tiers: free (
|
|
786
|
-
'speed (
|
|
704
|
+
'Available tiers: free (30 MB), dev (256 MB, €8/mo), pro (1 GB, €25/mo), ' +
|
|
705
|
+
'speed (1 GB Dragonfly + Semantic Cache, €39/mo), business (8 GB, €99/mo).',
|
|
787
706
|
inputSchema: {
|
|
788
707
|
type: 'object',
|
|
789
708
|
properties: {
|
|
@@ -1139,10 +1058,10 @@ const TOOLS = [
|
|
|
1139
1058
|
// ── Auth & API-Status ─────────────────────────────────────────────────────
|
|
1140
1059
|
{
|
|
1141
1060
|
name: 'get_api_status',
|
|
1142
|
-
description: 'Check the cachly API health and your authentication status. ' +
|
|
1143
|
-
'Returns whether the
|
|
1144
|
-
'token expiry, and the auth provider
|
|
1145
|
-
'Use this to debug connection issues
|
|
1061
|
+
description: 'Check the cachly API health, auth service reachability, and your authentication status. ' +
|
|
1062
|
+
'Returns API health, whether the Keycloak auth service is reachable, JWT validity, ' +
|
|
1063
|
+
'user ID (sub claim), token expiry, and the auth provider. ' +
|
|
1064
|
+
'Use this to debug connection issues, verify your CACHLY_JWT, or diagnose "Auth Service Unavailable" errors.',
|
|
1146
1065
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
1147
1066
|
},
|
|
1148
1067
|
// ── Thinking/Context Cache (for AI assistants) ────────────────────────────
|
|
@@ -1228,9 +1147,7 @@ const TOOLS = [
|
|
|
1228
1147
|
'Fields: topic (short slug like "deploy:web"), outcome ("success"|"failure"), ' +
|
|
1229
1148
|
'what_worked (what solved it), what_failed (what did NOT work), context (extra details). ' +
|
|
1230
1149
|
'Supports structured metadata: severity, file_paths (files involved), commands (working commands), tags. ' +
|
|
1231
|
-
'Deduplication: if a lesson for this topic already exists, it is updated
|
|
1232
|
-
'Contradiction detection: warns if new outcome conflicts with existing lesson outcome. ' +
|
|
1233
|
-
'Confidence: lesson starts at 1.0, decays after 5d (→0.7) and 10d (→0.5) without recall. ' +
|
|
1150
|
+
'Deduplication: if a lesson for this topic already exists, it is updated instead of duplicated. ' +
|
|
1234
1151
|
'Example: learn_from_attempts(topic="deploy:api", outcome="success", what_worked="nohup docker compose up -d --build", what_failed="docker compose up hangs on SSH timeout", severity="critical", commands=["nohup docker compose up -d --build"])',
|
|
1235
1152
|
inputSchema: {
|
|
1236
1153
|
type: 'object',
|
|
@@ -1261,15 +1178,6 @@ const TOOLS = [
|
|
|
1261
1178
|
items: { type: 'string' },
|
|
1262
1179
|
description: 'Topic tags for filtering (e.g. ["bash", "deploy", "env"])',
|
|
1263
1180
|
},
|
|
1264
|
-
depends_on: {
|
|
1265
|
-
type: 'array',
|
|
1266
|
-
items: { type: 'string' },
|
|
1267
|
-
description: 'Prerequisites this lesson depends on (e.g. ["node:>=20", "docker:running", "wireguard:active"]). When a dependency is marked stale, all dependent lessons get needs_review.',
|
|
1268
|
-
},
|
|
1269
|
-
author: {
|
|
1270
|
-
type: 'string',
|
|
1271
|
-
description: 'Name or handle of the person storing this lesson (e.g. "alice", "bob"). Used for Team Telepathy — teammates see each other\'s lessons in session_start.',
|
|
1272
|
-
},
|
|
1273
1181
|
},
|
|
1274
1182
|
required: ['instance_id', 'topic', 'outcome', 'what_worked'],
|
|
1275
1183
|
},
|
|
@@ -1278,9 +1186,7 @@ const TOOLS = [
|
|
|
1278
1186
|
name: 'recall_best_solution',
|
|
1279
1187
|
description: 'Recall the best known solution for a topic from past lessons. ' +
|
|
1280
1188
|
'Call this BEFORE attempting any task that might have been done before. ' +
|
|
1281
|
-
'Returns the most recent successful lesson for the topic,
|
|
1282
|
-
'⚠️ badge = lesson is >5d old (verify before applying). 🔴 = >10d old (likely stale!). ' +
|
|
1283
|
-
'Recalling a lesson resets its confidence clock to 1.0 (marks as recently verified). ' +
|
|
1189
|
+
'Returns the most recent successful lesson for the topic, or a summary of attempts. ' +
|
|
1284
1190
|
'Example: recall_best_solution(topic="deploy:web") → returns the working deploy command.',
|
|
1285
1191
|
inputSchema: {
|
|
1286
1192
|
type: 'object',
|
|
@@ -1291,6 +1197,20 @@ const TOOLS = [
|
|
|
1291
1197
|
required: ['instance_id', 'topic'],
|
|
1292
1198
|
},
|
|
1293
1199
|
},
|
|
1200
|
+
{
|
|
1201
|
+
name: 'forget_lesson',
|
|
1202
|
+
description: 'Delete a stored lesson from the Brain. Use this when a lesson is wrong, outdated, or was learned incorrectly. ' +
|
|
1203
|
+
'Removes both the best-solution entry and the attempt history for the topic. ' +
|
|
1204
|
+
'Example: forget_lesson(topic="deploy:web") → deletes the wrong deployment lesson so a correct one can be stored.',
|
|
1205
|
+
inputSchema: {
|
|
1206
|
+
type: 'object',
|
|
1207
|
+
properties: {
|
|
1208
|
+
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
1209
|
+
topic: { type: 'string', description: 'Exact topic slug to delete, e.g. "deploy:web"' },
|
|
1210
|
+
},
|
|
1211
|
+
required: ['instance_id', 'topic'],
|
|
1212
|
+
},
|
|
1213
|
+
},
|
|
1294
1214
|
{
|
|
1295
1215
|
name: 'smart_recall',
|
|
1296
1216
|
description: 'Semantically search cached context using natural language. ' +
|
|
@@ -1311,8 +1231,7 @@ const TOOLS = [
|
|
|
1311
1231
|
name: 'session_start',
|
|
1312
1232
|
description: 'Single-call session briefing. Call this at the START of every session INSTEAD of multiple separate smart_recall/recall_best_solution calls. ' +
|
|
1313
1233
|
'Returns: last session summary, recent lessons sorted by recency, relevant lessons for your focus area, ' +
|
|
1314
|
-
'open failures (topics with only failure outcomes), brain health stats
|
|
1315
|
-
'predictive pre-warnings (if your focus area has known failure patterns), and memory crystals (compressed wisdom from old sessions). ' +
|
|
1234
|
+
'open failures (topics with only failure outcomes), and brain health stats (lesson count, context count). ' +
|
|
1316
1235
|
'Also saves a session start marker so session_end can compute duration.',
|
|
1317
1236
|
inputSchema: {
|
|
1318
1237
|
type: 'object',
|
|
@@ -1322,10 +1241,6 @@ const TOOLS = [
|
|
|
1322
1241
|
type: 'string',
|
|
1323
1242
|
description: 'Keywords for what you plan to work on today (e.g. "deploy infra api"). Used to surface relevant lessons at the top.',
|
|
1324
1243
|
},
|
|
1325
|
-
author: {
|
|
1326
|
-
type: 'string',
|
|
1327
|
-
description: 'Your name or handle (e.g. "alice"). Enables Team Telepathy — filters YOUR lessons vs TEAM lessons from past 7 days.',
|
|
1328
|
-
},
|
|
1329
1244
|
},
|
|
1330
1245
|
required: ['instance_id'],
|
|
1331
1246
|
},
|
|
@@ -1335,8 +1250,7 @@ const TOOLS = [
|
|
|
1335
1250
|
description: 'Save a session summary when you finish working. ' +
|
|
1336
1251
|
'Records what was accomplished, files changed, and lesson count. ' +
|
|
1337
1252
|
'The next session_start will show this summary as "Last session". ' +
|
|
1338
|
-
'Call this when ending a work session, before going idle, or before summarizing.
|
|
1339
|
-
'Ambient Learning: if workspace_path is provided, reads git log since session start and auto-learns from commits.',
|
|
1253
|
+
'Call this when ending a work session, before going idle, or before summarizing.',
|
|
1340
1254
|
inputSchema: {
|
|
1341
1255
|
type: 'object',
|
|
1342
1256
|
properties: {
|
|
@@ -1354,66 +1268,10 @@ const TOOLS = [
|
|
|
1354
1268
|
type: 'number',
|
|
1355
1269
|
description: 'Number of new lessons stored this session (optional)',
|
|
1356
1270
|
},
|
|
1357
|
-
workspace_path: {
|
|
1358
|
-
type: 'string',
|
|
1359
|
-
description: 'Absolute path to the project root (e.g. "/Users/you/myproject"). Enables Ambient Learning — reads git log since session start and auto-learns from commit messages.',
|
|
1360
|
-
},
|
|
1361
1271
|
},
|
|
1362
1272
|
required: ['instance_id', 'summary'],
|
|
1363
1273
|
},
|
|
1364
1274
|
},
|
|
1365
|
-
// ── Session Handoff — cross-window continuity ─────────────────────────────
|
|
1366
|
-
{
|
|
1367
|
-
name: 'session_handoff',
|
|
1368
|
-
description: 'Save a detailed handoff for the NEXT chat window / session. ' +
|
|
1369
|
-
'Stores: current progress, TODO list (done + remaining), changed files with descriptions, ' +
|
|
1370
|
-
'instructions for the next assistant, and any incomplete work. ' +
|
|
1371
|
-
'The next session_start automatically includes this handoff so the new window knows EXACTLY what happened and what remains. ' +
|
|
1372
|
-
'Call this BEFORE closing a chat window, especially if work is incomplete. ' +
|
|
1373
|
-
'This prevents the "continue" problem where new windows lose context, skip tasks, or produce broken code.',
|
|
1374
|
-
inputSchema: {
|
|
1375
|
-
type: 'object',
|
|
1376
|
-
properties: {
|
|
1377
|
-
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
1378
|
-
completed_tasks: {
|
|
1379
|
-
type: 'array',
|
|
1380
|
-
items: { type: 'string' },
|
|
1381
|
-
description: 'Tasks that were fully completed (e.g. "Implemented brainSearch() in JS SDK")',
|
|
1382
|
-
},
|
|
1383
|
-
remaining_tasks: {
|
|
1384
|
-
type: 'array',
|
|
1385
|
-
items: { type: 'string' },
|
|
1386
|
-
description: 'Tasks NOT yet done — the next window MUST pick these up',
|
|
1387
|
-
},
|
|
1388
|
-
files_changed: {
|
|
1389
|
-
type: 'array',
|
|
1390
|
-
items: {
|
|
1391
|
-
type: 'object',
|
|
1392
|
-
properties: {
|
|
1393
|
-
path: { type: 'string', description: 'File path relative to project root' },
|
|
1394
|
-
status: { type: 'string', enum: ['complete', 'partial', 'broken'], description: 'State of this file' },
|
|
1395
|
-
description: { type: 'string', description: 'What was changed and what still needs work' },
|
|
1396
|
-
},
|
|
1397
|
-
required: ['path', 'status'],
|
|
1398
|
-
},
|
|
1399
|
-
description: 'Changed files with their current state — marks partial/broken files so next window knows to fix them',
|
|
1400
|
-
},
|
|
1401
|
-
instructions: {
|
|
1402
|
-
type: 'string',
|
|
1403
|
-
description: 'Free-form instructions for the next assistant. Be specific: what to do next, what to avoid, what broke.',
|
|
1404
|
-
},
|
|
1405
|
-
context_summary: {
|
|
1406
|
-
type: 'string',
|
|
1407
|
-
description: 'Brief summary of what happened this session (architecture decisions, key findings, blockers)',
|
|
1408
|
-
},
|
|
1409
|
-
blocked_on: {
|
|
1410
|
-
type: 'string',
|
|
1411
|
-
description: 'If work is blocked, describe what is needed to unblock (e.g. "waiting for API deploy", "needs user input on design")',
|
|
1412
|
-
},
|
|
1413
|
-
},
|
|
1414
|
-
required: ['instance_id', 'completed_tasks', 'remaining_tasks'],
|
|
1415
|
-
},
|
|
1416
|
-
},
|
|
1417
1275
|
// ── AI Brain — Extended features ─────────────────────────────────────────
|
|
1418
1276
|
{
|
|
1419
1277
|
name: 'auto_learn_session',
|
|
@@ -1500,147 +1358,15 @@ const TOOLS = [
|
|
|
1500
1358
|
required: ['instance_id'],
|
|
1501
1359
|
},
|
|
1502
1360
|
},
|
|
1503
|
-
{
|
|
1504
|
-
name: 'team_synthesize',
|
|
1505
|
-
description: 'Team Brain Synthesis — merge multiple contributors\' lessons on the same topic into one canonical version. ' +
|
|
1506
|
-
'When 2+ developers store lessons for the same topic with different details, this proposes the best merged version. ' +
|
|
1507
|
-
'Shows: all contributions by author, what worked (consensus), what failed (union), canonical lesson to store. ' +
|
|
1508
|
-
'Use this when onboarding new team members or before documenting a process.',
|
|
1509
|
-
inputSchema: {
|
|
1510
|
-
type: 'object',
|
|
1511
|
-
properties: {
|
|
1512
|
-
instance_id: { type: 'string', description: 'UUID of the shared team brain instance' },
|
|
1513
|
-
topic: { type: 'string', description: 'Topic slug to synthesize (e.g. "deploy:api")' },
|
|
1514
|
-
},
|
|
1515
|
-
required: ['instance_id', 'topic'],
|
|
1516
|
-
},
|
|
1517
|
-
},
|
|
1518
|
-
{
|
|
1519
|
-
name: 'memory_crystalize',
|
|
1520
|
-
description: 'Compress the last 30-50 sessions and auto-learned lessons into a dense Memory Crystal. ' +
|
|
1521
|
-
'A crystal is a compact, structured summary of everything the brain learned — grouped by category (deploy, fix, debug, …). ' +
|
|
1522
|
-
'Crystals survive session cleanup and appear in session_start once enough sessions have accumulated. ' +
|
|
1523
|
-
'Run this monthly or after a big milestone to preserve institutional knowledge. ' +
|
|
1524
|
-
'Returns a digest of what was crystallized.',
|
|
1525
|
-
inputSchema: {
|
|
1526
|
-
type: 'object',
|
|
1527
|
-
properties: {
|
|
1528
|
-
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
1529
|
-
label: {
|
|
1530
|
-
type: 'string',
|
|
1531
|
-
description: 'Optional label for this crystal (e.g. "Q1 2026", "v2 launch"). Auto-generated from date if omitted.',
|
|
1532
|
-
},
|
|
1533
|
-
},
|
|
1534
|
-
required: ['instance_id'],
|
|
1535
|
-
},
|
|
1536
|
-
},
|
|
1537
|
-
// ── Roadmap — Persistent project plan tracker ───────────────────────────
|
|
1538
|
-
{
|
|
1539
|
-
name: 'roadmap_add',
|
|
1540
|
-
description: 'Add a new item to the persistent project roadmap stored in the Brain. ' +
|
|
1541
|
-
'Items survive across sessions and editors — the roadmap is always up to date. ' +
|
|
1542
|
-
'Use for features, bugs, refactors, or any planned work. ' +
|
|
1543
|
-
'Call roadmap_list to see all open items, roadmap_next to get the next actionable item.',
|
|
1544
|
-
inputSchema: {
|
|
1545
|
-
type: 'object',
|
|
1546
|
-
properties: {
|
|
1547
|
-
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
1548
|
-
title: { type: 'string', description: 'Short title of the task/feature (3–10 words)' },
|
|
1549
|
-
description: { type: 'string', description: 'What needs to be done, acceptance criteria, context' },
|
|
1550
|
-
priority: {
|
|
1551
|
-
type: 'string',
|
|
1552
|
-
enum: ['critical', 'high', 'medium', 'low'],
|
|
1553
|
-
description: 'Priority level (default: medium)',
|
|
1554
|
-
},
|
|
1555
|
-
tags: {
|
|
1556
|
-
type: 'array',
|
|
1557
|
-
items: { type: 'string' },
|
|
1558
|
-
description: 'Tags for filtering (e.g. ["api", "web", "sdk", "infra"])',
|
|
1559
|
-
},
|
|
1560
|
-
milestone: { type: 'string', description: 'Milestone/epic this belongs to (optional)' },
|
|
1561
|
-
},
|
|
1562
|
-
required: ['instance_id', 'title'],
|
|
1563
|
-
},
|
|
1564
|
-
},
|
|
1565
|
-
{
|
|
1566
|
-
name: 'roadmap_update',
|
|
1567
|
-
description: 'Update the status, priority, or details of a roadmap item. ' +
|
|
1568
|
-
'Use to move items through the lifecycle: planned → in-progress → done (or blocked/cancelled). ' +
|
|
1569
|
-
'Also use to add notes/findings while working on an item.',
|
|
1570
|
-
inputSchema: {
|
|
1571
|
-
type: 'object',
|
|
1572
|
-
properties: {
|
|
1573
|
-
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
1574
|
-
id: { type: 'string', description: 'Item ID returned by roadmap_add or roadmap_list' },
|
|
1575
|
-
status: {
|
|
1576
|
-
type: 'string',
|
|
1577
|
-
enum: ['planned', 'in-progress', 'done', 'blocked', 'cancelled'],
|
|
1578
|
-
description: 'New status for the item',
|
|
1579
|
-
},
|
|
1580
|
-
priority: {
|
|
1581
|
-
type: 'string',
|
|
1582
|
-
enum: ['critical', 'high', 'medium', 'low'],
|
|
1583
|
-
description: 'Updated priority (optional)',
|
|
1584
|
-
},
|
|
1585
|
-
notes: { type: 'string', description: 'Progress notes, findings, or blockers (appended to existing notes)' },
|
|
1586
|
-
title: { type: 'string', description: 'Updated title (optional)' },
|
|
1587
|
-
description: { type: 'string', description: 'Updated description (optional)' },
|
|
1588
|
-
},
|
|
1589
|
-
required: ['instance_id', 'id'],
|
|
1590
|
-
},
|
|
1591
|
-
},
|
|
1592
|
-
{
|
|
1593
|
-
name: 'roadmap_list',
|
|
1594
|
-
description: 'List all roadmap items, optionally filtered by status, priority, tag, or milestone. ' +
|
|
1595
|
-
'Returns items sorted by priority then creation date. ' +
|
|
1596
|
-
'Called automatically by session_start to show open work.',
|
|
1597
|
-
inputSchema: {
|
|
1598
|
-
type: 'object',
|
|
1599
|
-
properties: {
|
|
1600
|
-
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
1601
|
-
status: {
|
|
1602
|
-
type: 'string',
|
|
1603
|
-
enum: ['planned', 'in-progress', 'done', 'blocked', 'cancelled', 'open'],
|
|
1604
|
-
description: 'Filter by status. Use \'open\' to see planned+in-progress+blocked (default: open)',
|
|
1605
|
-
},
|
|
1606
|
-
tag: { type: 'string', description: 'Filter by tag (optional)' },
|
|
1607
|
-
milestone: { type: 'string', description: 'Filter by milestone (optional)' },
|
|
1608
|
-
priority: {
|
|
1609
|
-
type: 'string',
|
|
1610
|
-
enum: ['critical', 'high', 'medium', 'low'],
|
|
1611
|
-
description: 'Filter by minimum priority (optional)',
|
|
1612
|
-
},
|
|
1613
|
-
},
|
|
1614
|
-
required: ['instance_id'],
|
|
1615
|
-
},
|
|
1616
|
-
},
|
|
1617
|
-
{
|
|
1618
|
-
name: 'roadmap_next',
|
|
1619
|
-
description: 'Get the single most important next actionable roadmap item. ' +
|
|
1620
|
-
'Returns the highest-priority in-progress item first, then planned items, sorted by priority. ' +
|
|
1621
|
-
'Call at session start to immediately know what to work on next.',
|
|
1622
|
-
inputSchema: {
|
|
1623
|
-
type: 'object',
|
|
1624
|
-
properties: {
|
|
1625
|
-
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
1626
|
-
tag: { type: 'string', description: 'Filter by tag (optional)' },
|
|
1627
|
-
},
|
|
1628
|
-
required: ['instance_id'],
|
|
1629
|
-
},
|
|
1630
|
-
},
|
|
1631
1361
|
{
|
|
1632
1362
|
name: 'brain_doctor',
|
|
1633
1363
|
description: 'Check the health of your AI Brain and get actionable recommendations. ' +
|
|
1634
|
-
'Reports: lesson count, context entries, last session age, open failures, quality score
|
|
1364
|
+
'Reports: lesson count, context entries, last session age, open failures, quality score. ' +
|
|
1635
1365
|
'Returns a prioritized list of issues with fix instructions.',
|
|
1636
1366
|
inputSchema: {
|
|
1637
1367
|
type: 'object',
|
|
1638
1368
|
properties: {
|
|
1639
1369
|
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
1640
|
-
workspace_path: {
|
|
1641
|
-
type: 'string',
|
|
1642
|
-
description: 'Absolute path to workspace root — enables package.json analysis for openclaw cross-promo (optional)',
|
|
1643
|
-
},
|
|
1644
1370
|
},
|
|
1645
1371
|
required: ['instance_id'],
|
|
1646
1372
|
},
|
|
@@ -1708,94 +1434,6 @@ const TOOLS = [
|
|
|
1708
1434
|
required: ['instance_id', 'framework'],
|
|
1709
1435
|
},
|
|
1710
1436
|
},
|
|
1711
|
-
// ── Brain Archaeology + Causal Chain ────────────────────────────────────
|
|
1712
|
-
{
|
|
1713
|
-
name: 'recall_at',
|
|
1714
|
-
description: 'Brain Archaeology — see what a lesson looked like at a specific point in time. ' +
|
|
1715
|
-
'"What did we know about deployments 3 months ago?" ' +
|
|
1716
|
-
'Returns the history of a topic filtered to entries before the given date. ' +
|
|
1717
|
-
'Shows how the lesson evolved: failure → partial → success. ' +
|
|
1718
|
-
'Also useful to understand WHY old code decisions were made.',
|
|
1719
|
-
inputSchema: {
|
|
1720
|
-
type: 'object',
|
|
1721
|
-
properties: {
|
|
1722
|
-
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
1723
|
-
topic: { type: 'string', description: 'Topic slug to look up, e.g. "deploy:api"' },
|
|
1724
|
-
date: { type: 'string', description: 'ISO date string (e.g. "2026-01-15") — returns entries stored BEFORE this date' },
|
|
1725
|
-
},
|
|
1726
|
-
required: ['instance_id', 'topic', 'date'],
|
|
1727
|
-
},
|
|
1728
|
-
},
|
|
1729
|
-
{
|
|
1730
|
-
name: 'trace_dependency',
|
|
1731
|
-
description: 'Causal Chain — find all lessons that depend on a given prerequisite. ' +
|
|
1732
|
-
'"What lessons are affected if node version changes?" ' +
|
|
1733
|
-
'When a dependency changes (new version, different provider, new OS), call this to see which lessons need review. ' +
|
|
1734
|
-
'Lessons store dependencies via the depends_on field in learn_from_attempts.',
|
|
1735
|
-
inputSchema: {
|
|
1736
|
-
type: 'object',
|
|
1737
|
-
properties: {
|
|
1738
|
-
instance_id: { type: 'string', description: 'UUID of the cache instance' },
|
|
1739
|
-
dependency: { type: 'string', description: 'Dependency to trace (e.g. "node:>=20", "docker:running", "wireguard:active")' },
|
|
1740
|
-
mark_review: { type: 'boolean', description: 'If true, marks all dependent lessons as needs_review (default: false)' },
|
|
1741
|
-
},
|
|
1742
|
-
required: ['instance_id', 'dependency'],
|
|
1743
|
-
},
|
|
1744
|
-
},
|
|
1745
|
-
// ── Team / Org Management ────────────────────────────────────────────────
|
|
1746
|
-
{
|
|
1747
|
-
name: 'list_orgs',
|
|
1748
|
-
description: 'List your Cachly organizations (team/org plans). ' +
|
|
1749
|
-
'Returns each org with plan, seat count, and member info. ' +
|
|
1750
|
-
'Org plans (Team €99, Business €299, Enterprise custom) are billed separately from cache tiers.',
|
|
1751
|
-
inputSchema: {
|
|
1752
|
-
type: 'object',
|
|
1753
|
-
properties: {},
|
|
1754
|
-
required: [],
|
|
1755
|
-
},
|
|
1756
|
-
},
|
|
1757
|
-
{
|
|
1758
|
-
name: 'create_org',
|
|
1759
|
-
description: 'Create a new Cachly organization for team collaboration. ' +
|
|
1760
|
-
'After creation, invite team members with invite_member and upgrade the plan via the billing portal. ' +
|
|
1761
|
-
'Org plans: Team (€99/mo, 10 seats), Business (€299/mo, 50 seats), Enterprise (custom).',
|
|
1762
|
-
inputSchema: {
|
|
1763
|
-
type: 'object',
|
|
1764
|
-
properties: {
|
|
1765
|
-
name: { type: 'string', description: 'Organization display name (e.g. "Acme Engineering")' },
|
|
1766
|
-
slug: { type: 'string', description: 'URL-safe slug (e.g. "acme-eng"). Auto-generated from name if omitted.' },
|
|
1767
|
-
},
|
|
1768
|
-
required: ['name'],
|
|
1769
|
-
},
|
|
1770
|
-
},
|
|
1771
|
-
{
|
|
1772
|
-
name: 'invite_member',
|
|
1773
|
-
description: 'Invite a team member to a Cachly organization by email. ' +
|
|
1774
|
-
'They will receive an invite email and can join via the dashboard. ' +
|
|
1775
|
-
'Roles: owner (full access), admin (manage members + instances), member (read + cache ops).',
|
|
1776
|
-
inputSchema: {
|
|
1777
|
-
type: 'object',
|
|
1778
|
-
properties: {
|
|
1779
|
-
org_id: { type: 'string', description: 'UUID of the organization' },
|
|
1780
|
-
email: { type: 'string', description: 'Email address to invite' },
|
|
1781
|
-
role: { type: 'string', enum: ['admin', 'member'], description: 'Role for the invited member (default: member)' },
|
|
1782
|
-
},
|
|
1783
|
-
required: ['org_id', 'email'],
|
|
1784
|
-
},
|
|
1785
|
-
},
|
|
1786
|
-
{
|
|
1787
|
-
name: 'get_org_plan',
|
|
1788
|
-
description: 'Get the current org plan, seat usage, and billing info for an organization. ' +
|
|
1789
|
-
'Shows: plan name, price, seats used/max, next billing date. ' +
|
|
1790
|
-
'To upgrade: use the billing portal URL returned by this tool.',
|
|
1791
|
-
inputSchema: {
|
|
1792
|
-
type: 'object',
|
|
1793
|
-
properties: {
|
|
1794
|
-
org_id: { type: 'string', description: 'UUID of the organization' },
|
|
1795
|
-
},
|
|
1796
|
-
required: ['org_id'],
|
|
1797
|
-
},
|
|
1798
|
-
},
|
|
1799
1437
|
// ── Legacy / Setup ────────────────────────────────────────────────────────
|
|
1800
1438
|
{
|
|
1801
1439
|
name: 'setup_ai_memory',
|
|
@@ -1886,52 +1524,10 @@ function buildConnectionString(inst) {
|
|
|
1886
1524
|
const pw = inst.password ? `:${inst.password}@` : '@';
|
|
1887
1525
|
return `${scheme}://${pw}${inst.host}:${inst.port}`;
|
|
1888
1526
|
}
|
|
1889
|
-
// Fires once per process when no JWT is set (anonymous, opt-out via CACHLY_NO_TELEMETRY=1)
|
|
1890
|
-
let _telemetryPingSent = false;
|
|
1891
|
-
async function sendAnonymousTelemetry(toolName) {
|
|
1892
|
-
if (_telemetryPingSent)
|
|
1893
|
-
return;
|
|
1894
|
-
if (process.env.CACHLY_NO_TELEMETRY === '1')
|
|
1895
|
-
return;
|
|
1896
|
-
_telemetryPingSent = true;
|
|
1897
|
-
// Detect editor from common env vars injected by IDE extensions
|
|
1898
|
-
const editor = process.env.CURSOR_TRACE_ID ? 'cursor'
|
|
1899
|
-
: process.env.WINDSURF_SESSION_ID ? 'windsurf'
|
|
1900
|
-
: process.env.GITHUB_COPILOT_WORKSPACE ? 'copilot'
|
|
1901
|
-
: process.env.CLAUDE_CODE_ENTRYPOINT ? 'claude'
|
|
1902
|
-
: 'unknown';
|
|
1903
|
-
try {
|
|
1904
|
-
await fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
1905
|
-
method: 'POST',
|
|
1906
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1907
|
-
body: JSON.stringify({ event: 'first_call_no_jwt', version: CURRENT_VERSION, editor, tool: toolName }),
|
|
1908
|
-
signal: AbortSignal.timeout(3000),
|
|
1909
|
-
});
|
|
1910
|
-
}
|
|
1911
|
-
catch { /* fire-and-forget, never block the user */ }
|
|
1912
|
-
}
|
|
1913
1527
|
async function handleTool(name, args) {
|
|
1914
|
-
//
|
|
1915
|
-
if (!
|
|
1916
|
-
|
|
1917
|
-
return [
|
|
1918
|
-
'🧠 **cachly AI Brain — Setup required**',
|
|
1919
|
-
'',
|
|
1920
|
-
'You have no API credentials configured. Get started in 30 seconds:',
|
|
1921
|
-
'',
|
|
1922
|
-
'**Step 1:** Register for free (no credit card):',
|
|
1923
|
-
' 👉 https://cachly.dev/setup-ai',
|
|
1924
|
-
'',
|
|
1925
|
-
'**Step 2:** Run the setup wizard in your terminal:',
|
|
1926
|
-
' ```',
|
|
1927
|
-
' npx @cachly-dev/mcp-server@latest setup',
|
|
1928
|
-
' ```',
|
|
1929
|
-
'',
|
|
1930
|
-
'The wizard auto-detects your editor (Claude Code, Cursor, Copilot, Windsurf)',
|
|
1931
|
-
'and writes the config file with your credentials.',
|
|
1932
|
-
'',
|
|
1933
|
-
'✨ Free tier includes: 1 Brain instance, persistent memory, semantic search.',
|
|
1934
|
-
].join('\n');
|
|
1528
|
+
// Default instance_id from env so tools work even if AI omits it
|
|
1529
|
+
if (!args.instance_id && process.env.CACHLY_BRAIN_INSTANCE_ID) {
|
|
1530
|
+
args = { ...args, instance_id: process.env.CACHLY_BRAIN_INSTANCE_ID };
|
|
1935
1531
|
}
|
|
1936
1532
|
// Delegate v0.2 bulk/lock/stream tools first
|
|
1937
1533
|
const bulkResult = await handleBulkLockStream(name, args);
|
|
@@ -1950,7 +1546,7 @@ async function handleTool(name, args) {
|
|
|
1950
1546
|
const { name: instName, tier } = args;
|
|
1951
1547
|
const res = await apiFetch('/api/v1/instances', {
|
|
1952
1548
|
method: 'POST',
|
|
1953
|
-
body: JSON.stringify({ name: instName, tier
|
|
1549
|
+
body: JSON.stringify({ name: instName, tier }),
|
|
1954
1550
|
});
|
|
1955
1551
|
if (res.checkout_url) {
|
|
1956
1552
|
return [
|
|
@@ -2007,63 +1603,6 @@ async function handleTool(name, args) {
|
|
|
2007
1603
|
await apiFetch(`/api/v1/instances/${instance_id}`, { method: 'DELETE' });
|
|
2008
1604
|
return `✅ Instance \`${instance_id}\` has been deleted and all data removed.`;
|
|
2009
1605
|
}
|
|
2010
|
-
// ── Org / Team management ────────────────────────────────────────────
|
|
2011
|
-
case 'list_orgs': {
|
|
2012
|
-
const res = await apiFetch('/api/v1/orgs');
|
|
2013
|
-
const orgs = res.orgs ?? [];
|
|
2014
|
-
if (orgs.length === 0)
|
|
2015
|
-
return `📭 No organizations yet.\n\nCreate one with \`create_org(name="My Team")\`.\nOrg plans: Team €99/mo (10 seats), Business €299/mo (50 seats), Enterprise custom.`;
|
|
2016
|
-
return [
|
|
2017
|
-
`🏢 **Your organizations (${orgs.length})**\n`,
|
|
2018
|
-
...orgs.map(o => `• **${o.name}** (\`${o.slug}\`) — plan: ${o.plan} · seats: ${o.member_count ?? '?'}/${o.max_members}\n ID: \`${o.id}\``),
|
|
2019
|
-
`\n_Manage: \`get_org_plan\`, \`invite_member\`, dashboard → /team_`,
|
|
2020
|
-
].join('\n');
|
|
2021
|
-
}
|
|
2022
|
-
case 'create_org': {
|
|
2023
|
-
const { name: orgName, slug } = args;
|
|
2024
|
-
const body = { name: orgName };
|
|
2025
|
-
if (slug)
|
|
2026
|
-
body.slug = slug;
|
|
2027
|
-
const res = await apiFetch('/api/v1/orgs', {
|
|
2028
|
-
method: 'POST',
|
|
2029
|
-
body: JSON.stringify(body),
|
|
2030
|
-
});
|
|
2031
|
-
return [
|
|
2032
|
-
`✅ **Organization created:** ${res.name}`,
|
|
2033
|
-
` ID: \`${res.id}\` · Slug: \`${res.slug}\` · Plan: ${res.plan}`,
|
|
2034
|
-
``,
|
|
2035
|
-
`**Next steps:**`,
|
|
2036
|
-
`1. Invite team members: \`invite_member(org_id="${res.id}", email="dev@example.com")\``,
|
|
2037
|
-
`2. Upgrade plan: open billing portal via dashboard → /team`,
|
|
2038
|
-
` Team: €99/mo (10 seats) · Business: €299/mo (50 seats)`,
|
|
2039
|
-
].join('\n');
|
|
2040
|
-
}
|
|
2041
|
-
case 'invite_member': {
|
|
2042
|
-
const { org_id, email, role = 'member' } = args;
|
|
2043
|
-
await apiFetch(`/api/v1/orgs/${org_id}/members`, {
|
|
2044
|
-
method: 'POST',
|
|
2045
|
-
body: JSON.stringify({ email, role }),
|
|
2046
|
-
});
|
|
2047
|
-
return `✅ Invite sent to **${email}** as \`${role}\` in org \`${org_id}\`.\n\nThey will receive an email to join the organization.`;
|
|
2048
|
-
}
|
|
2049
|
-
case 'get_org_plan': {
|
|
2050
|
-
const { org_id } = args;
|
|
2051
|
-
const org = await apiFetch(`/api/v1/orgs/${org_id}`);
|
|
2052
|
-
const accepted = (org.members ?? []).filter(m => m.accepted_at).length;
|
|
2053
|
-
const pending = (org.members ?? []).filter(m => !m.accepted_at).length;
|
|
2054
|
-
const planPrice = { free: '€0', team: '€99/mo', business: '€299/mo', enterprise: 'custom' };
|
|
2055
|
-
return [
|
|
2056
|
-
`🏢 **${org.name}** — Plan: **${org.plan}** (${planPrice[org.plan] ?? org.plan})`,
|
|
2057
|
-
` Seats: ${accepted} active + ${pending} pending / ${org.max_members} max`,
|
|
2058
|
-
``,
|
|
2059
|
-
`**Members:**`,
|
|
2060
|
-
...(org.members ?? []).map(m => ` • ${m.invite_email} (${m.role})${m.accepted_at ? '' : ' — pending'}`),
|
|
2061
|
-
``,
|
|
2062
|
-
org.stripe_customer_id
|
|
2063
|
-
? `💳 Billing: managed via Stripe. Upgrade/cancel: dashboard → /billing`
|
|
2064
|
-
: `💳 No payment method yet. Upgrade: dashboard → /billing → Team Plans`,
|
|
2065
|
-
].join('\n');
|
|
2066
|
-
}
|
|
2067
1606
|
// ── Live cache operations ────────────────────────────────────────────
|
|
2068
1607
|
case 'cache_get': {
|
|
2069
1608
|
const { instance_id, key } = args;
|
|
@@ -2386,42 +1925,24 @@ async function handleTool(name, args) {
|
|
|
2386
1925
|
let skipped = 0;
|
|
2387
1926
|
let errors = 0;
|
|
2388
1927
|
let semanticIndexed = 0;
|
|
2389
|
-
let unchanged = 0;
|
|
2390
1928
|
const details = [];
|
|
2391
1929
|
const expiresAt = new Date(Date.now() + ttl * 1000).toISOString();
|
|
2392
1930
|
const redis = await getConnection(instance_id);
|
|
2393
1931
|
for (const filePath of files) {
|
|
2394
1932
|
const relPath = relative(dir, filePath);
|
|
2395
1933
|
let content;
|
|
2396
|
-
let fileSize;
|
|
2397
1934
|
try {
|
|
2398
1935
|
const s = await stat(filePath);
|
|
2399
1936
|
if (s.size > 200_000) {
|
|
2400
1937
|
skipped++;
|
|
2401
1938
|
continue;
|
|
2402
1939
|
} // skip files >200 KB
|
|
2403
|
-
fileSize = s.size;
|
|
2404
1940
|
content = await readFile(filePath, 'utf-8');
|
|
2405
1941
|
}
|
|
2406
1942
|
catch {
|
|
2407
1943
|
errors++;
|
|
2408
1944
|
continue;
|
|
2409
1945
|
}
|
|
2410
|
-
// ── Smart invalidation: hash-based change detection ──
|
|
2411
|
-
// Compute a simple hash of file content to skip unchanged files
|
|
2412
|
-
const hashKey = `cachly:idx:hash:${relPath}`;
|
|
2413
|
-
const contentHash = `${fileSize}:${content.length}:${simpleHash(content)}`;
|
|
2414
|
-
const existingHash = await redis.get(hashKey);
|
|
2415
|
-
if (existingHash === contentHash) {
|
|
2416
|
-
// File unchanged — refresh TTL but skip re-indexing
|
|
2417
|
-
const idxKey = `cachly:idx:${relPath}`;
|
|
2418
|
-
if (ttl > 0)
|
|
2419
|
-
await redis.expire(idxKey, ttl);
|
|
2420
|
-
if (ttl > 0)
|
|
2421
|
-
await redis.expire(hashKey, ttl);
|
|
2422
|
-
unchanged++;
|
|
2423
|
-
continue;
|
|
2424
|
-
}
|
|
2425
1946
|
const excerpt = content.slice(0, summary_chars).replace(/\s+/g, ' ').trim();
|
|
2426
1947
|
// ── Layer 1: Keyword index in Valkey (always works, no embedding needed) ──
|
|
2427
1948
|
const idxKey = `cachly:idx:${relPath}`;
|
|
@@ -2432,13 +1953,6 @@ async function handleTool(name, args) {
|
|
|
2432
1953
|
else {
|
|
2433
1954
|
await redis.set(idxKey, idxValue);
|
|
2434
1955
|
}
|
|
2435
|
-
// Store content hash for smart invalidation on next run
|
|
2436
|
-
if (ttl > 0) {
|
|
2437
|
-
await redis.set(hashKey, contentHash, 'EX', ttl);
|
|
2438
|
-
}
|
|
2439
|
-
else {
|
|
2440
|
-
await redis.set(hashKey, contentHash);
|
|
2441
|
-
}
|
|
2442
1956
|
indexed++;
|
|
2443
1957
|
details.push(` ✅ ${relPath}`);
|
|
2444
1958
|
// ── Layer 2: Semantic vector index (optional, only if embedding available) ──
|
|
@@ -2465,8 +1979,7 @@ async function handleTool(name, args) {
|
|
|
2465
1979
|
`📂 **index_project Complete** — ${mode}`,
|
|
2466
1980
|
``,
|
|
2467
1981
|
` 📁 Dir: ${dir}`,
|
|
2468
|
-
` ✅ Indexed: **${indexed}** files (
|
|
2469
|
-
` ♻️ Unchanged: ${unchanged} files (hash match — skipped)`,
|
|
1982
|
+
` ✅ Indexed: **${indexed}** files (keyword-searchable via smart_recall)`,
|
|
2470
1983
|
...(canEmbed ? [` 🎯 Semantic: **${semanticIndexed}** files (vector-searchable)`] : []),
|
|
2471
1984
|
` ⏭️ Skipped: ${skipped} (too large or filtered)`,
|
|
2472
1985
|
` ❌ Errors: ${errors}`,
|
|
@@ -2648,92 +2161,24 @@ async function handleTool(name, args) {
|
|
|
2648
2161
|
return `🗑️ **Forgot ${deleted} context entries.**\n\nKeys: ${keys.map(k => `\`${k}\``).join(', ')}`;
|
|
2649
2162
|
}
|
|
2650
2163
|
case 'learn_from_attempts': {
|
|
2651
|
-
const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [],
|
|
2164
|
+
const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], } = args;
|
|
2652
2165
|
const redis = await getConnection(instance_id);
|
|
2653
2166
|
const ts = new Date().toISOString();
|
|
2654
|
-
// ──
|
|
2655
|
-
const category = topic.split(':')[0];
|
|
2656
|
-
const template = STRUCTURED_TEMPLATES[category];
|
|
2657
|
-
const templateWarnings = [];
|
|
2658
|
-
if (template) {
|
|
2659
|
-
for (const req of template.required) {
|
|
2660
|
-
if (req === 'commands' && commands.length === 0) {
|
|
2661
|
-
templateWarnings.push(`📋 ${template.hint}`);
|
|
2662
|
-
}
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2665
|
-
// ── Deduplication + audit trail ────────────────────────────────────────
|
|
2167
|
+
// ── Deduplication: check for existing lesson with same topic ─────────────
|
|
2666
2168
|
let isUpdate = false;
|
|
2667
2169
|
let recallCount = 0;
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
try {
|
|
2672
|
-
const prev = JSON.parse(existingRaw);
|
|
2673
|
-
recallCount = prev.recall_count ?? 0;
|
|
2674
|
-
auditTrail = prev.audit_trail ?? [];
|
|
2675
|
-
auditTrail.push({ ts, action: 'updated', prev_outcome: prev.outcome });
|
|
2676
|
-
if (auditTrail.length > 20)
|
|
2677
|
-
auditTrail = auditTrail.slice(-20);
|
|
2678
|
-
isUpdate = true;
|
|
2679
|
-
// ── Contradiction detection ─────────────────────────────────────────
|
|
2680
|
-
const contradictionWarning = [];
|
|
2681
|
-
if (prev.outcome === 'success' && outcome === 'failure') {
|
|
2682
|
-
contradictionWarning.push(`⚠️ **Contradiction detected!** Existing lesson has outcome: \`success\`, but you're storing \`failure\`.`, `The existing "success" lesson will be preserved. Only the audit trail is updated.`, `If you meant to mark this as failed permanently, store a new lesson with a distinct topic slug.`);
|
|
2683
|
-
}
|
|
2684
|
-
else if (prev.outcome === 'failure' && outcome === 'success') {
|
|
2685
|
-
contradictionWarning.push(`✅ **Conflict resolved!** Previous lesson was \`failure\` — now overwriting with \`success\`.`);
|
|
2686
|
-
}
|
|
2687
|
-
if (contradictionWarning.length > 0) {
|
|
2688
|
-
// Store contradiction audit but don't block
|
|
2689
|
-
auditTrail[auditTrail.length - 1].action = 'contradiction-resolved';
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
catch { /* ignore parse error */ }
|
|
2693
|
-
}
|
|
2694
|
-
else {
|
|
2695
|
-
auditTrail = [{ ts, action: 'created' }];
|
|
2696
|
-
}
|
|
2697
|
-
// ── "I Was Wrong" Protocol — failure attribution ───────────────────────
|
|
2698
|
-
const iWasWrongWarning = [];
|
|
2699
|
-
if (outcome === 'failure') {
|
|
2700
|
-
// Search for related success lessons that might have prevented this failure
|
|
2701
|
-
const scanKeys = [];
|
|
2702
|
-
const scanStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 100 });
|
|
2703
|
-
await new Promise((res, rej) => {
|
|
2704
|
-
scanStream.on('data', (b) => scanKeys.push(...b));
|
|
2705
|
-
scanStream.on('end', res);
|
|
2706
|
-
scanStream.on('error', rej);
|
|
2707
|
-
});
|
|
2708
|
-
const topicWords = topic.split(/[:\-_]/).filter(w => w.length > 2);
|
|
2709
|
-
for (const k of scanKeys.slice(0, 50)) {
|
|
2710
|
-
const raw = await redis.get(k);
|
|
2711
|
-
if (!raw)
|
|
2712
|
-
continue;
|
|
2170
|
+
if (outcome === 'success') {
|
|
2171
|
+
const existing = await redis.get(`cachly:lesson:best:${topic}`);
|
|
2172
|
+
if (existing) {
|
|
2713
2173
|
try {
|
|
2714
|
-
const
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
const lWords = (l.topic ?? '').split(/[:\-_]/).filter(w => w.length > 2);
|
|
2718
|
-
const overlap = topicWords.filter(w => lWords.includes(w)).length;
|
|
2719
|
-
if (overlap >= 1 && l.topic !== topic) {
|
|
2720
|
-
iWasWrongWarning.push(`⚠️ **"I Was Wrong"**: lesson \`${l.topic}\` (success, ${l.severity ?? 'major'}) might have prevented this failure.`, ` → Use \`recall_best_solution(topic="${l.topic}")\` before next attempt.`, ` → To mark it critical: \`learn_from_attempts(topic="${l.topic}", ..., severity="critical")\``);
|
|
2721
|
-
break; // only show the most relevant match
|
|
2722
|
-
}
|
|
2174
|
+
const prev = JSON.parse(existing);
|
|
2175
|
+
recallCount = (prev.recall_count ?? 0); // preserve existing recall count
|
|
2176
|
+
isUpdate = true;
|
|
2723
2177
|
}
|
|
2724
|
-
catch { /*
|
|
2178
|
+
catch { /* ignore parse error */ }
|
|
2725
2179
|
}
|
|
2726
2180
|
}
|
|
2727
|
-
|
|
2728
|
-
for (const dep of depends_on) {
|
|
2729
|
-
const depKey = `cachly:dep:${dep}`;
|
|
2730
|
-
const existing = await redis.get(depKey);
|
|
2731
|
-
const depTopics = existing ? JSON.parse(existing) : [];
|
|
2732
|
-
if (!depTopics.includes(topic))
|
|
2733
|
-
depTopics.push(topic);
|
|
2734
|
-
await redis.set(depKey, JSON.stringify(depTopics));
|
|
2735
|
-
}
|
|
2736
|
-
const lessonObj = {
|
|
2181
|
+
const lesson = JSON.stringify({
|
|
2737
2182
|
topic,
|
|
2738
2183
|
outcome,
|
|
2739
2184
|
what_worked,
|
|
@@ -2743,34 +2188,17 @@ async function handleTool(name, args) {
|
|
|
2743
2188
|
file_paths,
|
|
2744
2189
|
commands,
|
|
2745
2190
|
tags,
|
|
2746
|
-
depends_on,
|
|
2747
|
-
...(author ? { author } : {}),
|
|
2748
2191
|
recall_count: recallCount,
|
|
2749
2192
|
ts,
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
version: 3,
|
|
2754
|
-
};
|
|
2755
|
-
const lesson = JSON.stringify(lessonObj);
|
|
2756
|
-
// Always append to the history list (audit log)
|
|
2193
|
+
version: 2,
|
|
2194
|
+
});
|
|
2195
|
+
// Always append to the history list
|
|
2757
2196
|
const listKey = `cachly:lessons:${topic}`;
|
|
2758
2197
|
await redis.rpush(listKey, lesson);
|
|
2759
|
-
// Update best key for success/partial
|
|
2198
|
+
// Update best key only for success/partial (not for failures)
|
|
2760
2199
|
if (outcome === 'success' || outcome === 'partial') {
|
|
2761
2200
|
await redis.set(`cachly:lesson:best:${topic}`, lesson);
|
|
2762
2201
|
}
|
|
2763
|
-
else if (!existingRaw) {
|
|
2764
|
-
await redis.set(`cachly:lesson:best:${topic}`, lesson);
|
|
2765
|
-
}
|
|
2766
|
-
// Track in decision log for session replay
|
|
2767
|
-
try {
|
|
2768
|
-
const dlKey = 'cachly:session:decision-log';
|
|
2769
|
-
const dlEntry = JSON.stringify({ ts, topic, outcome, what_worked: what_worked.slice(0, 120) });
|
|
2770
|
-
await redis.rpush(dlKey, dlEntry);
|
|
2771
|
-
await redis.ltrim(dlKey, -50, -1);
|
|
2772
|
-
}
|
|
2773
|
-
catch { /* non-critical */ }
|
|
2774
2202
|
const emoji = outcome === 'success' ? '✅' : outcome === 'partial' ? '⚠️' : '❌';
|
|
2775
2203
|
const sevEmoji = severity === 'critical' ? '🔴' : severity === 'major' ? '🟡' : '🟢';
|
|
2776
2204
|
const action = isUpdate ? 'updated' : 'stored';
|
|
@@ -2785,13 +2213,8 @@ async function handleTool(name, args) {
|
|
|
2785
2213
|
tags.length > 0 ? `**Tags:** ${tags.map(t => `#${t}`).join(' ')}` : '',
|
|
2786
2214
|
``,
|
|
2787
2215
|
isUpdate
|
|
2788
|
-
? `♻️ Updated (recall count: ${recallCount}
|
|
2216
|
+
? `♻️ Updated existing lesson (preserved recall count: ${recallCount})`
|
|
2789
2217
|
: `💡 Recall later with \`recall_best_solution(topic="${topic}")\``,
|
|
2790
|
-
depends_on.length > 0
|
|
2791
|
-
? `🔗 Depends on: ${depends_on.map(d => `\`${d}\``).join(', ')} → trace with \`trace_dependency\``
|
|
2792
|
-
: '',
|
|
2793
|
-
...templateWarnings,
|
|
2794
|
-
...iWasWrongWarning,
|
|
2795
2218
|
].filter(l => l !== '').join('\n');
|
|
2796
2219
|
}
|
|
2797
2220
|
case 'recall_best_solution': {
|
|
@@ -2800,36 +2223,40 @@ async function handleTool(name, args) {
|
|
|
2800
2223
|
// Try exact best-solution key first
|
|
2801
2224
|
const best = await redis.get(`cachly:lesson:best:${topic}`);
|
|
2802
2225
|
if (best) {
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
recall_count: (lesson.recall_count ?? 0) + 1,
|
|
2813
|
-
verified_at: new Date().toISOString(),
|
|
2814
|
-
confidence: 1.0,
|
|
2815
|
-
};
|
|
2226
|
+
let lesson;
|
|
2227
|
+
try {
|
|
2228
|
+
lesson = JSON.parse(best);
|
|
2229
|
+
}
|
|
2230
|
+
catch {
|
|
2231
|
+
return `⚠️ Lesson for \`${topic}\` could not be read (corrupted data). Use \`learn_from_attempts\` to overwrite it.`;
|
|
2232
|
+
}
|
|
2233
|
+
// Increment recall_count
|
|
2234
|
+
const updatedLesson = { ...lesson, recall_count: (lesson.recall_count ?? 0) + 1 };
|
|
2816
2235
|
await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(updatedLesson));
|
|
2236
|
+
// Fire-and-forget: track Magic Moment in Plausible + API engagement counter
|
|
2237
|
+
const isFirstRecall = (lesson.recall_count ?? 0) === 0;
|
|
2238
|
+
const recallEvent = isFirstRecall ? 'First Brain Recall' : 'Brain Recall Hit';
|
|
2239
|
+
fetch('https://analytics.cachly.dev/api/event', {
|
|
2240
|
+
method: 'POST',
|
|
2241
|
+
headers: { 'Content-Type': 'application/json', 'User-Agent': 'cachly-mcp/recall' },
|
|
2242
|
+
body: JSON.stringify({
|
|
2243
|
+
domain: 'cachly.dev', name: recallEvent,
|
|
2244
|
+
url: 'https://cachly.dev/mcp/recall', props: { topic },
|
|
2245
|
+
}),
|
|
2246
|
+
}).catch(() => { });
|
|
2247
|
+
if (JWT) {
|
|
2248
|
+
fetch(`${API_URL}/api/v1/telemetry/mcp`, {
|
|
2249
|
+
method: 'POST',
|
|
2250
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2251
|
+
body: JSON.stringify({ event: isFirstRecall ? 'first_recall' : 'recall', api_key: JWT }),
|
|
2252
|
+
}).catch(() => { });
|
|
2253
|
+
}
|
|
2817
2254
|
const sevEmoji = lesson.severity === 'critical' ? '🔴' : lesson.severity === 'major' ? '🟡' : lesson.severity ? '🟢' : '';
|
|
2818
|
-
const
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
// "Remember when..." — emotional header for lessons > 60 days old
|
|
2822
|
-
const ageFromStoreDays = (Date.now() - new Date(lesson.ts).getTime()) / 86400000;
|
|
2823
|
-
const rememberWhen = ageFromStoreDays > 60
|
|
2824
|
-
? `💭 _Remember when you solved this ${Math.round(ageFromStoreDays / 30)} months ago? Still works._`
|
|
2825
|
-
: '';
|
|
2826
|
-
// "Never Google This Again" — suggest pinning after 3rd recall
|
|
2827
|
-
const suggestPin = updatedLesson.recall_count === 3 && !lesson.pinned
|
|
2828
|
-
? `📌 **You've looked this up 3 times.** Consider pinning it for instant access: add \`pinned: true\` via \`learn_from_attempts\` to always surface it first.`
|
|
2829
|
-
: '';
|
|
2255
|
+
const sessionAgeMs = Date.now() - new Date(lesson.ts).getTime();
|
|
2256
|
+
const sessionAgeDays = Math.round(sessionAgeMs / 86_400_000);
|
|
2257
|
+
const ageLabel = sessionAgeDays === 0 ? 'today' : sessionAgeDays === 1 ? 'yesterday' : `${sessionAgeDays} days ago`;
|
|
2830
2258
|
return [
|
|
2831
|
-
|
|
2832
|
-
`${badge} **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}×`,
|
|
2259
|
+
`✅ **Best solution for \`${topic}\`** ${sevEmoji}${lesson.severity ? ` (${lesson.severity})` : ''} · recalled ${updatedLesson.recall_count}× · stored ${ageLabel}`,
|
|
2833
2260
|
``,
|
|
2834
2261
|
`**What worked:** ${lesson.what_worked}`,
|
|
2835
2262
|
lesson.what_failed ? `**What failed (avoid this):** ${lesson.what_failed}` : '',
|
|
@@ -2837,8 +2264,8 @@ async function handleTool(name, args) {
|
|
|
2837
2264
|
(lesson.file_paths ?? []).length > 0 ? `**Files:** ${(lesson.file_paths ?? []).map((f) => `\`${f}\``).join(', ')}` : '',
|
|
2838
2265
|
(lesson.commands ?? []).length > 0 ? `**Commands:** ${(lesson.commands ?? []).map((c) => `\`${c}\``).join(', ')}` : '',
|
|
2839
2266
|
(lesson.tags ?? []).length > 0 ? `**Tags:** ${(lesson.tags ?? []).map((t) => `#${t}`).join(' ')}` : '',
|
|
2840
|
-
|
|
2841
|
-
|
|
2267
|
+
``,
|
|
2268
|
+
`> 🧠 *Your AI remembered this from ${ageLabel} — persistent memory by [cachly.dev](https://cachly.dev)*`,
|
|
2842
2269
|
].filter(l => l !== '').join('\n');
|
|
2843
2270
|
}
|
|
2844
2271
|
// Partial match: scan all lesson keys for topic substring
|
|
@@ -2855,7 +2282,12 @@ async function handleTool(name, args) {
|
|
|
2855
2282
|
const histKey = `cachly:lessons:${topic}`;
|
|
2856
2283
|
const all = await redis.lrange(histKey, -3, -1);
|
|
2857
2284
|
if (all.length > 0) {
|
|
2858
|
-
const parsed = all.
|
|
2285
|
+
const parsed = all.flatMap(e => { try {
|
|
2286
|
+
return [JSON.parse(e)];
|
|
2287
|
+
}
|
|
2288
|
+
catch {
|
|
2289
|
+
return [];
|
|
2290
|
+
} });
|
|
2859
2291
|
const lines = parsed.map(p => `- ${p.outcome === 'success' ? '✅' : '❌'} ${p.what_worked.slice(0, 120)} (${new Date(p.ts).toLocaleDateString('de-DE')})`);
|
|
2860
2292
|
return `⚠️ No successful solution for \`${topic}\` yet. Last attempts:\n\n${lines.join('\n')}`;
|
|
2861
2293
|
}
|
|
@@ -2867,11 +2299,31 @@ async function handleTool(name, args) {
|
|
|
2867
2299
|
const raw = await redis.get(k);
|
|
2868
2300
|
if (!raw)
|
|
2869
2301
|
continue;
|
|
2870
|
-
|
|
2302
|
+
let lesson;
|
|
2303
|
+
try {
|
|
2304
|
+
lesson = JSON.parse(raw);
|
|
2305
|
+
}
|
|
2306
|
+
catch {
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2871
2309
|
results.push(`**\`${lesson.topic}\`** — ${lesson.what_worked.slice(0, 200)}`);
|
|
2872
2310
|
}
|
|
2873
2311
|
return `🔍 **Partial matches for \`${topic}\`:**\n\n${results.join('\n\n')}`;
|
|
2874
2312
|
}
|
|
2313
|
+
case 'forget_lesson': {
|
|
2314
|
+
const { instance_id, topic } = args;
|
|
2315
|
+
const redis = await getConnection(instance_id);
|
|
2316
|
+
const bestKey = `cachly:lesson:best:${topic}`;
|
|
2317
|
+
const histKey = `cachly:lessons:${topic}`;
|
|
2318
|
+
const [delBest, delHist] = await Promise.all([
|
|
2319
|
+
redis.del(bestKey),
|
|
2320
|
+
redis.del(histKey),
|
|
2321
|
+
]);
|
|
2322
|
+
if (delBest === 0 && delHist === 0) {
|
|
2323
|
+
return `📭 No lesson found for \`${topic}\` — nothing to delete.`;
|
|
2324
|
+
}
|
|
2325
|
+
return `🗑️ Lesson for \`${topic}\` deleted (best-solution: ${delBest > 0 ? 'removed' : 'not found'}, history: ${delHist > 0 ? 'removed' : 'not found'}). You can now store a corrected lesson with \`learn_from_attempts\`.`;
|
|
2326
|
+
}
|
|
2875
2327
|
case 'smart_recall': {
|
|
2876
2328
|
const { instance_id, query, threshold = 0.78, } = args;
|
|
2877
2329
|
const redis = await getConnection(instance_id);
|
|
@@ -2966,35 +2418,50 @@ async function handleTool(name, args) {
|
|
|
2966
2418
|
}
|
|
2967
2419
|
// ── get_api_status ────────────────────────────────────────────────────────
|
|
2968
2420
|
case 'get_api_status': {
|
|
2969
|
-
// Check health
|
|
2421
|
+
// Check API health and auth service in parallel
|
|
2422
|
+
const [healthResult, authServiceResult] = await Promise.allSettled([
|
|
2423
|
+
fetch(`${API_URL}/health`, { signal: AbortSignal.timeout(5000) }),
|
|
2424
|
+
fetch(`${AUTH_URL}/.well-known/openid-configuration`, { signal: AbortSignal.timeout(5000) }),
|
|
2425
|
+
]);
|
|
2970
2426
|
let healthStatus = 'unknown';
|
|
2971
|
-
|
|
2972
|
-
const healthRes =
|
|
2427
|
+
if (healthResult.status === 'fulfilled') {
|
|
2428
|
+
const healthRes = healthResult.value;
|
|
2973
2429
|
if (healthRes.ok) {
|
|
2974
|
-
const body = await healthRes.json();
|
|
2975
|
-
healthStatus =
|
|
2430
|
+
const body = await healthRes.json().catch(() => ({}));
|
|
2431
|
+
healthStatus = `✅ ${body.status ?? 'ok'}${body.db ? ` (db: ${body.db})` : ''}`;
|
|
2976
2432
|
}
|
|
2977
2433
|
else {
|
|
2978
|
-
healthStatus =
|
|
2434
|
+
healthStatus = `❌ HTTP ${healthRes.status}`;
|
|
2979
2435
|
}
|
|
2980
2436
|
}
|
|
2981
|
-
|
|
2982
|
-
healthStatus =
|
|
2437
|
+
else {
|
|
2438
|
+
healthStatus = `❌ unreachable: ${healthResult.reason?.message ?? 'timeout'}`;
|
|
2439
|
+
}
|
|
2440
|
+
let authSvcStatus = 'unknown';
|
|
2441
|
+
if (authServiceResult.status === 'fulfilled') {
|
|
2442
|
+
authSvcStatus = authServiceResult.value.ok
|
|
2443
|
+
? `✅ reachable (${AUTH_URL})`
|
|
2444
|
+
: `❌ HTTP ${authServiceResult.value.status} — auth service may be down`;
|
|
2445
|
+
}
|
|
2446
|
+
else {
|
|
2447
|
+
authSvcStatus = `❌ unreachable: ${authServiceResult.reason?.message ?? 'timeout'} — auth service may be down`;
|
|
2983
2448
|
}
|
|
2984
2449
|
// Check JWT / auth
|
|
2985
2450
|
if (!JWT) {
|
|
2986
2451
|
return [
|
|
2987
2452
|
`📡 **cachly API Status**`,
|
|
2988
2453
|
``,
|
|
2989
|
-
` 🌐 API:
|
|
2990
|
-
` 💓 Health: ${healthStatus}`,
|
|
2991
|
-
`
|
|
2454
|
+
` 🌐 API: ${API_URL}`,
|
|
2455
|
+
` 💓 API Health: ${healthStatus}`,
|
|
2456
|
+
` 🔐 Auth Service: ${authSvcStatus}`,
|
|
2457
|
+
` 🔑 JWT: ❌ CACHLY_JWT not set`,
|
|
2992
2458
|
``,
|
|
2993
|
-
`💡 Get your API token at https://cachly.dev/
|
|
2459
|
+
`💡 Get your API token at https://cachly.dev/settings`,
|
|
2994
2460
|
].join('\n');
|
|
2995
2461
|
}
|
|
2996
2462
|
// Decode JWT claims (inspection only, no verification)
|
|
2997
2463
|
let authInfo = '❌ invalid JWT format';
|
|
2464
|
+
let isExpired = false;
|
|
2998
2465
|
try {
|
|
2999
2466
|
const parts = JWT.split('.');
|
|
3000
2467
|
if (parts.length === 3) {
|
|
@@ -3003,32 +2470,41 @@ async function handleTool(name, args) {
|
|
|
3003
2470
|
const iss = payload.iss ?? '(unknown)';
|
|
3004
2471
|
const provider = iss.includes('keycloak') ? 'Keycloak' : 'OIDC';
|
|
3005
2472
|
const expTs = payload.exp ? new Date(payload.exp * 1000) : null;
|
|
3006
|
-
|
|
2473
|
+
isExpired = expTs ? expTs < new Date() : false;
|
|
3007
2474
|
authInfo = [
|
|
3008
|
-
|
|
2475
|
+
`${isExpired ? '⚠️ ' : '✅'} JWT decoded`,
|
|
3009
2476
|
` Sub (user ID): ${sub}`,
|
|
3010
2477
|
` Provider: ${provider}`,
|
|
3011
2478
|
` Issuer: ${iss}`,
|
|
3012
|
-
` Expires: ${expTs ? expTs.toISOString() : 'never'} ${
|
|
2479
|
+
` Expires: ${expTs ? expTs.toISOString() : 'never'} ${isExpired ? '⚠️ EXPIRED' : '✅'}`,
|
|
3013
2480
|
].join('\n');
|
|
3014
2481
|
}
|
|
3015
2482
|
}
|
|
3016
2483
|
catch {
|
|
3017
2484
|
authInfo = '❌ JWT decode failed – check CACHLY_JWT format';
|
|
3018
2485
|
}
|
|
3019
|
-
|
|
2486
|
+
const lines = [
|
|
3020
2487
|
`📡 **cachly API Status**`,
|
|
3021
2488
|
``,
|
|
3022
|
-
` 🌐 API:
|
|
3023
|
-
` 💓 Health:
|
|
2489
|
+
` 🌐 API: ${API_URL}`,
|
|
2490
|
+
` 💓 API Health: ${healthStatus}`,
|
|
2491
|
+
` 🔐 Auth Service: ${authSvcStatus}`,
|
|
3024
2492
|
``,
|
|
3025
|
-
`🔑 **Auth:**`,
|
|
2493
|
+
`🔑 **Auth (JWT):**`,
|
|
3026
2494
|
authInfo,
|
|
3027
|
-
]
|
|
2495
|
+
];
|
|
2496
|
+
if (isExpired) {
|
|
2497
|
+
lines.push(``, `⚠️ Your token is expired — get a new one at https://cachly.dev/settings`);
|
|
2498
|
+
}
|
|
2499
|
+
if (authSvcStatus.startsWith('❌')) {
|
|
2500
|
+
lines.push(``, `⚠️ Auth service appears down. Registration and login on cachly.dev may be unavailable.`);
|
|
2501
|
+
lines.push(` Contact support@cachly.dev if this persists.`);
|
|
2502
|
+
}
|
|
2503
|
+
return lines.join('\n');
|
|
3028
2504
|
}
|
|
3029
2505
|
// ── session_start ─────────────────────────────────────────────────────────
|
|
3030
2506
|
case 'session_start': {
|
|
3031
|
-
const { instance_id, focus = ''
|
|
2507
|
+
const { instance_id, focus = '' } = args;
|
|
3032
2508
|
const redis = await getConnection(instance_id);
|
|
3033
2509
|
// 1. Scan all best-solution lessons
|
|
3034
2510
|
const lessonKeys = [];
|
|
@@ -3074,86 +2550,13 @@ async function handleTool(name, args) {
|
|
|
3074
2550
|
? lessons.filter(l => focusTerms.some(term => l.topic.toLowerCase().includes(term) ||
|
|
3075
2551
|
(l.tags ?? []).some((t) => t.toLowerCase().includes(term))))
|
|
3076
2552
|
: [];
|
|
3077
|
-
// 6.
|
|
3078
|
-
let streakDays = 0;
|
|
3079
|
-
let streakRecord = 0;
|
|
3080
|
-
let streakMessage = '';
|
|
3081
|
-
try {
|
|
3082
|
-
const streakRaw = await redis.get('cachly:streak:current');
|
|
3083
|
-
const streak = streakRaw ? JSON.parse(streakRaw) : null;
|
|
3084
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
3085
|
-
if (streak) {
|
|
3086
|
-
const lastDate = streak.last_date;
|
|
3087
|
-
const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
|
|
3088
|
-
if (lastDate === today) {
|
|
3089
|
-
// Already counted today
|
|
3090
|
-
streakDays = streak.days;
|
|
3091
|
-
streakRecord = streak.record;
|
|
3092
|
-
}
|
|
3093
|
-
else if (lastDate === yesterday) {
|
|
3094
|
-
// Continuing streak
|
|
3095
|
-
streakDays = streak.days + 1;
|
|
3096
|
-
streakRecord = Math.max(streakDays, streak.record);
|
|
3097
|
-
await redis.set('cachly:streak:current', JSON.stringify({ days: streakDays, last_date: today, record: streakRecord }));
|
|
3098
|
-
}
|
|
3099
|
-
else {
|
|
3100
|
-
// Streak broken
|
|
3101
|
-
streakDays = 1;
|
|
3102
|
-
streakRecord = streak.record;
|
|
3103
|
-
await redis.set('cachly:streak:current', JSON.stringify({ days: 1, last_date: today, record: streakRecord }));
|
|
3104
|
-
}
|
|
3105
|
-
}
|
|
3106
|
-
else {
|
|
3107
|
-
// First session ever
|
|
3108
|
-
streakDays = 1;
|
|
3109
|
-
streakRecord = 1;
|
|
3110
|
-
await redis.set('cachly:streak:current', JSON.stringify({ days: 1, last_date: today, record: 1 }));
|
|
3111
|
-
}
|
|
3112
|
-
if (streakDays >= 7)
|
|
3113
|
-
streakMessage = `🔥 **${streakDays}-day streak!** ${streakDays === streakRecord ? ' New record!' : `Best: ${streakRecord}d`}`;
|
|
3114
|
-
else if (streakDays > 1)
|
|
3115
|
-
streakMessage = `🔥 ${streakDays}-day streak`;
|
|
3116
|
-
}
|
|
3117
|
-
catch { /* non-critical */ }
|
|
3118
|
-
// 7. Save session start marker
|
|
2553
|
+
// 6. Save session start marker
|
|
3119
2554
|
await redis.set('cachly:session:current', JSON.stringify({
|
|
3120
2555
|
started: new Date().toISOString(),
|
|
3121
2556
|
focus,
|
|
3122
2557
|
}), 'EX', 86400); // auto-expire after 24h if session_end never called
|
|
3123
2558
|
// ── Build briefing ──────────────────────────────────────────────────────
|
|
3124
2559
|
const lines = ['🧠 **Session Briefing**', ''];
|
|
3125
|
-
if (streakMessage)
|
|
3126
|
-
lines.push(streakMessage, '');
|
|
3127
|
-
// Handoff from previous window (if any)
|
|
3128
|
-
const handoffRaw = await redis.get('cachly:session:handoff');
|
|
3129
|
-
if (handoffRaw) {
|
|
3130
|
-
try {
|
|
3131
|
-
const handoff = JSON.parse(handoffRaw);
|
|
3132
|
-
const ago = Math.round((Date.now() - new Date(handoff.ts).getTime()) / 60000);
|
|
3133
|
-
const agoStr = ago < 60 ? `${ago}m ago` : ago < 1440 ? `${Math.round(ago / 60)}h ago` : `${Math.round(ago / 1440)}d ago`;
|
|
3134
|
-
lines.push(`🤝 **Handoff from previous window** (${agoStr}):`);
|
|
3135
|
-
if (handoff.context_summary)
|
|
3136
|
-
lines.push(` ${handoff.context_summary}`);
|
|
3137
|
-
if (handoff.remaining_tasks.length > 0) {
|
|
3138
|
-
lines.push(` ⏳ **Remaining tasks:**`);
|
|
3139
|
-
for (const t of handoff.remaining_tasks)
|
|
3140
|
-
lines.push(` - ${t}`);
|
|
3141
|
-
}
|
|
3142
|
-
if (handoff.completed_tasks.length > 0) {
|
|
3143
|
-
lines.push(` ✅ **Already done:** ${handoff.completed_tasks.join(', ')}`);
|
|
3144
|
-
}
|
|
3145
|
-
const brokenFiles = (handoff.files_changed ?? []).filter(f => f.status === 'broken' || f.status === 'partial');
|
|
3146
|
-
if (brokenFiles.length > 0) {
|
|
3147
|
-
lines.push(` ⚠️ **Needs fix:** ${brokenFiles.map(f => `\`${f.path}\` (${f.status}${f.description ? ': ' + f.description : ''})`).join(', ')}`);
|
|
3148
|
-
}
|
|
3149
|
-
if (handoff.blocked_on)
|
|
3150
|
-
lines.push(` 🚫 **Blocked on:** ${handoff.blocked_on}`);
|
|
3151
|
-
if (handoff.instructions)
|
|
3152
|
-
lines.push(` 📝 **Instructions:** ${handoff.instructions}`);
|
|
3153
|
-
lines.push('');
|
|
3154
|
-
}
|
|
3155
|
-
catch { /* ignore corrupt handoff */ }
|
|
3156
|
-
}
|
|
3157
2560
|
// Last session
|
|
3158
2561
|
if (lastSession) {
|
|
3159
2562
|
const ago = Math.round((Date.now() - new Date(lastSession.ts).getTime()) / 60000);
|
|
@@ -3201,137 +2604,11 @@ async function handleTool(name, args) {
|
|
|
3201
2604
|
}
|
|
3202
2605
|
lines.push('');
|
|
3203
2606
|
}
|
|
3204
|
-
// ── Stale / low-confidence lessons (confidence decay) ─────────────────
|
|
3205
|
-
const staleSuccessLessons = lessons.filter(l => {
|
|
3206
|
-
if (l.outcome !== 'success' && l.outcome !== 'partial')
|
|
3207
|
-
return false;
|
|
3208
|
-
return calculateConfidence(l) < CONFIDENCE_WARN_VALUE;
|
|
3209
|
-
});
|
|
3210
|
-
if (staleSuccessLessons.length > 0) {
|
|
3211
|
-
lines.push(`🔴 **Stale lessons** (not recalled in >${CONFIDENCE_WARN_DAYS}d — verify before applying):`);
|
|
3212
|
-
for (const l of staleSuccessLessons.slice(0, 4)) {
|
|
3213
|
-
const conf = calculateConfidence(l);
|
|
3214
|
-
const ageDays = Math.round((Date.now() - new Date(l.verified_at ?? l.ts).getTime()) / 86400000);
|
|
3215
|
-
const flag = conf < CONFIDENCE_STALE_VALUE ? '🔴' : '⚠️';
|
|
3216
|
-
lines.push(` ${flag} \`${l.topic}\` — ${ageDays}d stale, ${(conf * 100).toFixed(0)}% confidence`);
|
|
3217
|
-
}
|
|
3218
|
-
lines.push(` _Run \`recall_best_solution\` on these to reset their confidence clock._`);
|
|
3219
|
-
lines.push('');
|
|
3220
|
-
}
|
|
3221
|
-
// ── Session Replay: show last session's decision log ──────────────────
|
|
3222
|
-
const lastSessionAny = lastSession;
|
|
3223
|
-
if (lastSessionAny?.decision_log?.length) {
|
|
3224
|
-
const dl = lastSessionAny.decision_log;
|
|
3225
|
-
const successes = dl.filter(d => d.outcome === 'success');
|
|
3226
|
-
const failures = dl.filter(d => d.outcome === 'failure');
|
|
3227
|
-
lines.push(`🎬 **Last session decisions** (${dl.length} lessons stored):`);
|
|
3228
|
-
if (successes.length > 0)
|
|
3229
|
-
lines.push(` ✅ Worked: ${successes.slice(0, 3).map(d => `\`${d.topic}\``).join(', ')}`);
|
|
3230
|
-
if (failures.length > 0)
|
|
3231
|
-
lines.push(` ❌ Failed: ${failures.slice(0, 3).map(d => `\`${d.topic}\``).join(', ')}`);
|
|
3232
|
-
lines.push('');
|
|
3233
|
-
}
|
|
3234
|
-
// ── 🔮 Predictive Pre-Warning — intent-based danger detection ────────────
|
|
3235
|
-
// Fires BEFORE work starts. If focus area has known failure patterns → warn loudly.
|
|
3236
|
-
if (focusTerms.length > 0) {
|
|
3237
|
-
const dangerLessons = lessons.filter(l => {
|
|
3238
|
-
if (l.outcome === 'success')
|
|
3239
|
-
return false;
|
|
3240
|
-
const topicCategory = l.topic.split(':')[0];
|
|
3241
|
-
return focusTerms.some(term => l.topic.toLowerCase().includes(term) ||
|
|
3242
|
-
topicCategory === term ||
|
|
3243
|
-
(l.tags ?? []).some((t) => t.toLowerCase() === term));
|
|
3244
|
-
});
|
|
3245
|
-
if (dangerLessons.length >= 1) {
|
|
3246
|
-
// Insert warning block right after the title line (index 1 = blank line after title)
|
|
3247
|
-
const warning = [
|
|
3248
|
-
`🚨 **PRE-WARNING** — Read this BEFORE starting:`,
|
|
3249
|
-
` Known pitfalls for **"${focus}"** (${dangerLessons.length} past failure${dangerLessons.length > 1 ? 's' : ''}):`,
|
|
3250
|
-
...dangerLessons.slice(0, 3).map(l => ` ❌ \`${l.topic}\` — ${(l.what_failed ?? l.what_worked).slice(0, 80)}`),
|
|
3251
|
-
'',
|
|
3252
|
-
];
|
|
3253
|
-
lines.splice(2, 0, ...warning); // after '🧠 **Session Briefing**' + empty line
|
|
3254
|
-
}
|
|
3255
|
-
}
|
|
3256
|
-
// ── 👥 Team Telepathy — what teammates learned this week ─────────────────
|
|
3257
|
-
if (author) {
|
|
3258
|
-
const oneWeekAgo = Date.now() - 7 * 86_400_000;
|
|
3259
|
-
const teamLessons = lessons.filter(l => l.author && l.author !== author && new Date(l.ts).getTime() > oneWeekAgo);
|
|
3260
|
-
if (teamLessons.length > 0) {
|
|
3261
|
-
// Group by author
|
|
3262
|
-
const byAuthor = new Map();
|
|
3263
|
-
for (const l of teamLessons) {
|
|
3264
|
-
const a = l.author;
|
|
3265
|
-
if (!byAuthor.has(a))
|
|
3266
|
-
byAuthor.set(a, []);
|
|
3267
|
-
byAuthor.get(a).push(l);
|
|
3268
|
-
}
|
|
3269
|
-
lines.push(`👥 **Team this week** (${teamLessons.length} lesson${teamLessons.length > 1 ? 's' : ''} from teammates):`);
|
|
3270
|
-
for (const [teamAuthor, tls] of byAuthor) {
|
|
3271
|
-
lines.push(` 👤 **${teamAuthor}**:`);
|
|
3272
|
-
for (const l of tls.slice(0, 3)) {
|
|
3273
|
-
const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
|
|
3274
|
-
lines.push(` ${emoji} \`${l.topic}\` — ${l.what_worked.slice(0, 80)}`);
|
|
3275
|
-
}
|
|
3276
|
-
if (tls.length > 3)
|
|
3277
|
-
lines.push(` … and ${tls.length - 3} more`);
|
|
3278
|
-
}
|
|
3279
|
-
lines.push('');
|
|
3280
|
-
}
|
|
3281
|
-
}
|
|
3282
|
-
// ── 💎 Memory Crystal — compressed wisdom from old sessions ──────────────
|
|
3283
|
-
try {
|
|
3284
|
-
const crystalRaw = await redis.get('cachly:crystal:latest');
|
|
3285
|
-
if (crystalRaw) {
|
|
3286
|
-
const crystal = JSON.parse(crystalRaw);
|
|
3287
|
-
const crystalAge = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
|
|
3288
|
-
if (crystalAge <= 90) {
|
|
3289
|
-
lines.push(`💎 **Memory Crystal** (${crystal.label} · ${crystal.session_count} sessions compressed):`);
|
|
3290
|
-
for (const p of crystal.top_patterns.slice(0, 3)) {
|
|
3291
|
-
lines.push(` • **${p.category}** (${p.count}×): ${p.insight.slice(0, 90)}`);
|
|
3292
|
-
}
|
|
3293
|
-
lines.push('');
|
|
3294
|
-
}
|
|
3295
|
-
}
|
|
3296
|
-
}
|
|
3297
|
-
catch { /* non-critical */ }
|
|
3298
|
-
// ── 🗺️ Roadmap — open items at session start ────────────────────────────
|
|
3299
|
-
try {
|
|
3300
|
-
const roadmapAll = await redis.hgetall(`cachly:roadmap:${instance_id}`);
|
|
3301
|
-
if (roadmapAll && Object.keys(roadmapAll).length > 0) {
|
|
3302
|
-
const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
3303
|
-
const PRIORITY_ICON = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
|
|
3304
|
-
const openStatuses = new Set(['planned', 'in-progress', 'blocked']);
|
|
3305
|
-
const allItems = Object.values(roadmapAll).map(v => JSON.parse(v));
|
|
3306
|
-
const openItems = allItems
|
|
3307
|
-
.filter(i => openStatuses.has(i.status))
|
|
3308
|
-
.sort((a, b) => {
|
|
3309
|
-
if (a.status === 'in-progress' && b.status !== 'in-progress')
|
|
3310
|
-
return -1;
|
|
3311
|
-
if (b.status === 'in-progress' && a.status !== 'in-progress')
|
|
3312
|
-
return 1;
|
|
3313
|
-
return (PRIORITY_ORDER[a.priority] ?? 99) - (PRIORITY_ORDER[b.priority] ?? 99);
|
|
3314
|
-
});
|
|
3315
|
-
const doneCount = allItems.filter(i => i.status === 'done').length;
|
|
3316
|
-
if (openItems.length > 0) {
|
|
3317
|
-
lines.push(`🗺️ **Roadmap** (${openItems.length} open · ${doneCount} done):`);
|
|
3318
|
-
for (const it of openItems.slice(0, 5)) {
|
|
3319
|
-
const statusIcon = it.status === 'in-progress' ? '⚡' : it.status === 'blocked' ? '🚫' : '📋';
|
|
3320
|
-
lines.push(` ${statusIcon} ${PRIORITY_ICON[it.priority] ?? '⚪'} \`${it.id}\` **${it.title}**`);
|
|
3321
|
-
}
|
|
3322
|
-
if (openItems.length > 5)
|
|
3323
|
-
lines.push(` … and ${openItems.length - 5} more`);
|
|
3324
|
-
lines.push(` _Use \`roadmap_next\` for the top priority item · \`roadmap_list\` for full view_`);
|
|
3325
|
-
lines.push('');
|
|
3326
|
-
}
|
|
3327
|
-
}
|
|
3328
|
-
}
|
|
3329
|
-
catch { /* non-critical */ }
|
|
3330
2607
|
return lines.join('\n');
|
|
3331
2608
|
}
|
|
3332
2609
|
// ── session_end ───────────────────────────────────────────────────────────
|
|
3333
2610
|
case 'session_end': {
|
|
3334
|
-
const { instance_id, summary, files_changed = [], lessons_learned,
|
|
2611
|
+
const { instance_id, summary, files_changed = [], lessons_learned, } = args;
|
|
3335
2612
|
const redis = await getConnection(instance_id);
|
|
3336
2613
|
const now = new Date();
|
|
3337
2614
|
// Calculate duration from session_start marker
|
|
@@ -3344,21 +2621,12 @@ async function handleTool(name, args) {
|
|
|
3344
2621
|
}
|
|
3345
2622
|
catch { /* ignore */ }
|
|
3346
2623
|
}
|
|
3347
|
-
// ── Session Replay: capture decision log ─────────────────────────────
|
|
3348
|
-
let decisionLog = [];
|
|
3349
|
-
try {
|
|
3350
|
-
const dlEntries = await redis.lrange('cachly:session:decision-log', 0, -1);
|
|
3351
|
-
decisionLog = dlEntries.map(e => JSON.parse(e));
|
|
3352
|
-
await redis.del('cachly:session:decision-log');
|
|
3353
|
-
}
|
|
3354
|
-
catch { /* non-critical */ }
|
|
3355
2624
|
const sessionRecord = {
|
|
3356
2625
|
ts: now.toISOString(),
|
|
3357
2626
|
summary,
|
|
3358
2627
|
files_changed,
|
|
3359
2628
|
...(lessons_learned !== undefined ? { lessons_learned } : {}),
|
|
3360
2629
|
...(durationMin !== undefined ? { duration_min: durationMin } : {}),
|
|
3361
|
-
...(decisionLog.length > 0 ? { decision_log: decisionLog } : {}),
|
|
3362
2630
|
};
|
|
3363
2631
|
// Save as "last session"
|
|
3364
2632
|
await redis.set('cachly:session:last', JSON.stringify(sessionRecord));
|
|
@@ -3368,131 +2636,6 @@ async function handleTool(name, args) {
|
|
|
3368
2636
|
await redis.expire('cachly:session:history', 90 * 86400);
|
|
3369
2637
|
// Clean up current session marker
|
|
3370
2638
|
await redis.del('cachly:session:current');
|
|
3371
|
-
// ── AUTO-LEARN from session summary (no manual call needed) ─────────────
|
|
3372
|
-
// Parse the summary for actionable lessons and store them automatically.
|
|
3373
|
-
const autoLearned = [];
|
|
3374
|
-
try {
|
|
3375
|
-
// Extract key sentences from the summary that contain action verbs
|
|
3376
|
-
const actionVerbs = /\b(fixed|deployed|added|removed|refactored|migrated|updated|resolved|implemented|improved|optimized|configured|created|deleted|disabled|enabled|discovered|found|learned|debugged|patched|upgraded|installed|tested|built|rewrote|moved|renamed|split|merged|extracted)\b/i;
|
|
3377
|
-
const sentences = summary
|
|
3378
|
-
.split(/[.!\n]+/)
|
|
3379
|
-
.map(s => s.trim())
|
|
3380
|
-
.filter(s => s.length > 20 && actionVerbs.test(s));
|
|
3381
|
-
for (const sentence of sentences.slice(0, 6)) {
|
|
3382
|
-
// Build a topic slug from the first meaningful words
|
|
3383
|
-
const words = sentence.toLowerCase()
|
|
3384
|
-
.replace(/[^a-z0-9\s]/g, ' ')
|
|
3385
|
-
.split(/\s+/)
|
|
3386
|
-
.filter(w => w.length > 3 && !['that', 'this', 'with', 'from', 'have', 'been', 'were', 'they', 'then', 'when', 'also', 'into', 'will', 'would', 'could', 'should'].includes(w));
|
|
3387
|
-
const slug = words.slice(0, 4).join('-');
|
|
3388
|
-
if (!slug)
|
|
3389
|
-
continue;
|
|
3390
|
-
const topic = `auto:${slug}`;
|
|
3391
|
-
const key = `cachly:lesson:best:${topic}`;
|
|
3392
|
-
// Don't overwrite existing successful lessons
|
|
3393
|
-
const existing = await redis.get(key);
|
|
3394
|
-
if (existing) {
|
|
3395
|
-
try {
|
|
3396
|
-
const ex = JSON.parse(existing);
|
|
3397
|
-
if (ex.outcome === 'success')
|
|
3398
|
-
continue;
|
|
3399
|
-
}
|
|
3400
|
-
catch { /* ignore */ }
|
|
3401
|
-
}
|
|
3402
|
-
const lesson = {
|
|
3403
|
-
topic,
|
|
3404
|
-
outcome: 'success',
|
|
3405
|
-
what_worked: sentence,
|
|
3406
|
-
context: `Auto-learned from session summary. Full summary: ${summary.slice(0, 300)}`,
|
|
3407
|
-
severity: 'minor',
|
|
3408
|
-
ts: now.toISOString(),
|
|
3409
|
-
recall_count: 0,
|
|
3410
|
-
auto_learned: true,
|
|
3411
|
-
session_ts: now.toISOString(),
|
|
3412
|
-
version: 2,
|
|
3413
|
-
};
|
|
3414
|
-
await redis.set(key, JSON.stringify(lesson));
|
|
3415
|
-
// 90-day TTL for auto-learned lessons
|
|
3416
|
-
await redis.expire(key, 90 * 86400);
|
|
3417
|
-
autoLearned.push(topic);
|
|
3418
|
-
}
|
|
3419
|
-
// Also store a lesson per changed file area if files were changed
|
|
3420
|
-
if (files_changed.length > 0) {
|
|
3421
|
-
const areas = [...new Set(files_changed.map(f => f.split('/').slice(0, 2).join('/')))].slice(0, 3);
|
|
3422
|
-
for (const area of areas) {
|
|
3423
|
-
const slug = area.replace(/[^a-z0-9]/gi, '-').toLowerCase().replace(/-+/g, '-').slice(0, 30);
|
|
3424
|
-
const topic = `auto:changed:${slug}`;
|
|
3425
|
-
const key = `cachly:lesson:best:${topic}`;
|
|
3426
|
-
const lesson = {
|
|
3427
|
-
topic,
|
|
3428
|
-
outcome: 'success',
|
|
3429
|
-
what_worked: `Files changed in ${area}: ${files_changed.filter(f => f.startsWith(area.split('/')[0])).slice(0, 5).join(', ')}`,
|
|
3430
|
-
context: summary.slice(0, 200),
|
|
3431
|
-
severity: 'minor',
|
|
3432
|
-
ts: now.toISOString(),
|
|
3433
|
-
recall_count: 0,
|
|
3434
|
-
auto_learned: true,
|
|
3435
|
-
version: 2,
|
|
3436
|
-
};
|
|
3437
|
-
await redis.set(key, JSON.stringify(lesson));
|
|
3438
|
-
await redis.expire(key, 90 * 86400);
|
|
3439
|
-
autoLearned.push(topic);
|
|
3440
|
-
}
|
|
3441
|
-
}
|
|
3442
|
-
}
|
|
3443
|
-
catch { /* auto-learn errors must never break session_end */ }
|
|
3444
|
-
// ── 🌿 Ambient Git Learning ────────────────────────────────────────────────
|
|
3445
|
-
// Read git commits since session start → auto-learn each meaningful commit.
|
|
3446
|
-
const ambientLearned = [];
|
|
3447
|
-
if (workspace_path) {
|
|
3448
|
-
try {
|
|
3449
|
-
// Get the session start time (stored by session_start)
|
|
3450
|
-
const sessionStartTs = currentRaw
|
|
3451
|
-
? (() => { try {
|
|
3452
|
-
return JSON.parse(currentRaw).started ?? '';
|
|
3453
|
-
}
|
|
3454
|
-
catch {
|
|
3455
|
-
return '';
|
|
3456
|
-
} })()
|
|
3457
|
-
: '';
|
|
3458
|
-
const sinceArg = sessionStartTs ? `--since="${sessionStartTs}"` : '--since="1 hour ago"';
|
|
3459
|
-
const gitOut = execSync(`git -C "${workspace_path}" log ${sinceArg} --oneline --format="%H|||%s|||%ai"`, { timeout: 5000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
3460
|
-
if (gitOut) {
|
|
3461
|
-
const commitActionRe = /\b(fix|add|remove|refactor|migrate|update|resolve|implement|improve|optimize|configure|create|delete|disable|enable|debug|patch|upgrade|build|rewrite|deploy|feat|chore|docs|test|perf|ci)\b/i;
|
|
3462
|
-
for (const line of gitOut.split('\n').slice(0, 10)) {
|
|
3463
|
-
const [hash, msg, dateStr] = line.split('|||');
|
|
3464
|
-
if (!msg || !commitActionRe.test(msg))
|
|
3465
|
-
continue;
|
|
3466
|
-
const slug = msg
|
|
3467
|
-
.toLowerCase().replace(/^(fix|feat|chore|docs|test|ci|perf|refactor|build|revert)[:(\s]/i, '')
|
|
3468
|
-
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
|
|
3469
|
-
if (!slug)
|
|
3470
|
-
continue;
|
|
3471
|
-
const topic = `git:${slug}`;
|
|
3472
|
-
const key = `cachly:lesson:best:${topic}`;
|
|
3473
|
-
const existing = await redis.get(key);
|
|
3474
|
-
if (existing)
|
|
3475
|
-
continue; // don't overwrite existing
|
|
3476
|
-
const commitLesson = {
|
|
3477
|
-
topic,
|
|
3478
|
-
outcome: 'success',
|
|
3479
|
-
what_worked: msg.slice(0, 200),
|
|
3480
|
-
context: `Auto-learned from git commit ${(hash ?? '').slice(0, 7)} at ${dateStr ?? ''} in ${workspace_path}`,
|
|
3481
|
-
severity: 'minor',
|
|
3482
|
-
ts: now.toISOString(),
|
|
3483
|
-
recall_count: 0,
|
|
3484
|
-
auto_learned: true,
|
|
3485
|
-
source: 'ambient-git',
|
|
3486
|
-
version: 3,
|
|
3487
|
-
};
|
|
3488
|
-
await redis.set(key, JSON.stringify(commitLesson));
|
|
3489
|
-
await redis.expire(key, 60 * 86400); // 60 day TTL for git lessons
|
|
3490
|
-
ambientLearned.push(topic);
|
|
3491
|
-
}
|
|
3492
|
-
}
|
|
3493
|
-
}
|
|
3494
|
-
catch { /* git not available or not a repo — silent skip */ }
|
|
3495
|
-
}
|
|
3496
2639
|
const durationStr = durationMin !== undefined ? ` · ${durationMin} min` : '';
|
|
3497
2640
|
return [
|
|
3498
2641
|
`✅ **Session saved**${durationStr}`,
|
|
@@ -3500,46 +2643,10 @@ async function handleTool(name, args) {
|
|
|
3500
2643
|
`📋 **Summary:** ${summary}`,
|
|
3501
2644
|
files_changed.length > 0 ? `📁 **Files changed:** ${files_changed.map(f => `\`${f}\``).join(', ')}` : '',
|
|
3502
2645
|
lessons_learned !== undefined ? `🧠 **Lessons stored:** ${lessons_learned}` : '',
|
|
3503
|
-
autoLearned.length > 0 ? `🤖 **Auto-learned:** ${autoLearned.length} lessons extracted from summary (${autoLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${autoLearned.length > 3 ? '…' : ''})` : '',
|
|
3504
|
-
ambientLearned.length > 0 ? `🌿 **Ambient git learning:** ${ambientLearned.length} commit${ambientLearned.length > 1 ? 's' : ''} auto-learned (${ambientLearned.slice(0, 3).map(t => `\`${t}\``).join(', ')}${ambientLearned.length > 3 ? '…' : ''})` : '',
|
|
3505
2646
|
``,
|
|
3506
2647
|
`💡 Next session: \`session_start(focus="...")\` to see this summary.`,
|
|
3507
2648
|
].filter(l => l !== '').join('\n');
|
|
3508
2649
|
}
|
|
3509
|
-
// ── session_handoff — cross-window continuity ─────────────────────────────
|
|
3510
|
-
case 'session_handoff': {
|
|
3511
|
-
const { instance_id, completed_tasks = [], remaining_tasks = [], files_changed = [], instructions = '', context_summary = '', blocked_on = '', } = args;
|
|
3512
|
-
const redis = await getConnection(instance_id);
|
|
3513
|
-
const now = new Date();
|
|
3514
|
-
const handoff = {
|
|
3515
|
-
ts: now.toISOString(),
|
|
3516
|
-
completed_tasks,
|
|
3517
|
-
remaining_tasks,
|
|
3518
|
-
files_changed,
|
|
3519
|
-
instructions,
|
|
3520
|
-
context_summary,
|
|
3521
|
-
blocked_on,
|
|
3522
|
-
};
|
|
3523
|
-
// Store handoff — never expires until next handoff overwrites it
|
|
3524
|
-
await redis.set('cachly:session:handoff', JSON.stringify(handoff));
|
|
3525
|
-
// Also append to history
|
|
3526
|
-
await redis.lpush('cachly:session:handoff:history', JSON.stringify(handoff));
|
|
3527
|
-
await redis.ltrim('cachly:session:handoff:history', 0, 19);
|
|
3528
|
-
const totalTasks = completed_tasks.length + remaining_tasks.length;
|
|
3529
|
-
const pct = totalTasks > 0 ? Math.round((completed_tasks.length / totalTasks) * 100) : 0;
|
|
3530
|
-
const brokenFiles = files_changed.filter(f => f.status === 'broken' || f.status === 'partial');
|
|
3531
|
-
return [
|
|
3532
|
-
`🤝 **Handoff saved** — ${completed_tasks.length}/${totalTasks} tasks done (${pct}%)`,
|
|
3533
|
-
``,
|
|
3534
|
-
completed_tasks.length > 0 ? `✅ **Completed:**\n${completed_tasks.map(t => ` - ${t}`).join('\n')}` : '',
|
|
3535
|
-
remaining_tasks.length > 0 ? `\n⏳ **Remaining for next window:**\n${remaining_tasks.map(t => ` - ${t}`).join('\n')}` : '',
|
|
3536
|
-
brokenFiles.length > 0 ? `\n⚠️ **Needs attention:** ${brokenFiles.map(f => `\`${f.path}\` (${f.status})`).join(', ')}` : '',
|
|
3537
|
-
blocked_on ? `\n🚫 **Blocked on:** ${blocked_on}` : '',
|
|
3538
|
-
instructions ? `\n📝 **Instructions:** ${instructions}` : '',
|
|
3539
|
-
``,
|
|
3540
|
-
`💡 The next \`session_start\` will include this handoff automatically.`,
|
|
3541
|
-
].filter(l => l !== '').join('\n');
|
|
3542
|
-
}
|
|
3543
2650
|
// ── auto_learn_session ────────────────────────────────────────────────────
|
|
3544
2651
|
case 'auto_learn_session': {
|
|
3545
2652
|
const { instance_id, observations } = args;
|
|
@@ -3729,142 +2836,9 @@ async function handleTool(name, args) {
|
|
|
3729
2836
|
}
|
|
3730
2837
|
return lines.join('\n');
|
|
3731
2838
|
}
|
|
3732
|
-
// ── team_synthesize — Team Brain Synthesis ────────────────────────────────
|
|
3733
|
-
case 'team_synthesize': {
|
|
3734
|
-
const { instance_id, topic } = args;
|
|
3735
|
-
const redis = await getConnection(instance_id);
|
|
3736
|
-
// Load history list for this topic (all authors' contributions)
|
|
3737
|
-
const listKey = `cachly:lessons:${topic}`;
|
|
3738
|
-
const all = await redis.lrange(listKey, 0, -1);
|
|
3739
|
-
if (all.length < 2) {
|
|
3740
|
-
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}", ...)\`.`;
|
|
3741
|
-
}
|
|
3742
|
-
const entries = all.map(r => { try {
|
|
3743
|
-
return JSON.parse(r);
|
|
3744
|
-
}
|
|
3745
|
-
catch {
|
|
3746
|
-
return null;
|
|
3747
|
-
} }).filter((e) => e !== null);
|
|
3748
|
-
// Group by outcome
|
|
3749
|
-
const successes = entries.filter(e => e.outcome === 'success');
|
|
3750
|
-
const failures = entries.filter(e => e.outcome === 'failure');
|
|
3751
|
-
const partials = entries.filter(e => e.outcome === 'partial');
|
|
3752
|
-
const authors = [...new Set(entries.map(e => e.author).filter(Boolean))];
|
|
3753
|
-
const hasMultiAuthor = authors.length > 1;
|
|
3754
|
-
// Build canonical merged version
|
|
3755
|
-
// what_worked: pick the most recent success, or longest for most detail
|
|
3756
|
-
const bestSuccess = successes.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime())[0];
|
|
3757
|
-
const whatWorkedCandidates = successes.map(e => e.what_worked).filter(w => w && w.length > 10);
|
|
3758
|
-
const canonicalWorked = whatWorkedCandidates.sort((a, b) => b.length - a.length)[0] ?? bestSuccess?.what_worked ?? '';
|
|
3759
|
-
// what_failed: union of all unique failure reasons
|
|
3760
|
-
const allFailed = [...new Set([...failures, ...partials].map(e => e.what_failed).filter((w) => !!w && w.length > 5))];
|
|
3761
|
-
const severities = entries.map(e => e.severity).filter(Boolean);
|
|
3762
|
-
const canonicalSeverity = severities.includes('critical') ? 'critical' : severities.includes('major') ? 'major' : 'minor';
|
|
3763
|
-
const lines = [
|
|
3764
|
-
`🧬 **Team Brain Synthesis: \`${topic}\`**`,
|
|
3765
|
-
`_${entries.length} entries from ${authors.length} author${authors.length === 1 ? '' : 's'}${hasMultiAuthor ? ` (${authors.join(', ')})` : ''} · ${successes.length} success · ${failures.length} failure · ${partials.length} partial_`,
|
|
3766
|
-
'',
|
|
3767
|
-
`**Canonical "what worked":**`,
|
|
3768
|
-
`> ${canonicalWorked}`,
|
|
3769
|
-
'',
|
|
3770
|
-
allFailed.length > 0 ? `**Avoid (combined failures):**` : '',
|
|
3771
|
-
...allFailed.map(f => `> ❌ ${f}`),
|
|
3772
|
-
allFailed.length > 0 ? '' : '',
|
|
3773
|
-
`**Suggested canonical lesson:**`,
|
|
3774
|
-
'```',
|
|
3775
|
-
`learn_from_attempts(`,
|
|
3776
|
-
` topic = "${topic}",`,
|
|
3777
|
-
` outcome = "success",`,
|
|
3778
|
-
` what_worked = "${canonicalWorked.replace(/"/g, "'")}",`,
|
|
3779
|
-
allFailed.length > 0 ? ` what_failed = "${allFailed[0].replace(/"/g, "'")}",` : '',
|
|
3780
|
-
` severity = "${canonicalSeverity}",`,
|
|
3781
|
-
`)`,
|
|
3782
|
-
'```',
|
|
3783
|
-
'',
|
|
3784
|
-
hasMultiAuthor
|
|
3785
|
-
? `💡 _${authors.length} team members contributed to this synthesis. Store the canonical version to replace individual entries._`
|
|
3786
|
-
: `💡 _Single author — more value when multiple team members contribute to the same topic._`,
|
|
3787
|
-
].filter(l => l !== undefined).join('\n');
|
|
3788
|
-
return lines;
|
|
3789
|
-
}
|
|
3790
2839
|
// ── brain_doctor ──────────────────────────────────────────────────────────
|
|
3791
|
-
// ── memory_crystalize ─────────────────────────────────────────────────────
|
|
3792
|
-
case 'memory_crystalize': {
|
|
3793
|
-
const { instance_id, label: crystalLabel = '' } = args;
|
|
3794
|
-
const redis = await getConnection(instance_id);
|
|
3795
|
-
const now = new Date();
|
|
3796
|
-
const week = `${now.getFullYear()}-W${String(Math.ceil((now.getDate() - now.getDay() + 10) / 7)).padStart(2, '0')}`;
|
|
3797
|
-
const effectiveLabel = crystalLabel || `${now.toISOString().slice(0, 7)} Crystal`;
|
|
3798
|
-
// Read session history
|
|
3799
|
-
const sessionHistory = await redis.lrange('cachly:session:history', 0, 49);
|
|
3800
|
-
// Read all auto-learned lessons
|
|
3801
|
-
const allLessonKeys = [];
|
|
3802
|
-
const ls = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
3803
|
-
await new Promise((res, rej) => {
|
|
3804
|
-
ls.on('data', (b) => allLessonKeys.push(...b));
|
|
3805
|
-
ls.on('end', res);
|
|
3806
|
-
ls.on('error', rej);
|
|
3807
|
-
});
|
|
3808
|
-
const allLessons = [];
|
|
3809
|
-
for (const k of allLessonKeys) {
|
|
3810
|
-
const raw = await redis.get(k);
|
|
3811
|
-
if (!raw)
|
|
3812
|
-
continue;
|
|
3813
|
-
try {
|
|
3814
|
-
allLessons.push(JSON.parse(raw));
|
|
3815
|
-
}
|
|
3816
|
-
catch { /* skip */ }
|
|
3817
|
-
}
|
|
3818
|
-
// Group lessons by top-level category
|
|
3819
|
-
const categoryMap = new Map();
|
|
3820
|
-
for (const l of allLessons) {
|
|
3821
|
-
const cat = l.topic.split(':')[0] || 'misc';
|
|
3822
|
-
if (!categoryMap.has(cat))
|
|
3823
|
-
categoryMap.set(cat, []);
|
|
3824
|
-
categoryMap.get(cat).push(l);
|
|
3825
|
-
}
|
|
3826
|
-
// Build top patterns (most frequent categories with a representative insight)
|
|
3827
|
-
const topPatterns = [];
|
|
3828
|
-
for (const [cat, catLessons] of [...categoryMap.entries()].sort((a, b) => b[1].length - a[1].length).slice(0, 8)) {
|
|
3829
|
-
const successLessons = catLessons.filter(l => l.outcome === 'success');
|
|
3830
|
-
const best = successLessons[0] ?? catLessons[0];
|
|
3831
|
-
if (!best)
|
|
3832
|
-
continue;
|
|
3833
|
-
topPatterns.push({
|
|
3834
|
-
category: cat,
|
|
3835
|
-
insight: best.what_worked.slice(0, 120),
|
|
3836
|
-
count: catLessons.length,
|
|
3837
|
-
});
|
|
3838
|
-
}
|
|
3839
|
-
const crystal = {
|
|
3840
|
-
label: effectiveLabel,
|
|
3841
|
-
ts: now.toISOString(),
|
|
3842
|
-
session_count: sessionHistory.length,
|
|
3843
|
-
lesson_count: allLessons.length,
|
|
3844
|
-
top_patterns: topPatterns,
|
|
3845
|
-
categories: [...categoryMap.keys()],
|
|
3846
|
-
created_from: `${sessionHistory.length} sessions, ${allLessons.length} lessons`,
|
|
3847
|
-
};
|
|
3848
|
-
const crystalJson = JSON.stringify(crystal);
|
|
3849
|
-
await redis.set('cachly:crystal:latest', crystalJson);
|
|
3850
|
-
await redis.expire('cachly:crystal:latest', 90 * 86400);
|
|
3851
|
-
await redis.set(`cachly:crystal:${week}`, crystalJson);
|
|
3852
|
-
await redis.expire(`cachly:crystal:${week}`, 365 * 86400);
|
|
3853
|
-
const lines = [
|
|
3854
|
-
`💎 **Memory Crystal created: ${effectiveLabel}**`,
|
|
3855
|
-
``,
|
|
3856
|
-
`📊 Compressed: **${sessionHistory.length} sessions** + **${allLessons.length} lessons** → ${topPatterns.length} top patterns`,
|
|
3857
|
-
``,
|
|
3858
|
-
`**Top patterns by category:**`,
|
|
3859
|
-
...topPatterns.slice(0, 6).map(p => ` • **${p.category}** (${p.count}×): ${p.insight.slice(0, 90)}`),
|
|
3860
|
-
``,
|
|
3861
|
-
`💡 This crystal will appear in every future \`session_start\` briefing.`,
|
|
3862
|
-
`💡 Re-run \`memory_crystalize\` monthly to keep it fresh.`,
|
|
3863
|
-
];
|
|
3864
|
-
return lines.join('\n');
|
|
3865
|
-
}
|
|
3866
2840
|
case 'brain_doctor': {
|
|
3867
|
-
const { instance_id
|
|
2841
|
+
const { instance_id } = args;
|
|
3868
2842
|
const redis = await getConnection(instance_id);
|
|
3869
2843
|
const issues = [];
|
|
3870
2844
|
const checks = [];
|
|
@@ -3911,15 +2885,6 @@ async function handleTool(name, args) {
|
|
|
3911
2885
|
const unusedLessons = lessons.filter(l => (l.recall_count ?? 0) === 0);
|
|
3912
2886
|
// Critical lessons
|
|
3913
2887
|
const criticalLessons = lessons.filter(l => l.severity === 'critical');
|
|
3914
|
-
// Confidence decay analysis
|
|
3915
|
-
const staleLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) < CONFIDENCE_STALE_VALUE);
|
|
3916
|
-
const warnLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) >= CONFIDENCE_STALE_VALUE && calculateConfidence(l) < CONFIDENCE_WARN_VALUE);
|
|
3917
|
-
const withAudit = lessons.filter(l => (l.audit_trail ?? []).length > 1);
|
|
3918
|
-
const teamLessons = lessons.filter(l => l.author);
|
|
3919
|
-
const uniqueAuthors = new Set(lessons.map(l => l.author).filter(Boolean));
|
|
3920
|
-
// Effective IQ boost: total recalls / lessons (how much the brain actually helped)
|
|
3921
|
-
const totalRecalls = lessons.reduce((sum, l) => sum + (l.recall_count ?? 0), 0);
|
|
3922
|
-
const iqBoostPct = lessons.length > 0 ? Math.min(100, Math.round((totalRecalls / lessons.length) * 10)) : 0;
|
|
3923
2888
|
// Quality score (0-100)
|
|
3924
2889
|
let score = 50;
|
|
3925
2890
|
if (lessonKeys.length >= 5)
|
|
@@ -3937,72 +2902,10 @@ async function handleTool(name, args) {
|
|
|
3937
2902
|
const unusedRatio = lessons.length > 0 ? unusedLessons.length / lessons.length : 0;
|
|
3938
2903
|
if (unusedRatio < 0.5)
|
|
3939
2904
|
score += 10;
|
|
3940
|
-
if (staleLessons.length === 0)
|
|
3941
|
-
score += 5;
|
|
3942
|
-
if (uniqueAuthors.size >= 2)
|
|
3943
|
-
score += 5; // team collaboration bonus
|
|
3944
2905
|
const scoreEmoji = score >= 80 ? '🟢' : score >= 50 ? '🟡' : '🔴';
|
|
3945
|
-
const iqEmoji = iqBoostPct >= 50 ? '🚀' : iqBoostPct >= 20 ? '📈' : '💤';
|
|
3946
2906
|
checks.push(`${scoreEmoji} **Brain Quality Score: ${score}/100**`);
|
|
3947
|
-
checks.push(
|
|
3948
|
-
checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical · ${withAudit.length} with audit trail · ${teamLessons.length} from team)`);
|
|
2907
|
+
checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical)`);
|
|
3949
2908
|
checks.push(`💾 **Context entries:** ${ctxCount}`);
|
|
3950
|
-
checks.push(`🎯 **Confidence:** ${lessons.length - staleLessons.length - warnLessons.length} fresh · ${warnLessons.length} warn · ${staleLessons.length} stale`);
|
|
3951
|
-
checks.push(`⏱️ **Decay config:** warn after ${CONFIDENCE_WARN_DAYS}d · stale after ${CONFIDENCE_STALE_DAYS}d`);
|
|
3952
|
-
if (uniqueAuthors.size >= 2) {
|
|
3953
|
-
checks.push(`👥 **Team:** ${uniqueAuthors.size} contributors (${[...uniqueAuthors].join(', ')})`);
|
|
3954
|
-
}
|
|
3955
|
-
// Stale index detection
|
|
3956
|
-
try {
|
|
3957
|
-
const lastIndexRaw = await redis.get('cachly:index:last_run');
|
|
3958
|
-
if (lastIndexRaw) {
|
|
3959
|
-
const lastIndexAge = Math.round((Date.now() - new Date(lastIndexRaw).getTime()) / 86_400_000);
|
|
3960
|
-
if (lastIndexAge > 7) {
|
|
3961
|
-
issues.push(`🔄 Index is ${lastIndexAge}d stale — run \`index_project\` to re-sync semantic search`);
|
|
3962
|
-
}
|
|
3963
|
-
else {
|
|
3964
|
-
checks.push(`🗂️ **Semantic index:** ${lastIndexAge}d old (fresh)`);
|
|
3965
|
-
}
|
|
3966
|
-
}
|
|
3967
|
-
else {
|
|
3968
|
-
issues.push(`💡 No semantic index — run \`index_project(dir="<your-src>")\` to enable semantic search`);
|
|
3969
|
-
}
|
|
3970
|
-
}
|
|
3971
|
-
catch { /* non-critical */ }
|
|
3972
|
-
// Memory crystal status
|
|
3973
|
-
try {
|
|
3974
|
-
const crystalRaw = await redis.get('cachly:crystal:latest');
|
|
3975
|
-
if (crystalRaw) {
|
|
3976
|
-
const crystal = JSON.parse(crystalRaw);
|
|
3977
|
-
const crystalAge = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
|
|
3978
|
-
checks.push(`💎 **Memory Crystal:** ${crystal.label} (${crystalAge}d ago)`);
|
|
3979
|
-
if (crystalAge > 30)
|
|
3980
|
-
issues.push(`💡 Memory Crystal is ${crystalAge}d old — re-run \`memory_crystalize\` to compress new sessions`);
|
|
3981
|
-
}
|
|
3982
|
-
else if (lessonKeys.length >= 10) {
|
|
3983
|
-
issues.push(`💡 ${lessonKeys.length} lessons but no Memory Crystal — run \`memory_crystalize\` to compress wisdom`);
|
|
3984
|
-
}
|
|
3985
|
-
}
|
|
3986
|
-
catch { /* non-critical */ }
|
|
3987
|
-
// openclaw cross-promo (check package.json in workspace)
|
|
3988
|
-
if (drWorkspacePath) {
|
|
3989
|
-
try {
|
|
3990
|
-
const pkgPath = drWorkspacePath.replace(/\/$/, '') + '/package.json';
|
|
3991
|
-
const pkgRaw = readFileSync(pkgPath, 'utf-8');
|
|
3992
|
-
const pkg = JSON.parse(pkgRaw);
|
|
3993
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3994
|
-
const hasLLMDep = ['openai', '@anthropic-ai/sdk', '@google/generative-ai', 'mistralai', 'cohere-ai'].some(d => d in allDeps);
|
|
3995
|
-
const hasOpenclaw = '@cachly-dev/openclaw' in allDeps;
|
|
3996
|
-
if (hasLLMDep && !hasOpenclaw) {
|
|
3997
|
-
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\``);
|
|
3998
|
-
issues.push(` → \`npm install @cachly-dev/openclaw\` cuts LLM costs 60–90% with 3 lines of code`);
|
|
3999
|
-
}
|
|
4000
|
-
else if (hasOpenclaw) {
|
|
4001
|
-
checks.push(`✅ **@cachly-dev/openclaw installed** (LLM cost caching active)`);
|
|
4002
|
-
}
|
|
4003
|
-
}
|
|
4004
|
-
catch { /* no package.json or unreadable */ }
|
|
4005
|
-
}
|
|
4006
2909
|
if (lastSession) {
|
|
4007
2910
|
const ageMin = Math.round((Date.now() - new Date(lastSession.ts).getTime()) / 60000);
|
|
4008
2911
|
const ageStr = ageMin < 60 ? `${ageMin}m` : ageMin < 1440 ? `${Math.round(ageMin / 60)}h` : `${Math.round(ageMin / 1440)}d`;
|
|
@@ -4017,22 +2920,12 @@ async function handleTool(name, args) {
|
|
|
4017
2920
|
else if (lessonKeys.length < 5) {
|
|
4018
2921
|
issues.push(`💡 Only ${lessonKeys.length} lessons — add more after each problem solved`);
|
|
4019
2922
|
}
|
|
4020
|
-
if (iqBoostPct === 0 && lessons.length >= 5) {
|
|
4021
|
-
issues.push(`💤 **IQ Boost is 0%** — lessons exist but are never recalled. Use \`recall_best_solution\` BEFORE tasks.`);
|
|
4022
|
-
}
|
|
4023
2923
|
if (ctxCount === 0) {
|
|
4024
2924
|
issues.push('💡 No context — use `remember_context` to cache architecture docs, ADRs, etc.');
|
|
4025
2925
|
}
|
|
4026
2926
|
if (openFailures.length > 0) {
|
|
4027
2927
|
issues.push(`⚠️ ${openFailures.length} unresolved failure${openFailures.length > 1 ? 's' : ''}: ${openFailures.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
|
|
4028
2928
|
}
|
|
4029
|
-
if (staleLessons.length > 0) {
|
|
4030
|
-
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(', ')}`);
|
|
4031
|
-
issues.push(` → Re-verify with \`recall_best_solution\` to reset confidence clock`);
|
|
4032
|
-
}
|
|
4033
|
-
if (warnLessons.length > 0) {
|
|
4034
|
-
issues.push(`⚠️ ${warnLessons.length} lesson${warnLessons.length > 1 ? 's' : ''} aging (>${CONFIDENCE_WARN_DAYS}d): ${warnLessons.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
|
|
4035
|
-
}
|
|
4036
2929
|
if (unusedRatio > 0.7 && lessons.length > 5) {
|
|
4037
2930
|
issues.push(`💡 ${unusedLessons.length} lessons never recalled — verify topics match your workflow`);
|
|
4038
2931
|
}
|
|
@@ -4048,86 +2941,6 @@ async function handleTool(name, args) {
|
|
|
4048
2941
|
}
|
|
4049
2942
|
return lines.join('\n');
|
|
4050
2943
|
}
|
|
4051
|
-
// ── recall_at — Brain Archaeology ────────────────────────────────────────
|
|
4052
|
-
case 'recall_at': {
|
|
4053
|
-
const { instance_id, topic, date } = args;
|
|
4054
|
-
const redis = await getConnection(instance_id);
|
|
4055
|
-
const cutoff = new Date(date).getTime();
|
|
4056
|
-
if (isNaN(cutoff))
|
|
4057
|
-
return `❌ Invalid date "${date}". Use ISO format: "2026-01-15"`;
|
|
4058
|
-
const listKey = `cachly:lessons:${topic}`;
|
|
4059
|
-
const all = await redis.lrange(listKey, 0, -1);
|
|
4060
|
-
if (all.length === 0)
|
|
4061
|
-
return `📭 No history found for \`${topic}\`. Lessons are stored via \`learn_from_attempts\`.`;
|
|
4062
|
-
const before = all
|
|
4063
|
-
.map(raw => { try {
|
|
4064
|
-
return JSON.parse(raw);
|
|
4065
|
-
}
|
|
4066
|
-
catch {
|
|
4067
|
-
return null;
|
|
4068
|
-
} })
|
|
4069
|
-
.filter((l) => l !== null && new Date(l.ts).getTime() <= cutoff)
|
|
4070
|
-
.sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
|
|
4071
|
-
if (before.length === 0)
|
|
4072
|
-
return `📭 No entries for \`${topic}\` found before **${date}**. Earliest entry: ${new Date(JSON.parse(all[0]).ts).toLocaleDateString('de-DE')}.`;
|
|
4073
|
-
const lines = [
|
|
4074
|
-
`🏺 **Brain Archaeology: \`${topic}\` before ${date}**`,
|
|
4075
|
-
`_${before.length} of ${all.length} total entries shown_`,
|
|
4076
|
-
'',
|
|
4077
|
-
];
|
|
4078
|
-
for (const l of before.slice(-10)) {
|
|
4079
|
-
const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
|
|
4080
|
-
const d = new Date(l.ts).toLocaleDateString('de-DE');
|
|
4081
|
-
lines.push(`**${d}** ${emoji} ${l.outcome}`);
|
|
4082
|
-
if (l.what_worked)
|
|
4083
|
-
lines.push(` → ${l.what_worked.slice(0, 100)}`);
|
|
4084
|
-
lines.push('');
|
|
4085
|
-
}
|
|
4086
|
-
lines.push(`_Full evolution: ${all.map(r => { try {
|
|
4087
|
-
const l = JSON.parse(r);
|
|
4088
|
-
return l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
|
|
4089
|
-
}
|
|
4090
|
-
catch {
|
|
4091
|
-
return '?';
|
|
4092
|
-
} }).join(' → ')}_`);
|
|
4093
|
-
return lines.join('\n');
|
|
4094
|
-
}
|
|
4095
|
-
// ── trace_dependency — Causal Chain ──────────────────────────────────────
|
|
4096
|
-
case 'trace_dependency': {
|
|
4097
|
-
const { instance_id, dependency, mark_review = false } = args;
|
|
4098
|
-
const redis = await getConnection(instance_id);
|
|
4099
|
-
const depKey = `cachly:dep:${dependency}`;
|
|
4100
|
-
const raw = await redis.get(depKey);
|
|
4101
|
-
if (!raw)
|
|
4102
|
-
return `📭 No lessons found that depend on \`${dependency}\`.\n\nAdd dependencies via: \`learn_from_attempts(..., depends_on=["${dependency}"])\``;
|
|
4103
|
-
const topics = JSON.parse(raw);
|
|
4104
|
-
const lines = [
|
|
4105
|
-
`🔗 **Causal Chain: \`${dependency}\`** — ${topics.length} dependent lesson${topics.length === 1 ? '' : 's'}`,
|
|
4106
|
-
'',
|
|
4107
|
-
];
|
|
4108
|
-
for (const t of topics) {
|
|
4109
|
-
const lessonRaw = await redis.get(`cachly:lesson:best:${t}`);
|
|
4110
|
-
if (!lessonRaw) {
|
|
4111
|
-
lines.push(` • \`${t}\` _(lesson deleted)_`);
|
|
4112
|
-
continue;
|
|
4113
|
-
}
|
|
4114
|
-
const lesson = JSON.parse(lessonRaw);
|
|
4115
|
-
const emoji = lesson.outcome === 'success' ? '✅' : lesson.outcome === 'partial' ? '⚠️' : '❌';
|
|
4116
|
-
const reviewBadge = lesson.needs_review ? ' 🔍 **needs_review**' : '';
|
|
4117
|
-
lines.push(` ${emoji} \`${t}\` (${lesson.severity ?? 'major'})${reviewBadge}`);
|
|
4118
|
-
if (mark_review) {
|
|
4119
|
-
const updated = { ...lesson, needs_review: true };
|
|
4120
|
-
await redis.set(`cachly:lesson:best:${t}`, JSON.stringify(updated));
|
|
4121
|
-
}
|
|
4122
|
-
}
|
|
4123
|
-
if (mark_review) {
|
|
4124
|
-
lines.push('', `🔍 All ${topics.length} lessons marked as **needs_review** — verify they still work with the changed dependency.`);
|
|
4125
|
-
}
|
|
4126
|
-
else {
|
|
4127
|
-
lines.push('', `_Run with \`mark_review: true\` to flag all dependent lessons for re-verification._`);
|
|
4128
|
-
}
|
|
4129
|
-
return lines.join('\n');
|
|
4130
|
-
}
|
|
4131
2944
|
// ── global_learn ──────────────────────────────────────────────────────────
|
|
4132
2945
|
case 'global_learn': {
|
|
4133
2946
|
const { instance_id, topic, lesson, severity = 'minor', tags = [] } = args;
|
|
@@ -4363,144 +3176,62 @@ async function handleTool(name, args) {
|
|
|
4363
3176
|
// Generate copilot-instructions.md content
|
|
4364
3177
|
const tier = inst.tier.toUpperCase();
|
|
4365
3178
|
const smartRecallNote = hasVector
|
|
4366
|
-
? '
|
|
4367
|
-
: '
|
|
3179
|
+
? '3. Call `smart_recall` with keywords for semantic context search'
|
|
3180
|
+
: '3. Call `recall_context` with relevant key patterns like "arch:*" or "file:*"';
|
|
4368
3181
|
const layerNote = hasVector
|
|
4369
|
-
? `Layer 3 (Semantic): smart_recall uses pgvector HNSW on your ${tier} instance`
|
|
4370
|
-
: `Layer 3 (Autopilot):
|
|
4371
|
-
const copilotInstructions = `# cachly AI
|
|
4372
|
-
|
|
4373
|
-
> 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.
|
|
4374
|
-
|
|
4375
|
-
---
|
|
4376
|
-
|
|
4377
|
-
## 🔴 MANDATORY RULES — Always enforced, no exceptions
|
|
4378
|
-
|
|
4379
|
-
### Rule 1 — Session Start (FIRST thing in every window)
|
|
4380
|
-
Call \`session_start\` **before reading any source files or making any changes**:
|
|
4381
|
-
|
|
4382
|
-
\`\`\`
|
|
4383
|
-
session_start(
|
|
4384
|
-
instance_id = "${instance_id}",
|
|
4385
|
-
focus = "<what you plan to work on today>",
|
|
4386
|
-
)
|
|
4387
|
-
\`\`\`
|
|
3182
|
+
? `Layer 3 (Semantic): smart_recall uses pgvector HNSW on your ${tier} instance for meaning-based search`
|
|
3183
|
+
: `Layer 3 (Autopilot): This file — upgrade to Speed/Business tier to unlock smart_recall`;
|
|
3184
|
+
const copilotInstructions = `# cachly AI Memory — ${project_description}
|
|
4388
3185
|
|
|
4389
|
-
|
|
3186
|
+
## 3-Layer AI Memory System
|
|
4390
3187
|
|
|
4391
|
-
**
|
|
4392
|
-
|
|
4393
|
-
|
|
3188
|
+
- **Layer 1 — Storage:** cachly instance \`${inst.name}\` (${tier}) — persistent Valkey brain, survives restarts
|
|
3189
|
+
- **Layer 2 — Tools:** learn_from_attempts · recall_best_solution · smart_recall — the memory API
|
|
3190
|
+
- **Layer 3 — Autopilot (this file):** instructs the AI to run memory tools automatically
|
|
4394
3191
|
|
|
4395
|
-
|
|
4396
|
-
After **every** bug fix, deployment, failed attempt, or discovery — call \`learn_from_attempts\` immediately:
|
|
3192
|
+
## BEFORE every task — check memory first
|
|
4397
3193
|
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
outcome = "success" | "failure" | "partial",
|
|
4403
|
-
what_worked = "exact approach or command that worked",
|
|
4404
|
-
what_failed = "what did NOT work (prevents future wasted time)",
|
|
4405
|
-
severity = "critical" | "major" | "minor",
|
|
4406
|
-
commands = ["exact command that worked"],
|
|
4407
|
-
file_paths = ["path/to/changed/file"],
|
|
4408
|
-
)
|
|
4409
|
-
\`\`\`
|
|
3194
|
+
1. Call \`recall_best_solution\` with the task topic to get known solutions
|
|
3195
|
+
- \`recall_best_solution("deploy:web")\` → returns working deploy command
|
|
3196
|
+
- \`recall_best_solution("debug:redis-tls")\` → returns root cause + fix
|
|
3197
|
+
- If a solution exists, use it — do NOT re-discover from scratch
|
|
4410
3198
|
|
|
4411
|
-
|
|
4412
|
-
-
|
|
4413
|
-
-
|
|
4414
|
-
- ✅ Discovered root cause → \`topic="debug:<area>", outcome="partial"\`
|
|
4415
|
-
- ✅ Tried something that failed → \`topic="<area>", outcome="failure", what_failed="..."\`
|
|
4416
|
-
- ✅ Found a config quirk → \`topic="infra:<topic>", outcome="success"\`
|
|
4417
|
-
- ✅ Ran a working command → store it in \`commands=[...]\`
|
|
3199
|
+
2. Call \`recall_context\` with the area you're working in
|
|
3200
|
+
- \`recall_context("arch:*")\` → cached architecture decisions
|
|
3201
|
+
- \`recall_context("file:src/auth*")\` → cached file summaries
|
|
4418
3202
|
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
session_handoff(
|
|
4422
|
-
instance_id = "${instance_id}",
|
|
4423
|
-
completed_tasks = ["what was done"],
|
|
4424
|
-
remaining_tasks = ["what is still to do"],
|
|
4425
|
-
files_changed = [{"path": "src/...", "status": "modified", "description": "what changed"}],
|
|
4426
|
-
instructions = "IMPORTANT: what the next window MUST know to not break things",
|
|
4427
|
-
context_summary = "current state of the system",
|
|
4428
|
-
)
|
|
4429
|
-
\`\`\`
|
|
3203
|
+
${smartRecallNote}
|
|
3204
|
+
- \`smart_recall("how does authentication work")\` → returns cached auth notes
|
|
4430
3205
|
|
|
4431
|
-
|
|
3206
|
+
4. If nothing is found, proceed normally — then save (see below)
|
|
4432
3207
|
|
|
4433
|
-
|
|
4434
|
-
\`\`\`
|
|
4435
|
-
session_end(
|
|
4436
|
-
instance_id = "${instance_id}",
|
|
4437
|
-
summary = "What was accomplished (2-3 sentences with specifics)",
|
|
4438
|
-
files_changed = ["list", "of", "changed", "files"],
|
|
4439
|
-
lessons_learned = <number of learn_from_attempts calls made>,
|
|
4440
|
-
)
|
|
4441
|
-
\`\`\`
|
|
4442
|
-
|
|
4443
|
-
> \`session_end\` **automatically extracts and stores lessons** from the summary text — no manual calls needed.
|
|
4444
|
-
|
|
4445
|
-
### Rule 5 — Code Index Invalidation (AUTOMATIC)
|
|
4446
|
-
- \`index_project\` tracks MD5 hashes per file — **skips unchanged files automatically**
|
|
4447
|
-
- After code changes: next \`index_project\` run re-indexes only modified files
|
|
4448
|
-
- After delete/rename: run \`index_project\` again to sync
|
|
4449
|
-
- No manual invalidation needed
|
|
4450
|
-
|
|
4451
|
-
---
|
|
3208
|
+
## AFTER every non-trivial task — save to memory
|
|
4452
3209
|
|
|
4453
|
-
|
|
3210
|
+
1. Call \`learn_from_attempts\` for deploy/debug/fix tasks:
|
|
3211
|
+
- Always set outcome = "success" | "failure" | "partial"
|
|
3212
|
+
- Always include what_failed — it prevents future wasted time
|
|
3213
|
+
- Example: \`learn_from_attempts(topic="deploy:api", outcome="success", what_worked="nohup docker compose up -d --build", what_failed="direct docker compose hangs on SSH timeout")\`
|
|
4454
3214
|
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
| Fix a bug | \`recall_best_solution("fix:<area>")\` | \`learn_from_attempts(topic="fix:...")\` |
|
|
4459
|
-
| Add a feature | \`session_start(focus="feat:<area>")\` | \`learn_from_attempts(topic="feat:...")\` |
|
|
4460
|
-
| Infra/server work | \`recall_best_solution("infra:<topic>")\` | \`learn_from_attempts(topic="infra:...")\` |
|
|
4461
|
-
| Debug an issue | \`smart_recall("<error message or symptom>")\` | \`learn_from_attempts(topic="debug:...")\` |
|
|
4462
|
-
|
|
4463
|
-
---
|
|
4464
|
-
|
|
4465
|
-
## Available Brain Tools
|
|
4466
|
-
|
|
4467
|
-
| Tool | When to use |
|
|
4468
|
-
|------|-------------|
|
|
4469
|
-
| \`session_start\` | **FIRST** — mandatory at start of every session |
|
|
4470
|
-
| \`session_end\` | **LAST** — mandatory at end, auto-learns from summary |
|
|
4471
|
-
| \`session_handoff\` | When closing window with remaining tasks |
|
|
4472
|
-
| \`learn_from_attempts\` | **AUTOMATIC** after every fix/deploy/discovery |
|
|
4473
|
-
| \`recall_best_solution\` | Before any non-trivial task |
|
|
4474
|
-
| \`remember_context\` | After analyzing code — save findings for future sessions |
|
|
4475
|
-
${smartRecallNote ? `| \`smart_recall\` | Search brain by meaning/keywords |\n` : ''}\
|
|
4476
|
-
| \`recall_context\` | Get exact key (supports glob: \`arch:*\`, \`file:*\`) |
|
|
4477
|
-
| \`brain_search\` | BM25+ full-text search over all brain data |
|
|
4478
|
-
| \`auto_learn_session\` | Batch-learn from a list of observations (optional) |
|
|
4479
|
-
| \`index_project\` | Index source files (smart hash, skips unchanged files) |
|
|
4480
|
-
| \`list_remembered\` | See what's cached in the brain |
|
|
4481
|
-
| \`forget_context\` | Remove stale context |
|
|
4482
|
-
|
|
4483
|
-
---
|
|
3215
|
+
2. Call \`remember_context\` for code/architecture findings:
|
|
3216
|
+
- \`remember_context("arch:auth", "Uses Keycloak with JWT...", category="architecture")\`
|
|
3217
|
+
- \`remember_context("file:src/index.ts", "Entry point, registers routes...", category="file_summary")\`
|
|
4484
3218
|
|
|
4485
3219
|
## Instance Details
|
|
4486
3220
|
|
|
4487
|
-
-
|
|
4488
|
-
-
|
|
4489
|
-
-
|
|
4490
|
-
-
|
|
4491
|
-
-
|
|
4492
|
-
|
|
4493
|
-
---
|
|
3221
|
+
- Instance ID: \`${instance_id}\`
|
|
3222
|
+
- Instance name: ${inst.name}
|
|
3223
|
+
- Tier: ${tier}
|
|
3224
|
+
- ${layerNote}
|
|
3225
|
+
- Embedding provider: ${providerArg}
|
|
4494
3226
|
|
|
4495
|
-
##
|
|
3227
|
+
## Quick reference
|
|
4496
3228
|
|
|
4497
3229
|
\`\`\`
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
3230
|
+
recall_best_solution("topic") # before task
|
|
3231
|
+
smart_recall("natural language") # find by meaning
|
|
3232
|
+
learn_from_attempts(...) # after task
|
|
3233
|
+
remember_context("key", "content") # save analysis
|
|
4501
3234
|
\`\`\`
|
|
4502
|
-
|
|
4503
|
-
Result: Your AI **never solves the same problem twice** and always picks up exactly where it left off. 🚀
|
|
4504
3235
|
`;
|
|
4505
3236
|
const lines = [
|
|
4506
3237
|
`🧠 **cachly AI Memory Setup Complete**`,
|
|
@@ -4525,8 +3256,8 @@ Result: Your AI **never solves the same problem twice** and always picks up exac
|
|
|
4525
3256
|
`**Step 3 — copilot-instructions.md (Layer 3 Autopilot):**`,
|
|
4526
3257
|
``,
|
|
4527
3258
|
...(project_dir
|
|
4528
|
-
? [
|
|
4529
|
-
: [`Copy this to \`.github/copilot-instructions.md\`
|
|
3259
|
+
? [`Writing to \`${project_dir}/.github/copilot-instructions.md\`…`]
|
|
3260
|
+
: [`Copy this to \`.github/copilot-instructions.md\` in your project:`]),
|
|
4530
3261
|
``,
|
|
4531
3262
|
`\`\`\`markdown`,
|
|
4532
3263
|
copilotInstructions,
|
|
@@ -4540,56 +3271,14 @@ Result: Your AI **never solves the same problem twice** and always picks up exac
|
|
|
4540
3271
|
``,
|
|
4541
3272
|
`Result: Your AI never solves the same problem twice. 🚀`,
|
|
4542
3273
|
];
|
|
4543
|
-
//
|
|
3274
|
+
// Optionally write the file
|
|
4544
3275
|
if (project_dir) {
|
|
4545
|
-
const { mkdir, writeFile
|
|
4546
|
-
const
|
|
4547
|
-
|
|
4548
|
-
const
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
ide: 'GitHub Copilot',
|
|
4552
|
-
path: join(project_dir, '.github', 'copilot-instructions.md'),
|
|
4553
|
-
content: copilotInstructions,
|
|
4554
|
-
});
|
|
4555
|
-
// Claude Code — CLAUDE.md in project root
|
|
4556
|
-
if (await exists(join(project_dir, 'CLAUDE.md')) || await exists(join(project_dir, '.claude'))) {
|
|
4557
|
-
targets.push({
|
|
4558
|
-
ide: 'Claude Code',
|
|
4559
|
-
path: join(project_dir, 'CLAUDE.md'),
|
|
4560
|
-
content: copilotInstructions,
|
|
4561
|
-
});
|
|
4562
|
-
}
|
|
4563
|
-
// Cursor — .cursor/rules (new format) or .cursorrules (legacy)
|
|
4564
|
-
if (await exists(join(project_dir, '.cursor')) ||
|
|
4565
|
-
await exists(join(project_dir, '.cursorrules'))) {
|
|
4566
|
-
const cursorDir = join(project_dir, '.cursor');
|
|
4567
|
-
await mkdir(cursorDir, { recursive: true });
|
|
4568
|
-
targets.push({
|
|
4569
|
-
ide: 'Cursor',
|
|
4570
|
-
path: join(cursorDir, 'rules'),
|
|
4571
|
-
content: copilotInstructions,
|
|
4572
|
-
});
|
|
4573
|
-
}
|
|
4574
|
-
// Windsurf — .windsurfrules
|
|
4575
|
-
if (await exists(join(project_dir, '.windsurfrules')) ||
|
|
4576
|
-
await exists(join(project_dir, '.windsurf'))) {
|
|
4577
|
-
targets.push({
|
|
4578
|
-
ide: 'Windsurf',
|
|
4579
|
-
path: join(project_dir, '.windsurfrules'),
|
|
4580
|
-
content: copilotInstructions,
|
|
4581
|
-
});
|
|
4582
|
-
}
|
|
4583
|
-
// VS Code (Copilot) — already covered by .github/copilot-instructions.md above
|
|
4584
|
-
// Continue.dev — .continue/config.json is JSON, not markdown — skip, copilot-instructions handles it
|
|
4585
|
-
const written = [];
|
|
4586
|
-
for (const target of targets) {
|
|
4587
|
-
const dir = target.path.substring(0, target.path.lastIndexOf('/'));
|
|
4588
|
-
await mkdir(dir, { recursive: true });
|
|
4589
|
-
await writeFile(target.path, target.content, 'utf-8');
|
|
4590
|
-
written.push(`✅ [${target.ide}] → \`${target.path.replace(project_dir, '.')}\``);
|
|
4591
|
-
}
|
|
4592
|
-
lines.push(...written);
|
|
3276
|
+
const { mkdir, writeFile } = await import('node:fs/promises');
|
|
3277
|
+
const githubDir = join(project_dir, '.github');
|
|
3278
|
+
await mkdir(githubDir, { recursive: true });
|
|
3279
|
+
const filePath = join(githubDir, 'copilot-instructions.md');
|
|
3280
|
+
await writeFile(filePath, copilotInstructions, 'utf-8');
|
|
3281
|
+
lines.splice(lines.indexOf(`Writing to \`${project_dir}/.github/copilot-instructions.md\`…`) + 1, 0, `✅ Written to \`${project_dir}/.github/copilot-instructions.md\``);
|
|
4593
3282
|
}
|
|
4594
3283
|
return lines.join('\n');
|
|
4595
3284
|
}
|
|
@@ -4713,236 +3402,15 @@ async function handleBulkLockStream(name, args) {
|
|
|
4713
3402
|
return (`✅ **cache_stream_get** – ${len} chunk(s) retrieved for \`${key}\`.\n\n` +
|
|
4714
3403
|
`**Preview** (first 500 chars):\n\`\`\`\n${preview}${preview.length < chunks.join('').length ? '…' : ''}\n\`\`\``);
|
|
4715
3404
|
}
|
|
4716
|
-
// ── Roadmap ──────────────────────────────────────────────────────────────
|
|
4717
|
-
case 'roadmap_add': {
|
|
4718
|
-
const { instance_id: rid, title, description: desc = '', priority = 'medium', tags: rtags = [], milestone = '', } = args;
|
|
4719
|
-
const redis = await getConnection(rid);
|
|
4720
|
-
const id = `rm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
4721
|
-
const item = {
|
|
4722
|
-
id, title, description: desc, priority, tags: rtags, milestone,
|
|
4723
|
-
status: 'planned',
|
|
4724
|
-
created: new Date().toISOString(),
|
|
4725
|
-
updated: new Date().toISOString(),
|
|
4726
|
-
notes: '',
|
|
4727
|
-
};
|
|
4728
|
-
await redis.hset(`cachly:roadmap:${rid}`, id, JSON.stringify(item));
|
|
4729
|
-
const PRIORITY_ICON = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
|
|
4730
|
-
return [
|
|
4731
|
-
`📋 **Roadmap item added**`,
|
|
4732
|
-
``,
|
|
4733
|
-
` ID: \`${id}\``,
|
|
4734
|
-
` Title: ${title}`,
|
|
4735
|
-
` Priority: ${PRIORITY_ICON[priority] ?? '⚪'} ${priority}`,
|
|
4736
|
-
` Status: planned`,
|
|
4737
|
-
milestone ? ` Milestone: ${milestone}` : '',
|
|
4738
|
-
rtags.length ? ` Tags: ${rtags.join(', ')}` : '',
|
|
4739
|
-
``,
|
|
4740
|
-
`💡 Use \`roadmap_update(id: "${id}", status: "in-progress")\` when you start working on it.`,
|
|
4741
|
-
].filter(Boolean).join('\n');
|
|
4742
|
-
}
|
|
4743
|
-
case 'roadmap_update': {
|
|
4744
|
-
const { instance_id: rid, id: itemId, status: newStatus, priority: newPriority, notes: newNotes, title: newTitle, description: newDesc, } = args;
|
|
4745
|
-
const redis = await getConnection(rid);
|
|
4746
|
-
const raw = await redis.hget(`cachly:roadmap:${rid}`, itemId);
|
|
4747
|
-
if (!raw)
|
|
4748
|
-
return `⚠️ **roadmap_update** – Item \`${itemId}\` not found. Use \`roadmap_list\` to see all items.`;
|
|
4749
|
-
const item = JSON.parse(raw);
|
|
4750
|
-
const oldStatus = item.status;
|
|
4751
|
-
if (newStatus)
|
|
4752
|
-
item.status = newStatus;
|
|
4753
|
-
if (newPriority)
|
|
4754
|
-
item.priority = newPriority;
|
|
4755
|
-
if (newTitle)
|
|
4756
|
-
item.title = newTitle;
|
|
4757
|
-
if (newDesc)
|
|
4758
|
-
item.description = newDesc;
|
|
4759
|
-
if (newNotes)
|
|
4760
|
-
item.notes = item.notes ? `${item.notes}\n[${new Date().toISOString().slice(0, 10)}] ${newNotes}` : `[${new Date().toISOString().slice(0, 10)}] ${newNotes}`;
|
|
4761
|
-
item.updated = new Date().toISOString();
|
|
4762
|
-
await redis.hset(`cachly:roadmap:${rid}`, itemId, JSON.stringify(item));
|
|
4763
|
-
const statusEmoji = { planned: '📋', 'in-progress': '⚡', done: '✅', blocked: '🚫', cancelled: '🗑️' };
|
|
4764
|
-
return [
|
|
4765
|
-
`${statusEmoji[newStatus ?? oldStatus] ?? '📋'} **Roadmap updated** \`${itemId}\``,
|
|
4766
|
-
``,
|
|
4767
|
-
` Title: ${item.title}`,
|
|
4768
|
-
oldStatus !== newStatus ? ` Status: ${oldStatus} → ${newStatus}` : ` Status: ${item.status}`,
|
|
4769
|
-
newNotes ? ` Notes: ${newNotes}` : '',
|
|
4770
|
-
].filter(Boolean).join('\n');
|
|
4771
|
-
}
|
|
4772
|
-
case 'roadmap_list': {
|
|
4773
|
-
const { instance_id: rid, status: filterStatus = 'open', tag: filterTag, milestone: filterMilestone, priority: filterPriority, } = args;
|
|
4774
|
-
const redis = await getConnection(rid);
|
|
4775
|
-
const all = await redis.hgetall(`cachly:roadmap:${rid}`);
|
|
4776
|
-
if (!all || Object.keys(all).length === 0) {
|
|
4777
|
-
return '📋 **Roadmap is empty.**\n\nUse `roadmap_add` to create your first item.';
|
|
4778
|
-
}
|
|
4779
|
-
const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
4780
|
-
const PRIORITY_ICON = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
|
|
4781
|
-
const STATUS_ICON = { planned: '📋', 'in-progress': '⚡', done: '✅', blocked: '🚫', cancelled: '🗑️' };
|
|
4782
|
-
const openStatuses = new Set(['planned', 'in-progress', 'blocked']);
|
|
4783
|
-
let items = Object.values(all).map(v => JSON.parse(v));
|
|
4784
|
-
// Filter
|
|
4785
|
-
if (filterStatus === 'open')
|
|
4786
|
-
items = items.filter(i => openStatuses.has(i.status));
|
|
4787
|
-
else if (filterStatus)
|
|
4788
|
-
items = items.filter(i => i.status === filterStatus);
|
|
4789
|
-
if (filterTag)
|
|
4790
|
-
items = items.filter(i => i.tags.includes(filterTag));
|
|
4791
|
-
if (filterMilestone)
|
|
4792
|
-
items = items.filter(i => i.milestone === filterMilestone);
|
|
4793
|
-
if (filterPriority)
|
|
4794
|
-
items = items.filter(i => (PRIORITY_ORDER[i.priority] ?? 99) <= (PRIORITY_ORDER[filterPriority] ?? 99));
|
|
4795
|
-
// Sort: priority asc, then created asc
|
|
4796
|
-
items.sort((a, b) => {
|
|
4797
|
-
const pa = PRIORITY_ORDER[a.priority] ?? 99;
|
|
4798
|
-
const pb = PRIORITY_ORDER[b.priority] ?? 99;
|
|
4799
|
-
return pa !== pb ? pa - pb : a.created.localeCompare(b.created);
|
|
4800
|
-
});
|
|
4801
|
-
if (items.length === 0)
|
|
4802
|
-
return `📋 **No roadmap items** match the current filter (status: ${filterStatus}).`;
|
|
4803
|
-
const grouped = {};
|
|
4804
|
-
for (const it of items) {
|
|
4805
|
-
const st = it.status;
|
|
4806
|
-
if (!grouped[st])
|
|
4807
|
-
grouped[st] = [];
|
|
4808
|
-
grouped[st].push(it);
|
|
4809
|
-
}
|
|
4810
|
-
const lines = [`📋 **Roadmap** (${items.length} item${items.length !== 1 ? 's' : ''})`, ''];
|
|
4811
|
-
for (const [st, grp] of Object.entries(grouped)) {
|
|
4812
|
-
lines.push(`**${STATUS_ICON[st] ?? '•'} ${st.toUpperCase()}** (${grp.length})`);
|
|
4813
|
-
for (const it of grp) {
|
|
4814
|
-
const tags = it.tags.length ? ` [${it.tags.join(', ')}]` : '';
|
|
4815
|
-
const milestone = it.milestone ? ` · ${it.milestone}` : '';
|
|
4816
|
-
lines.push(` ${PRIORITY_ICON[it.priority] ?? '⚪'} \`${it.id}\` **${it.title}**${tags}${milestone}`);
|
|
4817
|
-
if (it.description)
|
|
4818
|
-
lines.push(` ${it.description.slice(0, 120)}`);
|
|
4819
|
-
if (it.notes)
|
|
4820
|
-
lines.push(` 📝 ${it.notes.split('\n').pop()?.slice(0, 100)}`);
|
|
4821
|
-
}
|
|
4822
|
-
lines.push('');
|
|
4823
|
-
}
|
|
4824
|
-
lines.push(`💡 \`roadmap_update(id, status: "in-progress")\` to start · \`roadmap_next\` for top priority item`);
|
|
4825
|
-
return lines.join('\n');
|
|
4826
|
-
}
|
|
4827
|
-
case 'roadmap_next': {
|
|
4828
|
-
const { instance_id: rid, tag: filterTag } = args;
|
|
4829
|
-
const redis = await getConnection(rid);
|
|
4830
|
-
const all = await redis.hgetall(`cachly:roadmap:${rid}`);
|
|
4831
|
-
if (!all || Object.keys(all).length === 0) {
|
|
4832
|
-
return '📋 **Roadmap is empty.** Use `roadmap_add` to plan your first task.';
|
|
4833
|
-
}
|
|
4834
|
-
const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
4835
|
-
const PRIORITY_ICON = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
|
|
4836
|
-
let items = Object.values(all)
|
|
4837
|
-
.map(v => JSON.parse(v))
|
|
4838
|
-
.filter(i => i.status === 'in-progress' || i.status === 'planned')
|
|
4839
|
-
.filter(i => !filterTag || i.tags.includes(filterTag));
|
|
4840
|
-
if (items.length === 0)
|
|
4841
|
-
return '🎉 **No open roadmap items!** All tasks are done (or use `roadmap_list` to check).';
|
|
4842
|
-
// in-progress first, then by priority
|
|
4843
|
-
items.sort((a, b) => {
|
|
4844
|
-
if (a.status === 'in-progress' && b.status !== 'in-progress')
|
|
4845
|
-
return -1;
|
|
4846
|
-
if (b.status === 'in-progress' && a.status !== 'in-progress')
|
|
4847
|
-
return 1;
|
|
4848
|
-
return (PRIORITY_ORDER[a.priority] ?? 99) - (PRIORITY_ORDER[b.priority] ?? 99);
|
|
4849
|
-
});
|
|
4850
|
-
const next = items[0];
|
|
4851
|
-
const remaining = items.length - 1;
|
|
4852
|
-
const tags = next.tags.length ? `\nTags: ${next.tags.join(', ')}` : '';
|
|
4853
|
-
const milestone = next.milestone ? `\nMilestone: ${next.milestone}` : '';
|
|
4854
|
-
const notes = next.notes ? `\nNotes: ${next.notes.split('\n').pop()?.slice(0, 120)}` : '';
|
|
4855
|
-
return [
|
|
4856
|
-
`${next.status === 'in-progress' ? '⚡' : '📋'} **Next up: ${next.title}**`,
|
|
4857
|
-
``,
|
|
4858
|
-
`ID: \`${next.id}\``,
|
|
4859
|
-
`Priority: ${PRIORITY_ICON[next.priority] ?? '⚪'} ${next.priority}`,
|
|
4860
|
-
`Status: ${next.status}`,
|
|
4861
|
-
next.description ? `\nWhat to do:\n${next.description}` : '',
|
|
4862
|
-
tags, milestone, notes,
|
|
4863
|
-
``,
|
|
4864
|
-
remaining > 0 ? `(+${remaining} more open item${remaining !== 1 ? 's' : ''} in roadmap)` : '(last open item)',
|
|
4865
|
-
``,
|
|
4866
|
-
next.status === 'planned'
|
|
4867
|
-
? `💡 Start with: \`roadmap_update(id: "${next.id}", status: "in-progress")\``
|
|
4868
|
-
: `💡 Finish with: \`roadmap_update(id: "${next.id}", status: "done", notes: "...")\``,
|
|
4869
|
-
].filter(s => s !== undefined).join('\n');
|
|
4870
|
-
}
|
|
4871
3405
|
default:
|
|
4872
3406
|
return null;
|
|
4873
3407
|
}
|
|
4874
3408
|
}
|
|
4875
3409
|
// ── Server setup ──────────────────────────────────────────────────────────────
|
|
4876
|
-
const server = new Server({ name: 'cachly-mcp', version:
|
|
3410
|
+
const server = new Server({ name: 'cachly-mcp', version: '0.4.0' }, { capabilities: { tools: {} } });
|
|
4877
3411
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
4878
|
-
// ── Auto-session management ───────────────────────────────────────────────────
|
|
4879
|
-
// Transparently starts a Brain session on the first tool call of a process
|
|
4880
|
-
// and saves a session_end summary on SIGTERM/exit so users never have to call
|
|
4881
|
-
// session_start/session_end manually.
|
|
4882
|
-
let _autoSessionInstanceId = null;
|
|
4883
|
-
let _autoSessionStarted = false;
|
|
4884
|
-
let _autoSessionToolCount = 0;
|
|
4885
|
-
async function autoStartSession(instanceId) {
|
|
4886
|
-
if (_autoSessionStarted)
|
|
4887
|
-
return;
|
|
4888
|
-
_autoSessionStarted = true;
|
|
4889
|
-
_autoSessionInstanceId = instanceId;
|
|
4890
|
-
try {
|
|
4891
|
-
await handleTool('session_start', { instance_id: instanceId, focus: 'auto (MCP session)' });
|
|
4892
|
-
}
|
|
4893
|
-
catch { /* non-fatal — session tracking is a best-effort feature */ }
|
|
4894
|
-
// Auto-index the project if it hasn't been indexed in the last 24h.
|
|
4895
|
-
// This is the main lever for growing token savings: more indexed code →
|
|
4896
|
-
// more semantic cache hits → fewer LLM calls for repeated questions.
|
|
4897
|
-
if (process.env.CACHLY_AUTO_INDEX !== 'false') {
|
|
4898
|
-
try {
|
|
4899
|
-
const redis = await getConnection(instanceId);
|
|
4900
|
-
const lastIndexed = await redis.get(`cachly:index:last_indexed:${instanceId}`);
|
|
4901
|
-
const staleMs = 24 * 60 * 60 * 1000;
|
|
4902
|
-
const isStale = !lastIndexed || (Date.now() - parseInt(lastIndexed, 10)) > staleMs;
|
|
4903
|
-
if (isStale) {
|
|
4904
|
-
// Mark as indexing now to prevent concurrent re-runs.
|
|
4905
|
-
await redis.set(`cachly:index:last_indexed:${instanceId}`, String(Date.now()), 'EX', 90000);
|
|
4906
|
-
// Run in background — don't block the first tool call.
|
|
4907
|
-
handleTool('index_project', {
|
|
4908
|
-
instance_id: instanceId,
|
|
4909
|
-
dir: process.cwd(),
|
|
4910
|
-
max_files: 150,
|
|
4911
|
-
ttl: 86400 * 7, // cache for 7 days
|
|
4912
|
-
namespace: 'cachly:sem:code',
|
|
4913
|
-
}).catch(() => undefined);
|
|
4914
|
-
}
|
|
4915
|
-
}
|
|
4916
|
-
catch { /* never block the session on indexing errors */ }
|
|
4917
|
-
}
|
|
4918
|
-
}
|
|
4919
|
-
async function autoEndSession() {
|
|
4920
|
-
if (!_autoSessionStarted || !_autoSessionInstanceId)
|
|
4921
|
-
return;
|
|
4922
|
-
_autoSessionStarted = false;
|
|
4923
|
-
try {
|
|
4924
|
-
await handleTool('session_end', {
|
|
4925
|
-
instance_id: _autoSessionInstanceId,
|
|
4926
|
-
summary: `Auto-ended after ${_autoSessionToolCount} tool call(s). Session was started automatically.`,
|
|
4927
|
-
files_changed: [],
|
|
4928
|
-
lessons_learned: 0,
|
|
4929
|
-
});
|
|
4930
|
-
}
|
|
4931
|
-
catch { /* non-fatal */ }
|
|
4932
|
-
}
|
|
4933
3412
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
4934
3413
|
const { name, arguments: args } = request.params;
|
|
4935
|
-
// Auto-start brain session on first tool call that has an instance_id.
|
|
4936
|
-
// Skip session management tools to avoid recursion.
|
|
4937
|
-
const sessionTools = new Set(['session_start', 'session_end', 'auto_learn_session']);
|
|
4938
|
-
if (!sessionTools.has(name) && !_autoSessionStarted) {
|
|
4939
|
-
const instanceId = args?.instance_id;
|
|
4940
|
-
if (instanceId) {
|
|
4941
|
-
await autoStartSession(instanceId).catch(() => undefined);
|
|
4942
|
-
}
|
|
4943
|
-
}
|
|
4944
|
-
if (!sessionTools.has(name))
|
|
4945
|
-
_autoSessionToolCount++;
|
|
4946
3414
|
try {
|
|
4947
3415
|
const text = await handleTool(name, (args ?? {}));
|
|
4948
3416
|
return { content: [{ type: 'text', text }] };
|
|
@@ -4953,15 +3421,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4953
3421
|
throw new McpError(ErrorCode.InternalError, err.message);
|
|
4954
3422
|
}
|
|
4955
3423
|
});
|
|
4956
|
-
// Graceful shutdown – close all Redis connections
|
|
3424
|
+
// Graceful shutdown – close all Redis connections
|
|
4957
3425
|
process.on('SIGTERM', async () => {
|
|
4958
|
-
await autoEndSession().catch(() => undefined);
|
|
4959
|
-
for (const [, client] of pool)
|
|
4960
|
-
await client.quit().catch(() => undefined);
|
|
4961
|
-
process.exit(0);
|
|
4962
|
-
});
|
|
4963
|
-
process.on('SIGINT', async () => {
|
|
4964
|
-
await autoEndSession().catch(() => undefined);
|
|
4965
3426
|
for (const [, client] of pool)
|
|
4966
3427
|
await client.quit().catch(() => undefined);
|
|
4967
3428
|
process.exit(0);
|
|
@@ -4980,8 +3441,6 @@ const EDITOR_FILES = {
|
|
|
4980
3441
|
windsurf: '.windsurf/mcp.json',
|
|
4981
3442
|
copilot: '.vscode/mcp.json',
|
|
4982
3443
|
continue: '.continue/config.json',
|
|
4983
|
-
cline: '.vscode/mcp.json', // Cline respects VS Code project MCP config
|
|
4984
|
-
zed: '.zed/settings.json', // Zed project-level context_servers
|
|
4985
3444
|
};
|
|
4986
3445
|
const CLAUDE_MD_MARKER_START = '<!-- cachly-brain-start -->';
|
|
4987
3446
|
const CLAUDE_MD_MARKER_END = '<!-- cachly-brain-end -->';
|
|
@@ -4996,20 +3455,6 @@ function buildMcpConfig(apiKey, instanceId, editor) {
|
|
|
4996
3455
|
},
|
|
4997
3456
|
}, null, 2);
|
|
4998
3457
|
}
|
|
4999
|
-
if (editor === 'zed') {
|
|
5000
|
-
return JSON.stringify({
|
|
5001
|
-
context_servers: {
|
|
5002
|
-
cachly: {
|
|
5003
|
-
command: {
|
|
5004
|
-
path: 'npx',
|
|
5005
|
-
args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
5006
|
-
env: { CACHLY_API_URL: 'https://api.cachly.dev', CACHLY_JWT: apiKey, CACHLY_BRAIN_INSTANCE_ID: instanceId },
|
|
5007
|
-
},
|
|
5008
|
-
settings: {},
|
|
5009
|
-
},
|
|
5010
|
-
},
|
|
5011
|
-
}, null, 2);
|
|
5012
|
-
}
|
|
5013
3458
|
return JSON.stringify({
|
|
5014
3459
|
mcpServers: {
|
|
5015
3460
|
cachly: {
|
|
@@ -5022,54 +3467,48 @@ function buildMcpConfig(apiKey, instanceId, editor) {
|
|
|
5022
3467
|
}
|
|
5023
3468
|
function buildClaudeMdBlock(instanceId) {
|
|
5024
3469
|
return `${CLAUDE_MD_MARKER_START}
|
|
5025
|
-
## Cachly AI Brain
|
|
3470
|
+
## Cachly AI Brain — Persistent Memory
|
|
5026
3471
|
|
|
5027
|
-
|
|
5028
|
-
Your instance ID: \`${instanceId}\`
|
|
3472
|
+
Your AI Brain instance ID: \`${instanceId}\`
|
|
5029
3473
|
|
|
5030
|
-
>
|
|
5031
|
-
>
|
|
3474
|
+
> The Brain gives you persistent memory across sessions. Every lesson you learn,
|
|
3475
|
+
> every bug you fix, every architecture decision — recalled instantly next time.
|
|
3476
|
+
> instance_id is pre-filled below. Do NOT ask the user for it.
|
|
5032
3477
|
|
|
5033
|
-
|
|
3478
|
+
### MANDATORY: Call at the START of every session
|
|
3479
|
+
\`\`\`
|
|
3480
|
+
session_start(instance_id="${instanceId}", focus="<what you're working on today>")
|
|
3481
|
+
\`\`\`
|
|
3482
|
+
This loads your previous context, recent lessons, and open tasks automatically.
|
|
5034
3483
|
|
|
5035
|
-
###
|
|
3484
|
+
### MANDATORY: Call at the END of every session
|
|
5036
3485
|
\`\`\`
|
|
5037
|
-
|
|
5038
|
-
instance_id = "${instanceId}",
|
|
5039
|
-
key = "wip-registry",
|
|
5040
|
-
content = "EDITING: <file:line> — goal: <what you are implementing>",
|
|
5041
|
-
category = "bug",
|
|
5042
|
-
ttl = 86400,
|
|
5043
|
-
)
|
|
3486
|
+
session_end(instance_id="${instanceId}", summary="<2-3 sentences on what was done>", files_changed=["src/..."], lessons_learned=1)
|
|
5044
3487
|
\`\`\`
|
|
5045
|
-
After the edit is complete, update \`content\` to \`"DONE: <file> — <what was completed>"\`.
|
|
5046
3488
|
|
|
5047
|
-
### After fixing
|
|
3489
|
+
### After fixing ANY bug or solving a hard problem
|
|
5048
3490
|
\`\`\`
|
|
5049
3491
|
learn_from_attempts(
|
|
5050
|
-
instance_id
|
|
5051
|
-
topic
|
|
5052
|
-
outcome
|
|
5053
|
-
what_worked
|
|
5054
|
-
what_failed
|
|
5055
|
-
|
|
5056
|
-
file_paths
|
|
5057
|
-
|
|
5058
|
-
tags = ["tag1"],
|
|
3492
|
+
instance_id="${instanceId}",
|
|
3493
|
+
topic="category:keyword", # e.g. "deploy:web", "bug:redis-timeout", "arch:auth"
|
|
3494
|
+
outcome="success",
|
|
3495
|
+
what_worked="exact solution",
|
|
3496
|
+
what_failed="what didn't work (optional)",
|
|
3497
|
+
commands=["the command that worked"],
|
|
3498
|
+
file_paths=["relevant/file.ts"],
|
|
3499
|
+
severity="critical"|"major"|"minor",
|
|
5059
3500
|
)
|
|
5060
3501
|
\`\`\`
|
|
5061
3502
|
|
|
5062
|
-
### Before starting any task
|
|
3503
|
+
### Before starting any non-trivial task
|
|
5063
3504
|
\`\`\`
|
|
5064
|
-
|
|
5065
|
-
instance_id = "${instanceId}",
|
|
5066
|
-
query = "<describe what you are about to do>",
|
|
5067
|
-
)
|
|
3505
|
+
recall_best_solution(instance_id="${instanceId}", topic="relevant:topic")
|
|
5068
3506
|
\`\`\`
|
|
5069
3507
|
|
|
5070
|
-
###
|
|
5071
|
-
|
|
5072
|
-
|
|
3508
|
+
### If a stored lesson is wrong
|
|
3509
|
+
\`\`\`
|
|
3510
|
+
forget_lesson(instance_id="${instanceId}", topic="wrong:topic")
|
|
3511
|
+
\`\`\`
|
|
5073
3512
|
${CLAUDE_MD_MARKER_END}`;
|
|
5074
3513
|
}
|
|
5075
3514
|
async function writeClaudeMd(projectDir, instanceId) {
|
|
@@ -5141,8 +3580,7 @@ if (process.argv[2] === 'setup') {
|
|
|
5141
3580
|
else {
|
|
5142
3581
|
const AUTH_BASE = 'https://auth.cachly.dev/realms/cachly/protocol/openid-connect';
|
|
5143
3582
|
const CLIENT_ID = 'cachly-cli';
|
|
5144
|
-
console.log('Step 1: Sign in to cachly (free
|
|
5145
|
-
// Start device flow
|
|
3583
|
+
console.log('Step 1: Sign in to cachly (free — no credit card required)\n');
|
|
5146
3584
|
let deviceCode = '', userCode = '', verifyUri = '', pollInterval = 5000;
|
|
5147
3585
|
try {
|
|
5148
3586
|
const deviceRes = await fetch(`${AUTH_BASE}/auth/device`, {
|
|
@@ -5151,7 +3589,7 @@ if (process.argv[2] === 'setup') {
|
|
|
5151
3589
|
body: `client_id=${CLIENT_ID}&scope=openid`,
|
|
5152
3590
|
});
|
|
5153
3591
|
if (!deviceRes.ok)
|
|
5154
|
-
throw new Error(`
|
|
3592
|
+
throw new Error(`HTTP ${deviceRes.status}`);
|
|
5155
3593
|
const data = await deviceRes.json();
|
|
5156
3594
|
deviceCode = data.device_code;
|
|
5157
3595
|
userCode = data.user_code;
|
|
@@ -5159,19 +3597,17 @@ if (process.argv[2] === 'setup') {
|
|
|
5159
3597
|
pollInterval = (data.interval ?? 5) * 1000;
|
|
5160
3598
|
}
|
|
5161
3599
|
catch (e) {
|
|
5162
|
-
console.error(`\
|
|
5163
|
-
console.error('
|
|
3600
|
+
console.error(`\nCould not reach auth service: ${e.message}`);
|
|
3601
|
+
console.error('Fallback: sign in at https://cachly.dev and paste your API token.\n');
|
|
5164
3602
|
token = await ask(' Paste API token (cky_live_...): ');
|
|
5165
3603
|
if (!token) {
|
|
5166
|
-
console.error('\nToken
|
|
3604
|
+
console.error('\nToken required. Aborting.\n');
|
|
5167
3605
|
rl.close();
|
|
5168
3606
|
process.exit(1);
|
|
5169
3607
|
}
|
|
5170
|
-
|
|
5171
|
-
deviceCode = ''; // mark as fallback so we skip polling
|
|
3608
|
+
deviceCode = '';
|
|
5172
3609
|
}
|
|
5173
3610
|
if (deviceCode) {
|
|
5174
|
-
// Open browser
|
|
5175
3611
|
console.log(` Code: \x1b[1;33m${userCode}\x1b[0m`);
|
|
5176
3612
|
console.log(` URL: ${verifyUri}\n`);
|
|
5177
3613
|
try {
|
|
@@ -5181,11 +3617,10 @@ if (process.argv[2] === 'setup') {
|
|
|
5181
3617
|
console.log(' ✓ Browser opened — confirm the code above to continue...\n');
|
|
5182
3618
|
}
|
|
5183
3619
|
catch {
|
|
5184
|
-
console.log(' 👉 Open the URL above in your browser and
|
|
3620
|
+
console.log(' 👉 Open the URL above in your browser and confirm the code.\n');
|
|
5185
3621
|
}
|
|
5186
|
-
// Poll for token
|
|
5187
3622
|
process.stdout.write(' Waiting for authorization');
|
|
5188
|
-
const deadline = Date.now() + 10 * 60 * 1000;
|
|
3623
|
+
const deadline = Date.now() + 10 * 60 * 1000;
|
|
5189
3624
|
while (Date.now() < deadline) {
|
|
5190
3625
|
await new Promise(r => setTimeout(r, pollInterval));
|
|
5191
3626
|
process.stdout.write('.');
|
|
@@ -5195,17 +3630,16 @@ if (process.argv[2] === 'setup') {
|
|
|
5195
3630
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
5196
3631
|
body: `client_id=${CLIENT_ID}&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=${deviceCode}`,
|
|
5197
3632
|
});
|
|
5198
|
-
const
|
|
5199
|
-
if (
|
|
5200
|
-
token =
|
|
3633
|
+
const td = await tokenRes.json();
|
|
3634
|
+
if (td.access_token) {
|
|
3635
|
+
token = td.access_token;
|
|
5201
3636
|
console.log(' \x1b[32m✓ Authorized!\x1b[0m\n');
|
|
5202
3637
|
break;
|
|
5203
3638
|
}
|
|
5204
|
-
|
|
5205
|
-
if (tokenData.error === 'slow_down')
|
|
3639
|
+
if (td.error === 'slow_down')
|
|
5206
3640
|
pollInterval = Math.min(pollInterval + 2000, 15000);
|
|
5207
|
-
else if (
|
|
5208
|
-
console.error(`\nAuth error: ${
|
|
3641
|
+
else if (td.error && td.error !== 'authorization_pending') {
|
|
3642
|
+
console.error(`\nAuth error: ${td.error}. Aborting.\n`);
|
|
5209
3643
|
rl.close();
|
|
5210
3644
|
process.exit(1);
|
|
5211
3645
|
}
|
|
@@ -5213,38 +3647,47 @@ if (process.argv[2] === 'setup') {
|
|
|
5213
3647
|
catch { /* network hiccup — keep polling */ }
|
|
5214
3648
|
}
|
|
5215
3649
|
if (!token) {
|
|
5216
|
-
console.error('\nTimed out
|
|
3650
|
+
console.error('\nTimed out. Aborting.\n');
|
|
5217
3651
|
rl.close();
|
|
5218
3652
|
process.exit(1);
|
|
5219
3653
|
}
|
|
5220
|
-
console.log('');
|
|
5221
3654
|
}
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
// Non-fatal: fall back to using the Keycloak JWT directly.
|
|
5245
|
-
// It will expire but setup still works for now.
|
|
3655
|
+
// Exchange short-lived Keycloak JWT → long-lived cky_live_ API key
|
|
3656
|
+
if (token.startsWith('eyJ')) {
|
|
3657
|
+
process.stdout.write('⏳ Generating your API key...');
|
|
3658
|
+
try {
|
|
3659
|
+
const keyRes = await fetch(`${API_URL}/api/v1/api-keys`, {
|
|
3660
|
+
method: 'POST',
|
|
3661
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
3662
|
+
body: JSON.stringify({ name: 'cachly-mcp-setup', scope: 'read_write' }),
|
|
3663
|
+
});
|
|
3664
|
+
if (!keyRes.ok)
|
|
3665
|
+
throw new Error(`HTTP ${keyRes.status}`);
|
|
3666
|
+
const keyBody = await keyRes.json();
|
|
3667
|
+
if (keyBody.key) {
|
|
3668
|
+
token = keyBody.key;
|
|
3669
|
+
console.log(' ✓\n');
|
|
3670
|
+
}
|
|
3671
|
+
else
|
|
3672
|
+
throw new Error('no key in response');
|
|
3673
|
+
}
|
|
3674
|
+
catch (e) {
|
|
3675
|
+
console.log(` (skipped — JWT will be used directly)\n`);
|
|
3676
|
+
}
|
|
5246
3677
|
}
|
|
5247
3678
|
}
|
|
3679
|
+
// ── Step 1b: Auto-provision a Brain instance if none exist ─────────────────
|
|
3680
|
+
// POST /api/v1/instances/auto is idempotent — returns existing or creates free Brain
|
|
3681
|
+
let autoProvisioned = false;
|
|
3682
|
+
try {
|
|
3683
|
+
const autoRes = await fetch(`${API_URL}/api/v1/instances/auto`, {
|
|
3684
|
+
method: 'POST',
|
|
3685
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
3686
|
+
});
|
|
3687
|
+
if (autoRes.ok)
|
|
3688
|
+
autoProvisioned = true;
|
|
3689
|
+
}
|
|
3690
|
+
catch { /* non-fatal */ }
|
|
5248
3691
|
// ── Step 2: Fetch & pick instance ─────────────────────────────────────────
|
|
5249
3692
|
process.stdout.write('⏳ Fetching your instances...');
|
|
5250
3693
|
let instances = [];
|
|
@@ -5258,70 +3701,32 @@ if (process.argv[2] === 'setup') {
|
|
|
5258
3701
|
instances = (body.data ?? []).filter(i => i.status === 'running');
|
|
5259
3702
|
}
|
|
5260
3703
|
catch (e) {
|
|
5261
|
-
|
|
5262
|
-
console.error(
|
|
5263
|
-
if (msg.includes('401')) {
|
|
5264
|
-
console.error('Token rejected. Get a valid token at https://cachly.dev/setup-ai\n');
|
|
5265
|
-
try {
|
|
5266
|
-
const { execSync } = await import('node:child_process');
|
|
5267
|
-
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
5268
|
-
execSync(`${openCmd} https://cachly.dev/setup-ai`, { stdio: 'ignore' });
|
|
5269
|
-
}
|
|
5270
|
-
catch { /* ignore */ }
|
|
5271
|
-
}
|
|
3704
|
+
console.error(`\n\nFailed to fetch instances: ${e.message}`);
|
|
3705
|
+
console.error('Check your token and try again.\n');
|
|
5272
3706
|
rl.close();
|
|
5273
3707
|
process.exit(1);
|
|
5274
3708
|
}
|
|
5275
3709
|
console.log(` found ${instances.length}\n`);
|
|
5276
3710
|
if (instances.length === 0) {
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
// Returned an existing instance.
|
|
5289
|
-
instances = [autoBody.instance];
|
|
5290
|
-
}
|
|
5291
|
-
else if (autoBody.instance_id) {
|
|
5292
|
-
// Newly created — poll until running or give up after 30 s.
|
|
5293
|
-
const newId = autoBody.instance_id;
|
|
5294
|
-
console.log(` ✓ created (${newId.slice(0, 8)}…)\n`);
|
|
5295
|
-
process.stdout.write('⏳ Waiting for instance to start');
|
|
5296
|
-
for (let attempt = 0; attempt < 15; attempt++) {
|
|
5297
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
5298
|
-
process.stdout.write('.');
|
|
5299
|
-
try {
|
|
5300
|
-
const checkRes = await fetch(`${API_URL}/api/v1/instances/${newId}`, { headers: { Authorization: `Bearer ${token}` } });
|
|
5301
|
-
if (checkRes.ok) {
|
|
5302
|
-
const inst = await checkRes.json();
|
|
5303
|
-
if (inst.status === 'running') {
|
|
5304
|
-
instances = [inst];
|
|
5305
|
-
break;
|
|
5306
|
-
}
|
|
5307
|
-
}
|
|
3711
|
+
if (autoProvisioned) {
|
|
3712
|
+
// Poll up to 10× every 3s (30s total) waiting for the Brain to start
|
|
3713
|
+
process.stdout.write(' waiting for Brain to start');
|
|
3714
|
+
for (let attempt = 0; attempt < 10 && instances.length === 0; attempt++) {
|
|
3715
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
3716
|
+
process.stdout.write('.');
|
|
3717
|
+
try {
|
|
3718
|
+
const retry = await fetch(`${API_URL}/api/v1/instances`, { headers: { Authorization: `Bearer ${token}` } });
|
|
3719
|
+
if (retry.ok) {
|
|
3720
|
+
const b = await retry.json();
|
|
3721
|
+
instances = (b.data ?? []).filter(i => i.status === 'running');
|
|
5308
3722
|
}
|
|
5309
|
-
catch { /* keep polling */ }
|
|
5310
3723
|
}
|
|
5311
|
-
|
|
3724
|
+
catch { /* ignore */ }
|
|
5312
3725
|
}
|
|
5313
|
-
|
|
5314
|
-
catch (e) {
|
|
5315
|
-
console.log(` failed: ${e.message}\n`);
|
|
3726
|
+
process.stdout.write(instances.length > 0 ? ' ready!\n' : '\n');
|
|
5316
3727
|
}
|
|
5317
3728
|
if (instances.length === 0) {
|
|
5318
|
-
console.error('\
|
|
5319
|
-
try {
|
|
5320
|
-
const { execSync } = await import('node:child_process');
|
|
5321
|
-
const openCmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
5322
|
-
execSync(`${openCmd} https://cachly.dev/instances`, { stdio: 'ignore' });
|
|
5323
|
-
}
|
|
5324
|
-
catch { /* ignore */ }
|
|
3729
|
+
console.error('\nBrain provisioning is taking longer than expected. Run setup again in a minute.\n');
|
|
5325
3730
|
rl.close();
|
|
5326
3731
|
process.exit(1);
|
|
5327
3732
|
}
|
|
@@ -5338,8 +3743,6 @@ if (process.argv[2] === 'setup') {
|
|
|
5338
3743
|
// ── Step 3: Detect editors ────────────────────────────────────────────────
|
|
5339
3744
|
const cwd = process.cwd();
|
|
5340
3745
|
const detected = [];
|
|
5341
|
-
const { homedir } = await import('node:os');
|
|
5342
|
-
const home = homedir();
|
|
5343
3746
|
// Claude Code: always include (CLAUDE.md is universal)
|
|
5344
3747
|
detected.push('claude');
|
|
5345
3748
|
if (existsSync(resolve(cwd, '.cursor')))
|
|
@@ -5350,47 +3753,14 @@ if (process.argv[2] === 'setup') {
|
|
|
5350
3753
|
detected.push('copilot');
|
|
5351
3754
|
if (existsSync(resolve(cwd, '.continue')))
|
|
5352
3755
|
detected.push('continue');
|
|
5353
|
-
|
|
5354
|
-
const clineGlobalDir = process.platform === 'darwin'
|
|
5355
|
-
? resolve(home, 'Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev')
|
|
5356
|
-
: resolve(home, '.vscode/extensions');
|
|
5357
|
-
if (existsSync(clineGlobalDir) && !detected.includes('copilot'))
|
|
5358
|
-
detected.push('cline');
|
|
5359
|
-
else if (existsSync(clineGlobalDir))
|
|
5360
|
-
detected.push('cline'); // both copilot + cline share .vscode/mcp.json — fine
|
|
5361
|
-
// Zed editor — detect via app data dir
|
|
5362
|
-
const zedDir = process.platform === 'darwin'
|
|
5363
|
-
? resolve(home, 'Library/Application Support/Zed')
|
|
5364
|
-
: resolve(home, '.config/zed');
|
|
5365
|
-
if (existsSync(zedDir))
|
|
5366
|
-
detected.push('zed');
|
|
5367
|
-
const editorLabel = (e) => ({ claude: 'Claude Code', cursor: 'Cursor', windsurf: 'Windsurf', copilot: 'GitHub Copilot', continue: 'Continue.dev', cline: 'Cline (VSCode)', zed: 'Zed' })[e] ?? e;
|
|
3756
|
+
const editorLabel = (e) => ({ claude: 'Claude Code', cursor: 'Cursor', windsurf: 'Windsurf', copilot: 'GitHub Copilot', continue: 'Continue.dev' })[e] ?? e;
|
|
5368
3757
|
console.log(`Step 3: Detected editors: ${detected.map(editorLabel).join(', ')}`);
|
|
5369
3758
|
const editorsRaw = await ask(` Configure for which? [all / ${detected.join('/')}] (Enter = all): `);
|
|
5370
3759
|
const editorsToSetup = !editorsRaw || editorsRaw.toLowerCase() === 'all'
|
|
5371
3760
|
? detected
|
|
5372
3761
|
: editorsRaw.split(/[,\s]+/).map(s => s.toLowerCase()).filter(s => EDITOR_FILES[s]);
|
|
5373
3762
|
console.log('');
|
|
5374
|
-
// ── Step 4:
|
|
5375
|
-
console.log('Step 4: Stay in the loop (optional)');
|
|
5376
|
-
const email = await ask(' Email for release notes & tips? [Enter to skip]: ');
|
|
5377
|
-
if (email && email.includes('@')) {
|
|
5378
|
-
try {
|
|
5379
|
-
await fetch(`${API_URL}/api/newsletter/subscribe`, {
|
|
5380
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
5381
|
-
body: JSON.stringify({ email: email.trim().toLowerCase(), source: 'mcp-setup' }),
|
|
5382
|
-
signal: AbortSignal.timeout(5000),
|
|
5383
|
-
});
|
|
5384
|
-
console.log('✅ Subscribed — you\'ll only hear from us when it matters.\n');
|
|
5385
|
-
}
|
|
5386
|
-
catch {
|
|
5387
|
-
console.log('⚠️ Could not subscribe (no internet?). Continuing...\n');
|
|
5388
|
-
}
|
|
5389
|
-
}
|
|
5390
|
-
else {
|
|
5391
|
-
console.log(' Skipped.\n');
|
|
5392
|
-
}
|
|
5393
|
-
// ── Step 5: Write editor configs ──────────────────────────────────────────
|
|
3763
|
+
// ── Step 4: Write editor configs ──────────────────────────────────────────
|
|
5394
3764
|
for (const editor of editorsToSetup) {
|
|
5395
3765
|
const configFile = EDITOR_FILES[editor] ?? '.mcp.json';
|
|
5396
3766
|
const configPath = resolve(cwd, configFile);
|
|
@@ -5398,163 +3768,16 @@ if (process.argv[2] === 'setup') {
|
|
|
5398
3768
|
await writeFile(configPath, buildMcpConfig(token, instance.id, editor), 'utf-8');
|
|
5399
3769
|
console.log(`✅ Written: ${configFile}`);
|
|
5400
3770
|
}
|
|
5401
|
-
// ── Step
|
|
3771
|
+
// ── Step 5: CLAUDE.md (always — idempotent) ───────────────────────────────
|
|
5402
3772
|
const mdResult = await writeClaudeMd(cwd, instance.id);
|
|
5403
3773
|
const mdLabel = mdResult === 'updated' ? '✅ Updated' : mdResult === 'appended' ? '✅ Appended to' : '✅ Written';
|
|
5404
3774
|
console.log(`${mdLabel}: CLAUDE.md`);
|
|
5405
3775
|
rl.close();
|
|
5406
3776
|
console.log(`\n🧠 Done! Restart your editor — the \`cachly\` MCP tools will appear.`);
|
|
5407
3777
|
console.log(` Your AI now has persistent memory across every session.\n`);
|
|
5408
|
-
// ── Step 7: Show first session_start preview (Aha moment) ────────────────
|
|
5409
|
-
// Fetch brain health from the API to show the user what their agent will see.
|
|
5410
|
-
process.stdout.write('\n⏳ Fetching your Brain health preview...');
|
|
5411
|
-
try {
|
|
5412
|
-
const brainRes = await fetch(`${API_URL}/api/v1/instances/${instance.id}/brain/stats`, {
|
|
5413
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
5414
|
-
signal: AbortSignal.timeout(6000),
|
|
5415
|
-
});
|
|
5416
|
-
if (brainRes.ok) {
|
|
5417
|
-
const brainData = await brainRes.json();
|
|
5418
|
-
const lessons = brainData.lesson_count ?? 0;
|
|
5419
|
-
const contexts = brainData.context_count ?? 0;
|
|
5420
|
-
const score = brainData.quality_score ?? 0;
|
|
5421
|
-
const level = lessons === 0 ? 'Intern 🌱' :
|
|
5422
|
-
lessons < 10 ? 'Junior Dev 🔧' :
|
|
5423
|
-
lessons < 30 ? 'Mid Dev ⚡' :
|
|
5424
|
-
lessons < 60 ? 'Senior Dev 🧠' :
|
|
5425
|
-
lessons < 100 ? 'Staff Eng 🚀' : 'Principal Eng 🏆';
|
|
5426
|
-
console.log(' ✓\n');
|
|
5427
|
-
console.log('┌──────────────────────────────────────────────────────┐');
|
|
5428
|
-
console.log(`│ 🧠 Brain Health Report │`);
|
|
5429
|
-
console.log('├──────────────────────────────────────────────────────┤');
|
|
5430
|
-
console.log(`│ Lessons stored : ${String(lessons).padEnd(6)} │ Level: ${level.padEnd(20)}│`);
|
|
5431
|
-
console.log(`│ Context entries : ${String(contexts).padEnd(6)} │ Quality score: ${String(Math.round(score * 100)).padEnd(3)}% │`);
|
|
5432
|
-
if (lessons === 0) {
|
|
5433
|
-
console.log('├──────────────────────────────────────────────────────┤');
|
|
5434
|
-
console.log('│ Your Brain is empty and ready to learn. │');
|
|
5435
|
-
console.log('│ After your first coding session it will contain: │');
|
|
5436
|
-
console.log('│ • Lessons from every bug you fixed │');
|
|
5437
|
-
console.log('│ • Your project\'s indexed files │');
|
|
5438
|
-
console.log('│ • A session summary for next time │');
|
|
5439
|
-
}
|
|
5440
|
-
console.log('└──────────────────────────────────────────────────────┘');
|
|
5441
|
-
}
|
|
5442
|
-
else {
|
|
5443
|
-
console.log(' (skipped)');
|
|
5444
|
-
}
|
|
5445
|
-
}
|
|
5446
|
-
catch {
|
|
5447
|
-
console.log(' (skipped)');
|
|
5448
|
-
}
|
|
5449
|
-
console.log(`\n Your AI will call session_start automatically when it uses the first tool.`);
|
|
5450
|
-
console.log(` To see your Brain at any time: https://cachly.dev/instances/${instance.id}\n`);
|
|
5451
|
-
process.exit(0);
|
|
5452
|
-
}
|
|
5453
|
-
// ── CLI: cachly index <dir> ───────────────────────────────────────────────────
|
|
5454
|
-
// Usage: npx @cachly-dev/mcp-server@latest index [./path/to/project]
|
|
5455
|
-
// Indexes the project directory into the Brain — perfect for CI/CD cron jobs.
|
|
5456
|
-
if (process.argv[2] === 'index') {
|
|
5457
|
-
const { resolve } = await import('node:path');
|
|
5458
|
-
const argv = process.argv.slice(3);
|
|
5459
|
-
const flag = (name) => { const i = argv.indexOf(`--${name}`); return i !== -1 ? argv[i + 1] : undefined; };
|
|
5460
|
-
const dir = resolve(flag('dir') ?? argv.find(a => !a.startsWith('--')) ?? '.');
|
|
5461
|
-
const instanceId = flag('instance-id') ?? process.env.CACHLY_BRAIN_INSTANCE_ID;
|
|
5462
|
-
const maxFiles = parseInt(flag('max-files') ?? '500', 10);
|
|
5463
|
-
const namespace = flag('namespace') ?? 'cachly:sem:code';
|
|
5464
|
-
if (!instanceId || !JWT) {
|
|
5465
|
-
console.error('\n❌ CACHLY_BRAIN_INSTANCE_ID and CACHLY_JWT must be set\n');
|
|
5466
|
-
console.error(' export CACHLY_BRAIN_INSTANCE_ID=<uuid>');
|
|
5467
|
-
console.error(' export CACHLY_JWT=<cky_live_...>');
|
|
5468
|
-
console.error(' npx @cachly-dev/mcp-server@latest index ./my-project\n');
|
|
5469
|
-
process.exit(1);
|
|
5470
|
-
}
|
|
5471
|
-
console.log(`\n📂 Indexing: ${dir}`);
|
|
5472
|
-
console.log(` Instance: ${instanceId.slice(0, 8)}… Max files: ${maxFiles}\n`);
|
|
5473
|
-
try {
|
|
5474
|
-
const result = await handleTool('index_project', {
|
|
5475
|
-
instance_id: instanceId,
|
|
5476
|
-
dir,
|
|
5477
|
-
max_files: maxFiles,
|
|
5478
|
-
ttl: 86400 * 7,
|
|
5479
|
-
namespace,
|
|
5480
|
-
});
|
|
5481
|
-
console.log(result);
|
|
5482
|
-
console.log('\n✅ Indexing complete.\n');
|
|
5483
|
-
}
|
|
5484
|
-
catch (err) {
|
|
5485
|
-
console.error(`\n❌ Indexing failed: ${err.message}\n`);
|
|
5486
|
-
process.exit(1);
|
|
5487
|
-
}
|
|
5488
3778
|
process.exit(0);
|
|
5489
3779
|
}
|
|
5490
3780
|
// ── Start ─────────────────────────────────────────────────────────────────────
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
if (!JWT) {
|
|
5494
|
-
process.stderr.write('\n' +
|
|
5495
|
-
'╔══════════════════════════════════════════════════════════════════╗\n' +
|
|
5496
|
-
'║ 🧠 cachly AI Brain — Setup required ║\n' +
|
|
5497
|
-
'╠══════════════════════════════════════════════════════════════════╣\n' +
|
|
5498
|
-
'║ ║\n' +
|
|
5499
|
-
'║ CACHLY_JWT is not set. Get your free credentials at: ║\n' +
|
|
5500
|
-
'║ ║\n' +
|
|
5501
|
-
'║ 👉 https://cachly.dev/setup-ai ║\n' +
|
|
5502
|
-
'║ ║\n' +
|
|
5503
|
-
'║ Then run the interactive setup wizard: ║\n' +
|
|
5504
|
-
'║ ║\n' +
|
|
5505
|
-
'║ npx @cachly-dev/mcp-server@latest setup ║\n' +
|
|
5506
|
-
'║ ║\n' +
|
|
5507
|
-
'║ Free tier — no credit card required. ║\n' +
|
|
5508
|
-
'╚══════════════════════════════════════════════════════════════════╝\n' +
|
|
5509
|
-
'\n');
|
|
5510
|
-
}
|
|
5511
|
-
// ── Update nudge (non-blocking, fire-and-forget) ─────────────────────────────
|
|
5512
|
-
// Check npm registry once per process start; if outdated, log to stderr so
|
|
5513
|
-
// the editor's MCP log shows an actionable one-liner. Skipped if opted out.
|
|
5514
|
-
if (!process.env.CACHLY_NO_UPDATE_CHECK) {
|
|
5515
|
-
(async () => {
|
|
5516
|
-
try {
|
|
5517
|
-
const res = await fetch(`https://registry.npmjs.org/@cachly-dev/mcp-server/latest`, { signal: AbortSignal.timeout(4000) });
|
|
5518
|
-
if (res.ok) {
|
|
5519
|
-
const data = await res.json();
|
|
5520
|
-
const latest = data?.version ?? '';
|
|
5521
|
-
if (latest && latest !== CURRENT_VERSION) {
|
|
5522
|
-
process.stderr.write(`\n⚡ cachly update available: ${CURRENT_VERSION} → ${latest}\n` +
|
|
5523
|
-
` Run: npx @cachly-dev/mcp-server@latest setup\n\n`);
|
|
5524
|
-
}
|
|
5525
|
-
}
|
|
5526
|
-
}
|
|
5527
|
-
catch { /* ignore – network unavailable or timeout */ }
|
|
5528
|
-
})();
|
|
5529
|
-
}
|
|
5530
|
-
const httpPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
|
|
5531
|
-
if (httpPort) {
|
|
5532
|
-
// ── HTTP mode (Streamable HTTP transport) ───────────────────────────────
|
|
5533
|
-
// Used for Smithery URL deployment: PORT=3000 node dist/index.js
|
|
5534
|
-
const { createServer } = await import('node:http');
|
|
5535
|
-
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
5536
|
-
await server.connect(transport);
|
|
5537
|
-
const httpServer = createServer((req, res) => {
|
|
5538
|
-
const url = new URL(req.url ?? '/', `http://localhost:${httpPort}`);
|
|
5539
|
-
if (url.pathname === '/health') {
|
|
5540
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
5541
|
-
res.end(JSON.stringify({ status: 'ok', version: CURRENT_VERSION }));
|
|
5542
|
-
return;
|
|
5543
|
-
}
|
|
5544
|
-
if (url.pathname === '/mcp' || url.pathname === '/') {
|
|
5545
|
-
transport.handleRequest(req, res);
|
|
5546
|
-
return;
|
|
5547
|
-
}
|
|
5548
|
-
res.writeHead(404);
|
|
5549
|
-
res.end('not found');
|
|
5550
|
-
});
|
|
5551
|
-
httpServer.listen(httpPort, () => {
|
|
5552
|
-
process.stderr.write(`cachly-mcp HTTP server listening on :${httpPort}\n`);
|
|
5553
|
-
});
|
|
5554
|
-
}
|
|
5555
|
-
else {
|
|
5556
|
-
// ── stdio mode (default for local editor use) ───────────────────────────
|
|
5557
|
-
const transport = new StdioServerTransport();
|
|
5558
|
-
await server.connect(transport);
|
|
5559
|
-
}
|
|
3781
|
+
const transport = new StdioServerTransport();
|
|
3782
|
+
await server.connect(transport);
|
|
5560
3783
|
//# sourceMappingURL=index.js.map
|