@kernel.chat/kbot 3.66.0 → 3.68.0
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 +26 -5
- package/dist/agent.js +13 -1
- package/dist/buddy.d.ts +77 -2
- package/dist/buddy.js +631 -6
- package/dist/cli.js +119 -1
- package/dist/dream.js +13 -0
- package/dist/tools/behavior-tools.d.ts +2 -0
- package/dist/tools/behavior-tools.js +63 -0
- package/dist/tools/buddy-tools.js +47 -4
- package/dist/tools/index.js +2 -0
- package/dist/tools/watchdog.d.ts +32 -0
- package/dist/tools/watchdog.js +356 -0
- package/dist/user-behavior.d.ts +65 -0
- package/dist/user-behavior.js +301 -0
- package/package.json +2 -2
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
|
-
//
|
|
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
|
-
|
|
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
|