vaal-code 0.6.910__tar.gz → 0.6.930__tar.gz

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 (99) hide show
  1. {vaal_code-0.6.910 → vaal_code-0.6.930}/PKG-INFO +1 -1
  2. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/__init__.py +1 -1
  3. vaal_code-0.6.930/vaal/core/buddy.py +349 -0
  4. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/command_palette.py +1 -0
  5. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/repl.py +88 -2
  6. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal_code.egg-info/PKG-INFO +1 -1
  7. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal_code.egg-info/SOURCES.txt +1 -0
  8. {vaal_code-0.6.910 → vaal_code-0.6.930}/README.md +0 -0
  9. {vaal_code-0.6.910 → vaal_code-0.6.930}/pyproject.toml +0 -0
  10. {vaal_code-0.6.910 → vaal_code-0.6.930}/setup.cfg +0 -0
  11. {vaal_code-0.6.910 → vaal_code-0.6.930}/tests/test_all.py +0 -0
  12. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/__main__.py +0 -0
  13. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/__init__.py +0 -0
  14. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/agent.py +0 -0
  15. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/auto_fix.py +0 -0
  16. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/bootstrap.py +0 -0
  17. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/branching.py +0 -0
  18. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/bridge.py +0 -0
  19. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/changelog.py +0 -0
  20. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/clipboard.py +0 -0
  21. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/code_review.py +0 -0
  22. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/collaborative.py +0 -0
  23. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/config.py +0 -0
  24. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/context.py +0 -0
  25. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/cost_tracker.py +0 -0
  26. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/cot_display.py +0 -0
  27. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/deps.py +0 -0
  28. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/env_manager.py +0 -0
  29. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/error_explain.py +0 -0
  30. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/font_screen.py +0 -0
  31. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/generate_icon.py +0 -0
  32. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/history.py +0 -0
  33. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/hooks.py +0 -0
  34. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/image_input.py +0 -0
  35. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/init_project.py +0 -0
  36. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/interactive.py +0 -0
  37. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/keybindings.py +0 -0
  38. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/llm.py +0 -0
  39. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/login_screen.py +0 -0
  40. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/logout_screen.py +0 -0
  41. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/mcp_client.py +0 -0
  42. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/memory.py +0 -0
  43. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/migration.py +0 -0
  44. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/model_manager.py +0 -0
  45. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/multiline.py +0 -0
  46. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/notifications.py +0 -0
  47. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/oauth.py +0 -0
  48. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/output_styles.py +0 -0
  49. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/permissions.py +0 -0
  50. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/plans.py +0 -0
  51. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/plugins.py +0 -0
  52. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/preview_overlay.py +0 -0
  53. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/preview_server.py +0 -0
  54. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/progress.py +0 -0
  55. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/providers.py +0 -0
  56. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/raw_input.py +0 -0
  57. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/remote_control.py +0 -0
  58. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/scheduler.py +0 -0
  59. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/self_repair.py +0 -0
  60. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/server.py +0 -0
  61. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/session.py +0 -0
  62. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/skills.py +0 -0
  63. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/split_pane.py +0 -0
  64. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/ssh_remote.py +0 -0
  65. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/task_manager.py +0 -0
  66. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/terminal_profile.py +0 -0
  67. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/terminal_theme.py +0 -0
  68. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/theme.py +0 -0
  69. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/theme_screen.py +0 -0
  70. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/tool_executor.py +0 -0
  71. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/undo_system.py +0 -0
  72. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/user_config.py +0 -0
  73. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/vim_mode.py +0 -0
  74. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/voice.py +0 -0
  75. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/core/watch_mode.py +0 -0
  76. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/__init__.py +0 -0
  77. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/agent_tools.py +0 -0
  78. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/api_tools.py +0 -0
  79. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/bash_tool.py +0 -0
  80. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/db_tools.py +0 -0
  81. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/diff_tools.py +0 -0
  82. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/file_tools.py +0 -0
  83. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/git_tools.py +0 -0
  84. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/interaction_tools.py +0 -0
  85. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/lsp_tools.py +0 -0
  86. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/memory_tools.py +0 -0
  87. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/notebook_tools.py +0 -0
  88. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/plugin_tools.py +0 -0
  89. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/regex_tools.py +0 -0
  90. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/registry.py +0 -0
  91. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/search_tools.py +0 -0
  92. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/sql_tools.py +0 -0
  93. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/ssh_tools.py +0 -0
  94. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/todo_tools.py +0 -0
  95. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal/tools/web_tools.py +0 -0
  96. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal_code.egg-info/dependency_links.txt +0 -0
  97. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal_code.egg-info/entry_points.txt +0 -0
  98. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal_code.egg-info/requires.txt +0 -0
  99. {vaal_code-0.6.910 → vaal_code-0.6.930}/vaal_code.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vaal-code
