ashlrcode 1.0.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 (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -0
  3. package/package.json +46 -0
  4. package/src/__tests__/branded-types.test.ts +47 -0
  5. package/src/__tests__/context.test.ts +163 -0
  6. package/src/__tests__/cost-tracker.test.ts +274 -0
  7. package/src/__tests__/cron.test.ts +197 -0
  8. package/src/__tests__/dream.test.ts +204 -0
  9. package/src/__tests__/error-handler.test.ts +192 -0
  10. package/src/__tests__/features.test.ts +69 -0
  11. package/src/__tests__/file-history.test.ts +177 -0
  12. package/src/__tests__/hooks.test.ts +145 -0
  13. package/src/__tests__/keybindings.test.ts +159 -0
  14. package/src/__tests__/model-patches.test.ts +82 -0
  15. package/src/__tests__/permissions-rules.test.ts +121 -0
  16. package/src/__tests__/permissions.test.ts +108 -0
  17. package/src/__tests__/project-config.test.ts +63 -0
  18. package/src/__tests__/retry.test.ts +321 -0
  19. package/src/__tests__/router.test.ts +158 -0
  20. package/src/__tests__/session-compact.test.ts +191 -0
  21. package/src/__tests__/session.test.ts +145 -0
  22. package/src/__tests__/skill-registry.test.ts +130 -0
  23. package/src/__tests__/speculation.test.ts +196 -0
  24. package/src/__tests__/tasks-v2.test.ts +267 -0
  25. package/src/__tests__/telemetry.test.ts +149 -0
  26. package/src/__tests__/tool-executor.test.ts +141 -0
  27. package/src/__tests__/tool-registry.test.ts +166 -0
  28. package/src/__tests__/undercover.test.ts +93 -0
  29. package/src/__tests__/workflow.test.ts +195 -0
  30. package/src/agent/async-context.ts +64 -0
  31. package/src/agent/context.ts +245 -0
  32. package/src/agent/cron.ts +189 -0
  33. package/src/agent/dream.ts +165 -0
  34. package/src/agent/error-handler.ts +108 -0
  35. package/src/agent/ipc.ts +256 -0
  36. package/src/agent/kairos.ts +207 -0
  37. package/src/agent/loop.ts +314 -0
  38. package/src/agent/model-patches.ts +68 -0
  39. package/src/agent/speculation.ts +219 -0
  40. package/src/agent/sub-agent.ts +125 -0
  41. package/src/agent/system-prompt.ts +231 -0
  42. package/src/agent/team.ts +220 -0
  43. package/src/agent/tool-executor.ts +162 -0
  44. package/src/agent/workflow.ts +189 -0
  45. package/src/agent/worktree-manager.ts +86 -0
  46. package/src/autopilot/queue.ts +186 -0
  47. package/src/autopilot/scanner.ts +245 -0
  48. package/src/autopilot/types.ts +58 -0
  49. package/src/bridge/bridge-client.ts +57 -0
  50. package/src/bridge/bridge-server.ts +81 -0
  51. package/src/cli.ts +1120 -0
  52. package/src/config/features.ts +51 -0
  53. package/src/config/git.ts +137 -0
  54. package/src/config/hooks.ts +201 -0
  55. package/src/config/permissions.ts +251 -0
  56. package/src/config/project-config.ts +63 -0
  57. package/src/config/remote-settings.ts +163 -0
  58. package/src/config/settings-sync.ts +170 -0
  59. package/src/config/settings.ts +113 -0
  60. package/src/config/undercover.ts +76 -0
  61. package/src/config/upgrade-notice.ts +65 -0
  62. package/src/mcp/client.ts +197 -0
  63. package/src/mcp/manager.ts +125 -0
  64. package/src/mcp/oauth.ts +252 -0
  65. package/src/mcp/types.ts +61 -0
  66. package/src/persistence/memory.ts +129 -0
  67. package/src/persistence/session.ts +289 -0
  68. package/src/planning/plan-mode.ts +128 -0
  69. package/src/planning/plan-tools.ts +138 -0
  70. package/src/providers/anthropic.ts +177 -0
  71. package/src/providers/cost-tracker.ts +184 -0
  72. package/src/providers/retry.ts +264 -0
  73. package/src/providers/router.ts +159 -0
  74. package/src/providers/types.ts +79 -0
  75. package/src/providers/xai.ts +217 -0
  76. package/src/repl.tsx +1384 -0
  77. package/src/setup.ts +119 -0
  78. package/src/skills/loader.ts +78 -0
  79. package/src/skills/registry.ts +78 -0
  80. package/src/skills/types.ts +11 -0
  81. package/src/state/file-history.ts +264 -0
  82. package/src/telemetry/event-log.ts +116 -0
  83. package/src/tools/agent.ts +133 -0
  84. package/src/tools/ask-user.ts +229 -0
  85. package/src/tools/bash.ts +146 -0
  86. package/src/tools/config.ts +147 -0
  87. package/src/tools/diff.ts +137 -0
  88. package/src/tools/file-edit.ts +123 -0
  89. package/src/tools/file-read.ts +82 -0
  90. package/src/tools/file-write.ts +82 -0
  91. package/src/tools/glob.ts +76 -0
  92. package/src/tools/grep.ts +187 -0
  93. package/src/tools/ls.ts +77 -0
  94. package/src/tools/lsp.ts +375 -0
  95. package/src/tools/mcp-resources.ts +83 -0
  96. package/src/tools/mcp-tool.ts +47 -0
  97. package/src/tools/memory.ts +148 -0
  98. package/src/tools/notebook-edit.ts +133 -0
  99. package/src/tools/peers.ts +113 -0
  100. package/src/tools/powershell.ts +83 -0
  101. package/src/tools/registry.ts +114 -0
  102. package/src/tools/send-message.ts +75 -0
  103. package/src/tools/sleep.ts +50 -0
  104. package/src/tools/snip.ts +143 -0
  105. package/src/tools/tasks.ts +349 -0
  106. package/src/tools/team.ts +309 -0
  107. package/src/tools/todo-write.ts +93 -0
  108. package/src/tools/tool-search.ts +83 -0
  109. package/src/tools/types.ts +52 -0
  110. package/src/tools/web-browser.ts +263 -0
  111. package/src/tools/web-fetch.ts +118 -0
  112. package/src/tools/web-search.ts +107 -0
  113. package/src/tools/workflow.ts +188 -0
  114. package/src/tools/worktree.ts +143 -0
  115. package/src/types/branded.ts +22 -0
  116. package/src/ui/App.tsx +184 -0
  117. package/src/ui/BuddyPanel.tsx +52 -0
  118. package/src/ui/PermissionPrompt.tsx +29 -0
  119. package/src/ui/banner.ts +217 -0
  120. package/src/ui/buddy-ai.ts +108 -0
  121. package/src/ui/buddy.ts +466 -0
  122. package/src/ui/context-bar.ts +60 -0
  123. package/src/ui/effort.ts +65 -0
  124. package/src/ui/keybindings.ts +143 -0
  125. package/src/ui/markdown.ts +271 -0
  126. package/src/ui/message-renderer.ts +73 -0
  127. package/src/ui/mode.ts +80 -0
  128. package/src/ui/notifications.ts +57 -0
  129. package/src/ui/speech-bubble.ts +95 -0
  130. package/src/ui/spinner.ts +116 -0
  131. package/src/ui/theme.ts +98 -0
  132. package/src/version.ts +5 -0
  133. package/src/voice/voice-mode.ts +169 -0
@@ -0,0 +1,108 @@
1
+ /**
2
+ * AI-powered buddy comments.
3
+ *
4
+ * Calls grok-4-1-fast-reasoning with minimal tokens for contextual
5
+ * buddy reactions. Mixed 80/20 with hardcoded quips for cost efficiency.
6
+ */
7
+
8
+ import OpenAI from "openai";
9
+
10
+ export type BuddyCommentType = "quip" | "suggestion" | "reaction";
11
+
12
+ export interface BuddyComment {
13
+ text: string;
14
+ type: BuddyCommentType;
15
+ }
16
+
17
+ const SYSTEM_PROMPT = `You are Glitch, a sarcastic capybara coding buddy in a terminal. Give ONE short sentence (max 15 words).
18
+
19
+ Rules:
20
+ - Be funny, edgy, sarcastic, or give a genuinely useful coding suggestion
21
+ - Never be boring or generic
22
+ - If the context suggests a problem, give a real helpful suggestion
23
+ - If things are going well, be witty and irreverent
24
+ - Never explain yourself or add caveats
25
+ - Just the one sentence, nothing else`;
26
+
27
+ /**
28
+ * Generate an AI-powered buddy comment.
29
+ * Falls back to a hardcoded quip on error.
30
+ */
31
+ export async function generateBuddyComment(
32
+ context: {
33
+ lastTool?: string;
34
+ lastResult?: string;
35
+ mood: string;
36
+ errorOccurred?: boolean;
37
+ },
38
+ apiKey: string,
39
+ baseURL?: string
40
+ ): Promise<BuddyComment> {
41
+ try {
42
+ const client = new OpenAI({
43
+ apiKey,
44
+ baseURL: baseURL ?? "https://api.x.ai/v1",
45
+ });
46
+
47
+ // Build a tiny context string
48
+ let userMsg = `Mood: ${context.mood}.`;
49
+ if (context.lastTool) userMsg += ` Last tool: ${context.lastTool}.`;
50
+ if (context.lastResult) userMsg += ` Result: ${context.lastResult.slice(0, 50)}.`;
51
+ if (context.errorOccurred) userMsg += " An error just happened.";
52
+
53
+ const response = await client.chat.completions.create({
54
+ model: "grok-4-1-fast-reasoning",
55
+ messages: [
56
+ { role: "system", content: SYSTEM_PROMPT },
57
+ { role: "user", content: userMsg },
58
+ ],
59
+ max_tokens: 30,
60
+ temperature: 0.9,
61
+ });
62
+
63
+ const text = response.choices[0]?.message?.content?.trim() ?? "";
64
+ if (!text) return fallback(context.mood);
65
+
66
+ // Classify the comment type
67
+ const type = classifyComment(text, context.errorOccurred);
68
+ return { text, type };
69
+ } catch {
70
+ return fallback(context.mood);
71
+ }
72
+ }
73
+
74
+ function classifyComment(text: string, hadError?: boolean): BuddyCommentType {
75
+ const lower = text.toLowerCase();
76
+ // Suggestions contain actionable words
77
+ if (lower.includes("try ") || lower.includes("consider ") || lower.includes("should ") ||
78
+ lower.includes("add ") || lower.includes("check ") || lower.includes("maybe ") ||
79
+ lower.includes("might want") || lower.includes("don't forget")) {
80
+ return "suggestion";
81
+ }
82
+ // Reactions to events
83
+ if (hadError || lower.includes("nice") || lower.includes("clean") || lower.includes("good") ||
84
+ lower.includes("oops") || lower.includes("yikes") || lower.includes("wow")) {
85
+ return "reaction";
86
+ }
87
+ return "quip";
88
+ }
89
+
90
+ const FALLBACK_QUIPS: Record<string, string[]> = {
91
+ happy: ["ship it", "lgtm", "we move"],
92
+ thinking: ["processing...", "hmm", "interesting"],
93
+ sleepy: ["*yawn*", "zz", "coffee?"],
94
+ };
95
+
96
+ function fallback(mood: string): BuddyComment {
97
+ const quips = FALLBACK_QUIPS[mood] ?? FALLBACK_QUIPS.happy!;
98
+ return { text: quips[Math.floor(Math.random() * quips.length)]!, type: "quip" };
99
+ }
100
+
101
+ /**
102
+ * Decide whether to use AI or hardcoded for this turn.
103
+ */
104
+ export function shouldUseAI(turnCount: number, hadError: boolean): boolean {
105
+ if (hadError) return true; // Always AI on errors
106
+ if (turnCount % 5 === 0) return true; // Every 5th turn
107
+ return false; // Otherwise hardcoded
108
+ }
@@ -0,0 +1,466 @@
1
+ /**
2
+ * Virtual pet buddy — a persistent companion that lives in your terminal.
3
+ *
4
+ * Species is deterministically chosen by hashing the user's home directory,
5
+ * so you always get the same pet. The pet reacts to session activity with
6
+ * a simple mood system.
7
+ */
8
+
9
+ import { existsSync } from "fs";
10
+ import { readFile, writeFile, mkdir } from "fs/promises";
11
+ import { join } from "path";
12
+ import { homedir } from "os";
13
+ import { createHash } from "crypto";
14
+ import { theme } from "./theme.ts";
15
+ import { getConfigDir } from "../config/settings.ts";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Types
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export type Species =
22
+ | "penguin"
23
+ | "cat"
24
+ | "ghost"
25
+ | "dragon"
26
+ | "owl"
27
+ | "robot"
28
+ | "axolotl"
29
+ | "capybara";
30
+
31
+ export type Mood = "happy" | "thinking" | "sleepy";
32
+
33
+ export type Rarity = "common" | "uncommon" | "rare" | "epic" | "legendary";
34
+
35
+ const RARITY_TABLE: Record<Species, Rarity> = {
36
+ penguin: "common", // 60%
37
+ cat: "common",
38
+ ghost: "uncommon", // 25%
39
+ owl: "uncommon",
40
+ robot: "rare", // 10%
41
+ dragon: "rare",
42
+ axolotl: "epic", // 4%
43
+ capybara: "legendary", // 1%
44
+ };
45
+
46
+ export const RARITY_COLORS: Record<Rarity, string> = {
47
+ common: "white",
48
+ uncommon: "green",
49
+ rare: "blue",
50
+ epic: "magenta",
51
+ legendary: "yellow",
52
+ };
53
+
54
+ export type Hat = "none" | "crown" | "tophat" | "propeller" | "halo" | "wizard" | "beanie";
55
+
56
+ const HAT_ART: Record<Hat, string> = {
57
+ none: "",
58
+ crown: " 👑 ",
59
+ tophat: " 🎩 ",
60
+ propeller: " 🧢 ",
61
+ halo: " ✨ ",
62
+ wizard: " 🧙 ",
63
+ beanie: " 🎿 ",
64
+ };
65
+
66
+ export interface BuddyStats {
67
+ debugging: number; // 1-10
68
+ patience: number;
69
+ chaos: number;
70
+ wisdom: number;
71
+ snark: number;
72
+ }
73
+
74
+ function generateStats(hash: number): BuddyStats {
75
+ return {
76
+ debugging: ((hash >>> 0) % 10) + 1,
77
+ patience: ((hash >>> 4) % 10) + 1,
78
+ chaos: ((hash >>> 8) % 10) + 1,
79
+ wisdom: ((hash >>> 12) % 10) + 1,
80
+ snark: ((hash >>> 16) % 10) + 1,
81
+ };
82
+ }
83
+
84
+ function isShiny(hash: number): boolean {
85
+ return (hash % 100) === 0; // 1% chance
86
+ }
87
+
88
+ export interface BuddyData {
89
+ species: Species;
90
+ name: string;
91
+ totalSessions: number;
92
+ mood: Mood;
93
+ /** Cumulative successful tool calls across sessions. */
94
+ toolCalls: number;
95
+ rarity: Rarity;
96
+ hat: Hat;
97
+ stats: BuddyStats;
98
+ shiny: boolean;
99
+ /** Level based on totalSessions. */
100
+ level: number;
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // ASCII art — 3-4 lines, ~12-15 chars wide
105
+ // ---------------------------------------------------------------------------
106
+
107
+ // All art lines are padded to exactly 10 chars wide for alignment.
108
+ // 2 animation frames per mood, cycles every 1.5s.
109
+ const W = 10; // art width
110
+ function pad(lines: string[]): string[] {
111
+ return lines.map(l => l.padEnd(W));
112
+ }
113
+
114
+ const ASCII_ART: Record<Species, Record<Mood, string[][]>> = {
115
+ capybara: {
116
+ happy: [
117
+ pad([" c\\ /c ", " ( . . ) ", " ( _nn_ ) ", " (______) ", " || || "]),
118
+ pad([" c\\ /C ", " ( . .) ", " ( _nn_ ) ", " (______) ", " || || "]),
119
+ ],
120
+ thinking: [
121
+ pad([" c\\ /c ", " ( o . ) ", " ( _nn_ ) ", " (__?___) ", " || || "]),
122
+ pad([" c\\ /c ", " ( . o ) ", " ( _nn_ ) ", " (___?__) ", " || || "]),
123
+ ],
124
+ sleepy: [
125
+ pad([" c\\ /c ", " ( - - ) ", " ( _nn_ )z", " (______) ", " || || "]),
126
+ pad([" c\\ /c ", " ( - - ) ", " ( _nn_ ) ", " (______)z", " || || "]),
127
+ ],
128
+ },
129
+ penguin: {
130
+ happy: [
131
+ pad([" .-. ", " (·>·) ", " /| |\\ ", " \" \" "]),
132
+ pad([" .-. ", " (·>·)/ ", " /| | ", " \" \" "]),
133
+ ],
134
+ thinking: [
135
+ pad([" .-. ", " (·.·) ", " /| |\\ ", " \" \" "]),
136
+ pad([" .-. ? ", " (·.·) ", " /| |\\ ", " \" \" "]),
137
+ ],
138
+ sleepy: [
139
+ pad([" .-. ", " (-.-) z", " /| |\\ ", " \" \" "]),
140
+ pad([" .-. zZ", " (-.-) ", " /| |\\ ", " \" \" "]),
141
+ ],
142
+ },
143
+ cat: {
144
+ happy: [
145
+ pad([" /\\_/\\ ", " ( ^.^ ) ", " > ~ < ", " "]),
146
+ pad([" /\\_/\\ ", " ( ^.^ )/", " > < ", " "]),
147
+ ],
148
+ thinking: [
149
+ pad([" /\\_/\\ ", " ( o.o ) ", " > . < ", " "]),
150
+ pad([" /\\_/\\ ?", " ( o.o ) ", " > . < ", " "]),
151
+ ],
152
+ sleepy: [
153
+ pad([" /\\_/\\ ", " ( -.- ) ", " > _ < z", " "]),
154
+ pad([" /\\_/\\ z", " ( -.- )Z", " > _ < ", " "]),
155
+ ],
156
+ },
157
+ ghost: {
158
+ happy: [
159
+ pad([" .-. ", " (^ ^) ", " | | | ", " '~~~' "]),
160
+ pad([" .-. ", " (^ ^)/ ", " | | ", " '~~~' "]),
161
+ ],
162
+ thinking: [
163
+ pad([" .-. ", " (o o) ", " | ? | ", " '~~~' "]),
164
+ pad([" .-. ? ", " (o o) ", " | | ", " '~~~' "]),
165
+ ],
166
+ sleepy: [
167
+ pad([" .-. ", " (- -) ", " | z | ", " '~~~' "]),
168
+ pad([" .-. z ", " (- -)Z ", " | | ", " '~~~' "]),
169
+ ],
170
+ },
171
+ dragon: {
172
+ happy: [
173
+ pad([" /\\_/\\ ~ ", " (^.^ )> ", " |)_(| ", " "]),
174
+ pad([" /\\_/\\~~ ", " (^.^ )>>", " |)_(| ", " "]),
175
+ ],
176
+ thinking: [
177
+ pad([" /\\_/\\ ", " (o.o ) ", " |)_(| ", " "]),
178
+ pad([" /\\_/\\ ? ", " (o.o ) ", " |)_(| ", " "]),
179
+ ],
180
+ sleepy: [
181
+ pad([" /\\_/\\ ", " (-.- ) z", " |)_(| ", " "]),
182
+ pad([" /\\_/\\ zZ", " (-.- ) ", " |)_(| ", " "]),
183
+ ],
184
+ },
185
+ owl: {
186
+ happy: [
187
+ pad([" (\\,/) ", " {^,^} ", " /| |\\ ", " "]),
188
+ pad([" (\\,/) ", " {^,^}/ ", " /| | ", " "]),
189
+ ],
190
+ thinking: [
191
+ pad([" (\\,/) ", " {o,o} ", " /| |\\ ", " "]),
192
+ pad([" (\\,/) ?", " {o,o} ", " /| |\\ ", " "]),
193
+ ],
194
+ sleepy: [
195
+ pad([" (\\,/) ", " {-,-} z", " /| |\\ ", " "]),
196
+ pad([" (\\,/) z", " {-,-}Z ", " /| |\\ ", " "]),
197
+ ],
198
+ },
199
+ robot: {
200
+ happy: [
201
+ pad([" ┌───┐ ", " [^_^] ", " /|=|\\ ", " d b "]),
202
+ pad([" ┌───┐ ", " [^_^]/ ", " /|=| ", " d b "]),
203
+ ],
204
+ thinking: [
205
+ pad([" ┌───┐ ", " [o_o] ", " /|=|\\ ", " d b "]),
206
+ pad([" ┌───┐ ?", " [o_o] ", " /|=|\\ ", " d b "]),
207
+ ],
208
+ sleepy: [
209
+ pad([" ┌───┐ ", " [-_-] z", " /|=|\\ ", " d b "]),
210
+ pad([" ┌───┐ z", " [-_-]Z ", " /|=|\\ ", " d b "]),
211
+ ],
212
+ },
213
+ axolotl: {
214
+ happy: [
215
+ pad([" \\(^u^)/ ", " | _ | ", " ~---~ ", " "]),
216
+ pad([" \\(^u^)~ ", " | _ | ", " ~---~ ", " "]),
217
+ ],
218
+ thinking: [
219
+ pad([" \\(·u·) ", " | _ | ?", " ~---~ ", " "]),
220
+ pad([" \\(·u·)? ", " | _ | ", " ~---~ ", " "]),
221
+ ],
222
+ sleepy: [
223
+ pad([" \\(-u-) ", " | _ | z", " ~---~ ", " "]),
224
+ pad([" \\(-u-) z", " | _ |Z ", " ~---~ ", " "]),
225
+ ],
226
+ },
227
+ };
228
+
229
+ // Animation frame counter — started/stopped explicitly to avoid leaked handles
230
+ let animFrame = 0;
231
+ let animInterval: ReturnType<typeof setInterval> | null = null;
232
+
233
+ export function startBuddyAnimation(): void {
234
+ if (animInterval) return;
235
+ animInterval = setInterval(() => { animFrame++; }, 1500);
236
+ }
237
+
238
+ export function stopBuddyAnimation(): void {
239
+ if (animInterval) {
240
+ clearInterval(animInterval);
241
+ animInterval = null;
242
+ }
243
+ }
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // Name pool
247
+ // ---------------------------------------------------------------------------
248
+
249
+ const NAMES = [
250
+ "Pixel", "Byte", "Chip", "Spark", "Nova", "Echo", "Zen", "Dot",
251
+ "Flux", "Nyx", "Orbit", "Glitch", "Rune", "Wren", "Maple", "Qubit",
252
+ "Fern", "Mochi", "Comet", "Nimbus", "Pebble", "Blink", "Cosmo", "Drift",
253
+ ];
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // Deterministic generation helpers
257
+ // ---------------------------------------------------------------------------
258
+
259
+ const SPECIES_LIST: Species[] = [
260
+ "penguin", "cat", "ghost", "dragon", "owl", "robot", "axolotl", "capybara",
261
+ ];
262
+
263
+ /**
264
+ * Simple 32-bit hash of a string, used to deterministically pick species/name.
265
+ */
266
+ function hashString(input: string): number {
267
+ const digest = createHash("sha256").update(input).digest();
268
+ // Read first 4 bytes as unsigned 32-bit int
269
+ return digest.readUInt32BE(0);
270
+ }
271
+
272
+ function pickSpecies(hash: number): Species {
273
+ return SPECIES_LIST[hash % SPECIES_LIST.length]!;
274
+ }
275
+
276
+ function pickName(hash: number): string {
277
+ // Use a different portion of the hash so name != species index
278
+ const nameIndex = (hash >>> 8) % NAMES.length;
279
+ return NAMES[nameIndex]!;
280
+ }
281
+
282
+ // ---------------------------------------------------------------------------
283
+ // Persistence
284
+ // ---------------------------------------------------------------------------
285
+
286
+ function getBuddyPath(): string {
287
+ return join(getConfigDir(), "buddy.json");
288
+ }
289
+
290
+ export async function loadBuddy(): Promise<BuddyData> {
291
+ const path = getBuddyPath();
292
+
293
+ const hash = hashString(homedir());
294
+
295
+ if (existsSync(path)) {
296
+ try {
297
+ const raw = await readFile(path, "utf-8");
298
+ const buddy = JSON.parse(raw) as BuddyData;
299
+
300
+ // Backfill new fields for buddies saved before the evolution update
301
+ if (!buddy.rarity) buddy.rarity = RARITY_TABLE[buddy.species] ?? "common";
302
+ if (!buddy.hat) buddy.hat = "none";
303
+ if (!buddy.stats) buddy.stats = generateStats(hash);
304
+ if (buddy.shiny === undefined) buddy.shiny = isShiny(hash);
305
+ if (!buddy.level) buddy.level = Math.floor(buddy.totalSessions / 5) + 1;
306
+
307
+ return buddy;
308
+ } catch {
309
+ // Corrupted file — regenerate below
310
+ }
311
+ }
312
+
313
+ // First run: generate deterministically from home directory
314
+ const buddy: BuddyData = {
315
+ species: pickSpecies(hash),
316
+ name: pickName(hash),
317
+ totalSessions: 0,
318
+ mood: "sleepy",
319
+ toolCalls: 0,
320
+ rarity: RARITY_TABLE[pickSpecies(hash)] ?? "common",
321
+ hat: "none",
322
+ stats: generateStats(hash),
323
+ shiny: isShiny(hash),
324
+ level: 1,
325
+ };
326
+
327
+ await saveBuddy(buddy);
328
+ return buddy;
329
+ }
330
+
331
+ export async function saveBuddy(buddy: BuddyData): Promise<void> {
332
+ const dir = getConfigDir();
333
+ await mkdir(dir, { recursive: true });
334
+ await writeFile(getBuddyPath(), JSON.stringify(buddy, null, 2) + "\n", "utf-8");
335
+ }
336
+
337
+ // ---------------------------------------------------------------------------
338
+ // Display
339
+ // ---------------------------------------------------------------------------
340
+
341
+ /**
342
+ * Get the buddy's ASCII art lines for current mood + animation frame.
343
+ */
344
+ export function getBuddyArt(buddy: BuddyData): string[] {
345
+ const moodArt = ASCII_ART[buddy.species]?.[buddy.mood];
346
+ if (!moodArt) return pad([" (?) "]);
347
+ const frameIndex = animFrame % moodArt.length;
348
+ const artLines = [...moodArt[frameIndex]!];
349
+
350
+ // Prepend hat art if the buddy has one equipped
351
+ const hat = HAT_ART[buddy.hat];
352
+ if (hat) {
353
+ artLines.unshift(hat.padEnd(W));
354
+ }
355
+
356
+ return artLines;
357
+ }
358
+
359
+ /**
360
+ * Print the buddy's ASCII art with its name and mood.
361
+ * Designed to sit neatly under the startup banner.
362
+ */
363
+ export function printBuddy(buddy: BuddyData): void {
364
+ const art = getBuddyArt(buddy);
365
+
366
+ const moodEmoji = buddy.mood === "happy"
367
+ ? theme.success("♥")
368
+ : buddy.mood === "thinking"
369
+ ? theme.warning("…")
370
+ : theme.muted("z");
371
+
372
+ // Print each art line in accent color
373
+ for (const line of art) {
374
+ console.log(` ${theme.accentDim(line)}`);
375
+ }
376
+
377
+ // Name + mood + rarity on the line after art
378
+ const shinyTag = buddy.shiny ? " ✨" : "";
379
+ const rarityTag = buddy.rarity !== "common" ? ` [${buddy.rarity.toUpperCase()}]` : "";
380
+ console.log(
381
+ ` ${theme.accent(buddy.name)} ${moodEmoji} ${theme.tertiary(`(${buddy.species}${rarityTag})${shinyTag}`)} ${theme.tertiary(`Lv.${buddy.level}`)}`
382
+ );
383
+ console.log("");
384
+ }
385
+
386
+ // ---------------------------------------------------------------------------
387
+ // Mood tracking
388
+ // ---------------------------------------------------------------------------
389
+
390
+ /**
391
+ * Simple in-memory mood tracker. Mutates the buddy object directly.
392
+ * Call `saveBuddy()` when the session ends to persist final state.
393
+ */
394
+ let consecutiveSuccesses = 0;
395
+ let totalToolCallsThisSession = 0;
396
+
397
+ export function recordToolCallSuccess(buddy: BuddyData): void {
398
+ buddy.toolCalls++;
399
+ buddy.mood = "happy";
400
+ consecutiveSuccesses++;
401
+ totalToolCallsThisSession++;
402
+ }
403
+
404
+ export function recordThinking(buddy: BuddyData): void {
405
+ buddy.mood = "thinking";
406
+ }
407
+
408
+ export function recordError(buddy: BuddyData): void {
409
+ buddy.mood = "sleepy";
410
+ consecutiveSuccesses = 0;
411
+ }
412
+
413
+ export function recordIdle(buddy: BuddyData): void {
414
+ buddy.mood = "sleepy";
415
+ }
416
+
417
+ // ---------------------------------------------------------------------------
418
+ // Speech bubbles — small reactions to major events
419
+ // ---------------------------------------------------------------------------
420
+
421
+ type BuddyEvent = "first_tool" | "success" | "error" | "streak" | "compact" | "exit" | "mode_switch";
422
+
423
+ const REACTIONS: Record<BuddyEvent, string[]> = {
424
+ first_tool: ["Let's go!", "Here we go!", "Time to code!", "On it!"],
425
+ success: ["Nice!", "Got it!", "Done!", "Easy!"],
426
+ error: ["Oops...", "Hmm...", "Let me think...", "We'll fix it!"],
427
+ streak: ["On fire!", "Unstoppable!", "Crushing it!", "Flow state!"],
428
+ compact: ["Getting cozy...", "Tidying up!", "Making room!", "Spring cleaning!"],
429
+ exit: ["See you!", "Bye for now!", "Until next time!", "Sweet dreams!"],
430
+ mode_switch: ["Switching gears!", "New vibes!", "Mode changed!", "Let's try this!"],
431
+ };
432
+
433
+ /**
434
+ * Get a buddy reaction for an event. Returns a formatted speech bubble.
435
+ */
436
+ export function getBuddyReaction(buddy: BuddyData, event: BuddyEvent): string {
437
+ // Streak detection
438
+ if (event === "success" && consecutiveSuccesses >= 5) {
439
+ event = "streak";
440
+ }
441
+
442
+ const phrases = REACTIONS[event];
443
+ if (!phrases) return "";
444
+ const phrase = phrases[Math.floor(Math.random() * phrases.length)]!;
445
+
446
+ const moodIcon = buddy.mood === "happy" ? "♥" : buddy.mood === "thinking" ? "…" : "z";
447
+ return theme.accentDim(` ${buddy.name} ${moodIcon} "${phrase}"`);
448
+ }
449
+
450
+ /**
451
+ * Check if this is the first tool call of the session.
452
+ */
453
+ export function isFirstToolCall(): boolean {
454
+ return totalToolCallsThisSession === 0; // Called in onToolStart, before recordToolCallSuccess increments
455
+ }
456
+
457
+ /**
458
+ * Bump session count. Don't reset mood — let it carry from last session.
459
+ */
460
+ export async function startSession(buddy: BuddyData): Promise<void> {
461
+ buddy.totalSessions++;
462
+ buddy.level = Math.floor(buddy.totalSessions / 5) + 1;
463
+ consecutiveSuccesses = 0;
464
+ totalToolCallsThisSession = 0;
465
+ await saveBuddy(buddy);
466
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Context window usage visualization with themed colors.
3
+ */
4
+
5
+ import { theme, styleTokens } from "./theme.ts";
6
+ import { estimateTokens, getProviderContextLimit } from "../agent/context.ts";
7
+ import type { Message } from "../providers/types.ts";
8
+
9
+ const BAR_WIDTH = 24;
10
+
11
+ /**
12
+ * Render the context usage bar with color-coded progress.
13
+ */
14
+ export function renderContextBar(
15
+ messages: Message[],
16
+ providerName: string,
17
+ systemPromptTokens: number = 0
18
+ ): string {
19
+ const limit = getProviderContextLimit(providerName);
20
+ const used = estimateTokens(messages) + systemPromptTokens;
21
+ const percentage = Math.min(100, Math.round((used / limit) * 100));
22
+
23
+ if (percentage < 1 && messages.length < 3) return ""; // Don't show on first message
24
+
25
+ const filled = Math.round((percentage / 100) * BAR_WIDTH);
26
+ const empty = BAR_WIDTH - filled;
27
+
28
+ // Color based on usage level
29
+ let barColor: (s: string) => string;
30
+ let label: string;
31
+ if (percentage < 25) {
32
+ barColor = theme.success;
33
+ label = theme.secondary(`${percentage}%`);
34
+ } else if (percentage < 50) {
35
+ barColor = theme.success;
36
+ label = theme.secondary(`${percentage}%`);
37
+ } else if (percentage < 75) {
38
+ barColor = theme.warning;
39
+ label = theme.warning(`${percentage}%`);
40
+ } else {
41
+ barColor = theme.error;
42
+ label = theme.error(`${percentage}%`);
43
+ }
44
+
45
+ const filledBar = barColor("█".repeat(filled));
46
+ const emptyBar = theme.muted("░".repeat(empty));
47
+
48
+ return (
49
+ theme.tertiary(" ctx ") +
50
+ theme.muted("[") +
51
+ filledBar +
52
+ emptyBar +
53
+ theme.muted("] ") +
54
+ label +
55
+ theme.muted(" · ") +
56
+ styleTokens(used) +
57
+ theme.muted(" / ") +
58
+ styleTokens(limit)
59
+ );
60
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Effort levels — adjust model behavior and token limits.
3
+ *
4
+ * /effort low → Fast, cheap, less thorough (fewer iterations, shorter responses)
5
+ * /effort normal → Default balanced behavior
6
+ * /effort high → Maximum thoroughness (more iterations, detailed responses)
7
+ */
8
+
9
+ export type EffortLevel = "low" | "normal" | "high";
10
+
11
+ let currentEffort: EffortLevel = "normal";
12
+
13
+ export function getEffort(): EffortLevel {
14
+ return currentEffort;
15
+ }
16
+
17
+ export function setEffort(level: EffortLevel): void {
18
+ currentEffort = level;
19
+ }
20
+
21
+ export function cycleEffort(): EffortLevel {
22
+ const levels: EffortLevel[] = ["low", "normal", "high"];
23
+ const idx = levels.indexOf(currentEffort);
24
+ currentEffort = levels[(idx + 1) % levels.length]!;
25
+ return currentEffort;
26
+ }
27
+
28
+ /**
29
+ * Get agent config overrides for the current effort level.
30
+ */
31
+ export function getEffortConfig(): {
32
+ maxIterations: number;
33
+ maxTokens: number;
34
+ systemPromptSuffix: string;
35
+ } {
36
+ switch (currentEffort) {
37
+ case "low":
38
+ return {
39
+ maxIterations: 10,
40
+ maxTokens: 4096,
41
+ systemPromptSuffix: "\n\nIMPORTANT: Be extremely concise. Give the shortest correct answer. Minimize tool calls. Prefer speed over thoroughness.",
42
+ };
43
+ case "high":
44
+ return {
45
+ maxIterations: 50,
46
+ maxTokens: 16384,
47
+ systemPromptSuffix: "\n\nIMPORTANT: Be extremely thorough. Explore all edge cases. Use multiple tools to verify. Explain your reasoning in detail. Quality over speed.",
48
+ };
49
+ case "normal":
50
+ default:
51
+ return {
52
+ maxIterations: 25,
53
+ maxTokens: 8192,
54
+ systemPromptSuffix: "",
55
+ };
56
+ }
57
+ }
58
+
59
+ export function getEffortEmoji(): string {
60
+ switch (currentEffort) {
61
+ case "low": return "⚡";
62
+ case "high": return "🔬";
63
+ default: return "⚖️";
64
+ }
65
+ }