@kernel.chat/kbot 3.67.0 → 3.68.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  <p align="center">
4
4
  <strong>kbot</strong><br>
5
- Open-source terminal AI agent. 35 agents. 686+ tools. 20 providers. Science, finance, security, and more.
5
+ Open-source terminal AI agent. 693+ tools. 35 agents. 20 providers. Dreams, learns, watches your system. $0 local.
6
6
  </p>
7
7
 
8
8
  <p align="center">
@@ -30,7 +30,7 @@ Most terminal AI agents lock you into one provider, one model, one way of workin
30
30
  - **Runs fully offline** — Embedded llama.cpp, Ollama, LM Studio, or Jan. $0, fully private.
31
31
  - **Learns your patterns** — Bayesian skill ratings + pattern extraction. Gets faster over time.
32
32
  - **35 specialist agents** — auto-routes your request to the right expert (coder, researcher, writer, guardian, quant, and 30 more).
33
- - **686+ tools** — files, bash, git, GitHub, web search, deploy, database, game dev, VFX, research, science, finance, security, and more.
33
+ - **693+ tools** — files, bash, git, GitHub, web search, deploy, database, game dev, VFX, research, science, finance, security, and more.
34
34
  - **Programmatic SDK** — use kbot as a library in your own apps.
35
35
  - **MCP server built in** — plug kbot into Claude Code, Cursor, VS Code, Zed, or Neovim as a tool provider.
36
36
 
@@ -47,7 +47,23 @@ kbot dream journal # Full insight history
47
47
  kbot dream search # Find specific memories