3
- Version: 0.6.910
3
+ Version: 0.6.930
4
4
  Summary: AI coding agent CLI — local LLMs via Ollama or cloud models via API
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://e-e.tools/vaal
@@ -1,2 +1,2 @@
1
1
  """Vaal — Local LLM coding assistant CLI."""
2
- __version__ = "0.6.910"
2
+ __version__ = "0.6.930"
@@ -0,0 +1,349 @@
1
+ """Vaal Buddy — procedurally generated ASCII companion with personality."""
2
+
3
+ import hashlib
4
+ import json
5
+ import time
6
+ from pathlib import Path
7
+ from .config import VAAL_DIR
8
+
9
+ BUDDY_FILE = VAAL_DIR / "buddy.json"
10
+
11
+ RARITIES = ["common", "uncommon", "rare", "epic", "legendary"]
12
+ RARITY_WEIGHTS = {"common": 60, "uncommon": 25, "rare": 10, "epic": 4, "legendary": 1}
13
+ RARITY_STARS = {"common": "★", "uncommon": "★★", "rare": "★★★", "epic": "★★★★", "legendary": "★★★★★"}
14
+ RARITY_COLORS = {"common": "dim", "uncommon": "#5fd75f", "rare": "#5f87ff", "epic": "#af5fff", "legendary": "#d7d75f"}
15
+
16
+ SPECIES = [
17
+ "duck", "goose", "blob", "cat", "dragon", "octopus", "owl",
18
+ "penguin", "turtle", "snail", "ghost", "axolotl", "capybara",
19
+ "cactus", "robot", "rabbit", "mushroom", "chonk",
20
+ ]
21
+
22
+ EYES = ["·", "✦", "×", "◉", "@", "°"]
23
+
24
+ HATS = ["none", "crown", "tophat", "propeller", "halo", "wizard", "beanie", "tinyduck"]
25
+
26
+ STATS = ["DEBUGGING", "PATIENCE", "CHAOS", "WISDOM", "SNARK"]
27
+
28
+ # ASCII sprites — 5 lines tall, {E} = eye char
29
+ SPRITES = {
30
+ "duck": [' ', ' __ ', ' <({E} )___ ', ' ( ._> ', ' `--´ '],
31
+ "goose": [' ', ' ({E}> ', ' || ', ' _(__)_ ', ' ^^^^ '],
32
+ "blob": [' ', ' .----. ', ' ( {E} {E} ) ', ' ( ) ', ' `----´ '],
33
+ "cat": [' ', ' /\\_/\\ ', ' ( {E} {E}) ', ' ( ω ) ', ' (")_(") '],
34
+ "dragon": [' ', ' /^\\ /^\\ ', ' < {E} {E} > ', ' ( ~~ ) ', ' `-vvvv-´ '],
35
+ "octopus": [' ', ' .----. ', ' ( {E} {E} ) ', ' (______) ', ' /\\/\\/\\/\\ '],
36
+ "owl": [' ', ' /\\ /\\ ', ' (({E})({E})) ', ' ( >< ) ', ' `----´ '],
37
+ "penguin": [' ', ' .---. ', ' ({E}>{E}) ', ' /( )\\ ', ' `---´ '],
38
+ "turtle": [' ', ' _,--._ ', ' ( {E} {E} ) ', ' /[______]\\ ', ' `` `` '],
39
+ "snail": [' ', ' {E} .--. ', ' \\ ( @ ) ', ' \\_`--´ ', ' ~~~~~~~ '],
40
+ "ghost": [' ', ' .----. ', ' / {E} {E} \\ ', ' | | ', ' ~`~``~`~ '],
41
+ "axolotl": [' ', '}~(______)~{', '}~({E} .. {E})~{', ' ( .--. ) ', ' (_/ \\_) '],
42
+ "capybara": [' ', ' n______n ', ' ( {E} {E} ) ', ' ( oo ) ', ' `------´ '],
43
+ "cactus": [' ', ' n ____ n ', ' | |{E} {E}| | ', ' |_| |_| ', ' | | '],
44
+ "robot": [' ', ' .[||]. ', ' [ {E} {E} ] ', ' [ ==== ] ', ' `------´ '],
45
+ "rabbit": [' ', ' (\\__/) ', ' ( {E} {E} ) ', ' =( .. )= ', ' (")__(") '],
46
+ "mushroom": [' ', ' .-o-OO-o-. ', '(__________)', ' |{E} {E}| ', ' |____| '],
47
+ "chonk": [' ', ' /\\ /\\ ', ' ( {E} {E} ) ', ' ( .. ) ', ' `------´ '],
48
+ }
49
+
50
+ HAT_LINES = {
51
+ "none": "",
52
+ "crown": " \\^^^/ ",
53
+ "tophat": " [___] ",
54
+ "propeller":" -+- ",
55
+ "halo": " ( ) ",
56
+ "wizard": " /^\\ ",
57
+ "beanie": " (___) ",
58
+ "tinyduck": " ,> ",
59
+ }
60
+
61
+
62
+ def _mulberry32(seed: int):
63
+ """Seeded PRNG."""
64
+ a = seed & 0xFFFFFFFF
65
+ def next_val():
66
+ nonlocal a
67
+ a = (a + 0x6D2B79F5) & 0xFFFFFFFF
68
+ t = ((a ^ (a >> 15)) * (1 | a)) & 0xFFFFFFFF
69
+ t = ((t + ((t ^ (t >> 7)) * (61 | t))) ^ t) & 0xFFFFFFFF
70
+ return ((t ^ (t >> 14)) & 0xFFFFFFFF) / 4294967296
71
+ return next_val
72
+
73
+
74
+ def _hash_string(s: str) -> int:
75
+ """FNV-1a hash to match Claude Code's buddy generation."""
76
+ h = 2166136261
77
+ for ch in s:
78
+ h ^= ord(ch)
79
+ h = (h * 16777619) & 0xFFFFFFFF
80
+ return h
81
+
82
+
83
+ def _pick(rng, arr):
84
+ return arr[int(rng() * len(arr))]
85
+
86
+
87
+ def _roll_rarity(rng) -> str:
88
+ total = sum(RARITY_WEIGHTS.values())
89
+ roll = rng() * total
90
+ for r in RARITIES:
91
+ roll -= RARITY_WEIGHTS[r]
92
+ if roll < 0:
93
+ return r
94
+ return "common"
95
+
96
+
97
+ def _roll_stats(rng, rarity: str) -> dict:
98
+ floors = {"common": 5, "uncommon": 15, "rare": 25, "epic": 35, "legendary": 50}
99
+ floor = floors.get(rarity, 5)
100
+ peak = _pick(rng, STATS)
101
+ dump = _pick(rng, STATS)
102
+ while dump == peak:
103
+ dump = _pick(rng, STATS)
104
+
105
+ stats = {}
106
+ for name in STATS:
107
+ if name == peak:
108
+ stats[name] = min(100, floor + 50 + int(rng() * 30))
109
+ elif name == dump:
110
+ stats[name] = max(1, floor - 10 + int(rng() * 15))
111
+ else:
112
+ stats[name] = floor + int(rng() * 40)
113
+ return stats
114
+
115
+
116
+ def roll_buddy(user_id: str = "anon") -> dict:
117
+ """Generate deterministic buddy from user ID."""
118
+ seed = _hash_string(user_id + "friend-2026-401")
119
+ rng = _mulberry32(seed)
120
+
121
+ rarity = _roll_rarity(rng)
122
+ species = _pick(rng, SPECIES)
123
+ eye = _pick(rng, EYES)
124
+ hat = "none" if rarity == "common" else _pick(rng, HATS)
125
+ shiny = rng() < 0.01
126
+ stats = _roll_stats(rng, rarity)
127
+
128
+ return {
129
+ "rarity": rarity,
130
+ "species": species,
131
+ "eye": eye,
132
+ "hat": hat,
133
+ "shiny": shiny,
134
+ "stats": stats,
135
+ }
136
+
137
+
138
+ def render_sprite(buddy: dict) -> list[str]:
139
+ """Render the buddy as ASCII lines."""
140
+ species = buddy.get("species", "blob")
141
+ eye = buddy.get("eye", "·")
142
+ hat = buddy.get("hat", "none")
143
+
144
+ body = SPRITES.get(species, SPRITES["blob"])
145
+ lines = [line.replace("{E}", eye) for line in body]
146
+
147
+ # Add hat
148
+ if hat != "none" and not lines[0].strip():
149
+ lines[0] = HAT_LINES.get(hat, "")
150
+
151
+ # Remove blank first line if no hat
152
+ if not lines[0].strip():
153
+ lines = lines[1:]
154
+
155
+ return lines
156
+
157
+
158
+ def get_buddy() -> dict | None:
159
+ """Get saved buddy, or None."""
160
+ if BUDDY_FILE.exists():
161
+ try:
162
+ return json.loads(BUDDY_FILE.read_text(encoding="utf-8"))
163
+ except Exception:
164
+ pass
165
+ return None
166
+
167
+
168
+ def save_buddy(buddy: dict):
169
+ """Save buddy to disk."""
170
+ VAAL_DIR.mkdir(exist_ok=True)
171
+ BUDDY_FILE.write_text(json.dumps(buddy, indent=2), encoding="utf-8")
172
+
173
+
174
+ def hatch_buddy(user_id: str = "anon", name: str = "", personality: str = "") -> dict:
175
+ """Hatch a new buddy."""
176
+ bones = roll_buddy(user_id)
177
+ buddy = {
178
+ **bones,
179
+ "name": name or bones["species"].title(),
180
+ "personality": personality or "A mysterious companion",
181
+ "hatched_at": int(time.time()),
182
+ }
183
+ save_buddy(buddy)
184
+ return buddy
185
+
186
+
187
+ def play_hatch_animation(buddy: dict):
188
+ """Play an egg hatching animation in the terminal."""
189
+ import sys
190
+ import time as _time
191
+
192
+ def _w(s):
193
+ sys.stdout.write(s)
194
+ sys.stdout.flush()
195
+
196
+ def _clear_lines(n):
197
+ for _ in range(n):
198
+ _w("\033[A\033[2K")
199
+
200
+ rarity = buddy.get("rarity", "common")
201
+ species = buddy.get("species", "blob")
202
+ eye = buddy.get("eye", "·")
203
+ rcolor_code = {"common": "37", "uncommon": "32", "rare": "34", "epic": "35", "legendary": "33"}.get(rarity, "37")
204
+
205
+ # Egg frames
206
+ egg_frames = [
207
+ [
208
+ " ",
209
+ " .--. ",
210
+ " / \\ ",
211
+ " | | ",
212
+ " \\ / ",
213
+ " `--´ ",
214
+ ],
215
+ [
216
+ " ",
217
+ " .--. ",
218
+ " / · \\ ",
219
+ " | | ",
220
+ " \\ / ",
221
+ " `--´ ",
222
+ ],
223
+ [
224
+ " ",
225
+ " .~--. ",
226
+ " / · \\ ",
227
+ " | · | ",
228
+ " \\ / ",
229
+ " `--´ ",
230
+ ],
231
+ [
232
+ " * ",
233
+ " .~--~. ",
234
+ " / · · \\ ",
235
+ " | · | ",
236
+ " \\ · / ",
237
+ " `~--~´ ",
238
+ ],
239
+ [
240
+ " \\|/ ",
241
+ " ,~--~. ",
242
+ " / · · \\ ",
243
+ " | · · | ",
244
+ " \\ · · / ",
245
+ " `~--~´ ",
246
+ ],
247
+ [
248
+ " \\ | / ",
249
+ " --,~~.-- ",
250
+ " / \\ ",
251
+ " | CRACK| ",
252
+ " \\ / ",
253
+ " --`~~´-- ",
254
+ ],
255
+ [
256
+ " * \\ | / * ",
257
+ " .~~~~. ",
258
+ " / /\\ \\ ",
259
+ " | / · \\| ",
260
+ " \\/ · / ",
261
+ " `~~~~´ ",
262
+ ],
263
+ [
264
+ " * *\\|/* * ",
265
+ " ._/\\. ",
266
+ " / / \\ \\ ",
267
+ " |/ · · \\| ",
268
+ " / \\ ",
269
+ " `_/\\_´ ",
270
+ ],
271
+ ]
272
+
273
+ # Phase 1: Egg wobbling
274
+ _w("\n")
275
+ for i in range(12):
276
+ frame = egg_frames[min(i // 3, 2)]
277
+ for line in frame:
278
+ _w(f" \033[2m{line}\033[0m\n")
279
+ _time.sleep(0.25)
280
+ _clear_lines(len(frame))
281
+
282
+ # Phase 2: Cracking
283
+ for i in range(3, 6):
284
+ frame = egg_frames[min(i, len(egg_frames) - 1)]
285
+ for line in frame:
286
+ _w(f" \033[1;37m{line}\033[0m\n")
287
+ _time.sleep(0.4)
288
+ _clear_lines(len(frame))
289
+
290
+ # Phase 3: Breaking open
291
+ for i in range(6, len(egg_frames)):
292
+ frame = egg_frames[i]
293
+ for line in frame:
294
+ _w(f" \033[1;{rcolor_code}m{line}\033[0m\n")
295
+ _time.sleep(0.35)
296
+ _clear_lines(len(frame))
297
+
298
+ # Phase 4: Flash
299
+ _w(f"\033[1;{rcolor_code}m")
300
+ for _ in range(3):
301
+ _w(" ✦ ✦ ✦ ✦ ✦ ✦ ✦ ✦\n")
302
+ _time.sleep(0.1)
303
+ _clear_lines(1)
304
+ _time.sleep(0.1)
305
+ _w("\033[0m")
306
+
307
+ # Phase 5: Reveal the buddy
308
+ sprite = render_sprite(buddy)
309
+ stars = RARITY_STARS.get(rarity, "★")
310
+ name = buddy.get("name", "???")
311
+
312
+ _w(f"\n \033[1;{rcolor_code}m{name}\033[0m {stars}\n")
313
+ _w(f" \033[2m{rarity} {species}\033[0m\n\n")
314
+ for line in sprite:
315
+ _w(f" \033[1;{rcolor_code}m{line}\033[0m\n")
316
+ _w("\n")
317
+
318
+ if buddy.get("shiny"):
319
+ _w(f" \033[1;33m✧ ✧ ✧ S H I N Y ✧ ✧ ✧\033[0m\n\n")
320
+
321
+ _time.sleep(0.5)
322
+
323
+
324
+ def format_buddy_card(buddy: dict) -> str:
325
+ """Format buddy as a display card."""
326
+ rarity = buddy.get("rarity", "common")
327
+ stars = RARITY_STARS.get(rarity, "★")
328
+ sprite = render_sprite(buddy)
329
+
330
+ lines = []
331
+ lines.append(f" {buddy.get('name', '???')} {stars}")
332
+ lines.append(f" {rarity} {buddy.get('species', '???')}")
333
+ if buddy.get("shiny"):
334
+ lines.append(" ✧ SHINY ✧")
335
+ lines.append("")
336
+ for sl in sprite:
337
+ lines.append(f" {sl}")
338
+ lines.append("")
339
+ if buddy.get("hat", "none") != "none":
340
+ lines.append(f" Hat: {buddy['hat']}")
341
+ stats = buddy.get("stats", {})
342
+ if stats:
343
+ for stat, val in stats.items():
344
+ bar = "█" * (val // 10) + "░" * (10 - val // 10)
345
+ lines.append(f" {stat:10s} {bar} {val}")
346
+ if buddy.get("personality"):
347
+ lines.append(f" \"{buddy['personality']}\"")
348
+
349
+ return "\n".join(lines)
@@ -74,6 +74,7 @@ COMMANDS = [
74
74
  ("/ssh", "SSH remote execution (connect/run/disconnect)"),
75
75
  ("/explain", "Explain an error or stack trace"),
76
76
  ("/migrate", "Guided framework/language migration"),
77
+ ("/buddy", "Your companion (hatch/show/reroll/name)"),
77
78
  ("/register", "Create a vaal account"),
78
79
  ("/remote", "Remote control from web browser"),
79
80
  ("/task", "Manage tasks (create/update/complete)"),
@@ -1148,8 +1148,31 @@ def run_repl(model: str = DEFAULT_MODEL, permission_mode: str = DEFAULT_PERMISSI
1148
1148
  }
1149
1149
 
1150
1150
  def print_prompt_line():
1151
- """Print the thin line above the prompt + mode hint below."""
1152
- console.print(f"[dim]{'─' * min(console.width, 70)}[/dim]")
1151
+ """Print the thin line above the prompt with buddy on the right."""
1152
+ from .buddy import get_buddy, render_sprite, RARITY_COLORS
1153
+ w = min(console.width or 80, 80)
1154
+ line_w = w - 14 # Leave room for buddy
1155
+
1156
+ buddy = get_buddy()
1157
+ if buddy:
1158
+ sprite = render_sprite(buddy)
1159
+ rcolor = RARITY_COLORS.get(buddy.get("rarity", "common"), "dim")
1160
+ bname = buddy.get("name", "")
1161
+
1162
+ # Print separator with buddy sprite on the right
1163
+ _write(f"\033[2m{'─' * line_w}\033[0m\n")
1164
+ # Print buddy sprite lines aligned to the right
1165
+ for i, sline in enumerate(sprite):
1166
+ # Cursor to column position for right-alignment
1167
+ col = line_w + 2
1168
+ _write(f"\033[{col}G\033[2m{sline}\033[0m\n")
1169
+ # Print buddy name under sprite
1170
+ _write(f"\033[{line_w + 4}G\033[2m{bname}\033[0m")
1171
+ # Move cursor back to start for the > prompt
1172
+ up = len(sprite) + 1
1173
+ _write(f"\033[{up}A\r")
1174
+ else:
1175
+ console.print(f"[dim]{'─' * line_w}[/dim]")
1153
1176
 
1154
1177
  # Check if any models are installed — if not, open model selector
1155
1178
  from .llm import list_models as _check_models
@@ -1651,6 +1674,7 @@ def handle_command(cmd: str, session: Session, active_model: list, permissions:
1651
1674
  ("/register", "Create a vaal account on e-e.tools"),
1652
1675
  ("/login vaal", "Login to your vaal account"),
1653
1676
  ("/remote start|stop", "Remote control from web browser"),
1677
+ ("/buddy", "Your companion (hatch/show/reroll/name)"),
1654
1678
  ("/doctor", "System health diagnostics"),
1655
1679
  ("/preview <path>", "Live preview with interactive inspector"),
1656
1680
  ("/preview stop", "Stop preview server"),
@@ -2380,6 +2404,9 @@ def handle_command(cmd: str, session: Session, active_model: list, permissions:
2380
2404
  elif command == "/remote":
2381
2405
  _handle_remote_command(arg, active_model, session, components)
2382
2406
 
2407
+ elif command == "/buddy":
2408
+ _handle_buddy_command(arg)
2409
+
2383
2410
  elif command == "/doctor":
2384
2411
  _handle_doctor_command(active_model)
2385
2412
 
@@ -3242,6 +3269,65 @@ def get_vaal_user() -> dict | None:
3242
3269
  return None
3243
3270
 
3244
3271
 
3272
+ def _handle_buddy_command(arg: str):
3273
+ """Handle /buddy [hatch|show|reroll|name] — companion system."""
3274
+ from .buddy import get_buddy, hatch_buddy, format_buddy_card, roll_buddy, save_buddy, RARITY_COLORS
3275
+
3276
+ action = arg.strip().lower().split(maxsplit=1) if arg.strip() else ["show"]
3277
+ cmd = action[0]
3278
+ cmd_arg = action[1] if len(action) > 1 else ""
3279
+
3280
+ buddy = get_buddy()
3281
+
3282
+ if cmd == "hatch" or (cmd == "show" and not buddy):
3283
+ # Hatch a new buddy
3284
+ from .buddy import play_hatch_animation
3285
+ user = get_vaal_user()
3286
+ user_id = user["username"] if user else "anon"
3287
+ buddy = hatch_buddy(user_id)
3288
+ play_hatch_animation(buddy)
3289
+ # Show stats after animation
3290
+ stats_dict = buddy.get("stats", {})
3291
+ for stat, val in stats_dict.items():
3292
+ bar = "█" * (val // 10) + "░" * (10 - val // 10)
3293
+ console.print(f" [dim]{stat:10s}[/dim] [{VAAL_CYAN}]{bar}[/] {val}")
3294
+ console.print()
3295
+
3296
+ elif cmd == "show":
3297
+ rcolor = RARITY_COLORS.get(buddy["rarity"], "dim")
3298
+ console.print()
3299
+ console.print(f"[{rcolor}]{format_buddy_card(buddy)}[/]")
3300
+ console.print()
3301
+
3302
+ elif cmd == "reroll":
3303
+ from .buddy import play_hatch_animation
3304
+ import secrets
3305
+ seed = secrets.token_hex(8)
3306
+ bones = roll_buddy(seed)
3307
+ buddy = {**bones, "name": bones["species"].title(), "personality": "A rerolled companion", "hatched_at": int(time.time())}
3308
+ save_buddy(buddy)
3309
+ play_hatch_animation(buddy)
3310
+ stats_dict = buddy.get("stats", {})
3311
+ for stat, val in stats_dict.items():
3312
+ bar = "█" * (val // 10) + "░" * (10 - val // 10)
3313
+ console.print(f" [dim]{stat:10s}[/dim] [{VAAL_CYAN}]{bar}[/] {val}")
3314
+ console.print()
3315
+
3316
+ elif cmd == "name" and cmd_arg:
3317
+ if not buddy:
3318
+ console.print(f" [dim]No buddy yet. Use /buddy hatch[/dim]")
3319
+ return
3320
+ buddy["name"] = cmd_arg
3321
+ save_buddy(buddy)
3322
+ console.print(f" [{VAAL_GREEN}]Renamed to {cmd_arg}[/]")
3323
+
3324
+ else:
3325
+ console.print(f"[dim] /buddy — Show your buddy[/dim]")
3326
+ console.print(f"[dim] /buddy hatch — Hatch a new buddy[/dim]")
3327
+ console.print(f"[dim] /buddy reroll — Reroll (random seed)[/dim]")
3328
+ console.print(f"[dim] /buddy name X — Rename your buddy[/dim]")
3329
+
3330
+
3245
3331
  def _handle_doctor_command(active_model: list):
3246
3332
  """Handle /doctor — system health diagnostics."""
3247
3333
  import sys
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vaal-code
3
- Version: 0.6.910
3
+ Version: 0.6.930
4
4
  Summary: AI coding agent CLI — local LLMs via Ollama or cloud models via API
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://e-e.tools/vaal
@@ -9,6 +9,7 @@ vaal/core/auto_fix.py
9
9
  vaal/core/bootstrap.py
10
10
  vaal/core/branching.py
11
11
  vaal/core/bridge.py
12
+ vaal/core/buddy.py
12
13
  vaal/core/changelog.py
13
14
  vaal/core/clipboard.py
14
15
  vaal/core/code_review.py
File without changes
File without changes
File without changes