@kernel.chat/kbot 3.72.0 → 3.73.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/dist/buddy.d.ts CHANGED
@@ -74,6 +74,27 @@ export declare function addBuddyXP(amount: number): {
74
74
  levelInfo: BuddyLevelInfo;
75
75
  leveledUp: boolean;
76
76
  };
77
+ /**
78
+ * Sync buddy stats to the cloud leaderboard.
79
+ * Anonymous — uses a SHA-256 hash of hostname+homedir, not user identity.
80
+ * Requires a kernel.chat token (cloud sync enabled).
81
+ */
82
+ export declare function syncBuddyToCloud(): Promise<boolean>;
83
+ /**
84
+ * Fetch the buddy leaderboard from the cloud.
85
+ * Returns ranked entries sorted by XP descending.
86
+ */
87
+ export declare function fetchBuddyLeaderboard(opts?: {
88
+ limit?: number;
89
+ species?: string;
90
+ }): Promise<Array<{
91
+ species: string;
92
+ level: number;
93
+ xp: number;
94
+ achievement_count: number;
95
+ sessions: number;
96
+ rank: number;
97
+ }>>;
77
98
  /**
78
99
  * Get the buddy's current level info without modifying state.
79
100
  * Includes level, XP, XP to next level, and species-specific title.
package/dist/buddy.js CHANGED
@@ -7,7 +7,7 @@
7
7
  // Achievements: milestones that unlock as the user uses kbot. Persisted in buddy.json.
8
8
  //
9
9
  // Persists buddy name + achievements to ~/.kbot/buddy.json
10
- import { homedir } from 'node:os';
10
+ import { homedir, hostname } from 'node:os';
11
11
  import { join } from 'node:path';
12
12
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
13
13
  import { createHash } from 'node:crypto';
@@ -15,6 +15,7 @@ import { createInterface } from 'node:readline';
15
15
  import { getDreamStatus, getDreamPrompt } from './dream.js';
16
16
  import { getExtendedStats, getProfileSummary } from './learning.js';
17
17
  import { getToolMetrics } from './tools/index.js';
18
+ import { getCloudToken } from './cloud-sync.js';
18
19
  // ── Paths ──
19
20
  const KBOT_DIR = join(homedir(), '.kbot');
20
21
  const BUDDY_FILE = join(KBOT_DIR, 'buddy.json');
@@ -930,6 +931,8 @@ export function addBuddyXP(amount) {
930
931
  config.evolution = evo;
931
932
  saveBuddyConfig(config);
932
933
  cachedEvolution = evo;
934
+ // Debounced cloud sync — leaderboard update
935
+ scheduleBuddySync();
933
936
  const species = resolveSpecies();
934
937
  const nextLevel = (newLevel < 3 ? (newLevel + 1) : null);
935
938
  const xpToNext = nextLevel !== null ? LEVEL_THRESHOLDS[nextLevel] - evo.xp : null;
@@ -943,6 +946,99 @@ export function addBuddyXP(amount) {
943
946
  leveledUp,
944
947
  };
945
948
  }
949
+ // ── Cloud Sync — Buddy Leaderboard ──
950
+ const ENGINE_URL = 'https://eoxxpyixdieprsxlpwcs.supabase.co/functions/v1/kbot-engine';
951
+ const BUDDY_SYNC_DEBOUNCE_MS = 5 * 60 * 1000; // max once per 5 minutes
952
+ let buddySyncTimer = null;
953
+ let lastBuddySync = 0;
954
+ /** Generate an anonymous device hash from hostname + homedir */
955
+ function getDeviceHash() {
956
+ return createHash('sha256')
957
+ .update(`${hostname()}:${homedir()}`)
958
+ .digest('hex');
959
+ }
960
+ /**
961
+ * Sync buddy stats to the cloud leaderboard.
962
+ * Anonymous — uses a SHA-256 hash of hostname+homedir, not user identity.
963
+ * Requires a kernel.chat token (cloud sync enabled).
964
+ */
965
+ export async function syncBuddyToCloud() {
966
+ const token = getCloudToken();
967
+ if (!token)
968
+ return false;
969
+ try {
970
+ const buddy = getBuddy();
971
+ const lvl = getBuddyLevel();
972
+ const achievements = getAchievements();
973
+ const stats = getExtendedStats();
974
+ const unlockedCount = achievements.filter(a => a.unlockedAt !== null).length;
975
+ const res = await fetch(`${ENGINE_URL}/sync`, {
976
+ method: 'POST',
977
+ headers: {
978
+ 'Content-Type': 'application/json',
979
+ 'Authorization': `Bearer ${token}`,
980
+ },
981
+ body: JSON.stringify({
982
+ action: 'buddy_sync',
983
+ device_hash: getDeviceHash(),
984
+ species: buddy.species,
985
+ level: lvl.level,
986
+ xp: lvl.xp,
987
+ achievement_count: unlockedCount,
988
+ sessions: stats.sessions,
989
+ }),
990
+ signal: AbortSignal.timeout(10_000),
991
+ });
992
+ return res.ok;
993
+ }
994
+ catch {
995
+ return false;
996
+ }
997
+ }
998
+ /** Debounced buddy sync — called from addBuddyXP, max once per 5 minutes */
999
+ function scheduleBuddySync() {
1000
+ const now = Date.now();
1001
+ if (now - lastBuddySync < BUDDY_SYNC_DEBOUNCE_MS)
1002
+ return;
1003
+ if (buddySyncTimer)
1004
+ return;
1005
+ buddySyncTimer = setTimeout(() => {
1006
+ buddySyncTimer = null;
1007
+ lastBuddySync = Date.now();
1008
+ syncBuddyToCloud().catch(() => { }); // fire and forget
1009
+ }, 1000); // short delay to batch rapid XP gains
1010
+ }
1011
+ /**
1012
+ * Fetch the buddy leaderboard from the cloud.
1013
+ * Returns ranked entries sorted by XP descending.
1014
+ */
1015
+ export async function fetchBuddyLeaderboard(opts) {
1016
+ const token = getCloudToken();
1017
+ if (!token)
1018
+ return [];
1019
+ try {
1020
+ const res = await fetch(`${ENGINE_URL}/sync`, {
1021
+ method: 'POST',
1022
+ headers: {
1023
+ 'Content-Type': 'application/json',
1024
+ 'Authorization': `Bearer ${token}`,
1025
+ },
1026
+ body: JSON.stringify({
1027
+ action: 'buddy_leaderboard',
1028
+ limit: opts?.limit ?? 50,
1029
+ ...(opts?.species ? { species: opts.species } : {}),
1030
+ }),
1031
+ signal: AbortSignal.timeout(10_000),
1032
+ });
1033
+ if (!res.ok)
1034
+ return [];
1035
+ const data = await res.json();
1036
+ return data.leaderboard ?? [];
1037
+ }
1038
+ catch {
1039
+ return [];
1040
+ }
1041
+ }
946
1042
  /**
947
1043
  * Get the buddy's current level info without modifying state.
948
1044
  * Includes level, XP, XP to next level, and species-specific title.
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, getBuddyDreamNarration, renameBuddy, buddyChat, getAchievements, getBuddyLevel } from './buddy.js';
30
+ import { getBuddy, getBuddyGreeting, formatBuddyStatus, getBuddyDreamNarration, renameBuddy, buddyChat, getAchievements, getBuddyLevel, fetchBuddyLeaderboard } from './buddy.js';
31
31
  import chalk from 'chalk';
32
32
  import { createRequire } from 'node:module';
33
33
  const __require = createRequire(import.meta.url);
@@ -3579,6 +3579,62 @@ async function main() {
3579
3579
  console.log();
3580
3580
  process.exit(0);
3581
3581
  });
3582
+ buddyCmd
3583
+ .command('leaderboard')
3584
+ .description('Show the global buddy leaderboard — anonymous rankings across all kbot installs')
3585
+ .option('-l, --limit <n>', 'Number of entries to show', '20')
3586
+ .option('-s, --species <species>', 'Filter by species (fox, owl, cat, robot, ghost, mushroom, octopus, dragon)')
3587
+ .action(async (opts) => {
3588
+ const limit = Math.min(Math.max(parseInt(opts.limit, 10) || 20, 1), 200);
3589
+ const species = opts.species?.toLowerCase();
3590
+ const validSpecies = ['fox', 'owl', 'cat', 'robot', 'ghost', 'mushroom', 'octopus', 'dragon'];
3591
+ if (species && !validSpecies.includes(species)) {
3592
+ printError(`Unknown species "${species}". Valid: ${validSpecies.join(', ')}`);
3593
+ process.exit(1);
3594
+ }
3595
+ printInfo('Fetching leaderboard...');
3596
+ const entries = await fetchBuddyLeaderboard({ limit, species });
3597
+ if (entries.length === 0) {
3598
+ console.log();
3599
+ printWarn('No entries on the leaderboard yet.');
3600
+ printInfo('Use kbot to earn XP and enable cloud sync to appear on the leaderboard.');
3601
+ console.log();
3602
+ process.exit(0);
3603
+ }
3604
+ const SPECIES_ICONS = {
3605
+ fox: '[fox]', owl: '[owl]', cat: '[cat]', robot: '[bot]',
3606
+ ghost: '[gho]', mushroom: '[msh]', octopus: '[oct]', dragon: '[drg]',
3607
+ };
3608
+ const LEVEL_TITLES_SHORT = {
3609
+ 0: 'Novice', 1: 'Adept', 2: 'Master', 3: 'Legend',
3610
+ };
3611
+ const header = species
3612
+ ? `Buddy Leaderboard — ${species}`
3613
+ : 'Global Buddy Leaderboard';
3614
+ console.log();
3615
+ console.log(` ${chalk.bold(header)}`);
3616
+ console.log(` ${chalk.dim('─'.repeat(56))}`);
3617
+ console.log(` ${chalk.dim('#'.padStart(3))} ${chalk.dim('Species'.padEnd(7))} ${chalk.dim('Level'.padEnd(12))} ${chalk.dim('XP'.padStart(6))} ${chalk.dim('Achv'.padStart(4))} ${chalk.dim('Sessions'.padStart(8))}`);
3618
+ console.log(` ${chalk.dim('─'.repeat(56))}`);
3619
+ for (const entry of entries) {
3620
+ const icon = SPECIES_ICONS[entry.species] || entry.species.slice(0, 5);
3621
+ const title = LEVEL_TITLES_SHORT[entry.level] ?? `L${entry.level}`;
3622
+ const levelStr = `${entry.level} ${title}`;
3623
+ const rankStr = String(entry.rank).padStart(3);
3624
+ const xpStr = String(entry.xp).padStart(6);
3625
+ const achvStr = String(entry.achievement_count).padStart(4);
3626
+ const sessStr = String(entry.sessions).padStart(8);
3627
+ // Highlight top 3
3628
+ const rankColor = entry.rank === 1 ? chalk.hex('#FFD700') :
3629
+ entry.rank === 2 ? chalk.hex('#C0C0C0') :
3630
+ entry.rank === 3 ? chalk.hex('#CD7F32') : chalk.white;
3631
+ console.log(` ${rankColor(rankStr)} ${chalk.hex('#A78BFA')(icon.padEnd(7))} ${levelStr.padEnd(12)} ${chalk.hex('#4ADE80')(xpStr)} ${achvStr} ${chalk.dim(sessStr)}`);
3632
+ }
3633
+ console.log();
3634
+ console.log(` ${chalk.dim(`${entries.length} entries shown`)}`);
3635
+ console.log();
3636
+ process.exit(0);
3637
+ });
3582
3638
  buddyCmd.action(() => {
3583
3639
  buddyCmd.commands.find(c => c.name() === 'status')?.parse(['', '', 'status']);
3584
3640
  });
@@ -1,12 +1,13 @@
1
1
  // kbot Buddy Tools — Interact with your terminal companion
2
2
  //
3
- // Four tools:
3
+ // Five tools:
4
4
  // buddy_status — Show buddy name, species, mood, and sprite
5
5
  // buddy_rename — Give your buddy a custom name (persisted to ~/.kbot/buddy.json)
6
6
  // buddy_achievements — Show all achievements with unlock status and progress
7
7
  // buddy_personality — Show species personality traits, style, and strength
8
+ // buddy_leaderboard — Global anonymous leaderboard across all kbot installs
8
9
  import { registerTool } from './index.js';
9
- import { getBuddy, getBuddySprite, getBuddyGreeting, getBuddyLevel, formatBuddyStatus, renameBuddy, getAchievements, getAchievementProgress, getSpeciesPersonality, } from '../buddy.js';
10
+ import { getBuddy, getBuddySprite, getBuddyGreeting, getBuddyLevel, formatBuddyStatus, renameBuddy, getAchievements, getAchievementProgress, getSpeciesPersonality, fetchBuddyLeaderboard, } from '../buddy.js';
10
11
  const VALID_MOODS = ['idle', 'thinking', 'success', 'error', 'learning', 'alert', 'dance', 'curious', 'proud'];
11
12
  export function registerBuddyTools() {
12
13
  registerTool({
@@ -124,5 +125,57 @@ export function registerBuddyTools() {
124
125
  ].join('\n');
125
126
  },
126
127
  });
128
+ registerTool({
129
+ name: 'buddy_leaderboard',
130
+ description: 'Show the global buddy leaderboard — anonymous rankings of all kbot buddies across installs, sorted by XP. Requires cloud sync (kernel.chat token).',
131
+ parameters: {
132
+ limit: {
133
+ type: 'number',
134
+ description: 'Number of entries to show (default 20, max 200)',
135
+ },
136
+ species: {
137
+ type: 'string',
138
+ description: 'Filter by species: fox, owl, cat, robot, ghost, mushroom, octopus, dragon',
139
+ },
140
+ },
141
+ tier: 'free',
142
+ async execute(args) {
143
+ const SPECIES_ICONS = {
144
+ fox: '[fox]', owl: '[owl]', cat: '[cat]', robot: '[bot]',
145
+ ghost: '[gho]', mushroom: '[msh]', octopus: '[oct]', dragon: '[drg]',
146
+ };
147
+ const LEVEL_TITLES_SHORT = {
148
+ 0: 'Novice', 1: 'Adept', 2: 'Master', 3: 'Legend',
149
+ };
150
+ const limit = Math.min(Math.max(Math.floor(Number(args.limit) || 20), 1), 200);
151
+ const species = args.species ? String(args.species).toLowerCase() : undefined;
152
+ const validSpecies = ['fox', 'owl', 'cat', 'robot', 'ghost', 'mushroom', 'octopus', 'dragon'];
153
+ if (species && !validSpecies.includes(species)) {
154
+ return `Unknown species "${species}". Valid: ${validSpecies.join(', ')}`;
155
+ }
156
+ const entries = await fetchBuddyLeaderboard({ limit, species });
157
+ if (entries.length === 0) {
158
+ return 'No entries on the leaderboard yet. Use kbot to earn XP and sync to the cloud!';
159
+ }
160
+ const lines = [];
161
+ const header = species
162
+ ? `=== Buddy Leaderboard — ${species} ===`
163
+ : '=== Global Buddy Leaderboard ===';
164
+ lines.push(header);
165
+ lines.push('');
166
+ // Table header
167
+ lines.push(` ${'#'.padStart(3)} ${'Species'.padEnd(7)} ${'Level'.padEnd(12)} ${'XP'.padStart(6)} ${'Achv'.padStart(4)} ${'Sessions'.padStart(8)}`);
168
+ lines.push(` ${'─'.repeat(3)} ${'─'.repeat(7)} ${'─'.repeat(12)} ${'─'.repeat(6)} ${'─'.repeat(4)} ${'─'.repeat(8)}`);
169
+ for (const entry of entries) {
170
+ const icon = SPECIES_ICONS[entry.species] || entry.species.slice(0, 5);
171
+ const title = LEVEL_TITLES_SHORT[entry.level] ?? `L${entry.level}`;
172
+ const levelStr = `${entry.level} ${title}`;
173
+ lines.push(` ${String(entry.rank).padStart(3)} ${icon.padEnd(7)} ${levelStr.padEnd(12)} ${String(entry.xp).padStart(6)} ${String(entry.achievement_count).padStart(4)} ${String(entry.sessions).padStart(8)}`);
174
+ }
175
+ lines.push('');
176
+ lines.push(`${entries.length} entries shown`);
177
+ return lines.join('\n');
178
+ },
179
+ });
127
180
  }
128
181
  //# sourceMappingURL=buddy-tools.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kernel.chat/kbot",
3
- "version": "3.72.0",
3
+ "version": "3.73.0",
4
4
  "description": "Open-source terminal AI agent. 764+ tools, 35 agents, 20 providers. Dreams, learns, watches your system. Controls your phone. Fully local, fully sovereign. MIT.",
5
5
  "type": "module",
6
6
  "repository": {