@ramarivera/coding-buddy 0.4.0-alpha.9 → 0.6.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 (92) hide show
  1. package/{adapters/claude/plugin → .claude-plugin}/marketplace.json +11 -6
  2. package/{adapters/claude/plugin → .claude-plugin}/plugin.json +7 -3
  3. package/LICENSE +1 -1
  4. package/README.md +88 -37
  5. package/adapters/omp/commands.ts +201 -0
  6. package/adapters/omp/context.ts +22 -0
  7. package/adapters/omp/events.ts +551 -0
  8. package/adapters/omp/identity.ts +10 -0
  9. package/adapters/omp/index.ts +39 -0
  10. package/adapters/omp/logger.ts +52 -0
  11. package/adapters/omp/prompt.ts +60 -0
  12. package/adapters/omp/renderers.ts +73 -0
  13. package/adapters/omp/storage.ts +25 -0
  14. package/adapters/omp/ui.ts +85 -0
  15. package/adapters/pi/commands.ts +64 -37
  16. package/adapters/pi/events.ts +147 -72
  17. package/adapters/pi/index.ts +9 -4
  18. package/adapters/pi/storage.ts +14 -285
  19. package/adapters/pi/ui.ts +56 -5
  20. package/adapters/shared/file-storage.ts +526 -0
  21. package/{adapters/claude/install → cli}/backup.ts +130 -33
  22. package/cli/buddy-shell.ts +1 -1
  23. package/{adapters/claude/install → cli}/disable.ts +19 -32
  24. package/cli/doctor.ts +231 -0
  25. package/{adapters/claude/install → cli}/hunt.ts +7 -9
  26. package/cli/index.ts +30 -26
  27. package/{adapters/claude/install → cli}/install.ts +65 -83
  28. package/{adapters/claude/install → cli}/pick.ts +111 -50
  29. package/{adapters/claude/install → cli}/settings.ts +2 -2
  30. package/{adapters/claude/install → cli}/show.ts +4 -4
  31. package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
  32. package/{adapters/claude/install → cli}/test-statusline.ts +21 -11
  33. package/cli/tui.tsx +106 -17
  34. package/{adapters/claude/install → cli}/uninstall.ts +40 -47
  35. package/cli/upgrade.ts +2 -2
  36. package/cli/validate-species.ts +209 -0
  37. package/cli/verify.ts +3 -3
  38. package/core/achievements.ts +12 -20
  39. package/core/art-data.ts +26 -0
  40. package/core/command-service.ts +99 -97
  41. package/core/engine.ts +83 -1
  42. package/core/ports.ts +10 -0
  43. package/core/reactions.ts +533 -95
  44. package/core/slot-slug.ts +22 -0
  45. package/{adapters/claude/hooks → hooks}/buddy-comment.sh +19 -1
  46. package/hooks/file-type-react.sh +401 -0
  47. package/hooks/hooks.json +52 -0
  48. package/hooks/mood-react.sh +208 -0
  49. package/{adapters/claude/hooks → hooks}/name-react.sh +1 -1
  50. package/hooks/react.sh +1368 -0
  51. package/hooks/suggest.sh +55 -0
  52. package/package.json +70 -31
  53. package/scripts/gen-emoji-widths.ts +34 -0
  54. package/scripts/paths.sh +17 -1
  55. package/scripts/postinstall.cjs +42 -0
  56. package/scripts/snapshot-statusline.sh +54 -0
  57. package/server/achievements.ts +1419 -15
  58. package/server/art.ts +450 -1
  59. package/server/award-xp.ts +48 -0
  60. package/server/check-suggestions.ts +102 -0
  61. package/server/consolidate.ts +33 -0
  62. package/{adapters/claude/server → server}/index.ts +556 -95
  63. package/server/mcp-launcher.sh +16 -6
  64. package/server/memory.ts +404 -0
  65. package/server/mood.ts +254 -0
  66. package/server/path.ts +70 -18
  67. package/server/shift-mood.ts +43 -0
  68. package/server/state.ts +540 -3
  69. package/server/suggestions.ts +335 -0
  70. package/server/theme.ts +53 -0
  71. package/server/xp.ts +435 -0
  72. package/{adapters/claude/skills → skills}/buddy/SKILL.md +36 -7
  73. package/statusline/buddy-status.sh +397 -0
  74. package/statusline/combined-status.sh +134 -0
  75. package/statusline/emoji-widths.data +4 -0
  76. package/adapters/claude/hooks/hooks.json +0 -35
  77. package/adapters/claude/hooks/react.sh +0 -207
  78. package/adapters/claude/install/doctor.ts +0 -172
  79. package/adapters/claude/rendering/art.ts +0 -264
  80. package/adapters/claude/server/instructions.ts +0 -24
  81. package/adapters/claude/server/path.ts +0 -12
  82. package/adapters/claude/server/resources.ts +0 -38
  83. package/adapters/claude/statusline/buddy-status.sh +0 -408
  84. package/adapters/claude/storage/achievements.ts +0 -253
  85. package/adapters/claude/storage/identity.ts +0 -13
  86. package/adapters/claude/storage/paths.ts +0 -33
  87. package/adapters/claude/storage/settings.ts +0 -91
  88. package/adapters/claude/storage/state.ts +0 -365
  89. package/adapters/pi/README.md +0 -83
  90. package/adapters/pi/tools.ts +0 -6
  91. package/server/engine.ts +0 -1
  92. 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
+ }
@@ -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
+ }