48
48
  ```
49
49
 
50
- 5-tier memory: pattern cache -> solution index -> user profile -> dream journal -> passive scanner. All tiers feed each other through the dream engine.
50
+ 7-tier memory: pattern cache -> solution index -> user profile -> dream journal -> passive scanner -> music production -> desktop behavior. All tiers feed each other bidirectionally through the dream engine. Dream insights automatically evolve kbot's prompts so it gets better at being *your* specific tool.
51
+
52
+ ### Always-On System Manager
53
+
54
+ kbot runs 24/7 in the background, managing your entire development environment:
55
+
56
+ ```
57
+ kbot watchdog # Service dashboard — CPU, RAM, disk, all services
58
+ kbot wd --restart email # Restart a crashed service
59
+ kbot dream status # What kbot learned about you
60
+ ```
61
+
62
+ - **Service watchdog** — health-checks all background services, auto-restarts crashes
63
+ - **Morning briefing** — daily email with downloads, stars, emails, dream insights, service health
64
+ - **Behavior learning** — observes which apps you use and when, dreams about your workflow patterns
65
+ - **Companion memory** — email agent remembers every user's preferences, goals, and conversation history
66
+ - **Proactive follow-ups** — checks in with users who go quiet, referencing their specific context
51
67
 
52
68
  ### Audit Any Repo in One Command
53
69
 
@@ -65,9 +81,12 @@ Checks security, documentation, code quality, CI/CD, community health, and DevOp
65
81
  |---|---|---|---|---|---|
66
82
  | AI providers | 20 | 1 | 1 | 6 | 75+ |
67
83
  | Specialist agents | 35 | 0 | 0 | 0 | 0 |
68
- | Built-in tools | 686+ | ~20 | ~15 | ~10 | ~15 |
84
+ | Built-in tools | 693+ | ~20 | ~15 | ~10 | ~15 |
69
85
  | Science tools | 114 | 0 | 0 | 0 | 0 |
70
- | Learning engine | Yes | No | No | No | No |
86
+ | Memory system | 7-tier bidirectional | File-based | No | No | No |
87
+ | Dream engine | Yes ($0 local) | Cloud API | No | No | No |
88
+ | Service watchdog | Yes | No | No | No | No |
89
+ | Behavior learning | Yes | No | No | No | No |
71
90
  | Offline mode | Embedded + Ollama | No | No | Ollama | Ollama |
72
91
  | SDK | Yes | No | Yes | No | No |
73
92
  | MCP server | Yes | N/A | No | No | No |
@@ -248,6 +267,8 @@ Works with Claude Code, Cursor, VS Code, Windsurf, Zed, Neovim.
248
267
  | `kbot dream status` | See what kbot has learned about you |
249
268
  | `kbot dream journal` | Full insight history |
250
269
  | `kbot dream search` | Find specific memories |
270
+ | `kbot watchdog` | System dashboard — services, CPU, RAM, disk, Ollama, dreams |
271
+ | `kbot wd --restart <svc>` | Restart a crashed background service |
251
272
  | `kbot contribute <repo>` | Find good-first-issues and quick wins |
252
273
  | `kbot share` | Share conversation as GitHub Gist |
253
274
  | `kbot pair` | File watcher with auto-analysis |
package/dist/agent.js CHANGED
@@ -17,7 +17,7 @@ import { getMatrixSystemPrompt } from './matrix.js';
17
17
  import { buildFullLearningContext, findPattern, recordPattern, cacheSolution, updateProfile, classifyTask, extractKeywords, learnFromExchange, updateProjectMemory, shouldAutoTrain, selfTrain, } from './learning.js';
18
18
  import { getMemoryPrompt, addTurn, getPreviousMessages, getHistory } from './memory.js';
19
19
  import { getDreamPrompt, dreamAfterSession } from './dream.js';
20
- import { setBuddyMood } from './buddy.js';
20
+ import { setBuddyMood, addBuddyXP, checkAchievements, formatAchievementUnlock } from './buddy.js';
21
21
  import { notifyTurn, startMemoryScanner, stopMemoryScanner } from './memory-scanner.js';
22
22
  import { captureUserBehavior } from './user-behavior.js';
23
23
  import { autoCompact, compressToolResult } from './context-manager.js';
@@ -1627,6 +1627,14 @@ Always quote file paths that contain spaces. Never reference internal system nam
1627
1627
  // ── Dream Engine: consolidate session memories (non-blocking, $0 via Ollama) ──
1628
1628
  setBuddyMood('learning');
1629
1629
  dreamAfterSession(sessionId);
1630
+ // ── Buddy Evolution: award XP for completing a session ──
1631
+ addBuddyXP(1);
1632
+ // ── Achievements: check for newly unlocked milestones ──
1633
+ const newAchievements = checkAchievements();
1634
+ for (const achievement of newAchievements) {
1635
+ // Print to stderr so it doesn't interfere with piped output
1636
+ process.stderr.write('\n' + formatAchievementUnlock(achievement) + '\n\n');
1637
+ }
1630
1638
  // Session complete — buddy returns to idle
1631
1639
  setBuddyMood('idle');
1632
1640
  const content = lastResponse?.content || 'Reached maximum tool iterations.';
package/dist/buddy.d.ts CHANGED
@@ -1,20 +1,86 @@
1
1
  export type BuddySpecies = 'fox' | 'owl' | 'cat' | 'robot' | 'ghost' | 'mushroom' | 'octopus' | 'dragon';
2
2
  export type BuddyMood = 'idle' | 'thinking' | 'success' | 'error' | 'learning';
3
+ export type BuddyLevel = 0 | 1 | 2 | 3;
4
+ export interface BuddyEvolution {
5
+ level: BuddyLevel;
6
+ xp: number;
7
+ evolvedAt: string[];
8
+ }
3
9
  export interface BuddyState {
4
10
  species: BuddySpecies;
5
11
  name: string;
6
12
  mood: BuddyMood;
13
+ evolution: BuddyEvolution;
14
+ }
15
+ export interface BuddyLevelInfo {
16
+ level: BuddyLevel;
17
+ xp: number;
18
+ xpToNext: number | null;
19
+ title: string;
20
+ }
21
+ export interface Achievement {
22
+ /** Unique achievement ID */
23
+ id: string;
24
+ /** Display name */
25
+ name: string;
26
+ /** Description of what the user did */
27
+ description: string;
28
+ /** Single ASCII char icon (trophy, star, bolt, etc.) */
29
+ icon: string;
30
+ /** ISO timestamp when unlocked, null if locked */
31
+ unlockedAt: string | null;
7
32
  }
8
- /** Get the buddy's current state (species, name, mood) */
33
+ /**
34
+ * Check all achievement conditions and unlock any newly earned.
35
+ * Returns the list of newly unlocked achievements (empty if none).
36
+ * Call this at session end in agent.ts.
37
+ */
38
+ export declare function checkAchievements(): Achievement[];
39
+ /**
40
+ * Get all achievements with their unlock status.
41
+ * Unlocked ones include the timestamp; locked ones show null.
42
+ */
43
+ export declare function getAchievements(): Achievement[];
44
+ /**
45
+ * Get a progress hint for a locked achievement.
46
+ * Returns null if the achievement is unlocked or not found.
47
+ */
48
+ export declare function getAchievementProgress(achievementId: string): string | null;
49
+ /**
50
+ * Format achievement unlock notification for terminal display.
51
+ * Shows the buddy sprite in success mood with a celebration message.
52
+ */
53
+ export declare function formatAchievementUnlock(achievement: Achievement): string;
54
+ /** Get the buddy's current state (species, name, mood, evolution) */
9
55
  export declare function getBuddy(): BuddyState;
10
56
  /** Set the buddy's mood */
11
57
  export declare function setBuddyMood(mood: BuddyMood): void;
12
- /** Get the ASCII sprite for the buddy in the given mood (defaults to current) */
58
+ /** Get the ASCII sprite for the buddy in the given mood (defaults to current).
59
+ * Applies evolution visual upgrades based on the buddy's current level. */
13
60
  export declare function getBuddySprite(mood?: BuddyMood): string[];
14
61
  /** Get a random greeting for the buddy */
15
62
  export declare function getBuddyGreeting(): string;
16
63
  /** Rename the buddy (persisted to ~/.kbot/buddy.json) */
17
64
  export declare function renameBuddy(newName: string): void;
65
+ /**
66
+ * Add XP to the buddy. Checks for level-ups and persists to buddy.json.
67
+ * Returns the updated level info, and whether a level-up just occurred.
68
+ *
69
+ * XP sources:
70
+ * - Session complete: +1
71
+ * - Dream cycle: +2
72
+ * - Tool creation: +3
73
+ * - First error fix: +1
74
+ */
75
+ export declare function addBuddyXP(amount: number): {
76
+ levelInfo: BuddyLevelInfo;
77
+ leveledUp: boolean;
78
+ };
79
+ /**
80
+ * Get the buddy's current level info without modifying state.
81
+ * Includes level, XP, XP to next level, and species-specific title.
82
+ */
83
+ export declare function getBuddyLevel(): BuddyLevelInfo;
18
84
  /**
19
85
  * Format the buddy with a speech bubble and status message.
20
86
  * Returns a multi-line string ready for terminal output.
@@ -30,4 +96,13 @@ export declare function renameBuddy(newName: string): void;
30
96
  * ~ Patch the fox ~
31
97
  */
32
98
  export declare function formatBuddyStatus(message?: string): string;
99
+ /**
100
+ * Get a dream narration for the buddy to tell the user at startup.
101
+ *
102
+ * Picks the highest-relevance insight that was reinforced in the last 24 hours
103
+ * and hasn't already been narrated. Returns `null` if there's nothing new to say.
104
+ *
105
+ * Tracks narrated insight IDs in buddy.json to avoid repeats.
106
+ */
107
+ export declare function getBuddyDreamNarration(): string | null;
33
108
  //# sourceMappingURL=buddy.d.ts.map
package/dist/buddy.js CHANGED
@@ -1,14 +1,19 @@
1
- // kbot Buddy System — Terminal companion sprites
1
+ // kbot Buddy System — Terminal companion sprites + Achievements
2
2
  //
3
3
  // Deterministic companion assignment based on config path hash.
4
4
  // Same user always gets the same buddy. Mood changes based on session activity.
5
5
  // Pure ASCII art, max 5 lines tall, 15 chars wide. Tamagotchi energy.
6
6
  //
7
- // Persists buddy name to ~/.kbot/buddy.json
7
+ // Achievements: milestones that unlock as the user uses kbot. Persisted in buddy.json.
8
+ //
9
+ // Persists buddy name + achievements to ~/.kbot/buddy.json
8
10
  import { homedir } from 'node:os';
9
11
  import { join } from 'node:path';
10
12
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
11
13
  import { createHash } from 'node:crypto';
14
+ import { getDreamStatus } from './dream.js';
15
+ import { getExtendedStats } from './learning.js';
16
+ import { getToolMetrics } from './tools/index.js';
12
17
  // ── Paths ──
13
18
  const KBOT_DIR = join(homedir(), '.kbot');
14
19
  const BUDDY_FILE = join(KBOT_DIR, 'buddy.json');
@@ -28,6 +33,25 @@ const DEFAULT_NAMES = {
28
33
  octopus: 'Ink',
29
34
  dragon: 'Ember',
30
35
  };
36
+ // ── Evolution thresholds ──
37
+ // XP required to reach each level. 1 XP per session, bonus for special actions.
38
+ const LEVEL_THRESHOLDS = {
39
+ 0: 0,
40
+ 1: 50,
41
+ 2: 150,
42
+ 3: 500,
43
+ };
44
+ /** Titles per species at each evolution level */
45
+ const LEVEL_TITLES = {
46
+ fox: { 0: 'Kit', 1: 'Scout', 2: 'Tracker', 3: 'Phantom' },
47
+ owl: { 0: 'Owlet', 1: 'Watcher', 2: 'Sage', 3: 'Oracle' },
48
+ cat: { 0: 'Kitten', 1: 'Prowler', 2: 'Shadow', 3: 'Sphinx' },
49
+ robot: { 0: 'Spark', 1: 'Circuit', 2: 'Core', 3: 'Singularity' },
50
+ ghost: { 0: 'Wisp', 1: 'Shade', 2: 'Phantom', 3: 'Wraith' },
51
+ mushroom: { 0: 'Spore', 1: 'Sprout', 2: 'Mycelium', 3: 'Overmind' },
52
+ octopus: { 0: 'Hatchling', 1: 'Drifter', 2: 'Kraken', 3: 'Leviathan' },
53
+ dragon: { 0: 'Whelp', 1: 'Drake', 2: 'Wyrm', 3: 'Ancient' },
54
+ };
31
55
  // ── ASCII Sprites ──
32
56
  // Each sprite is an array of strings (lines). Max 5 lines, max 15 chars wide.
33
57
  // Pure ASCII only — no unicode box drawing.
@@ -329,6 +353,106 @@ const SPRITES = {
329
353
  ],
330
354
  },
331
355
  };
356
+ // ── Evolved Sprite Transforms ──
357
+ // Each level modifies the base sprite with small visual upgrades.
358
+ // Level 0 = base sprites. Levels 1-3 add sparkles, upgraded features, crowns.
359
+ function applySpriteEvolution(species, mood, level) {
360
+ const base = SPRITES[species][mood].map(l => l);
361
+ if (level === 0)
362
+ return base;
363
+ switch (species) {
364
+ case 'fox':
365
+ // L1: sparkle ear tips. L2: star paws. L3: glowing eyes.
366
+ if (level >= 1)
367
+ base[0] = ' /\\* /\\*';
368
+ if (level >= 2)
369
+ base[4] = '(*| |*) ';
370
+ if (level >= 3)
371
+ base[1] = ' ( @ . @ ) ';
372
+ break;
373
+ case 'owl':
374
+ // L1: sparkle crest. L2: reinforced perch. L3: crown + glowing eyes.
375
+ if (level >= 1)
376
+ base[0] = ' {o,o}* ';
377
+ if (level >= 2)
378
+ base[2] = ' ="--"=" ';
379
+ if (level >= 3) {
380
+ base[0] = ' ^{@,@}* ';
381
+ base[2] = ' ="=="=" ';
382
+ }
383
+ break;
384
+ case 'cat':
385
+ // L1: whisker sparks. L2: flared paws. L3: crown + glowing eyes.
386
+ if (level >= 1)
387
+ base[2] = ' >*^*< ';
388
+ if (level >= 2)
389
+ base[4] = ' *~ ~* ';
390
+ if (level >= 3) {
391
+ base[0] = '^/\\_/\\^ ';
392
+ base[1] = '( @.@ ) ';
393
+ }
394
+ break;
395
+ case 'robot':
396
+ // L1: antenna spark. L2: bracket eyes upgrade. L3: glow eyes + crown.
397
+ if (level >= 1)
398
+ base[0] = ' [====*] ';
399
+ if (level >= 2) {
400
+ base[1] = base[1].replace('[o o]', '{o o}').replace('[^ ^]', '{^ ^}').replace('[x x]', '{x x}');
401
+ }
402
+ if (level >= 3) {
403
+ base[0] = '^[==*==]^';
404
+ base[1] = base[1]
405
+ .replace('{o o}', '{@ @}').replace('{^ ^}', '{@ @}').replace('{x x}', '{X X}')
406
+ .replace('[o o]', '{@ @}').replace('[^ ^]', '{@ @}').replace('[x x]', '{X X}');
407
+ }
408
+ break;
409
+ case 'ghost':
410
+ // L1: glow aura dots. L2: sparkle trail. L3: crown + glowing eyes.
411
+ if (level >= 1)
412
+ base[0] = ' *.---.* ';
413
+ if (level >= 2)
414
+ base[4] = ' *~~W~~* ';
415
+ if (level >= 3) {
416
+ base[0] = '^*.---.*^';
417
+ base[1] = base[1].replace('o o', '@ @').replace('^ ^', '@ @').replace('; ;', '@ @');
418
+ }
419
+ break;
420
+ case 'mushroom':
421
+ // L1: spore dots on cap. L2: sparkle stem. L3: crown + glow spots.
422
+ if (level >= 1)
423
+ base[0] = ' .*-^-.* ';
424
+ if (level >= 2)
425
+ base[2] = '|==*=*==|';
426
+ if (level >= 3) {
427
+ base[0] = '^.*-^-.*^';
428
+ base[1] = base[1].replace('o o', '@ @').replace('^ ^', '@ @').replace('; ;', '@ @');
429
+ }
430
+ break;
431
+ case 'octopus':
432
+ // L1: bubble on head. L2: sparkle tentacles. L3: crown + glow eyes.
433
+ if (level >= 1)
434
+ base[0] = ' *.---. ';
435
+ if (level >= 2)
436
+ base[4] = ' *~ ~ ~* ';
437
+ if (level >= 3) {
438
+ base[0] = '^*.---.*';
439
+ base[1] = base[1].replace('o o', '@ @').replace('^ ^', '@ @').replace('; ;', '@ @');
440
+ }
441
+ break;
442
+ case 'dragon':
443
+ // L1: flame spark on tail. L2: bigger wing marks. L3: crown + fire eyes.
444
+ if (level >= 1)
445
+ base[4] = ' ^^ * ';
446
+ if (level >= 2)
447
+ base[2] = '| ==/\\* ';
448
+ if (level >= 3) {
449
+ base[0] = ' ^/\\_ ';
450
+ base[1] = base[1].replace('o >', '@ >*').replace('^ >', '@ >*').replace('; >', '@ >*');
451
+ }
452
+ break;
453
+ }
454
+ return base;
455
+ }
332
456
  // ── Greetings per species ──
333
457
  const GREETINGS = {
334
458
  fox: ['Yip! Ready to dig in.', 'What are we hunting today?', 'Tail wagging. Lets go.'],
@@ -352,6 +476,7 @@ const MOOD_MESSAGES = {
352
476
  let currentMood = 'idle';
353
477
  let cachedSpecies = null;
354
478
  let cachedName = null;
479
+ let cachedEvolution = null;
355
480
  // ── Config persistence ──
356
481
  function ensureDir() {
357
482
  if (!existsSync(KBOT_DIR))
@@ -390,24 +515,370 @@ function resolveName() {
390
515
  cachedName = config.name || DEFAULT_NAMES[resolveSpecies()];
391
516
  return cachedName;
392
517
  }
518
+ function resolveEvolution() {
519
+ if (cachedEvolution)
520
+ return cachedEvolution;
521
+ const config = loadBuddyConfig();
522
+ cachedEvolution = config.evolution ?? { level: 0, xp: 0, evolvedAt: [] };
523
+ return cachedEvolution;
524
+ }
525
+ /**
526
+ * Compute the level for a given XP total.
527
+ * Returns the highest level whose threshold the XP meets or exceeds.
528
+ */
529
+ function computeLevel(xp) {
530
+ if (xp >= LEVEL_THRESHOLDS[3])
531
+ return 3;
532
+ if (xp >= LEVEL_THRESHOLDS[2])
533
+ return 2;
534
+ if (xp >= LEVEL_THRESHOLDS[1])
535
+ return 1;
536
+ return 0;
537
+ }
538
+ /** Build context snapshot for achievement checks */
539
+ function buildAchievementContext() {
540
+ const stats = getExtendedStats();
541
+ const dreamStatus = getDreamStatus();
542
+ const toolMetrics = getToolMetrics();
543
+ const config = loadBuddyConfig();
544
+ const uniqueToolsUsed = toolMetrics.length;
545
+ const buddyRenamed = config.name != null && config.name !== DEFAULT_NAMES[resolveSpecies()];
546
+ const currentHour = new Date().getHours();
547
+ const today = new Date().toISOString().slice(0, 10);
548
+ const usageDates = config.usageDates ?? [];
549
+ const sessionsToday = usageDates.filter(d => d === today).length;
550
+ const streakDays = calculateStreak(usageDates);
551
+ const providersConfigured = countProviders();
552
+ return {
553
+ stats,
554
+ dreamCycles: dreamStatus.state.cycles,
555
+ dreamInsights: dreamStatus.insights.length,
556
+ uniqueToolsUsed,
557
+ buddyRenamed,
558
+ currentHour,
559
+ sessionsToday,
560
+ streakDays,
561
+ providersConfigured,
562
+ };
563
+ }
564
+ /** Count consecutive usage days ending at today or yesterday */
565
+ function calculateStreak(usageDates) {
566
+ if (usageDates.length === 0)
567
+ return 0;
568
+ const unique = [...new Set(usageDates)].sort().reverse();
569
+ const today = new Date();
570
+ today.setHours(0, 0, 0, 0);
571
+ const mostRecent = unique[0];
572
+ const diffFromToday = Math.floor((today.getTime() - new Date(mostRecent).getTime()) / (1000 * 60 * 60 * 24));
573
+ if (diffFromToday > 1)
574
+ return 0;
575
+ let streak = 1;
576
+ for (let i = 1; i < unique.length; i++) {
577
+ const prev = new Date(unique[i - 1]);
578
+ const curr = new Date(unique[i]);
579
+ const diff = Math.floor((prev.getTime() - curr.getTime()) / (1000 * 60 * 60 * 24));
580
+ if (diff === 1) {
581
+ streak++;
582
+ }
583
+ else {
584
+ break;
585
+ }
586
+ }
587
+ return streak;
588
+ }
589
+ /** Count configured AI providers from ~/.kbot/config.json */
590
+ function countProviders() {
591
+ const configPath = join(KBOT_DIR, 'config.json');
592
+ if (!existsSync(configPath))
593
+ return 0;
594
+ try {
595
+ const raw = readFileSync(configPath, 'utf-8');
596
+ const config = JSON.parse(raw);
597
+ let count = 0;
598
+ const providerKeys = [
599
+ 'anthropic', 'openai', 'google', 'mistral', 'xai', 'deepseek', 'groq',
600
+ 'cohere', 'together', 'fireworks', 'perplexity', 'openrouter', 'replicate',
601
+ 'sambanova', 'cerebras', 'hyperbolic', 'lepton', 'novita', 'ollama', 'lmstudio',
602
+ ];
603
+ for (const p of providerKeys) {
604
+ if (config[`${p}_key`] || config['byok_provider'] === p)
605
+ count++;
606
+ }
607
+ return Math.max(count, config['byok_provider'] ? 1 : 0);
608
+ }
609
+ catch {
610
+ return 0;
611
+ }
612
+ }
613
+ const ACHIEVEMENT_DEFS = [
614
+ {
615
+ id: 'first_steps',
616
+ name: 'First Steps',
617
+ description: 'Complete your first session',
618
+ icon: '+',
619
+ check: ctx => ctx.stats.sessions >= 1,
620
+ progressHint: ctx => `${ctx.stats.sessions}/1 sessions`,
621
+ },
622
+ {
623
+ id: 'first_dream',
624
+ name: 'First Dream',
625
+ description: 'Complete your first dream cycle',
626
+ icon: '*',
627
+ check: ctx => ctx.dreamCycles >= 1,
628
+ progressHint: ctx => `${ctx.dreamCycles}/1 dream cycles`,
629
+ },
630
+ {
631
+ id: 'pattern_seeker',
632
+ name: 'Pattern Seeker',
633
+ description: 'Reach 10 cached patterns in the learning engine',
634
+ icon: '?',
635
+ check: ctx => ctx.stats.patternsCount >= 10,
636
+ progressHint: ctx => `${ctx.stats.patternsCount}/10 patterns`,
637
+ },
638
+ {
639
+ id: 'solution_architect',
640
+ name: 'Solution Architect',
641
+ description: 'Reach 25 cached solutions',
642
+ icon: '!',
643
+ check: ctx => ctx.stats.solutionsCount >= 25,
644
+ progressHint: ctx => `${ctx.stats.solutionsCount}/25 solutions`,
645
+ },
646
+ {
647
+ id: 'night_owl',
648
+ name: 'Night Owl',
649
+ description: 'Use kbot after midnight',
650
+ icon: '@',
651
+ check: ctx => ctx.currentHour >= 0 && ctx.currentHour < 5,
652
+ progressHint: () => 'Use kbot between 12am-5am',
653
+ },
654
+ {
655
+ id: 'centurion',
656
+ name: 'Centurion',
657
+ description: 'Reach 100 sessions',
658
+ icon: '#',
659
+ check: ctx => ctx.stats.sessions >= 100,
660
+ progressHint: ctx => `${ctx.stats.sessions}/100 sessions`,
661
+ },
662
+ {
663
+ id: 'tool_master',
664
+ name: 'Tool Master',
665
+ description: 'Use 50 different tools',
666
+ icon: '%',
667
+ check: ctx => ctx.uniqueToolsUsed >= 50,
668
+ progressHint: ctx => `${ctx.uniqueToolsUsed}/50 unique tools`,
669
+ },
670
+ {
671
+ id: 'dream_weaver',
672
+ name: 'Dream Weaver',
673
+ description: 'Reach 10 dream insights',
674
+ icon: '~',
675
+ check: ctx => ctx.dreamInsights >= 10,
676
+ progressHint: ctx => `${ctx.dreamInsights}/10 dream insights`,
677
+ },
678
+ {
679
+ id: 'speed_demon',
680
+ name: 'Speed Demon',
681
+ description: 'Complete 5 sessions in one day',
682
+ icon: '>',
683
+ check: ctx => ctx.sessionsToday >= 5,
684
+ progressHint: ctx => `${ctx.sessionsToday}/5 sessions today`,
685
+ },
686
+ {
687
+ id: 'memory_palace',
688
+ name: 'Memory Palace',
689
+ description: 'Reach 50 facts in the knowledge base',
690
+ icon: '^',
691
+ check: ctx => ctx.stats.knowledgeCount >= 50,
692
+ progressHint: ctx => `${ctx.stats.knowledgeCount}/50 knowledge facts`,
693
+ },
694
+ {
695
+ id: 'companion_bond',
696
+ name: 'Companion Bond',
697
+ description: 'Rename your buddy',
698
+ icon: '&',
699
+ check: ctx => ctx.buddyRenamed,
700
+ progressHint: () => 'Use buddy_rename to name your companion',
701
+ },
702
+ {
703
+ id: 'chatterbox',
704
+ name: 'Chatterbox',
705
+ description: 'Send 500 messages',
706
+ icon: '$',
707
+ check: ctx => ctx.stats.totalMessages >= 500,
708
+ progressHint: ctx => `${ctx.stats.totalMessages}/500 messages`,
709
+ },
710
+ {
711
+ id: 'streak_7',
712
+ name: 'On a Roll',
713
+ description: 'Use kbot 7 days in a row',
714
+ icon: '=',
715
+ check: ctx => ctx.streakDays >= 7,
716
+ progressHint: ctx => `${ctx.streakDays}/7 day streak`,
717
+ },
718
+ {
719
+ id: 'polyglot',
720
+ name: 'Polyglot',
721
+ description: 'Configure 3 or more AI providers',
722
+ icon: ':',
723
+ check: ctx => ctx.providersConfigured >= 3,
724
+ progressHint: ctx => `${ctx.providersConfigured}/3 providers`,
725
+ },
726
+ {
727
+ id: 'deep_dreamer',
728
+ name: 'Deep Dreamer',
729
+ description: 'Complete 25 dream cycles',
730
+ icon: '(',
731
+ check: ctx => ctx.dreamCycles >= 25,
732
+ progressHint: ctx => `${ctx.dreamCycles}/25 dream cycles`,
733
+ },
734
+ {
735
+ id: 'thousand_voices',
736
+ name: 'Thousand Voices',
737
+ description: 'Send 1,000 messages',
738
+ icon: ')',
739
+ check: ctx => ctx.stats.totalMessages >= 1000,
740
+ progressHint: ctx => `${ctx.stats.totalMessages}/1000 messages`,
741
+ },
742
+ {
743
+ id: 'knowledge_sage',
744
+ name: 'Knowledge Sage',
745
+ description: 'Reach 100 facts in the knowledge base',
746
+ icon: '{',
747
+ check: ctx => ctx.stats.knowledgeCount >= 100,
748
+ progressHint: ctx => `${ctx.stats.knowledgeCount}/100 knowledge facts`,
749
+ },
750
+ {
751
+ id: 'streak_30',
752
+ name: 'Ironclad',
753
+ description: 'Use kbot 30 days in a row',
754
+ icon: '}',
755
+ check: ctx => ctx.streakDays >= 30,
756
+ progressHint: ctx => `${ctx.streakDays}/30 day streak`,
757
+ },
758
+ ];
759
+ // ── Achievement Engine ──
760
+ /** Record today's usage date for streak tracking */
761
+ function recordUsageDate() {
762
+ const config = loadBuddyConfig();
763
+ const today = new Date().toISOString().slice(0, 10);
764
+ const dates = config.usageDates ?? [];
765
+ dates.push(today);
766
+ // Keep last 90 days of usage data to bound storage
767
+ const cutoff = new Date();
768
+ cutoff.setDate(cutoff.getDate() - 90);
769
+ const cutoffStr = cutoff.toISOString().slice(0, 10);
770
+ config.usageDates = dates.filter(d => d >= cutoffStr);
771
+ saveBuddyConfig(config);
772
+ }
773
+ /**
774
+ * Check all achievement conditions and unlock any newly earned.
775
+ * Returns the list of newly unlocked achievements (empty if none).
776
+ * Call this at session end in agent.ts.
777
+ */
778
+ export function checkAchievements() {
779
+ recordUsageDate();
780
+ const config = loadBuddyConfig();
781
+ const existing = config.achievements ?? [];
782
+ const unlockedIds = new Set(existing.map(a => a.id));
783
+ const ctx = buildAchievementContext();
784
+ const newlyUnlocked = [];
785
+ for (const def of ACHIEVEMENT_DEFS) {
786
+ if (unlockedIds.has(def.id))
787
+ continue;
788
+ if (def.check(ctx)) {
789
+ const now = new Date().toISOString();
790
+ existing.push({ id: def.id, unlockedAt: now });
791
+ newlyUnlocked.push({
792
+ id: def.id,
793
+ name: def.name,
794
+ description: def.description,
795
+ icon: def.icon,
796
+ unlockedAt: now,
797
+ });
798
+ }
799
+ }
800
+ if (newlyUnlocked.length > 0) {
801
+ config.achievements = existing;
802
+ saveBuddyConfig(config);
803
+ }
804
+ return newlyUnlocked;
805
+ }
806
+ /**
807
+ * Get all achievements with their unlock status.
808
+ * Unlocked ones include the timestamp; locked ones show null.
809
+ */
810
+ export function getAchievements() {
811
+ const config = loadBuddyConfig();
812
+ const unlockedMap = new Map((config.achievements ?? []).map(a => [a.id, a.unlockedAt]));
813
+ return ACHIEVEMENT_DEFS.map(def => ({
814
+ id: def.id,
815
+ name: def.name,
816
+ description: def.description,
817
+ icon: def.icon,
818
+ unlockedAt: unlockedMap.get(def.id) ?? null,
819
+ }));
820
+ }
821
+ /**
822
+ * Get a progress hint for a locked achievement.
823
+ * Returns null if the achievement is unlocked or not found.
824
+ */
825
+ export function getAchievementProgress(achievementId) {
826
+ const def = ACHIEVEMENT_DEFS.find(d => d.id === achievementId);
827
+ if (!def)
828
+ return null;
829
+ const config = loadBuddyConfig();
830
+ const isUnlocked = (config.achievements ?? []).some(a => a.id === achievementId);
831
+ if (isUnlocked)
832
+ return null;
833
+ const ctx = buildAchievementContext();
834
+ return def.progressHint(ctx);
835
+ }
836
+ /**
837
+ * Format achievement unlock notification for terminal display.
838
+ * Shows the buddy sprite in success mood with a celebration message.
839
+ */
840
+ export function formatAchievementUnlock(achievement) {
841
+ const name = resolveName();
842
+ const species = resolveSpecies();
843
+ const sprite = getBuddySprite('success');
844
+ const text = `Achievement unlocked: ${achievement.name}!`;
845
+ const inner = ` ${achievement.icon} ${text} ${achievement.icon} `;
846
+ const width = inner.length;
847
+ const top = '.' + '-'.repeat(width) + '.';
848
+ const mid = '|' + inner + '|';
849
+ const bot = "'" + '-'.repeat(width) + "'";
850
+ const lines = [];
851
+ lines.push(` ${top}`);
852
+ lines.push(` ${mid}`);
853
+ lines.push(` ${bot}`);
854
+ for (const line of sprite) {
855
+ lines.push(` ${line}`);
856
+ }
857
+ lines.push(` ~ ${name} the ${species} ~`);
858
+ lines.push(` ${achievement.description}`);
859
+ return lines.join('\n');
860
+ }
393
861
  // ── Public API ──
394
- /** Get the buddy's current state (species, name, mood) */
862
+ /** Get the buddy's current state (species, name, mood, evolution) */
395
863
  export function getBuddy() {
396
864
  return {
397
865
  species: resolveSpecies(),
398
866
  name: resolveName(),
399
867
  mood: currentMood,
868
+ evolution: resolveEvolution(),
400
869
  };
401
870
  }
402
871
  /** Set the buddy's mood */
403
872
  export function setBuddyMood(mood) {
404
873
  currentMood = mood;
405
874
  }
406
- /** Get the ASCII sprite for the buddy in the given mood (defaults to current) */
875
+ /** Get the ASCII sprite for the buddy in the given mood (defaults to current).
876
+ * Applies evolution visual upgrades based on the buddy's current level. */
407
877
  export function getBuddySprite(mood) {
408
878
  const m = mood ?? currentMood;
409
879
  const species = resolveSpecies();
410
- return SPRITES[species][m];
880
+ const evo = resolveEvolution();
881
+ return applySpriteEvolution(species, m, evo.level);
411
882
  }
412
883
  /** Get a random greeting for the buddy */
413
884
  export function getBuddyGreeting() {
@@ -423,6 +894,60 @@ export function renameBuddy(newName) {
423
894
  saveBuddyConfig(config);
424
895
  cachedName = config.name;
425
896
  }
897
+ /**
898
+ * Add XP to the buddy. Checks for level-ups and persists to buddy.json.
899
+ * Returns the updated level info, and whether a level-up just occurred.
900
+ *
901
+ * XP sources:
902
+ * - Session complete: +1
903
+ * - Dream cycle: +2
904
+ * - Tool creation: +3
905
+ * - First error fix: +1
906
+ */
907
+ export function addBuddyXP(amount) {
908
+ const config = loadBuddyConfig();
909
+ const evo = config.evolution ?? { level: 0, xp: 0, evolvedAt: [] };
910
+ const prevLevel = evo.level;
911
+ evo.xp += amount;
912
+ const newLevel = computeLevel(evo.xp);
913
+ let leveledUp = false;
914
+ if (newLevel > prevLevel) {
915
+ evo.level = newLevel;
916
+ evo.evolvedAt.push(new Date().toISOString());
917
+ leveledUp = true;
918
+ }
919
+ config.evolution = evo;
920
+ saveBuddyConfig(config);
921
+ cachedEvolution = evo;
922
+ const species = resolveSpecies();
923
+ const nextLevel = (newLevel < 3 ? (newLevel + 1) : null);
924
+ const xpToNext = nextLevel !== null ? LEVEL_THRESHOLDS[nextLevel] - evo.xp : null;
925
+ return {
926
+ levelInfo: {
927
+ level: evo.level,
928
+ xp: evo.xp,
929
+ xpToNext,
930
+ title: LEVEL_TITLES[species][evo.level],
931
+ },
932
+ leveledUp,
933
+ };
934
+ }
935
+ /**
936
+ * Get the buddy's current level info without modifying state.
937
+ * Includes level, XP, XP to next level, and species-specific title.
938
+ */
939
+ export function getBuddyLevel() {
940
+ const species = resolveSpecies();
941
+ const evo = resolveEvolution();
942
+ const nextLevel = (evo.level < 3 ? (evo.level + 1) : null);
943
+ const xpToNext = nextLevel !== null ? LEVEL_THRESHOLDS[nextLevel] - evo.xp : null;
944
+ return {
945
+ level: evo.level,
946
+ xp: evo.xp,
947
+ xpToNext,
948
+ title: LEVEL_TITLES[species][evo.level],
949
+ };
950
+ }
426
951
  /** Pick a random message for the current mood */
427
952
  function moodMessage() {
428
953
  const msgs = MOOD_MESSAGES[currentMood];
@@ -446,6 +971,7 @@ export function formatBuddyStatus(message) {
446
971
  const name = resolveName();
447
972
  const species = resolveSpecies();
448
973
  const sprite = getBuddySprite();
974
+ const lvl = getBuddyLevel();
449
975
  const text = message || moodMessage();
450
976
  // Build speech bubble
451
977
  const inner = ` ${text} `;
@@ -461,8 +987,107 @@ export function formatBuddyStatus(message) {
461
987
  for (const line of sprite) {
462
988
  lines.push(` ${line}`);
463
989
  }
464
- // Name tag
990
+ // Name tag with level title
991
+ const xpBar = lvl.xpToNext !== null ? ` [${lvl.xp}/${lvl.xp + lvl.xpToNext} XP]` : ' [MAX]';
465
992
  lines.push(` ~ ${name} the ${species} ~`);
993
+ lines.push(` Lv.${lvl.level} ${lvl.title}${xpBar}`);
466
994
  return lines.join('\n');
467
995
  }
996
+ // ── Dream Narration ──
997
+ /** Max narrated IDs to keep in buddy.json (rolling window) */
998
+ const MAX_NARRATED_IDS = 200;
999
+ /** 24 hours in milliseconds */
1000
+ const RECENT_THRESHOLD_MS = 24 * 60 * 60 * 1000;
1001
+ /**
1002
+ * Narration templates per dream category.
1003
+ * Each array has several options — one is picked at random for variety.
1004
+ * The placeholder `%s` is replaced with the insight content.
1005
+ */
1006
+ const NARRATION_TEMPLATES = {
1007
+ preference: [
1008
+ 'I noticed you always %s. I\'ll remember that.',
1009
+ 'I dreamed about how you %s. Makes sense to me.',
1010
+ 'Something I picked up on — you %s. Noted.',
1011
+ ],
1012
+ pattern: [
1013
+ 'I learned that when you %s, things go better.',
1014
+ 'I dreamed about your workflow — %s.',
1015
+ 'I noticed a pattern: %s.',
1016
+ ],
1017
+ project: [
1018
+ 'About your project — %s.',
1019
+ 'I was thinking about the codebase. %s.',
1020
+ 'Dreamed about the project last night. %s.',
1021
+ ],
1022
+ relationship: [
1023
+ 'Working with you, I\'ve learned %s.',
1024
+ 'I picked up on something between us — %s.',
1025
+ 'After our sessions together, %s.',
1026
+ ],
1027
+ music: [
1028
+ 'Your music sessions taught me — %s.',
1029
+ 'I dreamed in sound last night. %s.',
1030
+ 'Something about your beats — %s.',
1031
+ ],
1032
+ skill: [
1033
+ 'I can tell you\'re getting sharper at %s.',
1034
+ 'I dreamed about your skills — %s.',
1035
+ 'You\'re leveling up. %s.',
1036
+ ],
1037
+ };
1038
+ /**
1039
+ * Convert an insight's content into a casual first-person buddy sentence.
1040
+ * Lowercases the first character of the insight to flow naturally into templates.
1041
+ */
1042
+ function narrateInsight(insight) {
1043
+ const templates = NARRATION_TEMPLATES[insight.category] ?? NARRATION_TEMPLATES.pattern;
1044
+ const template = templates[Math.floor(Math.random() * templates.length)];
1045
+ // Lowercase the first char so it reads naturally after the template prefix
1046
+ const content = insight.content.charAt(0).toLowerCase() + insight.content.slice(1);
1047
+ // Strip trailing period from content if the template already ends the sentence
1048
+ const trimmed = content.replace(/\.\s*$/, '');
1049
+ return template.replace('%s', trimmed);
1050
+ }
1051
+ /**
1052
+ * Get a dream narration for the buddy to tell the user at startup.
1053
+ *
1054
+ * Picks the highest-relevance insight that was reinforced in the last 24 hours
1055
+ * and hasn't already been narrated. Returns `null` if there's nothing new to say.
1056
+ *
1057
+ * Tracks narrated insight IDs in buddy.json to avoid repeats.
1058
+ */
1059
+ export function getBuddyDreamNarration() {
1060
+ let status;
1061
+ try {
1062
+ status = getDreamStatus();
1063
+ }
1064
+ catch {
1065
+ return null;
1066
+ }
1067
+ const { insights } = status;
1068
+ if (insights.length === 0)
1069
+ return null;
1070
+ const config = loadBuddyConfig();
1071
+ const narrated = new Set(config.narratedDreamIds ?? []);
1072
+ const now = Date.now();
1073
+ // Find the best un-narrated insight reinforced in the last 24 hours
1074
+ const candidate = insights
1075
+ .filter(i => {
1076
+ if (narrated.has(i.id))
1077
+ return false;
1078
+ const reinforcedAge = now - new Date(i.lastReinforced).getTime();
1079
+ return reinforcedAge < RECENT_THRESHOLD_MS && i.relevance > 0.3;
1080
+ })
1081
+ .sort((a, b) => b.relevance - a.relevance)[0] ?? null;
1082
+ if (!candidate)
1083
+ return null;
1084
+ // Record this insight as narrated
1085
+ const updatedIds = [...(config.narratedDreamIds ?? []), candidate.id];
1086
+ // Keep the list bounded
1087
+ config.narratedDreamIds = updatedIds.length > MAX_NARRATED_IDS
1088
+ ? updatedIds.slice(-MAX_NARRATED_IDS)
1089
+ : updatedIds;
1090
+ saveBuddyConfig(config);
1091
+ return narrateInsight(candidate);
1092
+ }
468
1093
  //# sourceMappingURL=buddy.js.map
package/dist/cli.js CHANGED
@@ -27,7 +27,7 @@ import { banner, bannerCompact, bannerAuth, prompt as kbotPrompt, printError, pr
27
27
  import { checkForUpdate, selfUpdate } from './updater.js';
28
28
  import { runTutorial } from './tutorial.js';
29
29
  import { syncOnStartup, schedulePush, flushCloudSync, isCloudSyncEnabled, setCloudToken, getCloudToken } from './cloud-sync.js';
30
- import { getBuddy, getBuddyGreeting, formatBuddyStatus } from './buddy.js';
30
+ import { getBuddy, getBuddyGreeting, formatBuddyStatus, getBuddyDreamNarration } from './buddy.js';
31
31
  import chalk from 'chalk';
32
32
  import { createRequire } from 'node:module';
33
33
  const __require = createRequire(import.meta.url);
@@ -4277,6 +4277,17 @@ async function startRepl(agentOpts, context, tier, byokActive = false, localActi
4277
4277
  : getBuddyGreeting();
4278
4278
  console.log();
4279
4279
  console.log(formatBuddyStatus(greeting));
4280
+ // Dream narration — buddy tells the user what it dreamed about
4281
+ if (!isFirstRun) {
4282
+ try {
4283
+ const dreamNarration = getBuddyDreamNarration();
4284
+ if (dreamNarration) {
4285
+ console.log();
4286
+ console.log(formatBuddyStatus(dreamNarration));
4287
+ }
4288
+ }
4289
+ catch { /* dream narration is non-critical */ }
4290
+ }
4280
4291
  console.log();
4281
4292
  }
4282
4293
  // Seed knowledge on first run — give new users a head start
@@ -1,10 +1,11 @@
1
1
  // kbot Buddy Tools — Interact with your terminal companion
2
2
  //
3
- // Two tools:
4
- // buddy_status — Show buddy name, species, mood, and sprite
5
- // buddy_rename — Give your buddy a custom name (persisted to ~/.kbot/buddy.json)
3
+ // Three tools:
4
+ // buddy_status — Show buddy name, species, mood, and sprite
5
+ // buddy_rename — Give your buddy a custom name (persisted to ~/.kbot/buddy.json)
6
+ // buddy_achievements — Show all achievements with unlock status and progress
6
7
  import { registerTool } from './index.js';
7
- import { getBuddy, getBuddySprite, getBuddyGreeting, formatBuddyStatus, renameBuddy, } from '../buddy.js';
8
+ import { getBuddy, getBuddySprite, getBuddyGreeting, getBuddyLevel, formatBuddyStatus, renameBuddy, getAchievements, getAchievementProgress, } from '../buddy.js';
8
9
  const VALID_MOODS = ['idle', 'thinking', 'success', 'error', 'learning'];
9
10
  export function registerBuddyTools() {
10
11
  registerTool({
@@ -19,16 +20,22 @@ export function registerBuddyTools() {
19
20
  tier: 'free',
20
21
  async execute(args) {
21
22
  const buddy = getBuddy();
23
+ const lvl = getBuddyLevel();
22
24
  const mood = args.mood ? String(args.mood) : undefined;
23
25
  if (mood && !VALID_MOODS.includes(mood)) {
24
26
  return `Unknown mood "${mood}". Valid moods: ${VALID_MOODS.join(', ')}`;
25
27
  }
26
28
  const sprite = getBuddySprite(mood).join('\n');
27
29
  const greeting = getBuddyGreeting();
30
+ const xpProgress = lvl.xpToNext !== null
31
+ ? `${lvl.xp}/${lvl.xp + lvl.xpToNext} XP (${lvl.xpToNext} to next)`
32
+ : `${lvl.xp} XP (MAX)`;
28
33
  return [
29
34
  `Name: ${buddy.name}`,
30
35
  `Species: ${buddy.species}`,
31
36
  `Mood: ${mood || buddy.mood}`,
37
+ `Level: ${lvl.level} — ${lvl.title}`,
38
+ `XP: ${xpProgress}`,
32
39
  '',
33
40
  sprite,
34
41
  '',
@@ -59,5 +66,41 @@ export function registerBuddyTools() {
59
66
  return formatBuddyStatus(`${oldName} is now ${newName}!`);
60
67
  },
61
68
  });
69
+ registerTool({
70
+ name: 'buddy_achievements',
71
+ description: 'Show all buddy achievements — unlocked ones with date, locked ones with progress hints. Milestones that unlock as you use kbot.',
72
+ parameters: {},
73
+ tier: 'free',
74
+ async execute() {
75
+ const achievements = getAchievements();
76
+ const buddy = getBuddy();
77
+ const unlocked = achievements.filter(a => a.unlockedAt !== null);
78
+ const locked = achievements.filter(a => a.unlockedAt === null);
79
+ const lines = [];
80
+ lines.push(`=== ${buddy.name}'s Achievements ===`);
81
+ lines.push(`${unlocked.length}/${achievements.length} unlocked`);
82
+ lines.push('');
83
+ if (unlocked.length > 0) {
84
+ lines.push('-- Unlocked --');
85
+ for (const a of unlocked) {
86
+ const date = new Date(a.unlockedAt).toLocaleDateString();
87
+ lines.push(` [${a.icon}] ${a.name} — ${a.description}`);
88
+ lines.push(` Unlocked ${date}`);
89
+ }
90
+ lines.push('');
91
+ }
92
+ if (locked.length > 0) {
93
+ lines.push('-- Locked --');
94
+ for (const a of locked) {
95
+ const progress = getAchievementProgress(a.id);
96
+ lines.push(` [ ] ${a.name} — ${a.description}`);
97
+ if (progress) {
98
+ lines.push(` Progress: ${progress}`);
99
+ }
100
+ }
101
+ }
102
+ return lines.join('\n');
103
+ },
104
+ });
62
105
  }
63
106
  //# sourceMappingURL=buddy-tools.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kernel.chat/kbot",
3
- "version": "3.67.0",
4
- "description": "Open-source terminal AI agent. 686+ tools, 35 agents, 20 providers. Fully local, fully sovereign. MIT.",
3
+ "version": "3.68.1",
4
+ "description": "Open-source terminal AI agent. 693+ tools, 35 agents, 20 providers. Dreams, learns, watches your system. Fully local, fully sovereign. MIT.",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",