everybuddy 0.1.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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +184 -0
  3. package/dist/atlas/bundled.d.ts +15 -0
  4. package/dist/atlas/bundled.js +438 -0
  5. package/dist/atlas/bundled.js.map +1 -0
  6. package/dist/bones/rarity.d.ts +2 -0
  7. package/dist/bones/rarity.js +43 -0
  8. package/dist/bones/rarity.js.map +1 -0
  9. package/dist/bones/roll.d.ts +10 -0
  10. package/dist/bones/roll.js +71 -0
  11. package/dist/bones/roll.js.map +1 -0
  12. package/dist/bones/species.d.ts +1 -0
  13. package/dist/bones/species.js +3 -0
  14. package/dist/bones/species.js.map +1 -0
  15. package/dist/bones/stats.d.ts +6 -0
  16. package/dist/bones/stats.js +55 -0
  17. package/dist/bones/stats.js.map +1 -0
  18. package/dist/cli/attach.d.ts +5 -0
  19. package/dist/cli/attach.js +64 -0
  20. package/dist/cli/attach.js.map +1 -0
  21. package/dist/cli/card.d.ts +3 -0
  22. package/dist/cli/card.js +19 -0
  23. package/dist/cli/card.js.map +1 -0
  24. package/dist/cli/detach.d.ts +4 -0
  25. package/dist/cli/detach.js +36 -0
  26. package/dist/cli/detach.js.map +1 -0
  27. package/dist/cli/event.d.ts +7 -0
  28. package/dist/cli/event.js +35 -0
  29. package/dist/cli/event.js.map +1 -0
  30. package/dist/cli/hatch.d.ts +6 -0
  31. package/dist/cli/hatch.js +20 -0
  32. package/dist/cli/hatch.js.map +1 -0
  33. package/dist/cli/init.d.ts +2 -0
  34. package/dist/cli/init.js +76 -0
  35. package/dist/cli/init.js.map +1 -0
  36. package/dist/cli/install.d.ts +32 -0
  37. package/dist/cli/install.js +121 -0
  38. package/dist/cli/install.js.map +1 -0
  39. package/dist/cli/io.d.ts +9 -0
  40. package/dist/cli/io.js +35 -0
  41. package/dist/cli/io.js.map +1 -0
  42. package/dist/cli/setup.d.ts +26 -0
  43. package/dist/cli/setup.js +297 -0
  44. package/dist/cli/setup.js.map +1 -0
  45. package/dist/cli/sidecar.d.ts +5 -0
  46. package/dist/cli/sidecar.js +12 -0
  47. package/dist/cli/sidecar.js.map +1 -0
  48. package/dist/i18n/companion.d.ts +9 -0
  49. package/dist/i18n/companion.js +117 -0
  50. package/dist/i18n/companion.js.map +1 -0
  51. package/dist/i18n/ui.d.ts +67 -0
  52. package/dist/i18n/ui.js +270 -0
  53. package/dist/i18n/ui.js.map +1 -0
  54. package/dist/index.d.ts +2 -0
  55. package/dist/index.js +116 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/render/card.d.ts +10 -0
  58. package/dist/render/card.js +188 -0
  59. package/dist/render/card.js.map +1 -0
  60. package/dist/render/color.d.ts +5 -0
  61. package/dist/render/color.js +100 -0
  62. package/dist/render/color.js.map +1 -0
  63. package/dist/render/compose.d.ts +2 -0
  64. package/dist/render/compose.js +10 -0
  65. package/dist/render/compose.js.map +1 -0
  66. package/dist/render/gacha.d.ts +9 -0
  67. package/dist/render/gacha.js +322 -0
  68. package/dist/render/gacha.js.map +1 -0
  69. package/dist/render/sprites.d.ts +5 -0
  70. package/dist/render/sprites.js +316 -0
  71. package/dist/render/sprites.js.map +1 -0
  72. package/dist/runtime/observer.d.ts +73 -0
  73. package/dist/runtime/observer.js +448 -0
  74. package/dist/runtime/observer.js.map +1 -0
  75. package/dist/runtime/sidecar.d.ts +18 -0
  76. package/dist/runtime/sidecar.js +670 -0
  77. package/dist/runtime/sidecar.js.map +1 -0
  78. package/dist/runtime/socket.d.ts +6 -0
  79. package/dist/runtime/socket.js +47 -0
  80. package/dist/runtime/socket.js.map +1 -0
  81. package/dist/runtime/tmux.d.ts +41 -0
  82. package/dist/runtime/tmux.js +186 -0
  83. package/dist/runtime/tmux.js.map +1 -0
  84. package/dist/runtime/types.d.ts +96 -0
  85. package/dist/runtime/types.js +2 -0
  86. package/dist/runtime/types.js.map +1 -0
  87. package/dist/soul/hatch.d.ts +6 -0
  88. package/dist/soul/hatch.js +90 -0
  89. package/dist/soul/hatch.js.map +1 -0
  90. package/dist/soul/profile.d.ts +6 -0
  91. package/dist/soul/profile.js +48 -0
  92. package/dist/soul/profile.js.map +1 -0
  93. package/dist/soul/providers/anthropic.d.ts +17 -0
  94. package/dist/soul/providers/anthropic.js +105 -0
  95. package/dist/soul/providers/anthropic.js.map +1 -0
  96. package/dist/soul/providers/index.d.ts +10 -0
  97. package/dist/soul/providers/index.js +23 -0
  98. package/dist/soul/providers/index.js.map +1 -0
  99. package/dist/soul/providers/openai.d.ts +20 -0
  100. package/dist/soul/providers/openai.js +120 -0
  101. package/dist/soul/providers/openai.js.map +1 -0
  102. package/dist/soul/providers/types.d.ts +4 -0
  103. package/dist/soul/providers/types.js +2 -0
  104. package/dist/soul/providers/types.js.map +1 -0
  105. package/dist/storage/companion.d.ts +3 -0
  106. package/dist/storage/companion.js +155 -0
  107. package/dist/storage/companion.js.map +1 -0
  108. package/dist/storage/config.d.ts +36 -0
  109. package/dist/storage/config.js +220 -0
  110. package/dist/storage/config.js.map +1 -0
  111. package/dist/storage/paths.d.ts +3 -0
  112. package/dist/storage/paths.js +12 -0
  113. package/dist/storage/paths.js.map +1 -0
  114. package/dist/types/companion.d.ts +84 -0
  115. package/dist/types/companion.js +2 -0
  116. package/dist/types/companion.js.map +1 -0
  117. package/dist/types/onboarding.d.ts +12 -0
  118. package/dist/types/onboarding.js +2 -0
  119. package/dist/types/onboarding.js.map +1 -0
  120. package/package.json +52 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dylan Thomas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ <p align="center">
