Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d3a6e2202 |
@@ -42,6 +42,7 @@ const FEATURE_FLAGS: Record<string, boolean> = {
|
||||
ABLATION_BASELINE: false,
|
||||
DUMP_SYSTEM_PROMPT: false,
|
||||
CHICAGO_MCP: false,
|
||||
BUDDY: true,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -121,13 +121,12 @@ export function companionUserId(): string {
|
||||
return config.oauthAccount?.accountUuid ?? config.userID ?? 'anon'
|
||||
}
|
||||
|
||||
// Regenerate bones from userId, merge with stored soul. Bones never persist
|
||||
// so species renames and SPECIES-array edits can't break stored companions,
|
||||
// and editing config.companion can't fake a rarity.
|
||||
// Regenerate bones from userId (or custom seed), merge with stored soul.
|
||||
export function getCompanion(): Companion | undefined {
|
||||
const stored = getGlobalConfig().companion
|
||||
const config = getGlobalConfig()
|
||||
const stored = config.companion
|
||||
if (!stored) return undefined
|
||||
const { bones } = roll(companionUserId())
|
||||
// bones last so stale bones fields in old-format configs get overridden
|
||||
const seed = config.companionSeed ?? companionUserId()
|
||||
const { bones } = roll(seed)
|
||||
return { ...stored, ...bones }
|
||||
}
|
||||
|
||||
@@ -10,14 +10,10 @@ import { getRainbowColor } from '../utils/thinking.js';
|
||||
// buzz instead of a single UTC-midnight spike, gentler on soul-gen load.
|
||||
// Teaser window: April 1-7, 2026 only. Command stays live forever after.
|
||||
export function isBuddyTeaserWindow(): boolean {
|
||||
if ("external" === 'ant') return true;
|
||||
const d = new Date();
|
||||
return d.getFullYear() === 2026 && d.getMonth() === 3 && d.getDate() <= 7;
|
||||
return true;
|
||||
}
|
||||
export function isBuddyLive(): boolean {
|
||||
if ("external" === 'ant') return true;
|
||||
const d = new Date();
|
||||
return d.getFullYear() > 2026 || d.getFullYear() === 2026 && d.getMonth() >= 3;
|
||||
return true;
|
||||
}
|
||||
function RainbowText(t0) {
|
||||
const $ = _c(2);
|
||||
|
||||
+3
-1
@@ -115,12 +115,14 @@ const forkCmd = feature('FORK_SUBAGENT')
|
||||
require('./commands/fork/index.js') as typeof import('./commands/fork/index.js')
|
||||
).default
|
||||
: null
|
||||
const buddy = feature('BUDDY')
|
||||
const buddyCmd = feature('BUDDY')
|
||||
? (
|
||||
require('./commands/buddy/index.js') as typeof import('./commands/buddy/index.js')
|
||||
).default
|
||||
: null
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
import buddyFallback from './commands/buddy/index.js'
|
||||
const buddy = buddyCmd ?? buddyFallback
|
||||
import thinkback from './commands/thinkback/index.js'
|
||||
import thinkbackPlay from './commands/thinkback-play/index.js'
|
||||
import permissions from './commands/permissions/index.js'
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import type {
|
||||
LocalJSXCommandContext,
|
||||
LocalJSXCommandOnDone,
|
||||
} from '../../types/command.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||
import { getCompanion, roll, rollWithSeed } from '../../buddy/companion.js'
|
||||
import { renderSprite, renderFace } from '../../buddy/sprites.js'
|
||||
import { RARITY_STARS, SPECIES } from '../../buddy/types.js'
|
||||
|
||||
// Fair reroll: pick a random seed whose species has the fewest rolls.
|
||||
// Once all species reach the same count, the pool resets effectively.
|
||||
function fairRoll(): { seed: string; species: string } {
|
||||
const config = getGlobalConfig()
|
||||
const counts: Record<string, number> = { ...config.companionRollCounts }
|
||||
|
||||
// Find the minimum roll count
|
||||
const minCount = Math.min(...SPECIES.map(s => counts[s] ?? 0))
|
||||
|
||||
// Candidates: species at the minimum count
|
||||
const candidates = SPECIES.filter(s => (counts[s] ?? 0) === minCount)
|
||||
|
||||
// Pick a random candidate
|
||||
const target = candidates[Math.floor(Math.random() * candidates.length)]!
|
||||
|
||||
// Generate random seeds until we hit the target species
|
||||
let seed: string
|
||||
let result: ReturnType<typeof rollWithSeed>
|
||||
let attempts = 0
|
||||
do {
|
||||
seed = `reroll-${Date.now()}-${Math.random().toString(36).slice(2)}-${attempts}`
|
||||
result = rollWithSeed(seed)
|
||||
attempts++
|
||||
} while (result.bones.species !== target && attempts < 500)
|
||||
|
||||
return { seed, species: result.bones.species }
|
||||
}
|
||||
|
||||
function renderCard(): string {
|
||||
const companion = getCompanion()
|
||||
if (!companion) return 'No companion hatched yet. Run /buddy to hatch one!'
|
||||
|
||||
const sprite = renderSprite(companion)
|
||||
const face = renderFace(companion)
|
||||
const stars = RARITY_STARS[companion.rarity]
|
||||
const shinyTag = companion.shiny ? ' ✨ SHINY' : ''
|
||||
const statsLines = Object.entries(companion.stats)
|
||||
.map(([k, v]) => {
|
||||
const bar = '█'.repeat(Math.round(v / 5)) + '░'.repeat(20 - Math.round(v / 5))
|
||||
return ` ${k.padEnd(10)} ${bar} ${v}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
// Show collection progress
|
||||
const config = getGlobalConfig()
|
||||
const counts = config.companionRollCounts ?? {}
|
||||
const seen = SPECIES.filter(s => (counts[s] ?? 0) > 0).length
|
||||
const total = SPECIES.length
|
||||
|
||||
return [
|
||||
`┌─── ${companion.name} ───┐`,
|
||||
` Species: ${companion.species} ${stars}${shinyTag}`,
|
||||
` Rarity: ${companion.rarity}`,
|
||||
` Hat: ${companion.hat}`,
|
||||
` Eyes: ${companion.eye}`,
|
||||
` Face: ${face}`,
|
||||
'',
|
||||
...sprite.map(l => ` ${l}`),
|
||||
'',
|
||||
' Stats:',
|
||||
statsLines,
|
||||
'',
|
||||
` Personality: ${companion.personality}`,
|
||||
` Hatched: ${new Date(companion.hatchedAt).toLocaleDateString()}`,
|
||||
` Collection: ${seen}/${total} species discovered`,
|
||||
`└${'─'.repeat(companion.name.length + 6)}┘`,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function hatchWithSeed(seed: string) {
|
||||
const { bones } = rollWithSeed(seed)
|
||||
const name = `Buddy_${bones.species}`
|
||||
const soul = {
|
||||
name,
|
||||
personality: `A cheerful ${bones.species} companion with a knack for ${bones.rarity === 'legendary' ? 'legendary wisdom' : 'coding adventures'}.`,
|
||||
hatchedAt: Date.now(),
|
||||
}
|
||||
|
||||
// Update config: save companion, seed, and bump roll count
|
||||
saveGlobalConfig(cfg => {
|
||||
const counts = { ...cfg.companionRollCounts }
|
||||
counts[bones.species] = (counts[bones.species] ?? 0) + 1
|
||||
return { ...cfg, companion: soul, companionSeed: seed, companionRollCounts: counts }
|
||||
})
|
||||
|
||||
return { ...soul, ...bones }
|
||||
}
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: ToolUseContext & LocalJSXCommandContext,
|
||||
args: string,
|
||||
): Promise<null> {
|
||||
const sub = args.trim().toLowerCase()
|
||||
|
||||
// No companion yet — hatch one
|
||||
if (!sub || sub === 'hatch') {
|
||||
const config = getGlobalConfig()
|
||||
if (config.companion) {
|
||||
onDone(renderCard(), { display: 'system' })
|
||||
return null
|
||||
}
|
||||
const { seed } = fairRoll()
|
||||
const companion = hatchWithSeed(seed)
|
||||
const sprite = renderSprite(companion)
|
||||
const stars = RARITY_STARS[companion.rarity]
|
||||
const shinyTag = companion.shiny ? ' ✨ SHINY!' : ''
|
||||
onDone(
|
||||
[
|
||||
'🥚 *crack* ...',
|
||||
'',
|
||||
...sprite.map(l => ` ${l}`),
|
||||
'',
|
||||
` A ${companion.rarity} ${companion.species} appeared! ${stars}${shinyTag}`,
|
||||
` Name: ${companion.name}`,
|
||||
'',
|
||||
' Use /buddy card to see full stats.',
|
||||
' Use /buddy rename <name> to rename.',
|
||||
' Use /buddy pet to show some love!',
|
||||
].join('\n'),
|
||||
{ display: 'system' },
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
if (sub === 'card' || sub === 'stats') {
|
||||
onDone(renderCard(), { display: 'system' })
|
||||
return null
|
||||
}
|
||||
|
||||
if (sub === 'pet') {
|
||||
const companion = getCompanion()
|
||||
if (!companion) {
|
||||
onDone('No companion to pet! Use /buddy to hatch one.', { display: 'system' })
|
||||
return null
|
||||
}
|
||||
context.setAppState(prev => ({ ...prev, companionPetAt: Date.now() }))
|
||||
onDone(`You pet ${companion.name}! 💕`, { display: 'system' })
|
||||
return null
|
||||
}
|
||||
|
||||
if (sub === 'mute') {
|
||||
const config = getGlobalConfig()
|
||||
const newMuted = !config.companionMuted
|
||||
saveGlobalConfig(cfg => ({ ...cfg, companionMuted: newMuted }))
|
||||
onDone(
|
||||
newMuted
|
||||
? 'Companion muted. Use /buddy mute to unmute.'
|
||||
: 'Companion unmuted!',
|
||||
{ display: 'system' },
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
if (sub.startsWith('rename ')) {
|
||||
const newName = args.trim().slice('rename '.length).trim()
|
||||
if (!newName) {
|
||||
onDone('Usage: /buddy rename <new name>', { display: 'system' })
|
||||
return null
|
||||
}
|
||||
const config = getGlobalConfig()
|
||||
if (!config.companion) {
|
||||
onDone('No companion to rename! Use /buddy to hatch one.', { display: 'system' })
|
||||
return null
|
||||
}
|
||||
saveGlobalConfig(cfg => ({
|
||||
...cfg,
|
||||
companion: { ...cfg.companion!, name: newName },
|
||||
}))
|
||||
onDone(`Companion renamed to ${newName}!`, { display: 'system' })
|
||||
return null
|
||||
}
|
||||
|
||||
if (sub === 'reroll') {
|
||||
const { seed, species } = fairRoll()
|
||||
const companion = hatchWithSeed(seed)
|
||||
const sprite = renderSprite(companion)
|
||||
const stars = RARITY_STARS[companion.rarity]
|
||||
const shinyTag = companion.shiny ? ' ✨ SHINY!' : ''
|
||||
const config = getGlobalConfig()
|
||||
const counts = config.companionRollCounts ?? {}
|
||||
const seen = SPECIES.filter(s => (counts[s] ?? 0) > 0).length
|
||||
onDone(
|
||||
[
|
||||
'🔄 Rerolling...',
|
||||
'',
|
||||
...sprite.map(l => ` ${l}`),
|
||||
'',
|
||||
` A ${companion.rarity} ${companion.species} appeared! ${stars}${shinyTag}`,
|
||||
` Name: ${companion.name}`,
|
||||
` Collection: ${seen}/${SPECIES.length} species discovered`,
|
||||
].join('\n'),
|
||||
{ display: 'system' },
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
onDone(
|
||||
[
|
||||
'Usage: /buddy [subcommand]',
|
||||
'',
|
||||
' /buddy — Hatch or view your companion',
|
||||
' /buddy card — Show companion card with stats',
|
||||
' /buddy pet — Pet your companion 💕',
|
||||
' /buddy mute — Toggle companion speech bubbles',
|
||||
' /buddy rename — Rename your companion',
|
||||
' /buddy reroll — Release and re-hatch (fair rotation)',
|
||||
].join('\n'),
|
||||
{ display: 'system' },
|
||||
)
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Command } from '../../commands.js'
|
||||
|
||||
const buddy = {
|
||||
type: 'local-jsx',
|
||||
name: 'buddy',
|
||||
description: 'Interact with your companion',
|
||||
immediate: true,
|
||||
argumentHint: '<pet|mute|card|rename|reroll>',
|
||||
load: () => import('./buddy.js'),
|
||||
} satisfies Command
|
||||
|
||||
export default buddy
|
||||
@@ -269,6 +269,10 @@ export type GlobalConfig = {
|
||||
// /buddy companion soul — bones regenerated from userId on read. See src/buddy/.
|
||||
companion?: import('../buddy/types.js').StoredCompanion
|
||||
companionMuted?: boolean
|
||||
// Track how many times each species has been rolled for fair distribution
|
||||
companionRollCounts?: Record<string, number>
|
||||
// Custom seed override for companion bones (bypasses userId-based generation)
|
||||
companionSeed?: string
|
||||
|
||||
// Feedback survey tracking
|
||||
feedbackSurveyState?: {
|
||||
|
||||
Reference in New Issue
Block a user