@ramarivera/coding-buddy 0.4.0-alpha.9 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +45 -0
- package/.claude-plugin/plugin.json +30 -0
- package/LICENSE +1 -1
- package/README.md +96 -45
- package/adapters/omp/commands.ts +201 -0
- package/adapters/omp/context.ts +22 -0
- package/adapters/omp/events.ts +551 -0
- package/adapters/omp/identity.ts +10 -0
- package/adapters/omp/index.ts +39 -0
- package/adapters/omp/logger.ts +52 -0
- package/adapters/omp/prompt.ts +60 -0
- package/adapters/omp/renderers.ts +73 -0
- package/adapters/omp/storage.ts +25 -0
- package/adapters/omp/ui.ts +85 -0
- package/adapters/pi/commands.ts +64 -37
- package/adapters/pi/events.ts +147 -72
- package/adapters/pi/index.ts +9 -4
- package/adapters/pi/storage.ts +14 -285
- package/adapters/pi/ui.ts +56 -5
- package/adapters/shared/file-storage.ts +526 -0
- package/{adapters/claude/install → cli}/backup.ts +130 -33
- package/cli/buddy-shell.ts +1 -1
- package/{adapters/claude/install → cli}/disable.ts +19 -32
- package/cli/doctor.ts +231 -0
- package/{adapters/claude/install → cli}/hunt.ts +7 -9
- package/cli/index.ts +30 -26
- package/{adapters/claude/install → cli}/install.ts +65 -83
- package/{adapters/claude/install → cli}/pick.ts +136 -57
- package/{adapters/claude/install → cli}/settings.ts +2 -2
- package/{adapters/claude/install → cli}/show.ts +4 -4
- package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
- package/{adapters/claude/install → cli}/test-statusline.ts +21 -11
- package/cli/tui.tsx +106 -17
- package/{adapters/claude/install → cli}/uninstall.ts +40 -47
- package/cli/upgrade.ts +46 -13
- package/cli/validate-species.ts +209 -0
- package/cli/verify.ts +3 -3
- package/core/achievements.ts +12 -20
- package/core/art-data.ts +26 -0
- package/core/command-service.ts +99 -97
- package/core/engine.ts +83 -1
- package/core/ports.ts +10 -0
- package/core/reactions.ts +533 -95
- package/core/slot-slug.ts +22 -0
- package/{adapters/claude/hooks → hooks}/buddy-comment.sh +22 -9
- package/hooks/file-type-react.sh +403 -0
- package/hooks/hooks.json +52 -0
- package/hooks/mood-react.sh +210 -0
- package/{adapters/claude/hooks → hooks}/name-react.sh +3 -4
- package/hooks/react.sh +1364 -0
- package/hooks/suggest.sh +55 -0
- package/package.json +70 -31
- package/scripts/gen-emoji-widths.ts +34 -0
- package/scripts/paths.sh +33 -2
- package/scripts/postinstall.cjs +42 -0
- package/scripts/snapshot-statusline.sh +54 -0
- package/server/achievements.ts +1419 -15
- package/server/art.ts +450 -1
- package/server/award-xp.ts +48 -0
- package/server/check-suggestions.ts +102 -0
- package/server/consolidate.ts +33 -0
- package/{adapters/claude/server → server}/index.ts +556 -95
- package/server/mcp-launcher.sh +16 -6
- package/server/memory.ts +404 -0
- package/server/mood.ts +254 -0
- package/server/path.ts +70 -18
- package/server/shift-mood.ts +43 -0
- package/server/state.ts +552 -3
- package/server/suggestions.ts +335 -0
- package/server/theme.ts +53 -0
- package/server/xp.ts +435 -0
- package/{adapters/claude/skills → skills}/buddy/SKILL.md +36 -7
- package/statusline/buddy-status.sh +402 -0
- package/statusline/combined-status.sh +135 -0
- package/statusline/emoji-widths.data +4 -0
- package/adapters/claude/hooks/hooks.json +0 -35
- package/adapters/claude/hooks/react.sh +0 -207
- package/adapters/claude/install/doctor.ts +0 -172
- package/adapters/claude/plugin/marketplace.json +0 -40
- package/adapters/claude/plugin/plugin.json +0 -26
- package/adapters/claude/rendering/art.ts +0 -264
- package/adapters/claude/server/instructions.ts +0 -24
- package/adapters/claude/server/path.ts +0 -12
- package/adapters/claude/server/resources.ts +0 -38
- package/adapters/claude/statusline/buddy-status.sh +0 -408
- package/adapters/claude/storage/achievements.ts +0 -253
- package/adapters/claude/storage/identity.ts +0 -13
- package/adapters/claude/storage/paths.ts +0 -33
- package/adapters/claude/storage/settings.ts +0 -91
- package/adapters/claude/storage/state.ts +0 -365
- package/adapters/pi/README.md +0 -83
- package/adapters/pi/tools.ts +0 -6
- package/server/engine.ts +0 -1
- package/server/reactions.ts +0 -1
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buddy pair-programming — proactive suggestions based on pattern detection.
|
|
3
|
+
*
|
|
4
|
+
* Pattern types:
|
|
5
|
+
* repeated_error — same error 3+ times
|
|
6
|
+
* escalated_large_diff — diff > 150 lines
|
|
7
|
+
* new_file_no_test — new file without test
|
|
8
|
+
* sequence_rename — multiple renames in sequence
|
|
9
|
+
* no_tests_long_session — many files changed, no tests
|
|
10
|
+
* long_function — function > 50 lines in diff
|
|
11
|
+
* todo_comment — TODO/FIXME in diff
|
|
12
|
+
*
|
|
13
|
+
* History is short-term (20 turns), stored in suggestions.json.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
import { buddyStateDir } from "./path";
|
|
19
|
+
|
|
20
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export type PatternType =
|
|
23
|
+
| "repeated_error"
|
|
24
|
+
| "escalated_large_diff"
|
|
25
|
+
| "new_file_no_test"
|
|
26
|
+
| "sequence_rename"
|
|
27
|
+
| "no_tests_long_session"
|
|
28
|
+
| "long_function"
|
|
29
|
+
| "todo_comment";
|
|
30
|
+
|
|
31
|
+
export interface Suggestion {
|
|
32
|
+
id: string;
|
|
33
|
+
pattern: PatternType;
|
|
34
|
+
trigger: string;
|
|
35
|
+
message: string;
|
|
36
|
+
severity: "info" | "warn" | "praise";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SuggestionHistory {
|
|
40
|
+
recentErrors: string[]; // last 20 error signatures
|
|
41
|
+
recentDiffs: number[]; // last 20 diff sizes
|
|
42
|
+
recentRenames: number; // rename count in recent turns
|
|
43
|
+
turnsSinceTest: number; // turns since a test file was mentioned
|
|
44
|
+
turnCount: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Suggestion templates ─────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
export const SUGGESTION_TEMPLATES: Record<PatternType, Suggestion[]> = {
|
|
50
|
+
repeated_error: [
|
|
51
|
+
{
|
|
52
|
+
id: "rep_err_1",
|
|
53
|
+
pattern: "repeated_error",
|
|
54
|
+
trigger: "same error 3x",
|
|
55
|
+
message: "That error keeps showing up. Time to fix it properly?",
|
|
56
|
+
severity: "warn",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "rep_err_2",
|
|
60
|
+
pattern: "repeated_error",
|
|
61
|
+
trigger: "same error 5x",
|
|
62
|
+
message: "*counting on tentacles* that error is now a roommate. Time to evict it.",
|
|
63
|
+
severity: "warn",
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
escalated_large_diff: [
|
|
67
|
+
{
|
|
68
|
+
id: "lrg_diff_1",
|
|
69
|
+
pattern: "escalated_large_diff",
|
|
70
|
+
trigger: "diff > 150 lines",
|
|
71
|
+
message: "*nervous laughter* that's a big one. Maybe split it up?",
|
|
72
|
+
severity: "info",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "lrg_diff_2",
|
|
76
|
+
pattern: "escalated_large_diff",
|
|
77
|
+
trigger: "diff > 300 lines",
|
|
78
|
+
message: "That's not a diff, that's a whole new file. Consider a branch.",
|
|
79
|
+
severity: "warn",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
new_file_no_test: [
|
|
83
|
+
{
|
|
84
|
+
id: "no_test_1",
|
|
85
|
+
pattern: "new_file_no_test",
|
|
86
|
+
trigger: "new source file without a test",
|
|
87
|
+
message: "New file, no test? I see a bug in your future.",
|
|
88
|
+
severity: "info",
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
sequence_rename: [
|
|
92
|
+
{
|
|
93
|
+
id: "rename_1",
|
|
94
|
+
pattern: "sequence_rename",
|
|
95
|
+
trigger: "3+ renames in sequence",
|
|
96
|
+
message: "Renaming things is good, but update all the references too.",
|
|
97
|
+
severity: "warn",
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
no_tests_long_session: [
|
|
101
|
+
{
|
|
102
|
+
id: "no_test_session_1",
|
|
103
|
+
pattern: "no_tests_long_session",
|
|
104
|
+
trigger: "10+ files changed, 0 tests",
|
|
105
|
+
message: "You've been busy. But where are the tests?",
|
|
106
|
+
severity: "info",
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
long_function: [
|
|
110
|
+
{
|
|
111
|
+
id: "long_fn_1",
|
|
112
|
+
pattern: "long_function",
|
|
113
|
+
trigger: "function > 50 lines",
|
|
114
|
+
message: "That's one long function. Consider breaking it up.",
|
|
115
|
+
severity: "warn",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "long_fn_2",
|
|
119
|
+
pattern: "long_function",
|
|
120
|
+
trigger: "function > 100 lines",
|
|
121
|
+
message: "*squints* is that function or a novel? Break it down.",
|
|
122
|
+
severity: "warn",
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
todo_comment: [
|
|
126
|
+
{
|
|
127
|
+
id: "todo_1",
|
|
128
|
+
pattern: "todo_comment",
|
|
129
|
+
trigger: "TODO in diff",
|
|
130
|
+
message: "TODO noted. Don't forget to actually do it.",
|
|
131
|
+
severity: "info",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "fixme_1",
|
|
135
|
+
pattern: "todo_comment",
|
|
136
|
+
trigger: "FIXME in diff",
|
|
137
|
+
message: "FIXME seen. The clock is ticking.",
|
|
138
|
+
severity: "warn",
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// ─── Suggestion history persistence ────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
const SUGGESTIONS_FILE = join(buddyStateDir(), "suggestions.json");
|
|
146
|
+
const LAST_SUGGESTION_FILE = join(buddyStateDir(), ".last_suggestion");
|
|
147
|
+
|
|
148
|
+
function loadHistory(): SuggestionHistory {
|
|
149
|
+
try {
|
|
150
|
+
return JSON.parse(readFileSync(SUGGESTIONS_FILE, "utf8")) as SuggestionHistory;
|
|
151
|
+
} catch {
|
|
152
|
+
return {
|
|
153
|
+
recentErrors: [],
|
|
154
|
+
recentDiffs: [],
|
|
155
|
+
recentRenames: 0,
|
|
156
|
+
turnsSinceTest: 0,
|
|
157
|
+
turnCount: 0,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function saveHistory(history: SuggestionHistory): void {
|
|
163
|
+
mkdirSync(buddyStateDir(), { recursive: true });
|
|
164
|
+
writeFileSync(SUGGESTIONS_FILE, JSON.stringify(history, null, 2));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─── Core functions ────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Simple hash for error messages
|
|
171
|
+
*/
|
|
172
|
+
function simpleHash(s: string): string {
|
|
173
|
+
let h = 0;
|
|
174
|
+
for (let i = 0; i < s.length; i++) {
|
|
175
|
+
const char = s.charCodeAt(i);
|
|
176
|
+
h = ((h << 5) - h) + char;
|
|
177
|
+
h = h & h;
|
|
178
|
+
}
|
|
179
|
+
return Math.abs(h).toString(16);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Record an error and check for repeated errors.
|
|
184
|
+
* Returns a Suggestion if the error is repeated 3+ times.
|
|
185
|
+
*/
|
|
186
|
+
export function checkRepeatedError(errorMsg: string): Suggestion | null {
|
|
187
|
+
const history = loadHistory();
|
|
188
|
+
const sig = simpleHash(errorMsg.slice(0, 200));
|
|
189
|
+
|
|
190
|
+
// Add to recent errors (keep last 20)
|
|
191
|
+
history.recentErrors = [sig, ...history.recentErrors].slice(0, 20);
|
|
192
|
+
saveHistory(history);
|
|
193
|
+
|
|
194
|
+
// Count occurrences
|
|
195
|
+
const count = history.recentErrors.filter((e) => e === sig).length;
|
|
196
|
+
if (count >= 3) {
|
|
197
|
+
const templates = SUGGESTION_TEMPLATES.repeated_error;
|
|
198
|
+
const t = templates[count >= 5 ? 1 : 0] ?? templates[0];
|
|
199
|
+
return { ...t, trigger: `same error ${count}x` };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Record a diff size and check for large diffs.
|
|
207
|
+
* Returns a Suggestion if diff > 150 lines.
|
|
208
|
+
*/
|
|
209
|
+
export function checkLargeDiff(lineCount: number): Suggestion | null {
|
|
210
|
+
const history = loadHistory();
|
|
211
|
+
history.recentDiffs = [lineCount, ...history.recentDiffs].slice(0, 20);
|
|
212
|
+
history.turnCount += 1;
|
|
213
|
+
saveHistory(history);
|
|
214
|
+
|
|
215
|
+
if (lineCount > 300) {
|
|
216
|
+
return SUGGESTION_TEMPLATES.escalated_large_diff[1];
|
|
217
|
+
}
|
|
218
|
+
if (lineCount > 150) {
|
|
219
|
+
return SUGGESTION_TEMPLATES.escalated_large_diff[0];
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check for TODO/FIXME in diff text.
|
|
226
|
+
* Returns a Suggestion if found.
|
|
227
|
+
*/
|
|
228
|
+
export function checkTodoComment(text: string): Suggestion | null {
|
|
229
|
+
const history = loadHistory();
|
|
230
|
+
history.turnCount += 1;
|
|
231
|
+
saveHistory(history);
|
|
232
|
+
|
|
233
|
+
if (/FIXME/i.test(text)) {
|
|
234
|
+
return SUGGESTION_TEMPLATES.todo_comment[1];
|
|
235
|
+
}
|
|
236
|
+
if (/TODO/i.test(text)) {
|
|
237
|
+
return SUGGESTION_TEMPLATES.todo_comment[0];
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Check for long functions in diff text.
|
|
244
|
+
* Returns a Suggestion if a function > 50 lines is detected.
|
|
245
|
+
*/
|
|
246
|
+
export function checkLongFunction(text: string): Suggestion | null {
|
|
247
|
+
const history = loadHistory();
|
|
248
|
+
history.turnCount += 1;
|
|
249
|
+
saveHistory(history);
|
|
250
|
+
|
|
251
|
+
// Simple heuristic: look for function-like patterns with many lines
|
|
252
|
+
const funcPatterns = [
|
|
253
|
+
/function\s+\w+[^{]*\{[\s\S]*?\n(?: {2,}.*\n){50,}/g, // named function, 50+ lines
|
|
254
|
+
/const\s+\w+\s*=\s*(?:async\s*)?\([^)]*\)\s*(?::\s*\w+)?\s*=>\s*\{[\s\S]*?\n(?: {2,}.*\n){50,}/g, // arrow function
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
for (const pattern of funcPatterns) {
|
|
258
|
+
if (pattern.test(text)) {
|
|
259
|
+
return SUGGESTION_TEMPLATES.long_function[0];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Check for especially long functions (> 100 lines)
|
|
264
|
+
const longFuncPattern = /\{[\s\S]{5000,}\}/;
|
|
265
|
+
if (longFuncPattern.test(text)) {
|
|
266
|
+
return SUGGESTION_TEMPLATES.long_function[1];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Record that a turn happened (for session tracking).
|
|
274
|
+
* This should be called on every Stop hook.
|
|
275
|
+
*/
|
|
276
|
+
export function recordTurn(): void {
|
|
277
|
+
const history = loadHistory();
|
|
278
|
+
history.turnCount += 1;
|
|
279
|
+
history.turnsSinceTest += 1;
|
|
280
|
+
saveHistory(history);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Record that tests were mentioned (resets the no-test counter).
|
|
285
|
+
*/
|
|
286
|
+
export function recordTestActivity(): void {
|
|
287
|
+
const history = loadHistory();
|
|
288
|
+
history.turnsSinceTest = 0;
|
|
289
|
+
saveHistory(history);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Check for "no tests in long session" pattern.
|
|
294
|
+
* Returns a Suggestion if 10+ turns with 0 tests.
|
|
295
|
+
*/
|
|
296
|
+
export function checkNoTestsLongSession(): Suggestion | null {
|
|
297
|
+
const history = loadHistory();
|
|
298
|
+
if (history.turnsSinceTest >= 10) {
|
|
299
|
+
return SUGGESTION_TEMPLATES.no_tests_long_session[0];
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ─── Cooldown ────────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Check if suggestion is on cooldown.
|
|
308
|
+
* Returns true if within cooldown period.
|
|
309
|
+
*/
|
|
310
|
+
export function isOnCooldown(cooldownSeconds: number = 180): boolean {
|
|
311
|
+
try {
|
|
312
|
+
if (!existsSync(LAST_SUGGESTION_FILE)) return false;
|
|
313
|
+
const last = parseInt(readFileSync(LAST_SUGGESTION_FILE, "utf8").trim(), 10);
|
|
314
|
+
const elapsed = (Date.now() / 1000) - last;
|
|
315
|
+
return elapsed < cooldownSeconds;
|
|
316
|
+
} catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Record that a suggestion was made (starts cooldown).
|
|
323
|
+
*/
|
|
324
|
+
export function recordSuggestion(): void {
|
|
325
|
+
mkdirSync(buddyStateDir(), { recursive: true });
|
|
326
|
+
writeFileSync(LAST_SUGGESTION_FILE, String(Math.floor(Date.now() / 1000)));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Pick a suggestion message for a pattern.
|
|
331
|
+
*/
|
|
332
|
+
export function pickSuggestion(pattern: PatternType): Suggestion {
|
|
333
|
+
const templates = SUGGESTION_TEMPLATES[pattern];
|
|
334
|
+
return templates[Math.floor(Math.random() * templates.length)] ?? templates[0];
|
|
335
|
+
}
|
package/server/theme.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme support — dark and light terminal color palettes.
|
|
3
|
+
*
|
|
4
|
+
* Both art.ts (TypeScript) and statusline/buddy-status.sh (bash) define
|
|
5
|
+
* color tables. These MUST stay in sync manually — there is no build step
|
|
6
|
+
* for the bash scripts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Rarity } from "../core/engine.ts"
|
|
10
|
+
import { loadConfig } from "./state";
|
|
11
|
+
|
|
12
|
+
export type Theme = "dark" | "light";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Rarity color tables per theme.
|
|
16
|
+
*
|
|
17
|
+
* Dark theme (default): bright, saturated colors — visible on dark backgrounds.
|
|
18
|
+
* Light theme: dark, high-contrast colors — visible on light backgrounds.
|
|
19
|
+
*/
|
|
20
|
+
export const THEME_COLORS: Record<Theme, Record<Rarity, string>> = {
|
|
21
|
+
dark: {
|
|
22
|
+
common: "\x1b[38;2;153;153;153m", // gray rgb(153,153,153)
|
|
23
|
+
uncommon: "\x1b[38;2;78;186;101m", // green rgb(78,186,101)
|
|
24
|
+
rare: "\x1b[38;2;177;185;249m", // blue rgb(177,185,249)
|
|
25
|
+
epic: "\x1b[38;2;175;135;255m", // purple rgb(175,135,255)
|
|
26
|
+
legendary: "\x1b[38;2;255;193;7m", // gold rgb(255,193,7)
|
|
27
|
+
},
|
|
28
|
+
light: {
|
|
29
|
+
common: "\x1b[38;2;90;90;90m", // dark gray rgb(90,90,90)
|
|
30
|
+
uncommon: "\x1b[38;2;22;115;55m", // dark green rgb(22,115,55)
|
|
31
|
+
rare: "\x1b[38;2;55;85;210m", // dark blue rgb(55,85,210)
|
|
32
|
+
epic: "\x1b[38;2;110;55;200m", // dark purple rgb(110,55,200)
|
|
33
|
+
legendary: "\x1b[38;2;180;120;0m", // dark gold rgb(180,120,0)
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Return the current theme based on user config.
|
|
39
|
+
* "auto" falls back to "dark" (terminal auto-detection is unreliable in MCP context).
|
|
40
|
+
*/
|
|
41
|
+
export function getTheme(): Theme {
|
|
42
|
+
const cfg = loadConfig();
|
|
43
|
+
if (cfg.theme === "light") return "light";
|
|
44
|
+
// "auto" or undefined → dark as safe default
|
|
45
|
+
return "dark";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the rarity color string for the active theme.
|
|
50
|
+
*/
|
|
51
|
+
export function getRarityColor(rarity: Rarity): string {
|
|
52
|
+
return THEME_COLORS[getTheme()][rarity];
|
|
53
|
+
}
|