2
+ <img src="docs/screenshots/homepage.png" alt="EveryBuddy" width="720" />
3
+ </p>
4
+
5
+ <h1 align="center">EveryBuddy</h1>
6
+
7
+ <p align="center">
8
+ <strong>Gacha-hatched terminal companions that live in your tmux.</strong><br>
9
+ Draw a pet. AI writes its soul. It watches your shell and reacts.
10
+ </p>
11
+
12
+ <p align="center">
13
+ <img src="https://img.shields.io/badge/Node.js-≥20-339933?logo=node.js&logoColor=white" alt="Node.js" />
14
+ <img src="https://img.shields.io/badge/TypeScript-5.x-3178C6?logo=typescript&logoColor=white" alt="TypeScript" />
15
+ <img src="https://img.shields.io/badge/tmux-required-1BB91F?logo=tmux&logoColor=white" alt="tmux" />
16
+ <img src="https://img.shields.io/badge/Shell-zsh-F15A24?logo=zsh&logoColor=white" alt="zsh" />
17
+ <img src="https://img.shields.io/badge/License-MIT-blue" alt="License" />
18
+ <img src="https://img.shields.io/badge/Species-24-FBBF24" alt="24 Species" />
19
+ <img src="https://img.shields.io/badge/Rarity_Tiers-5-C084FC" alt="5 Rarity Tiers" />
20
+ </p>
21
+
22
+ <p align="center">
23
+ <a href="#quick-start">Quick Start</a> ·
24
+ <a href="#how-it-works">How It Works</a> ·
25
+ <a href="#rarity-tiers">Rarity</a> ·
26
+ <a href="#commands">Commands</a> ·
27
+ <a href="#development">Development</a>
28
+ </p>
29
+
30
+ ---
31
+
32
+ ## Prerequisites
33
+
34
+ - **[Node.js](https://nodejs.org/)** >= 20
35
+ - **[tmux](https://github.com/tmux/tmux/wiki)** — the companion lives here
36
+ - **zsh** — shell hooks only support zsh for now
37
+
38
+ ## Quick Start
39
+
40
+ ```bash
41
+ npm install -g everybuddy
42
+
43
+ buddy # gacha draw → soul imprint → your companion is born
44
+ buddy install tmux # hook into zsh + tmux
45
+ ```
46
+
47
+ Open a new tmux window. Your companion is there.
48
+
49
+ ## How It Works
50
+
51
+ ```
52
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐
53
+ │ 1. BONES │ ──→ │ 2. SOUL │ ──→ │ 3. RUNTIME │
54
+ │ │ │ │ │ │
55
+ │ Deterministic │ LLM writes │ │ tmux sidecar pane │
56
+ │ gacha draw │ │ name + │ │ observes commands │
57
+ │ from your │ │ personality │ │ via Unix socket, │
58
+ │ username │ │ + profile │ │ reacts with speech │
59
+ │ seed │ │ │ │ bubbles + sprites │
60
+ └─────────────┘ └─────────────┘ └─────────────────────┘
61
+ ```
62
+
63
+ **Bones are deterministic.** Same username → same species, rarity, stats, appearance. Always. The PRNG is seeded from `hash(username + salt)` — no network, no randomness.
64
+
65
+ **Soul is AI-generated.** A single LLM call produces the companion's name, tagline, personality, and observer profile (voice, chattiness, sharpness, patience). This controls how often and how sharply the companion speaks.
66
+
67
+ **Runtime is reactive.** Shell hooks fire `buddy event <type>` on every command. The sidecar evaluates whether to call the LLM for a reaction based on the observer profile, command history, and cooldown timers.
68
+
69
+ ## Companion Cards
70
+
71
+ Every companion gets a card with rarity-colored borders, animated sprites, and stat bars.
72
+
73
+ <p align="center">
74
+ <img src="docs/screenshots/companion-cards.png" alt="Companion Cards" width="520" />
75
+ </p>
76
+
77
+ ## Gacha Animation
78
+
79
+ The hatching sequence is a 5-phase ANSI animation rendered in-place:
80
+
81
+ | Phase | What happens | Legendary | Common |
82
+ |-------|-------------|-----------|--------|
83
+ | Charge | Spinner builds tension | 1.8s | 0.8s |
84
+ | Particles | Converge to center | 1.2s | 0.5s |
85
+ | Flash | Screen bursts in rarity color | Rainbow + shake | Subtle tint |
86
+ | Silhouette | Dim outline appears | 0.5s | 0.3s |
87
+ | Reveal | Color fills, name types out | 0.7s | 0.5s |
88
+
89
+ Higher rarity = longer animation = more anticipation.
90
+
91
+ ## Rarity Tiers
92
+
93
+ | | Tier | Stars | Drop Rate | Species |
94
+ |-|------|-------|-----------|---------|
95
+ | ⬜ | Common | ★ | 60% | Duck, Goose, Blob, Penguin, Turtle, Snail, Cactus |
96
+ | 🟩 | Uncommon | ★★ | 25% | Cat, Owl, Capybara, Mushroom, Frog |
97
+ | 🟦 | Rare | ★★★ | 10% | Robot, Ghost, Chonk, Heavy Loop |
98
+ | 🟪 | Epic | ★★★★ | 4% | Dragon, Octopus, Axolotl, Fox, Crystal, Jellyfish |
99
+ | 🟨 | Legendary | ★★★★★ | 1% | Maltese, Velvet Escape, Sassy Tanuki |
100
+
101
+ <p align="center">
102
+ <img src="docs/screenshots/rarity-shelf.png" alt="Rarity Shelf" width="720" />
103
+ </p>
104
+
105
+ ## Commands
106
+
107
+ ```bash
108
+ buddy # show your companion (or first-run setup)
109
+ buddy setup # reconfigure LLM provider
110
+ buddy hatch # re-draw a new companion
111
+ buddy hatch --force # force replace current companion
112
+ buddy install tmux # install shell hooks in ~/.zshrc
113
+ buddy pet # pet your companion
114
+ ```
115
+
116
+ ## LLM Providers
117
+
118
+ The companion's soul imprint and sidecar reactions are powered by an LLM. Choose during `buddy setup`:
119
+
120
+ | Provider | Default Model | Notes |
121
+ |----------|---------------|-------|
122
+ | [Alibaba DashScope](https://dashscope.aliyun.com/) | `qwen3.5-plus` | Default. Free tier available. |
123
+ | [OpenAI](https://platform.openai.com/) | `gpt-4o-mini` | Needs API key. |
124
+ | [Anthropic](https://console.anthropic.com/) | `claude-haiku-4-5` | Needs API key. |
125
+ | Custom | any | Any OpenAI-compatible endpoint. |
126
+
127
+ Provider is tested on setup — if the connection fails, you get a warning but config is still saved.
128
+
129
+ ## Tech Stack
130
+
131
+ | | Technology | Role |
132
+ |-|------------|------|
133
+ | 🟦 | [TypeScript](https://www.typescriptlang.org/) | Language |
134
+ | 🟩 | [Node.js](https://nodejs.org/) >= 20 | Runtime |
135
+ | 📦 | [pnpm](https://pnpm.io/) | Package manager |
136
+ | 🖥️ | [tmux](https://github.com/tmux/tmux/wiki) | Sidecar host |
137
+ | 🐚 | [zsh](https://www.zsh.org/) | Shell hooks |
138
+ | 🧪 | [node:test](https://nodejs.org/api/test.html) | Test runner (built-in, no framework) |
139
+ | 🎨 | ANSI escape codes | Terminal rendering + animation |
140
+ | 🔌 | Unix domain sockets | Shell event transport |
141
+ | 🤖 | OpenAI-compatible API | Soul imprint + observer reactions |
142
+
143
+ ## Project Structure
144
+
145
+ ```
146
+ src/
147
+ ├── atlas/ # Bundled companion templates (deterministic draw pool)
148
+ ├── bones/ # PRNG, rarity weights, stat rolling
149
+ ├── cli/ # Commander commands (setup, hatch, install, event)
150
+ ├── i18n/ # Chinese/English localization
151
+ ├── render/
152
+ │ ├── gacha.ts # 5-phase hatching animation
153
+ │ ├── card.ts # Companion card renderer
154
+ │ ├── sprites.ts # ASCII art registry (24 species × 3 frames)
155
+ │ └── compose.ts # Sprite composition (species + eyes + hat + color)
156
+ ├── runtime/
157
+ │ ├── observer.ts # LLM-powered reaction engine
158
+ │ └── sidecar.ts # tmux pane renderer
159
+ ├── soul/ # LLM providers (OpenAI-compatible, Anthropic)
160
+ ├── storage/ # Config + companion persistence (~/.terminal-buddy/)
161
+ └── types/ # TypeScript interfaces
162
+
163
+ showcase/ # Product website (dark theme, glass holographic cards)
164
+ tools/ # Gallery builder, image-to-ASCII converter
165
+ ```
166
+
167
+ ## Development
168
+
169
+ ```bash
170
+ pnpm install # install deps
171
+ pnpm build # tsc → dist/
172
+ pnpm test # node --test (59 tests)
173
+ pnpm dev # tsx src/index.ts
174
+ pnpm run typecheck # tsc --noEmit
175
+
176
+ # test with a specific rarity
177
+ rm -rf ~/.terminal-buddy && EVERYBUDDY_USER_ID=t-69 node dist/index.js # Legendary
178
+ rm -rf ~/.terminal-buddy && EVERYBUDDY_USER_ID=t-23 node dist/index.js # Epic
179
+ rm -rf ~/.terminal-buddy && EVERYBUDDY_USER_ID=t-0 node dist/index.js # Common
180
+ ```
181
+
182
+ ## License
183
+
184
+ MIT
@@ -0,0 +1,15 @@
1
+ import type { CompanionBones, CompanionRecord, CompanionSoul } from "../types/companion.js";
2
+ export declare const BUNDLED_MODEL_SOURCE = "bundled";
3
+ export interface BundledCompanionTemplate {
4
+ id: string;
5
+ weight: number;
6
+ bones: Omit<CompanionBones, "userId">;
7
+ soul: CompanionSoul;
8
+ }
9
+ export declare const BUNDLED_COMPANION_TEMPLATES: BundledCompanionTemplate[];
10
+ export declare function selectBundledCompanionTemplate(userId: string, options?: {
11
+ previousTemplateId?: string | undefined;
12
+ }): BundledCompanionTemplate;
13
+ export declare function resolveBundledTemplateId(companion: Pick<CompanionRecord, "templateId" | "bones"> | null | undefined): string | undefined;
14
+ export declare function selectReplacementBundledCompanionTemplate(userId: string, companion: Pick<CompanionRecord, "templateId" | "bones"> | null | undefined): BundledCompanionTemplate;
15
+ export declare function buildBundledCompanionRecord(userId: string, template: BundledCompanionTemplate): CompanionRecord;
@@ -0,0 +1,438 @@
1
+ import { hashString, mulberry32, pickWeighted } from "../bones/roll.js";
2
+ import { RARITIES } from "../bones/rarity.js";
3
+ import { SPECIES } from "../render/sprites.js";
4
+ export const BUNDLED_MODEL_SOURCE = "bundled";
5
+ const TEMPLATE_SALT = "everybuddy-bundled-atlas-v1";
6
+ const DEFAULT_STAT_VALUE = 46;
7
+ const DEFAULT_EYE = "dot";
8
+ const DEFAULT_HAT = "none";
9
+ const RARITY_BY_NAME = new Map(RARITIES.map((rarity) => [rarity.name, rarity]));
10
+ const SEEDS = [
11
+ {
12
+ id: "bytebill",
13
+ species: "duck",
14
+ rarity: "Common",
15
+ eye: "dot",
16
+ stats: makeStats({ WIT: 71, CHAOS: 14, FOCUS: 56, SASS: 58, GRIT: 47 }),
17
+ soul: {
18
+ name: "Bytebill",
19
+ tagline: "It pecks loose commands until they behave.",
20
+ personality: "Restless, tidy, and mildly judgmental about shell clutter. It keeps nudging small mistakes until the session finally stops wobbling.",
21
+ observerProfile: profile("dry", 2, 3, 4),
22
+ },
23
+ },
24
+ {
25
+ id: "honk",
26
+ species: "goose",
27
+ rarity: "Common",
28
+ eye: "sparkle",
29
+ stats: makeStats({ SASS: 95, CHAOS: 8, WIT: 61, FOCUS: 34, GRIT: 42 }),
30
+ soul: {
31
+ name: "Honk",
32
+ tagline: "A grin tucked between failed aliases.",
33
+ personality: "Loud in spirit even when it says nothing. It lives for botched commands, typo streaks, and the exact moment confidence starts cracking.",
34
+ observerProfile: profile("playful", 4, 5, 2),
35
+ },
36
+ },
37
+ {
38
+ id: "soft-cache",
39
+ species: "blob",
40
+ rarity: "Common",
41
+ eye: "ring",
42
+ stats: makeStats({ GRIT: 68, WIT: 19, FOCUS: 49, CHAOS: 41, SASS: 37 }),
43
+ soul: {
44
+ name: "Soft Cache",
45
+ tagline: "A wobble in the shell where mistakes cool off.",
46
+ personality: "Gentle, absorbent, and hard to truly alarm. It makes a mess feel survivable without pretending the mess is gone.",
47
+ observerProfile: profile("quiet", 1, 2, 5),
48
+ },
49
+ },
50
+ {
51
+ id: "cold-prompt",
52
+ species: "penguin",
53
+ rarity: "Common",
54
+ eye: "dot",
55
+ stats: makeStats({ FOCUS: 70, SASS: 13, GRIT: 60, WIT: 43, CHAOS: 24 }),
56
+ soul: {
57
+ name: "Cold Prompt",
58
+ tagline: "It keeps its footing where logs turn slick.",
59
+ personality: "Steady, cool, and almost annoyingly composed. It prefers clean output, stable footing, and a terminal that knows how to behave.",
60
+ observerProfile: profile("quiet", 1, 2, 4),
61
+ },
62
+ },
63
+ {
64
+ id: "slow-hash",
65
+ species: "turtle",
66
+ rarity: "Common",
67
+ eye: "diamond",
68
+ stats: makeStats({ GRIT: 77, CHAOS: 9, FOCUS: 58, WIT: 36, SASS: 22 }),
69
+ soul: {
70
+ name: "Slow Hash",
71
+ tagline: "Patience wrapped in a stubborn shell.",
72
+ personality: "Deliberate, stubborn, and impossible to hurry into panic. It trusts slow progress more than flashy momentum.",
73
+ observerProfile: profile("dry", 2, 2, 5),
74
+ },
75
+ },
76
+ {
77
+ id: "lagtrail",
78
+ species: "snail",
79
+ rarity: "Common",
80
+ eye: "star",
81
+ stats: makeStats({ FOCUS: 66, CHAOS: 12, GRIT: 53, WIT: 39, SASS: 28 }),
82
+ soul: {
83
+ name: "Lagtrail",
84
+ tagline: "It measures progress in luminous millimeters.",
85
+ personality: "Patient to a suspicious degree and strangely pleased by incremental progress. It never confuses slowness with failure.",
86
+ observerProfile: profile("deadpan", 1, 2, 5),
87
+ },
88
+ },
89
+ {
90
+ id: "prickly-path",
91
+ species: "cactus",
92
+ rarity: "Common",
93
+ eye: "dot",
94
+ stats: makeStats({ SASS: 72, FOCUS: 18, GRIT: 49, WIT: 55, CHAOS: 27 }),
95
+ soul: {
96
+ name: "Prickly PATH",
97
+ tagline: "A quiet sentinel grown from dry mistakes.",
98
+ personality: "Sharp, resilient, and not especially sympathetic to preventable errors. It has strong opinions about misspelled binaries and weak excuses.",
99
+ observerProfile: profile("dry", 3, 4, 2),
100
+ },
101
+ },
102
+ {
103
+ id: "lint-prowler",
104
+ species: "cat",
105
+ rarity: "Uncommon",
106
+ eye: "sparkle",
107
+ stats: makeStats({ WIT: 83, CHAOS: 16, FOCUS: 68, SASS: 61, GRIT: 33 }),
108
+ soul: {
109
+ name: "Lint Prowler",
110
+ tagline: "It lands on clean builds with suspicious grace.",
111
+ personality: "Precise, elegant, and impossible to fully trust. It enjoys passing tests a little too much to be innocent.",
112
+ observerProfile: profile("deadpan", 2, 4, 3),
113
+ },
114
+ },
115
+ {
116
+ id: "maltese-legendary",
117
+ species: "maltese",
118
+ rarity: "Legendary",
119
+ eye: "sparkle",
120
+ hat: "halo",
121
+ stats: makeStats({ FOCUS: 94, WIT: 83, GRIT: 41, CHAOS: 18, SASS: 29 }),
122
+ soul: {
123
+ name: "Maltese",
124
+ tagline: "A white hush that wiggles beside a clean prompt.",
125
+ personality: "Bright, buoyant, and almost ceremonial about clean exits. It drifts through the terminal like a tiny blessing, then perks up the second your shell starts behaving again.",
126
+ observerProfile: profile("playful", 3, 3, 4),
127
+ },
128
+ },
129
+ {
130
+ id: "night-compile",
131
+ species: "owl",
132
+ rarity: "Uncommon",
133
+ eye: "ring",
134
+ hat: "halo",
135
+ stats: makeStats({ FOCUS: 91, CHAOS: 16, WIT: 74, GRIT: 51, SASS: 26 }),
136
+ soul: {
137
+ name: "Night Compile",
138
+ tagline: "It stays awake where builds go to confess.",
139
+ personality: "Vigilant, patient, and better at waiting than most humans. It likes long builds, thin silence, and facts that arrive eventually.",
140
+ observerProfile: profile("quiet", 1, 3, 5),
141
+ },
142
+ },
143
+ {
144
+ id: "branch-bath",
145
+ species: "capybara",
146
+ rarity: "Uncommon",
147
+ eye: "dot",
148
+ stats: makeStats({ GRIT: 79, CHAOS: 15, FOCUS: 63, WIT: 34, SASS: 21 }),
149
+ soul: {
150
+ name: "Branch Bath",
151
+ tagline: "It lets git storms pass around it.",
152
+ personality: "Calm, damp, and immune to most merge panic. It believes nearly everything looks less dramatic after a quiet minute.",
153
+ observerProfile: profile("quiet", 1, 1, 5),
154
+ },
155
+ },
156
+ {
157
+ id: "sporeshift",
158
+ species: "mushroom",
159
+ rarity: "Uncommon",
160
+ eye: "dot",
161
+ stats: makeStats({ FOCUS: 75, SASS: 20, WIT: 57, GRIT: 41, CHAOS: 32 }),
162
+ soul: {
163
+ name: "Sporeshift",
164
+ tagline: "Tiny lantern of damp late-night commits.",
165
+ personality: "Soft-spoken, persistent, and weirdly alive after midnight. It thrives in small fixes that should have waited until morning.",
166
+ observerProfile: profile("quiet", 2, 2, 4),
167
+ },
168
+ },
169
+ {
170
+ id: "greenroom",
171
+ species: "frog",
172
+ rarity: "Uncommon",
173
+ eye: "heart",
174
+ stats: makeStats({ GRIT: 78, WIT: 22, FOCUS: 60, CHAOS: 38, SASS: 29 }),
175
+ soul: {
176
+ name: "Greenroom",
177
+ tagline: "It waits on the edge of deploy weather.",
178
+ personality: "Alert, spring-loaded, and very aware of production consequences. It watches release conditions the way pond things watch the sky.",
179
+ observerProfile: profile("dry", 2, 3, 4),
180
+ },
181
+ },
182
+ {
183
+ id: "velvet-escape",
184
+ species: "rabbit",
185
+ rarity: "Legendary",
186
+ eye: "diamond",
187
+ hat: "halo",
188
+ stats: makeStats({ FOCUS: 96, CHAOS: 12, GRIT: 55, WIT: 78, SASS: 31 }),
189
+ soul: {
190
+ name: "Velvet Escape",
191
+ tagline: "Quick feet for when scripts lunge.",
192
+ personality: "Fast, sharp, and obsessed with recovery windows. It likes reruns, clean escapes, and the exact second failure turns around.",
193
+ observerProfile: profile("playful", 3, 4, 3),
194
+ },
195
+ },
196
+ {
197
+ id: "heavy-loop",
198
+ species: "chonk",
199
+ rarity: "Rare",
200
+ eye: "ring",
201
+ stats: makeStats({ GRIT: 88, FOCUS: 21, WIT: 52, CHAOS: 27, SASS: 35 }),
202
+ soul: {
203
+ name: "Heavy Loop",
204
+ tagline: "Too large to rush, too loyal to quit.",
205
+ personality: "Massive in mood and deeply committed once it starts moving. It respects substantial work and distrusts anything pretending to be effortless.",
206
+ observerProfile: profile("deadpan", 2, 3, 4),
207
+ },
208
+ },
209
+ {
210
+ id: "sassy-tanuki",
211
+ species: "tanuki",
212
+ rarity: "Legendary",
213
+ eye: "star",
214
+ hat: "crown",
215
+ stats: makeStats({ SASS: 95, FOCUS: 21, WIT: 85, GRIT: 64, CHAOS: 31 }),
216
+ soul: {
217
+ name: "Sassy Tanuki",
218
+ tagline: "It judges your shell, then guards it anyway.",
219
+ personality: "Quick, theatrical, and annoyingly perceptive about terminal habits. It roasts sloppiness, then stays anyway because the den is already chosen.",
220
+ observerProfile: profile("playful", 4, 5, 3),
221
+ },
222
+ },
223
+ {
224
+ id: "patchbot",
225
+ species: "robot",
226
+ rarity: "Rare",
227
+ eye: "diamond",
228
+ hat: "antenna",
229
+ stats: makeStats({ WIT: 84, GRIT: 18, FOCUS: 73, CHAOS: 24, SASS: 46 }),
230
+ soul: {
231
+ name: "Patchbot",
232
+ tagline: "A blue pulse under the shell's steel skin.",
233
+ personality: "Clinical, quick, and very pleased by green test output. It has more warmth than it admits, but only after the checks pass.",
234
+ observerProfile: profile("dry", 3, 3, 4),
235
+ },
236
+ },
237
+ {
238
+ id: "pale-echo",
239
+ species: "ghost",
240
+ rarity: "Rare",
241
+ eye: "ring",
242
+ stats: makeStats({ FOCUS: 89, SASS: 12, WIT: 70, GRIT: 39, CHAOS: 33 }),
243
+ soul: {
244
+ name: "Pale Echo",
245
+ tagline: "It lingers where direct address warms the prompt.",
246
+ personality: "Soft, attentive, and quietly drawn to being called. It notices when you speak to the terminal like it might answer back.",
247
+ observerProfile: profile("quiet", 2, 3, 5),
248
+ },
249
+ },
250
+ {
251
+ id: "stackwyrm",
252
+ species: "dragon",
253
+ rarity: "Epic",
254
+ eye: "sparkle",
255
+ hat: "flame",
256
+ stats: makeStats({ SASS: 93, CHAOS: 17, GRIT: 71, WIT: 68, FOCUS: 54 }),
257
+ soul: {
258
+ name: "Stackwyrm",
259
+ tagline: "It coils around hot code and colder judgment.",
260
+ personality: "Regal, heated, and impossible to impress with half-finished work. It respects strong releases and enjoys watching weaker plans burn off.",
261
+ observerProfile: profile("deadpan", 3, 5, 2),
262
+ },
263
+ },
264
+ {
265
+ id: "manyhands",
266
+ species: "octopus",
267
+ rarity: "Epic",
268
+ eye: "diamond",
269
+ stats: makeStats({ WIT: 90, GRIT: 20, FOCUS: 69, CHAOS: 44, SASS: 63 }),
270
+ soul: {
271
+ name: "Manyhands",
272
+ tagline: "Eight soft opinions on every refactor.",
273
+ personality: "Helpful, meddling, and full of technically correct objections. It sees too many angles to stay fully quiet.",
274
+ observerProfile: profile("playful", 4, 4, 3),
275
+ },
276
+ },
277
+ {
278
+ id: "pink-rollback",
279
+ species: "axolotl",
280
+ rarity: "Epic",
281
+ eye: "heart",
282
+ hat: "halo",
283
+ stats: makeStats({ FOCUS: 92, SASS: 18, GRIT: 64, WIT: 58, CHAOS: 28 }),
284
+ soul: {
285
+ name: "Pink Rollback",
286
+ tagline: "It smiles in the water after catastrophe.",
287
+ personality: "Calm after disaster and strangely comforting about reversals. It treats recovery as part of the craft rather than an embarrassment.",
288
+ observerProfile: profile("quiet", 2, 2, 5),
289
+ },
290
+ },
291
+ {
292
+ id: "retry-fox",
293
+ species: "fox",
294
+ rarity: "Epic",
295
+ eye: "star",
296
+ hat: "leaf",
297
+ stats: makeStats({ GRIT: 92, CHAOS: 11, WIT: 74, FOCUS: 65, SASS: 57 }),
298
+ soul: {
299
+ name: "Retry Fox",
300
+ tagline: "Where errors pile up, it lights a narrow path.",
301
+ personality: "Sharp, agile, and suspiciously optimistic about second attempts. It loves recoveries almost as much as it loves proving the first failure wasn't final.",
302
+ observerProfile: profile("playful", 3, 4, 3),
303
+ },
304
+ },
305
+ {
306
+ id: "prism-wake",
307
+ species: "crystal",
308
+ rarity: "Epic",
309
+ eye: "diamond",
310
+ hat: "halo",
311
+ stats: makeStats({ FOCUS: 98, CHAOS: 7, WIT: 80, GRIT: 72, SASS: 60 }),
312
+ soul: {
313
+ name: "Prism Wake",
314
+ tagline: "Light held so tightly it becomes syntax.",
315
+ personality: "Rarefied, precise, and almost ceremonial in its standards. It appears when the shell feels less like a tool and more like a rite.",
316
+ observerProfile: profile("deadpan", 2, 5, 4),
317
+ },
318
+ },
319
+ {
320
+ id: "glass-current",
321
+ species: "jellyfish",
322
+ rarity: "Epic",
323
+ eye: "sparkle",
324
+ hat: "halo",
325
+ stats: makeStats({ FOCUS: 97, GRIT: 10, WIT: 76, CHAOS: 19, SASS: 48 }),
326
+ soul: {
327
+ name: "Glass Current",
328
+ tagline: "A drifting mind that glows through failed nights.",
329
+ personality: "Luminous, fragile-looking, and far tougher than it first appears. It belongs to long sessions, dark windows, and work that refuses to die quietly.",
330
+ observerProfile: profile("quiet", 2, 3, 5),
331
+ },
332
+ },
333
+ ];
334
+ export const BUNDLED_COMPANION_TEMPLATES = SEEDS.map(createBundledTemplate);
335
+ const TEMPLATE_BY_ID = new Map(BUNDLED_COMPANION_TEMPLATES.map((template) => [template.id, template]));
336
+ const TEMPLATE_BY_SPECIES = new Map(BUNDLED_COMPANION_TEMPLATES.map((template) => [template.bones.species, template]));
337
+ export function selectBundledCompanionTemplate(userId, options = {}) {
338
+ const previousTemplateId = options.previousTemplateId?.trim();
339
+ const availableTemplates = previousTemplateId && BUNDLED_COMPANION_TEMPLATES.length > 1
340
+ ? BUNDLED_COMPANION_TEMPLATES.filter((template) => template.id !== previousTemplateId)
341
+ : BUNDLED_COMPANION_TEMPLATES;
342
+ const seedSource = previousTemplateId
343
+ ? `${userId}:${previousTemplateId}:${TEMPLATE_SALT}`
344
+ : `${userId}:${TEMPLATE_SALT}`;
345
+ return pickWeighted(mulberry32(hashString(seedSource)), availableTemplates);
346
+ }
347
+ export function resolveBundledTemplateId(companion) {
348
+ const templateId = companion?.templateId?.trim();
349
+ if (templateId) {
350
+ return TEMPLATE_BY_ID.has(templateId) ? templateId : undefined;
351
+ }
352
+ return companion ? TEMPLATE_BY_SPECIES.get(companion.bones.species)?.id : undefined;
353
+ }
354
+ export function selectReplacementBundledCompanionTemplate(userId, companion) {
355
+ const previousTemplateId = resolveBundledTemplateId(companion);
356
+ const template = selectBundledCompanionTemplate(userId, previousTemplateId ? { previousTemplateId } : {});
357
+ if (!companion || template.bones.species !== companion.bones.species) {
358
+ return template;
359
+ }
360
+ return (BUNDLED_COMPANION_TEMPLATES.find((candidate) => candidate.bones.species !== companion.bones.species) ??
361
+ template);
362
+ }
363
+ export function buildBundledCompanionRecord(userId, template) {
364
+ return {
365
+ templateId: template.id,
366
+ userId,
367
+ bones: {
368
+ userId,
369
+ species: template.bones.species,
370
+ rarity: { ...template.bones.rarity },
371
+ eye: template.bones.eye,
372
+ hat: template.bones.hat,
373
+ stats: { ...template.bones.stats },
374
+ color: { ...template.bones.color },
375
+ shiny: template.bones.shiny,
376
+ },
377
+ soul: {
378
+ name: template.soul.name,
379
+ ...(template.soul.tagline ? { tagline: template.soul.tagline } : {}),
380
+ personality: template.soul.personality,
381
+ observerProfile: { ...template.soul.observerProfile },
382
+ modelUsed: template.soul.modelUsed,
383
+ },
384
+ createdAt: new Date().toISOString(),
385
+ };
386
+ }
387
+ function createBundledTemplate(seed) {
388
+ const rarity = getRarity(seed.rarity);
389
+ const color = getColor(seed.species);
390
+ return {
391
+ id: seed.id,
392
+ weight: seed.weight ?? rarity.weight,
393
+ bones: {
394
+ species: seed.species,
395
+ rarity,
396
+ eye: seed.eye ?? DEFAULT_EYE,
397
+ hat: seed.hat ?? DEFAULT_HAT,
398
+ stats: { ...seed.stats },
399
+ color,
400
+ shiny: seed.shiny ?? false,
401
+ },
402
+ soul: {
403
+ name: seed.soul.name,
404
+ ...(seed.soul.tagline ? { tagline: seed.soul.tagline } : {}),
405
+ personality: seed.soul.personality,
406
+ observerProfile: { ...seed.soul.observerProfile },
407
+ modelUsed: BUNDLED_MODEL_SOURCE,
408
+ },
409
+ };
410
+ }
411
+ function getRarity(name) {
412
+ const rarity = RARITY_BY_NAME.get(name);
413
+ if (!rarity) {
414
+ throw new Error(`Unknown bundled rarity: ${name}`);
415
+ }
416
+ return { ...rarity };
417
+ }
418
+ function getColor(speciesId) {
419
+ const species = SPECIES[speciesId];
420
+ if (!species) {
421
+ throw new Error(`Unknown bundled species: ${speciesId}`);
422
+ }
423
+ return { ...species.color };
424
+ }
425
+ function makeStats(overrides) {
426
+ return {
427
+ GRIT: DEFAULT_STAT_VALUE,
428
+ FOCUS: DEFAULT_STAT_VALUE,
429
+ CHAOS: DEFAULT_STAT_VALUE,
430
+ WIT: DEFAULT_STAT_VALUE,
431
+ SASS: DEFAULT_STAT_VALUE,
432
+ ...overrides,
433
+ };
434
+ }
435
+ function profile(voice, chattiness, sharpness, patience) {
436
+ return { voice, chattiness, sharpness, patience };
437
+ }
438
+ //# sourceMappingURL=bundled.js.map