@cachly-dev/mcp-server 0.9.19 → 0.9.21

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