@cachly-dev/mcp-server 0.9.19 → 0.9.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/handlers/advanced.d.ts +6 -0
- package/dist/handlers/advanced.js +586 -0
- package/dist/handlers/advanced.js.map +1 -0
- package/dist/handlers/cache.d.ts +6 -0
- package/dist/handlers/cache.js +520 -0
- package/dist/handlers/cache.js.map +1 -0
- package/dist/handlers/context.d.ts +6 -0
- package/dist/handlers/context.js +177 -0
- package/dist/handlers/context.js.map +1 -0
- package/dist/handlers/fedbrain.d.ts +6 -0
- package/dist/handlers/fedbrain.js +1078 -0
- package/dist/handlers/fedbrain.js.map +1 -0
- package/dist/handlers/instances.d.ts +9 -0
- package/dist/handlers/instances.js +153 -0
- package/dist/handlers/instances.js.map +1 -0
- package/dist/handlers/roadmap.d.ts +6 -0
- package/dist/handlers/roadmap.js +165 -0
- package/dist/handlers/roadmap.js.map +1 -0
- package/dist/handlers/syndicate.d.ts +6 -0
- package/dist/handlers/syndicate.js +339 -0
- package/dist/handlers/syndicate.js.map +1 -0
- package/dist/handlers/team.d.ts +6 -0
- package/dist/handlers/team.js +1048 -0
- package/dist/handlers/team.js.map +1 -0
- package/dist/index.js +38 -4018
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/handlers/advanced.ts +637 -0
- package/src/handlers/cache.ts +634 -0
- package/src/handlers/context.ts +218 -0
- package/src/handlers/fedbrain.ts +1143 -0
- package/src/handlers/instances.ts +180 -0
- package/src/handlers/roadmap.ts +192 -0
- package/src/handlers/syndicate.ts +451 -0
- package/src/handlers/team.ts +1163 -0
- package/src/index.ts +42 -4460
package/dist/index.js
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { createHmac } from 'node:crypto';
|
|
4
|
-
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
5
|
-
import { readFileSync } from 'node:fs';
|
|
6
|
-
import { join, relative, extname } from 'node:path';
|
|
7
2
|
/**
|
|
8
3
|
* cachly MCP Server v0.4.0
|
|
9
4
|
*
|
|
@@ -167,12 +162,10 @@ async function pollDeviceFlow(flow) {
|
|
|
167
162
|
return 'pending';
|
|
168
163
|
}
|
|
169
164
|
}
|
|
170
|
-
// ── Confidence utils (imported from confidence.ts) ──────────────────────────
|
|
171
|
-
import { calculateConfidence, CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS, CONFIDENCE_STALE_DAYS, simpleHash } from './confidence.js';
|
|
172
165
|
// ── Embeddings (imported from embeddings.ts) ────────────────────────────────
|
|
173
|
-
import { setEmbedJwt
|
|
166
|
+
import { setEmbedJwt } from './embeddings.js';
|
|
174
167
|
// ── Search Engine (imported from search.ts) ─────────────────────────────────
|
|
175
|
-
import { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS, katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP,
|
|
168
|
+
import { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS, katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP, ZERO_RESULTS_LOG, zeroResultsTotal } from './search.js';
|
|
176
169
|
// ── Exported for testing (re-export from search.ts) ─────────────────────────
|
|
177
170
|
export { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS, katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP };
|
|
178
171
|
// ── Connection pool ───────────────────────────────────────────────────────────
|
|
@@ -274,33 +267,18 @@ async function apiFetch(path, options = {}) {
|
|
|
274
267
|
return undefined;
|
|
275
268
|
return res.json();
|
|
276
269
|
}
|
|
277
|
-
import { detectNamespace } from './namespace.js';
|
|
278
|
-
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge } from './ckg.js';
|
|
279
270
|
import { handleBrainTool } from './handlers/brain.js';
|
|
271
|
+
import { handleContextTool } from './handlers/context.js';
|
|
272
|
+
import { handleInstanceTool } from './handlers/instances.js';
|
|
273
|
+
import { handleCacheTool } from './handlers/cache.js';
|
|
274
|
+
import { handleTeamTool } from './handlers/team.js';
|
|
275
|
+
import { handleRoadmapTool } from './handlers/roadmap.js';
|
|
276
|
+
import { handleAdvancedTool } from './handlers/advanced.js';
|
|
277
|
+
import { handleSyndicateTool } from './handlers/syndicate.js';
|
|
278
|
+
import { handleFedbrainTool } from './handlers/fedbrain.js';
|
|
280
279
|
// ── Tools (imported from tools.ts) ─────────────────────────────────────────
|
|
281
280
|
import { TOOLS } from './tools.js';
|
|
282
281
|
// ── Handlers ──────────────────────────────────────────────────────────────────
|
|
283
|
-
function formatInstance(inst) {
|
|
284
|
-
const lines = [
|
|
285
|
-
`**${inst.name}** (${inst.tier.toUpperCase()})`,
|
|
286
|
-
` ID: ${inst.id}`,
|
|
287
|
-
` Status: ${inst.status}`,
|
|
288
|
-
` Region: ${inst.region}`,
|
|
289
|
-
` Memory: ${inst.memory_mb} MB`,
|
|
290
|
-
` Enc: ${inst.encryption_at_rest ? 'AES-256 at rest' : 'TLS in-transit'}`,
|
|
291
|
-
` Created: ${new Date(inst.created_at).toLocaleDateString('de-DE')}`,
|
|
292
|
-
];
|
|
293
|
-
if (inst.host && inst.port)
|
|
294
|
-
lines.push(` Host: ${inst.host}:${inst.port}`);
|
|
295
|
-
return lines.join('\n');
|
|
296
|
-
}
|
|
297
|
-
function buildConnectionString(inst) {
|
|
298
|
-
if (!inst.host || !inst.port)
|
|
299
|
-
return '(not yet provisioned)';
|
|
300
|
-
const scheme = inst.tls_enabled !== false ? 'rediss' : 'redis';
|
|
301
|
-
const pw = inst.password ? `:${inst.password}@` : '@';
|
|
302
|
-
return `${scheme}://${pw}${inst.host}:${inst.port}`;
|
|
303
|
-
}
|
|
304
282
|
// Fires once per process when no JWT is set (anonymous, opt-out via CACHLY_NO_TELEMETRY=1)
|
|
305
283
|
let _telemetryPingSent = false;
|
|
306
284
|
async function sendAnonymousTelemetry(toolName) {
|
|
@@ -390,724 +368,39 @@ async function handleTool(name, args) {
|
|
|
390
368
|
if (defaultId)
|
|
391
369
|
args = { ...args, instance_id: defaultId };
|
|
392
370
|
}
|
|
393
|
-
// Delegate v0.2 bulk/lock/stream tools first
|
|
394
|
-
const bulkResult = await handleBulkLockStream(name, args);
|
|
395
|
-
if (bulkResult !== null)
|
|
396
|
-
return bulkResult;
|
|
397
371
|
// Delegate brain tools (learn, recall, session, etc.)
|
|
398
372
|
const brainResult = await handleBrainTool(name, args, getConnection, apiFetch);
|
|
399
373
|
if (brainResult !== null)
|
|
400
374
|
return brainResult;
|
|
375
|
+
// Delegate context tools (remember/recall/list/forget)
|
|
376
|
+
const contextResult = await handleContextTool(name, args, getConnection, apiFetch);
|
|
377
|
+
if (contextResult !== null)
|
|
378
|
+
return contextResult;
|
|
379
|
+
// Delegate instance + cache tools
|
|
380
|
+
const instanceResult = await handleInstanceTool(name, args, getConnection, apiFetch);
|
|
381
|
+
if (instanceResult !== null)
|
|
382
|
+
return instanceResult;
|
|
383
|
+
const cacheResult = await handleCacheTool(name, args, getConnection, apiFetch);
|
|
384
|
+
if (cacheResult !== null)
|
|
385
|
+
return cacheResult;
|
|
386
|
+
// Delegate team/brain advanced tools
|
|
387
|
+
const teamResult = await handleTeamTool(name, args, getConnection, apiFetch);
|
|
388
|
+
if (teamResult !== null)
|
|
389
|
+
return teamResult;
|
|
390
|
+
const roadmapResult = await handleRoadmapTool(name, args, getConnection, apiFetch);
|
|
391
|
+
if (roadmapResult !== null)
|
|
392
|
+
return roadmapResult;
|
|
393
|
+
const advancedResult = await handleAdvancedTool(name, args, getConnection, apiFetch);
|
|
394
|
+
if (advancedResult !== null)
|
|
395
|
+
return advancedResult;
|
|
396
|
+
const syndicateResult = await handleSyndicateTool(name, args, getConnection, apiFetch);
|
|
397
|
+
if (syndicateResult !== null)
|
|
398
|
+
return syndicateResult;
|
|
399
|
+
const fedbrainResult = await handleFedbrainTool(name, args, getConnection, apiFetch);
|
|
400
|
+
if (fedbrainResult !== null)
|
|
401
|
+
return fedbrainResult;
|
|
401
402
|
switch (name) {
|
|
402
403
|
// ── Instance management ──────────────────────────────────────────────
|
|
403
|
-
case 'list_instances': {
|
|
404
|
-
const res = await apiFetch('/api/v1/instances');
|
|
405
|
-
const instances = res.data ?? [];
|
|
406
|
-
if (instances.length === 0)
|
|
407
|
-
return 'You have no cache instances yet. Use `create_instance` to create one.';
|
|
408
|
-
return [`Found ${instances.length} instance(s):\n`, ...instances.map(formatInstance)].join('\n');
|
|
409
|
-
}
|
|
410
|
-
case 'create_instance': {
|
|
411
|
-
const { name: instName, tier } = args;
|
|
412
|
-
const res = await apiFetch('/api/v1/instances', {
|
|
413
|
-
method: 'POST',
|
|
414
|
-
body: JSON.stringify({ name: instName, tier, created_via: 'api' }),
|
|
415
|
-
});
|
|
416
|
-
if (res.checkout_url) {
|
|
417
|
-
return [
|
|
418
|
-
`✅ Instance **${instName}** (${tier}) created! ID: \`${res.instance_id}\``,
|
|
419
|
-
``,
|
|
420
|
-
`💳 This is a paid tier. Complete payment to activate:`,
|
|
421
|
-
` ${res.checkout_url}`,
|
|
422
|
-
``,
|
|
423
|
-
`After payment, provisioning starts automatically (~30 seconds).`,
|
|
424
|
-
].join('\n');
|
|
425
|
-
}
|
|
426
|
-
return [
|
|
427
|
-
`✅ Instance **${instName}** (${tier}) created and provisioning started!`,
|
|
428
|
-
` ID: \`${res.instance_id}\``,
|
|
429
|
-
` Status: ${res.status}`,
|
|
430
|
-
``,
|
|
431
|
-
`Use \`get_instance\` or \`get_connection_string\` to get your connection details.`,
|
|
432
|
-
].join('\n');
|
|
433
|
-
}
|
|
434
|
-
case 'get_instance': {
|
|
435
|
-
const inst = await apiFetch(`/api/v1/instances/${args.instance_id}`);
|
|
436
|
-
return formatInstance(inst);
|
|
437
|
-
}
|
|
438
|
-
case 'get_connection_string': {
|
|
439
|
-
const inst = await apiFetch(`/api/v1/instances/${args.instance_id}`);
|
|
440
|
-
if (inst.status !== 'running') {
|
|
441
|
-
return `Instance is not running yet (status: ${inst.status}). Provisioning takes ~30 seconds after payment.`;
|
|
442
|
-
}
|
|
443
|
-
const connStr = buildConnectionString(inst);
|
|
444
|
-
return [
|
|
445
|
-
`**Connection string for ${inst.name}:**`,
|
|
446
|
-
`\`\`\``,
|
|
447
|
-
connStr,
|
|
448
|
-
`\`\`\``,
|
|
449
|
-
``,
|
|
450
|
-
`**Environment variable:**`,
|
|
451
|
-
`\`\`\`bash`,
|
|
452
|
-
`REDIS_URL="${connStr}"`,
|
|
453
|
-
`CACHLY_URL="${connStr}"`,
|
|
454
|
-
`\`\`\``,
|
|
455
|
-
``,
|
|
456
|
-
`**Quick test:**`,
|
|
457
|
-
`\`\`\`bash`,
|
|
458
|
-
`redis-cli -u "${connStr}" PING`,
|
|
459
|
-
`\`\`\``,
|
|
460
|
-
].join('\n');
|
|
461
|
-
}
|
|
462
|
-
case 'delete_instance': {
|
|
463
|
-
const { instance_id, confirm } = args;
|
|
464
|
-
if (!confirm)
|
|
465
|
-
return 'Deletion cancelled. Set `confirm: true` to proceed.';
|
|
466
|
-
pool.get(instance_id)?.quit().catch(() => undefined);
|
|
467
|
-
pool.delete(instance_id);
|
|
468
|
-
await apiFetch(`/api/v1/instances/${instance_id}`, { method: 'DELETE' });
|
|
469
|
-
return `✅ Instance \`${instance_id}\` has been deleted and all data removed.`;
|
|
470
|
-
}
|
|
471
|
-
// ── Org / Team management ────────────────────────────────────────────
|
|
472
|
-
case 'list_orgs': {
|
|
473
|
-
const res = await apiFetch('/api/v1/orgs');
|
|
474
|
-
const orgs = res.orgs ?? [];
|
|
475
|
-
if (orgs.length === 0)
|
|
476
|
-
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.`;
|
|
477
|
-
return [
|
|
478
|
-
`🏢 **Your organizations (${orgs.length})**\n`,
|
|
479
|
-
...orgs.map(o => `• **${o.name}** (\`${o.slug}\`) — plan: ${o.plan} · seats: ${o.member_count ?? '?'}/${o.max_members}\n ID: \`${o.id}\``),
|
|
480
|
-
`\n_Manage: \`get_org_plan\`, \`invite_member\`, dashboard → /team_`,
|
|
481
|
-
].join('\n');
|
|
482
|
-
}
|
|
483
|
-
case 'create_org': {
|
|
484
|
-
const { name: orgName, slug } = args;
|
|
485
|
-
const body = { name: orgName };
|
|
486
|
-
if (slug)
|
|
487
|
-
body.slug = slug;
|
|
488
|
-
const res = await apiFetch('/api/v1/orgs', {
|
|
489
|
-
method: 'POST',
|
|
490
|
-
body: JSON.stringify(body),
|
|
491
|
-
});
|
|
492
|
-
return [
|
|
493
|
-
`✅ **Organization created:** ${res.name}`,
|
|
494
|
-
` ID: \`${res.id}\` · Slug: \`${res.slug}\` · Plan: ${res.plan}`,
|
|
495
|
-
``,
|
|
496
|
-
`**Next steps:**`,
|
|
497
|
-
`1. Invite team members: \`invite_member(org_id="${res.id}", email="dev@example.com")\``,
|
|
498
|
-
`2. Upgrade plan: open billing portal via dashboard → /team`,
|
|
499
|
-
` Team: €99/mo (10 seats) · Business: €299/mo (50 seats)`,
|
|
500
|
-
].join('\n');
|
|
501
|
-
}
|
|
502
|
-
case 'invite_member': {
|
|
503
|
-
const { org_id, email, role = 'member' } = args;
|
|
504
|
-
await apiFetch(`/api/v1/orgs/${org_id}/members`, {
|
|
505
|
-
method: 'POST',
|
|
506
|
-
body: JSON.stringify({ email, role }),
|
|
507
|
-
});
|
|
508
|
-
return `✅ Invite sent to **${email}** as \`${role}\` in org \`${org_id}\`.\n\nThey will receive an email to join the organization.`;
|
|
509
|
-
}
|
|
510
|
-
case 'get_org_plan': {
|
|
511
|
-
const { org_id } = args;
|
|
512
|
-
const org = await apiFetch(`/api/v1/orgs/${org_id}`);
|
|
513
|
-
const accepted = (org.members ?? []).filter(m => m.accepted_at).length;
|
|
514
|
-
const pending = (org.members ?? []).filter(m => !m.accepted_at).length;
|
|
515
|
-
const planPrice = { free: '€0', team: '€99/mo', business: '€299/mo', enterprise: 'custom' };
|
|
516
|
-
return [
|
|
517
|
-
`🏢 **${org.name}** — Plan: **${org.plan}** (${planPrice[org.plan] ?? org.plan})`,
|
|
518
|
-
` Seats: ${accepted} active + ${pending} pending / ${org.max_members} max`,
|
|
519
|
-
``,
|
|
520
|
-
`**Members:**`,
|
|
521
|
-
...(org.members ?? []).map(m => ` • ${m.invite_email} (${m.role})${m.accepted_at ? '' : ' — pending'}`),
|
|
522
|
-
``,
|
|
523
|
-
org.stripe_customer_id
|
|
524
|
-
? `💳 Billing: managed via Stripe. Upgrade/cancel: dashboard → /billing`
|
|
525
|
-
: `💳 No payment method yet. Upgrade: dashboard → /billing → Team Plans`,
|
|
526
|
-
].join('\n');
|
|
527
|
-
}
|
|
528
|
-
// ── Live cache operations ────────────────────────────────────────────
|
|
529
|
-
case 'cache_get': {
|
|
530
|
-
const { instance_id, key } = args;
|
|
531
|
-
const redis = await getConnection(instance_id);
|
|
532
|
-
const value = await redis.get(key);
|
|
533
|
-
if (value === null)
|
|
534
|
-
return `Key \`${key}\` → **not found** (null)`;
|
|
535
|
-
let pretty = value;
|
|
536
|
-
try {
|
|
537
|
-
pretty = JSON.stringify(JSON.parse(value), null, 2);
|
|
538
|
-
}
|
|
539
|
-
catch {
|
|
540
|
-
// not JSON — return raw
|
|
541
|
-
}
|
|
542
|
-
return `Key \`${key}\`:\n\`\`\`\n${pretty}\n\`\`\``;
|
|
543
|
-
}
|
|
544
|
-
case 'cache_set': {
|
|
545
|
-
const { instance_id, key, value, ttl } = args;
|
|
546
|
-
const redis = await getConnection(instance_id);
|
|
547
|
-
if (ttl && ttl > 0) {
|
|
548
|
-
await redis.set(key, value, 'EX', ttl);
|
|
549
|
-
return `✅ Set \`${key}\` (TTL: ${ttl}s)`;
|
|
550
|
-
}
|
|
551
|
-
await redis.set(key, value);
|
|
552
|
-
return `✅ Set \`${key}\` (no expiry)`;
|
|
553
|
-
}
|
|
554
|
-
case 'cache_delete': {
|
|
555
|
-
const { instance_id, keys } = args;
|
|
556
|
-
const redis = await getConnection(instance_id);
|
|
557
|
-
const deleted = await redis.del(...keys);
|
|
558
|
-
return `✅ Deleted **${deleted}** of ${keys.length} key(s): ${keys.map((k) => `\`${k}\``).join(', ')}`;
|
|
559
|
-
}
|
|
560
|
-
case 'cache_exists': {
|
|
561
|
-
const { instance_id, keys } = args;
|
|
562
|
-
const redis = await getConnection(instance_id);
|
|
563
|
-
const count = await redis.exists(...keys);
|
|
564
|
-
return `**${count}** of ${keys.length} key(s) exist in cache.`;
|
|
565
|
-
}
|
|
566
|
-
case 'cache_ttl': {
|
|
567
|
-
const { instance_id, key } = args;
|
|
568
|
-
const redis = await getConnection(instance_id);
|
|
569
|
-
const ttl = await redis.ttl(key);
|
|
570
|
-
if (ttl === -2)
|
|
571
|
-
return `Key \`${key}\` → **does not exist**`;
|
|
572
|
-
if (ttl === -1)
|
|
573
|
-
return `Key \`${key}\` → **no expiry** (persists forever)`;
|
|
574
|
-
const mins = Math.floor(ttl / 60);
|
|
575
|
-
const secs = ttl % 60;
|
|
576
|
-
return `Key \`${key}\` → TTL: **${ttl}s** (${mins}m ${secs}s remaining)`;
|
|
577
|
-
}
|
|
578
|
-
case 'cache_keys': {
|
|
579
|
-
const { instance_id, pattern = '*', count = 50 } = args;
|
|
580
|
-
const limit = Math.min(count, 500);
|
|
581
|
-
const redis = await getConnection(instance_id);
|
|
582
|
-
const keys = [];
|
|
583
|
-
const stream = redis.scanStream({ match: pattern, count: 100 });
|
|
584
|
-
await new Promise((resolve, reject) => {
|
|
585
|
-
stream.on('data', (batch) => {
|
|
586
|
-
keys.push(...batch);
|
|
587
|
-
if (keys.length >= limit) {
|
|
588
|
-
stream.destroy();
|
|
589
|
-
resolve();
|
|
590
|
-
}
|
|
591
|
-
});
|
|
592
|
-
stream.on('end', resolve);
|
|
593
|
-
stream.on('error', reject);
|
|
594
|
-
});
|
|
595
|
-
const result = keys.slice(0, limit);
|
|
596
|
-
if (result.length === 0)
|
|
597
|
-
return `No keys found matching pattern \`${pattern}\`.`;
|
|
598
|
-
return [
|
|
599
|
-
`Found **${result.length}** key(s) matching \`${pattern}\`:`,
|
|
600
|
-
...result.map((k) => ` • \`${k}\``),
|
|
601
|
-
result.length === limit ? `\n_(showing first ${limit} — narrow pattern to see more)_` : '',
|
|
602
|
-
]
|
|
603
|
-
.filter(Boolean)
|
|
604
|
-
.join('\n');
|
|
605
|
-
}
|
|
606
|
-
case 'cache_stats': {
|
|
607
|
-
const { instance_id } = args;
|
|
608
|
-
const redis = await getConnection(instance_id);
|
|
609
|
-
const [infoAll, infoStats, infoKeyspace] = await Promise.all([
|
|
610
|
-
redis.info('memory'),
|
|
611
|
-
redis.info('stats'),
|
|
612
|
-
redis.info('keyspace'),
|
|
613
|
-
]);
|
|
614
|
-
const parse = (section, field) => section.match(new RegExp(`${field}:([^\r\n]+)`))?.[1]?.trim() ?? 'n/a';
|
|
615
|
-
const usedMem = parse(infoAll, 'used_memory_human');
|
|
616
|
-
const peakMem = parse(infoAll, 'used_memory_peak_human');
|
|
617
|
-
const hits = parse(infoStats, 'keyspace_hits');
|
|
618
|
-
const misses = parse(infoStats, 'keyspace_misses');
|
|
619
|
-
const opsPerSec = parse(infoStats, 'instantaneous_ops_per_sec');
|
|
620
|
-
const connectedClients = (await redis.info('clients')).match(/connected_clients:(\d+)/)?.[1] ?? 'n/a';
|
|
621
|
-
const hitsN = parseInt(hits) || 0;
|
|
622
|
-
const missesN = parseInt(misses) || 0;
|
|
623
|
-
const total = hitsN + missesN;
|
|
624
|
-
const hitRate = total > 0 ? ((hitsN / total) * 100).toFixed(1) : 'n/a';
|
|
625
|
-
const keyspaceLines = infoKeyspace
|
|
626
|
-
.split('\n')
|
|
627
|
-
.filter((l) => l.startsWith('db'))
|
|
628
|
-
.map((l) => ` ${l.trim()}`);
|
|
629
|
-
return [
|
|
630
|
-
`📊 **Cache Stats for instance \`${instance_id}\`:**`,
|
|
631
|
-
``,
|
|
632
|
-
` 💾 Memory used: ${usedMem} (peak: ${peakMem})`,
|
|
633
|
-
` ⚡ Ops/sec: ${opsPerSec}`,
|
|
634
|
-
` 🎯 Hit rate: ${hitRate}% (${hits} hits / ${misses} misses)`,
|
|
635
|
-
` 🔗 Clients: ${connectedClients}`,
|
|
636
|
-
``,
|
|
637
|
-
keyspaceLines.length > 0
|
|
638
|
-
? ` 🗂️ Keyspace:\n${keyspaceLines.join('\n')}`
|
|
639
|
-
: ` 🗂️ Keyspace: (empty)`,
|
|
640
|
-
].join('\n');
|
|
641
|
-
}
|
|
642
|
-
case 'semantic_search': {
|
|
643
|
-
const { instance_id, query, threshold = 0.82, namespace: nsArg = 'cachly:sem', top_k = 5, use_hybrid = false, auto_namespace = false, } = args;
|
|
644
|
-
// resolve namespace from query text when requested
|
|
645
|
-
const namespace = auto_namespace ? detectNamespace(query) : nsArg;
|
|
646
|
-
if (!hasEmbedProvider()) {
|
|
647
|
-
return (`❌ semantic_search requires an embedding provider.\n\n` +
|
|
648
|
-
`Current: ${embedProviderHint()}\n\n` +
|
|
649
|
-
`Set one of these in your MCP env config:\n` +
|
|
650
|
-
` OPENAI_API_KEY (provider: openai – default)\n` +
|
|
651
|
-
` MISTRAL_API_KEY (provider: mistral)\n` +
|
|
652
|
-
` COHERE_API_KEY (provider: cohere)\n` +
|
|
653
|
-
` GEMINI_API_KEY (provider: gemini)\n` +
|
|
654
|
-
` OLLAMA_BASE_URL (provider: ollama – local, no key needed)\n` +
|
|
655
|
-
`Also set: CACHLY_EMBED_PROVIDER=<provider>`);
|
|
656
|
-
}
|
|
657
|
-
const inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
658
|
-
if (!inst.vector_token) {
|
|
659
|
-
return (`❌ Semantic search is only available on Speed and Business tiers.\n\n` +
|
|
660
|
-
`Your instance "${inst.name}" is on the **${inst.tier.toUpperCase()}** tier.\n` +
|
|
661
|
-
`Upgrade at https://cachly.dev/instances/${instance_id}`);
|
|
662
|
-
}
|
|
663
|
-
// Compute embedding via configured provider
|
|
664
|
-
const embedding = await computeEmbedding(query);
|
|
665
|
-
// Query cachly vector API
|
|
666
|
-
const vectorUrl = process.env.CACHLY_VECTOR_URL ?? `https://api.cachly.dev/v1/sem/${inst.vector_token}`;
|
|
667
|
-
const searchPayload = { embedding, namespace, threshold, top_k };
|
|
668
|
-
// hybrid BM25+Vector RRF: include query text when requested.
|
|
669
|
-
if (use_hybrid) {
|
|
670
|
-
searchPayload['hybrid'] = true;
|
|
671
|
-
searchPayload['prompt'] = query;
|
|
672
|
-
}
|
|
673
|
-
const searchRes = await fetch(`${vectorUrl}/search`, {
|
|
674
|
-
method: 'POST',
|
|
675
|
-
headers: { 'Content-Type': 'application/json' },
|
|
676
|
-
body: JSON.stringify(searchPayload),
|
|
677
|
-
});
|
|
678
|
-
if (!searchRes.ok) {
|
|
679
|
-
throw new McpError(ErrorCode.InternalError, `Vector search failed: ${searchRes.statusText}`);
|
|
680
|
-
}
|
|
681
|
-
const results = (await searchRes.json());
|
|
682
|
-
if (!results.length || (results.length === 1 && !results[0].found)) {
|
|
683
|
-
return (`🔍 No semantically similar entries found for:\n _"${query}"_\n\n` +
|
|
684
|
-
`Try lowering the threshold (current: ${threshold}) or using different keywords.`);
|
|
685
|
-
}
|
|
686
|
-
const redis = await getConnection(instance_id);
|
|
687
|
-
const lines = [
|
|
688
|
-
`🔍 **Semantic search results** for: _"${query}"_`,
|
|
689
|
-
` Threshold: ${threshold} · Namespace: \`${namespace}\``,
|
|
690
|
-
``,
|
|
691
|
-
];
|
|
692
|
-
for (const hit of results) {
|
|
693
|
-
if (!hit.found || !hit.id)
|
|
694
|
-
continue;
|
|
695
|
-
const value = await redis.get(`${namespace}:val:${hit.id}`);
|
|
696
|
-
lines.push(`**Match** (similarity: ${((hit.similarity ?? 0) * 100).toFixed(1)}%)`, ` Prompt: _"${hit.prompt ?? '(unknown)'}"_`, value ? ` Value: \`${value.slice(0, 200)}${value.length > 200 ? '…' : ''}\`` : ` Value: _(evicted from cache)_`, ``);
|
|
697
|
-
}
|
|
698
|
-
return lines.join('\n');
|
|
699
|
-
}
|
|
700
|
-
// ── Namespace Auto-Detection ──────────────────────────────────────────
|
|
701
|
-
case 'detect_namespace': {
|
|
702
|
-
const { prompt } = args;
|
|
703
|
-
const ns = detectNamespace(prompt);
|
|
704
|
-
const typeLabel = ns.split(':').pop();
|
|
705
|
-
const descriptions = {
|
|
706
|
-
code: '💻 Code — contains programming constructs or syntax',
|
|
707
|
-
translation: '🌐 Translation — asks to translate between languages',
|
|
708
|
-
summary: '📝 Summary — requests a summary or key points (TL;DR)',
|
|
709
|
-
qa: '❓ Q&A — a direct question or query',
|
|
710
|
-
creative: '🎨 Creative — general, creative, or conversational prompt',
|
|
711
|
-
};
|
|
712
|
-
return [
|
|
713
|
-
`**Detected namespace:** \`${ns}\``,
|
|
714
|
-
`**Type:** ${descriptions[typeLabel] ?? typeLabel}`,
|
|
715
|
-
``,
|
|
716
|
-
`_Prompt: "${prompt.slice(0, 120)}${prompt.length > 120 ? '…' : ''}"_`,
|
|
717
|
-
``,
|
|
718
|
-
`💡 Use this namespace in \`semantic_search\` or \`cache_warmup\` for better hit rates.`,
|
|
719
|
-
` Set \`auto_namespace: true\` to apply this detection automatically.`,
|
|
720
|
-
].join('\n');
|
|
721
|
-
}
|
|
722
|
-
// ── Cache Warmup ───────────────────────────────────────────────────────
|
|
723
|
-
case 'cache_warmup': {
|
|
724
|
-
const { instance_id, entries: rawEntries, namespace: nsArg = 'cachly:sem', ttl, auto_namespace = false, } = args;
|
|
725
|
-
if (!hasEmbedProvider()) {
|
|
726
|
-
return (`❌ cache_warmup requires an embedding provider.\n\n` +
|
|
727
|
-
`Current: ${embedProviderHint()}\n\n` +
|
|
728
|
-
`Supported: openai (default) · mistral · cohere · ollama (local) · gemini\n` +
|
|
729
|
-
`Set CACHLY_EMBED_PROVIDER and the matching API key env var.`);
|
|
730
|
-
}
|
|
731
|
-
const inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
732
|
-
const vectorUrl = process.env.CACHLY_VECTOR_URL ??
|
|
733
|
-
(inst.vector_token ? `https://api.cachly.dev/v1/sem/${inst.vector_token}` : null);
|
|
734
|
-
const redis = await getConnection(instance_id);
|
|
735
|
-
let warmed = 0;
|
|
736
|
-
let skipped = 0;
|
|
737
|
-
const details = [];
|
|
738
|
-
for (const entry of rawEntries) {
|
|
739
|
-
// resolve namespace per entry
|
|
740
|
-
const ns = entry.namespace ?? (auto_namespace ? detectNamespace(entry.prompt) : nsArg);
|
|
741
|
-
// Compute embedding for this prompt
|
|
742
|
-
const embedding = await computeEmbedding(entry.prompt);
|
|
743
|
-
// Check if a very-similar entry already exists (threshold 0.98 → skip to avoid duplicates)
|
|
744
|
-
let alreadyCached = false;
|
|
745
|
-
if (vectorUrl) {
|
|
746
|
-
const checkRes = await fetch(`${vectorUrl}/search`, {
|
|
747
|
-
method: 'POST',
|
|
748
|
-
headers: { 'Content-Type': 'application/json' },
|
|
749
|
-
body: JSON.stringify({ embedding, namespace: ns, threshold: 0.98 }),
|
|
750
|
-
}).catch(() => null);
|
|
751
|
-
if (checkRes?.ok) {
|
|
752
|
-
const results = (await checkRes.json());
|
|
753
|
-
alreadyCached = results[0]?.found ?? false;
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
if (alreadyCached) {
|
|
757
|
-
skipped++;
|
|
758
|
-
details.push(` ⏭️ _"${entry.prompt.slice(0, 60)}${entry.prompt.length > 60 ? '…' : ''}"_ → already cached`);
|
|
759
|
-
continue;
|
|
760
|
-
}
|
|
761
|
-
// Write value to Valkey
|
|
762
|
-
const id = randomUUID();
|
|
763
|
-
const vk = `${ns}:val:${id}`;
|
|
764
|
-
if (ttl && ttl > 0) {
|
|
765
|
-
await redis.set(vk, entry.value, 'EX', ttl);
|
|
766
|
-
}
|
|
767
|
-
else {
|
|
768
|
-
await redis.set(vk, entry.value);
|
|
769
|
-
}
|
|
770
|
-
if (vectorUrl) {
|
|
771
|
-
// pgvector path – index embedding in HNSW
|
|
772
|
-
const body = { id, prompt: entry.prompt, namespace: ns, embedding };
|
|
773
|
-
if (ttl && ttl > 0) {
|
|
774
|
-
body['expires_at'] = new Date(Date.now() + ttl * 1000).toISOString();
|
|
775
|
-
}
|
|
776
|
-
await fetch(`${vectorUrl}/entries`, {
|
|
777
|
-
method: 'POST',
|
|
778
|
-
headers: { 'Content-Type': 'application/json' },
|
|
779
|
-
body: JSON.stringify(body),
|
|
780
|
-
}).catch(() => undefined);
|
|
781
|
-
}
|
|
782
|
-
else {
|
|
783
|
-
// Legacy SCAN path – write emb key to Valkey
|
|
784
|
-
const embKey = `${ns}:emb:${id}`;
|
|
785
|
-
const embPayload = JSON.stringify({ embedding, prompt: entry.prompt });
|
|
786
|
-
if (ttl && ttl > 0) {
|
|
787
|
-
await redis.set(embKey, embPayload, 'EX', ttl);
|
|
788
|
-
}
|
|
789
|
-
else {
|
|
790
|
-
await redis.set(embKey, embPayload);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
warmed++;
|
|
794
|
-
details.push(` ✅ _"${entry.prompt.slice(0, 60)}${entry.prompt.length > 60 ? '…' : ''}"_ → \`${ns}\``);
|
|
795
|
-
}
|
|
796
|
-
return [
|
|
797
|
-
`🔥 **Cache Warmup Complete**`,
|
|
798
|
-
``,
|
|
799
|
-
` ✅ Warmed: **${warmed}** new entries`,
|
|
800
|
-
` ⏭️ Skipped: **${skipped}** (already cached at ≥ 0.98 similarity)`,
|
|
801
|
-
` 📦 Total: ${rawEntries.length}`,
|
|
802
|
-
auto_namespace
|
|
803
|
-
? ` 🏷️ Namespacing: auto-detected per prompt`
|
|
804
|
-
: ` 🏷️ Namespace: \`${nsArg}\``,
|
|
805
|
-
vectorUrl
|
|
806
|
-
? ` 🔍 Mode: pgvector HNSW (Speed/Business tier)`
|
|
807
|
-
: ` 🔍 Mode: Valkey SCAN (upgrade to Speed tier for scalable search)`,
|
|
808
|
-
``,
|
|
809
|
-
...details,
|
|
810
|
-
].join('\n');
|
|
811
|
-
}
|
|
812
|
-
// ── index_project – Codebase Indexing ─────────────────────────────────────
|
|
813
|
-
case 'index_project': {
|
|
814
|
-
const { instance_id, dir, extensions: extArg, max_files = 100, ttl = 86400, summary_chars = 1200, namespace: nsArg = 'cachly:sem:code', } = args;
|
|
815
|
-
const ALLOWED_EXT = new Set((extArg ?? ['ts', 'js', 'tsx', 'jsx', 'go', 'py', 'java', 'rs', 'md', 'kt', 'swift']).map((e) => (e.startsWith('.') ? e : `.${e}`)));
|
|
816
|
-
// Recursively collect files up to max_files limit
|
|
817
|
-
const files = [];
|
|
818
|
-
async function walk(d) {
|
|
819
|
-
if (files.length >= max_files)
|
|
820
|
-
return;
|
|
821
|
-
const entries = await readdir(d, { withFileTypes: true }).catch(() => null);
|
|
822
|
-
if (!entries)
|
|
823
|
-
return;
|
|
824
|
-
for (const entry of entries) {
|
|
825
|
-
if (files.length >= max_files)
|
|
826
|
-
break;
|
|
827
|
-
const full = join(d, entry.name);
|
|
828
|
-
if (entry.isDirectory()) {
|
|
829
|
-
if (['.git', 'node_modules', 'dist', 'build', '.next', '__pycache__', 'vendor'].includes(entry.name))
|
|
830
|
-
continue;
|
|
831
|
-
await walk(full);
|
|
832
|
-
}
|
|
833
|
-
else if (entry.isFile() && ALLOWED_EXT.has(extname(entry.name).toLowerCase())) {
|
|
834
|
-
files.push(full);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
await walk(dir);
|
|
839
|
-
if (files.length === 0) {
|
|
840
|
-
return `❌ No matching files found in \`${dir}\`.\nExtensions checked: ${[...ALLOWED_EXT].join(', ')}`;
|
|
841
|
-
}
|
|
842
|
-
const inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
843
|
-
const vectorUrl = process.env.CACHLY_VECTOR_URL ??
|
|
844
|
-
(inst.vector_token ? `https://api.cachly.dev/v1/sem/${inst.vector_token}` : null);
|
|
845
|
-
const canEmbed = vectorUrl && hasEmbedProvider();
|
|
846
|
-
let indexed = 0;
|
|
847
|
-
let skipped = 0;
|
|
848
|
-
let errors = 0;
|
|
849
|
-
let semanticIndexed = 0;
|
|
850
|
-
let unchanged = 0;
|
|
851
|
-
const details = [];
|
|
852
|
-
const expiresAt = new Date(Date.now() + ttl * 1000).toISOString();
|
|
853
|
-
const redis = await getConnection(instance_id);
|
|
854
|
-
for (const filePath of files) {
|
|
855
|
-
const relPath = relative(dir, filePath);
|
|
856
|
-
let content;
|
|
857
|
-
let fileSize;
|
|
858
|
-
try {
|
|
859
|
-
const s = await stat(filePath);
|
|
860
|
-
if (s.size > 200_000) {
|
|
861
|
-
skipped++;
|
|
862
|
-
continue;
|
|
863
|
-
} // skip files >200 KB
|
|
864
|
-
fileSize = s.size;
|
|
865
|
-
content = await readFile(filePath, 'utf-8');
|
|
866
|
-
}
|
|
867
|
-
catch {
|
|
868
|
-
errors++;
|
|
869
|
-
continue;
|
|
870
|
-
}
|
|
871
|
-
// ── Smart invalidation: hash-based change detection ──
|
|
872
|
-
// Compute a simple hash of file content to skip unchanged files
|
|
873
|
-
const hashKey = `cachly:idx:hash:${relPath}`;
|
|
874
|
-
const contentHash = `${fileSize}:${content.length}:${simpleHash(content)}`;
|
|
875
|
-
const existingHash = await redis.get(hashKey);
|
|
876
|
-
if (existingHash === contentHash) {
|
|
877
|
-
// File unchanged — refresh TTL but skip re-indexing
|
|
878
|
-
const idxKey = `cachly:idx:${relPath}`;
|
|
879
|
-
if (ttl > 0)
|
|
880
|
-
await redis.expire(idxKey, ttl);
|
|
881
|
-
if (ttl > 0)
|
|
882
|
-
await redis.expire(hashKey, ttl);
|
|
883
|
-
unchanged++;
|
|
884
|
-
continue;
|
|
885
|
-
}
|
|
886
|
-
const excerpt = content.slice(0, summary_chars).replace(/\s+/g, ' ').trim();
|
|
887
|
-
// ── Layer 1: Keyword index in Valkey (always works, no embedding needed) ──
|
|
888
|
-
const idxKey = `cachly:idx:${relPath}`;
|
|
889
|
-
const idxValue = `File: ${relPath}\n${excerpt}`;
|
|
890
|
-
if (ttl > 0) {
|
|
891
|
-
await redis.set(idxKey, idxValue, 'EX', ttl);
|
|
892
|
-
}
|
|
893
|
-
else {
|
|
894
|
-
await redis.set(idxKey, idxValue);
|
|
895
|
-
}
|
|
896
|
-
// Store content hash for smart invalidation on next run
|
|
897
|
-
if (ttl > 0) {
|
|
898
|
-
await redis.set(hashKey, contentHash, 'EX', ttl);
|
|
899
|
-
}
|
|
900
|
-
else {
|
|
901
|
-
await redis.set(hashKey, contentHash);
|
|
902
|
-
}
|
|
903
|
-
indexed++;
|
|
904
|
-
details.push(` ✅ ${relPath}`);
|
|
905
|
-
// ── Layer 2: Semantic vector index (optional, only if embedding available) ──
|
|
906
|
-
if (canEmbed) {
|
|
907
|
-
try {
|
|
908
|
-
const prompt = `File: ${relPath}\n${excerpt}`;
|
|
909
|
-
const embedding = await computeEmbedding(prompt);
|
|
910
|
-
const id = randomUUID();
|
|
911
|
-
await fetch(`${vectorUrl}/entries`, {
|
|
912
|
-
method: 'POST',
|
|
913
|
-
headers: { 'Content-Type': 'application/json' },
|
|
914
|
-
body: JSON.stringify({ id, prompt, namespace: nsArg, embedding, expires_at: expiresAt }),
|
|
915
|
-
});
|
|
916
|
-
await redis.set(`${nsArg}:val:${id}`, relPath, 'EX', ttl);
|
|
917
|
-
semanticIndexed++;
|
|
918
|
-
}
|
|
919
|
-
catch {
|
|
920
|
-
// Semantic indexing failed — keyword index is enough
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
const mode = canEmbed ? '🔍 Keyword + 🎯 Semantic' : '🔍 Keyword only (no embedding provider)';
|
|
925
|
-
return [
|
|
926
|
-
`📂 **index_project Complete** — ${mode}`,
|
|
927
|
-
``,
|
|
928
|
-
` 📁 Dir: ${dir}`,
|
|
929
|
-
` ✅ Indexed: **${indexed}** files (new/changed)`,
|
|
930
|
-
` ♻️ Unchanged: ${unchanged} files (hash match — skipped)`,
|
|
931
|
-
...(canEmbed ? [` 🎯 Semantic: **${semanticIndexed}** files (vector-searchable)`] : []),
|
|
932
|
-
` ⏭️ Skipped: ${skipped} (too large or filtered)`,
|
|
933
|
-
` ❌ Errors: ${errors}`,
|
|
934
|
-
` ⏱️ TTL: ${ttl}s (${Math.round(ttl / 3600)}h)`,
|
|
935
|
-
``,
|
|
936
|
-
`💡 **Next steps:**`,
|
|
937
|
-
` 1. Use \`smart_recall("how does auth work")\` to find relevant files.`,
|
|
938
|
-
` 2. Re-run index_project after major refactors.`,
|
|
939
|
-
...(canEmbed ? [] : [` 3. Set OPENAI_API_KEY (or similar) in .env to also enable semantic search.`]),
|
|
940
|
-
``,
|
|
941
|
-
...(details.length <= 20 ? details : [...details.slice(0, 20), ` … and ${details.length - 20} more`]),
|
|
942
|
-
].join('\n');
|
|
943
|
-
}
|
|
944
|
-
// ── Thinking/Context Cache Tools ────────────────────────────────────────
|
|
945
|
-
case 'remember_context': {
|
|
946
|
-
const { instance_id, key, content, category = 'custom', ttl = 86400, } = args;
|
|
947
|
-
const redis = await getConnection(instance_id);
|
|
948
|
-
const cacheKey = `cachly:ctx:${category}:${key}`;
|
|
949
|
-
const meta = JSON.stringify({
|
|
950
|
-
key,
|
|
951
|
-
category,
|
|
952
|
-
size: content.length,
|
|
953
|
-
created: new Date().toISOString(),
|
|
954
|
-
});
|
|
955
|
-
if (ttl && ttl > 0) {
|
|
956
|
-
await redis.set(cacheKey, content, 'EX', ttl);
|
|
957
|
-
await redis.set(`${cacheKey}:meta`, meta, 'EX', ttl);
|
|
958
|
-
}
|
|
959
|
-
else {
|
|
960
|
-
await redis.set(cacheKey, content);
|
|
961
|
-
await redis.set(`${cacheKey}:meta`, meta);
|
|
962
|
-
}
|
|
963
|
-
// Also index semantically for smart_recall (if vector available)
|
|
964
|
-
const inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
965
|
-
if (inst.vector_token) {
|
|
966
|
-
try {
|
|
967
|
-
const embedding = await computeEmbedding(`${key}: ${content.slice(0, 500)}`);
|
|
968
|
-
const vectorUrl = `https://api.cachly.dev/v1/sem/${inst.vector_token}`;
|
|
969
|
-
const body = {
|
|
970
|
-
id: `ctx:${category}:${key}`,
|
|
971
|
-
prompt: key,
|
|
972
|
-
namespace: 'cachly:ctx',
|
|
973
|
-
embedding,
|
|
974
|
-
};
|
|
975
|
-
if (ttl && ttl > 0) {
|
|
976
|
-
body['expires_at'] = new Date(Date.now() + ttl * 1000).toISOString();
|
|
977
|
-
}
|
|
978
|
-
await fetch(`${vectorUrl}/entries`, {
|
|
979
|
-
method: 'POST',
|
|
980
|
-
headers: { 'Content-Type': 'application/json' },
|
|
981
|
-
body: JSON.stringify(body),
|
|
982
|
-
}).catch(() => undefined);
|
|
983
|
-
}
|
|
984
|
-
catch {
|
|
985
|
-
// Embedding optional — continue silently
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
return [
|
|
989
|
-
`🧠 **Context Saved**`,
|
|
990
|
-
``,
|
|
991
|
-
` Key: \`${key}\``,
|
|
992
|
-
` Category: ${category}`,
|
|
993
|
-
` Size: ${content.length} chars`,
|
|
994
|
-
` TTL: ${ttl > 0 ? `${ttl}s (${Math.round(ttl / 3600)}h)` : 'no expiry'}`,
|
|
995
|
-
``,
|
|
996
|
-
`💡 Use \`recall_context("${key}")\` to retrieve this later.`,
|
|
997
|
-
` Or \`smart_recall("${key.split('_').join(' ')}")\` for semantic search.`,
|
|
998
|
-
].join('\n');
|
|
999
|
-
}
|
|
1000
|
-
case 'recall_context': {
|
|
1001
|
-
const { instance_id, key } = args;
|
|
1002
|
-
const redis = await getConnection(instance_id);
|
|
1003
|
-
// Check if key is a glob pattern
|
|
1004
|
-
if (key.includes('*')) {
|
|
1005
|
-
const keys = [];
|
|
1006
|
-
const stream = redis.scanStream({ match: `cachly:ctx:*:${key}`, count: 100 });
|
|
1007
|
-
await new Promise((resolve, reject) => {
|
|
1008
|
-
stream.on('data', (batch) => {
|
|
1009
|
-
keys.push(...batch.filter((k) => !k.endsWith(':meta')));
|
|
1010
|
-
if (keys.length >= 20) {
|
|
1011
|
-
stream.destroy();
|
|
1012
|
-
resolve();
|
|
1013
|
-
}
|
|
1014
|
-
});
|
|
1015
|
-
stream.on('end', resolve);
|
|
1016
|
-
stream.on('error', reject);
|
|
1017
|
-
});
|
|
1018
|
-
if (keys.length === 0)
|
|
1019
|
-
return `⚠️ No cached context found matching pattern \`${key}\`.`;
|
|
1020
|
-
const results = [`🧠 **Recalled ${keys.length} context entries matching \`${key}\`:**\n`];
|
|
1021
|
-
for (const k of keys.slice(0, 10)) {
|
|
1022
|
-
const content = await redis.get(k);
|
|
1023
|
-
const shortKey = k.replace('cachly:ctx:', '');
|
|
1024
|
-
results.push(`### ${shortKey}\n\`\`\`\n${content?.slice(0, 500)}${(content?.length ?? 0) > 500 ? '…' : ''}\n\`\`\`\n`);
|
|
1025
|
-
}
|
|
1026
|
-
if (keys.length > 10)
|
|
1027
|
-
results.push(`_(+${keys.length - 10} more matches)_`);
|
|
1028
|
-
return results.join('\n');
|
|
1029
|
-
}
|
|
1030
|
-
// Try exact match across categories
|
|
1031
|
-
const categories = ['overview', 'architecture', 'file_summary', 'dependency', 'thinking', 'custom'];
|
|
1032
|
-
for (const cat of categories) {
|
|
1033
|
-
const content = await redis.get(`cachly:ctx:${cat}:${key}`);
|
|
1034
|
-
if (content) {
|
|
1035
|
-
const ttl = await redis.ttl(`cachly:ctx:${cat}:${key}`);
|
|
1036
|
-
return [
|
|
1037
|
-
`🧠 **Recalled Context: \`${key}\`**`,
|
|
1038
|
-
``,
|
|
1039
|
-
` Category: ${cat}`,
|
|
1040
|
-
` Size: ${content.length} chars`,
|
|
1041
|
-
` TTL: ${ttl === -1 ? 'no expiry' : ttl === -2 ? 'expired' : `${ttl}s remaining`}`,
|
|
1042
|
-
``,
|
|
1043
|
-
`---`,
|
|
1044
|
-
``,
|
|
1045
|
-
content,
|
|
1046
|
-
].join('\n');
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
return `⚠️ No cached context found for key \`${key}\`.\n\nUse \`list_remembered\` to see available cached context.`;
|
|
1050
|
-
}
|
|
1051
|
-
case 'list_remembered': {
|
|
1052
|
-
const { instance_id, category = 'all', limit = 50, } = args;
|
|
1053
|
-
const redis = await getConnection(instance_id);
|
|
1054
|
-
const pattern = category === 'all' ? 'cachly:ctx:*' : `cachly:ctx:${category}:*`;
|
|
1055
|
-
const keys = [];
|
|
1056
|
-
const stream = redis.scanStream({ match: pattern, count: 100 });
|
|
1057
|
-
await new Promise((resolve, reject) => {
|
|
1058
|
-
stream.on('data', (batch) => {
|
|
1059
|
-
keys.push(...batch.filter((k) => !k.endsWith(':meta')));
|
|
1060
|
-
if (keys.length >= limit) {
|
|
1061
|
-
stream.destroy();
|
|
1062
|
-
resolve();
|
|
1063
|
-
}
|
|
1064
|
-
});
|
|
1065
|
-
stream.on('end', resolve);
|
|
1066
|
-
stream.on('error', reject);
|
|
1067
|
-
});
|
|
1068
|
-
if (keys.length === 0) {
|
|
1069
|
-
return `📭 No cached context found.\n\nUse \`remember_context\` to cache context for faster future access.`;
|
|
1070
|
-
}
|
|
1071
|
-
const lines = [`🧠 **Cached Context** (${keys.length} entries):\n`];
|
|
1072
|
-
for (const k of keys.slice(0, limit)) {
|
|
1073
|
-
const ttl = await redis.ttl(k);
|
|
1074
|
-
const content = await redis.get(k);
|
|
1075
|
-
const parts = k.replace('cachly:ctx:', '').split(':');
|
|
1076
|
-
const cat = parts[0];
|
|
1077
|
-
const key = parts.slice(1).join(':');
|
|
1078
|
-
const preview = content?.slice(0, 80).replace(/\n/g, ' ') ?? '';
|
|
1079
|
-
lines.push(` • **${key}** (${cat})`, ` Size: ${content?.length ?? 0} chars · TTL: ${ttl === -1 ? '∞' : `${Math.round(ttl / 60)}m`}`, ` _"${preview}${(content?.length ?? 0) > 80 ? '…' : ''}"_`, ``);
|
|
1080
|
-
}
|
|
1081
|
-
return lines.join('\n');
|
|
1082
|
-
}
|
|
1083
|
-
case 'forget_context': {
|
|
1084
|
-
const { instance_id, keys } = args;
|
|
1085
|
-
const redis = await getConnection(instance_id);
|
|
1086
|
-
let deleted = 0;
|
|
1087
|
-
for (const key of keys) {
|
|
1088
|
-
if (key.includes('*')) {
|
|
1089
|
-
// Glob delete
|
|
1090
|
-
const toDelete = [];
|
|
1091
|
-
const stream = redis.scanStream({ match: `cachly:ctx:*:${key}*`, count: 100 });
|
|
1092
|
-
await new Promise((resolve, reject) => {
|
|
1093
|
-
stream.on('data', (batch) => toDelete.push(...batch));
|
|
1094
|
-
stream.on('end', resolve);
|
|
1095
|
-
stream.on('error', reject);
|
|
1096
|
-
});
|
|
1097
|
-
if (toDelete.length > 0) {
|
|
1098
|
-
deleted += await redis.del(...toDelete);
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
else {
|
|
1102
|
-
// Try all categories
|
|
1103
|
-
const categories = ['overview', 'architecture', 'file_summary', 'dependency', 'thinking', 'custom'];
|
|
1104
|
-
for (const cat of categories) {
|
|
1105
|
-
deleted += await redis.del(`cachly:ctx:${cat}:${key}`, `cachly:ctx:${cat}:${key}:meta`);
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
return `🗑️ **Forgot ${deleted} context entries.**\n\nKeys: ${keys.map(k => `\`${k}\``).join(', ')}`;
|
|
1110
|
-
}
|
|
1111
404
|
case 'get_api_status': {
|
|
1112
405
|
// Check health
|
|
1113
406
|
let healthStatus = 'unknown';
|
|
@@ -1169,3264 +462,10 @@ async function handleTool(name, args) {
|
|
|
1169
462
|
authInfo,
|
|
1170
463
|
].join('\n');
|
|
1171
464
|
}
|
|
1172
|
-
// ── session_start ─────────────────────────────────────────────────────────
|
|
1173
|
-
case 'sync_file_changes': {
|
|
1174
|
-
const { instance_id, changed_files, git_diff_stat, commit_msg } = args;
|
|
1175
|
-
const redis = await getConnection(instance_id);
|
|
1176
|
-
// Store file change event in session history
|
|
1177
|
-
const changeRecord = {
|
|
1178
|
-
ts: new Date().toISOString(),
|
|
1179
|
-
files: changed_files,
|
|
1180
|
-
commit_msg,
|
|
1181
|
-
diff_stat: git_diff_stat?.slice(0, 500),
|
|
1182
|
-
};
|
|
1183
|
-
await redis.lpush('cachly:session:file_changes', JSON.stringify(changeRecord));
|
|
1184
|
-
await redis.ltrim('cachly:session:file_changes', 0, 99);
|
|
1185
|
-
await redis.expire('cachly:session:file_changes', 30 * 86400);
|
|
1186
|
-
// Find lessons relevant to the changed files
|
|
1187
|
-
const lessonKeys = [];
|
|
1188
|
-
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
1189
|
-
await new Promise((resolve, reject) => {
|
|
1190
|
-
lStream.on('data', (batch) => lessonKeys.push(...batch));
|
|
1191
|
-
lStream.on('end', resolve);
|
|
1192
|
-
lStream.on('error', reject);
|
|
1193
|
-
});
|
|
1194
|
-
const relevant = [];
|
|
1195
|
-
for (const k of lessonKeys) {
|
|
1196
|
-
const raw = await redis.get(k);
|
|
1197
|
-
if (!raw)
|
|
1198
|
-
continue;
|
|
1199
|
-
const lesson = JSON.parse(raw);
|
|
1200
|
-
// Match by file_paths stored in lesson OR by topic keywords matching file name
|
|
1201
|
-
const topicWords = lesson.topic.toLowerCase().split(/[:\-_]/);
|
|
1202
|
-
const fileMatches = changed_files.some(f => {
|
|
1203
|
-
const fname = f.split('/').pop()?.replace(/\.[^.]+$/, '').toLowerCase() ?? '';
|
|
1204
|
-
return topicWords.some(w => w.length > 3 && fname.includes(w))
|
|
1205
|
-
|| (lesson.file_paths ?? []).some(lf => f.includes(lf) || lf.includes(f));
|
|
1206
|
-
});
|
|
1207
|
-
if (fileMatches) {
|
|
1208
|
-
const emoji = lesson.outcome === 'success' ? '✅' : '⚠️';
|
|
1209
|
-
relevant.push(` ${emoji} \`${lesson.topic}\` — ${lesson.what_worked.slice(0, 80)}`);
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
const lines = [
|
|
1213
|
-
`📁 **File sync recorded**: ${changed_files.length} files`,
|
|
1214
|
-
commit_msg ? `📝 Commit: "${commit_msg}"` : '',
|
|
1215
|
-
'',
|
|
1216
|
-
`**Changed:** ${changed_files.slice(0, 8).map(f => `\`${f}\``).join(', ')}${changed_files.length > 8 ? ` +${changed_files.length - 8} more` : ''}`,
|
|
1217
|
-
'',
|
|
1218
|
-
];
|
|
1219
|
-
if (relevant.length > 0) {
|
|
1220
|
-
lines.push(`🧠 **Relevant brain lessons (${relevant.length}):**`, ...relevant);
|
|
1221
|
-
}
|
|
1222
|
-
else {
|
|
1223
|
-
lines.push(`💡 No existing lessons match these files yet. Add them with \`learn_from_attempts\`.`);
|
|
1224
|
-
}
|
|
1225
|
-
return lines.filter(Boolean).join('\n');
|
|
1226
|
-
}
|
|
1227
|
-
// ── team_learn ────────────────────────────────────────────────────────────
|
|
1228
|
-
case 'team_learn': {
|
|
1229
|
-
const { instance_id, author, topic, outcome, what_worked, what_failed, severity, file_paths, commands, tags } = args;
|
|
1230
|
-
if (!author || !topic || !outcome || !what_worked) {
|
|
1231
|
-
return '❌ Required: author, topic, outcome, what_worked';
|
|
1232
|
-
}
|
|
1233
|
-
const iid = instance_id;
|
|
1234
|
-
if (!iid)
|
|
1235
|
-
return '❌ instance_id required';
|
|
1236
|
-
// Store with author attribution via the same learn_from_attempts Redis structure
|
|
1237
|
-
const lesson = {
|
|
1238
|
-
topic, outcome, what_worked,
|
|
1239
|
-
what_failed: what_failed ?? '',
|
|
1240
|
-
severity: severity ?? 'minor',
|
|
1241
|
-
author,
|
|
1242
|
-
file_paths: file_paths ?? [],
|
|
1243
|
-
commands: commands ?? [],
|
|
1244
|
-
tags: [...(tags ?? []), 'team'],
|
|
1245
|
-
timestamp: new Date().toISOString(),
|
|
1246
|
-
recall_count: 0,
|
|
1247
|
-
version: 2,
|
|
1248
|
-
};
|
|
1249
|
-
const redis = await getConnection(iid);
|
|
1250
|
-
const key = `cachly:lessons:${topic}`;
|
|
1251
|
-
await redis.rpush(key, JSON.stringify(lesson));
|
|
1252
|
-
if (outcome === 'success') {
|
|
1253
|
-
await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(lesson));
|
|
1254
|
-
}
|
|
1255
|
-
return `✅ Team lesson stored by **${author}**: \`${topic}\` (${outcome})\n💡 ${what_worked.slice(0, 120)}`;
|
|
1256
|
-
}
|
|
1257
|
-
// ── team_recall ───────────────────────────────────────────────────────────
|
|
1258
|
-
case 'team_recall': {
|
|
1259
|
-
const { instance_id, topic, author, limit = 10 } = args;
|
|
1260
|
-
const redis = await getConnection(instance_id);
|
|
1261
|
-
const lessonKeys = [];
|
|
1262
|
-
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
1263
|
-
await new Promise((resolve, reject) => {
|
|
1264
|
-
lStream.on('data', (batch) => lessonKeys.push(...batch));
|
|
1265
|
-
lStream.on('end', resolve);
|
|
1266
|
-
lStream.on('error', reject);
|
|
1267
|
-
});
|
|
1268
|
-
let lessons = [];
|
|
1269
|
-
for (const k of lessonKeys) {
|
|
1270
|
-
const raw = await redis.get(k);
|
|
1271
|
-
if (!raw)
|
|
1272
|
-
continue;
|
|
1273
|
-
try {
|
|
1274
|
-
lessons.push(JSON.parse(raw));
|
|
1275
|
-
}
|
|
1276
|
-
catch { /* skip */ }
|
|
1277
|
-
}
|
|
1278
|
-
// Filter
|
|
1279
|
-
if (topic) {
|
|
1280
|
-
const t = topic.toLowerCase();
|
|
1281
|
-
lessons = lessons.filter(l => l.topic.toLowerCase().includes(t) ||
|
|
1282
|
-
(l.tags ?? []).some((tag) => tag.toLowerCase().includes(t)));
|
|
1283
|
-
}
|
|
1284
|
-
if (author) {
|
|
1285
|
-
const a = author.toLowerCase();
|
|
1286
|
-
lessons = lessons.filter(l => l.author?.toLowerCase().includes(a));
|
|
1287
|
-
}
|
|
1288
|
-
// Sort by recall_count desc
|
|
1289
|
-
lessons.sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0));
|
|
1290
|
-
lessons = lessons.slice(0, limit);
|
|
1291
|
-
if (lessons.length === 0) {
|
|
1292
|
-
return topic
|
|
1293
|
-
? `📭 No team lessons found for \`${topic}\`.\n\nShared instance: add lessons with \`learn_from_attempts\` and include an \`author\` field.`
|
|
1294
|
-
: `📭 No lessons in this brain yet.\n\nAll team members sharing this instance will see lessons here.`;
|
|
1295
|
-
}
|
|
1296
|
-
const lines = [`👥 **Team Brain** — ${lessons.length} lesson${lessons.length > 1 ? 's' : ''}`, ''];
|
|
1297
|
-
for (const l of lessons) {
|
|
1298
|
-
const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
|
|
1299
|
-
const sev = l.severity === 'critical' ? '🔴 ' : l.severity === 'major' ? '🟡 ' : '';
|
|
1300
|
-
const authorStr = l.author ? ` · _by ${l.author}_` : '';
|
|
1301
|
-
const recallStr = (l.recall_count ?? 0) > 0 ? ` · recalled ${l.recall_count}×` : '';
|
|
1302
|
-
const ago = Math.round((Date.now() - new Date(l.ts).getTime()) / 86400000);
|
|
1303
|
-
const agoStr = ago === 0 ? 'today' : ago === 1 ? 'yesterday' : `${ago}d ago`;
|
|
1304
|
-
lines.push(`${emoji} ${sev}**\`${l.topic}\`**${authorStr}${recallStr} · ${agoStr}`);
|
|
1305
|
-
lines.push(` ${l.what_worked.slice(0, 120)}`);
|
|
1306
|
-
lines.push('');
|
|
1307
|
-
}
|
|
1308
|
-
return lines.join('\n');
|
|
1309
|
-
}
|
|
1310
|
-
// ── team_synthesize — Team Brain Synthesis ────────────────────────────────
|
|
1311
|
-
case 'team_synthesize': {
|
|
1312
|
-
const { instance_id, topic } = args;
|
|
1313
|
-
const redis = await getConnection(instance_id);
|
|
1314
|
-
// Load history list for this topic (all authors' contributions)
|
|
1315
|
-
const listKey = `cachly:lessons:${topic}`;
|
|
1316
|
-
const all = await redis.lrange(listKey, 0, -1);
|
|
1317
|
-
if (all.length < 2) {
|
|
1318
|
-
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}", ...)\`.`;
|
|
1319
|
-
}
|
|
1320
|
-
const entries = all.map(r => { try {
|
|
1321
|
-
return JSON.parse(r);
|
|
1322
|
-
}
|
|
1323
|
-
catch {
|
|
1324
|
-
return null;
|
|
1325
|
-
} }).filter((e) => e !== null);
|
|
1326
|
-
// Group by outcome
|
|
1327
|
-
const successes = entries.filter(e => e.outcome === 'success');
|
|
1328
|
-
const failures = entries.filter(e => e.outcome === 'failure');
|
|
1329
|
-
const partials = entries.filter(e => e.outcome === 'partial');
|
|
1330
|
-
const authors = [...new Set(entries.map(e => e.author).filter(Boolean))];
|
|
1331
|
-
const hasMultiAuthor = authors.length > 1;
|
|
1332
|
-
// Build canonical merged version
|
|
1333
|
-
// what_worked: pick the most recent success, or longest for most detail
|
|
1334
|
-
const bestSuccess = successes.sort((a, b) => new Date(b.ts).getTime() - new Date(a.ts).getTime())[0];
|
|
1335
|
-
const whatWorkedCandidates = successes.map(e => e.what_worked).filter(w => w && w.length > 10);
|
|
1336
|
-
const canonicalWorked = whatWorkedCandidates.sort((a, b) => b.length - a.length)[0] ?? bestSuccess?.what_worked ?? '';
|
|
1337
|
-
// what_failed: union of all unique failure reasons
|
|
1338
|
-
const allFailed = [...new Set([...failures, ...partials].map(e => e.what_failed).filter((w) => !!w && w.length > 5))];
|
|
1339
|
-
const severities = entries.map(e => e.severity).filter(Boolean);
|
|
1340
|
-
const canonicalSeverity = severities.includes('critical') ? 'critical' : severities.includes('major') ? 'major' : 'minor';
|
|
1341
|
-
const lines = [
|
|
1342
|
-
`🧬 **Team Brain Synthesis: \`${topic}\`**`,
|
|
1343
|
-
`_${entries.length} entries from ${authors.length} author${authors.length === 1 ? '' : 's'}${hasMultiAuthor ? ` (${authors.join(', ')})` : ''} · ${successes.length} success · ${failures.length} failure · ${partials.length} partial_`,
|
|
1344
|
-
'',
|
|
1345
|
-
`**Canonical "what worked":**`,
|
|
1346
|
-
`> ${canonicalWorked}`,
|
|
1347
|
-
'',
|
|
1348
|
-
allFailed.length > 0 ? `**Avoid (combined failures):**` : '',
|
|
1349
|
-
...allFailed.map(f => `> ❌ ${f}`),
|
|
1350
|
-
allFailed.length > 0 ? '' : '',
|
|
1351
|
-
`**Suggested canonical lesson:**`,
|
|
1352
|
-
'```',
|
|
1353
|
-
`learn_from_attempts(`,
|
|
1354
|
-
` topic = "${topic}",`,
|
|
1355
|
-
` outcome = "success",`,
|
|
1356
|
-
` what_worked = "${canonicalWorked.replace(/"/g, "'")}",`,
|
|
1357
|
-
allFailed.length > 0 ? ` what_failed = "${allFailed[0].replace(/"/g, "'")}",` : '',
|
|
1358
|
-
` severity = "${canonicalSeverity}",`,
|
|
1359
|
-
`)`,
|
|
1360
|
-
'```',
|
|
1361
|
-
'',
|
|
1362
|
-
hasMultiAuthor
|
|
1363
|
-
? `💡 _${authors.length} team members contributed to this synthesis. Store the canonical version to replace individual entries._`
|
|
1364
|
-
: `💡 _Single author — more value when multiple team members contribute to the same topic._`,
|
|
1365
|
-
].filter(l => l !== undefined).join('\n');
|
|
1366
|
-
return lines;
|
|
1367
|
-
}
|
|
1368
|
-
// ── brain_doctor ──────────────────────────────────────────────────────────
|
|
1369
|
-
// ── memory_crystalize ─────────────────────────────────────────────────────
|
|
1370
|
-
case 'memory_crystalize': {
|
|
1371
|
-
const { instance_id, label: crystalLabel = '' } = args;
|
|
1372
|
-
const redis = await getConnection(instance_id);
|
|
1373
|
-
const now = new Date();
|
|
1374
|
-
const week = `${now.getFullYear()}-W${String(Math.ceil((now.getDate() - now.getDay() + 10) / 7)).padStart(2, '0')}`;
|
|
1375
|
-
const effectiveLabel = crystalLabel || `${now.toISOString().slice(0, 7)} Crystal`;
|
|
1376
|
-
// Read session history
|
|
1377
|
-
const sessionHistory = await redis.lrange('cachly:session:history', 0, 49);
|
|
1378
|
-
// Read all auto-learned lessons
|
|
1379
|
-
const allLessonKeys = [];
|
|
1380
|
-
const ls = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
1381
|
-
await new Promise((res, rej) => {
|
|
1382
|
-
ls.on('data', (b) => allLessonKeys.push(...b));
|
|
1383
|
-
ls.on('end', res);
|
|
1384
|
-
ls.on('error', rej);
|
|
1385
|
-
});
|
|
1386
|
-
const allLessons = [];
|
|
1387
|
-
for (const k of allLessonKeys) {
|
|
1388
|
-
const raw = await redis.get(k);
|
|
1389
|
-
if (!raw)
|
|
1390
|
-
continue;
|
|
1391
|
-
try {
|
|
1392
|
-
allLessons.push(JSON.parse(raw));
|
|
1393
|
-
}
|
|
1394
|
-
catch { /* skip */ }
|
|
1395
|
-
}
|
|
1396
|
-
// Group lessons by top-level category
|
|
1397
|
-
const categoryMap = new Map();
|
|
1398
|
-
for (const l of allLessons) {
|
|
1399
|
-
const cat = l.topic.split(':')[0] || 'misc';
|
|
1400
|
-
if (!categoryMap.has(cat))
|
|
1401
|
-
categoryMap.set(cat, []);
|
|
1402
|
-
categoryMap.get(cat).push(l);
|
|
1403
|
-
}
|
|
1404
|
-
// Build top patterns (most frequent categories with a representative insight)
|
|
1405
|
-
const topPatterns = [];
|
|
1406
|
-
for (const [cat, catLessons] of [...categoryMap.entries()].sort((a, b) => b[1].length - a[1].length).slice(0, 8)) {
|
|
1407
|
-
const successLessons = catLessons.filter(l => l.outcome === 'success');
|
|
1408
|
-
const best = successLessons[0] ?? catLessons[0];
|
|
1409
|
-
if (!best)
|
|
1410
|
-
continue;
|
|
1411
|
-
topPatterns.push({
|
|
1412
|
-
category: cat,
|
|
1413
|
-
insight: best.what_worked.slice(0, 120),
|
|
1414
|
-
count: catLessons.length,
|
|
1415
|
-
});
|
|
1416
|
-
}
|
|
1417
|
-
const crystal = {
|
|
1418
|
-
label: effectiveLabel,
|
|
1419
|
-
ts: now.toISOString(),
|
|
1420
|
-
session_count: sessionHistory.length,
|
|
1421
|
-
lesson_count: allLessons.length,
|
|
1422
|
-
top_patterns: topPatterns,
|
|
1423
|
-
categories: [...categoryMap.keys()],
|
|
1424
|
-
created_from: `${sessionHistory.length} sessions, ${allLessons.length} lessons`,
|
|
1425
|
-
};
|
|
1426
|
-
const crystalJson = JSON.stringify(crystal);
|
|
1427
|
-
await redis.set('cachly:crystal:latest', crystalJson);
|
|
1428
|
-
await redis.expire('cachly:crystal:latest', 90 * 86400);
|
|
1429
|
-
await redis.set(`cachly:crystal:${week}`, crystalJson);
|
|
1430
|
-
await redis.expire(`cachly:crystal:${week}`, 365 * 86400);
|
|
1431
|
-
const lines = [
|
|
1432
|
-
`💎 **Memory Crystal created: ${effectiveLabel}**`,
|
|
1433
|
-
``,
|
|
1434
|
-
`📊 Compressed: **${sessionHistory.length} sessions** + **${allLessons.length} lessons** → ${topPatterns.length} top patterns`,
|
|
1435
|
-
``,
|
|
1436
|
-
`**Top patterns by category:**`,
|
|
1437
|
-
...topPatterns.slice(0, 6).map(p => ` • **${p.category}** (${p.count}×): ${p.insight.slice(0, 90)}`),
|
|
1438
|
-
``,
|
|
1439
|
-
`💡 This crystal will appear in every future \`session_start\` briefing.`,
|
|
1440
|
-
`💡 Re-run \`memory_crystalize\` monthly to keep it fresh.`,
|
|
1441
|
-
];
|
|
1442
|
-
return lines.join('\n');
|
|
1443
|
-
}
|
|
1444
|
-
case 'brain_doctor': {
|
|
1445
|
-
const { instance_id, workspace_path: drWorkspacePath = '' } = args;
|
|
1446
|
-
const redis = await getConnection(instance_id);
|
|
1447
|
-
const issues = [];
|
|
1448
|
-
const checks = [];
|
|
1449
|
-
// Count lessons
|
|
1450
|
-
const lessonKeys = [];
|
|
1451
|
-
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
1452
|
-
await new Promise((resolve, reject) => {
|
|
1453
|
-
lStream.on('data', (batch) => lessonKeys.push(...batch));
|
|
1454
|
-
lStream.on('end', resolve);
|
|
1455
|
-
lStream.on('error', reject);
|
|
1456
|
-
});
|
|
1457
|
-
// Count context
|
|
1458
|
-
let ctxCount = 0;
|
|
1459
|
-
const ctxStream = redis.scanStream({ match: 'cachly:ctx:*', count: 200 });
|
|
1460
|
-
await new Promise((resolve, reject) => {
|
|
1461
|
-
ctxStream.on('data', (batch) => {
|
|
1462
|
-
ctxCount += batch.filter((k) => !k.endsWith(':meta')).length;
|
|
1463
|
-
});
|
|
1464
|
-
ctxStream.on('end', resolve);
|
|
1465
|
-
ctxStream.on('error', reject);
|
|
1466
|
-
});
|
|
1467
|
-
const lessons = [];
|
|
1468
|
-
for (const k of lessonKeys) {
|
|
1469
|
-
const raw = await redis.get(k);
|
|
1470
|
-
if (!raw)
|
|
1471
|
-
continue;
|
|
1472
|
-
try {
|
|
1473
|
-
lessons.push(JSON.parse(raw));
|
|
1474
|
-
}
|
|
1475
|
-
catch { /* skip */ }
|
|
1476
|
-
}
|
|
1477
|
-
// Last session
|
|
1478
|
-
const lastSessionRaw = await redis.get('cachly:session:last');
|
|
1479
|
-
let lastSession = null;
|
|
1480
|
-
if (lastSessionRaw) {
|
|
1481
|
-
try {
|
|
1482
|
-
lastSession = JSON.parse(lastSessionRaw);
|
|
1483
|
-
}
|
|
1484
|
-
catch { /* ignore */ }
|
|
1485
|
-
}
|
|
1486
|
-
// Open failures
|
|
1487
|
-
const openFailures = lessons.filter(l => l.outcome === 'failure' || l.outcome === 'partial');
|
|
1488
|
-
// Unused lessons (never recalled)
|
|
1489
|
-
const unusedLessons = lessons.filter(l => (l.recall_count ?? 0) === 0);
|
|
1490
|
-
// Critical lessons
|
|
1491
|
-
const criticalLessons = lessons.filter(l => l.severity === 'critical');
|
|
1492
|
-
// Confidence decay analysis
|
|
1493
|
-
const staleLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) < CONFIDENCE_STALE_VALUE);
|
|
1494
|
-
const warnLessons = lessons.filter(l => l.outcome === 'success' && calculateConfidence(l) >= CONFIDENCE_STALE_VALUE && calculateConfidence(l) < CONFIDENCE_WARN_VALUE);
|
|
1495
|
-
const withAudit = lessons.filter(l => (l.audit_trail ?? []).length > 1);
|
|
1496
|
-
const teamLessons = lessons.filter(l => l.author);
|
|
1497
|
-
const uniqueAuthors = new Set(lessons.map(l => l.author).filter(Boolean));
|
|
1498
|
-
// Effective IQ boost: total recalls / lessons (how much the brain actually helped)
|
|
1499
|
-
const totalRecalls = lessons.reduce((sum, l) => sum + (l.recall_count ?? 0), 0);
|
|
1500
|
-
const iqBoostPct = lessons.length > 0 ? Math.min(100, Math.round((totalRecalls / lessons.length) * 10)) : 0;
|
|
1501
|
-
// Quality score (0-100)
|
|
1502
|
-
let score = 50;
|
|
1503
|
-
if (lessonKeys.length >= 5)
|
|
1504
|
-
score += 10;
|
|
1505
|
-
if (lessonKeys.length >= 20)
|
|
1506
|
-
score += 10;
|
|
1507
|
-
if (ctxCount >= 3)
|
|
1508
|
-
score += 10;
|
|
1509
|
-
if (ctxCount >= 10)
|
|
1510
|
-
score += 5;
|
|
1511
|
-
if (lastSession)
|
|
1512
|
-
score += 10;
|
|
1513
|
-
if (openFailures.length === 0)
|
|
1514
|
-
score += 5;
|
|
1515
|
-
const unusedRatio = lessons.length > 0 ? unusedLessons.length / lessons.length : 0;
|
|
1516
|
-
if (unusedRatio < 0.5)
|
|
1517
|
-
score += 10;
|
|
1518
|
-
if (staleLessons.length === 0)
|
|
1519
|
-
score += 5;
|
|
1520
|
-
if (uniqueAuthors.size >= 2)
|
|
1521
|
-
score += 5; // team collaboration bonus
|
|
1522
|
-
const scoreEmoji = score >= 80 ? '🟢' : score >= 50 ? '🟡' : '🔴';
|
|
1523
|
-
const iqEmoji = iqBoostPct >= 50 ? '🚀' : iqBoostPct >= 20 ? '📈' : '💤';
|
|
1524
|
-
checks.push(`${scoreEmoji} **Brain Quality Score: ${score}/100**`);
|
|
1525
|
-
checks.push(`${iqEmoji} **Effective IQ Boost: ${iqBoostPct}%** (${totalRecalls} recalls across ${lessons.length} lessons)`);
|
|
1526
|
-
checks.push(`📚 **Lessons:** ${lessonKeys.length} (${criticalLessons.length} critical · ${withAudit.length} with audit trail · ${teamLessons.length} from team)`);
|
|
1527
|
-
checks.push(`💾 **Context entries:** ${ctxCount}`);
|
|
1528
|
-
checks.push(`🎯 **Confidence:** ${lessons.length - staleLessons.length - warnLessons.length} fresh · ${warnLessons.length} warn · ${staleLessons.length} stale`);
|
|
1529
|
-
checks.push(`⏱️ **Decay config:** warn after ${CONFIDENCE_WARN_DAYS}d · stale after ${CONFIDENCE_STALE_DAYS}d`);
|
|
1530
|
-
if (uniqueAuthors.size >= 2) {
|
|
1531
|
-
checks.push(`👥 **Team:** ${uniqueAuthors.size} contributors (${[...uniqueAuthors].join(', ')})`);
|
|
1532
|
-
}
|
|
1533
|
-
// Stale index detection
|
|
1534
|
-
try {
|
|
1535
|
-
const lastIndexRaw = await redis.get('cachly:index:last_run');
|
|
1536
|
-
if (lastIndexRaw) {
|
|
1537
|
-
const lastIndexAge = Math.round((Date.now() - new Date(lastIndexRaw).getTime()) / 86_400_000);
|
|
1538
|
-
if (lastIndexAge > 7) {
|
|
1539
|
-
issues.push(`🔄 Index is ${lastIndexAge}d stale — run \`index_project\` to re-sync semantic search`);
|
|
1540
|
-
}
|
|
1541
|
-
else {
|
|
1542
|
-
checks.push(`🗂️ **Semantic index:** ${lastIndexAge}d old (fresh)`);
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
else {
|
|
1546
|
-
issues.push(`💡 No semantic index — run \`index_project(dir="<your-src>")\` to enable semantic search`);
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
catch { /* non-critical */ }
|
|
1550
|
-
// Memory crystal status
|
|
1551
|
-
try {
|
|
1552
|
-
const crystalRaw = await redis.get('cachly:crystal:latest');
|
|
1553
|
-
if (crystalRaw) {
|
|
1554
|
-
const crystal = JSON.parse(crystalRaw);
|
|
1555
|
-
const crystalAge = Math.round((Date.now() - new Date(crystal.ts).getTime()) / 86_400_000);
|
|
1556
|
-
checks.push(`💎 **Memory Crystal:** ${crystal.label} (${crystalAge}d ago)`);
|
|
1557
|
-
if (crystalAge > 30)
|
|
1558
|
-
issues.push(`💡 Memory Crystal is ${crystalAge}d old — re-run \`memory_crystalize\` to compress new sessions`);
|
|
1559
|
-
}
|
|
1560
|
-
else if (lessonKeys.length >= 10) {
|
|
1561
|
-
issues.push(`💡 ${lessonKeys.length} lessons but no Memory Crystal — run \`memory_crystalize\` to compress wisdom`);
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
catch { /* non-critical */ }
|
|
1565
|
-
// openclaw cross-promo (check package.json in workspace)
|
|
1566
|
-
if (drWorkspacePath) {
|
|
1567
|
-
try {
|
|
1568
|
-
const pkgPath = drWorkspacePath.replace(/\/$/, '') + '/package.json';
|
|
1569
|
-
const pkgRaw = readFileSync(pkgPath, 'utf-8');
|
|
1570
|
-
const pkg = JSON.parse(pkgRaw);
|
|
1571
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1572
|
-
const hasLLMDep = ['openai', '@anthropic-ai/sdk', '@google/generative-ai', 'mistralai', 'cohere-ai'].some(d => d in allDeps);
|
|
1573
|
-
const hasOpenclaw = '@cachly-dev/openclaw' in allDeps;
|
|
1574
|
-
if (hasLLMDep && !hasOpenclaw) {
|
|
1575
|
-
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\``);
|
|
1576
|
-
issues.push(` → \`npm install @cachly-dev/openclaw\` cuts LLM costs 60–90% with 3 lines of code`);
|
|
1577
|
-
}
|
|
1578
|
-
else if (hasOpenclaw) {
|
|
1579
|
-
checks.push(`✅ **@cachly-dev/openclaw installed** (LLM cost caching active)`);
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
catch { /* no package.json or unreadable */ }
|
|
1583
|
-
}
|
|
1584
|
-
if (lastSession) {
|
|
1585
|
-
const ageMin = Math.round((Date.now() - new Date(lastSession.ts).getTime()) / 60000);
|
|
1586
|
-
const ageStr = ageMin < 60 ? `${ageMin}m` : ageMin < 1440 ? `${Math.round(ageMin / 60)}h` : `${Math.round(ageMin / 1440)}d`;
|
|
1587
|
-
checks.push(`🕐 **Last session:** ${ageStr} ago`);
|
|
1588
|
-
}
|
|
1589
|
-
else {
|
|
1590
|
-
issues.push('❌ No session history — call `session_start` + `session_end` to start tracking');
|
|
1591
|
-
}
|
|
1592
|
-
if (lessonKeys.length === 0) {
|
|
1593
|
-
issues.push('❌ No lessons — call `learn_from_attempts` after solving bugs');
|
|
1594
|
-
}
|
|
1595
|
-
else if (lessonKeys.length < 5) {
|
|
1596
|
-
issues.push(`💡 Only ${lessonKeys.length} lessons — add more after each problem solved`);
|
|
1597
|
-
}
|
|
1598
|
-
if (iqBoostPct === 0 && lessons.length >= 5) {
|
|
1599
|
-
issues.push(`💤 **IQ Boost is 0%** — lessons exist but are never recalled. Use \`recall_best_solution\` BEFORE tasks.`);
|
|
1600
|
-
}
|
|
1601
|
-
if (ctxCount === 0) {
|
|
1602
|
-
issues.push('💡 No context — use `remember_context` to cache architecture docs, ADRs, etc.');
|
|
1603
|
-
}
|
|
1604
|
-
if (openFailures.length > 0) {
|
|
1605
|
-
issues.push(`⚠️ ${openFailures.length} unresolved failure${openFailures.length > 1 ? 's' : ''}: ${openFailures.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
|
|
1606
|
-
}
|
|
1607
|
-
if (staleLessons.length > 0) {
|
|
1608
|
-
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(', ')}`);
|
|
1609
|
-
issues.push(` → Re-verify with \`recall_best_solution\` to reset confidence clock`);
|
|
1610
|
-
}
|
|
1611
|
-
if (warnLessons.length > 0) {
|
|
1612
|
-
issues.push(`⚠️ ${warnLessons.length} lesson${warnLessons.length > 1 ? 's' : ''} aging (>${CONFIDENCE_WARN_DAYS}d): ${warnLessons.slice(0, 3).map(l => `\`${l.topic}\``).join(', ')}`);
|
|
1613
|
-
}
|
|
1614
|
-
if (unusedRatio > 0.7 && lessons.length > 5) {
|
|
1615
|
-
issues.push(`💡 ${unusedLessons.length} lessons never recalled — verify topics match your workflow`);
|
|
1616
|
-
}
|
|
1617
|
-
const lines = ['🩺 **Brain Doctor Report**', '', ...checks.map(c => ' ' + c), ''];
|
|
1618
|
-
if (issues.length > 0) {
|
|
1619
|
-
lines.push('**Issues to fix:**');
|
|
1620
|
-
for (const i of issues)
|
|
1621
|
-
lines.push(' ' + i);
|
|
1622
|
-
lines.push('');
|
|
1623
|
-
}
|
|
1624
|
-
else {
|
|
1625
|
-
lines.push(' 🎉 Brain looks healthy! Keep calling session_start/session_end.');
|
|
1626
|
-
}
|
|
1627
|
-
return lines.join('\n');
|
|
1628
|
-
}
|
|
1629
|
-
// ── recall_at — Brain Archaeology ────────────────────────────────────────
|
|
1630
|
-
case 'recall_at': {
|
|
1631
|
-
const { instance_id, topic, date } = args;
|
|
1632
|
-
const redis = await getConnection(instance_id);
|
|
1633
|
-
const cutoff = new Date(date).getTime();
|
|
1634
|
-
if (isNaN(cutoff))
|
|
1635
|
-
return `❌ Invalid date "${date}". Use ISO format: "2026-01-15"`;
|
|
1636
|
-
const listKey = `cachly:lessons:${topic}`;
|
|
1637
|
-
const all = await redis.lrange(listKey, 0, -1);
|
|
1638
|
-
if (all.length === 0)
|
|
1639
|
-
return `📭 No history found for \`${topic}\`. Lessons are stored via \`learn_from_attempts\`.`;
|
|
1640
|
-
const before = all
|
|
1641
|
-
.map(raw => { try {
|
|
1642
|
-
return JSON.parse(raw);
|
|
1643
|
-
}
|
|
1644
|
-
catch {
|
|
1645
|
-
return null;
|
|
1646
|
-
} })
|
|
1647
|
-
.filter((l) => l !== null && new Date(l.ts).getTime() <= cutoff)
|
|
1648
|
-
.sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
|
|
1649
|
-
if (before.length === 0)
|
|
1650
|
-
return `📭 No entries for \`${topic}\` found before **${date}**. Earliest entry: ${new Date(JSON.parse(all[0]).ts).toLocaleDateString('de-DE')}.`;
|
|
1651
|
-
const lines = [
|
|
1652
|
-
`🏺 **Brain Archaeology: \`${topic}\` before ${date}**`,
|
|
1653
|
-
`_${before.length} of ${all.length} total entries shown_`,
|
|
1654
|
-
'',
|
|
1655
|
-
];
|
|
1656
|
-
for (const l of before.slice(-10)) {
|
|
1657
|
-
const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
|
|
1658
|
-
const d = new Date(l.ts).toLocaleDateString('de-DE');
|
|
1659
|
-
lines.push(`**${d}** ${emoji} ${l.outcome}`);
|
|
1660
|
-
if (l.what_worked)
|
|
1661
|
-
lines.push(` → ${l.what_worked.slice(0, 100)}`);
|
|
1662
|
-
lines.push('');
|
|
1663
|
-
}
|
|
1664
|
-
lines.push(`_Full evolution: ${all.map(r => { try {
|
|
1665
|
-
const l = JSON.parse(r);
|
|
1666
|
-
return l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
|
|
1667
|
-
}
|
|
1668
|
-
catch {
|
|
1669
|
-
return '?';
|
|
1670
|
-
} }).join(' → ')}_`);
|
|
1671
|
-
return lines.join('\n');
|
|
1672
|
-
}
|
|
1673
|
-
// ── trace_dependency — Causal Chain ──────────────────────────────────────
|
|
1674
|
-
case 'trace_dependency': {
|
|
1675
|
-
const { instance_id, dependency, mark_review = false } = args;
|
|
1676
|
-
const redis = await getConnection(instance_id);
|
|
1677
|
-
const depKey = `cachly:dep:${dependency}`;
|
|
1678
|
-
const raw = await redis.get(depKey);
|
|
1679
|
-
if (!raw)
|
|
1680
|
-
return `📭 No lessons found that depend on \`${dependency}\`.\n\nAdd dependencies via: \`learn_from_attempts(..., depends_on=["${dependency}"])\``;
|
|
1681
|
-
const topics = JSON.parse(raw);
|
|
1682
|
-
const lines = [
|
|
1683
|
-
`🔗 **Causal Chain: \`${dependency}\`** — ${topics.length} dependent lesson${topics.length === 1 ? '' : 's'}`,
|
|
1684
|
-
'',
|
|
1685
|
-
];
|
|
1686
|
-
for (const t of topics) {
|
|
1687
|
-
const lessonRaw = await redis.get(`cachly:lesson:best:${t}`);
|
|
1688
|
-
if (!lessonRaw) {
|
|
1689
|
-
lines.push(` • \`${t}\` _(lesson deleted)_`);
|
|
1690
|
-
continue;
|
|
1691
|
-
}
|
|
1692
|
-
const lesson = JSON.parse(lessonRaw);
|
|
1693
|
-
const emoji = lesson.outcome === 'success' ? '✅' : lesson.outcome === 'partial' ? '⚠️' : '❌';
|
|
1694
|
-
const reviewBadge = lesson.needs_review ? ' 🔍 **needs_review**' : '';
|
|
1695
|
-
lines.push(` ${emoji} \`${t}\` (${lesson.severity ?? 'major'})${reviewBadge}`);
|
|
1696
|
-
if (mark_review) {
|
|
1697
|
-
const updated = { ...lesson, needs_review: true };
|
|
1698
|
-
await redis.set(`cachly:lesson:best:${t}`, JSON.stringify(updated));
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
if (mark_review) {
|
|
1702
|
-
lines.push('', `🔍 All ${topics.length} lessons marked as **needs_review** — verify they still work with the changed dependency.`);
|
|
1703
|
-
}
|
|
1704
|
-
else {
|
|
1705
|
-
lines.push('', `_Run with \`mark_review: true\` to flag all dependent lessons for re-verification._`);
|
|
1706
|
-
}
|
|
1707
|
-
return lines.join('\n');
|
|
1708
|
-
}
|
|
1709
|
-
// ── global_learn ──────────────────────────────────────────────────────────
|
|
1710
|
-
case 'global_learn': {
|
|
1711
|
-
const { instance_id, topic, lesson, severity = 'minor', tags = [] } = args;
|
|
1712
|
-
const redis = await getConnection(instance_id);
|
|
1713
|
-
const key = `cachly:global:lesson:${topic}`;
|
|
1714
|
-
const record = {
|
|
1715
|
-
topic,
|
|
1716
|
-
lesson,
|
|
1717
|
-
severity,
|
|
1718
|
-
tags,
|
|
1719
|
-
ts: new Date().toISOString(),
|
|
1720
|
-
scope: 'global',
|
|
1721
|
-
recall_count: 0,
|
|
1722
|
-
};
|
|
1723
|
-
// Preserve recall_count on update
|
|
1724
|
-
const existing = await redis.get(key);
|
|
1725
|
-
if (existing) {
|
|
1726
|
-
const prev = JSON.parse(existing);
|
|
1727
|
-
record.recall_count = prev.recall_count ?? 0;
|
|
1728
|
-
}
|
|
1729
|
-
await redis.set(key, JSON.stringify(record));
|
|
1730
|
-
return `🌐 **Global lesson stored**: \`${topic}\`\n\n${lesson}\n\nRecallable from any project via \`global_recall(topic="${topic}")\`.`;
|
|
1731
|
-
}
|
|
1732
|
-
// ── global_recall ─────────────────────────────────────────────────────────
|
|
1733
|
-
case 'global_recall': {
|
|
1734
|
-
const { instance_id, topic } = args;
|
|
1735
|
-
const redis = await getConnection(instance_id);
|
|
1736
|
-
const keys = [];
|
|
1737
|
-
const gStream = redis.scanStream({ match: 'cachly:global:lesson:*', count: 200 });
|
|
1738
|
-
await new Promise((resolve, reject) => {
|
|
1739
|
-
gStream.on('data', (batch) => keys.push(...batch));
|
|
1740
|
-
gStream.on('end', resolve);
|
|
1741
|
-
gStream.on('error', reject);
|
|
1742
|
-
});
|
|
1743
|
-
let lessons = [];
|
|
1744
|
-
for (const k of keys) {
|
|
1745
|
-
const raw = await redis.get(k);
|
|
1746
|
-
if (!raw)
|
|
1747
|
-
continue;
|
|
1748
|
-
try {
|
|
1749
|
-
lessons.push(JSON.parse(raw));
|
|
1750
|
-
}
|
|
1751
|
-
catch { /* skip */ }
|
|
1752
|
-
}
|
|
1753
|
-
if (topic) {
|
|
1754
|
-
const t = topic.toLowerCase();
|
|
1755
|
-
lessons = lessons.filter(l => l.topic.toLowerCase().includes(t));
|
|
1756
|
-
}
|
|
1757
|
-
if (lessons.length === 0) {
|
|
1758
|
-
return `📭 No global lessons${topic ? ` for \`${topic}\`` : ''}.\n\nAdd cross-project knowledge with \`global_learn(topic="...", lesson="...")\`.`;
|
|
1759
|
-
}
|
|
1760
|
-
// Increment recall_count
|
|
1761
|
-
for (const l of lessons) {
|
|
1762
|
-
const k = `cachly:global:lesson:${l.topic}`;
|
|
1763
|
-
const raw = await redis.get(k);
|
|
1764
|
-
if (raw) {
|
|
1765
|
-
const rec = JSON.parse(raw);
|
|
1766
|
-
rec.recall_count = (rec.recall_count ?? 0) + 1;
|
|
1767
|
-
await redis.set(k, JSON.stringify(rec));
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
const lines = [`🌐 **Global Brain** — ${lessons.length} lesson${lessons.length > 1 ? 's' : ''}`, ''];
|
|
1771
|
-
for (const l of lessons) {
|
|
1772
|
-
const sev = l.severity === 'critical' ? '🔴 ' : l.severity === 'major' ? '🟡 ' : '';
|
|
1773
|
-
lines.push(`${sev}**\`${l.topic}\`**`);
|
|
1774
|
-
lines.push(l.lesson.slice(0, 200));
|
|
1775
|
-
lines.push('');
|
|
1776
|
-
}
|
|
1777
|
-
return lines.join('\n');
|
|
1778
|
-
}
|
|
1779
|
-
// ── publish_lesson ────────────────────────────────────────────────────────
|
|
1780
|
-
case 'publish_lesson': {
|
|
1781
|
-
const { instance_id, topic, lesson, framework = 'general', severity = 'minor' } = args;
|
|
1782
|
-
const redis = await getConnection(instance_id);
|
|
1783
|
-
// Strip potential PII patterns (emails, tokens, paths)
|
|
1784
|
-
const sanitized = lesson
|
|
1785
|
-
.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '[email]')
|
|
1786
|
-
.replace(/\b(sk-|cky_live_|Bearer\s)[A-Za-z0-9_\-]{8,}/g, '[token]')
|
|
1787
|
-
.replace(/\/Users\/[^\s/]+/g, '/Users/[user]')
|
|
1788
|
-
.replace(/\/home\/[^\s/]+/g, '/home/[user]');
|
|
1789
|
-
const publicLesson = {
|
|
1790
|
-
topic,
|
|
1791
|
-
lesson: sanitized,
|
|
1792
|
-
framework,
|
|
1793
|
-
severity,
|
|
1794
|
-
ts: new Date().toISOString(),
|
|
1795
|
-
published: true,
|
|
1796
|
-
votes: 0,
|
|
1797
|
-
};
|
|
1798
|
-
// Store locally with public flag (future: sync to Cachly public API)
|
|
1799
|
-
const key = `cachly:public:lesson:${framework}:${topic}`;
|
|
1800
|
-
await redis.set(key, JSON.stringify(publicLesson), 'EX', 365 * 86400);
|
|
1801
|
-
return [
|
|
1802
|
-
`📢 **Lesson published!**`,
|
|
1803
|
-
``,
|
|
1804
|
-
`**Topic:** \`${topic}\``,
|
|
1805
|
-
`**Framework:** ${framework}`,
|
|
1806
|
-
`**Content:** ${sanitized.slice(0, 200)}${sanitized.length > 200 ? '…' : ''}`,
|
|
1807
|
-
``,
|
|
1808
|
-
`This lesson is now available in the Public Brain for other developers.`,
|
|
1809
|
-
`Import it anywhere: \`import_public_brain(framework="${framework}")\``,
|
|
1810
|
-
].join('\n');
|
|
1811
|
-
}
|
|
1812
|
-
// ── import_public_brain ───────────────────────────────────────────────────
|
|
1813
|
-
case 'import_public_brain': {
|
|
1814
|
-
const { instance_id, framework, limit = 20 } = args;
|
|
1815
|
-
const redis = await getConnection(instance_id);
|
|
1816
|
-
// Community-curated lessons per framework
|
|
1817
|
-
const COMMUNITY_LESSONS = {
|
|
1818
|
-
nextjs: [
|
|
1819
|
-
{ topic: 'nextjs:image-layout', lesson: 'Use fill + relative parent instead of layout="fill" (deprecated since Next.js 13)', severity: 'major' },
|
|
1820
|
-
{ topic: 'nextjs:app-router-fetch', lesson: 'fetch() in Server Components is cached by default — add {cache:"no-store"} for dynamic data', severity: 'major' },
|
|
1821
|
-
{ topic: 'nextjs:metadata-export', lesson: 'Export metadata const or generateMetadata() — never both in same file', severity: 'minor' },
|
|
1822
|
-
{ topic: 'nextjs:client-boundary', lesson: '"use client" propagates down — keep it at the lowest component, not at page level', severity: 'major' },
|
|
1823
|
-
{ topic: 'nextjs:env-prefix', lesson: 'Only NEXT_PUBLIC_* env vars are exposed to client — others are server-only', severity: 'critical' },
|
|
1824
|
-
{ topic: 'nextjs:revalidate', lesson: 'export const revalidate = 0 disables caching for entire route; use revalidatePath() for on-demand', severity: 'minor' },
|
|
1825
|
-
],
|
|
1826
|
-
fastapi: [
|
|
1827
|
-
{ topic: 'fastapi:async-db', lesson: 'Use async session with asyncpg — sync SQLAlchemy blocks the event loop', severity: 'critical' },
|
|
1828
|
-
{ topic: 'fastapi:pydantic-v2', lesson: 'Pydantic v2: use model_validate() instead of parse_obj(), .dict() → .model_dump()', severity: 'major' },
|
|
1829
|
-
{ topic: 'fastapi:lifespan', lesson: 'Use lifespan context manager instead of deprecated on_event startup/shutdown', severity: 'minor' },
|
|
1830
|
-
{ topic: 'fastapi:background-tasks', lesson: 'BackgroundTasks run after response is sent — not in a separate thread pool', severity: 'major' },
|
|
1831
|
-
{ topic: 'fastapi:cors-order', lesson: 'CORSMiddleware must be added before other middleware to work correctly', severity: 'critical' },
|
|
1832
|
-
],
|
|
1833
|
-
go: [
|
|
1834
|
-
{ topic: 'go:context-cancel', lesson: 'Always call cancel() from context.WithCancel — leak goroutines if not cancelled', severity: 'critical' },
|
|
1835
|
-
{ topic: 'go:defer-in-loop', lesson: 'defer in a loop runs at function return, not loop iteration — use IIFE or explicit close', severity: 'major' },
|
|
1836
|
-
{ topic: 'go:nil-interface', lesson: 'nil interface != interface containing nil pointer — use explicit nil checks', severity: 'major' },
|
|
1837
|
-
{ topic: 'go:goroutine-leak', lesson: 'Goroutines with channel sends block forever if receiver is gone — use select with done chan', severity: 'critical' },
|
|
1838
|
-
{ topic: 'go:embed-path', lesson: '//go:embed path must be relative and known at compile time — no os.Getenv', severity: 'minor' },
|
|
1839
|
-
],
|
|
1840
|
-
docker: [
|
|
1841
|
-
{ topic: 'docker:layer-cache', lesson: 'Copy package.json before source code — Docker caches layers, npm install only reruns on dep changes', severity: 'major' },
|
|
1842
|
-
{ topic: 'docker:non-root', lesson: 'Run as non-root user (USER 1001) — some k8s clusters reject root containers by policy', severity: 'critical' },
|
|
1843
|
-
{ topic: 'docker:build-arg-secret', lesson: 'Never use ARG for secrets — visible in image history. Use --secret mount instead', severity: 'critical' },
|
|
1844
|
-
{ topic: 'docker:entrypoint-exec', lesson: 'Use exec form ["cmd","arg"] not shell form "cmd arg" — shell form ignores SIGTERM', severity: 'major' },
|
|
1845
|
-
{ topic: 'docker:multi-stage', lesson: 'Multi-stage builds: copy only built artifacts to final stage — keep image small', severity: 'minor' },
|
|
1846
|
-
],
|
|
1847
|
-
kubernetes: [
|
|
1848
|
-
{ topic: 'k8s:resource-limits', lesson: 'Always set resource limits — unbounded pods cause node evictions and OOMKill', severity: 'critical' },
|
|
1849
|
-
{ topic: 'k8s:liveness-vs-readiness', lesson: 'Liveness failures restart pod; Readiness failures remove from LB. Use different endpoints', severity: 'major' },
|
|
1850
|
-
{ topic: 'k8s:imagepullpolicy', lesson: 'imagePullPolicy: Always in production — IfNotPresent can serve stale images', severity: 'major' },
|
|
1851
|
-
{ topic: 'k8s:configmap-env', lesson: 'ConfigMap changes don\'t restart pods — use rollout restart or mount as volume', severity: 'critical' },
|
|
1852
|
-
{ topic: 'k8s:pdb', lesson: 'Set PodDisruptionBudget for stateful apps — node drains kill all pods without it', severity: 'major' },
|
|
1853
|
-
],
|
|
1854
|
-
react: [
|
|
1855
|
-
{ topic: 'react:useeffect-deps', lesson: 'Omitting dependencies from useEffect deps array causes stale closure bugs — use exhaustive-deps ESLint rule', severity: 'critical' },
|
|
1856
|
-
{ topic: 'react:key-index', lesson: 'Never use array index as key in lists — causes subtle re-render bugs on reorder/delete', severity: 'major' },
|
|
1857
|
-
{ topic: 'react:setState-in-render', lesson: 'setState() during render causes infinite loop — move to useEffect or event handler', severity: 'critical' },
|
|
1858
|
-
{ topic: 'react:memo-reference', lesson: 'Object/array literals in JSX recreate on every render — useMemo for expensive derived values', severity: 'minor' },
|
|
1859
|
-
],
|
|
1860
|
-
typescript: [
|
|
1861
|
-
{ topic: 'ts:type-guard', lesson: 'Use "x is Type" return type for type guard functions — not "boolean"', severity: 'minor' },
|
|
1862
|
-
{ topic: 'ts:strict-null', lesson: 'Enable strictNullChecks in tsconfig — catches 90% of runtime null errors at compile time', severity: 'critical' },
|
|
1863
|
-
{ topic: 'ts:enum-avoid', lesson: 'Prefer union types ("a"|"b") over enum — enums have surprising runtime behavior', severity: 'minor' },
|
|
1864
|
-
{ topic: 'ts:satisfies', lesson: 'Use "satisfies" operator to validate type without widening — more precise than explicit annotation', severity: 'minor' },
|
|
1865
|
-
],
|
|
1866
|
-
python: [
|
|
1867
|
-
{ topic: 'python:mutable-default', lesson: 'Never use mutable default arguments (def f(x=[])) — shared across all calls. Use None + guard', severity: 'critical' },
|
|
1868
|
-
{ topic: 'python:walrus-operator', lesson: ':= (walrus) assigns and returns — useful in while/comprehensions but hard to read in complex expr', severity: 'minor' },
|
|
1869
|
-
{ topic: 'python:asyncio-run', lesson: 'asyncio.run() creates new event loop — calling it inside an existing loop raises RuntimeError', severity: 'major' },
|
|
1870
|
-
{ topic: 'python:typing-optional', lesson: 'Optional[X] == Union[X, None] — in Python 3.10+ use X | None syntax', severity: 'minor' },
|
|
1871
|
-
],
|
|
1872
|
-
};
|
|
1873
|
-
const fw = framework.toLowerCase();
|
|
1874
|
-
const lessons = COMMUNITY_LESSONS[fw];
|
|
1875
|
-
if (!lessons) {
|
|
1876
|
-
const available = Object.keys(COMMUNITY_LESSONS).join(', ');
|
|
1877
|
-
return `❌ No public brain available for \`${framework}\`.\n\nAvailable: ${available}\n\nOr publish your own: \`publish_lesson(framework="${framework}", ...)\``;
|
|
1878
|
-
}
|
|
1879
|
-
const toImport = lessons.slice(0, limit);
|
|
1880
|
-
let importedCount = 0;
|
|
1881
|
-
for (const l of toImport) {
|
|
1882
|
-
const key = `cachly:lesson:best:${l.topic}`;
|
|
1883
|
-
const existing = await redis.get(key);
|
|
1884
|
-
if (!existing) {
|
|
1885
|
-
await redis.set(key, JSON.stringify({
|
|
1886
|
-
...l,
|
|
1887
|
-
what_worked: l.lesson,
|
|
1888
|
-
outcome: 'success',
|
|
1889
|
-
ts: new Date().toISOString(),
|
|
1890
|
-
recall_count: 0,
|
|
1891
|
-
source: 'public_brain',
|
|
1892
|
-
version: 2,
|
|
1893
|
-
}));
|
|
1894
|
-
importedCount++;
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
const lines = [
|
|
1898
|
-
`📥 **Public Brain imported: ${framework}**`,
|
|
1899
|
-
``,
|
|
1900
|
-
`${importedCount} new lessons added (${toImport.length - importedCount} already existed)`,
|
|
1901
|
-
``,
|
|
1902
|
-
`**Imported topics:**`,
|
|
1903
|
-
...toImport.map(l => {
|
|
1904
|
-
const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '💡';
|
|
1905
|
-
return ` ${sev} \`${l.topic}\``;
|
|
1906
|
-
}),
|
|
1907
|
-
``,
|
|
1908
|
-
`These lessons will now appear in \`session_start\` when relevant.`,
|
|
1909
|
-
`Recall any time: \`recall_best_solution(topic="${fw}:...")\``,
|
|
1910
|
-
];
|
|
1911
|
-
return lines.join('\n');
|
|
1912
|
-
}
|
|
1913
|
-
// ── setup_ai_memory ───────────────────────────────────────────────────────
|
|
1914
|
-
case 'setup_ai_memory': {
|
|
1915
|
-
const { instance_id, project_dir, embed_provider: providerArg = 'openai', project_description = 'a software project', } = args;
|
|
1916
|
-
const inst = await apiFetch(`/api/v1/instances/${instance_id}`);
|
|
1917
|
-
// Provider-specific env var instructions
|
|
1918
|
-
const providerEnvMap = {
|
|
1919
|
-
openai: { key: 'OPENAI_API_KEY', hint: 'Get at: https://platform.openai.com/api-keys' },
|
|
1920
|
-
mistral: { key: 'MISTRAL_API_KEY', hint: 'Get at: https://console.mistral.ai/api-keys' },
|
|
1921
|
-
cohere: { key: 'COHERE_API_KEY', hint: 'Get at: https://dashboard.cohere.com/api-keys' },
|
|
1922
|
-
ollama: { key: 'OLLAMA_BASE_URL', hint: 'Run: brew install ollama && ollama serve (free, local, no key needed)' },
|
|
1923
|
-
gemini: { key: 'GEMINI_API_KEY', hint: 'Get at: https://aistudio.google.com/app/apikey' },
|
|
1924
|
-
};
|
|
1925
|
-
const provInfo = providerEnvMap[providerArg] ?? providerEnvMap['openai'];
|
|
1926
|
-
const hasVector = !!inst.vector_token;
|
|
1927
|
-
// Generate .mcp.json snippet
|
|
1928
|
-
const mcpJsonSnippet = JSON.stringify({
|
|
1929
|
-
mcpServers: {
|
|
1930
|
-
cachly: {
|
|
1931
|
-
command: 'npx',
|
|
1932
|
-
args: ['-y', '@cachly-dev/mcp-server@latest'],
|
|
1933
|
-
env: {
|
|
1934
|
-
CACHLY_JWT: 'your-api-token-from-cachly.dev/settings',
|
|
1935
|
-
[provInfo.key]: providerArg === 'ollama' ? 'http://localhost:11434' : 'your-key-here',
|
|
1936
|
-
...(providerArg !== 'openai' ? { CACHLY_EMBED_PROVIDER: providerArg } : {}),
|
|
1937
|
-
},
|
|
1938
|
-
},
|
|
1939
|
-
},
|
|
1940
|
-
}, null, 2);
|
|
1941
|
-
// Generate copilot-instructions.md content
|
|
1942
|
-
const tier = inst.tier.toUpperCase();
|
|
1943
|
-
const smartRecallNote = hasVector
|
|
1944
|
-
? '- `smart_recall("natural language query")` — semantic search (finds by meaning)'
|
|
1945
|
-
: '- `recall_context("arch:*")` — exact/glob key lookup (upgrade to Speed/Business for smart_recall)';
|
|
1946
|
-
const layerNote = hasVector
|
|
1947
|
-
? `Layer 3 (Semantic): smart_recall uses pgvector HNSW on your ${tier} instance`
|
|
1948
|
-
: `Layer 3 (Autopilot): this file — upgrade to Speed/Business tier to unlock smart_recall`;
|
|
1949
|
-
const copilotInstructions = `# cachly AI Brain — ${project_description}
|
|
1950
|
-
|
|
1951
|
-
> 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.
|
|
1952
|
-
|
|
1953
|
-
---
|
|
1954
|
-
|
|
1955
|
-
## 🔴 MANDATORY RULES — Always enforced, no exceptions
|
|
1956
|
-
|
|
1957
|
-
### Rule 1 — Session Start (FIRST thing in every window)
|
|
1958
|
-
Call \`session_start\` **before reading any source files or making any changes**:
|
|
1959
|
-
|
|
1960
|
-
\`\`\`
|
|
1961
|
-
session_start(
|
|
1962
|
-
instance_id = "${instance_id}",
|
|
1963
|
-
focus = "<what you plan to work on today>",
|
|
1964
|
-
)
|
|
1965
|
-
\`\`\`
|
|
1966
|
-
|
|
1967
|
-
Returns: last session summary, handoff from previous window, recent lessons, open failures, brain health.
|
|
1968
|
-
|
|
1969
|
-
**If \`session_start\` is unavailable**, fall back:
|
|
1970
|
-
1. \`smart_recall("recent lessons <topic>")\`
|
|
1971
|
-
2. \`recall_best_solution("<topic>")\`
|
|
1972
|
-
|
|
1973
|
-
### Rule 2 — Learn After Every Fix/Deploy/Discovery (AUTOMATIC — no prompting needed)
|
|
1974
|
-
After **every** bug fix, deployment, failed attempt, or discovery — call \`learn_from_attempts\` immediately:
|
|
1975
|
-
|
|
1976
|
-
\`\`\`
|
|
1977
|
-
learn_from_attempts(
|
|
1978
|
-
instance_id = "${instance_id}",
|
|
1979
|
-
topic = "category:keyword", # deploy:web | fix:auth | debug:db | infra:docker
|
|
1980
|
-
outcome = "success" | "failure" | "partial",
|
|
1981
|
-
what_worked = "exact approach or command that worked",
|
|
1982
|
-
what_failed = "what did NOT work (prevents future wasted time)",
|
|
1983
|
-
severity = "critical" | "major" | "minor",
|
|
1984
|
-
commands = ["exact command that worked"],
|
|
1985
|
-
file_paths = ["path/to/changed/file"],
|
|
1986
|
-
)
|
|
1987
|
-
\`\`\`
|
|
1988
|
-
|
|
1989
|
-
**These events MUST automatically trigger \`learn_from_attempts\`:**
|
|
1990
|
-
- ✅ Successfully deployed → \`topic="deploy:<component>", outcome="success"\`
|
|
1991
|
-
- ✅ Fixed a bug → \`topic="fix:<description>", outcome="success"\`
|
|
1992
|
-
- ✅ Discovered root cause → \`topic="debug:<area>", outcome="partial"\`
|
|
1993
|
-
- ✅ Tried something that failed → \`topic="<area>", outcome="failure", what_failed="..."\`
|
|
1994
|
-
- ✅ Found a config quirk → \`topic="infra:<topic>", outcome="success"\`
|
|
1995
|
-
- ✅ Ran a working command → store it in \`commands=[...]\`
|
|
1996
|
-
|
|
1997
|
-
### Rule 3 — Session Handoff (when closing window or task list > 3 items)
|
|
1998
|
-
\`\`\`
|
|
1999
|
-
session_handoff(
|
|
2000
|
-
instance_id = "${instance_id}",
|
|
2001
|
-
completed_tasks = ["what was done"],
|
|
2002
|
-
remaining_tasks = ["what is still to do"],
|
|
2003
|
-
files_changed = [{"path": "src/...", "status": "modified", "description": "what changed"}],
|
|
2004
|
-
instructions = "IMPORTANT: what the next window MUST know to not break things",
|
|
2005
|
-
context_summary = "current state of the system",
|
|
2006
|
-
)
|
|
2007
|
-
\`\`\`
|
|
2008
|
-
|
|
2009
|
-
The next \`session_start\` automatically includes this handoff. The new window sees exactly what's done, what remains, and what NOT to break.
|
|
2010
|
-
|
|
2011
|
-
### Rule 4 — Session End (LAST thing before closing)
|
|
2012
|
-
\`\`\`
|
|
2013
|
-
session_end(
|
|
2014
|
-
instance_id = "${instance_id}",
|
|
2015
|
-
summary = "What was accomplished (2-3 sentences with specifics)",
|
|
2016
|
-
files_changed = ["list", "of", "changed", "files"],
|
|
2017
|
-
lessons_learned = <number of learn_from_attempts calls made>,
|
|
2018
|
-
)
|
|
2019
|
-
\`\`\`
|
|
2020
|
-
|
|
2021
|
-
> \`session_end\` **automatically extracts and stores lessons** from the summary text — no manual calls needed.
|
|
2022
|
-
|
|
2023
|
-
### Rule 5 — Code Index Invalidation (AUTOMATIC)
|
|
2024
|
-
- \`index_project\` tracks MD5 hashes per file — **skips unchanged files automatically**
|
|
2025
|
-
- After code changes: next \`index_project\` run re-indexes only modified files
|
|
2026
|
-
- After delete/rename: run \`index_project\` again to sync
|
|
2027
|
-
- No manual invalidation needed
|
|
2028
|
-
|
|
2029
|
-
---
|
|
2030
|
-
|
|
2031
|
-
## Task-type trigger table
|
|
2032
|
-
|
|
2033
|
-
| You're about to... | Call BEFORE | Call AFTER |
|
|
2034
|
-
|---|---|---|
|
|
2035
|
-
| Deploy anything | \`recall_best_solution("deploy:<component>")\` | \`learn_from_attempts(topic="deploy:...")\` |
|
|
2036
|
-
| Fix a bug | \`recall_best_solution("fix:<area>")\` | \`learn_from_attempts(topic="fix:...")\` |
|
|
2037
|
-
| Add a feature | \`session_start(focus="feat:<area>")\` | \`learn_from_attempts(topic="feat:...")\` |
|
|
2038
|
-
| Infra/server work | \`recall_best_solution("infra:<topic>")\` | \`learn_from_attempts(topic="infra:...")\` |
|
|
2039
|
-
| Debug an issue | \`smart_recall("<error message or symptom>")\` | \`learn_from_attempts(topic="debug:...")\` |
|
|
2040
|
-
|
|
2041
|
-
---
|
|
2042
|
-
|
|
2043
|
-
## Available Brain Tools
|
|
2044
|
-
|
|
2045
|
-
| Tool | When to use |
|
|
2046
|
-
|------|-------------|
|
|
2047
|
-
| \`session_start\` | **FIRST** — mandatory at start of every session |
|
|
2048
|
-
| \`session_end\` | **LAST** — mandatory at end, auto-learns from summary |
|
|
2049
|
-
| \`session_handoff\` | When closing window with remaining tasks |
|
|
2050
|
-
| \`learn_from_attempts\` | **AUTOMATIC** after every fix/deploy/discovery |
|
|
2051
|
-
| \`recall_best_solution\` | Before any non-trivial task |
|
|
2052
|
-
| \`remember_context\` | After analyzing code — save findings for future sessions |
|
|
2053
|
-
${smartRecallNote ? `| \`smart_recall\` | Search brain by meaning/keywords |\n` : ''}\
|
|
2054
|
-
| \`recall_context\` | Get exact key (supports glob: \`arch:*\`, \`file:*\`) |
|
|
2055
|
-
| \`brain_search\` | BM25+ full-text search over all brain data |
|
|
2056
|
-
| \`auto_learn_session\` | Batch-learn from a list of observations (optional) |
|
|
2057
|
-
| \`index_project\` | Index source files (smart hash, skips unchanged files) |
|
|
2058
|
-
| \`list_remembered\` | See what's cached in the brain |
|
|
2059
|
-
| \`forget_context\` | Remove stale context |
|
|
2060
|
-
|
|
2061
|
-
---
|
|
2062
|
-
|
|
2063
|
-
## Instance Details
|
|
2064
|
-
|
|
2065
|
-
- **Instance ID:** \`${instance_id}\`
|
|
2066
|
-
- **Instance name:** ${inst.name}
|
|
2067
|
-
- **Tier:** ${tier}
|
|
2068
|
-
- **${layerNote}**
|
|
2069
|
-
- **Embedding provider:** ${providerArg}
|
|
2070
|
-
|
|
2071
|
-
---
|
|
2072
|
-
|
|
2073
|
-
## How the 3-layer system works
|
|
2074
|
-
|
|
2075
|
-
\`\`\`
|
|
2076
|
-
Layer 1 — Storage: Your cachly Valkey instance (${inst.name}) — persists forever
|
|
2077
|
-
Layer 2 — Tools: learn_from_attempts · recall_best_solution · brain_search · session_start/end
|
|
2078
|
-
Layer 3 — Autopilot: This file — AI reads it and runs tools automatically every session
|
|
2079
|
-
\`\`\`
|
|
2080
|
-
|
|
2081
|
-
Result: Your AI **never solves the same problem twice** and always picks up exactly where it left off. 🚀
|
|
2082
|
-
`;
|
|
2083
|
-
const lines = [
|
|
2084
|
-
`🧠 **cachly AI Memory Setup Complete**`,
|
|
2085
|
-
``,
|
|
2086
|
-
`**Instance:** ${inst.name} (${tier}) · ID: \`${instance_id}\``,
|
|
2087
|
-
`**Embedding Provider:** ${providerArg}`,
|
|
2088
|
-
`**Semantic Search:** ${hasVector ? '✅ pgvector HNSW available' : '⚠️ Not available on ' + tier + ' — upgrade to Speed/Business'}`,
|
|
2089
|
-
``,
|
|
2090
|
-
`─────────────────────────────────────────────`,
|
|
2091
|
-
`**Step 1 — Add to .mcp.json:**`,
|
|
2092
|
-
`\`\`\`json`,
|
|
2093
|
-
mcpJsonSnippet,
|
|
2094
|
-
`\`\`\``,
|
|
2095
|
-
``,
|
|
2096
|
-
`**Step 2 — Set your ${providerArg} key:**`,
|
|
2097
|
-
`\`\`\`bash`,
|
|
2098
|
-
`export ${provInfo.key}="your-key-here"`,
|
|
2099
|
-
`\`\`\``,
|
|
2100
|
-
`_(${provInfo.hint})_`,
|
|
2101
|
-
``,
|
|
2102
|
-
`─────────────────────────────────────────────`,
|
|
2103
|
-
`**Step 3 — copilot-instructions.md (Layer 3 Autopilot):**`,
|
|
2104
|
-
``,
|
|
2105
|
-
...(project_dir
|
|
2106
|
-
? [`🔍 Detecting IDEs in \`${project_dir}\`…`]
|
|
2107
|
-
: [`Copy this to \`.github/copilot-instructions.md\` (Copilot), \`CLAUDE.md\` (Claude Code), or \`.cursor/rules\` (Cursor) in your project:`]),
|
|
2108
|
-
``,
|
|
2109
|
-
`\`\`\`markdown`,
|
|
2110
|
-
copilotInstructions,
|
|
2111
|
-
`\`\`\``,
|
|
2112
|
-
``,
|
|
2113
|
-
`─────────────────────────────────────────────`,
|
|
2114
|
-
`**How the 3 layers work together:**`,
|
|
2115
|
-
` Layer 1 → Your Valkey instance stores all lessons + context (persists forever)`,
|
|
2116
|
-
` Layer 2 → MCP tools (learn_from_attempts, recall_best_solution, smart_recall) read/write it`,
|
|
2117
|
-
` Layer 3 → copilot-instructions.md makes your AI run them automatically`,
|
|
2118
|
-
``,
|
|
2119
|
-
`Result: Your AI never solves the same problem twice. 🚀`,
|
|
2120
|
-
];
|
|
2121
|
-
// ── IDE auto-detection + file writing ────────────────────────────────
|
|
2122
|
-
if (project_dir) {
|
|
2123
|
-
const { mkdir, writeFile, access } = await import('node:fs/promises');
|
|
2124
|
-
const { constants } = await import('node:fs');
|
|
2125
|
-
const exists = async (p) => access(p, constants.F_OK).then(() => true).catch(() => false);
|
|
2126
|
-
const targets = [];
|
|
2127
|
-
let stopHookWritten = false;
|
|
2128
|
-
// GitHub Copilot — always write (universal fallback)
|
|
2129
|
-
targets.push({
|
|
2130
|
-
ide: 'GitHub Copilot',
|
|
2131
|
-
path: join(project_dir, '.github', 'copilot-instructions.md'),
|
|
2132
|
-
content: copilotInstructions,
|
|
2133
|
-
});
|
|
2134
|
-
// Claude Code — CLAUDE.md in project root
|
|
2135
|
-
if (await exists(join(project_dir, 'CLAUDE.md')) || await exists(join(project_dir, '.claude'))) {
|
|
2136
|
-
targets.push({
|
|
2137
|
-
ide: 'Claude Code',
|
|
2138
|
-
path: join(project_dir, 'CLAUDE.md'),
|
|
2139
|
-
content: copilotInstructions,
|
|
2140
|
-
});
|
|
2141
|
-
// Claude Code Stop-Hook — auto-saves checkpoint when Claude stops responding
|
|
2142
|
-
const claudeDir = join(project_dir, '.claude');
|
|
2143
|
-
await mkdir(claudeDir, { recursive: true });
|
|
2144
|
-
const stopHook = {
|
|
2145
|
-
hooks: {
|
|
2146
|
-
Stop: [
|
|
2147
|
-
{
|
|
2148
|
-
matcher: '',
|
|
2149
|
-
hooks: [
|
|
2150
|
-
{
|
|
2151
|
-
type: 'command',
|
|
2152
|
-
command: `npx --yes @cachly-dev/mcp-server@latest checkpoint --instance-id ${instance_id}`,
|
|
2153
|
-
},
|
|
2154
|
-
],
|
|
2155
|
-
},
|
|
2156
|
-
],
|
|
2157
|
-
},
|
|
2158
|
-
};
|
|
2159
|
-
const settingsPath = join(claudeDir, 'settings.json');
|
|
2160
|
-
let existingSettings = {};
|
|
2161
|
-
try {
|
|
2162
|
-
const { readFile: rf } = await import('node:fs/promises');
|
|
2163
|
-
existingSettings = JSON.parse(await rf(settingsPath, 'utf-8'));
|
|
2164
|
-
}
|
|
2165
|
-
catch { /* new file */ }
|
|
2166
|
-
const merged = { ...existingSettings, hooks: stopHook.hooks };
|
|
2167
|
-
await writeFile(settingsPath, JSON.stringify(merged, null, 2), 'utf-8');
|
|
2168
|
-
stopHookWritten = true;
|
|
2169
|
-
}
|
|
2170
|
-
// Cursor — .cursor/rules (new format) or .cursorrules (legacy)
|
|
2171
|
-
if (await exists(join(project_dir, '.cursor')) ||
|
|
2172
|
-
await exists(join(project_dir, '.cursorrules'))) {
|
|
2173
|
-
const cursorDir = join(project_dir, '.cursor');
|
|
2174
|
-
await mkdir(cursorDir, { recursive: true });
|
|
2175
|
-
targets.push({
|
|
2176
|
-
ide: 'Cursor',
|
|
2177
|
-
path: join(cursorDir, 'rules'),
|
|
2178
|
-
content: copilotInstructions,
|
|
2179
|
-
});
|
|
2180
|
-
}
|
|
2181
|
-
// Windsurf — .windsurfrules
|
|
2182
|
-
if (await exists(join(project_dir, '.windsurfrules')) ||
|
|
2183
|
-
await exists(join(project_dir, '.windsurf'))) {
|
|
2184
|
-
targets.push({
|
|
2185
|
-
ide: 'Windsurf',
|
|
2186
|
-
path: join(project_dir, '.windsurfrules'),
|
|
2187
|
-
content: copilotInstructions,
|
|
2188
|
-
});
|
|
2189
|
-
}
|
|
2190
|
-
// VS Code (Copilot) — already covered by .github/copilot-instructions.md above
|
|
2191
|
-
// Continue.dev — .continue/config.json is JSON, not markdown — skip, copilot-instructions handles it
|
|
2192
|
-
const written = [];
|
|
2193
|
-
for (const target of targets) {
|
|
2194
|
-
const dir = target.path.substring(0, target.path.lastIndexOf('/'));
|
|
2195
|
-
await mkdir(dir, { recursive: true });
|
|
2196
|
-
await writeFile(target.path, target.content, 'utf-8');
|
|
2197
|
-
written.push(`✅ [${target.ide}] → \`${target.path.replace(project_dir, '.')}\``);
|
|
2198
|
-
}
|
|
2199
|
-
if (stopHookWritten) {
|
|
2200
|
-
written.push(`✅ [Claude Code Stop-Hook] → \`.claude/settings.json\` (auto-checkpoint on stop)`);
|
|
2201
|
-
}
|
|
2202
|
-
lines.push(...written);
|
|
2203
|
-
}
|
|
2204
|
-
return lines.join('\n');
|
|
2205
|
-
}
|
|
2206
|
-
default:
|
|
2207
|
-
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
async function handleBulkLockStream(name, args) {
|
|
2211
|
-
const instance_id = args.instance_id;
|
|
2212
|
-
switch (name) {
|
|
2213
|
-
// ── cache_mset ────────────────────────────────────────────────────────
|
|
2214
|
-
case 'cache_mset': {
|
|
2215
|
-
const items = args.items;
|
|
2216
|
-
if (!Array.isArray(items) || items.length === 0)
|
|
2217
|
-
return '⚠️ No items provided.';
|
|
2218
|
-
const redis = await getConnection(instance_id);
|
|
2219
|
-
const pipe = redis.pipeline();
|
|
2220
|
-
for (const item of items) {
|
|
2221
|
-
const serialized = typeof item.value === 'string' ? item.value : JSON.stringify(item.value);
|
|
2222
|
-
if (item.ttl && item.ttl > 0) {
|
|
2223
|
-
pipe.set(item.key, serialized, 'EX', item.ttl);
|
|
2224
|
-
}
|
|
2225
|
-
else {
|
|
2226
|
-
pipe.set(item.key, serialized);
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
await pipe.exec();
|
|
2230
|
-
return `✅ **cache_mset** – ${items.length} key(s) written in one pipeline round-trip.\n` +
|
|
2231
|
-
items.map(i => ` • \`${i.key}\`${i.ttl ? ` (TTL ${i.ttl}s)` : ''}`).join('\n');
|
|
2232
|
-
}
|
|
2233
|
-
// ── cache_mget ────────────────────────────────────────────────────────
|
|
2234
|
-
case 'cache_mget': {
|
|
2235
|
-
const keys = args.keys;
|
|
2236
|
-
if (!Array.isArray(keys) || keys.length === 0)
|
|
2237
|
-
return '⚠️ No keys provided.';
|
|
2238
|
-
const redis = await getConnection(instance_id);
|
|
2239
|
-
const raws = await redis.mget(...keys);
|
|
2240
|
-
const result = keys.map((k, i) => {
|
|
2241
|
-
const raw = raws[i];
|
|
2242
|
-
if (raw === null)
|
|
2243
|
-
return ` • \`${k}\`: _null (miss)_`;
|
|
2244
|
-
try {
|
|
2245
|
-
return ` • \`${k}\`: ${raw}`;
|
|
2246
|
-
}
|
|
2247
|
-
catch {
|
|
2248
|
-
return ` • \`${k}\`: ${raw}`;
|
|
2249
|
-
}
|
|
2250
|
-
});
|
|
2251
|
-
return `✅ **cache_mget** – ${keys.length} key(s) fetched in one round-trip.\n` + result.join('\n');
|
|
2252
|
-
}
|
|
2253
|
-
// ── cache_lock_acquire ────────────────────────────────────────────────
|
|
2254
|
-
case 'cache_lock_acquire': {
|
|
2255
|
-
const key = args.key;
|
|
2256
|
-
const ttlMs = Number(args.ttl_ms ?? 5000);
|
|
2257
|
-
const retries = Number(args.retries ?? 3);
|
|
2258
|
-
const retryDelayMs = Number(args.retry_delay_ms ?? 50);
|
|
2259
|
-
const redis = await getConnection(instance_id);
|
|
2260
|
-
const lockKey = `cachly:lock:${key}`;
|
|
2261
|
-
const token = randomUUID();
|
|
2262
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
2263
|
-
const result = await redis.set(lockKey, token, 'PX', ttlMs, 'NX');
|
|
2264
|
-
if (result === 'OK') {
|
|
2265
|
-
return (`🔒 **cache_lock_acquire** – Lock acquired!\n\n` +
|
|
2266
|
-
` Key: \`${key}\`\n` +
|
|
2267
|
-
` Token: \`${token}\`\n` +
|
|
2268
|
-
` TTL: ${ttlMs} ms\n\n` +
|
|
2269
|
-
`💡 Use **cache_lock_release** with this token to release early.`);
|
|
2270
|
-
}
|
|
2271
|
-
if (attempt < retries)
|
|
2272
|
-
await new Promise(r => setTimeout(r, retryDelayMs));
|
|
2273
|
-
}
|
|
2274
|
-
return `❌ **cache_lock_acquire** – Could not acquire lock for \`${key}\` after ${retries + 1} attempts.`;
|
|
2275
|
-
}
|
|
2276
|
-
// ── cache_lock_release ────────────────────────────────────────────────
|
|
2277
|
-
case 'cache_lock_release': {
|
|
2278
|
-
const key = args.key;
|
|
2279
|
-
const token = args.token;
|
|
2280
|
-
const redis = await getConnection(instance_id);
|
|
2281
|
-
const script = `
|
|
2282
|
-
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
2283
|
-
return redis.call("del", KEYS[1])
|
|
2284
|
-
else
|
|
2285
|
-
return 0
|
|
2286
|
-
end`;
|
|
2287
|
-
const released = await redis.eval(script, 1, `cachly:lock:${key}`, token);
|
|
2288
|
-
return released === 1
|
|
2289
|
-
? `🔓 **cache_lock_release** – Lock \`${key}\` released successfully.`
|
|
2290
|
-
: `⚠️ **cache_lock_release** – Lock \`${key}\` was already expired or token mismatch.`;
|
|
2291
|
-
}
|
|
2292
|
-
// ── cache_stream_set ──────────────────────────────────────────────────
|
|
2293
|
-
case 'cache_stream_set': {
|
|
2294
|
-
const key = args.key;
|
|
2295
|
-
const chunks = args.chunks;
|
|
2296
|
-
const ttl = args.ttl ? Number(args.ttl) : null;
|
|
2297
|
-
if (!Array.isArray(chunks) || chunks.length === 0)
|
|
2298
|
-
return '⚠️ No chunks provided.';
|
|
2299
|
-
const redis = await getConnection(instance_id);
|
|
2300
|
-
const listKey = `cachly:stream:${key}`;
|
|
2301
|
-
await redis.del(listKey);
|
|
2302
|
-
const pipe = redis.pipeline();
|
|
2303
|
-
for (const chunk of chunks)
|
|
2304
|
-
pipe.rpush(listKey, chunk);
|
|
2305
|
-
if (ttl && ttl > 0)
|
|
2306
|
-
pipe.expire(listKey, ttl);
|
|
2307
|
-
await pipe.exec();
|
|
2308
|
-
return (`✅ **cache_stream_set** – ${chunks.length} chunk(s) stored.\n` +
|
|
2309
|
-
` Key: \`${key}\`\n` +
|
|
2310
|
-
(ttl ? ` TTL: ${ttl}s\n` : '') +
|
|
2311
|
-
` Total size: ${chunks.reduce((a, c) => a + c.length, 0)} chars`);
|
|
2312
|
-
}
|
|
2313
|
-
// ── cache_stream_get ──────────────────────────────────────────────────
|
|
2314
|
-
case 'cache_stream_get': {
|
|
2315
|
-
const key = args.key;
|
|
2316
|
-
const redis = await getConnection(instance_id);
|
|
2317
|
-
const listKey = `cachly:stream:${key}`;
|
|
2318
|
-
const len = await redis.llen(listKey);
|
|
2319
|
-
if (len === 0)
|
|
2320
|
-
return `⚠️ **cache_stream_get** – Cache miss for key \`${key}\`.`;
|
|
2321
|
-
const chunks = await redis.lrange(listKey, 0, -1);
|
|
2322
|
-
const preview = chunks.join('').slice(0, 500);
|
|
2323
|
-
return (`✅ **cache_stream_get** – ${len} chunk(s) retrieved for \`${key}\`.\n\n` +
|
|
2324
|
-
`**Preview** (first 500 chars):\n\`\`\`\n${preview}${preview.length < chunks.join('').length ? '…' : ''}\n\`\`\``);
|
|
2325
|
-
}
|
|
2326
|
-
// ── Roadmap ──────────────────────────────────────────────────────────────
|
|
2327
|
-
case 'roadmap_add': {
|
|
2328
|
-
const { instance_id: rid, title, description: desc = '', priority = 'medium', tags: rtags = [], milestone = '', } = args;
|
|
2329
|
-
const redis = await getConnection(rid);
|
|
2330
|
-
const id = `rm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
2331
|
-
const item = {
|
|
2332
|
-
id, title, description: desc, priority, tags: rtags, milestone,
|
|
2333
|
-
status: 'planned',
|
|
2334
|
-
created: new Date().toISOString(),
|
|
2335
|
-
updated: new Date().toISOString(),
|
|
2336
|
-
notes: '',
|
|
2337
|
-
};
|
|
2338
|
-
await redis.hset(`cachly:roadmap:${rid}`, id, JSON.stringify(item));
|
|
2339
|
-
const PRIORITY_ICON = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
|
|
2340
|
-
return [
|
|
2341
|
-
`📋 **Roadmap item added**`,
|
|
2342
|
-
``,
|
|
2343
|
-
` ID: \`${id}\``,
|
|
2344
|
-
` Title: ${title}`,
|
|
2345
|
-
` Priority: ${PRIORITY_ICON[priority] ?? '⚪'} ${priority}`,
|
|
2346
|
-
` Status: planned`,
|
|
2347
|
-
milestone ? ` Milestone: ${milestone}` : '',
|
|
2348
|
-
rtags.length ? ` Tags: ${rtags.join(', ')}` : '',
|
|
2349
|
-
``,
|
|
2350
|
-
`💡 Use \`roadmap_update(id: "${id}", status: "in-progress")\` when you start working on it.`,
|
|
2351
|
-
].filter(Boolean).join('\n');
|
|
2352
|
-
}
|
|
2353
|
-
case 'roadmap_update': {
|
|
2354
|
-
const { instance_id: rid, id: itemId, status: newStatus, priority: newPriority, notes: newNotes, title: newTitle, description: newDesc, } = args;
|
|
2355
|
-
const redis = await getConnection(rid);
|
|
2356
|
-
const raw = await redis.hget(`cachly:roadmap:${rid}`, itemId);
|
|
2357
|
-
if (!raw)
|
|
2358
|
-
return `⚠️ **roadmap_update** – Item \`${itemId}\` not found. Use \`roadmap_list\` to see all items.`;
|
|
2359
|
-
const item = JSON.parse(raw);
|
|
2360
|
-
const oldStatus = item.status;
|
|
2361
|
-
if (newStatus)
|
|
2362
|
-
item.status = newStatus;
|
|
2363
|
-
if (newPriority)
|
|
2364
|
-
item.priority = newPriority;
|
|
2365
|
-
if (newTitle)
|
|
2366
|
-
item.title = newTitle;
|
|
2367
|
-
if (newDesc)
|
|
2368
|
-
item.description = newDesc;
|
|
2369
|
-
if (newNotes)
|
|
2370
|
-
item.notes = item.notes ? `${item.notes}\n[${new Date().toISOString().slice(0, 10)}] ${newNotes}` : `[${new Date().toISOString().slice(0, 10)}] ${newNotes}`;
|
|
2371
|
-
item.updated = new Date().toISOString();
|
|
2372
|
-
await redis.hset(`cachly:roadmap:${rid}`, itemId, JSON.stringify(item));
|
|
2373
|
-
const statusEmoji = { planned: '📋', 'in-progress': '⚡', done: '✅', blocked: '🚫', cancelled: '🗑️' };
|
|
2374
|
-
return [
|
|
2375
|
-
`${statusEmoji[newStatus ?? oldStatus] ?? '📋'} **Roadmap updated** \`${itemId}\``,
|
|
2376
|
-
``,
|
|
2377
|
-
` Title: ${item.title}`,
|
|
2378
|
-
oldStatus !== newStatus ? ` Status: ${oldStatus} → ${newStatus}` : ` Status: ${item.status}`,
|
|
2379
|
-
newNotes ? ` Notes: ${newNotes}` : '',
|
|
2380
|
-
].filter(Boolean).join('\n');
|
|
2381
|
-
}
|
|
2382
|
-
case 'roadmap_list': {
|
|
2383
|
-
const { instance_id: rid, status: filterStatus = 'open', tag: filterTag, milestone: filterMilestone, priority: filterPriority, } = args;
|
|
2384
|
-
const redis = await getConnection(rid);
|
|
2385
|
-
const all = await redis.hgetall(`cachly:roadmap:${rid}`);
|
|
2386
|
-
if (!all || Object.keys(all).length === 0) {
|
|
2387
|
-
return '📋 **Roadmap is empty.**\n\nUse `roadmap_add` to create your first item.';
|
|
2388
|
-
}
|
|
2389
|
-
const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
2390
|
-
const PRIORITY_ICON = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
|
|
2391
|
-
const STATUS_ICON = { planned: '📋', 'in-progress': '⚡', done: '✅', blocked: '🚫', cancelled: '🗑️' };
|
|
2392
|
-
const openStatuses = new Set(['planned', 'in-progress', 'blocked']);
|
|
2393
|
-
let items = Object.values(all).map(v => JSON.parse(v));
|
|
2394
|
-
// Filter
|
|
2395
|
-
if (filterStatus === 'open')
|
|
2396
|
-
items = items.filter(i => openStatuses.has(i.status));
|
|
2397
|
-
else if (filterStatus)
|
|
2398
|
-
items = items.filter(i => i.status === filterStatus);
|
|
2399
|
-
if (filterTag)
|
|
2400
|
-
items = items.filter(i => i.tags.includes(filterTag));
|
|
2401
|
-
if (filterMilestone)
|
|
2402
|
-
items = items.filter(i => i.milestone === filterMilestone);
|
|
2403
|
-
if (filterPriority)
|
|
2404
|
-
items = items.filter(i => (PRIORITY_ORDER[i.priority] ?? 99) <= (PRIORITY_ORDER[filterPriority] ?? 99));
|
|
2405
|
-
// Sort: priority asc, then created asc
|
|
2406
|
-
items.sort((a, b) => {
|
|
2407
|
-
const pa = PRIORITY_ORDER[a.priority] ?? 99;
|
|
2408
|
-
const pb = PRIORITY_ORDER[b.priority] ?? 99;
|
|
2409
|
-
return pa !== pb ? pa - pb : a.created.localeCompare(b.created);
|
|
2410
|
-
});
|
|
2411
|
-
if (items.length === 0)
|
|
2412
|
-
return `📋 **No roadmap items** match the current filter (status: ${filterStatus}).`;
|
|
2413
|
-
const grouped = {};
|
|
2414
|
-
for (const it of items) {
|
|
2415
|
-
const st = it.status;
|
|
2416
|
-
if (!grouped[st])
|
|
2417
|
-
grouped[st] = [];
|
|
2418
|
-
grouped[st].push(it);
|
|
2419
|
-
}
|
|
2420
|
-
const lines = [`📋 **Roadmap** (${items.length} item${items.length !== 1 ? 's' : ''})`, ''];
|
|
2421
|
-
for (const [st, grp] of Object.entries(grouped)) {
|
|
2422
|
-
lines.push(`**${STATUS_ICON[st] ?? '•'} ${st.toUpperCase()}** (${grp.length})`);
|
|
2423
|
-
for (const it of grp) {
|
|
2424
|
-
const tags = it.tags.length ? ` [${it.tags.join(', ')}]` : '';
|
|
2425
|
-
const milestone = it.milestone ? ` · ${it.milestone}` : '';
|
|
2426
|
-
lines.push(` ${PRIORITY_ICON[it.priority] ?? '⚪'} \`${it.id}\` **${it.title}**${tags}${milestone}`);
|
|
2427
|
-
if (it.description)
|
|
2428
|
-
lines.push(` ${it.description.slice(0, 120)}`);
|
|
2429
|
-
if (it.notes)
|
|
2430
|
-
lines.push(` 📝 ${it.notes.split('\n').pop()?.slice(0, 100)}`);
|
|
2431
|
-
}
|
|
2432
|
-
lines.push('');
|
|
2433
|
-
}
|
|
2434
|
-
lines.push(`💡 \`roadmap_update(id, status: "in-progress")\` to start · \`roadmap_next\` for top priority item`);
|
|
2435
|
-
return lines.join('\n');
|
|
2436
|
-
}
|
|
2437
|
-
case 'roadmap_next': {
|
|
2438
|
-
const { instance_id: rid, tag: filterTag } = args;
|
|
2439
|
-
const redis = await getConnection(rid);
|
|
2440
|
-
const all = await redis.hgetall(`cachly:roadmap:${rid}`);
|
|
2441
|
-
if (!all || Object.keys(all).length === 0) {
|
|
2442
|
-
return '📋 **Roadmap is empty.** Use `roadmap_add` to plan your first task.';
|
|
2443
|
-
}
|
|
2444
|
-
const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
2445
|
-
const PRIORITY_ICON = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' };
|
|
2446
|
-
let items = Object.values(all)
|
|
2447
|
-
.map(v => JSON.parse(v))
|
|
2448
|
-
.filter(i => i.status === 'in-progress' || i.status === 'planned')
|
|
2449
|
-
.filter(i => !filterTag || i.tags.includes(filterTag));
|
|
2450
|
-
if (items.length === 0)
|
|
2451
|
-
return '🎉 **No open roadmap items!** All tasks are done (or use `roadmap_list` to check).';
|
|
2452
|
-
// in-progress first, then by priority
|
|
2453
|
-
items.sort((a, b) => {
|
|
2454
|
-
if (a.status === 'in-progress' && b.status !== 'in-progress')
|
|
2455
|
-
return -1;
|
|
2456
|
-
if (b.status === 'in-progress' && a.status !== 'in-progress')
|
|
2457
|
-
return 1;
|
|
2458
|
-
return (PRIORITY_ORDER[a.priority] ?? 99) - (PRIORITY_ORDER[b.priority] ?? 99);
|
|
2459
|
-
});
|
|
2460
|
-
const next = items[0];
|
|
2461
|
-
const remaining = items.length - 1;
|
|
2462
|
-
const tags = next.tags.length ? `\nTags: ${next.tags.join(', ')}` : '';
|
|
2463
|
-
const milestone = next.milestone ? `\nMilestone: ${next.milestone}` : '';
|
|
2464
|
-
const notes = next.notes ? `\nNotes: ${next.notes.split('\n').pop()?.slice(0, 120)}` : '';
|
|
2465
|
-
return [
|
|
2466
|
-
`${next.status === 'in-progress' ? '⚡' : '📋'} **Next up: ${next.title}**`,
|
|
2467
|
-
``,
|
|
2468
|
-
`ID: \`${next.id}\``,
|
|
2469
|
-
`Priority: ${PRIORITY_ICON[next.priority] ?? '⚪'} ${next.priority}`,
|
|
2470
|
-
`Status: ${next.status}`,
|
|
2471
|
-
next.description ? `\nWhat to do:\n${next.description}` : '',
|
|
2472
|
-
tags, milestone, notes,
|
|
2473
|
-
``,
|
|
2474
|
-
remaining > 0 ? `(+${remaining} more open item${remaining !== 1 ? 's' : ''} in roadmap)` : '(last open item)',
|
|
2475
|
-
``,
|
|
2476
|
-
next.status === 'planned'
|
|
2477
|
-
? `💡 Start with: \`roadmap_update(id: "${next.id}", status: "in-progress")\``
|
|
2478
|
-
: `💡 Finish with: \`roadmap_update(id: "${next.id}", status: "done", notes: "...")\``,
|
|
2479
|
-
].filter(s => s !== undefined).join('\n');
|
|
2480
|
-
}
|
|
2481
|
-
// ── v0.6 Cognitive Cache: memory_consolidate ─────────────────────────────
|
|
2482
|
-
case 'memory_consolidate': {
|
|
2483
|
-
const { instance_id, dry_run = false, stale_days = 90 } = args;
|
|
2484
|
-
const redis = await getConnection(instance_id);
|
|
2485
|
-
const now = Date.now();
|
|
2486
|
-
const staleMs = stale_days * 86400 * 1000;
|
|
2487
|
-
// Scan all lessons
|
|
2488
|
-
let cursor = 0;
|
|
2489
|
-
const lessonKeys = [];
|
|
2490
|
-
do {
|
|
2491
|
-
const [next, keys] = await redis.scan(cursor, 'MATCH', 'cachly:lesson:best:*', 'COUNT', 200);
|
|
2492
|
-
cursor = parseInt(next);
|
|
2493
|
-
lessonKeys.push(...keys);
|
|
2494
|
-
} while (cursor !== 0);
|
|
2495
|
-
if (lessonKeys.length === 0) {
|
|
2496
|
-
return '🧠 **Brain is empty** — no lessons to consolidate yet. Use `learn_from_attempts` after your next bug fix.';
|
|
2497
|
-
}
|
|
2498
|
-
const lessons = new Map();
|
|
2499
|
-
for (const k of lessonKeys) {
|
|
2500
|
-
const raw = await redis.get(k);
|
|
2501
|
-
if (!raw)
|
|
2502
|
-
continue;
|
|
2503
|
-
try {
|
|
2504
|
-
lessons.set(k, JSON.parse(raw));
|
|
2505
|
-
}
|
|
2506
|
-
catch { /* skip malformed */ }
|
|
2507
|
-
}
|
|
2508
|
-
// Detect duplicates: same topic prefix (e.g. deploy:api vs deploy:api-v2)
|
|
2509
|
-
const duplicates = [];
|
|
2510
|
-
const topicGroups = new Map();
|
|
2511
|
-
for (const [k, l] of lessons) {
|
|
2512
|
-
const prefix = l.topic.split(':')[0];
|
|
2513
|
-
const group = topicGroups.get(prefix) ?? [];
|
|
2514
|
-
group.push(k);
|
|
2515
|
-
topicGroups.set(prefix, group);
|
|
2516
|
-
}
|
|
2517
|
-
// Detect contradictions: same exact topic, different outcomes
|
|
2518
|
-
const contradictions = [];
|
|
2519
|
-
const exactTopics = new Map();
|
|
2520
|
-
for (const [k, l] of lessons) {
|
|
2521
|
-
const arr = exactTopics.get(l.topic) ?? [];
|
|
2522
|
-
arr.push(k);
|
|
2523
|
-
exactTopics.set(l.topic, arr);
|
|
2524
|
-
}
|
|
2525
|
-
for (const [topic, keys] of exactTopics) {
|
|
2526
|
-
const outcomes = new Set(keys.map(k => lessons.get(k)?.outcome));
|
|
2527
|
-
if (outcomes.size > 1)
|
|
2528
|
-
contradictions.push({ topic, keys });
|
|
2529
|
-
}
|
|
2530
|
-
// Detect stale: not recalled in stale_days
|
|
2531
|
-
const stale = [];
|
|
2532
|
-
for (const [k, l] of lessons) {
|
|
2533
|
-
const age = now - new Date(l.ts).getTime();
|
|
2534
|
-
const recalls = l.recall_count ?? 0;
|
|
2535
|
-
if (age > staleMs && recalls === 0)
|
|
2536
|
-
stale.push(k);
|
|
2537
|
-
}
|
|
2538
|
-
// Merge duplicates by prefix: keep the success/highest-severity one
|
|
2539
|
-
let merged = 0;
|
|
2540
|
-
if (!dry_run) {
|
|
2541
|
-
for (const [, keys] of topicGroups) {
|
|
2542
|
-
if (keys.length < 2)
|
|
2543
|
-
continue;
|
|
2544
|
-
const bySuccess = keys.filter(k => lessons.get(k)?.outcome === 'success');
|
|
2545
|
-
const winner = bySuccess[0] ?? keys[0];
|
|
2546
|
-
for (const k of keys) {
|
|
2547
|
-
if (k !== winner) {
|
|
2548
|
-
await redis.del(k);
|
|
2549
|
-
merged++;
|
|
2550
|
-
}
|
|
2551
|
-
}
|
|
2552
|
-
}
|
|
2553
|
-
// Resolve contradictions: keep success, delete failure for same topic
|
|
2554
|
-
for (const { keys } of contradictions) {
|
|
2555
|
-
const success = keys.find(k => lessons.get(k)?.outcome === 'success');
|
|
2556
|
-
if (success) {
|
|
2557
|
-
for (const k of keys) {
|
|
2558
|
-
if (k !== success) {
|
|
2559
|
-
await redis.del(k);
|
|
2560
|
-
}
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
}
|
|
2564
|
-
// Flag stale entries with a TTL of 30 days (not deleted, just expiring)
|
|
2565
|
-
for (const k of stale) {
|
|
2566
|
-
await redis.expire(k, 86400 * 30);
|
|
2567
|
-
}
|
|
2568
|
-
}
|
|
2569
|
-
const lines = [
|
|
2570
|
-
`🔬 **Memory Consolidation Report** ${dry_run ? '(dry run — no changes made)' : '✅ Applied'}`,
|
|
2571
|
-
``,
|
|
2572
|
-
`📊 **Before:** ${lessonKeys.length} lessons`,
|
|
2573
|
-
``,
|
|
2574
|
-
`🔁 **Contradictions detected:** ${contradictions.length}`,
|
|
2575
|
-
...contradictions.slice(0, 5).map(c => ` → \`${c.topic}\`: ${c.keys.length} conflicting entries (kept: success)`),
|
|
2576
|
-
contradictions.length > 5 ? ` … and ${contradictions.length - 5} more` : '',
|
|
2577
|
-
``,
|
|
2578
|
-
`♻️ **Duplicate clusters:** ${Array.from(topicGroups.values()).filter(v => v.length > 1).length}` +
|
|
2579
|
-
(merged > 0 ? ` → ${merged} entries merged` : ''),
|
|
2580
|
-
``,
|
|
2581
|
-
`🕰️ **Stale entries (${stale_days}d, 0 recalls):** ${stale.length}` +
|
|
2582
|
-
(stale.length > 0 && !dry_run ? ` → set to expire in 30 days` : ''),
|
|
2583
|
-
``,
|
|
2584
|
-
`📊 **After:** ${dry_run ? lessonKeys.length : lessonKeys.length - merged} lessons`,
|
|
2585
|
-
``,
|
|
2586
|
-
dry_run
|
|
2587
|
-
? `💡 Re-run without dry_run=true to apply changes.`
|
|
2588
|
-
: `✨ Brain consolidated. Run \`brain_diff(since="1h")\` to see the delta.`,
|
|
2589
|
-
].filter(s => s !== '').join('\n');
|
|
2590
|
-
return lines;
|
|
2591
|
-
}
|
|
2592
|
-
// ── v0.6 Cognitive Cache: brain_diff ─────────────────────────────────────
|
|
2593
|
-
case 'brain_diff': {
|
|
2594
|
-
const { instance_id, since = '7d', format = 'summary' } = args;
|
|
2595
|
-
const redis = await getConnection(instance_id);
|
|
2596
|
-
// Parse since
|
|
2597
|
-
let sinceMs;
|
|
2598
|
-
const match = since.match(/^(\d+)([dhm])$/);
|
|
2599
|
-
if (match) {
|
|
2600
|
-
const n = parseInt(match[1]);
|
|
2601
|
-
const unit = match[2];
|
|
2602
|
-
const mult = unit === 'd' ? 86400000 : unit === 'h' ? 3600000 : 60000;
|
|
2603
|
-
sinceMs = Date.now() - n * mult;
|
|
2604
|
-
}
|
|
2605
|
-
else {
|
|
2606
|
-
sinceMs = new Date(since).getTime() || Date.now() - 7 * 86400000;
|
|
2607
|
-
}
|
|
2608
|
-
// Scan all lessons
|
|
2609
|
-
let cursor = 0;
|
|
2610
|
-
const lessonKeys = [];
|
|
2611
|
-
do {
|
|
2612
|
-
const [next, keys] = await redis.scan(cursor, 'MATCH', 'cachly:lesson:best:*', 'COUNT', 200);
|
|
2613
|
-
cursor = parseInt(next);
|
|
2614
|
-
lessonKeys.push(...keys);
|
|
2615
|
-
} while (cursor !== 0);
|
|
2616
|
-
const added = [];
|
|
2617
|
-
const updated = [];
|
|
2618
|
-
const recalled = [];
|
|
2619
|
-
const total = lessonKeys.length;
|
|
2620
|
-
for (const k of lessonKeys) {
|
|
2621
|
-
const raw = await redis.get(k);
|
|
2622
|
-
if (!raw)
|
|
2623
|
-
continue;
|
|
2624
|
-
try {
|
|
2625
|
-
const l = JSON.parse(raw);
|
|
2626
|
-
const ts = new Date(l.ts).getTime();
|
|
2627
|
-
if (ts >= sinceMs) {
|
|
2628
|
-
// Check history to determine add vs update
|
|
2629
|
-
const histKey = `cachly:lesson:history:${l.topic}`;
|
|
2630
|
-
const histLen = await redis.llen(histKey);
|
|
2631
|
-
if (histLen <= 1)
|
|
2632
|
-
added.push(l);
|
|
2633
|
-
else
|
|
2634
|
-
updated.push(l);
|
|
2635
|
-
}
|
|
2636
|
-
if ((l.recall_count ?? 0) > 0) {
|
|
2637
|
-
// We can't easily know when last recalled without extra metadata, so include
|
|
2638
|
-
// lessons with recalls as "active"
|
|
2639
|
-
recalled.push(l);
|
|
2640
|
-
}
|
|
2641
|
-
}
|
|
2642
|
-
catch { /* skip */ }
|
|
2643
|
-
}
|
|
2644
|
-
const sinceLabel = match ? `last ${since}` : new Date(sinceMs).toLocaleDateString('de-DE');
|
|
2645
|
-
const lines = [
|
|
2646
|
-
`📊 **Brain Diff** — ${sinceLabel}`,
|
|
2647
|
-
``,
|
|
2648
|
-
`Total lessons in brain: **${total}**`,
|
|
2649
|
-
``,
|
|
2650
|
-
`✅ **New** (${added.length}):`,
|
|
2651
|
-
...added.slice(0, format === 'detailed' ? 20 : 5).map(l => ` + \`${l.topic}\` — ${l.outcome} ${l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟠' : '🟢'}`),
|
|
2652
|
-
added.length > 5 && format === 'summary' ? ` … and ${added.length - 5} more` : '',
|
|
2653
|
-
``,
|
|
2654
|
-
`🔄 **Updated** (${updated.length}):`,
|
|
2655
|
-
...updated.slice(0, format === 'detailed' ? 20 : 5).map(l => ` ~ \`${l.topic}\` — now: ${l.outcome}`),
|
|
2656
|
-
updated.length > 5 && format === 'summary' ? ` … and ${updated.length - 5} more` : '',
|
|
2657
|
-
``,
|
|
2658
|
-
`🔍 **Active** (recalled at least once): ${recalled.length}`,
|
|
2659
|
-
``,
|
|
2660
|
-
].filter(s => s !== '');
|
|
2661
|
-
// ── 10X brain_diff extensions ──────────────────────────────────────────
|
|
2662
|
-
// 1. Contested → Established (resolution nodes created in window)
|
|
2663
|
-
const resolutionKeys = [];
|
|
2664
|
-
const rsStream = redis.scanStream({ match: 'cachly:ckg:node:resolution-*', count: 100 });
|
|
2665
|
-
await new Promise((res, rej) => { rsStream.on('data', (b) => resolutionKeys.push(...b)); rsStream.on('end', res); rsStream.on('error', rej); });
|
|
2666
|
-
const recentResolutions = [];
|
|
2667
|
-
for (const rk of resolutionKeys) {
|
|
2668
|
-
const rr = await redis.get(rk);
|
|
2669
|
-
if (!rr)
|
|
2670
|
-
continue;
|
|
2671
|
-
try {
|
|
2672
|
-
const rn = JSON.parse(rr);
|
|
2673
|
-
if (rn.ts && new Date(rn.ts).getTime() >= sinceMs)
|
|
2674
|
-
recentResolutions.push({ topic: rn.topic ?? rk, resolution: rn.resolution ?? 'unknown', ts: rn.ts });
|
|
2675
|
-
}
|
|
2676
|
-
catch { /* skip */ }
|
|
2677
|
-
}
|
|
2678
|
-
if (recentResolutions.length > 0) {
|
|
2679
|
-
lines.push(`🗳️ **MADC Resolutions** (${recentResolutions.length} contested beliefs resolved):`);
|
|
2680
|
-
for (const r of recentResolutions.slice(0, 5)) {
|
|
2681
|
-
const rIcon = r.resolution === 'unanimous_success' ? '✅' : r.resolution === 'unanimous_failure' ? '❌' : '⚠️';
|
|
2682
|
-
lines.push(` ${rIcon} \`${r.topic}\` → ${r.resolution}`);
|
|
2683
|
-
}
|
|
2684
|
-
lines.push('');
|
|
2685
|
-
}
|
|
2686
|
-
// 2. New domains bootstrapped (domains in added lessons that weren't in older lessons)
|
|
2687
|
-
const existingDomains = new Set(updated.concat(recalled).map(l => l.topic.split(':')[0]));
|
|
2688
|
-
const newDomains = [...new Set(added.map(l => l.topic.split(':')[0]))].filter(d => !existingDomains.has(d));
|
|
2689
|
-
if (newDomains.length > 0) {
|
|
2690
|
-
lines.push(`🌱 **New domains bootstrapped:** ${newDomains.map(d => `\`${d}\``).join(', ')}`, '');
|
|
2691
|
-
}
|
|
2692
|
-
// 3. FedBrain transfers received in window
|
|
2693
|
-
const fedHistRaw = await redis.lrange('cachly:fedbrain:federations', -20, -1);
|
|
2694
|
-
const recentFeds = fedHistRaw.map(r => { try {
|
|
2695
|
-
return JSON.parse(r);
|
|
2696
|
-
}
|
|
2697
|
-
catch {
|
|
2698
|
-
return null;
|
|
2699
|
-
} })
|
|
2700
|
-
.filter(f => f && new Date(f.transferred_at).getTime() >= sinceMs);
|
|
2701
|
-
if (recentFeds.length > 0) {
|
|
2702
|
-
lines.push(`🧠 **FedBrain transfers received (${recentFeds.length}):**`);
|
|
2703
|
-
for (const f of recentFeds.slice(0, 3)) {
|
|
2704
|
-
lines.push(` 📥 domain \`${f.domain}\` from \`${f.source.slice(0, 16)}…\` — ${f.nodes} nodes, ${f.edges} edges`);
|
|
2705
|
-
}
|
|
2706
|
-
lines.push('');
|
|
2707
|
-
}
|
|
2708
|
-
lines.push(`💡 Run \`memory_consolidate\` to merge duplicates · \`knowledge_decay\` to see confidence scores.`);
|
|
2709
|
-
return lines.join('\n');
|
|
2710
|
-
}
|
|
2711
|
-
// ── v0.6 Cognitive Cache: causal_trace ───────────────────────────────────
|
|
2712
|
-
case 'causal_trace': {
|
|
2713
|
-
const { instance_id, problem, max_depth = 5, tags: filterTags = [] } = args;
|
|
2714
|
-
const redis = await getConnection(instance_id);
|
|
2715
|
-
// Normalize problem to keyword tokens
|
|
2716
|
-
const tokens = problem.toLowerCase()
|
|
2717
|
-
.replace(/[^a-z0-9\s\-_:]/g, ' ')
|
|
2718
|
-
.split(/\s+/)
|
|
2719
|
-
.filter(t => t.length > 2);
|
|
2720
|
-
const SEV_ICON = { critical: '🔴', major: '🟠', minor: '🟡' };
|
|
2721
|
-
const ckgResults = [];
|
|
2722
|
-
try {
|
|
2723
|
-
for (const token of tokens.slice(0, 4)) {
|
|
2724
|
-
// Search for CKG nodes matching this token
|
|
2725
|
-
const fromKeys = await redis.smembers(`cachly:ckg:idx:from:${ckgSlug(token)}`);
|
|
2726
|
-
const toKeys = await redis.smembers(`cachly:ckg:idx:to:${ckgSlug(token)}`);
|
|
2727
|
-
// Also try pattern: scan nodes containing the token
|
|
2728
|
-
const nodeKeys = [];
|
|
2729
|
-
const nStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 50 });
|
|
2730
|
-
await new Promise((res, rej) => { nStream.on('data', (b) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
|
|
2731
|
-
for (const nodeKey of nodeKeys.slice(0, 10)) {
|
|
2732
|
-
const nodeRaw = await redis.get(nodeKey);
|
|
2733
|
-
if (!nodeRaw)
|
|
2734
|
-
continue;
|
|
2735
|
-
const node = JSON.parse(nodeRaw);
|
|
2736
|
-
// Get edges from this node
|
|
2737
|
-
const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
2738
|
-
for (const ek of edgeKeys.slice(0, 20)) {
|
|
2739
|
-
const edgeRaw = await redis.get(ek);
|
|
2740
|
-
if (!edgeRaw)
|
|
2741
|
-
continue;
|
|
2742
|
-
const edge = JSON.parse(edgeRaw);
|
|
2743
|
-
if (edge.edgeType !== 'fixes' && edge.edgeType !== 'requires')
|
|
2744
|
-
continue;
|
|
2745
|
-
// Find lesson for this concept
|
|
2746
|
-
const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from.replace(/-/g, ':').replace(/^fix:/, 'fix:')}`);
|
|
2747
|
-
const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
|
|
2748
|
-
ckgResults.push({ conceptId: node.id, edge, lesson });
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
2751
|
-
for (const ek of [...fromKeys, ...toKeys].slice(0, 20)) {
|
|
2752
|
-
const edgeRaw = await redis.get(ek);
|
|
2753
|
-
if (!edgeRaw)
|
|
2754
|
-
continue;
|
|
2755
|
-
const edge = JSON.parse(edgeRaw);
|
|
2756
|
-
const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from}`);
|
|
2757
|
-
const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
|
|
2758
|
-
ckgResults.push({ conceptId: edge.from, edge, lesson });
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
}
|
|
2762
|
-
catch { /* CKG traversal non-critical */ }
|
|
2763
|
-
// Deduplicate CKG results and sort by confidence
|
|
2764
|
-
const ckgSeen = new Set();
|
|
2765
|
-
const ckgDeduped = ckgResults.filter(r => {
|
|
2766
|
-
const key = `${r.edge.from}:${r.edge.edgeType}:${r.edge.to}`;
|
|
2767
|
-
if (ckgSeen.has(key))
|
|
2768
|
-
return false;
|
|
2769
|
-
ckgSeen.add(key);
|
|
2770
|
-
return true;
|
|
2771
|
-
}).sort((a, b) => b.edge.confidence - a.edge.confidence).slice(0, max_depth);
|
|
2772
|
-
// ── Layer 2 (fallback): text similarity over all lessons ─────────────────
|
|
2773
|
-
let cursor = 0;
|
|
2774
|
-
const lessonKeys = [];
|
|
2775
|
-
do {
|
|
2776
|
-
const [next, keys] = await redis.scan(cursor, 'MATCH', 'cachly:lesson:*', 'COUNT', 200);
|
|
2777
|
-
cursor = parseInt(next);
|
|
2778
|
-
lessonKeys.push(...keys);
|
|
2779
|
-
} while (cursor !== 0);
|
|
2780
|
-
// Score each lesson by token overlap with problem description
|
|
2781
|
-
const scored = [];
|
|
2782
|
-
for (const k of lessonKeys) {
|
|
2783
|
-
const raw = await redis.get(k);
|
|
2784
|
-
if (!raw)
|
|
2785
|
-
continue;
|
|
2786
|
-
try {
|
|
2787
|
-
const l = JSON.parse(raw);
|
|
2788
|
-
if (filterTags.length > 0 && !(l.tags ?? []).some((t) => filterTags.includes(t)))
|
|
2789
|
-
continue;
|
|
2790
|
-
const haystack = [l.topic, l.what_failed ?? '', l.what_worked ?? '', l.context ?? '']
|
|
2791
|
-
.join(' ').toLowerCase();
|
|
2792
|
-
const score = tokens.reduce((s, t) => s + (haystack.includes(t) ? 1 : 0), 0);
|
|
2793
|
-
if (score > 0)
|
|
2794
|
-
scored.push({ score, lesson: l, key: k });
|
|
2795
|
-
}
|
|
2796
|
-
catch { /* skip */ }
|
|
2797
|
-
}
|
|
2798
|
-
scored.sort((a, b) => b.score - a.score);
|
|
2799
|
-
const chain = scored.slice(0, max_depth);
|
|
2800
|
-
if (chain.length === 0 && ckgDeduped.length === 0) {
|
|
2801
|
-
return [
|
|
2802
|
-
`🔍 **Causal Trace: "${problem}"**`,
|
|
2803
|
-
``,
|
|
2804
|
-
`No matching lessons found in brain.`,
|
|
2805
|
-
``,
|
|
2806
|
-
`💡 After you solve this, call:`,
|
|
2807
|
-
`\`\`\``,
|
|
2808
|
-
`learn_from_attempts(`,
|
|
2809
|
-
` instance_id = "${instance_id}",`,
|
|
2810
|
-
` topic = "fix:${tokens[0] ?? 'issue'}",`,
|
|
2811
|
-
` outcome = "success",`,
|
|
2812
|
-
` what_worked = "...",`,
|
|
2813
|
-
` what_failed = "${problem}",`,
|
|
2814
|
-
`)`,
|
|
2815
|
-
`\`\`\``,
|
|
2816
|
-
].join('\n');
|
|
2817
|
-
}
|
|
2818
|
-
const lines = [
|
|
2819
|
-
`🔍 **Causal Trace: "${problem}"**`,
|
|
2820
|
-
``,
|
|
2821
|
-
];
|
|
2822
|
-
// Show CKG graph results first if available
|
|
2823
|
-
if (ckgDeduped.length > 0) {
|
|
2824
|
-
lines.push(`### 🕸️ CKG Graph (confidence-ranked)`);
|
|
2825
|
-
for (const r of ckgDeduped) {
|
|
2826
|
-
const confPct = Math.round(r.edge.confidence * 100);
|
|
2827
|
-
const confBar = '▓'.repeat(Math.round(confPct / 10)) + '░'.repeat(10 - Math.round(confPct / 10));
|
|
2828
|
-
lines.push(` ${r.edge.from} **→[${r.edge.edgeType}]→** ${r.edge.to}`);
|
|
2829
|
-
lines.push(` ${confBar} ${confPct}% confidence (${r.edge.successes}/${r.edge.trials} confirmed)`);
|
|
2830
|
-
if (r.lesson?.what_worked)
|
|
2831
|
-
lines.push(` ✅ Fix: ${r.lesson.what_worked.slice(0, 150)}`);
|
|
2832
|
-
lines.push('');
|
|
2833
|
-
}
|
|
2834
|
-
}
|
|
2835
|
-
// Build text-based causal chain narrative
|
|
2836
|
-
if (chain.length > 0) {
|
|
2837
|
-
lines.push(ckgDeduped.length > 0 ? `### 📚 Text Search (${chain.length} related lessons)` : `Found **${chain.length}** related lessons. Reconstructed causal chain:`, '');
|
|
2838
|
-
const failures = chain.filter(c => c.lesson.outcome !== 'success');
|
|
2839
|
-
const solutions = chain.filter(c => c.lesson.outcome === 'success');
|
|
2840
|
-
if (failures.length > 0) {
|
|
2841
|
-
lines.push(`**Root causes & failure chain:**`);
|
|
2842
|
-
failures.forEach((c, i) => {
|
|
2843
|
-
const l = c.lesson;
|
|
2844
|
-
const sev = SEV_ICON[l.severity ?? 'minor'] ?? '🟡';
|
|
2845
|
-
lines.push(`${i === 0 ? ' Root:' : ' → :'} ${sev} \`${l.topic}\``);
|
|
2846
|
-
if (l.what_failed)
|
|
2847
|
-
lines.push(` ↳ ${l.what_failed.slice(0, 120)}`);
|
|
2848
|
-
});
|
|
2849
|
-
lines.push('');
|
|
2850
|
-
}
|
|
2851
|
-
if (solutions.length > 0) {
|
|
2852
|
-
lines.push(`**Solutions that worked before:**`);
|
|
2853
|
-
solutions.forEach((c, i) => {
|
|
2854
|
-
const l = c.lesson;
|
|
2855
|
-
const date = new Date(l.ts).toLocaleDateString('de-DE');
|
|
2856
|
-
lines.push(` ${i + 1}. ✅ \`${l.topic}\` — ${date} · recalled ${l.recall_count ?? 0}×`);
|
|
2857
|
-
if (l.what_worked)
|
|
2858
|
-
lines.push(` ${l.what_worked.slice(0, 200)}`);
|
|
2859
|
-
});
|
|
2860
|
-
lines.push('');
|
|
2861
|
-
}
|
|
2862
|
-
const topSolution = solutions[0]?.lesson;
|
|
2863
|
-
if (topSolution?.what_worked) {
|
|
2864
|
-
lines.push(`**⚡ Most likely fix:**`);
|
|
2865
|
-
lines.push(`\`\`\``);
|
|
2866
|
-
lines.push(topSolution.what_worked.slice(0, 500));
|
|
2867
|
-
lines.push(`\`\`\``);
|
|
2868
|
-
lines.push('');
|
|
2869
|
-
}
|
|
2870
|
-
}
|
|
2871
|
-
lines.push(`💡 After applying: \`learn_from_attempts(topic="fix:${tokens[0] ?? 'issue'}", outcome="success", ...)\``);
|
|
2872
|
-
if (ckgDeduped.length > 0)
|
|
2873
|
-
lines.push(`🕸️ Explore graph: \`ckg_inspect(concept="${tokens[0] ?? 'fix'}")\``);
|
|
2874
|
-
return lines.join('\n');
|
|
2875
|
-
}
|
|
2876
|
-
case 'knowledge_decay': {
|
|
2877
|
-
const { instance_id, min_age_days = 0, show_top = 20 } = args;
|
|
2878
|
-
const redis = await getConnection(instance_id);
|
|
2879
|
-
const now = Date.now();
|
|
2880
|
-
const minAgeMs = min_age_days * 86400000;
|
|
2881
|
-
let cursor = 0;
|
|
2882
|
-
const lessonKeys = [];
|
|
2883
|
-
do {
|
|
2884
|
-
const [next, keys] = await redis.scan(cursor, 'MATCH', 'cachly:lesson:best:*', 'COUNT', 200);
|
|
2885
|
-
cursor = parseInt(next);
|
|
2886
|
-
lessonKeys.push(...keys);
|
|
2887
|
-
} while (cursor !== 0);
|
|
2888
|
-
const scores = [];
|
|
2889
|
-
for (const k of lessonKeys) {
|
|
2890
|
-
const raw = await redis.get(k);
|
|
2891
|
-
if (!raw)
|
|
2892
|
-
continue;
|
|
2893
|
-
try {
|
|
2894
|
-
const l = JSON.parse(raw);
|
|
2895
|
-
const ageMs = now - new Date(l.ts).getTime();
|
|
2896
|
-
if (ageMs < minAgeMs)
|
|
2897
|
-
continue;
|
|
2898
|
-
const age_days = Math.floor(ageMs / 86400000);
|
|
2899
|
-
const recalls = l.recall_count ?? 0;
|
|
2900
|
-
// Confidence formula:
|
|
2901
|
-
// base = 100 → decays by 1pt/day after 7 days, floored at 5
|
|
2902
|
-
// boost: +5 per recall, capped at +50
|
|
2903
|
-
// penalty: failure outcome → -20
|
|
2904
|
-
const decayPts = Math.max(0, age_days - 7);
|
|
2905
|
-
const base = Math.max(5, 100 - decayPts);
|
|
2906
|
-
const recallBoost = Math.min(50, recalls * 5);
|
|
2907
|
-
const outcomePenalty = l.outcome === 'failure' ? -20 : 0;
|
|
2908
|
-
const confidence = Math.min(100, Math.max(0, base + recallBoost + outcomePenalty));
|
|
2909
|
-
scores.push({ topic: l.topic, confidence, age_days, recalls, outcome: l.outcome });
|
|
2910
|
-
}
|
|
2911
|
-
catch { /* skip */ }
|
|
2912
|
-
}
|
|
2913
|
-
// Sort by lowest confidence first
|
|
2914
|
-
scores.sort((a, b) => a.confidence - b.confidence);
|
|
2915
|
-
const shown = scores.slice(0, show_top);
|
|
2916
|
-
function bar(pct) {
|
|
2917
|
-
const filled = Math.round(pct / 10);
|
|
2918
|
-
return '█'.repeat(filled) + '░'.repeat(10 - filled) + ` ${pct}%`;
|
|
2919
|
-
}
|
|
2920
|
-
const avgConf = scores.length > 0
|
|
2921
|
-
? Math.round(scores.reduce((s, e) => s + e.confidence, 0) / scores.length)
|
|
2922
|
-
: 0;
|
|
2923
|
-
const critical = scores.filter(s => s.confidence < 30).length;
|
|
2924
|
-
const healthy = scores.filter(s => s.confidence >= 70).length;
|
|
2925
|
-
const lines = [
|
|
2926
|
-
`🧪 **Knowledge Decay Report** — ${scores.length} lessons`,
|
|
2927
|
-
``,
|
|
2928
|
-
`Average confidence: **${bar(avgConf)}**`,
|
|
2929
|
-
`🔴 Critical (<30%): ${critical} 🟢 Healthy (≥70%): ${healthy}`,
|
|
2930
|
-
``,
|
|
2931
|
-
`Lowest confidence lessons (fix these first):`,
|
|
2932
|
-
``,
|
|
2933
|
-
...shown.map((s, i) => `${(i + 1).toString().padStart(2)}. \`${s.topic}\`\n ${bar(s.confidence)} · ${s.age_days}d old · ${s.recalls} recalls · ${s.outcome}`),
|
|
2934
|
-
``,
|
|
2935
|
-
scores.length > show_top ? `… and ${scores.length - show_top} more lessons` : '',
|
|
2936
|
-
``,
|
|
2937
|
-
critical > 0
|
|
2938
|
-
? `⚠️ ${critical} lessons are critically stale. Validate them and re-run \`learn_from_attempts\` with updated findings.`
|
|
2939
|
-
: `✅ Brain confidence is healthy overall.`,
|
|
2940
|
-
``,
|
|
2941
|
-
`💡 Run \`memory_consolidate\` to prune stale entries · \`causal_trace\` to find root causes.`,
|
|
2942
|
-
].filter(s => s !== '');
|
|
2943
|
-
return lines.join('\n');
|
|
2944
|
-
}
|
|
2945
|
-
// ── v0.6 Cognitive Cache: autopilot ──────────────────────────────────────
|
|
2946
|
-
case 'autopilot': {
|
|
2947
|
-
const { instance_id, editor = 'claude', project_name = 'this project', style = 'full' } = args;
|
|
2948
|
-
const minimal = `## Cachly AI Brain — Autopilot
|
|
2949
|
-
|
|
2950
|
-
**Instance:** \`${instance_id}\`
|
|
2951
|
-
|
|
2952
|
-
> Sessions are fully automatic. The Brain starts when you call any tool and saves a summary when you close.
|
|
2953
|
-
|
|
2954
|
-
### MANDATORY hooks (always fire, no exceptions):
|
|
2955
|
-
- **Window opens** → call \`session_start(instance_id="${instance_id}", focus="<today's goal>")\`
|
|
2956
|
-
- **Bug fixed / deployed / discovered** → call \`learn_from_attempts(instance_id="${instance_id}", topic="fix:<area>", outcome="success", what_worked="...")\`
|
|
2957
|
-
- **Window closes** → call \`session_end(instance_id="${instance_id}", summary="...", files_changed=[...])\`
|
|
2958
|
-
`;
|
|
2959
|
-
const full = `## Cachly AI Brain — Autopilot for ${project_name}
|
|
2960
|
-
|
|
2961
|
-
**Instance:** \`${instance_id}\`
|
|
2962
|
-
|
|
2963
|
-
---
|
|
2964
|
-
|
|
2965
|
-
## 🔴 MANDATORY RULES — No exceptions, no prompting needed
|
|
2966
|
-
|
|
2967
|
-
### Rule 1 — Session Start (FIRST thing in every window)
|
|
2968
|
-
\`\`\`
|
|
2969
|
-
session_start(
|
|
2970
|
-
instance_id = "${instance_id}",
|
|
2971
|
-
focus = "<what you plan to work on today>",
|
|
2972
|
-
)
|
|
2973
|
-
\`\`\`
|
|
2974
|
-
Returns: last session summary, handoff tasks, recent lessons, open failures.
|
|
2975
|
-
|
|
2976
|
-
### Rule 2 — Learn After Every Fix/Deploy/Discovery (AUTOMATIC)
|
|
2977
|
-
**After EVERY** bug fix, deployment, or technical discovery — call immediately:
|
|
2978
|
-
\`\`\`
|
|
2979
|
-
learn_from_attempts(
|
|
2980
|
-
instance_id = "${instance_id}",
|
|
2981
|
-
topic = "category:keyword",
|
|
2982
|
-
outcome = "success" | "failure" | "partial",
|
|
2983
|
-
what_worked = "exact command or approach",
|
|
2984
|
-
what_failed = "what failed (if applicable)",
|
|
2985
|
-
severity = "critical" | "major" | "minor",
|
|
2986
|
-
commands = ["the exact command that worked"],
|
|
2987
|
-
file_paths = ["path/to/file"],
|
|
2988
|
-
tags = ["relevant", "tags"],
|
|
2989
|
-
)
|
|
2990
|
-
\`\`\`
|
|
2991
|
-
|
|
2992
|
-
**Triggers:**
|
|
2993
|
-
- ✅ Deployed → \`topic="deploy:web", outcome="success"\`
|
|
2994
|
-
- ✅ Fixed bug → \`topic="fix:auth-race", outcome="success"\`
|
|
2995
|
-
- ✅ Got error → \`topic="debug:docker", outcome="partial"\`
|
|
2996
|
-
|
|
2997
|
-
### Rule 3 — Session End (LAST thing before closing)
|
|
2998
|
-
\`\`\`
|
|
2999
|
-
session_end(
|
|
3000
|
-
instance_id = "${instance_id}",
|
|
3001
|
-
summary = "What was accomplished (2-3 sentences)",
|
|
3002
|
-
files_changed = ["list", "of", "changed", "files"],
|
|
3003
|
-
lessons_learned = <number of learn_from_attempts calls>,
|
|
3004
|
-
)
|
|
3005
|
-
\`\`\`
|
|
3006
|
-
|
|
3007
|
-
### Rule 4 — Before any non-trivial task
|
|
3008
|
-
\`\`\`
|
|
3009
|
-
smart_recall(
|
|
3010
|
-
instance_id = "${instance_id}",
|
|
3011
|
-
query = "<describe what you are about to do>",
|
|
3012
|
-
)
|
|
3013
|
-
\`\`\`
|
|
3014
|
-
|
|
3015
|
-
---
|
|
3016
|
-
|
|
3017
|
-
## ⚡ Cognitive Tools (v0.6)
|
|
3018
|
-
|
|
3019
|
-
| When | Call |
|
|
3020
|
-
|---|---|
|
|
3021
|
-
| Brain feeling cluttered | \`memory_consolidate(instance_id="${instance_id}")\` |
|
|
3022
|
-
| Weekly review | \`brain_diff(instance_id="${instance_id}", since="7d")\` |
|
|
3023
|
-
| Weird bug, no idea why | \`causal_trace(instance_id="${instance_id}", problem="<symptom>")\` |
|
|
3024
|
-
| Before big refactor | \`knowledge_decay(instance_id="${instance_id}")\` |
|
|
3025
|
-
|
|
3026
|
-
---
|
|
3027
|
-
|
|
3028
|
-
*Cachly v0.6 · Generated ${new Date().toISOString().slice(0, 10)}*
|
|
3029
|
-
`;
|
|
3030
|
-
const content = style === 'minimal' ? minimal : full;
|
|
3031
|
-
const filename = editor === 'copilot'
|
|
3032
|
-
? '.github/copilot-instructions.md'
|
|
3033
|
-
: editor === 'continue'
|
|
3034
|
-
? '.continue/cachly-autopilot.md'
|
|
3035
|
-
: 'CLAUDE.md';
|
|
3036
|
-
return [
|
|
3037
|
-
`🤖 **Autopilot instructions generated** for **${editor === 'all' ? 'all editors' : editor}**`,
|
|
3038
|
-
``,
|
|
3039
|
-
`**File to create:** \`${filename}\``,
|
|
3040
|
-
``,
|
|
3041
|
-
`\`\`\`markdown`,
|
|
3042
|
-
content,
|
|
3043
|
-
`\`\`\``,
|
|
3044
|
-
``,
|
|
3045
|
-
`**How to apply:**`,
|
|
3046
|
-
`\`\`\`bash`,
|
|
3047
|
-
`# Copy to your project root:`,
|
|
3048
|
-
`cat > ${filename} << 'EOF'`,
|
|
3049
|
-
content,
|
|
3050
|
-
`EOF`,
|
|
3051
|
-
`\`\`\``,
|
|
3052
|
-
``,
|
|
3053
|
-
`✨ Once this file is in place, **${editor === 'copilot' ? 'GitHub Copilot' : editor === 'continue' ? 'Continue.dev' : 'Claude/Cursor/Windsurf'}** will manage the Brain automatically — no manual calls needed, ever.`,
|
|
3054
|
-
].join('\n');
|
|
3055
|
-
}
|
|
3056
|
-
// ── v0.7 Knowledge Syndication: syndicate ────────────────────────────────
|
|
3057
|
-
case 'syndicate': {
|
|
3058
|
-
const { topic, outcome = 'success', what_worked, what_failed = '', severity = 'minor', tags = [], scope = 'public' } = args;
|
|
3059
|
-
if (!topic || !what_worked) {
|
|
3060
|
-
throw new McpError(ErrorCode.InvalidParams, 'topic and what_worked are required');
|
|
3061
|
-
}
|
|
3062
|
-
const body = { topic, outcome, what_worked, what_failed, severity, tags, scope };
|
|
3063
|
-
const res = await apiFetch('/api/v1/syndication/contribute', { method: 'POST', body: JSON.stringify(body) });
|
|
3064
|
-
const scopeLabel = scope === 'org' ? '🏢 org-private' : '🌐 global commons';
|
|
3065
|
-
const dedupNote = res.deduped
|
|
3066
|
-
? `\n> ♻️ Duplicate detected — trust score incremented for the existing lesson.`
|
|
3067
|
-
: '';
|
|
3068
|
-
return [
|
|
3069
|
-
`${scope === 'org' ? '🏢' : '🌐'} **Lesson syndicated to the ${scope === 'org' ? 'org Knowledge Commons' : 'global Knowledge Commons'}**${dedupNote}`,
|
|
3070
|
-
``,
|
|
3071
|
-
`**ID:** \`${res.id}\``,
|
|
3072
|
-
`**Topic:** \`${res.topic}\` · **Outcome:** ${res.outcome} · **Scope:** ${scopeLabel}`,
|
|
3073
|
-
``,
|
|
3074
|
-
scope === 'org'
|
|
3075
|
-
? `This lesson is visible only within your organisation. Use \`syndicate_search(scope="org")\` to find it.`
|
|
3076
|
-
: `Your lesson is now searchable by every AI brain in the network.`,
|
|
3077
|
-
`When another instance confirms it works, its trust score rises — and so does your contributor reputation.`,
|
|
3078
|
-
``,
|
|
3079
|
-
`**Tip:** Use \`syndicate_search(q="${topic}")\` to see all community lessons on this topic.`,
|
|
3080
|
-
].join('\n');
|
|
3081
|
-
}
|
|
3082
|
-
// ── v0.7 Knowledge Syndication: syndicate_search ─────────────────────────
|
|
3083
|
-
case 'syndicate_search': {
|
|
3084
|
-
const { q = '', limit = 20, category = '', scope = '' } = args;
|
|
3085
|
-
const params = new URLSearchParams();
|
|
3086
|
-
if (q)
|
|
3087
|
-
params.set('q', q);
|
|
3088
|
-
if (category)
|
|
3089
|
-
params.set('category', category);
|
|
3090
|
-
if (scope)
|
|
3091
|
-
params.set('scope', scope);
|
|
3092
|
-
params.set('limit', String(Math.min(Math.max(1, limit), 50)));
|
|
3093
|
-
const res = await apiFetch(`/api/v1/syndication/search?${params}`);
|
|
3094
|
-
if (!res.results || res.results.length === 0) {
|
|
3095
|
-
return q
|
|
3096
|
-
? `No lessons found for "${q}" in the global Knowledge Commons yet.\n\nBe the first to contribute: \`syndicate(topic="...", what_worked="...")\``
|
|
3097
|
-
: `The global Knowledge Commons is empty. Be the first contributor:\n\`syndicate(topic="deploy:api", what_worked="...")\``;
|
|
3098
|
-
}
|
|
3099
|
-
const outcomeIcon = (o) => o === 'success' ? '✅' : o === 'failure' ? '❌' : '⚠️';
|
|
3100
|
-
const severityLabel = (s) => s === 'critical' ? '🔴' : s === 'major' ? '🟡' : '🟢';
|
|
3101
|
-
const confirmBar = (n) => {
|
|
3102
|
-
const filled = Math.min(10, Math.round(n / 5));
|
|
3103
|
-
return '█'.repeat(filled) + '░'.repeat(10 - filled) + ` ×${n}`;
|
|
3104
|
-
};
|
|
3105
|
-
const header = [q, category].filter(Boolean).join(' · ');
|
|
3106
|
-
const lines = [
|
|
3107
|
-
`## 🌐 Global Knowledge Commons${header ? ` — ${header}` : ' — Recent'}`,
|
|
3108
|
-
`*${res.count} lesson${res.count === 1 ? '' : 's'} found*`,
|
|
3109
|
-
``,
|
|
3110
|
-
];
|
|
3111
|
-
for (const lesson of res.results) {
|
|
3112
|
-
lines.push(`### ${outcomeIcon(lesson.outcome)} \`${lesson.topic}\` ${severityLabel(lesson.severity)}`, `**Trust:** ${confirmBar(lesson.confirm_count)}`, lesson.what_worked ? `**What worked:** ${lesson.what_worked}` : '', lesson.what_failed ? `**What failed:** ${lesson.what_failed}` : '', `*Contributed ${new Date(lesson.created_at).toLocaleDateString('de-DE')} · ID: \`${lesson.id}\`*`, ``);
|
|
3113
|
-
}
|
|
3114
|
-
lines.push(`---`, `**Confirm** (this helped you): \`syndicate(topic="${res.results[0]?.topic ?? '...'}", what_worked="...")\` → auto-deduped, trust +1`, `**Contribute your own:** \`syndicate(topic="fix:...", what_worked="...")\``, `**Filter by category:** \`syndicate_search(category="fix")\``);
|
|
3115
|
-
return lines.filter(l => l !== '').join('\n');
|
|
3116
|
-
}
|
|
3117
|
-
// ── v0.7 Knowledge Syndication: syndicate_stats ──────────────────────────
|
|
3118
|
-
case 'syndicate_stats': {
|
|
3119
|
-
const res = await apiFetch('/api/v1/syndication/stats');
|
|
3120
|
-
const confirmBar = (n) => {
|
|
3121
|
-
const filled = Math.min(10, Math.round(n / 5));
|
|
3122
|
-
return '█'.repeat(filled) + '░'.repeat(10 - filled) + ` ×${n}`;
|
|
3123
|
-
};
|
|
3124
|
-
const lines = [
|
|
3125
|
-
`## 🌐 Global Knowledge Commons — Stats`,
|
|
3126
|
-
``,
|
|
3127
|
-
`| Metric | Value |`,
|
|
3128
|
-
`|---|---|`,
|
|
3129
|
-
`| Total lessons | **${res.total_lessons.toLocaleString()}** |`,
|
|
3130
|
-
`| Total confirms | **${res.total_confirms.toLocaleString()}** |`,
|
|
3131
|
-
`| Added last 7 days | **${res.added_last_7_days}** |`,
|
|
3132
|
-
``,
|
|
3133
|
-
`### Top Categories`,
|
|
3134
|
-
];
|
|
3135
|
-
for (const cat of res.top_categories ?? []) {
|
|
3136
|
-
lines.push(`- \`${cat.category}\` — ${cat.count} lesson${cat.count === 1 ? '' : 's'}`);
|
|
3137
|
-
}
|
|
3138
|
-
lines.push(``, `### Most Trusted Lessons`);
|
|
3139
|
-
for (const lesson of res.most_trusted ?? []) {
|
|
3140
|
-
lines.push(`**\`${lesson.topic}\`** ${confirmBar(lesson.confirm_count)}`, `> ${lesson.what_worked.slice(0, 120)}${lesson.what_worked.length > 120 ? '…' : ''}`, ``);
|
|
3141
|
-
}
|
|
3142
|
-
lines.push(`---`, `**Contribute:** \`syndicate(topic="...", what_worked="...")\` | **Search:** \`syndicate_search(q="your problem")\``);
|
|
3143
|
-
// Top contributors (anonymous scores)
|
|
3144
|
-
if (res.top_contributors?.length) {
|
|
3145
|
-
lines.push(``, `### 🏅 Top Contributors (anonymous)`);
|
|
3146
|
-
for (const c of res.top_contributors) {
|
|
3147
|
-
lines.push(`- Trust **${c.trust_score}** · ${c.lessons_count} lesson${c.lessons_count === 1 ? '' : 's'} · ${c.confirms_received} confirms received`);
|
|
3148
|
-
}
|
|
3149
|
-
}
|
|
3150
|
-
return lines.join('\n');
|
|
3151
|
-
}
|
|
3152
|
-
// ── v0.8 Knowledge Syndication: syndicate_trending ───────────────────────
|
|
3153
|
-
case 'syndicate_trending': {
|
|
3154
|
-
const { limit = 10 } = args;
|
|
3155
|
-
const params = new URLSearchParams({ limit: String(Math.min(Math.max(1, limit), 50)) });
|
|
3156
|
-
const res = await apiFetch(`/api/v1/syndication/trending?${params}`);
|
|
3157
|
-
if (!res.results || res.results.length === 0) {
|
|
3158
|
-
return [
|
|
3159
|
-
`## 📈 Trending in the Knowledge Commons`,
|
|
3160
|
-
``,
|
|
3161
|
-
`No trending lessons yet (need at least 2 confirms in the last 7 days).`,
|
|
3162
|
-
``,
|
|
3163
|
-
`Contribute and confirm lessons to see them trend: \`syndicate(topic="...", what_worked="...")\``,
|
|
3164
|
-
].join('\n');
|
|
3165
|
-
}
|
|
3166
|
-
const outcomeIcon = (o) => o === 'success' ? '✅' : o === 'failure' ? '❌' : '⚠️';
|
|
3167
|
-
const severityLabel = (s) => s === 'critical' ? '🔴' : s === 'major' ? '🟡' : '🟢';
|
|
3168
|
-
const confirmBar = (n) => {
|
|
3169
|
-
const filled = Math.min(10, Math.round(n / 5));
|
|
3170
|
-
return '█'.repeat(filled) + '░'.repeat(10 - filled) + ` ×${n}`;
|
|
3171
|
-
};
|
|
3172
|
-
const trendBar = (score) => {
|
|
3173
|
-
const filled = Math.min(10, Math.round(score * 2));
|
|
3174
|
-
return '▲'.repeat(filled) + '△'.repeat(10 - filled) + ` ${score.toFixed(2)}/day`;
|
|
3175
|
-
};
|
|
3176
|
-
const lines = [
|
|
3177
|
-
`## 📈 Trending in the Knowledge Commons`,
|
|
3178
|
-
`*Lessons with the fastest confirmation velocity in the last 7 days*`,
|
|
3179
|
-
``,
|
|
3180
|
-
];
|
|
3181
|
-
for (const lesson of res.results) {
|
|
3182
|
-
lines.push(`### ${outcomeIcon(lesson.outcome)} \`${lesson.topic}\` ${severityLabel(lesson.severity)}`, `**Trend:** ${trendBar(lesson.trend_score)} | **Trust:** ${confirmBar(lesson.confirm_count)}`, lesson.what_worked ? `**What worked:** ${lesson.what_worked.slice(0, 200)}${lesson.what_worked.length > 200 ? '…' : ''}` : '', `*ID: \`${lesson.id}\` · ${new Date(lesson.created_at).toLocaleDateString('de-DE')}*`, ``);
|
|
3183
|
-
}
|
|
3184
|
-
lines.push(`---`, `**Confirm** (if this helped you): \`syndicate(topic="${res.results[0]?.topic ?? '...'}", what_worked="...")\` → auto-deduped, trust +1`, `**All trending:** \`syndicate_trending(limit=50)\` | **Search:** \`syndicate_search(q="...")\``);
|
|
3185
|
-
return lines.filter(l => l !== '').join('\n');
|
|
3186
|
-
}
|
|
3187
|
-
// ── Layer 1: brain_search ─────────────────────────────────────────────────
|
|
3188
|
-
case 'brain_search': {
|
|
3189
|
-
const { instance_id, query, limit = 15 } = args;
|
|
3190
|
-
const redis = await getConnection(instance_id);
|
|
3191
|
-
// BM25+ over ALL brain key namespaces
|
|
3192
|
-
const allMatches = await keywordSearch(redis, [
|
|
3193
|
-
'cachly:lesson:best:*',
|
|
3194
|
-
'cachly:ctx:*',
|
|
3195
|
-
'cachly:idx:*',
|
|
3196
|
-
'cachly:session:last',
|
|
3197
|
-
'cachly:session:handoff',
|
|
3198
|
-
'cachly:roadmap:*',
|
|
3199
|
-
'cachly:ckg:node:*',
|
|
3200
|
-
], query, limit);
|
|
3201
|
-
if (allMatches.length === 0) {
|
|
3202
|
-
return [`🔎 **Brain Search: "${query}"**`, '', `No results found across all brain data.`, '', `💡 Try \`smart_recall\` or check \`list_remembered\`.`].join('\n');
|
|
3203
|
-
}
|
|
3204
|
-
const lines = [`🔎 **Brain Search: "${query}"** — ${allMatches.length} result${allMatches.length !== 1 ? 's' : ''} across all brain data\n`];
|
|
3205
|
-
for (const m of allMatches.slice(0, limit)) {
|
|
3206
|
-
const ns = m.key.startsWith('cachly:lesson:') ? '💡 lesson'
|
|
3207
|
-
: m.key.startsWith('cachly:ctx:') ? '📝 context'
|
|
3208
|
-
: m.key.startsWith('cachly:idx:') ? '📂 index'
|
|
3209
|
-
: m.key.startsWith('cachly:session:') ? '🕐 session'
|
|
3210
|
-
: m.key.startsWith('cachly:roadmap:') ? '🗺️ roadmap'
|
|
3211
|
-
: m.key.startsWith('cachly:ckg:node:') ? '🕸️ ckg-node'
|
|
3212
|
-
: '🗄️ data';
|
|
3213
|
-
const preview = m.content.slice(0, 280).replace(/\n/g, ' ');
|
|
3214
|
-
lines.push(`**${ns}** \`${m.key.split(':').slice(2).join(':')}\` _(BM25: ${m.score.toFixed(2)})_`);
|
|
3215
|
-
lines.push(`> ${preview}${m.content.length > 280 ? '…' : ''}\n`);
|
|
3216
|
-
}
|
|
3217
|
-
return lines.join('\n');
|
|
3218
|
-
}
|
|
3219
|
-
// ── Layer 1: ckg_inspect ─────────────────────────────────────────────────
|
|
3220
|
-
case 'ckg_inspect': {
|
|
3221
|
-
const { instance_id, concept, max_hops = 2 } = args;
|
|
3222
|
-
const redis = await getConnection(instance_id);
|
|
3223
|
-
const conceptId = ckgSlug(concept);
|
|
3224
|
-
const visited = new Set();
|
|
3225
|
-
const allEdges = [];
|
|
3226
|
-
// BFS traversal of CKG
|
|
3227
|
-
const queue = [{ id: conceptId, hop: 0 }];
|
|
3228
|
-
while (queue.length > 0) {
|
|
3229
|
-
const { id, hop } = queue.shift();
|
|
3230
|
-
if (visited.has(id) || hop > max_hops)
|
|
3231
|
-
continue;
|
|
3232
|
-
visited.add(id);
|
|
3233
|
-
const fromKeys = await redis.smembers(`cachly:ckg:idx:from:${id}`);
|
|
3234
|
-
const toKeys = await redis.smembers(`cachly:ckg:idx:to:${id}`);
|
|
3235
|
-
for (const ek of [...fromKeys, ...toKeys].slice(0, 50)) {
|
|
3236
|
-
const raw = await redis.get(ek);
|
|
3237
|
-
if (!raw)
|
|
3238
|
-
continue;
|
|
3239
|
-
const edge = JSON.parse(raw);
|
|
3240
|
-
allEdges.push(edge);
|
|
3241
|
-
if (hop < max_hops) {
|
|
3242
|
-
if (!visited.has(edge.from))
|
|
3243
|
-
queue.push({ id: edge.from, hop: hop + 1 });
|
|
3244
|
-
if (!visited.has(edge.to))
|
|
3245
|
-
queue.push({ id: edge.to, hop: hop + 1 });
|
|
3246
|
-
}
|
|
3247
|
-
}
|
|
3248
|
-
}
|
|
3249
|
-
if (allEdges.length === 0) {
|
|
3250
|
-
// Try fuzzy: scan for nodes matching the concept as substring
|
|
3251
|
-
const nodeKeys = [];
|
|
3252
|
-
const nStream = redis.scanStream({ match: `cachly:ckg:node:*${conceptId}*`, count: 100 });
|
|
3253
|
-
await new Promise((res, rej) => { nStream.on('data', (b) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
|
|
3254
|
-
if (nodeKeys.length === 0) {
|
|
3255
|
-
return [`🕸️ **CKG Inspect: "${concept}"**`, '', `No CKG nodes found. The graph builds automatically as you call \`learn_from_attempts\`.`, '', `💡 Once you have lessons stored, CKG edges will appear here.`].join('\n');
|
|
3256
|
-
}
|
|
3257
|
-
const nodeList = nodeKeys.slice(0, 10).map(k => ` • \`${k.replace('cachly:ckg:node:', '')}\``).join('\n');
|
|
3258
|
-
return [`🕸️ **CKG Inspect: "${concept}"**`, '', `No edges found for \`${conceptId}\`, but found similar nodes:`, nodeList, '', `Try: \`ckg_inspect(concept="<exact-node-id>")\``].join('\n');
|
|
3259
|
-
}
|
|
3260
|
-
// Sort by confidence desc, deduplicate
|
|
3261
|
-
const edgeSeen = new Set();
|
|
3262
|
-
const unique = allEdges.filter(e => {
|
|
3263
|
-
const k = `${e.from}:${e.edgeType}:${e.to}`;
|
|
3264
|
-
if (edgeSeen.has(k))
|
|
3265
|
-
return false;
|
|
3266
|
-
edgeSeen.add(k);
|
|
3267
|
-
return true;
|
|
3268
|
-
}).sort((a, b) => b.confidence - a.confidence);
|
|
3269
|
-
const EDGE_ICON = { fixes: '🔧', requires: '🔗', 'co-occurs': '🔄', causes: '⚡', contradicts: '⚠️', degrades_under: '📉' };
|
|
3270
|
-
const lines = [`🕸️ **CKG Inspect: "${concept}"** (${unique.length} edge${unique.length !== 1 ? 's' : ''}, ${visited.size} node${visited.size !== 1 ? 's' : ''} traversed)\n`];
|
|
3271
|
-
// Group by edge type
|
|
3272
|
-
const byType = new Map();
|
|
3273
|
-
for (const e of unique) {
|
|
3274
|
-
if (!byType.has(e.edgeType))
|
|
3275
|
-
byType.set(e.edgeType, []);
|
|
3276
|
-
byType.get(e.edgeType).push(e);
|
|
3277
|
-
}
|
|
3278
|
-
for (const [eType, edges] of byType) {
|
|
3279
|
-
const icon = EDGE_ICON[eType] ?? '→';
|
|
3280
|
-
lines.push(`**${icon} ${eType}** (${edges.length})`);
|
|
3281
|
-
for (const e of edges.slice(0, 8)) {
|
|
3282
|
-
const confPct = Math.round(e.confidence * 100);
|
|
3283
|
-
const bar = '▓'.repeat(Math.round(confPct / 10)) + '░'.repeat(10 - Math.round(confPct / 10));
|
|
3284
|
-
lines.push(` \`${e.from}\` → \`${e.to}\` ${bar} ${confPct}% (${e.successes.toFixed(1)}/${e.trials} trials)`);
|
|
3285
|
-
}
|
|
3286
|
-
lines.push('');
|
|
3287
|
-
}
|
|
3288
|
-
lines.push(`💡 Expand: \`ckg_inspect(concept="${concept}", max_hops=3)\` | Predict: \`brain_predict(context="${concept}")\``);
|
|
3289
|
-
return lines.join('\n');
|
|
3290
|
-
}
|
|
3291
|
-
// ── Layer 4: brain_predict (PPE) ─────────────────────────────────────────
|
|
3292
|
-
case 'brain_predict': {
|
|
3293
|
-
const { instance_id, context: ctx, top_k = 5 } = args;
|
|
3294
|
-
const redis = await getConnection(instance_id);
|
|
3295
|
-
const ctxTokens = ctx.toLowerCase().replace(/[^a-z0-9\s\-_:]/g, ' ').split(/\s+/).filter(t => t.length > 2);
|
|
3296
|
-
const predictions = [];
|
|
3297
|
-
for (const token of ctxTokens.slice(0, 6)) {
|
|
3298
|
-
const nodeKeys = [];
|
|
3299
|
-
const nStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 50 });
|
|
3300
|
-
await new Promise((res, rej) => { nStream.on('data', (b) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
|
|
3301
|
-
for (const nk of nodeKeys.slice(0, 5)) {
|
|
3302
|
-
const nodeRaw = await redis.get(nk);
|
|
3303
|
-
if (!nodeRaw)
|
|
3304
|
-
continue;
|
|
3305
|
-
const node = JSON.parse(nodeRaw);
|
|
3306
|
-
const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
3307
|
-
for (const ek of edgeKeys.slice(0, 20)) {
|
|
3308
|
-
const edgeRaw = await redis.get(ek);
|
|
3309
|
-
if (!edgeRaw)
|
|
3310
|
-
continue;
|
|
3311
|
-
const edge = JSON.parse(edgeRaw);
|
|
3312
|
-
// Only interested in fixes and co-occurs for prediction
|
|
3313
|
-
if (edge.edgeType !== 'fixes' && edge.edgeType !== 'co-occurs' && edge.edgeType !== 'causes')
|
|
3314
|
-
continue;
|
|
3315
|
-
const lessonRaw = await redis.get(`cachly:lesson:best:${edge.from}`);
|
|
3316
|
-
const lesson = lessonRaw ? JSON.parse(lessonRaw) : undefined;
|
|
3317
|
-
predictions.push({ concept: node.id, edgeType: edge.edgeType, target: edge.to, confidence: edge.confidence, lesson });
|
|
3318
|
-
}
|
|
3319
|
-
}
|
|
3320
|
-
}
|
|
3321
|
-
// Step 2: Text-based fallback — scan lessons for matching topics
|
|
3322
|
-
const textPredictions = [];
|
|
3323
|
-
const lessonKeys = [];
|
|
3324
|
-
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
3325
|
-
await new Promise((res, rej) => { lStream.on('data', (b) => lessonKeys.push(...b)); lStream.on('end', res); lStream.on('error', rej); });
|
|
3326
|
-
for (const k of lessonKeys) {
|
|
3327
|
-
const raw = await redis.get(k);
|
|
3328
|
-
if (!raw)
|
|
3329
|
-
continue;
|
|
3330
|
-
try {
|
|
3331
|
-
const l = JSON.parse(raw);
|
|
3332
|
-
const haystack = [l.topic, l.what_failed ?? '', l.what_worked ?? ''].join(' ').toLowerCase();
|
|
3333
|
-
const score = ctxTokens.reduce((s, t) => s + (haystack.includes(t) ? 1 : 0), 0);
|
|
3334
|
-
if (score >= 1 && l.outcome !== 'failure') {
|
|
3335
|
-
const conf = calculateConfidence(l);
|
|
3336
|
-
textPredictions.push({ ...l, confidence: conf });
|
|
3337
|
-
}
|
|
3338
|
-
}
|
|
3339
|
-
catch { /* skip */ }
|
|
3340
|
-
}
|
|
3341
|
-
textPredictions.sort((a, b) => b.confidence - a.confidence);
|
|
3342
|
-
if (predictions.length === 0 && textPredictions.length === 0) {
|
|
3343
|
-
return [
|
|
3344
|
-
`🔮 **Brain Predict: "${ctx}"**`,
|
|
3345
|
-
``,
|
|
3346
|
-
`No predictions yet — the brain hasn't seen this domain.`,
|
|
3347
|
-
``,
|
|
3348
|
-
`💡 As you solve problems in this area and call \`learn_from_attempts\`, the CKG builds up and predictions become available.`,
|
|
3349
|
-
].join('\n');
|
|
3350
|
-
}
|
|
3351
|
-
const lines = [`🔮 **Brain Predict: "${ctx}"**\n`];
|
|
3352
|
-
// CKG-based predictions
|
|
3353
|
-
if (predictions.length > 0) {
|
|
3354
|
-
const pSeen = new Set();
|
|
3355
|
-
const pUniq = predictions.filter(p => { const k = `${p.concept}:${p.edgeType}:${p.target}`; if (pSeen.has(k))
|
|
3356
|
-
return false; pSeen.add(k); return true; })
|
|
3357
|
-
.sort((a, b) => b.confidence - a.confidence).slice(0, top_k);
|
|
3358
|
-
lines.push(`### 🕸️ CKG Predictions (based on ${pUniq.length} known edges)`);
|
|
3359
|
-
for (const p of pUniq) {
|
|
3360
|
-
const confPct = Math.round(p.confidence * 100);
|
|
3361
|
-
const icon = p.edgeType === 'fixes' ? '🔧' : p.edgeType === 'co-occurs' ? '🔄' : '⚡';
|
|
3362
|
-
lines.push(`${icon} **${confPct}%** \`${p.concept}\` _${p.edgeType}_ \`${p.target}\``);
|
|
3363
|
-
if (p.lesson?.what_worked)
|
|
3364
|
-
lines.push(` ✅ ${p.lesson.what_worked.slice(0, 120)}`);
|
|
3365
|
-
}
|
|
3366
|
-
lines.push('');
|
|
3367
|
-
}
|
|
3368
|
-
// Text-based lesson predictions
|
|
3369
|
-
if (textPredictions.length > 0) {
|
|
3370
|
-
lines.push(`### 📚 Relevant Lessons (${Math.min(textPredictions.length, top_k)} pre-loaded)`);
|
|
3371
|
-
for (const l of textPredictions.slice(0, top_k)) {
|
|
3372
|
-
const confPct = Math.round(l.confidence * 100);
|
|
3373
|
-
lines.push(` ✅ **${confPct}%** \`${l.topic}\` — ${(l.what_worked ?? '').slice(0, 120)}`);
|
|
3374
|
-
}
|
|
3375
|
-
lines.push('');
|
|
3376
|
-
}
|
|
3377
|
-
lines.push(`💡 Outcome confirmed? \`learn_from_attempts(topic="fix:...", outcome="success", ...)\` → improves future predictions`);
|
|
3378
|
-
return lines.join('\n');
|
|
3379
|
-
}
|
|
3380
|
-
// ── Layer 3: MADC ─────────────────────────────────────────────────────────
|
|
3381
|
-
case 'madc_deliberate': {
|
|
3382
|
-
const { instance_id, topic } = args;
|
|
3383
|
-
const redis = await getConnection(instance_id);
|
|
3384
|
-
const historyRaw = await redis.lrange(`cachly:lessons:${topic}`, 0, -1);
|
|
3385
|
-
const history = historyRaw.map(r => { try {
|
|
3386
|
-
return JSON.parse(r);
|
|
3387
|
-
}
|
|
3388
|
-
catch {
|
|
3389
|
-
return null;
|
|
3390
|
-
} }).filter(Boolean);
|
|
3391
|
-
if (history.length < 2) {
|
|
3392
|
-
return [
|
|
3393
|
-
`🗳️ **MADC: "${topic}"**`, '',
|
|
3394
|
-
`Not enough history for deliberation (need ≥ 2 entries, found ${history.length}).`,
|
|
3395
|
-
'', `Call \`learn_from_attempts\` with conflicting outcomes to trigger deliberation.`,
|
|
3396
|
-
].join('\n');
|
|
3397
|
-
}
|
|
3398
|
-
// Specialist agents and their domain keywords
|
|
3399
|
-
const AGENTS = [
|
|
3400
|
-
{ name: 'InfraAgent', domains: ['infra', 'k8s', 'docker', 'server', 'wireguard', 'helm'] },
|
|
3401
|
-
{ name: 'AuthAgent', domains: ['auth', 'jwt', 'keycloak', 'oauth', 'oidc', 'token'] },
|
|
3402
|
-
{ name: 'DeployAgent', domains: ['deploy', 'ci', 'pipeline', 'rsync', 'release'] },
|
|
3403
|
-
{ name: 'DatabaseAgent', domains: ['db', 'gorm', 'migration', 'postgres', 'clickhouse', 'redis'] },
|
|
3404
|
-
{ name: 'DebugAgent', domains: ['debug', 'panic', 'race', 'nil', 'fix', 'error'] },
|
|
3405
|
-
{ name: 'APIAgent', domains: ['api', 'http', 'grpc', 'rest', 'fiber', 'web'] },
|
|
3406
|
-
];
|
|
3407
|
-
const topicDomain = topic.split(':')[0] ?? '';
|
|
3408
|
-
const relevantAgents = AGENTS.filter(a => a.domains.some(d => topicDomain === d || topic.includes(d)));
|
|
3409
|
-
const votingAgents = relevantAgents.length > 0 ? relevantAgents : AGENTS;
|
|
3410
|
-
// Measure each agent's CKG coverage in their domains
|
|
3411
|
-
const agentCoverage = new Map();
|
|
3412
|
-
for (const agent of votingAgents) {
|
|
3413
|
-
let edgeCount = 0;
|
|
3414
|
-
for (const domain of agent.domains) {
|
|
3415
|
-
const nodeKeys = [];
|
|
3416
|
-
const nStream = redis.scanStream({ match: `cachly:ckg:node:${domain}*`, count: 50 });
|
|
3417
|
-
await new Promise((res, rej) => { nStream.on('data', (b) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
|
|
3418
|
-
edgeCount += nodeKeys.length;
|
|
3419
|
-
}
|
|
3420
|
-
agentCoverage.set(agent.name, edgeCount);
|
|
3421
|
-
}
|
|
3422
|
-
const successLessons = history.filter(l => l.outcome === 'success' || l.outcome === 'partial');
|
|
3423
|
-
const failureLessons = history.filter(l => l.outcome === 'failure');
|
|
3424
|
-
if (failureLessons.length === 0) {
|
|
3425
|
-
return [
|
|
3426
|
-
`🗳️ **MADC: "${topic}"**`, '',
|
|
3427
|
-
`No contradictions found — all ${successLessons.length} entries have non-failure outcomes.`,
|
|
3428
|
-
'', `Use \`ckg_inspect(concept="${ckgSlug(topic)}")\` to explore the confidence graph.`,
|
|
3429
|
-
].join('\n');
|
|
3430
|
-
}
|
|
3431
|
-
// Agent voting logic
|
|
3432
|
-
const votes = [];
|
|
3433
|
-
for (const agent of votingAgents) {
|
|
3434
|
-
const coverage = agentCoverage.get(agent.name) ?? 0;
|
|
3435
|
-
let vote;
|
|
3436
|
-
let reason;
|
|
3437
|
-
if (coverage < 2) {
|
|
3438
|
-
vote = 'abstain';
|
|
3439
|
-
reason = 'insufficient domain coverage';
|
|
3440
|
-
}
|
|
3441
|
-
else if (successLessons.length >= failureLessons.length * 2) {
|
|
3442
|
-
vote = 'success';
|
|
3443
|
-
reason = `${successLessons.length}/${history.length} entries confirm success`;
|
|
3444
|
-
}
|
|
3445
|
-
else if (failureLessons.length >= successLessons.length * 2) {
|
|
3446
|
-
vote = 'failure';
|
|
3447
|
-
reason = `${failureLessons.length}/${history.length} entries confirm failure`;
|
|
3448
|
-
}
|
|
3449
|
-
else {
|
|
3450
|
-
vote = 'abstain';
|
|
3451
|
-
reason = `contested (${successLessons.length} success vs ${failureLessons.length} failure)`;
|
|
3452
|
-
}
|
|
3453
|
-
votes.push({ agent: agent.name, vote, coverage, reason });
|
|
3454
|
-
}
|
|
3455
|
-
const successVotes = votes.filter(v => v.vote === 'success').length;
|
|
3456
|
-
const failureVotes = votes.filter(v => v.vote === 'failure').length;
|
|
3457
|
-
const abstainVotes = votes.filter(v => v.vote === 'abstain').length;
|
|
3458
|
-
let resolution;
|
|
3459
|
-
let resolutionText;
|
|
3460
|
-
if (successVotes > 0 && failureVotes === 0) {
|
|
3461
|
-
resolution = 'unanimous_success';
|
|
3462
|
-
resolutionText = `✅ **Unanimous: SUCCESS** — ${successVotes} agent(s) confirm, ${abstainVotes} abstain`;
|
|
3463
|
-
}
|
|
3464
|
-
else if (failureVotes > 0 && successVotes === 0) {
|
|
3465
|
-
resolution = 'unanimous_failure';
|
|
3466
|
-
resolutionText = `❌ **Unanimous: FAILURE** — ${failureVotes} agent(s) confirm, ${abstainVotes} abstain`;
|
|
3467
|
-
}
|
|
3468
|
-
else {
|
|
3469
|
-
resolution = 'contested';
|
|
3470
|
-
resolutionText = `⚠️ **CONTESTED** — ${successVotes} success votes, ${failureVotes} failure votes, ${abstainVotes} abstain`;
|
|
3471
|
-
}
|
|
3472
|
-
// Store resolution as CKG node
|
|
3473
|
-
const resNodeId = ckgSlug(`resolution:${topic}`);
|
|
3474
|
-
const resNode = {
|
|
3475
|
-
id: resNodeId, domain: 'resolution', type: 'resolution', count: 1,
|
|
3476
|
-
ts: new Date().toISOString(), resolution, topic,
|
|
3477
|
-
votes: { success: successVotes, failure: failureVotes, abstain: abstainVotes },
|
|
3478
|
-
};
|
|
3479
|
-
await redis.set(`cachly:ckg:node:${resNodeId}`, JSON.stringify(resNode));
|
|
3480
|
-
// Write contradicts edge if contested; decay loser confidence to 0.1 if unanimous
|
|
3481
|
-
if (resolution === 'contested') {
|
|
3482
|
-
await ckgUpdateEdge(redis, ckgSlug(topic), 'contradicts', resNodeId, false);
|
|
3483
|
-
}
|
|
3484
|
-
else {
|
|
3485
|
-
// Unanimous — demote the losing side's edges to near-zero confidence
|
|
3486
|
-
const loserOutcome = resolution === 'unanimous_success' ? 'failure' : 'success';
|
|
3487
|
-
const loserLessons = (loserOutcome === 'failure' ? failureLessons : successLessons);
|
|
3488
|
-
if (loserLessons.length > 0) {
|
|
3489
|
-
// Decay all 'fixes' edges from this concept that contradict the resolution
|
|
3490
|
-
const fromKeys = await redis.smembers(`cachly:ckg:idx:from:${ckgSlug(topic)}`);
|
|
3491
|
-
for (const fk of fromKeys) {
|
|
3492
|
-
const er = await redis.get(fk);
|
|
3493
|
-
if (!er)
|
|
3494
|
-
continue;
|
|
3495
|
-
try {
|
|
3496
|
-
const edge = JSON.parse(er);
|
|
3497
|
-
if (edge.edgeType === 'fixes' && edge.confidence > 0.15) {
|
|
3498
|
-
if (resolution === 'unanimous_failure') {
|
|
3499
|
-
// Fix was wrong — decay to 0.1
|
|
3500
|
-
edge.confidence = 0.1;
|
|
3501
|
-
edge.last_updated = new Date().toISOString();
|
|
3502
|
-
await redis.set(fk, JSON.stringify(edge));
|
|
3503
|
-
}
|
|
3504
|
-
}
|
|
3505
|
-
}
|
|
3506
|
-
catch { /* skip corrupt edges */ }
|
|
3507
|
-
}
|
|
3508
|
-
}
|
|
3509
|
-
// Remove the conflict marker — deliberation resolved it
|
|
3510
|
-
await redis.del(`cachly:ckg:conflict:${ckgSlug(topic)}`);
|
|
3511
|
-
}
|
|
3512
|
-
const lines = [
|
|
3513
|
-
`🗳️ **MADC Deliberation: "${topic}"**`, '',
|
|
3514
|
-
`📊 Evidence: ${successLessons.length} success/partial vs ${failureLessons.length} failure entries (${history.length} total)`,
|
|
3515
|
-
'', `**Voting agents (${votingAgents.length}):**`,
|
|
3516
|
-
...votes.map(v => {
|
|
3517
|
-
const icon = v.vote === 'success' ? '✅' : v.vote === 'failure' ? '❌' : '⬜';
|
|
3518
|
-
const covBar = '▓'.repeat(Math.min(v.coverage, 5)) + '░'.repeat(Math.max(0, 5 - v.coverage));
|
|
3519
|
-
return ` ${icon} **${v.agent}** [${covBar}] ${v.coverage} CKG edges — ${v.reason}`;
|
|
3520
|
-
}),
|
|
3521
|
-
'', resolutionText, '',
|
|
3522
|
-
];
|
|
3523
|
-
if (resolution === 'unanimous_success') {
|
|
3524
|
-
lines.push(`🔧 Failure entries superseded — store confirmed lesson: \`learn_from_attempts(topic="${topic}", outcome="success", ...)\``);
|
|
3525
|
-
}
|
|
3526
|
-
else if (resolution === 'unanimous_failure') {
|
|
3527
|
-
lines.push(`🚫 Success claims unconfirmed — re-verify: \`recall_best_solution(topic="${topic}")\``);
|
|
3528
|
-
}
|
|
3529
|
-
else {
|
|
3530
|
-
lines.push(`⚠️ Contested — run \`causal_trace\` before acting. Explore: \`ckg_inspect(concept="${ckgSlug(topic)}")\``);
|
|
3531
|
-
}
|
|
3532
|
-
lines.push('', `📝 Resolution node: \`cachly:ckg:node:${resNodeId}\``);
|
|
3533
|
-
return lines.join('\n');
|
|
3534
|
-
}
|
|
3535
|
-
// ── Layer 5: CLS — cls_ingest ─────────────────────────────────────────────
|
|
3536
|
-
case 'cls_ingest': {
|
|
3537
|
-
const { instance_id, source, payload } = args;
|
|
3538
|
-
const redis = await getConnection(instance_id);
|
|
3539
|
-
const ts = new Date().toISOString();
|
|
3540
|
-
const clsKey = 'cachly:cls:events';
|
|
3541
|
-
if (source === 'git_commit') {
|
|
3542
|
-
const message = String(payload.message ?? '');
|
|
3543
|
-
const sha = String(payload.sha ?? '');
|
|
3544
|
-
const files = (Array.isArray(payload.files) ? payload.files : []);
|
|
3545
|
-
const domain = /^fix/i.test(message) ? 'fix' : /^feat/i.test(message) ? 'feat' : /^refactor/i.test(message) ? 'refactor' : /^test/i.test(message) ? 'test' : 'commit';
|
|
3546
|
-
const slug = `${domain}:${ckgSlug(message.slice(0, 60))}`;
|
|
3547
|
-
const conceptId = ckgSlug(slug);
|
|
3548
|
-
await ckgUpsertNode(redis, conceptId, domain, 'commit');
|
|
3549
|
-
for (const f of files.slice(0, 10)) {
|
|
3550
|
-
const fd = f.includes('auth') ? 'auth' : f.includes('api') ? 'api' : f.includes('infra') ? 'infra' : f.includes('web') ? 'web' : 'code';
|
|
3551
|
-
const fileId = ckgSlug(`file:${fd}`);
|
|
3552
|
-
await ckgUpsertNode(redis, fileId, 'file', fd);
|
|
3553
|
-
await ckgUpdateEdge(redis, conceptId, 'co-occurs', fileId, true);
|
|
3554
|
-
}
|
|
3555
|
-
const lessonObj = {
|
|
3556
|
-
topic: slug, outcome: 'success', what_worked: message, what_failed: '',
|
|
3557
|
-
context: `CLS/git: sha=${sha}`, severity: 'minor',
|
|
3558
|
-
file_paths: files.slice(0, 10), commands: sha ? [`git show ${sha}`] : [],
|
|
3559
|
-
tags: ['cls', 'git'], depends_on: [], recall_count: 0, ts, verified_at: ts,
|
|
3560
|
-
confidence: 0.6, audit_trail: [{ ts, action: 'cls_git_commit' }], version: 3,
|
|
3561
|
-
};
|
|
3562
|
-
await redis.rpush(`cachly:lessons:${slug}`, JSON.stringify(lessonObj));
|
|
3563
|
-
const existing = await redis.get(`cachly:lesson:best:${slug}`);
|
|
3564
|
-
if (!existing)
|
|
3565
|
-
await redis.set(`cachly:lesson:best:${slug}`, JSON.stringify(lessonObj));
|
|
3566
|
-
await redis.rpush(clsKey, JSON.stringify({ source, payload: { message, sha }, ts }));
|
|
3567
|
-
await redis.ltrim(clsKey, -200, -1);
|
|
3568
|
-
return [
|
|
3569
|
-
`📨 **CLS Ingested: git_commit**`, '',
|
|
3570
|
-
`Commit \`${sha.slice(0, 8) || '?'}\`: ${message.slice(0, 80)}`,
|
|
3571
|
-
`Concept: \`${conceptId}\` (${domain}) · Files: ${files.length}`,
|
|
3572
|
-
'', `🕸️ CKG: \`${conceptId}\` + ${files.length} file edges · Lesson: \`${slug}\``,
|
|
3573
|
-
`💡 Inspect: \`ckg_inspect(concept="${domain}")\``,
|
|
3574
|
-
].join('\n');
|
|
3575
|
-
}
|
|
3576
|
-
if (source === 'ci_outcome') {
|
|
3577
|
-
const status = String(payload.status ?? '');
|
|
3578
|
-
const prev_status = String(payload.prev_status ?? '');
|
|
3579
|
-
const job = String(payload.job ?? 'unknown');
|
|
3580
|
-
const ciCtx = String(payload.context ?? '');
|
|
3581
|
-
const isFixed = ['failure', 'red', 'error'].includes(prev_status) && ['success', 'green', 'passed'].includes(status);
|
|
3582
|
-
const isBroken = ['success', 'green', 'passed'].includes(prev_status) && ['failure', 'red', 'error'].includes(status);
|
|
3583
|
-
const slug = `ci:${ckgSlug(job)}`;
|
|
3584
|
-
const conceptId = ckgSlug(slug);
|
|
3585
|
-
await ckgUpsertNode(redis, conceptId, 'ci', 'job');
|
|
3586
|
-
if (isFixed) {
|
|
3587
|
-
const problemId = ckgSlug(`problem:${ckgSlug(job)}`);
|
|
3588
|
-
await ckgUpsertNode(redis, problemId, 'problem', 'ci-failure');
|
|
3589
|
-
await ckgUpdateEdge(redis, conceptId, 'fixes', problemId, true);
|
|
3590
|
-
const lessonObj = {
|
|
3591
|
-
topic: slug, outcome: 'success',
|
|
3592
|
-
what_worked: `CI job "${job}" went ${prev_status} → ${status}`,
|
|
3593
|
-
what_failed: `Job "${job}" was failing`, context: `CLS/ci: ${ciCtx}`,
|
|
3594
|
-
severity: 'major', file_paths: [], commands: [], tags: ['cls', 'ci'],
|
|
3595
|
-
depends_on: [], recall_count: 0, ts, verified_at: ts, confidence: 0.75,
|
|
3596
|
-
audit_trail: [{ ts, action: 'cls_ci_fixed' }], version: 3,
|
|
3597
|
-
};
|
|
3598
|
-
await redis.rpush(`cachly:lessons:${slug}`, JSON.stringify(lessonObj));
|
|
3599
|
-
await redis.set(`cachly:lesson:best:${slug}`, JSON.stringify(lessonObj));
|
|
3600
|
-
}
|
|
3601
|
-
else if (isBroken) {
|
|
3602
|
-
const causeId = ckgSlug(`cause:${ckgSlug(job)}`);
|
|
3603
|
-
await ckgUpsertNode(redis, causeId, 'cause', 'ci-break');
|
|
3604
|
-
await ckgUpdateEdge(redis, conceptId, 'causes', causeId, false);
|
|
3605
|
-
}
|
|
3606
|
-
await redis.rpush(clsKey, JSON.stringify({ source, payload: { status, prev_status, job }, ts }));
|
|
3607
|
-
await redis.ltrim(clsKey, -200, -1);
|
|
3608
|
-
const statusIcon = isFixed ? '✅ Fixed' : isBroken ? '🔴 Broken' : '📊 Recorded';
|
|
3609
|
-
return [
|
|
3610
|
-
`📨 **CLS Ingested: ci_outcome**`, '',
|
|
3611
|
-
`${statusIcon}: \`${job}\` — ${prev_status || '?'} → ${status}`,
|
|
3612
|
-
isFixed ? `🔧 CKG \`fixes\` edge added (75% confidence)` : isBroken ? `⚡ CKG \`causes\` edge added` : `📊 State recorded`,
|
|
3613
|
-
`💡 Lesson: \`${slug}\` | Predict: \`brain_predict(context="${job}")\``,
|
|
3614
|
-
].join('\n');
|
|
3615
|
-
}
|
|
3616
|
-
if (source === 'ide_diagnostic') {
|
|
3617
|
-
const error = String(payload.error ?? '');
|
|
3618
|
-
const fix = String(payload.fix ?? '');
|
|
3619
|
-
const file = String(payload.file ?? '');
|
|
3620
|
-
const errorConcept = extractProblemConcept(error) ?? 'unknown-error';
|
|
3621
|
-
const slug = `debug:${ckgSlug(errorConcept)}`;
|
|
3622
|
-
const conceptId = ckgSlug(slug);
|
|
3623
|
-
const problemId = ckgSlug(`problem:${errorConcept}`);
|
|
3624
|
-
await ckgUpsertNode(redis, conceptId, 'debug', 'diagnostic');
|
|
3625
|
-
await ckgUpsertNode(redis, problemId, 'problem', 'compiler-error');
|
|
3626
|
-
await ckgUpdateEdge(redis, conceptId, 'fixes', problemId, true);
|
|
3627
|
-
const lessonObj = {
|
|
3628
|
-
topic: slug, outcome: 'success', what_worked: fix, what_failed: error,
|
|
3629
|
-
context: `CLS/ide: ${file}`, severity: 'minor',
|
|
3630
|
-
file_paths: file ? [file] : [], commands: [], tags: ['cls', 'ide-diagnostic'],
|
|
3631
|
-
depends_on: [], recall_count: 0, ts, verified_at: ts, confidence: 0.65,
|
|
3632
|
-
audit_trail: [{ ts, action: 'cls_ide_diagnostic' }], version: 3,
|
|
3633
|
-
};
|
|
3634
|
-
await redis.rpush(`cachly:lessons:${slug}`, JSON.stringify(lessonObj));
|
|
3635
|
-
const existingL = await redis.get(`cachly:lesson:best:${slug}`);
|
|
3636
|
-
if (!existingL)
|
|
3637
|
-
await redis.set(`cachly:lesson:best:${slug}`, JSON.stringify(lessonObj));
|
|
3638
|
-
await redis.rpush(clsKey, JSON.stringify({ source, payload: { error: error.slice(0, 60), fix: fix.slice(0, 60), file }, ts }));
|
|
3639
|
-
await redis.ltrim(clsKey, -200, -1);
|
|
3640
|
-
return [
|
|
3641
|
-
`📨 **CLS Ingested: ide_diagnostic**`, '',
|
|
3642
|
-
`Error: \`${error.slice(0, 80)}\``,
|
|
3643
|
-
`Fix: ${fix.slice(0, 100)}`,
|
|
3644
|
-
file ? `File: \`${file}\`` : '',
|
|
3645
|
-
'', `🕸️ CKG: \`${conceptId}\` → fixes → \`${problemId}\` | Lesson: \`${slug}\``,
|
|
3646
|
-
].filter(l => l !== '').join('\n');
|
|
3647
|
-
}
|
|
3648
|
-
return `❌ Unknown CLS source: "${source}". Valid: git_commit, ci_outcome, ide_diagnostic`;
|
|
3649
|
-
}
|
|
3650
|
-
// ── Layer 5: CLS — cls_install_hooks ─────────────────────────────────────
|
|
3651
|
-
case 'cls_install_hooks': {
|
|
3652
|
-
const { instance_id, repo_path = '.', hooks = ['git', 'ci'] } = args;
|
|
3653
|
-
const hooksArr = Array.isArray(hooks) ? hooks : ['git', 'ci'];
|
|
3654
|
-
const lines = [`🔌 **CLS Hook Installation Guide**\n`];
|
|
3655
|
-
if (hooksArr.includes('git')) {
|
|
3656
|
-
const hookScript = [
|
|
3657
|
-
`#!/bin/sh`,
|
|
3658
|
-
`# cachly CLS — Continuous Learning Stream git hook`,
|
|
3659
|
-
`# Installed by cls_install_hooks · runs silently on every commit`,
|
|
3660
|
-
`INSTANCE="${instance_id}"`,
|
|
3661
|
-
`SHA=$(git rev-parse HEAD 2>/dev/null || echo "")`,
|
|
3662
|
-
`MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1)`,
|
|
3663
|
-
`FILES=$(git diff-tree --no-commit-id -r --name-only HEAD 2>/dev/null | tr '\\n' ',' | sed 's/,$//')`,
|
|
3664
|
-
`node -e "`,
|
|
3665
|
-
`const p={instance_id:'$INSTANCE',source:'git_commit',payload:{message:$(echo "$MSG" | jq -R . 2>/dev/null || echo '"commit"'),sha:'$SHA',files:'$FILES'.split(',').filter(Boolean)}};`,
|
|
3666
|
-
`try{require('child_process').execSync('npx @cachly-dev/mcp-server@latest cls-ingest \\''+JSON.stringify(p)+'\\'',{stdio:'ignore',timeout:5000})}catch(e){}`,
|
|
3667
|
-
`" 2>/dev/null &`,
|
|
3668
|
-
`exit 0`,
|
|
3669
|
-
].join('\n');
|
|
3670
|
-
lines.push(`### Git post-commit hook`);
|
|
3671
|
-
lines.push(`**Quick install (run once per repo):**`);
|
|
3672
|
-
lines.push('```sh');
|
|
3673
|
-
lines.push(`cat > ${repo_path}/.git/hooks/post-commit << 'HOOK'`);
|
|
3674
|
-
lines.push(hookScript);
|
|
3675
|
-
lines.push(`HOOK`);
|
|
3676
|
-
lines.push(`chmod +x ${repo_path}/.git/hooks/post-commit`);
|
|
3677
|
-
lines.push('```');
|
|
3678
|
-
lines.push(`After install: every \`git commit\` automatically updates your brain's CKG.`);
|
|
3679
|
-
lines.push('');
|
|
3680
|
-
}
|
|
3681
|
-
if (hooksArr.includes('ci')) {
|
|
3682
|
-
lines.push(`### GitHub Actions CI outcome hook`);
|
|
3683
|
-
lines.push(`**Add at the end of each job** (after build/test steps):`);
|
|
3684
|
-
lines.push('```yaml');
|
|
3685
|
-
lines.push(`- name: cachly CLS — record CI outcome`);
|
|
3686
|
-
lines.push(` if: always()`);
|
|
3687
|
-
lines.push(` run: |`);
|
|
3688
|
-
lines.push(` node -e "`);
|
|
3689
|
-
lines.push(` const r=require('https');`);
|
|
3690
|
-
lines.push(` const d=JSON.stringify({instance_id:'${instance_id}',source:'ci_outcome',payload:{`);
|
|
3691
|
-
lines.push(` status:'\${{ job.status }}',prev_status:'unknown',job:'\${{ github.job }}',`);
|
|
3692
|
-
lines.push(` context:'github-actions run \${{ github.run_number }}'}});`);
|
|
3693
|
-
lines.push(` r.request({hostname:'api.cachly.dev',path:'/api/v1/cls/ingest',method:'POST',`);
|
|
3694
|
-
lines.push(` headers:{'Content-Type':'application/json','Authorization':'Bearer \$CACHLY_JWT',`);
|
|
3695
|
-
lines.push(` 'Content-Length':d.length}},()=>{}).end(d);`);
|
|
3696
|
-
lines.push(` " 2>/dev/null || true`);
|
|
3697
|
-
lines.push(` env:`);
|
|
3698
|
-
lines.push(` CACHLY_JWT: \${{ secrets.CACHLY_JWT }}`);
|
|
3699
|
-
lines.push('```');
|
|
3700
|
-
lines.push('');
|
|
3701
|
-
}
|
|
3702
|
-
lines.push(`💡 Once installed: \`brain_search(query="cls")\` to verify events are arriving.`);
|
|
3703
|
-
lines.push(`📊 Monitor CKG growth: \`ckg_inspect(concept="ci")\` or \`ckg_inspect(concept="fix")\``);
|
|
3704
|
-
return lines.join('\n');
|
|
3705
|
-
}
|
|
3706
|
-
// ── Layer 6: FedBrain — fedbrain_contribute ───────────────────────────────
|
|
3707
|
-
case 'fedbrain_contribute': {
|
|
3708
|
-
const { instance_id, lesson_key, visibility = 'public' } = args;
|
|
3709
|
-
const redis = await getConnection(instance_id);
|
|
3710
|
-
const raw = await redis.get(`cachly:lesson:best:${lesson_key}`);
|
|
3711
|
-
if (!raw)
|
|
3712
|
-
return `❌ Lesson \`${lesson_key}\` not found. Store it first with \`learn_from_attempts\`.`;
|
|
3713
|
-
const lesson = JSON.parse(raw);
|
|
3714
|
-
const domainTokens = [lesson.topic.split(':')[0], ...(lesson.tags ?? [])].filter(Boolean);
|
|
3715
|
-
const domainFingerprint = [...new Set(domainTokens)].sort().join(',');
|
|
3716
|
-
// HMAC certificate ID (non-reversible, privacy-safe)
|
|
3717
|
-
const certContent = `${lesson.topic}:${lesson.outcome}:${lesson.what_worked}`;
|
|
3718
|
-
const certId = createHmac('sha256', `cachly-fedbrain:${instance_id}`).update(certContent).digest('hex').slice(0, 16);
|
|
3719
|
-
const cert = {
|
|
3720
|
-
cert_id: certId, lesson_key, visibility,
|
|
3721
|
-
domain_fingerprint: domainFingerprint,
|
|
3722
|
-
contributed_at: new Date().toISOString(),
|
|
3723
|
-
confirm_count: 0,
|
|
3724
|
-
trust_score: lesson.outcome === 'success' ? 0.85 : 0.5,
|
|
3725
|
-
};
|
|
3726
|
-
await redis.set(`cachly:fedbrain:cert:${certId}`, JSON.stringify(cert));
|
|
3727
|
-
await redis.sadd('cachly:fedbrain:contributed', certId);
|
|
3728
|
-
// Try global commons via syndication API
|
|
3729
|
-
let syndicationResult;
|
|
3730
|
-
try {
|
|
3731
|
-
await apiFetch('/api/v1/syndication/contribute', {
|
|
3732
|
-
method: 'POST',
|
|
3733
|
-
body: JSON.stringify({
|
|
3734
|
-
topic: lesson.topic, outcome: lesson.outcome,
|
|
3735
|
-
what_worked: lesson.what_worked, what_failed: lesson.what_failed ?? '',
|
|
3736
|
-
severity: lesson.severity ?? 'major', cert_id: certId,
|
|
3737
|
-
domain_fingerprint: domainFingerprint, visibility,
|
|
3738
|
-
}),
|
|
3739
|
-
});
|
|
3740
|
-
syndicationResult = `✅ Contributed to global commons`;
|
|
3741
|
-
}
|
|
3742
|
-
catch {
|
|
3743
|
-
syndicationResult = `📦 Stored locally (commons API unavailable — will sync when online)`;
|
|
3744
|
-
}
|
|
3745
|
-
return [
|
|
3746
|
-
`🌐 **FedBrain Contribute: "${lesson_key}"**`, '',
|
|
3747
|
-
`📜 Certificate: \`${certId}\``,
|
|
3748
|
-
`🏷️ Domain fingerprint: ${domainTokens.slice(0, 6).map(t => `\`${t}\``).join(', ')}`,
|
|
3749
|
-
`🔒 Visibility: ${visibility}`,
|
|
3750
|
-
'', syndicationResult, '',
|
|
3751
|
-
`💡 At 10 independent confirms → 🏆 Gold Standard`,
|
|
3752
|
-
`🔍 Search: \`fedbrain_search(query="${lesson.topic.split(':').slice(-1)[0]}")\``,
|
|
3753
|
-
].join('\n');
|
|
3754
|
-
}
|
|
3755
|
-
// ── Layer 6: FedBrain — fedbrain_search ──────────────────────────────────
|
|
3756
|
-
case 'fedbrain_search': {
|
|
3757
|
-
const { instance_id, query, context_hints = [], limit = 10 } = args;
|
|
3758
|
-
const redis = await getConnection(instance_id);
|
|
3759
|
-
// Build local domain context from contributed certificates + explicit hints
|
|
3760
|
-
const contribIds = await redis.smembers('cachly:fedbrain:contributed');
|
|
3761
|
-
const localDomains = new Map();
|
|
3762
|
-
for (const certId of contribIds.slice(0, 30)) {
|
|
3763
|
-
const certRaw = await redis.get(`cachly:fedbrain:cert:${certId}`);
|
|
3764
|
-
if (!certRaw)
|
|
3765
|
-
continue;
|
|
3766
|
-
try {
|
|
3767
|
-
const cert = JSON.parse(certRaw);
|
|
3768
|
-
for (const d of (cert.domain_fingerprint ?? '').split(',').filter(Boolean)) {
|
|
3769
|
-
localDomains.set(d, (localDomains.get(d) ?? 0) + 1);
|
|
3770
|
-
}
|
|
3771
|
-
}
|
|
3772
|
-
catch { /* skip */ }
|
|
3773
|
-
}
|
|
3774
|
-
for (const hint of (Array.isArray(context_hints) ? context_hints : [])) {
|
|
3775
|
-
localDomains.set(hint.toLowerCase(), (localDomains.get(hint.toLowerCase()) ?? 0) + 2);
|
|
3776
|
-
}
|
|
3777
|
-
let results = [];
|
|
3778
|
-
try {
|
|
3779
|
-
const params = new URLSearchParams({ q: query, limit: String(Math.min(limit * 2, 50)) });
|
|
3780
|
-
const res = await apiFetch(`/api/v1/syndication/search?${params}`);
|
|
3781
|
-
results = res.results ?? [];
|
|
3782
|
-
}
|
|
3783
|
-
catch {
|
|
3784
|
-
// Fallback: search local lessons
|
|
3785
|
-
const lessonKeys = [];
|
|
3786
|
-
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
3787
|
-
await new Promise((res, rej) => { lStream.on('data', (b) => lessonKeys.push(...b)); lStream.on('end', res); lStream.on('error', rej); });
|
|
3788
|
-
for (const k of lessonKeys.slice(0, 60)) {
|
|
3789
|
-
const r = await redis.get(k);
|
|
3790
|
-
if (!r)
|
|
3791
|
-
continue;
|
|
3792
|
-
try {
|
|
3793
|
-
const l = JSON.parse(r);
|
|
3794
|
-
const haystack = `${l.topic} ${l.what_worked ?? ''} ${l.what_failed ?? ''}`.toLowerCase();
|
|
3795
|
-
if (query.toLowerCase().split(/\s+/).some(t => t.length > 2 && haystack.includes(t))) {
|
|
3796
|
-
results.push({ id: k.split(':').pop() ?? k, topic: l.topic, category: l.topic.split(':')[0], outcome: l.outcome, what_worked: l.what_worked ?? '', severity: l.severity ?? 'major', confirm_count: 0, created_at: l.ts ?? new Date().toISOString() });
|
|
3797
|
-
}
|
|
3798
|
-
}
|
|
3799
|
-
catch { /* skip */ }
|
|
3800
|
-
}
|
|
3801
|
-
}
|
|
3802
|
-
if (results.length === 0) {
|
|
3803
|
-
return [`🌐 **FedBrain Search: "${query}"**`, '', `No results. Contribute: \`fedbrain_contribute(lesson_key="fix:...")\``].join('\n');
|
|
3804
|
-
}
|
|
3805
|
-
// Context-weighted ranking
|
|
3806
|
-
const ranked = results.map(r => {
|
|
3807
|
-
const rDomains = (r.domain_fingerprint ?? r.category ?? '').split(',').filter(Boolean);
|
|
3808
|
-
const overlap = rDomains.reduce((s, d) => s + (localDomains.get(d) ?? 0), 0);
|
|
3809
|
-
const contextScore = localDomains.size > 0 ? overlap / Math.max(1, localDomains.size + rDomains.length) : 0;
|
|
3810
|
-
const confirmedScore = Math.min(1, r.confirm_count / 10);
|
|
3811
|
-
const weightedScore = (contextScore * 0.4) + (confirmedScore * 0.4) + (r.outcome === 'success' ? 0.2 : 0);
|
|
3812
|
-
return { ...r, weightedScore, isGoldStandard: r.confirm_count >= 10 };
|
|
3813
|
-
}).sort((a, b) => b.weightedScore - a.weightedScore).slice(0, limit);
|
|
3814
|
-
const lines = [`🌐 **FedBrain Search: "${query}"** — ${ranked.length} result${ranked.length !== 1 ? 's' : ''} (context-weighted)\n`];
|
|
3815
|
-
for (const r of ranked) {
|
|
3816
|
-
const icon = r.outcome === 'success' ? '✅' : r.outcome === 'failure' ? '❌' : '⚠️';
|
|
3817
|
-
const goldBadge = r.isGoldStandard ? ' 🏆 _Gold Standard_' : r.confirm_count >= 3 ? ` ✓${r.confirm_count}` : '';
|
|
3818
|
-
const ctxPct = Math.round(r.weightedScore * 100);
|
|
3819
|
-
lines.push(`${icon}${goldBadge} **\`${r.topic}\`** [ctx: ${ctxPct}%]`);
|
|
3820
|
-
if (r.what_worked)
|
|
3821
|
-
lines.push(` ✅ ${r.what_worked.slice(0, 150)}`);
|
|
3822
|
-
if (r.what_failed)
|
|
3823
|
-
lines.push(` ❌ ${r.what_failed.slice(0, 80)}`);
|
|
3824
|
-
lines.push(` _${r.confirm_count} confirm${r.confirm_count !== 1 ? 's' : ''} | \`${r.id.slice(0, 12)}\`_`, '');
|
|
3825
|
-
}
|
|
3826
|
-
if (localDomains.size > 0) {
|
|
3827
|
-
const topDomains = [...localDomains.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([d, n]) => `\`${d}\`(${n})`).join(', ');
|
|
3828
|
-
lines.push(`🎯 Your context: ${topDomains}`);
|
|
3829
|
-
}
|
|
3830
|
-
lines.push(`💡 Confirm: \`fedbrain_confirm(topic="<topic>", outcome="worked")\``);
|
|
3831
|
-
return lines.join('\n');
|
|
3832
|
-
}
|
|
3833
|
-
// ── Layer 6: FedBrain — fedbrain_confirm ─────────────────────────────────
|
|
3834
|
-
case 'fedbrain_confirm': {
|
|
3835
|
-
const { instance_id, topic, outcome } = args;
|
|
3836
|
-
const redis = await getConnection(instance_id);
|
|
3837
|
-
const ts = new Date().toISOString();
|
|
3838
|
-
const confirmEntry = JSON.stringify({ topic, outcome, ts });
|
|
3839
|
-
await redis.rpush('cachly:fedbrain:confirmations', confirmEntry);
|
|
3840
|
-
await redis.ltrim('cachly:fedbrain:confirmations', -200, -1);
|
|
3841
|
-
// Update local CKG confidence
|
|
3842
|
-
const worked = outcome === 'worked';
|
|
3843
|
-
const partial = outcome === 'partially_worked';
|
|
3844
|
-
await ckgUpdateEdge(redis, ckgSlug(topic), 'fixes', ckgSlug(`syndicated:${topic}`), worked, partial);
|
|
3845
|
-
// Propagate to global commons
|
|
3846
|
-
let propResult;
|
|
3847
|
-
try {
|
|
3848
|
-
await apiFetch('/api/v1/syndication/confirm', { method: 'POST', body: JSON.stringify({ topic, outcome }) });
|
|
3849
|
-
propResult = `✅ Confirmation propagated to global commons`;
|
|
3850
|
-
}
|
|
3851
|
-
catch {
|
|
3852
|
-
await redis.rpush('cachly:fedbrain:pending_confirms', confirmEntry);
|
|
3853
|
-
await redis.ltrim('cachly:fedbrain:pending_confirms', -50, -1);
|
|
3854
|
-
propResult = `📦 Queued locally (API unavailable — will propagate on next online session)`;
|
|
3855
|
-
}
|
|
3856
|
-
const icon = worked ? '✅' : partial ? '⚠️' : '❌';
|
|
3857
|
-
return [
|
|
3858
|
-
`${icon} **FedBrain Confirm: "${topic}"** → ${outcome}`, '',
|
|
3859
|
-
propResult, '',
|
|
3860
|
-
`🕸️ CKG confidence ${worked || partial ? 'boosted' : 'reduced'} for \`${ckgSlug(topic)}\``,
|
|
3861
|
-
`💡 Your confirmation helps other brains worldwide.`,
|
|
3862
|
-
`📊 Status: \`fedbrain_status(instance_id="...")\``,
|
|
3863
|
-
].join('\n');
|
|
3864
|
-
}
|
|
3865
|
-
// ── Layer 6: FedBrain — fedbrain_status ──────────────────────────────────
|
|
3866
|
-
case 'fedbrain_status': {
|
|
3867
|
-
const { instance_id } = args;
|
|
3868
|
-
const redis = await getConnection(instance_id);
|
|
3869
|
-
const contribIds = await redis.smembers('cachly:fedbrain:contributed');
|
|
3870
|
-
const confirmsRaw = await redis.lrange('cachly:fedbrain:confirmations', -10, -1);
|
|
3871
|
-
const pendingConfirms = await redis.llen('cachly:fedbrain:pending_confirms');
|
|
3872
|
-
const confirms = confirmsRaw.map(r => { try {
|
|
3873
|
-
return JSON.parse(r);
|
|
3874
|
-
}
|
|
3875
|
-
catch {
|
|
3876
|
-
return null;
|
|
3877
|
-
} }).filter(Boolean);
|
|
3878
|
-
const certDetails = [];
|
|
3879
|
-
for (const certId of contribIds.slice(0, 15)) {
|
|
3880
|
-
const raw = await redis.get(`cachly:fedbrain:cert:${certId}`);
|
|
3881
|
-
if (!raw)
|
|
3882
|
-
continue;
|
|
3883
|
-
try {
|
|
3884
|
-
const cert = JSON.parse(raw);
|
|
3885
|
-
certDetails.push({ cert_id: cert.cert_id, lesson_key: cert.lesson_key, confirm_count: cert.confirm_count ?? 0, trust_score: cert.trust_score ?? 0.5, isGold: (cert.confirm_count ?? 0) >= 10 });
|
|
3886
|
-
}
|
|
3887
|
-
catch { /* skip */ }
|
|
3888
|
-
}
|
|
3889
|
-
const lines = [
|
|
3890
|
-
`🌐 **FedBrain Status**\n`,
|
|
3891
|
-
`### 📤 Contributed Lessons: ${contribIds.length}`,
|
|
3892
|
-
];
|
|
3893
|
-
if (certDetails.length > 0) {
|
|
3894
|
-
for (const c of certDetails) {
|
|
3895
|
-
const goldBadge = c.isGold ? ' 🏆 Gold Standard' : '';
|
|
3896
|
-
const confBar = '█'.repeat(Math.min(10, c.confirm_count)) + '░'.repeat(Math.max(0, 10 - c.confirm_count));
|
|
3897
|
-
lines.push(` \`${c.lesson_key}\` [${confBar}] ×${c.confirm_count}${goldBadge}`);
|
|
3898
|
-
}
|
|
3899
|
-
}
|
|
3900
|
-
else {
|
|
3901
|
-
lines.push(` _None yet. Contribute with \`fedbrain_contribute(lesson_key="fix:...")\`_`);
|
|
3902
|
-
}
|
|
3903
|
-
lines.push('', `### 📥 Recent Confirmations: ${confirms.length}`);
|
|
3904
|
-
if (confirms.length > 0) {
|
|
3905
|
-
for (const c of confirms.slice(-5)) {
|
|
3906
|
-
const icon = c.outcome === 'worked' ? '✅' : c.outcome === 'partially_worked' ? '⚠️' : '❌';
|
|
3907
|
-
lines.push(` ${icon} \`${c.topic}\` — ${c.outcome} (${new Date(c.ts).toLocaleDateString('de-DE')})`);
|
|
3908
|
-
}
|
|
3909
|
-
}
|
|
3910
|
-
else {
|
|
3911
|
-
lines.push(` _None yet. Confirm syndicated lessons with \`fedbrain_confirm\`_`);
|
|
3912
|
-
}
|
|
3913
|
-
if (pendingConfirms > 0) {
|
|
3914
|
-
lines.push('', `⚠️ ${pendingConfirms} confirmation${pendingConfirms !== 1 ? 's' : ''} pending propagation`);
|
|
3915
|
-
}
|
|
3916
|
-
lines.push('', '---', `**Contribute:** \`fedbrain_contribute(lesson_key="fix:...")\``, `**Search:** \`fedbrain_search(query="...")\``, `**Confirm:** \`fedbrain_confirm(topic="...", outcome="worked")\``);
|
|
3917
|
-
return lines.join('\n');
|
|
3918
|
-
}
|
|
3919
|
-
// ── Layer 6: FedBrain — brain_federate ───────────────────────────────────
|
|
3920
|
-
case 'brain_federate': {
|
|
3921
|
-
const { instance_id, source, domain, min_confidence = 0.6, dry_run = false } = args;
|
|
3922
|
-
if (instance_id === source)
|
|
3923
|
-
return `❌ Source and destination cannot be the same brain.`;
|
|
3924
|
-
const destRedis = await getConnection(instance_id);
|
|
3925
|
-
const srcRedis = await getConnection(source);
|
|
3926
|
-
const domainPattern = domain === '*' ? 'cachly:ckg:node:*' : `cachly:ckg:node:${domain}*`;
|
|
3927
|
-
// Scan source CKG nodes matching the domain
|
|
3928
|
-
const nodeKeys = [];
|
|
3929
|
-
const nStream = srcRedis.scanStream({ match: domainPattern, count: 100 });
|
|
3930
|
-
await new Promise((res, rej) => { nStream.on('data', (b) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
|
|
3931
|
-
if (nodeKeys.length === 0) {
|
|
3932
|
-
return [
|
|
3933
|
-
`🧠 **brain_federate: "${domain}"**`, '',
|
|
3934
|
-
`No CKG nodes found in source brain for domain \`${domain}\`.`,
|
|
3935
|
-
`The source brain may not have knowledge in this area yet.`,
|
|
3936
|
-
`Try: \`fedbrain_search(query="${domain}")\` to find global commons knowledge instead.`,
|
|
3937
|
-
].join('\n');
|
|
3938
|
-
}
|
|
3939
|
-
// Transfer nodes + their outgoing edges
|
|
3940
|
-
let nodesTransferred = 0;
|
|
3941
|
-
let edgesTransferred = 0;
|
|
3942
|
-
let lessonsTransferred = 0;
|
|
3943
|
-
let skippedLowConf = 0;
|
|
3944
|
-
const transferLog = [];
|
|
3945
|
-
for (const nk of nodeKeys) {
|
|
3946
|
-
const nodeRaw = await srcRedis.get(nk);
|
|
3947
|
-
if (!nodeRaw)
|
|
3948
|
-
continue;
|
|
3949
|
-
let node;
|
|
3950
|
-
try {
|
|
3951
|
-
node = JSON.parse(nodeRaw);
|
|
3952
|
-
}
|
|
3953
|
-
catch {
|
|
3954
|
-
continue;
|
|
3955
|
-
}
|
|
3956
|
-
if (!dry_run)
|
|
3957
|
-
await destRedis.set(nk, nodeRaw);
|
|
3958
|
-
nodesTransferred++;
|
|
3959
|
-
// Transfer outgoing edges for this node
|
|
3960
|
-
const edgeKeys = await srcRedis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
3961
|
-
for (const ek of edgeKeys) {
|
|
3962
|
-
const edgeRaw = await srcRedis.get(ek);
|
|
3963
|
-
if (!edgeRaw)
|
|
3964
|
-
continue;
|
|
3965
|
-
let edge;
|
|
3966
|
-
try {
|
|
3967
|
-
edge = JSON.parse(edgeRaw);
|
|
3968
|
-
}
|
|
3969
|
-
catch {
|
|
3970
|
-
continue;
|
|
3971
|
-
}
|
|
3972
|
-
if (edge.confidence < min_confidence) {
|
|
3973
|
-
skippedLowConf++;
|
|
3974
|
-
continue;
|
|
3975
|
-
}
|
|
3976
|
-
if (!dry_run) {
|
|
3977
|
-
await destRedis.set(ek, edgeRaw);
|
|
3978
|
-
await destRedis.sadd(`cachly:ckg:idx:from:${edge.from}`, ek);
|
|
3979
|
-
await destRedis.sadd(`cachly:ckg:idx:to:${edge.to}`, ek);
|
|
3980
|
-
}
|
|
3981
|
-
edgesTransferred++;
|
|
3982
|
-
}
|
|
3983
|
-
// Transfer best-lesson for the same topic slug
|
|
3984
|
-
const lessonKey = `cachly:lesson:best:${node.id}`;
|
|
3985
|
-
const lessonRaw = await srcRedis.get(lessonKey);
|
|
3986
|
-
if (lessonRaw) {
|
|
3987
|
-
if (!dry_run)
|
|
3988
|
-
await destRedis.set(lessonKey, lessonRaw);
|
|
3989
|
-
lessonsTransferred++;
|
|
3990
|
-
if (transferLog.length < 8) {
|
|
3991
|
-
try {
|
|
3992
|
-
const l = JSON.parse(lessonRaw);
|
|
3993
|
-
const icon = l.outcome === 'success' ? '✅' : l.outcome === 'failure' ? '❌' : '⚠️';
|
|
3994
|
-
transferLog.push(` ${icon} \`${l.topic}\` — ${(l.what_worked ?? '').slice(0, 80)}`);
|
|
3995
|
-
}
|
|
3996
|
-
catch { /* skip */ }
|
|
3997
|
-
}
|
|
3998
|
-
}
|
|
3999
|
-
}
|
|
4000
|
-
// Record federation provenance
|
|
4001
|
-
if (!dry_run) {
|
|
4002
|
-
const provEntry = JSON.stringify({
|
|
4003
|
-
source, domain, transferred_at: new Date().toISOString(),
|
|
4004
|
-
nodes: nodesTransferred, edges: edgesTransferred, lessons: lessonsTransferred,
|
|
4005
|
-
});
|
|
4006
|
-
await destRedis.rpush('cachly:fedbrain:federations', provEntry);
|
|
4007
|
-
await destRedis.ltrim('cachly:fedbrain:federations', -50, -1);
|
|
4008
|
-
}
|
|
4009
|
-
const dryTag = dry_run ? ' (DRY RUN — no writes)' : '';
|
|
4010
|
-
const lines = [
|
|
4011
|
-
`🧠 **brain_federate: "${domain}"**${dryTag}`, '',
|
|
4012
|
-
`📤 Source: \`${source}\``,
|
|
4013
|
-
`📥 Destination: \`${instance_id}\``,
|
|
4014
|
-
`🔍 Domain filter: \`${domain}\` | min_confidence: ${min_confidence}`,
|
|
4015
|
-
'',
|
|
4016
|
-
`### Transfer Summary`,
|
|
4017
|
-
` 🕸️ CKG nodes: ${nodesTransferred}`,
|
|
4018
|
-
` 🔗 CKG edges: ${edgesTransferred} (${skippedLowConf} skipped, confidence < ${min_confidence})`,
|
|
4019
|
-
` 📚 Lessons: ${lessonsTransferred}`,
|
|
4020
|
-
'',
|
|
4021
|
-
];
|
|
4022
|
-
if (transferLog.length > 0) {
|
|
4023
|
-
lines.push(`### Sample lessons transferred:`, ...transferLog);
|
|
4024
|
-
if (lessonsTransferred > transferLog.length)
|
|
4025
|
-
lines.push(` _... and ${lessonsTransferred - transferLog.length} more_`);
|
|
4026
|
-
lines.push('');
|
|
4027
|
-
}
|
|
4028
|
-
if (dry_run) {
|
|
4029
|
-
lines.push(`💡 Run without \`dry_run: true\` to apply the transfer.`);
|
|
4030
|
-
}
|
|
4031
|
-
else {
|
|
4032
|
-
lines.push(`✅ Transfer complete. Your brain now has the \`${domain}\` knowledge from \`${source}\`.`);
|
|
4033
|
-
lines.push(`🔍 Explore: \`ckg_inspect(concept="${domain}")\` | \`recall_best_solution(topic="${domain}:...")\``);
|
|
4034
|
-
}
|
|
4035
|
-
return lines.join('\n');
|
|
4036
|
-
}
|
|
4037
|
-
// ── crystal_view ──────────────────────────────────────────────────────────
|
|
4038
|
-
case 'crystal_view': {
|
|
4039
|
-
const { instance_id, show_raw = false } = args;
|
|
4040
|
-
const redis = await getConnection(instance_id);
|
|
4041
|
-
const raw = await redis.get('cachly:crystal:latest');
|
|
4042
|
-
if (!raw) {
|
|
4043
|
-
return [
|
|
4044
|
-
`💎 **Memory Crystal: not yet created**`, '',
|
|
4045
|
-
`No crystal found. Create one with \`memory_crystalize()\` to compress your accumulated wisdom.`,
|
|
4046
|
-
'', `💡 Tip: run \`memory_crystalize\` monthly for best results.`,
|
|
4047
|
-
].join('\n');
|
|
4048
|
-
}
|
|
4049
|
-
const crystal = JSON.parse(raw);
|
|
4050
|
-
const age = Math.floor((Date.now() - new Date(crystal.ts).getTime()) / 86400000);
|
|
4051
|
-
const freshEmoji = age <= 7 ? '🟢' : age <= 30 ? '🟡' : '🔴';
|
|
4052
|
-
const lines = [
|
|
4053
|
-
`💎 **Memory Crystal: ${crystal.label}**`, '',
|
|
4054
|
-
`📅 Created: ${new Date(crystal.ts).toLocaleDateString('de-DE')} (${age}d ago ${freshEmoji})`,
|
|
4055
|
-
`📊 Compressed from: ${crystal.created_from}`,
|
|
4056
|
-
`🗂️ Categories: ${crystal.categories.slice(0, 10).map(c => `\`${c}\``).join(', ')}${crystal.categories.length > 10 ? ` +${crystal.categories.length - 10} more` : ''}`,
|
|
4057
|
-
'',
|
|
4058
|
-
`**🔑 Top patterns (${crystal.top_patterns.length}):**`,
|
|
4059
|
-
];
|
|
4060
|
-
for (const p of crystal.top_patterns) {
|
|
4061
|
-
lines.push(` • **${p.category}** (${p.count}×): ${p.insight.slice(0, 110)}`);
|
|
4062
|
-
}
|
|
4063
|
-
if (age > 30) {
|
|
4064
|
-
lines.push('', `⚠️ Crystal is ${age}d old — run \`memory_crystalize()\` to refresh it.`);
|
|
4065
|
-
}
|
|
4066
|
-
if (show_raw) {
|
|
4067
|
-
lines.push('', '```json', JSON.stringify(crystal, null, 2), '```');
|
|
4068
|
-
}
|
|
4069
|
-
lines.push('', `💡 Refresh: \`memory_crystalize()\` | Recover: \`compact_recover(instance_id="...")\``);
|
|
4070
|
-
return lines.join('\n');
|
|
4071
|
-
}
|
|
4072
|
-
// ── compact_recover ───────────────────────────────────────────────────────
|
|
4073
|
-
case 'compact_recover': {
|
|
4074
|
-
const { instance_id, focus = '' } = args;
|
|
4075
|
-
const redis = await getConnection(instance_id);
|
|
4076
|
-
const lines = [`🔁 **Compact Recovery Briefing**\n`];
|
|
4077
|
-
lines.push(`> *Call this first after any context limit hit. Reconstructs where you left off.*\n`);
|
|
4078
|
-
// 1. Memory Crystal
|
|
4079
|
-
const crystalRaw = await redis.get('cachly:crystal:latest');
|
|
4080
|
-
if (crystalRaw) {
|
|
4081
|
-
const crystal = JSON.parse(crystalRaw);
|
|
4082
|
-
lines.push(`### 💎 Memory Crystal: ${crystal.label}`);
|
|
4083
|
-
lines.push(`Compressed from ${crystal.session_count} sessions, ${crystal.lesson_count} lessons.`);
|
|
4084
|
-
const topN = focus
|
|
4085
|
-
? crystal.top_patterns.filter(p => p.category.toLowerCase().includes(focus.toLowerCase()) || p.insight.toLowerCase().includes(focus.toLowerCase())).slice(0, 4)
|
|
4086
|
-
: crystal.top_patterns.slice(0, 4);
|
|
4087
|
-
for (const p of topN)
|
|
4088
|
-
lines.push(` • **${p.category}**: ${p.insight.slice(0, 100)}`);
|
|
4089
|
-
lines.push('');
|
|
4090
|
-
}
|
|
4091
|
-
// 2. Last session summary
|
|
4092
|
-
const lastSession = await redis.get('cachly:session:last');
|
|
4093
|
-
if (lastSession) {
|
|
4094
|
-
const sess = JSON.parse(lastSession);
|
|
4095
|
-
lines.push(`### 🕐 Last Session`);
|
|
4096
|
-
if (sess.focus)
|
|
4097
|
-
lines.push(`Focus: _${sess.focus}_`);
|
|
4098
|
-
if (sess.summary)
|
|
4099
|
-
lines.push(`Summary: ${sess.summary.slice(0, 300)}`);
|
|
4100
|
-
lines.push('');
|
|
4101
|
-
}
|
|
4102
|
-
// 3. Session handoff
|
|
4103
|
-
const handoff = await redis.get('cachly:session:handoff');
|
|
4104
|
-
if (handoff) {
|
|
4105
|
-
const h = JSON.parse(handoff);
|
|
4106
|
-
lines.push(`### 📋 Handoff (from last window)`);
|
|
4107
|
-
if (h.context_summary)
|
|
4108
|
-
lines.push(`Context: ${h.context_summary.slice(0, 200)}`);
|
|
4109
|
-
if (h.remaining_tasks?.length) {
|
|
4110
|
-
lines.push(`Remaining tasks:`);
|
|
4111
|
-
for (const t of h.remaining_tasks.slice(0, 5))
|
|
4112
|
-
lines.push(` • ${t}`);
|
|
4113
|
-
}
|
|
4114
|
-
if (h.instructions)
|
|
4115
|
-
lines.push(`⚠️ Instructions: ${h.instructions.slice(0, 200)}`);
|
|
4116
|
-
if (h.blocked_on)
|
|
4117
|
-
lines.push(`🚧 Blocked on: ${h.blocked_on}`);
|
|
4118
|
-
lines.push('');
|
|
4119
|
-
}
|
|
4120
|
-
// 4. WIP registry
|
|
4121
|
-
const wipRaw = await redis.get('cachly:ctx:wip-registry');
|
|
4122
|
-
if (wipRaw) {
|
|
4123
|
-
const wip = JSON.parse(wipRaw);
|
|
4124
|
-
if (wip.content) {
|
|
4125
|
-
lines.push(`### 🔧 WIP Registry`);
|
|
4126
|
-
lines.push(wip.content.slice(0, 400));
|
|
4127
|
-
lines.push('');
|
|
4128
|
-
}
|
|
4129
|
-
}
|
|
4130
|
-
// 5. Open failures (roadmap with status=blocked/in_progress)
|
|
4131
|
-
const roadmapKeys = [];
|
|
4132
|
-
const rStream = redis.scanStream({ match: 'cachly:roadmap:*', count: 100 });
|
|
4133
|
-
await new Promise((res, rej) => { rStream.on('data', (b) => roadmapKeys.push(...b)); rStream.on('end', res); rStream.on('error', rej); });
|
|
4134
|
-
const openItems = [];
|
|
4135
|
-
for (const k of roadmapKeys.slice(0, 30)) {
|
|
4136
|
-
const r = await redis.get(k);
|
|
4137
|
-
if (!r)
|
|
4138
|
-
continue;
|
|
4139
|
-
try {
|
|
4140
|
-
const item = JSON.parse(r);
|
|
4141
|
-
if (item.status === 'in_progress' || item.status === 'blocked')
|
|
4142
|
-
openItems.push({ title: item.title ?? k, status: item.status, priority: item.priority });
|
|
4143
|
-
}
|
|
4144
|
-
catch { /* skip */ }
|
|
4145
|
-
}
|
|
4146
|
-
if (openItems.length > 0) {
|
|
4147
|
-
lines.push(`### 🚧 Open Items`);
|
|
4148
|
-
for (const i of openItems.slice(0, 5))
|
|
4149
|
-
lines.push(` • [${i.status}] ${i.title}`);
|
|
4150
|
-
lines.push('');
|
|
4151
|
-
}
|
|
4152
|
-
// 6. Focus-relevant lessons
|
|
4153
|
-
if (focus) {
|
|
4154
|
-
const lessonKeys = [];
|
|
4155
|
-
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 200 });
|
|
4156
|
-
await new Promise((res, rej) => { lStream.on('data', (b) => lessonKeys.push(...b)); lStream.on('end', res); lStream.on('error', rej); });
|
|
4157
|
-
const relevant = [];
|
|
4158
|
-
for (const k of lessonKeys) {
|
|
4159
|
-
const r = await redis.get(k);
|
|
4160
|
-
if (!r)
|
|
4161
|
-
continue;
|
|
4162
|
-
try {
|
|
4163
|
-
const l = JSON.parse(r);
|
|
4164
|
-
if (l.topic.toLowerCase().includes(focus.toLowerCase()) && l.what_worked) {
|
|
4165
|
-
relevant.push({ topic: l.topic, what_worked: l.what_worked });
|
|
4166
|
-
}
|
|
4167
|
-
}
|
|
4168
|
-
catch { /* skip */ }
|
|
4169
|
-
}
|
|
4170
|
-
if (relevant.length > 0) {
|
|
4171
|
-
lines.push(`### 💡 Relevant Lessons for "${focus}"`);
|
|
4172
|
-
for (const l of relevant.slice(0, 4))
|
|
4173
|
-
lines.push(` • **${l.topic}**: ${l.what_worked.slice(0, 100)}`);
|
|
4174
|
-
lines.push('');
|
|
4175
|
-
}
|
|
4176
|
-
}
|
|
4177
|
-
if (lines.length <= 3) {
|
|
4178
|
-
lines.push(`_No brain data found. Start accumulating knowledge with \`learn_from_attempts\` and \`session_start\`._`);
|
|
4179
|
-
}
|
|
4180
|
-
lines.push(`---`, `🧠 Brain is ready. Continue your work — full context restored.`);
|
|
4181
|
-
return lines.join('\n');
|
|
4182
|
-
}
|
|
4183
|
-
// ── brain_from_git ────────────────────────────────────────────────────────
|
|
4184
|
-
case 'brain_from_git': {
|
|
4185
|
-
const { instance_id, repo_path = '.', limit = 100, branch = 'HEAD', since = '' } = args;
|
|
4186
|
-
const redis = await getConnection(instance_id);
|
|
4187
|
-
const { execSync } = await import('node:child_process');
|
|
4188
|
-
const { resolve } = await import('node:path');
|
|
4189
|
-
const repoDir = resolve(repo_path);
|
|
4190
|
-
const maxCommits = Math.min(Number(limit) || 100, 500);
|
|
4191
|
-
// Semaphore: max 10 concurrent git subprocesses per MCP process
|
|
4192
|
-
await _gitSemAcquire();
|
|
4193
|
-
try {
|
|
4194
|
-
// Verify it's a git repo
|
|
4195
|
-
try {
|
|
4196
|
-
execSync('git rev-parse --git-dir', { cwd: repoDir, stdio: 'pipe' });
|
|
4197
|
-
}
|
|
4198
|
-
catch {
|
|
4199
|
-
return `❌ Not a git repository: \`${repoDir}\`. Pass \`repo_path\` pointing to a git checkout.`;
|
|
4200
|
-
}
|
|
4201
|
-
// Build git log command
|
|
4202
|
-
const sinceFlag = since ? `--since="${since}"` : '';
|
|
4203
|
-
const logCmd = `git log ${branch} ${sinceFlag} --pretty=format:"%H|||%s|||%ad|||%an" --date=short --no-merges -n ${maxCommits}`;
|
|
4204
|
-
let logOutput = '';
|
|
4205
|
-
try {
|
|
4206
|
-
logOutput = execSync(logCmd, { cwd: repoDir, encoding: 'utf-8', stdio: 'pipe' });
|
|
4207
|
-
}
|
|
4208
|
-
catch (e) {
|
|
4209
|
-
return `❌ git log failed: ${e.message}. Check \`repo_path\` and \`branch\`.`;
|
|
4210
|
-
}
|
|
4211
|
-
const commits = logOutput.trim().split('\n').filter(Boolean).map(line => {
|
|
4212
|
-
const [sha, subject, date, author] = line.split('|||');
|
|
4213
|
-
return { sha: (sha ?? '').trim(), subject: (subject ?? '').trim(), date: (date ?? '').trim(), author: (author ?? '').trim() };
|
|
4214
|
-
});
|
|
4215
|
-
if (commits.length === 0) {
|
|
4216
|
-
return `⚠️ No commits found in \`${repoDir}\` on branch \`${branch}\`${since ? ` since ${since}` : ''}.`;
|
|
4217
|
-
}
|
|
4218
|
-
// Pattern classifiers
|
|
4219
|
-
const classifyCommit = (subject) => {
|
|
4220
|
-
const s = subject.toLowerCase();
|
|
4221
|
-
if (/\b(fix|fixed|fixes|bug|hotfix|patch|revert|resolve|closes? #\d+)\b/.test(s)) {
|
|
4222
|
-
const sev = /\b(critical|crash|security|auth|data loss|outage|prod|production)\b/.test(s) ? 'critical' : /\b(major|breaking|regression|hotfix)\b/.test(s) ? 'major' : 'minor';
|
|
4223
|
-
return { category: 'fix', outcome: 'success', severity: sev };
|
|
4224
|
-
}
|
|
4225
|
-
if (/\b(feat|feature|add|added|implement|new|introduce)\b/.test(s))
|
|
4226
|
-
return { category: 'feat', outcome: 'success', severity: 'minor' };
|
|
4227
|
-
if (/\b(refactor|clean|cleanup|improve|simplify|extract|rename)\b/.test(s))
|
|
4228
|
-
return { category: 'refactor', outcome: 'success', severity: 'minor' };
|
|
4229
|
-
if (/\b(perf|optimize|speed|cache|latency|memory|performance)\b/.test(s))
|
|
4230
|
-
return { category: 'perf', outcome: 'success', severity: 'major' };
|
|
4231
|
-
if (/\b(security|cve|auth|csrf|xss|sql|injection|sanitize|escape|encrypt)\b/.test(s))
|
|
4232
|
-
return { category: 'security', outcome: 'success', severity: 'critical' };
|
|
4233
|
-
if (/\b(deploy|ci|cd|build|docker|k8s|helm|infra|devops)\b/.test(s))
|
|
4234
|
-
return { category: 'deploy', outcome: 'success', severity: 'major' };
|
|
4235
|
-
if (/\b(test|spec|coverage|assert|mock|unit|integration)\b/.test(s))
|
|
4236
|
-
return { category: 'test', outcome: 'success', severity: 'minor' };
|
|
4237
|
-
return { category: 'chore', outcome: 'success', severity: 'minor' };
|
|
4238
|
-
};
|
|
4239
|
-
// Extract domain keywords from commit subject
|
|
4240
|
-
const extractDomain = (subject) => {
|
|
4241
|
-
const s = subject.toLowerCase();
|
|
4242
|
-
const tokens = s.replace(/[^a-z0-9\s\-_]/g, ' ').split(/\s+/).filter(t => t.length > 3 && !['that', 'this', 'with', 'from', 'when', 'into', 'also', 'some', 'were'].includes(t));
|
|
4243
|
-
return tokens.slice(0, 3).join('-') || 'general';
|
|
4244
|
-
};
|
|
4245
|
-
const ts = new Date().toISOString();
|
|
4246
|
-
let ingested = 0;
|
|
4247
|
-
let skipped = 0;
|
|
4248
|
-
const categoryCount = new Map();
|
|
4249
|
-
for (const commit of commits) {
|
|
4250
|
-
if (!commit.subject) {
|
|
4251
|
-
skipped++;
|
|
4252
|
-
continue;
|
|
4253
|
-
}
|
|
4254
|
-
const { category, outcome, severity } = classifyCommit(commit.subject);
|
|
4255
|
-
const domain = extractDomain(commit.subject);
|
|
4256
|
-
const topic = `${category}:${domain}`;
|
|
4257
|
-
categoryCount.set(category, (categoryCount.get(category) ?? 0) + 1);
|
|
4258
|
-
const lessonObj = {
|
|
4259
|
-
topic, outcome, severity,
|
|
4260
|
-
what_worked: commit.subject.slice(0, 200),
|
|
4261
|
-
what_failed: '',
|
|
4262
|
-
context: `git:${commit.sha.slice(0, 8)} by ${commit.author} on ${commit.date}`,
|
|
4263
|
-
file_paths: [], commands: [`git show ${commit.sha.slice(0, 8)}`],
|
|
4264
|
-
tags: ['brain_from_git', category, 'git-history'],
|
|
4265
|
-
depends_on: [], recall_count: 0, ts, verified_at: ts,
|
|
4266
|
-
confidence: 0.55, // lower confidence for auto-inferred lessons
|
|
4267
|
-
audit_trail: [{ ts, action: 'brain_from_git', sha: commit.sha.slice(0, 8) }],
|
|
4268
|
-
version: 3,
|
|
4269
|
-
};
|
|
4270
|
-
// Only store if no existing lesson for this topic (avoid overwriting higher-confidence lessons)
|
|
4271
|
-
const existing = await redis.get(`cachly:lesson:best:${topic}`);
|
|
4272
|
-
if (!existing) {
|
|
4273
|
-
await redis.set(`cachly:lesson:best:${topic}`, JSON.stringify(lessonObj));
|
|
4274
|
-
await redis.rpush(`cachly:lessons:${topic}`, JSON.stringify(lessonObj));
|
|
4275
|
-
}
|
|
4276
|
-
await redis.rpush('cachly:lessons:brain_from_git:all', JSON.stringify({ topic, sha: commit.sha.slice(0, 8), subject: commit.subject.slice(0, 60) }));
|
|
4277
|
-
await redis.ltrim('cachly:lessons:brain_from_git:all', -500, -1);
|
|
4278
|
-
// Update CKG
|
|
4279
|
-
const conceptId = ckgSlug(topic);
|
|
4280
|
-
await ckgUpsertNode(redis, conceptId, category, 'git-derived');
|
|
4281
|
-
ingested++;
|
|
4282
|
-
}
|
|
4283
|
-
const categoryBreakdown = [...categoryCount.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => ` • **${k}** (${v})`).join('\n');
|
|
4284
|
-
return [
|
|
4285
|
-
`🔁 **brain_from_git: ${repoDir}**`, '',
|
|
4286
|
-
`📂 Branch: \`${branch}\` | Processed: **${commits.length}** commits | Ingested: **${ingested}** lessons | Skipped: ${skipped}`,
|
|
4287
|
-
``,
|
|
4288
|
-
`**Breakdown by category:**`,
|
|
4289
|
-
categoryBreakdown,
|
|
4290
|
-
``,
|
|
4291
|
-
`💡 New lessons are stored with confidence 0.55 (auto-inferred).`,
|
|
4292
|
-
`💡 As you confirm them via \`learn_from_attempts\`, confidence rises automatically.`,
|
|
4293
|
-
`🔍 Explore: \`brain_search(query="fix")\` | \`ckg_inspect(concept="deploy")\``,
|
|
4294
|
-
].join('\n');
|
|
4295
|
-
}
|
|
4296
|
-
finally {
|
|
4297
|
-
_gitSemRelease();
|
|
4298
|
-
}
|
|
4299
|
-
}
|
|
4300
|
-
// ── brain_predict_failures ─────────────────────────────────────────────────
|
|
4301
|
-
case 'brain_predict_failures': {
|
|
4302
|
-
const { instance_id, context: ctx, top_k = 5, format = 'detailed' } = args;
|
|
4303
|
-
const redis = await getConnection(instance_id);
|
|
4304
|
-
const ctxTokens = ctx.toLowerCase().replace(/[^a-z0-9\s\-_:.]/g, ' ').split(/\s+/).filter(t => t.length > 2);
|
|
4305
|
-
const failures = [];
|
|
4306
|
-
// Step 1: CKG — find 'causes' and 'degrades_under' edges from context tokens
|
|
4307
|
-
for (const token of ctxTokens.slice(0, 8)) {
|
|
4308
|
-
const nodeKeys = [];
|
|
4309
|
-
const nStream = redis.scanStream({ match: `cachly:ckg:node:*${token}*`, count: 50 });
|
|
4310
|
-
await new Promise((res, rej) => { nStream.on('data', (b) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
|
|
4311
|
-
for (const nk of nodeKeys.slice(0, 5)) {
|
|
4312
|
-
const nodeRaw = await redis.get(nk);
|
|
4313
|
-
if (!nodeRaw)
|
|
4314
|
-
continue;
|
|
4315
|
-
const node = JSON.parse(nodeRaw);
|
|
4316
|
-
const edgeKeys = await redis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
4317
|
-
for (const ek of edgeKeys.slice(0, 20)) {
|
|
4318
|
-
const edgeRaw = await redis.get(ek);
|
|
4319
|
-
if (!edgeRaw)
|
|
4320
|
-
continue;
|
|
4321
|
-
const edge = JSON.parse(edgeRaw);
|
|
4322
|
-
if (edge.edgeType !== 'causes' && edge.edgeType !== 'degrades_under')
|
|
4323
|
-
continue;
|
|
4324
|
-
// Look up fix for this failure from CKG 'fixes' edges
|
|
4325
|
-
const fixEdgeKeys = await redis.smembers(`cachly:ckg:idx:from:${edge.to}`);
|
|
4326
|
-
let fix;
|
|
4327
|
-
for (const fek of fixEdgeKeys.slice(0, 10)) {
|
|
4328
|
-
const feRaw = await redis.get(fek);
|
|
4329
|
-
if (!feRaw)
|
|
4330
|
-
continue;
|
|
4331
|
-
const fe = JSON.parse(feRaw);
|
|
4332
|
-
if (fe.edgeType === 'fixes') {
|
|
4333
|
-
const lessonRaw = await redis.get(`cachly:lesson:best:${fe.from}`);
|
|
4334
|
-
if (lessonRaw) {
|
|
4335
|
-
const lesson = JSON.parse(lessonRaw);
|
|
4336
|
-
fix = lesson.what_worked?.slice(0, 120);
|
|
4337
|
-
break;
|
|
4338
|
-
}
|
|
4339
|
-
}
|
|
4340
|
-
}
|
|
4341
|
-
failures.push({
|
|
4342
|
-
concept: node.id,
|
|
4343
|
-
failure: edge.to.replace(/-/g, ' '),
|
|
4344
|
-
probability: edge.confidence,
|
|
4345
|
-
fix,
|
|
4346
|
-
source: 'ckg',
|
|
4347
|
-
});
|
|
4348
|
-
}
|
|
4349
|
-
}
|
|
4350
|
-
}
|
|
4351
|
-
// Step 2: Lesson history — find failure-outcome lessons matching context
|
|
4352
|
-
const lessonKeys = [];
|
|
4353
|
-
const lStream = redis.scanStream({ match: 'cachly:lesson:best:*', count: 300 });
|
|
4354
|
-
await new Promise((res, rej) => { lStream.on('data', (b) => lessonKeys.push(...b)); lStream.on('end', res); lStream.on('error', rej); });
|
|
4355
|
-
for (const k of lessonKeys.slice(0, 100)) {
|
|
4356
|
-
const r = await redis.get(k);
|
|
4357
|
-
if (!r)
|
|
4358
|
-
continue;
|
|
4359
|
-
try {
|
|
4360
|
-
const l = JSON.parse(r);
|
|
4361
|
-
if (l.outcome !== 'failure' && l.outcome !== 'partial')
|
|
4362
|
-
continue;
|
|
4363
|
-
if (!l.what_failed)
|
|
4364
|
-
continue;
|
|
4365
|
-
const haystack = `${l.topic} ${l.what_failed}`.toLowerCase();
|
|
4366
|
-
const matchScore = ctxTokens.filter(t => haystack.includes(t)).length / Math.max(1, ctxTokens.length);
|
|
4367
|
-
if (matchScore < 0.15)
|
|
4368
|
-
continue;
|
|
4369
|
-
const sevBoost = l.severity === 'critical' ? 0.15 : l.severity === 'major' ? 0.05 : 0;
|
|
4370
|
-
failures.push({
|
|
4371
|
-
concept: l.topic,
|
|
4372
|
-
failure: l.what_failed.slice(0, 80),
|
|
4373
|
-
probability: Math.min(0.97, (l.confidence ?? 0.5) * matchScore * 1.5 + sevBoost),
|
|
4374
|
-
fix: l.what_worked?.slice(0, 120),
|
|
4375
|
-
topic: l.topic,
|
|
4376
|
-
source: 'lesson',
|
|
4377
|
-
});
|
|
4378
|
-
}
|
|
4379
|
-
catch { /* skip */ }
|
|
4380
|
-
}
|
|
4381
|
-
if (failures.length === 0) {
|
|
4382
|
-
return [
|
|
4383
|
-
`🔮 **Failure Prediction: "${ctx}"**`, '',
|
|
4384
|
-
`No known failure patterns found for this context.`,
|
|
4385
|
-
`💡 The brain learns from every \`learn_from_attempts(outcome="failure")\` call.`,
|
|
4386
|
-
`🔍 Try: \`brain_predict(context="${ctx}")\` for broader predictions.`,
|
|
4387
|
-
].join('\n');
|
|
4388
|
-
}
|
|
4389
|
-
// Deduplicate and rank by probability
|
|
4390
|
-
const seen = new Set();
|
|
4391
|
-
const ranked = failures.filter(f => {
|
|
4392
|
-
const k = f.failure.slice(0, 40);
|
|
4393
|
-
if (seen.has(k))
|
|
4394
|
-
return false;
|
|
4395
|
-
seen.add(k);
|
|
4396
|
-
return true;
|
|
4397
|
-
}).sort((a, b) => b.probability - a.probability).slice(0, Number(top_k));
|
|
4398
|
-
const lines = [`🔮 **Failure Prediction for: "${ctx}"**\n`];
|
|
4399
|
-
lines.push(`> Pre-deploy failure analysis based on ${failures.length} patterns. Ranked by probability.\n`);
|
|
4400
|
-
for (let i = 0; i < ranked.length; i++) {
|
|
4401
|
-
const f = ranked[i];
|
|
4402
|
-
const pct = Math.round(f.probability * 100);
|
|
4403
|
-
const bar = '█'.repeat(Math.round(pct / 10)) + '░'.repeat(10 - Math.round(pct / 10));
|
|
4404
|
-
const icon = pct >= 70 ? '🔴' : pct >= 40 ? '🟡' : '🟢';
|
|
4405
|
-
lines.push(`${icon} **${i + 1}. ${f.failure}**`);
|
|
4406
|
-
lines.push(` Probability: ${bar} **${pct}%** (${f.source === 'ckg' ? 'CKG causal edge' : 'lesson history'})`);
|
|
4407
|
-
if (format === 'detailed' && f.fix) {
|
|
4408
|
-
lines.push(` ✅ Pre-loaded fix: _${f.fix}_`);
|
|
4409
|
-
}
|
|
4410
|
-
if (format === 'detailed' && f.topic) {
|
|
4411
|
-
lines.push(` 📚 Lesson: \`${f.topic}\``);
|
|
4412
|
-
}
|
|
4413
|
-
lines.push('');
|
|
4414
|
-
}
|
|
4415
|
-
const highRisk = ranked.filter(f => f.probability >= 0.6);
|
|
4416
|
-
if (highRisk.length > 0) {
|
|
4417
|
-
lines.push(`⚠️ **${highRisk.length} high-risk failure${highRisk.length > 1 ? 's' : ''} detected** (≥60% probability). Review fixes before proceeding.`);
|
|
4418
|
-
}
|
|
4419
|
-
else {
|
|
4420
|
-
lines.push(`✅ No high-risk failures detected. Proceed with caution and monitor closely.`);
|
|
4421
|
-
}
|
|
4422
|
-
lines.push('', `💡 After deploy: \`learn_from_attempts(topic="deploy:...", outcome="success|failure")\` to improve future predictions.`);
|
|
4423
|
-
return lines.join('\n');
|
|
4424
|
-
}
|
|
4425
465
|
default:
|
|
4426
|
-
return
|
|
466
|
+
return `⚠️ Unknown tool: ${name}`;
|
|
4427
467
|
}
|
|
4428
468
|
}
|
|
4429
|
-
// ── Server setup ──────────────────────────────────────────────────────────────
|
|
4430
469
|
const server = new Server({ name: 'cachly-mcp', version: CURRENT_VERSION }, { capabilities: { tools: {} } });
|
|
4431
470
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
4432
471
|
// ── Auto-session management ───────────────────────────────────────────────────
|
|
@@ -4440,25 +479,6 @@ let _autoSessionToolCount = 0;
|
|
|
4440
479
|
// ── brain_from_git concurrency limiter ────────────────────────────────────────
|
|
4441
480
|
// Spawning many `git log` subprocesses in parallel can fork-bomb the host.
|
|
4442
481
|
// This simple semaphore caps concurrent brain_from_git calls to 10 per process.
|
|
4443
|
-
let _gitSemCount = 0;
|
|
4444
|
-
const _GIT_SEM_MAX = 10;
|
|
4445
|
-
const _gitSemQueue = [];
|
|
4446
|
-
function _gitSemAcquire() {
|
|
4447
|
-
if (_gitSemCount < _GIT_SEM_MAX) {
|
|
4448
|
-
_gitSemCount++;
|
|
4449
|
-
return Promise.resolve();
|
|
4450
|
-
}
|
|
4451
|
-
return new Promise(resolve => _gitSemQueue.push(resolve));
|
|
4452
|
-
}
|
|
4453
|
-
function _gitSemRelease() {
|
|
4454
|
-
const next = _gitSemQueue.shift();
|
|
4455
|
-
if (next) {
|
|
4456
|
-
next();
|
|
4457
|
-
}
|
|
4458
|
-
else {
|
|
4459
|
-
_gitSemCount--;
|
|
4460
|
-
}
|
|
4461
|
-
}
|
|
4462
482
|
async function autoStartSession(instanceId) {
|
|
4463
483
|
if (_autoSessionStarted)
|
|
4464
484
|
return;
|