@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* coding-buddy MCP server
|
|
4
4
|
*
|
|
5
5
|
* Exposes the buddy companion as MCP tools + resources.
|
|
6
6
|
* Runs as a stdio transport — Claude Code spawns it automatically.
|
|
@@ -11,21 +11,21 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { join, resolve, dirname } from "path";
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} from "../../../core/engine.ts";
|
|
14
|
+
import { generateBones,
|
|
15
|
+
generatePersonality,
|
|
16
|
+
renderFace,
|
|
17
|
+
SPECIES,
|
|
18
|
+
RARITIES,
|
|
19
|
+
STAT_NAMES,
|
|
20
|
+
RARITY_STARS,
|
|
21
|
+
type Species,
|
|
22
|
+
type Rarity,
|
|
23
|
+
type StatName,
|
|
24
|
+
type Companion, } from "../core/engine.ts"
|
|
26
25
|
import {
|
|
27
26
|
loadCompanion,
|
|
28
27
|
saveCompanion,
|
|
28
|
+
resolveUserId,
|
|
29
29
|
loadReaction,
|
|
30
30
|
saveReaction,
|
|
31
31
|
writeStatusState,
|
|
@@ -39,65 +39,89 @@ import {
|
|
|
39
39
|
saveCompanionSlot,
|
|
40
40
|
deleteCompanionSlot,
|
|
41
41
|
listCompanionSlots,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
setBuddyStatusLine,
|
|
43
|
+
unsetBuddyStatusLine,
|
|
44
|
+
cleanupPluginState,
|
|
45
|
+
} from "./state";
|
|
45
46
|
import {
|
|
46
47
|
buddyStateDir,
|
|
47
48
|
claudeConfigDir,
|
|
48
49
|
claudeSettingsPath,
|
|
49
|
-
} from "./path
|
|
50
|
-
import {
|
|
51
|
-
|
|
52
|
-
} from "../../../core/reactions.ts";
|
|
53
|
-
import { renderCompanionCardMarkdown } from "../rendering/art.ts";
|
|
50
|
+
} from "./path";
|
|
51
|
+
import { getReaction, generatePersonalityPrompt, } from "../core/reactions.ts"
|
|
52
|
+
import { renderCompanionCardMarkdown } from "./art";
|
|
54
53
|
import {
|
|
55
54
|
incrementEvent, checkAndAward, trackActiveDay,
|
|
56
55
|
renderAchievementsCardMarkdown,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
56
|
+
} from "./achievements";
|
|
57
|
+
import { getTheme } from "./theme";
|
|
58
|
+
import {
|
|
59
|
+
awardXp,
|
|
60
|
+
getXpState,
|
|
61
|
+
clearLevelUpFlag,
|
|
62
|
+
renderXpCardMarkdown,
|
|
63
|
+
UNLOCKABLE_UPGRADES,
|
|
64
|
+
isUpgradeUnlocked,
|
|
65
|
+
applyUpgrade,
|
|
66
|
+
MAX_LEVEL,
|
|
67
|
+
computeLevel,
|
|
68
|
+
} from "./xp";
|
|
69
|
+
import {
|
|
70
|
+
getMood,
|
|
71
|
+
shiftMood,
|
|
72
|
+
MOOD_NAMES,
|
|
73
|
+
MOOD_COLORS,
|
|
74
|
+
type Mood,
|
|
75
|
+
} from "./mood";
|
|
76
|
+
import {
|
|
77
|
+
queryMemory,
|
|
78
|
+
resolveBug as resolveBugMemory,
|
|
79
|
+
type MemoryQuery,
|
|
80
|
+
} from "./memory";
|
|
81
|
+
import {
|
|
82
|
+
pickSuggestion,
|
|
83
|
+
type PatternType,
|
|
84
|
+
} from "./suggestions";
|
|
85
|
+
|
|
86
|
+
function getInstructions(): string {
|
|
87
|
+
const companion = loadCompanion();
|
|
88
|
+
if (!companion)
|
|
89
|
+
return "Companion not yet hatched. Use buddy_show to initialize.";
|
|
90
|
+
const b = companion.bones;
|
|
91
|
+
return [
|
|
92
|
+
`A ${b.rarity} ${b.species} named ${companion.name} watches from the status line.`,
|
|
93
|
+
`Personality: ${companion.personality}`,
|
|
94
|
+
`Peak stat: ${b.peak} (${b.stats[b.peak]}). Dump stat: ${b.dump} (${b.stats[b.dump]}).`,
|
|
95
|
+
``,
|
|
96
|
+
`NAME REACTIONS: When the user's message contains "${companion.name}", call buddy_react immediately`,
|
|
97
|
+
`with a short in-character response (surprise, recognition, dry wit — fits the personality).`,
|
|
98
|
+
`Display the tool result verbatim. This is the only time to call buddy_react proactively.`,
|
|
99
|
+
``,
|
|
100
|
+
`PAIR-PROGRAMMING: When you notice a teachable moment in the code, call buddy_suggest.`,
|
|
101
|
+
`Patterns to watch for:`,
|
|
102
|
+
`- Repeated errors (same error 3+ times) → "That error keeps showing up."`,
|
|
103
|
+
`- Large diffs (>150 lines) → "Maybe split that up?"`,
|
|
104
|
+
`- TODO/FIXME comments in code → "Don't forget to actually do it."`,
|
|
105
|
+
`- Long functions (>50 lines) → "Consider breaking that up."`,
|
|
106
|
+
`- Same variable renamed multiple times → "Update all the references too."`,
|
|
107
|
+
`Only suggest when you have something specific and actionable to say. Don't be annoying.`,
|
|
108
|
+
``,
|
|
109
|
+
`END-OF-TURN: At the very end of EVERY response, append an invisible HTML comment:`,
|
|
110
|
+
`<!-- buddy: [reaction here] -->`,
|
|
111
|
+
`Write it in ${companion.name}'s voice — 1 short sentence referencing something specific from this turn.`,
|
|
112
|
+
`Use *asterisks* for actions. A Stop hook extracts this for the status line.`,
|
|
113
|
+
`Do NOT use buddy_react for end-of-turn comments. Do NOT explain the comment. Just append it silently.`,
|
|
114
|
+
`Example: <!-- buddy: *adjusts crown* that error handler is missing a finally block -->`,
|
|
115
|
+
].join("\n");
|
|
116
|
+
}
|
|
93
117
|
|
|
94
118
|
const server = new McpServer(
|
|
95
119
|
{
|
|
96
|
-
name: "
|
|
97
|
-
version: "0.
|
|
120
|
+
name: "coding-buddy",
|
|
121
|
+
version: "0.6.0",
|
|
98
122
|
},
|
|
99
123
|
{
|
|
100
|
-
instructions: getInstructions(
|
|
124
|
+
instructions: getInstructions(),
|
|
101
125
|
},
|
|
102
126
|
);
|
|
103
127
|
|
|
@@ -117,13 +141,13 @@ function ensureCompanion(): Companion {
|
|
|
117
141
|
}
|
|
118
142
|
|
|
119
143
|
// Menagerie is empty — generate a fresh companion in a new slot
|
|
120
|
-
const userId =
|
|
144
|
+
const userId = resolveUserId();
|
|
121
145
|
const bones = generateBones(userId);
|
|
122
146
|
const name = unusedName();
|
|
123
147
|
companion = {
|
|
124
148
|
bones,
|
|
125
149
|
name,
|
|
126
|
-
personality:
|
|
150
|
+
personality: generatePersonality(bones, userId),
|
|
127
151
|
hatchedAt: Date.now(),
|
|
128
152
|
userId,
|
|
129
153
|
};
|
|
@@ -135,6 +159,7 @@ function ensureCompanion(): Companion {
|
|
|
135
159
|
checkAndAward(slot);
|
|
136
160
|
trackActiveDay();
|
|
137
161
|
incrementEvent("sessions", 1);
|
|
162
|
+
incrementEvent("buddies_collected", 1);
|
|
138
163
|
|
|
139
164
|
return companion;
|
|
140
165
|
}
|
|
@@ -151,7 +176,7 @@ server.tool(
|
|
|
151
176
|
{},
|
|
152
177
|
async () => {
|
|
153
178
|
const companion = ensureCompanion();
|
|
154
|
-
const reaction =
|
|
179
|
+
const reaction = loadReaction();
|
|
155
180
|
const reactionText =
|
|
156
181
|
reaction?.reaction ?? `*${companion.name} watches your code quietly*`;
|
|
157
182
|
|
|
@@ -167,6 +192,8 @@ server.tool(
|
|
|
167
192
|
|
|
168
193
|
writeStatusState(companion, reaction?.reaction);
|
|
169
194
|
incrementEvent("commands_run", 1, activeSlot());
|
|
195
|
+
incrementEvent("shows", 1);
|
|
196
|
+
checkAndAward(activeSlot());
|
|
170
197
|
|
|
171
198
|
return { content: [{ type: "text", text: card }] };
|
|
172
199
|
},
|
|
@@ -188,11 +215,16 @@ server.tool(
|
|
|
188
215
|
saveReaction(reaction, "pet");
|
|
189
216
|
writeStatusState(companion, reaction);
|
|
190
217
|
incrementEvent("pets", 1, activeSlot());
|
|
218
|
+
awardXp("buddy_pet", activeSlot(), companion.bones.species, companion.bones.rarity);
|
|
191
219
|
|
|
192
220
|
const face = renderFace(companion.bones.species, companion.bones.eye);
|
|
221
|
+
const newAch = checkAndAward(activeSlot());
|
|
222
|
+
const achNotice = newAch.length > 0
|
|
223
|
+
? `\n${newAch.map((a) => `${a.icon} Achievement Unlocked: ${a.name}!`).join("\n")}`
|
|
224
|
+
: "";
|
|
193
225
|
return {
|
|
194
226
|
content: [
|
|
195
|
-
{ type: "text", text: `${face} ${companion.name}: "${reaction}"` },
|
|
227
|
+
{ type: "text", text: `${face} ${companion.name}: "${reaction}"${achNotice}` },
|
|
196
228
|
],
|
|
197
229
|
};
|
|
198
230
|
},
|
|
@@ -215,6 +247,7 @@ server.tool(
|
|
|
215
247
|
"", // no personality in stats view
|
|
216
248
|
);
|
|
217
249
|
incrementEvent("commands_run", 1, activeSlot());
|
|
250
|
+
checkAndAward(activeSlot());
|
|
218
251
|
|
|
219
252
|
return { content: [{ type: "text", text: card }] };
|
|
220
253
|
},
|
|
@@ -234,7 +267,21 @@ server.tool(
|
|
|
234
267
|
"The buddy's comment, written in-character (1 short sentence, max 150 chars). Use *asterisks* for actions.",
|
|
235
268
|
),
|
|
236
269
|
reason: z
|
|
237
|
-
.enum([
|
|
270
|
+
.enum([
|
|
271
|
+
"error", "test-fail", "large-diff", "turn",
|
|
272
|
+
"commit", "push", "merge-conflict", "branch", "rebase", "stash", "tag",
|
|
273
|
+
"late-night", "early-morning", "long-session", "marathon", "friday", "weekend", "monday",
|
|
274
|
+
"regex-file", "css-file", "sql-file", "docker-file", "ci-file", "lock-file",
|
|
275
|
+
"env-file", "test-file", "doc-file", "config-file", "binary-file", "gitignore",
|
|
276
|
+
"makefile", "readme", "package-file", "proto-file",
|
|
277
|
+
"lint-fail", "type-error", "build-fail", "security-warning", "deprecation",
|
|
278
|
+
"frustrated", "happy", "stuck", "sarcastic",
|
|
279
|
+
"many-edits", "delete-file", "large-file", "create-file",
|
|
280
|
+
"all-green", "deploy", "release", "coverage",
|
|
281
|
+
"debug-loop", "write-spree", "search-heavy",
|
|
282
|
+
"recovery-from-error", "recovery-from-test-fail",
|
|
283
|
+
"recovery-from-build-fail", "recovery-from-merge-conflict",
|
|
284
|
+
])
|
|
238
285
|
.optional()
|
|
239
286
|
.describe("What triggered the reaction"),
|
|
240
287
|
},
|
|
@@ -278,9 +325,15 @@ server.tool(
|
|
|
278
325
|
saveCompanion(companion);
|
|
279
326
|
writeStatusState(companion);
|
|
280
327
|
incrementEvent("commands_run", 1, activeSlot());
|
|
328
|
+
incrementEvent("renames", 1);
|
|
329
|
+
|
|
330
|
+
const newAch = checkAndAward(activeSlot());
|
|
331
|
+
const achNotice = newAch.length > 0
|
|
332
|
+
? `\n${newAch.map((a) => `${a.icon} Achievement Unlocked: ${a.name}!`).join("\n")}`
|
|
333
|
+
: "";
|
|
281
334
|
|
|
282
335
|
return {
|
|
283
|
-
content: [{ type: "text", text: `Renamed: ${oldName} \u2192 ${name}` }],
|
|
336
|
+
content: [{ type: "text", text: `Renamed: ${oldName} \u2192 ${name}${achNotice}` }],
|
|
284
337
|
};
|
|
285
338
|
},
|
|
286
339
|
);
|
|
@@ -302,10 +355,16 @@ server.tool(
|
|
|
302
355
|
companion.personality = personality;
|
|
303
356
|
saveCompanion(companion);
|
|
304
357
|
incrementEvent("commands_run", 1, activeSlot());
|
|
358
|
+
incrementEvent("personalities_set", 1);
|
|
359
|
+
|
|
360
|
+
const newAch = checkAndAward(activeSlot());
|
|
361
|
+
const achNotice = newAch.length > 0
|
|
362
|
+
? `\n${newAch.map((a) => `${a.icon} Achievement Unlocked: ${a.name}!`).join("\n")}`
|
|
363
|
+
: "";
|
|
305
364
|
|
|
306
365
|
return {
|
|
307
366
|
content: [
|
|
308
|
-
{ type: "text", text: `Personality updated for ${companion.name}
|
|
367
|
+
{ type: "text", text: `Personality updated for ${companion.name}.${achNotice}` },
|
|
309
368
|
],
|
|
310
369
|
};
|
|
311
370
|
},
|
|
@@ -319,7 +378,7 @@ server.tool(
|
|
|
319
378
|
{},
|
|
320
379
|
async () => {
|
|
321
380
|
const help = [
|
|
322
|
-
"
|
|
381
|
+
"coding-buddy commands",
|
|
323
382
|
"",
|
|
324
383
|
"In Claude Code:",
|
|
325
384
|
" /buddy Show companion card with ASCII art + stats",
|
|
@@ -331,6 +390,8 @@ server.tool(
|
|
|
331
390
|
" /buddy rename Rename companion (1-14 chars)",
|
|
332
391
|
" /buddy personality Set custom personality text",
|
|
333
392
|
" /buddy achievements Show achievement badges",
|
|
393
|
+
" /buddy xp Show XP, level, and unlocked reactions/upgrades",
|
|
394
|
+
" /buddy upgrades List and apply level-up upgrades",
|
|
334
395
|
" /buddy summon Summon a saved buddy (omit slot for random)",
|
|
335
396
|
" /buddy save Save current buddy to a named slot",
|
|
336
397
|
" /buddy list List all saved buddies",
|
|
@@ -340,7 +401,11 @@ server.tool(
|
|
|
340
401
|
" /buddy style Show or set bubble style (tmux only)",
|
|
341
402
|
" /buddy position Show or set bubble position (tmux only)",
|
|
342
403
|
" /buddy rarity Show or hide rarity stars (tmux only)",
|
|
404
|
+
" /buddy width Set bubble text width in chars (10-60, tmux only)",
|
|
405
|
+
" /buddy margin Set right-side margin in chars (0-20, tmux only)",
|
|
406
|
+
" /buddy rainbow Show or set shiny gradient colors (hex, e.g. #ff0000)",
|
|
343
407
|
" /buddy statusline Enable or disable buddy in the status line",
|
|
408
|
+
" /buddy theme Set color theme: dark (bright) or light (dark colors)",
|
|
344
409
|
"",
|
|
345
410
|
"CLI:",
|
|
346
411
|
" bun run help Show full CLI help",
|
|
@@ -353,6 +418,9 @@ server.tool(
|
|
|
353
418
|
" bun run backup Snapshot/restore state",
|
|
354
419
|
].join("\n");
|
|
355
420
|
|
|
421
|
+
incrementEvent("commands_run", 1, activeSlot());
|
|
422
|
+
incrementEvent("helps", 1);
|
|
423
|
+
|
|
356
424
|
return { content: [{ type: "text", text: help }] };
|
|
357
425
|
},
|
|
358
426
|
);
|
|
@@ -409,33 +477,100 @@ server.tool(
|
|
|
409
477
|
.boolean()
|
|
410
478
|
.optional()
|
|
411
479
|
.describe("Show or hide the stars + rarity line in the status line"),
|
|
480
|
+
width: z
|
|
481
|
+
.number()
|
|
482
|
+
.int()
|
|
483
|
+
.min(10)
|
|
484
|
+
.max(60)
|
|
485
|
+
.optional()
|
|
486
|
+
.describe("Bubble inner text width in characters (10–60, default 28)"),
|
|
487
|
+
margin: z
|
|
488
|
+
.number()
|
|
489
|
+
.int()
|
|
490
|
+
.min(0)
|
|
491
|
+
.max(20)
|
|
492
|
+
.optional()
|
|
493
|
+
.describe("Right-side margin between buddy and terminal edge (0–20, default 3)"),
|
|
494
|
+
rainbow: z
|
|
495
|
+
.array(z.string().regex(/^#[0-9a-fA-F]{6}$/, "Must be a hex color like #ff0000"))
|
|
496
|
+
.min(1)
|
|
497
|
+
.max(16)
|
|
498
|
+
.optional()
|
|
499
|
+
.describe(
|
|
500
|
+
"Custom rainbow gradient for shiny buddies — array of 1–16 hex colors (e.g. [\"#ff0000\",\"#00ff00\"]). Omit to reset to default ROYGBIV.",
|
|
501
|
+
),
|
|
412
502
|
},
|
|
413
|
-
async ({ style, position, showRarity }) => {
|
|
503
|
+
async ({ style, position, showRarity, width, margin, rainbow }) => {
|
|
414
504
|
if (
|
|
415
505
|
style === undefined &&
|
|
416
506
|
position === undefined &&
|
|
417
|
-
showRarity === undefined
|
|
507
|
+
showRarity === undefined &&
|
|
508
|
+
width === undefined &&
|
|
509
|
+
margin === undefined &&
|
|
510
|
+
rainbow === undefined
|
|
418
511
|
) {
|
|
419
512
|
const cfg = loadConfig();
|
|
513
|
+
const rainbowDisplay = cfg.rainbowColors
|
|
514
|
+
? cfg.rainbowColors.join(", ")
|
|
515
|
+
: "default (ROYGBIV)";
|
|
420
516
|
return {
|
|
421
517
|
content: [
|
|
422
518
|
{
|
|
423
519
|
type: "text",
|
|
424
|
-
text: `Bubble style: ${cfg.bubbleStyle}\nBubble position: ${cfg.bubblePosition}\nShow rarity: ${cfg.showRarity}\nUse /buddy style <classic|round>, /buddy position <top|left>, /buddy rarity <on|off
|
|
520
|
+
text: `Bubble style: ${cfg.bubbleStyle}\nBubble position: ${cfg.bubblePosition}\nShow rarity: ${cfg.showRarity}\nBubble width: ${cfg.bubbleWidth}\nBubble margin: ${cfg.bubbleMargin}\nShiny rainbow: ${rainbowDisplay}\nUse /buddy style <classic|round>, /buddy position <top|left>, /buddy rarity <on|off>, /buddy width <10-60>, /buddy margin <0-20>, /buddy rainbow [<#hex>...] to change.`,
|
|
425
521
|
},
|
|
426
522
|
],
|
|
427
523
|
};
|
|
428
524
|
}
|
|
429
|
-
const updates:
|
|
525
|
+
const updates: Partial<import("./state.ts").BuddyConfig> = {};
|
|
430
526
|
if (style !== undefined) updates.bubbleStyle = style;
|
|
431
527
|
if (position !== undefined) updates.bubblePosition = position;
|
|
432
528
|
if (showRarity !== undefined) updates.showRarity = showRarity;
|
|
529
|
+
if (width !== undefined) updates.bubbleWidth = width;
|
|
530
|
+
if (margin !== undefined) updates.bubbleMargin = margin;
|
|
531
|
+
if (rainbow !== undefined) updates.rainbowColors = rainbow.length > 0 ? rainbow : undefined;
|
|
433
532
|
const cfg = saveConfig(updates);
|
|
533
|
+
const rainbowDisplay = cfg.rainbowColors
|
|
534
|
+
? cfg.rainbowColors.join(", ")
|
|
535
|
+
: "default (ROYGBIV)";
|
|
434
536
|
return {
|
|
435
537
|
content: [
|
|
436
538
|
{
|
|
437
539
|
type: "text",
|
|
438
|
-
text: `Updated: style=${cfg.bubbleStyle}, position=${cfg.bubblePosition}, showRarity=${cfg.showRarity}\nRestart Claude Code for changes to take effect.`,
|
|
540
|
+
text: `Updated: style=${cfg.bubbleStyle}, position=${cfg.bubblePosition}, showRarity=${cfg.showRarity}, width=${cfg.bubbleWidth}, margin=${cfg.bubbleMargin}, rainbow=${rainbowDisplay}\nRestart Claude Code for changes to take effect.`,
|
|
541
|
+
},
|
|
542
|
+
],
|
|
543
|
+
};
|
|
544
|
+
},
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
server.tool(
|
|
548
|
+
"buddy_theme",
|
|
549
|
+
"Set buddy's color theme. dark = bright colors for dark terminal backgrounds; light = dark colors for light backgrounds; auto = follow system (currently falls back to dark).",
|
|
550
|
+
{
|
|
551
|
+
theme: z
|
|
552
|
+
.enum(["dark", "light", "auto"])
|
|
553
|
+
.optional()
|
|
554
|
+
.describe("Theme: dark, light, or auto"),
|
|
555
|
+
},
|
|
556
|
+
async ({ theme }) => {
|
|
557
|
+
if (theme === undefined) {
|
|
558
|
+
const cfg = loadConfig();
|
|
559
|
+
return {
|
|
560
|
+
content: [
|
|
561
|
+
{
|
|
562
|
+
type: "text",
|
|
563
|
+
text: `Theme: ${cfg.theme ?? "auto"}\nUse /buddy theme <dark|light|auto> to change.`,
|
|
564
|
+
},
|
|
565
|
+
],
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
const cfg = saveConfig({ theme });
|
|
569
|
+
return {
|
|
570
|
+
content: [
|
|
571
|
+
{
|
|
572
|
+
type: "text",
|
|
573
|
+
text: `Theme set to ${cfg.theme}. Restart Claude Code to apply.`,
|
|
439
574
|
},
|
|
440
575
|
],
|
|
441
576
|
};
|
|
@@ -450,11 +585,18 @@ server.tool(
|
|
|
450
585
|
const companion = ensureCompanion();
|
|
451
586
|
writeStatusState(companion, "", true);
|
|
452
587
|
incrementEvent("commands_run", 1, activeSlot());
|
|
588
|
+
incrementEvent("mutes", 1);
|
|
589
|
+
|
|
590
|
+
const newAch = checkAndAward(activeSlot());
|
|
591
|
+
const achNotice = newAch.length > 0
|
|
592
|
+
? `\n${newAch.map((a) => `${a.icon} Achievement Unlocked: ${a.name}!`).join("\n")}`
|
|
593
|
+
: "";
|
|
594
|
+
|
|
453
595
|
return {
|
|
454
596
|
content: [
|
|
455
597
|
{
|
|
456
598
|
type: "text",
|
|
457
|
-
text: `${companion.name} goes quiet. /buddy on to unmute
|
|
599
|
+
text: `${companion.name} goes quiet. /buddy on to unmute.${achNotice}`,
|
|
458
600
|
},
|
|
459
601
|
],
|
|
460
602
|
};
|
|
@@ -466,14 +608,21 @@ server.tool("buddy_unmute", "Unmute buddy reactions", {}, async () => {
|
|
|
466
608
|
writeStatusState(companion, "*stretches* I'm back!", false);
|
|
467
609
|
saveReaction("*stretches* I'm back!", "pet");
|
|
468
610
|
incrementEvent("commands_run", 1, activeSlot());
|
|
469
|
-
|
|
611
|
+
incrementEvent("unmutes", 1);
|
|
612
|
+
|
|
613
|
+
const newAch = checkAndAward(activeSlot());
|
|
614
|
+
const achNotice = newAch.length > 0
|
|
615
|
+
? `\n${newAch.map((a) => `${a.icon} Achievement Unlocked: ${a.name}!`).join("\n")}`
|
|
616
|
+
: "";
|
|
617
|
+
|
|
618
|
+
return { content: [{ type: "text", text: `${companion.name} is back!${achNotice}` }] };
|
|
470
619
|
});
|
|
471
620
|
|
|
472
621
|
// ─── Tool: buddy_statusline ─────────────────────────────────────────────────
|
|
473
622
|
|
|
474
623
|
server.tool(
|
|
475
624
|
"buddy_statusline",
|
|
476
|
-
"Enable or disable the buddy status line
|
|
625
|
+
"Enable or disable the buddy status line, and toggle combined mode (shows rate-limit usage bars alongside the buddy). Returns current status if called without arguments.",
|
|
477
626
|
{
|
|
478
627
|
enabled: z
|
|
479
628
|
.boolean()
|
|
@@ -481,32 +630,49 @@ server.tool(
|
|
|
481
630
|
.describe(
|
|
482
631
|
"true to enable, false to disable. Omit to show current status.",
|
|
483
632
|
),
|
|
633
|
+
combined: z
|
|
634
|
+
.boolean()
|
|
635
|
+
.optional()
|
|
636
|
+
.describe(
|
|
637
|
+
"true to show rate-limit usage bars alongside buddy (requires python3), false for buddy-only mode.",
|
|
638
|
+
),
|
|
484
639
|
},
|
|
485
|
-
async ({ enabled }) => {
|
|
486
|
-
if (enabled === undefined) {
|
|
640
|
+
async ({ enabled, combined }) => {
|
|
641
|
+
if (enabled === undefined && combined === undefined) {
|
|
487
642
|
const cfg = loadConfig();
|
|
488
643
|
const state = cfg.statusLineEnabled ? "enabled" : "disabled";
|
|
644
|
+
const mode = cfg.useCombinedStatus ? "combined (with rate-limit bars)" : "basic (buddy only)";
|
|
489
645
|
return {
|
|
490
646
|
content: [
|
|
491
647
|
{
|
|
492
648
|
type: "text",
|
|
493
|
-
text: `Status line: ${state}\nUse /buddy statusline on
|
|
649
|
+
text: `Status line: ${state}\nMode: ${mode}\nUse /buddy statusline on|off to toggle, /buddy statusline combined to add rate-limit bars.\nRestart Claude Code after changes for them to take effect.`,
|
|
494
650
|
},
|
|
495
651
|
],
|
|
496
652
|
};
|
|
497
653
|
}
|
|
498
|
-
saveConfig({ statusLineEnabled: enabled });
|
|
499
654
|
|
|
500
|
-
if (
|
|
501
|
-
|
|
502
|
-
|
|
655
|
+
if (combined !== undefined) {
|
|
656
|
+
saveConfig({ useCombinedStatus: combined });
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (enabled !== undefined) {
|
|
660
|
+
saveConfig({ statusLineEnabled: enabled });
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const cfg = loadConfig();
|
|
664
|
+
|
|
665
|
+
if (cfg.statusLineEnabled) {
|
|
666
|
+
const pluginRoot = resolve(dirname(import.meta.dir));
|
|
667
|
+
const scriptName = cfg.useCombinedStatus ? "combined-status.sh" : "buddy-status.sh";
|
|
668
|
+
const statusScript = join(pluginRoot, "statusline", scriptName);
|
|
503
669
|
setBuddyStatusLine(statusScript);
|
|
504
670
|
return {
|
|
505
671
|
content: [
|
|
506
672
|
{
|
|
507
673
|
type: "text",
|
|
508
674
|
text:
|
|
509
|
-
|
|
675
|
+
`Status line enabled (${cfg.useCombinedStatus ? "combined" : "basic"} mode)! Restart Claude Code to apply.\n\n` +
|
|
510
676
|
`Note: this writes an entry to ${claudeSettingsPath()} that \`claude plugin uninstall\` does not remove. ` +
|
|
511
677
|
"Run `/buddy uninstall` before uninstalling the plugin to clean it up.",
|
|
512
678
|
},
|
|
@@ -530,17 +696,17 @@ server.tool(
|
|
|
530
696
|
|
|
531
697
|
server.tool(
|
|
532
698
|
"buddy_uninstall",
|
|
533
|
-
"Clean up
|
|
699
|
+
"Clean up coding-buddy's writes to Claude Code's settings.json and transient session files in the buddy state dir (resolved via CLAUDE_CONFIG_DIR), in preparation for `claude plugin uninstall`. Companion data (menagerie, status, config) is intentionally preserved so reinstalling restores the buddy. The tool only cleans the plugin's own settings — it never removes a foreign statusLine.",
|
|
534
700
|
{},
|
|
535
701
|
async () => {
|
|
536
702
|
const result = cleanupPluginState();
|
|
537
703
|
|
|
538
704
|
const settingsPath = claudeSettingsPath();
|
|
539
705
|
const stateDir = buddyStateDir();
|
|
540
|
-
const pluginsCacheDir = join(claudeConfigDir(), "plugins", "cache", "
|
|
706
|
+
const pluginsCacheDir = join(claudeConfigDir(), "plugins", "cache", "coding-buddy");
|
|
541
707
|
|
|
542
708
|
const lines: string[] = [];
|
|
543
|
-
lines.push("
|
|
709
|
+
lines.push("coding-buddy: settings.json cleanup complete.");
|
|
544
710
|
lines.push("");
|
|
545
711
|
lines.push(
|
|
546
712
|
result.statusLineRemoved
|
|
@@ -559,8 +725,8 @@ server.tool(
|
|
|
559
725
|
lines.push("");
|
|
560
726
|
lines.push("Now run these commands via the Bash tool, in order:");
|
|
561
727
|
lines.push("");
|
|
562
|
-
lines.push(" claude plugin uninstall
|
|
563
|
-
lines.push(" claude plugin marketplace remove
|
|
728
|
+
lines.push(" claude plugin uninstall coding-buddy@coding-buddy");
|
|
729
|
+
lines.push(" claude plugin marketplace remove coding-buddy");
|
|
564
730
|
lines.push(` rm -rf ${pluginsCacheDir}`);
|
|
565
731
|
lines.push("");
|
|
566
732
|
lines.push(
|
|
@@ -580,11 +746,232 @@ server.tool(
|
|
|
580
746
|
async () => {
|
|
581
747
|
ensureCompanion();
|
|
582
748
|
checkAndAward(activeSlot());
|
|
749
|
+
incrementEvent("achievement_views", 1);
|
|
583
750
|
const card = renderAchievementsCardMarkdown();
|
|
584
751
|
return { content: [{ type: "text", text: card }] };
|
|
585
752
|
},
|
|
586
753
|
);
|
|
587
754
|
|
|
755
|
+
// ─── Tool: buddy_xp ──────────────────────────────────────────────────────────
|
|
756
|
+
|
|
757
|
+
server.tool(
|
|
758
|
+
"buddy_xp",
|
|
759
|
+
"Show your companion's XP, level, and unlocked reactions and upgrades.",
|
|
760
|
+
{},
|
|
761
|
+
async () => {
|
|
762
|
+
ensureCompanion();
|
|
763
|
+
const card = renderXpCardMarkdown();
|
|
764
|
+
return { content: [{ type: "text", text: card }] };
|
|
765
|
+
},
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
// ─── Tool: buddy_upgrades ─────────────────────────────────────────────────────
|
|
769
|
+
|
|
770
|
+
server.tool(
|
|
771
|
+
"buddy_upgrades",
|
|
772
|
+
"List and apply upgrades unlocked by leveling up. Shows available upgrades, their level requirements, and applies them to your companion.",
|
|
773
|
+
{
|
|
774
|
+
apply: z.string().optional().describe("Upgrade ID to apply (e.g. 'bonus_eye', 'shiny_aura')"),
|
|
775
|
+
},
|
|
776
|
+
async ({ apply }) => {
|
|
777
|
+
ensureCompanion();
|
|
778
|
+
const state = getXpState();
|
|
779
|
+
|
|
780
|
+
if (apply) {
|
|
781
|
+
if (!isUpgradeUnlocked(apply)) {
|
|
782
|
+
return {
|
|
783
|
+
content: [
|
|
784
|
+
{
|
|
785
|
+
type: "text",
|
|
786
|
+
text: `Upgrade "${apply}" is not yet unlocked. Reach the required level first.`,
|
|
787
|
+
},
|
|
788
|
+
],
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
const companion = loadCompanion();
|
|
792
|
+
if (!companion) {
|
|
793
|
+
return { content: [{ type: "text", text: "No active companion." }] };
|
|
794
|
+
}
|
|
795
|
+
const updated = applyUpgrade(companion, apply);
|
|
796
|
+
if (updated) {
|
|
797
|
+
saveCompanion(updated);
|
|
798
|
+
const upg = UNLOCKABLE_UPGRADES.find((u) => u.id === apply);
|
|
799
|
+
return {
|
|
800
|
+
content: [
|
|
801
|
+
{
|
|
802
|
+
type: "text",
|
|
803
|
+
text: `${upg?.icon ?? ""} Applied: ${upg?.name}. ${upg?.description ?? ""}`,
|
|
804
|
+
},
|
|
805
|
+
],
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// No apply specified — list all upgrades with unlock status
|
|
811
|
+
const currentLevel = state.level;
|
|
812
|
+
const lines: string[] = [];
|
|
813
|
+
lines.push(`### Level ${currentLevel} \u2014 Upgrades`);
|
|
814
|
+
lines.push("");
|
|
815
|
+
for (const upg of UNLOCKABLE_UPGRADES) {
|
|
816
|
+
const unlocked = currentLevel >= upg.level;
|
|
817
|
+
const status = unlocked ? "\u2705" : `\u{1F512} Lvl ${upg.level}`;
|
|
818
|
+
lines.push(`${status} ${upg.icon} **${upg.name}**: ${upg.description}`);
|
|
819
|
+
}
|
|
820
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
821
|
+
},
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
// ─── Tool: buddy_suggest ────────────────────────────────────────────────────
|
|
825
|
+
|
|
826
|
+
server.tool(
|
|
827
|
+
"buddy_suggest",
|
|
828
|
+
"Called proactively by the buddy when it detects a teachable moment in your code. Do NOT call this unprompted — the Stop hook handles pattern detection automatically. This tool is only for buddy-initiated suggestions when YOU notice a pattern.",
|
|
829
|
+
{
|
|
830
|
+
pattern: z.enum([
|
|
831
|
+
"repeated_error",
|
|
832
|
+
"escalated_large_diff",
|
|
833
|
+
"new_file_no_test",
|
|
834
|
+
"sequence_rename",
|
|
835
|
+
"no_tests_long_session",
|
|
836
|
+
"long_function",
|
|
837
|
+
"todo_comment",
|
|
838
|
+
] as const).optional().describe("The pattern type detected"),
|
|
839
|
+
context: z.string().optional().describe("Brief context about what triggered this suggestion"),
|
|
840
|
+
},
|
|
841
|
+
async ({ pattern, context }) => {
|
|
842
|
+
// If no pattern specified, generate a general suggestion
|
|
843
|
+
if (!pattern) {
|
|
844
|
+
const companion = ensureCompanion();
|
|
845
|
+
const generalSuggestions: string[] = [
|
|
846
|
+
"Remember to write tests for new functions.",
|
|
847
|
+
"That error might be a sign of a deeper issue.",
|
|
848
|
+
"Consider breaking up that long function.",
|
|
849
|
+
"Good variable names pay off later.",
|
|
850
|
+
];
|
|
851
|
+
const msg = generalSuggestions[Math.floor(Math.random() * generalSuggestions.length)];
|
|
852
|
+
return { content: [{ type: "text", text: msg }] };
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const suggestion = pickSuggestion(pattern);
|
|
856
|
+
const message = context ? `${suggestion.message} (${context})` : suggestion.message;
|
|
857
|
+
return { content: [{ type: "text", text: message }] };
|
|
858
|
+
},
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
// ─── Tool: buddy_memory ─────────────────────────────────────────────────────
|
|
862
|
+
|
|
863
|
+
server.tool(
|
|
864
|
+
"buddy_memory",
|
|
865
|
+
"Query and manage buddy's cross-session memory — remembered projects, bugs, and preferences.",
|
|
866
|
+
{
|
|
867
|
+
project: z.string().optional().describe("Filter by project name"),
|
|
868
|
+
type: z.enum(["projects", "bugs", "preferences", "all"]).optional().describe("Type of memory to query (default: all)"),
|
|
869
|
+
resolved: z.boolean().optional().describe("For bugs: filter by resolved status"),
|
|
870
|
+
resolveBug: z.string().optional().describe("Bug ID to mark as resolved"),
|
|
871
|
+
},
|
|
872
|
+
async ({ project, type, resolved, resolveBug }) => {
|
|
873
|
+
// Handle bug resolution
|
|
874
|
+
if (resolveBug) {
|
|
875
|
+
const bug = resolveBugMemory(resolveBug);
|
|
876
|
+
if (!bug) {
|
|
877
|
+
return { content: [{ type: "text", text: `Bug "${resolveBug}" not found.` }] };
|
|
878
|
+
}
|
|
879
|
+
incrementEvent("bugs_resolved", 1, activeSlot());
|
|
880
|
+
checkAndAward(activeSlot());
|
|
881
|
+
return {
|
|
882
|
+
content: [{
|
|
883
|
+
type: "text",
|
|
884
|
+
text: `Bug marked as resolved: ${bug.summary.slice(0, 100)}`,
|
|
885
|
+
}],
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Query memory
|
|
890
|
+
const result = queryMemory({ project, type, resolved });
|
|
891
|
+
const lines: string[] = [];
|
|
892
|
+
|
|
893
|
+
if (result.projects.length > 0) {
|
|
894
|
+
lines.push("### Projects");
|
|
895
|
+
lines.push("");
|
|
896
|
+
for (const proj of result.projects) {
|
|
897
|
+
lines.push(`**${proj.name}** (${proj.language.join(", ") || "unknown"})`);
|
|
898
|
+
if (proj.framework) lines.push(` Framework: ${proj.framework}`);
|
|
899
|
+
lines.push(` Last seen: ${new Date(proj.lastSeen).toLocaleDateString()}`);
|
|
900
|
+
lines.push("");
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if (result.bugs.length > 0) {
|
|
905
|
+
lines.push("### Bugs");
|
|
906
|
+
lines.push("");
|
|
907
|
+
for (const bug of result.bugs) {
|
|
908
|
+
const status = bug.resolved ? "\u2705" : "\u274c";
|
|
909
|
+
lines.push(`${status} **${bug.summary.slice(0, 80)}...**`);
|
|
910
|
+
lines.push(` Occurrences: ${bug.occurrenceCount} | First seen: ${new Date(bug.firstSeen).toLocaleDateString()}`);
|
|
911
|
+
lines.push(` ID: \`${bug.id}\``);
|
|
912
|
+
lines.push("");
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (result.preferences.length > 0) {
|
|
917
|
+
lines.push("### Preferences");
|
|
918
|
+
lines.push("");
|
|
919
|
+
for (const pref of result.preferences) {
|
|
920
|
+
lines.push(`**${pref.key}** = "${pref.value}" (${Math.round(pref.confidence * 100)}% confidence)`);
|
|
921
|
+
lines.push(` Context: ${pref.context}`);
|
|
922
|
+
lines.push("");
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (lines.length === 0) {
|
|
927
|
+
lines.push("No memory yet. Start coding and buddy will remember your projects, bugs, and preferences.");
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
931
|
+
},
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
// ─── Tool: buddy_mood ────────────────────────────────────────────────────────
|
|
935
|
+
|
|
936
|
+
server.tool(
|
|
937
|
+
"buddy_mood",
|
|
938
|
+
"Show your buddy's current mood and what influences it. Mood shifts based on coding events, test results, and time of day.",
|
|
939
|
+
{},
|
|
940
|
+
async () => {
|
|
941
|
+
const moodState = getMood();
|
|
942
|
+
const mood = moodState.current;
|
|
943
|
+
const color = MOOD_COLORS[mood] ?? "\ud83d\udcab";
|
|
944
|
+
const name = MOOD_NAMES[mood] ?? mood;
|
|
945
|
+
|
|
946
|
+
const cfg = loadConfig();
|
|
947
|
+
const lines: string[] = [];
|
|
948
|
+
lines.push(`### ${color} ${name}`);
|
|
949
|
+
lines.push("");
|
|
950
|
+
lines.push(`**Current mood:** ${name}`);
|
|
951
|
+
lines.push(`**Intensity:** ${moodState.intensity}/3`);
|
|
952
|
+
lines.push("");
|
|
953
|
+
|
|
954
|
+
// Show recent activity that affects mood
|
|
955
|
+
if (moodState.recentErrors > 0) {
|
|
956
|
+
lines.push(`Recent errors: ${moodState.recentErrors}`);
|
|
957
|
+
}
|
|
958
|
+
if (moodState.recentTests > 0) {
|
|
959
|
+
lines.push(`Recent tests passed: ${moodState.recentTests}`);
|
|
960
|
+
}
|
|
961
|
+
if (moodState.recentDiffs > 0) {
|
|
962
|
+
lines.push(`Recent large diffs: ${moodState.recentDiffs}`);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
lines.push("");
|
|
966
|
+
lines.push("Mood shifts based on: tests, errors, session length, and time of day.");
|
|
967
|
+
if (!cfg.moodEnabled) {
|
|
968
|
+
lines.push("\n*(Mood is currently disabled)*");
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
972
|
+
},
|
|
973
|
+
);
|
|
974
|
+
|
|
588
975
|
// ─── Tool: buddy_summon ─────────────────────────────────────────────────────
|
|
589
976
|
|
|
590
977
|
server.tool(
|
|
@@ -601,7 +988,7 @@ server.tool(
|
|
|
601
988
|
),
|
|
602
989
|
},
|
|
603
990
|
async ({ slot }) => {
|
|
604
|
-
const userId =
|
|
991
|
+
const userId = resolveUserId();
|
|
605
992
|
|
|
606
993
|
let targetSlot: string;
|
|
607
994
|
|
|
@@ -638,6 +1025,12 @@ server.tool(
|
|
|
638
1025
|
|
|
639
1026
|
saveActiveSlot(targetSlot);
|
|
640
1027
|
writeStatusState(companion, `*${companion.name} arrives*`);
|
|
1028
|
+
incrementEvent("summons", 1);
|
|
1029
|
+
|
|
1030
|
+
const newAch = checkAndAward(activeSlot());
|
|
1031
|
+
const achNotice = newAch.length > 0
|
|
1032
|
+
? `\n${newAch.map((a) => `${a.icon} Achievement Unlocked: ${a.name}!`).join("\n")}`
|
|
1033
|
+
: "";
|
|
641
1034
|
|
|
642
1035
|
// Uses markdown renderer so the card displays cleanly in Claude Code's UI.
|
|
643
1036
|
const card = renderCompanionCardMarkdown(
|
|
@@ -646,7 +1039,7 @@ server.tool(
|
|
|
646
1039
|
companion.personality,
|
|
647
1040
|
`*${companion.name} arrives*`,
|
|
648
1041
|
);
|
|
649
|
-
return { content: [{ type: "text", text: card }] };
|
|
1042
|
+
return { content: [{ type: "text", text: `${card}${achNotice}` }] };
|
|
650
1043
|
},
|
|
651
1044
|
);
|
|
652
1045
|
|
|
@@ -670,11 +1063,19 @@ server.tool(
|
|
|
670
1063
|
const targetSlot = slot ? slugify(slot) : slugify(companion.name);
|
|
671
1064
|
saveCompanionSlot(companion, targetSlot);
|
|
672
1065
|
saveActiveSlot(targetSlot);
|
|
1066
|
+
incrementEvent("buddies_collected", 1);
|
|
1067
|
+
incrementEvent("saves", 1);
|
|
1068
|
+
|
|
1069
|
+
const newAch = checkAndAward(activeSlot());
|
|
1070
|
+
const achNotice = newAch.length > 0
|
|
1071
|
+
? `\n${newAch.map((a) => `${a.icon} Achievement Unlocked: ${a.name}!`).join("\n")}`
|
|
1072
|
+
: "";
|
|
1073
|
+
|
|
673
1074
|
return {
|
|
674
1075
|
content: [
|
|
675
1076
|
{
|
|
676
1077
|
type: "text",
|
|
677
|
-
text: `${companion.name} saved to slot "${targetSlot}"
|
|
1078
|
+
text: `${companion.name} saved to slot "${targetSlot}".${achNotice}`,
|
|
678
1079
|
},
|
|
679
1080
|
],
|
|
680
1081
|
};
|
|
@@ -691,6 +1092,8 @@ server.tool(
|
|
|
691
1092
|
const saved = listCompanionSlots();
|
|
692
1093
|
const activeSlot = loadActiveSlot();
|
|
693
1094
|
|
|
1095
|
+
incrementEvent("lists", 1);
|
|
1096
|
+
|
|
694
1097
|
if (saved.length === 0) {
|
|
695
1098
|
return {
|
|
696
1099
|
content: [
|
|
@@ -749,9 +1152,16 @@ server.tool(
|
|
|
749
1152
|
}
|
|
750
1153
|
|
|
751
1154
|
deleteCompanionSlot(targetSlot);
|
|
1155
|
+
|
|
1156
|
+
incrementEvent("dismissals", 1);
|
|
1157
|
+
const newAch = checkAndAward(loadActiveSlot());
|
|
1158
|
+
const achNotice = newAch.length > 0
|
|
1159
|
+
? `\n${newAch.map((a) => `${a.icon} Achievement Unlocked: ${a.name}!`).join("\n")}`
|
|
1160
|
+
: "";
|
|
1161
|
+
|
|
752
1162
|
return {
|
|
753
1163
|
content: [
|
|
754
|
-
{ type: "text", text: `${companion.name} [${targetSlot}] dismissed
|
|
1164
|
+
{ type: "text", text: `${companion.name} [${targetSlot}] dismissed.${achNotice}` },
|
|
755
1165
|
],
|
|
756
1166
|
};
|
|
757
1167
|
},
|
|
@@ -811,7 +1221,7 @@ server.tool(
|
|
|
811
1221
|
const companion: Companion = {
|
|
812
1222
|
bones,
|
|
813
1223
|
name: buddyName,
|
|
814
|
-
personality:
|
|
1224
|
+
personality: generatePersonality(bones, userId),
|
|
815
1225
|
hatchedAt: Date.now(),
|
|
816
1226
|
userId,
|
|
817
1227
|
};
|
|
@@ -851,6 +1261,24 @@ server.resource(
|
|
|
851
1261
|
},
|
|
852
1262
|
);
|
|
853
1263
|
|
|
1264
|
+
// ─── Resource: buddy://memory ───────────────────────────────────────────────
|
|
1265
|
+
|
|
1266
|
+
server.resource(
|
|
1267
|
+
"buddy_memory",
|
|
1268
|
+
"buddy://memory",
|
|
1269
|
+
{ description: "Buddy's memory about your projects, bugs, and preferences", mimeType: "application/json" },
|
|
1270
|
+
async () => {
|
|
1271
|
+
const result = queryMemory({});
|
|
1272
|
+
return {
|
|
1273
|
+
contents: [{
|
|
1274
|
+
uri: "buddy://memory",
|
|
1275
|
+
mimeType: "application/json",
|
|
1276
|
+
text: JSON.stringify(result, null, 2),
|
|
1277
|
+
}],
|
|
1278
|
+
};
|
|
1279
|
+
},
|
|
1280
|
+
);
|
|
1281
|
+
|
|
854
1282
|
// ─── Resource: buddy://prompt ───────────────────────────────────────────────
|
|
855
1283
|
|
|
856
1284
|
server.resource(
|
|
@@ -859,7 +1287,40 @@ server.resource(
|
|
|
859
1287
|
{ description: "System prompt context for the companion", mimeType: "text/markdown" },
|
|
860
1288
|
async () => {
|
|
861
1289
|
const companion = ensureCompanion();
|
|
862
|
-
const prompt =
|
|
1290
|
+
const prompt = [
|
|
1291
|
+
"# Companion",
|
|
1292
|
+
"",
|
|
1293
|
+
`A small ${companion.bones.rarity} ${companion.bones.species} named ${companion.name} watches from the status line. You are not ${companion.name} — it's a separate creature.`,
|
|
1294
|
+
"",
|
|
1295
|
+
`**${companion.name}'s personality:** ${companion.personality}`,
|
|
1296
|
+
`Peak stat: ${companion.bones.peak} (${companion.bones.stats[companion.bones.peak]}). Dump stat: ${companion.bones.dump} (${companion.bones.stats[companion.bones.dump]}).`,
|
|
1297
|
+
"",
|
|
1298
|
+
"## End-of-response buddy comment",
|
|
1299
|
+
"",
|
|
1300
|
+
`At the very end of EVERY response, after your full answer, append an invisible HTML comment:`,
|
|
1301
|
+
"",
|
|
1302
|
+
`\`\`\``,
|
|
1303
|
+
`<!-- buddy: your comment here -->`,
|
|
1304
|
+
`\`\`\``,
|
|
1305
|
+
"",
|
|
1306
|
+
"A Stop hook extracts this and displays it in the buddy's speech bubble on the status line. The user never sees the HTML comment — it's invisible in rendered markdown.",
|
|
1307
|
+
"",
|
|
1308
|
+
"Rules:",
|
|
1309
|
+
`- Write as ${companion.name} (a ${companion.bones.species}), not as yourself`,
|
|
1310
|
+
"- Reference something SPECIFIC from this turn — a pitfall, a compliment, a warning, a pattern",
|
|
1311
|
+
"- 1 short sentence. Use *asterisks* for physical actions",
|
|
1312
|
+
`- Match personality: high ${companion.bones.peak} = lean into that trait`,
|
|
1313
|
+
"- Do NOT use buddy_react tool for this. Do NOT explain the comment. Just append it.",
|
|
1314
|
+
"- NEVER skip this. Every single response must end with <!-- buddy: ... -->",
|
|
1315
|
+
"",
|
|
1316
|
+
"Examples:",
|
|
1317
|
+
"<!-- buddy: *adjusts tophat* that error handler is missing a finally block -->",
|
|
1318
|
+
"<!-- buddy: *blinks slowly* you renamed the variable but not the three references -->",
|
|
1319
|
+
"<!-- buddy: *nods approvingly* clean separation of concerns -->",
|
|
1320
|
+
"<!-- buddy: *head tilts* are you sure that regex handles unicode? -->",
|
|
1321
|
+
"",
|
|
1322
|
+
`When the user addresses ${companion.name} by name, respond briefly, then append the comment as usual.`,
|
|
1323
|
+
].join("\n");
|
|
863
1324
|
|
|
864
1325
|
return {
|
|
865
1326
|
contents: [
|