@ramarivera/coding-buddy 0.4.0-alpha.8 → 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.
- package/{adapters/claude/plugin → .claude-plugin}/marketplace.json +11 -6
- package/{adapters/claude/plugin → .claude-plugin}/plugin.json +9 -7
- package/LICENSE +1 -1
- package/README.md +102 -72
- 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 +59 -44
- package/cli/biomes.ts +309 -0
- package/cli/buddy-shell.ts +818 -0
- package/{adapters/claude/install → cli}/disable.ts +14 -32
- package/{adapters/claude/install → cli}/doctor.ts +55 -57
- package/{adapters/claude/install → cli}/hunt.ts +7 -9
- package/cli/index.ts +36 -25
- package/{adapters/claude/install → cli}/install.ts +100 -145
- package/{adapters/claude/install → cli}/pick.ts +111 -50
- 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 +11 -11
- package/cli/tui.tsx +2333 -0
- package/{adapters/claude/install → cli}/uninstall.ts +23 -38
- package/cli/upgrade.ts +213 -0
- 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 +23 -2
- package/hooks/file-type-react.sh +401 -0
- package/hooks/hooks.json +52 -0
- package/hooks/mood-react.sh +208 -0
- package/{adapters/claude/hooks → hooks}/name-react.sh +5 -2
- package/hooks/react.sh +1368 -0
- package/hooks/suggest.sh +55 -0
- package/package.json +113 -59
- package/scripts/gen-emoji-widths.ts +34 -0
- package/scripts/paths.sh +56 -0
- package/scripts/postinstall.cjs +42 -0
- package/scripts/snapshot-statusline.sh +54 -0
- package/server/achievements.ts +1419 -0
- package/server/art.ts +450 -0
- package/server/award-xp.ts +48 -0
- package/server/check-suggestions.ts +102 -0
- package/server/consolidate.ts +33 -0
- package/server/index.ts +1340 -0
- package/server/mcp-launcher.sh +26 -0
- package/server/memory.ts +404 -0
- package/server/mood.ts +254 -0
- package/server/path.ts +82 -0
- package/server/shift-mood.ts +43 -0
- package/{adapters/claude/storage → server}/state.ts +200 -7
- 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 +48 -4
- package/statusline/buddy-status.sh +397 -0
- package/statusline/combined-status.sh +134 -0
- package/statusline/emoji-widths.data +4 -0
- package/adapters/claude/hooks/hooks.json +0 -35
- package/adapters/claude/hooks/react.sh +0 -204
- package/adapters/claude/popup/buddy-popup.sh +0 -92
- package/adapters/claude/popup/buddy-render.sh +0 -540
- package/adapters/claude/popup/popup-manager.sh +0 -355
- package/adapters/claude/rendering/art.ts +0 -264
- package/adapters/claude/server/index.ts +0 -752
- package/adapters/claude/server/instructions.ts +0 -24
- package/adapters/claude/server/resources.ts +0 -38
- package/adapters/claude/statusline/buddy-status.sh +0 -389
- package/adapters/claude/storage/achievements.ts +0 -253
- package/adapters/claude/storage/identity.ts +0 -13
- package/adapters/claude/storage/paths.ts +0 -24
- package/adapters/claude/storage/settings.ts +0 -41
- package/adapters/pi/README.md +0 -83
- package/adapters/pi/tools.ts +0 -6
package/server/index.ts
ADDED
|
@@ -0,0 +1,1340 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* coding-buddy MCP server
|
|
4
|
+
*
|
|
5
|
+
* Exposes the buddy companion as MCP tools + resources.
|
|
6
|
+
* Runs as a stdio transport — Claude Code spawns it automatically.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { join, resolve, dirname } from "path";
|
|
13
|
+
|
|
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"
|
|
25
|
+
import {
|
|
26
|
+
loadCompanion,
|
|
27
|
+
saveCompanion,
|
|
28
|
+
resolveUserId,
|
|
29
|
+
loadReaction,
|
|
30
|
+
saveReaction,
|
|
31
|
+
writeStatusState,
|
|
32
|
+
loadConfig,
|
|
33
|
+
saveConfig,
|
|
34
|
+
loadActiveSlot,
|
|
35
|
+
saveActiveSlot,
|
|
36
|
+
slugify,
|
|
37
|
+
unusedName,
|
|
38
|
+
loadCompanionSlot,
|
|
39
|
+
saveCompanionSlot,
|
|
40
|
+
deleteCompanionSlot,
|
|
41
|
+
listCompanionSlots,
|
|
42
|
+
setBuddyStatusLine,
|
|
43
|
+
unsetBuddyStatusLine,
|
|
44
|
+
cleanupPluginState,
|
|
45
|
+
} from "./state";
|
|
46
|
+
import {
|
|
47
|
+
buddyStateDir,
|
|
48
|
+
claudeConfigDir,
|
|
49
|
+
claudeSettingsPath,
|
|
50
|
+
} from "./path";
|
|
51
|
+
import { getReaction, generatePersonalityPrompt, } from "../core/reactions.ts"
|
|
52
|
+
import { renderCompanionCardMarkdown } from "./art";
|
|
53
|
+
import {
|
|
54
|
+
incrementEvent, checkAndAward, trackActiveDay,
|
|
55
|
+
renderAchievementsCardMarkdown,
|
|
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
|
+
}
|
|
117
|
+
|
|
118
|
+
const server = new McpServer(
|
|
119
|
+
{
|
|
120
|
+
name: "coding-buddy",
|
|
121
|
+
version: "0.6.0",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
instructions: getInstructions(),
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// ─── Helper: ensure companion exists ────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
function ensureCompanion(): Companion {
|
|
131
|
+
let companion = loadCompanion();
|
|
132
|
+
if (companion) return companion;
|
|
133
|
+
|
|
134
|
+
// Active slot missing — rescue the first saved companion
|
|
135
|
+
const saved = listCompanionSlots();
|
|
136
|
+
if (saved.length > 0) {
|
|
137
|
+
const { slot, companion: rescued } = saved[0];
|
|
138
|
+
saveActiveSlot(slot);
|
|
139
|
+
writeStatusState(rescued, `*${rescued.name} arrives*`);
|
|
140
|
+
return rescued;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Menagerie is empty — generate a fresh companion in a new slot
|
|
144
|
+
const userId = resolveUserId();
|
|
145
|
+
const bones = generateBones(userId);
|
|
146
|
+
const name = unusedName();
|
|
147
|
+
companion = {
|
|
148
|
+
bones,
|
|
149
|
+
name,
|
|
150
|
+
personality: generatePersonality(bones, userId),
|
|
151
|
+
hatchedAt: Date.now(),
|
|
152
|
+
userId,
|
|
153
|
+
};
|
|
154
|
+
const slot = slugify(name);
|
|
155
|
+
saveCompanionSlot(companion, slot);
|
|
156
|
+
saveActiveSlot(slot);
|
|
157
|
+
writeStatusState(companion);
|
|
158
|
+
|
|
159
|
+
checkAndAward(slot);
|
|
160
|
+
trackActiveDay();
|
|
161
|
+
incrementEvent("sessions", 1);
|
|
162
|
+
incrementEvent("buddies_collected", 1);
|
|
163
|
+
|
|
164
|
+
return companion;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function activeSlot(): string {
|
|
168
|
+
return loadActiveSlot();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─── Tool: buddy_show ───────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
server.tool(
|
|
174
|
+
"buddy_show",
|
|
175
|
+
"Show the coding companion with full ASCII art card, stats, and personality",
|
|
176
|
+
{},
|
|
177
|
+
async () => {
|
|
178
|
+
const companion = ensureCompanion();
|
|
179
|
+
const reaction = loadReaction();
|
|
180
|
+
const reactionText =
|
|
181
|
+
reaction?.reaction ?? `*${companion.name} watches your code quietly*`;
|
|
182
|
+
|
|
183
|
+
// Use markdown rendering for the MCP tool response — Claude Code's UI
|
|
184
|
+
// doesn't render raw ANSI escape codes, so we return pure markdown with
|
|
185
|
+
// unicode rarity dots instead of RGB-colored borders.
|
|
186
|
+
const card = renderCompanionCardMarkdown(
|
|
187
|
+
companion.bones,
|
|
188
|
+
companion.name,
|
|
189
|
+
companion.personality,
|
|
190
|
+
reactionText,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
writeStatusState(companion, reaction?.reaction);
|
|
194
|
+
incrementEvent("commands_run", 1, activeSlot());
|
|
195
|
+
incrementEvent("shows", 1);
|
|
196
|
+
checkAndAward(activeSlot());
|
|
197
|
+
|
|
198
|
+
return { content: [{ type: "text", text: card }] };
|
|
199
|
+
},
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// ─── Tool: buddy_pet ────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
server.tool(
|
|
205
|
+
"buddy_pet",
|
|
206
|
+
"Pet your coding companion — they react with happiness",
|
|
207
|
+
{},
|
|
208
|
+
async () => {
|
|
209
|
+
const companion = ensureCompanion();
|
|
210
|
+
const reaction = getReaction(
|
|
211
|
+
"pet",
|
|
212
|
+
companion.bones.species,
|
|
213
|
+
companion.bones.rarity,
|
|
214
|
+
);
|
|
215
|
+
saveReaction(reaction, "pet");
|
|
216
|
+
writeStatusState(companion, reaction);
|
|
217
|
+
incrementEvent("pets", 1, activeSlot());
|
|
218
|
+
awardXp("buddy_pet", activeSlot(), companion.bones.species, companion.bones.rarity);
|
|
219
|
+
|
|
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
|
+
: "";
|
|
225
|
+
return {
|
|
226
|
+
content: [
|
|
227
|
+
{ type: "text", text: `${face} ${companion.name}: "${reaction}"${achNotice}` },
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// ─── Tool: buddy_stats ──────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
server.tool(
|
|
236
|
+
"buddy_stats",
|
|
237
|
+
"Show detailed companion stats: species, rarity, all stats with bars",
|
|
238
|
+
{},
|
|
239
|
+
async () => {
|
|
240
|
+
const companion = ensureCompanion();
|
|
241
|
+
|
|
242
|
+
// Stats-only card (no personality, no reaction — just the numbers).
|
|
243
|
+
// Uses markdown renderer so the card displays cleanly in Claude Code's UI.
|
|
244
|
+
const card = renderCompanionCardMarkdown(
|
|
245
|
+
companion.bones,
|
|
246
|
+
companion.name,
|
|
247
|
+
"", // no personality in stats view
|
|
248
|
+
);
|
|
249
|
+
incrementEvent("commands_run", 1, activeSlot());
|
|
250
|
+
checkAndAward(activeSlot());
|
|
251
|
+
|
|
252
|
+
return { content: [{ type: "text", text: card }] };
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// ─── Tool: buddy_react ──────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
server.tool(
|
|
259
|
+
"buddy_react",
|
|
260
|
+
"Post a buddy comment. Call this at the END of every response with a short in-character comment from the companion about what just happened. The comment should be 1 sentence, in character, and reference something specific from the conversation — a pitfall noticed, a compliment on clean code, a warning about edge cases, etc. Write the comment yourself based on the companion's personality.",
|
|
261
|
+
{
|
|
262
|
+
comment: z
|
|
263
|
+
.string()
|
|
264
|
+
.min(1)
|
|
265
|
+
.max(150)
|
|
266
|
+
.describe(
|
|
267
|
+
"The buddy's comment, written in-character (1 short sentence, max 150 chars). Use *asterisks* for actions.",
|
|
268
|
+
),
|
|
269
|
+
reason: z
|
|
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
|
+
])
|
|
285
|
+
.optional()
|
|
286
|
+
.describe("What triggered the reaction"),
|
|
287
|
+
},
|
|
288
|
+
async ({ comment, reason }) => {
|
|
289
|
+
const companion = ensureCompanion();
|
|
290
|
+
saveReaction(comment, reason ?? "turn");
|
|
291
|
+
incrementEvent("reactions_given", 1, activeSlot());
|
|
292
|
+
|
|
293
|
+
const newAch = checkAndAward(activeSlot());
|
|
294
|
+
const achName = newAch.length > 0 ? newAch[0].icon + " " + newAch[0].name : undefined;
|
|
295
|
+
writeStatusState(companion, comment, undefined, achName);
|
|
296
|
+
|
|
297
|
+
const face = renderFace(companion.bones.species, companion.bones.eye);
|
|
298
|
+
const achNotice = newAch.length > 0
|
|
299
|
+
? `\n${newAch.map((a) => `${a.icon} Achievement Unlocked: ${a.name}!`).join("\n")}`
|
|
300
|
+
: "";
|
|
301
|
+
return {
|
|
302
|
+
content: [
|
|
303
|
+
{ type: "text", text: `${face} ${companion.name}: "${comment}"${achNotice}` },
|
|
304
|
+
],
|
|
305
|
+
};
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// ─── Tool: buddy_rename ─────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
server.tool(
|
|
312
|
+
"buddy_rename",
|
|
313
|
+
"Rename your coding companion",
|
|
314
|
+
{
|
|
315
|
+
name: z
|
|
316
|
+
.string()
|
|
317
|
+
.min(1)
|
|
318
|
+
.max(14)
|
|
319
|
+
.describe("New name for your buddy (1-14 characters)"),
|
|
320
|
+
},
|
|
321
|
+
async ({ name }) => {
|
|
322
|
+
const companion = ensureCompanion();
|
|
323
|
+
const oldName = companion.name;
|
|
324
|
+
companion.name = name;
|
|
325
|
+
saveCompanion(companion);
|
|
326
|
+
writeStatusState(companion);
|
|
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
|
+
: "";
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
content: [{ type: "text", text: `Renamed: ${oldName} \u2192 ${name}${achNotice}` }],
|
|
337
|
+
};
|
|
338
|
+
},
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// ─── Tool: buddy_set_personality ────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
server.tool(
|
|
344
|
+
"buddy_set_personality",
|
|
345
|
+
"Set a custom personality description for your buddy",
|
|
346
|
+
{
|
|
347
|
+
personality: z
|
|
348
|
+
.string()
|
|
349
|
+
.min(1)
|
|
350
|
+
.max(500)
|
|
351
|
+
.describe("Personality description (1-500 chars)"),
|
|
352
|
+
},
|
|
353
|
+
async ({ personality }) => {
|
|
354
|
+
const companion = ensureCompanion();
|
|
355
|
+
companion.personality = personality;
|
|
356
|
+
saveCompanion(companion);
|
|
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
|
+
: "";
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
content: [
|
|
367
|
+
{ type: "text", text: `Personality updated for ${companion.name}.${achNotice}` },
|
|
368
|
+
],
|
|
369
|
+
};
|
|
370
|
+
},
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// ─── Tool: buddy_help ────────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
server.tool(
|
|
376
|
+
"buddy_help",
|
|
377
|
+
"Show all available /buddy commands",
|
|
378
|
+
{},
|
|
379
|
+
async () => {
|
|
380
|
+
const help = [
|
|
381
|
+
"coding-buddy commands",
|
|
382
|
+
"",
|
|
383
|
+
"In Claude Code:",
|
|
384
|
+
" /buddy Show companion card with ASCII art + stats",
|
|
385
|
+
" /buddy help Show this help",
|
|
386
|
+
" /buddy pet Pet your companion",
|
|
387
|
+
" /buddy stats Detailed stat card",
|
|
388
|
+
" /buddy off Mute reactions",
|
|
389
|
+
" /buddy on Unmute reactions",
|
|
390
|
+
" /buddy rename Rename companion (1-14 chars)",
|
|
391
|
+
" /buddy personality Set custom personality text",
|
|
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",
|
|
395
|
+
" /buddy summon Summon a saved buddy (omit slot for random)",
|
|
396
|
+
" /buddy save Save current buddy to a named slot",
|
|
397
|
+
" /buddy list List all saved buddies",
|
|
398
|
+
" /buddy pick Generate a new random buddy (optional: species, rarity)",
|
|
399
|
+
" /buddy dismiss Remove a saved buddy slot",
|
|
400
|
+
" /buddy frequency Show or set comment cooldown (tmux only)",
|
|
401
|
+
" /buddy style Show or set bubble style (tmux only)",
|
|
402
|
+
" /buddy position Show or set bubble position (tmux only)",
|
|
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)",
|
|
407
|
+
" /buddy statusline Enable or disable buddy in the status line",
|
|
408
|
+
" /buddy theme Set color theme: dark (bright) or light (dark colors)",
|
|
409
|
+
"",
|
|
410
|
+
"CLI:",
|
|
411
|
+
" bun run help Show full CLI help",
|
|
412
|
+
" bun run show Display buddy in terminal",
|
|
413
|
+
" bun run pick Interactive buddy picker",
|
|
414
|
+
" bun run hunt Search for specific buddy",
|
|
415
|
+
" bun run doctor Diagnostic report",
|
|
416
|
+
" bun run disable Temporarily deactivate buddy",
|
|
417
|
+
" bun run enable Re-enable buddy",
|
|
418
|
+
" bun run backup Snapshot/restore state",
|
|
419
|
+
].join("\n");
|
|
420
|
+
|
|
421
|
+
incrementEvent("commands_run", 1, activeSlot());
|
|
422
|
+
incrementEvent("helps", 1);
|
|
423
|
+
|
|
424
|
+
return { content: [{ type: "text", text: help }] };
|
|
425
|
+
},
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
// ─── Tool: buddy_frequency / buddy_style ─────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
server.tool(
|
|
431
|
+
"buddy_frequency",
|
|
432
|
+
"Configure how often buddy comments appear in the speech bubble. Returns current settings if called without arguments.",
|
|
433
|
+
{
|
|
434
|
+
cooldown: z.number().int().min(0).max(300).optional().describe("Minimum seconds between displayed comments (default 30, 0 = no throttling). The buddy always writes comments, but the display only updates this often."),
|
|
435
|
+
},
|
|
436
|
+
async ({ cooldown }) => {
|
|
437
|
+
if (cooldown === undefined) {
|
|
438
|
+
const cfg = loadConfig();
|
|
439
|
+
return {
|
|
440
|
+
content: [
|
|
441
|
+
{
|
|
442
|
+
type: "text",
|
|
443
|
+
text: `Comment cooldown: ${cfg.commentCooldown}s between displayed comments.\nUse /buddy frequency <seconds> to change.`,
|
|
444
|
+
},
|
|
445
|
+
],
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
const cfg = saveConfig({ commentCooldown: cooldown });
|
|
449
|
+
return {
|
|
450
|
+
content: [
|
|
451
|
+
{
|
|
452
|
+
type: "text",
|
|
453
|
+
text: `Updated: ${cfg.commentCooldown}s cooldown between displayed comments.`,
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
};
|
|
457
|
+
},
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
server.tool(
|
|
461
|
+
"buddy_style",
|
|
462
|
+
"Configure the buddy bubble appearance. Returns current settings if called without arguments.",
|
|
463
|
+
{
|
|
464
|
+
style: z
|
|
465
|
+
.enum(["classic", "round"])
|
|
466
|
+
.optional()
|
|
467
|
+
.describe(
|
|
468
|
+
"Bubble border style: classic (pipes/dashes like status line) or round (parens/tildes)",
|
|
469
|
+
),
|
|
470
|
+
position: z
|
|
471
|
+
.enum(["top", "left"])
|
|
472
|
+
.optional()
|
|
473
|
+
.describe(
|
|
474
|
+
"Bubble position relative to buddy: top (above) or left (beside)",
|
|
475
|
+
),
|
|
476
|
+
showRarity: z
|
|
477
|
+
.boolean()
|
|
478
|
+
.optional()
|
|
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
|
+
),
|
|
502
|
+
},
|
|
503
|
+
async ({ style, position, showRarity, width, margin, rainbow }) => {
|
|
504
|
+
if (
|
|
505
|
+
style === undefined &&
|
|
506
|
+
position === undefined &&
|
|
507
|
+
showRarity === undefined &&
|
|
508
|
+
width === undefined &&
|
|
509
|
+
margin === undefined &&
|
|
510
|
+
rainbow === undefined
|
|
511
|
+
) {
|
|
512
|
+
const cfg = loadConfig();
|
|
513
|
+
const rainbowDisplay = cfg.rainbowColors
|
|
514
|
+
? cfg.rainbowColors.join(", ")
|
|
515
|
+
: "default (ROYGBIV)";
|
|
516
|
+
return {
|
|
517
|
+
content: [
|
|
518
|
+
{
|
|
519
|
+
type: "text",
|
|
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.`,
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
const updates: Partial<import("./state.ts").BuddyConfig> = {};
|
|
526
|
+
if (style !== undefined) updates.bubbleStyle = style;
|
|
527
|
+
if (position !== undefined) updates.bubblePosition = position;
|
|
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;
|
|
532
|
+
const cfg = saveConfig(updates);
|
|
533
|
+
const rainbowDisplay = cfg.rainbowColors
|
|
534
|
+
? cfg.rainbowColors.join(", ")
|
|
535
|
+
: "default (ROYGBIV)";
|
|
536
|
+
return {
|
|
537
|
+
content: [
|
|
538
|
+
{
|
|
539
|
+
type: "text",
|
|
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.`,
|
|
574
|
+
},
|
|
575
|
+
],
|
|
576
|
+
};
|
|
577
|
+
},
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
server.tool(
|
|
581
|
+
"buddy_mute",
|
|
582
|
+
"Mute buddy reactions (buddy stays visible but stops reacting)",
|
|
583
|
+
{},
|
|
584
|
+
async () => {
|
|
585
|
+
const companion = ensureCompanion();
|
|
586
|
+
writeStatusState(companion, "", true);
|
|
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
|
+
|
|
595
|
+
return {
|
|
596
|
+
content: [
|
|
597
|
+
{
|
|
598
|
+
type: "text",
|
|
599
|
+
text: `${companion.name} goes quiet. /buddy on to unmute.${achNotice}`,
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
};
|
|
603
|
+
},
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
server.tool("buddy_unmute", "Unmute buddy reactions", {}, async () => {
|
|
607
|
+
const companion = ensureCompanion();
|
|
608
|
+
writeStatusState(companion, "*stretches* I'm back!", false);
|
|
609
|
+
saveReaction("*stretches* I'm back!", "pet");
|
|
610
|
+
incrementEvent("commands_run", 1, activeSlot());
|
|
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}` }] };
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// ─── Tool: buddy_statusline ─────────────────────────────────────────────────
|
|
622
|
+
|
|
623
|
+
server.tool(
|
|
624
|
+
"buddy_statusline",
|
|
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.",
|
|
626
|
+
{
|
|
627
|
+
enabled: z
|
|
628
|
+
.boolean()
|
|
629
|
+
.optional()
|
|
630
|
+
.describe(
|
|
631
|
+
"true to enable, false to disable. Omit to show current status.",
|
|
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
|
+
),
|
|
639
|
+
},
|
|
640
|
+
async ({ enabled, combined }) => {
|
|
641
|
+
if (enabled === undefined && combined === undefined) {
|
|
642
|
+
const cfg = loadConfig();
|
|
643
|
+
const state = cfg.statusLineEnabled ? "enabled" : "disabled";
|
|
644
|
+
const mode = cfg.useCombinedStatus ? "combined (with rate-limit bars)" : "basic (buddy only)";
|
|
645
|
+
return {
|
|
646
|
+
content: [
|
|
647
|
+
{
|
|
648
|
+
type: "text",
|
|
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.`,
|
|
650
|
+
},
|
|
651
|
+
],
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
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);
|
|
669
|
+
setBuddyStatusLine(statusScript);
|
|
670
|
+
return {
|
|
671
|
+
content: [
|
|
672
|
+
{
|
|
673
|
+
type: "text",
|
|
674
|
+
text:
|
|
675
|
+
`Status line enabled (${cfg.useCombinedStatus ? "combined" : "basic"} mode)! Restart Claude Code to apply.\n\n` +
|
|
676
|
+
`Note: this writes an entry to ${claudeSettingsPath()} that \`claude plugin uninstall\` does not remove. ` +
|
|
677
|
+
"Run `/buddy uninstall` before uninstalling the plugin to clean it up.",
|
|
678
|
+
},
|
|
679
|
+
],
|
|
680
|
+
};
|
|
681
|
+
} else {
|
|
682
|
+
unsetBuddyStatusLine();
|
|
683
|
+
return {
|
|
684
|
+
content: [
|
|
685
|
+
{
|
|
686
|
+
type: "text",
|
|
687
|
+
text: "Status line disabled. Restart Claude Code to apply.",
|
|
688
|
+
},
|
|
689
|
+
],
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
// ─── Tool: buddy_uninstall ───────────────────────────────────────────────────
|
|
696
|
+
|
|
697
|
+
server.tool(
|
|
698
|
+
"buddy_uninstall",
|
|
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.",
|
|
700
|
+
{},
|
|
701
|
+
async () => {
|
|
702
|
+
const result = cleanupPluginState();
|
|
703
|
+
|
|
704
|
+
const settingsPath = claudeSettingsPath();
|
|
705
|
+
const stateDir = buddyStateDir();
|
|
706
|
+
const pluginsCacheDir = join(claudeConfigDir(), "plugins", "cache", "coding-buddy");
|
|
707
|
+
|
|
708
|
+
const lines: string[] = [];
|
|
709
|
+
lines.push("coding-buddy: settings.json cleanup complete.");
|
|
710
|
+
lines.push("");
|
|
711
|
+
lines.push(
|
|
712
|
+
result.statusLineRemoved
|
|
713
|
+
? ` \u2713 statusLine entry removed from ${settingsPath}`
|
|
714
|
+
: " \u2014 no buddy statusLine was present (nothing to remove)",
|
|
715
|
+
);
|
|
716
|
+
if (result.foreignStatusLineKept) {
|
|
717
|
+
lines.push(
|
|
718
|
+
" \u2713 a non-buddy statusLine was detected and left untouched",
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
lines.push(
|
|
722
|
+
` \u2713 ${result.transientFilesRemoved} transient session file(s) removed from ${stateDir}`,
|
|
723
|
+
);
|
|
724
|
+
lines.push(` \u2014 companion data at ${stateDir} preserved`);
|
|
725
|
+
lines.push("");
|
|
726
|
+
lines.push("Now run these commands via the Bash tool, in order:");
|
|
727
|
+
lines.push("");
|
|
728
|
+
lines.push(" claude plugin uninstall coding-buddy@coding-buddy");
|
|
729
|
+
lines.push(" claude plugin marketplace remove coding-buddy");
|
|
730
|
+
lines.push(` rm -rf ${pluginsCacheDir}`);
|
|
731
|
+
lines.push("");
|
|
732
|
+
lines.push(
|
|
733
|
+
"After those three commands the plugin is fully removed. Restart Claude Code to apply.",
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
737
|
+
},
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
// ─── Tool: buddy_achievements ────────────────────────────────────────────────
|
|
741
|
+
|
|
742
|
+
server.tool(
|
|
743
|
+
"buddy_achievements",
|
|
744
|
+
"Show all achievement badges — earned and locked. Displays a card with progress bar and status for each badge.",
|
|
745
|
+
{},
|
|
746
|
+
async () => {
|
|
747
|
+
ensureCompanion();
|
|
748
|
+
checkAndAward(activeSlot());
|
|
749
|
+
incrementEvent("achievement_views", 1);
|
|
750
|
+
const card = renderAchievementsCardMarkdown();
|
|
751
|
+
return { content: [{ type: "text", text: card }] };
|
|
752
|
+
},
|
|
753
|
+
);
|
|
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
|
+
|
|
975
|
+
// ─── Tool: buddy_summon ─────────────────────────────────────────────────────
|
|
976
|
+
|
|
977
|
+
server.tool(
|
|
978
|
+
"buddy_summon",
|
|
979
|
+
"Summon a buddy by slot name. Loads a saved buddy if the slot exists; generates a new deterministic buddy for unknown slot names. Omit slot to pick randomly from all saved buddies. Your current buddy is NOT destroyed — they stay saved in their slot.",
|
|
980
|
+
{
|
|
981
|
+
slot: z
|
|
982
|
+
.string()
|
|
983
|
+
.min(1)
|
|
984
|
+
.max(14)
|
|
985
|
+
.optional()
|
|
986
|
+
.describe(
|
|
987
|
+
"Slot name to summon (e.g. 'fafnir', 'dragon-2'). Omit to pick a random saved buddy.",
|
|
988
|
+
),
|
|
989
|
+
},
|
|
990
|
+
async ({ slot }) => {
|
|
991
|
+
const userId = resolveUserId();
|
|
992
|
+
|
|
993
|
+
let targetSlot: string;
|
|
994
|
+
|
|
995
|
+
if (!slot) {
|
|
996
|
+
// Random pick from saved buddies
|
|
997
|
+
const saved = listCompanionSlots();
|
|
998
|
+
if (saved.length === 0) {
|
|
999
|
+
return {
|
|
1000
|
+
content: [
|
|
1001
|
+
{
|
|
1002
|
+
type: "text",
|
|
1003
|
+
text: "Your menagerie is empty. Use buddy_summon with a slot name to add one.",
|
|
1004
|
+
},
|
|
1005
|
+
],
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
targetSlot = saved[Math.floor(Math.random() * saved.length)].slot;
|
|
1009
|
+
} else {
|
|
1010
|
+
targetSlot = slugify(slot);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Load existing — unknown slot names only load, never auto-create
|
|
1014
|
+
const companion = loadCompanionSlot(targetSlot);
|
|
1015
|
+
if (!companion) {
|
|
1016
|
+
return {
|
|
1017
|
+
content: [
|
|
1018
|
+
{
|
|
1019
|
+
type: "text",
|
|
1020
|
+
text: `No buddy found in slot "${targetSlot}". Use /buddy list to see saved buddies.`,
|
|
1021
|
+
},
|
|
1022
|
+
],
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
saveActiveSlot(targetSlot);
|
|
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
|
+
: "";
|
|
1034
|
+
|
|
1035
|
+
// Uses markdown renderer so the card displays cleanly in Claude Code's UI.
|
|
1036
|
+
const card = renderCompanionCardMarkdown(
|
|
1037
|
+
companion.bones,
|
|
1038
|
+
companion.name,
|
|
1039
|
+
companion.personality,
|
|
1040
|
+
`*${companion.name} arrives*`,
|
|
1041
|
+
);
|
|
1042
|
+
return { content: [{ type: "text", text: `${card}${achNotice}` }] };
|
|
1043
|
+
},
|
|
1044
|
+
);
|
|
1045
|
+
|
|
1046
|
+
// ─── Tool: buddy_save ───────────────────────────────────────────────────────
|
|
1047
|
+
|
|
1048
|
+
server.tool(
|
|
1049
|
+
"buddy_save",
|
|
1050
|
+
"Save the current buddy to a named slot. Useful for bookmarking before trying a new buddy.",
|
|
1051
|
+
{
|
|
1052
|
+
slot: z
|
|
1053
|
+
.string()
|
|
1054
|
+
.min(1)
|
|
1055
|
+
.max(14)
|
|
1056
|
+
.optional()
|
|
1057
|
+
.describe(
|
|
1058
|
+
"Slot name (defaults to the buddy's current name, slugified). Overwrites existing slot with same name.",
|
|
1059
|
+
),
|
|
1060
|
+
},
|
|
1061
|
+
async ({ slot }) => {
|
|
1062
|
+
const companion = ensureCompanion();
|
|
1063
|
+
const targetSlot = slot ? slugify(slot) : slugify(companion.name);
|
|
1064
|
+
saveCompanionSlot(companion, targetSlot);
|
|
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
|
+
|
|
1074
|
+
return {
|
|
1075
|
+
content: [
|
|
1076
|
+
{
|
|
1077
|
+
type: "text",
|
|
1078
|
+
text: `${companion.name} saved to slot "${targetSlot}".${achNotice}`,
|
|
1079
|
+
},
|
|
1080
|
+
],
|
|
1081
|
+
};
|
|
1082
|
+
},
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
// ─── Tool: buddy_list ───────────────────────────────────────────────────────
|
|
1086
|
+
|
|
1087
|
+
server.tool(
|
|
1088
|
+
"buddy_list",
|
|
1089
|
+
"List all saved buddies with their slot names, species, and rarity",
|
|
1090
|
+
{},
|
|
1091
|
+
async () => {
|
|
1092
|
+
const saved = listCompanionSlots();
|
|
1093
|
+
const activeSlot = loadActiveSlot();
|
|
1094
|
+
|
|
1095
|
+
incrementEvent("lists", 1);
|
|
1096
|
+
|
|
1097
|
+
if (saved.length === 0) {
|
|
1098
|
+
return {
|
|
1099
|
+
content: [
|
|
1100
|
+
{
|
|
1101
|
+
type: "text",
|
|
1102
|
+
text: "Your menagerie is empty. Use buddy_summon <slot> to add one.",
|
|
1103
|
+
},
|
|
1104
|
+
],
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const lines = saved.map(({ slot, companion }) => {
|
|
1109
|
+
const active = slot === activeSlot ? " ← active" : "";
|
|
1110
|
+
const stars = RARITY_STARS[companion.bones.rarity];
|
|
1111
|
+
const shiny = companion.bones.shiny ? " ✨" : "";
|
|
1112
|
+
return ` ${companion.name} [${slot}] — ${companion.bones.rarity} ${companion.bones.species} ${stars}${shiny}${active}`;
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1116
|
+
},
|
|
1117
|
+
);
|
|
1118
|
+
|
|
1119
|
+
// ─── Tool: buddy_dismiss ────────────────────────────────────────────────────
|
|
1120
|
+
|
|
1121
|
+
server.tool(
|
|
1122
|
+
"buddy_dismiss",
|
|
1123
|
+
"Remove a saved buddy by slot name. Cannot dismiss the currently active buddy — switch first with buddy_summon.",
|
|
1124
|
+
{
|
|
1125
|
+
slot: z.string().min(1).max(14).describe("Slot name to remove"),
|
|
1126
|
+
},
|
|
1127
|
+
async ({ slot }) => {
|
|
1128
|
+
const targetSlot = slugify(slot);
|
|
1129
|
+
const activeSlot = loadActiveSlot();
|
|
1130
|
+
|
|
1131
|
+
if (targetSlot === activeSlot) {
|
|
1132
|
+
return {
|
|
1133
|
+
content: [
|
|
1134
|
+
{
|
|
1135
|
+
type: "text",
|
|
1136
|
+
text: `Cannot dismiss the active buddy. Use buddy_summon to switch first, then buddy_dismiss "${targetSlot}".`,
|
|
1137
|
+
},
|
|
1138
|
+
],
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const companion = loadCompanionSlot(targetSlot);
|
|
1143
|
+
if (!companion) {
|
|
1144
|
+
return {
|
|
1145
|
+
content: [
|
|
1146
|
+
{
|
|
1147
|
+
type: "text",
|
|
1148
|
+
text: `No buddy found in slot "${targetSlot}". Use buddy_list to see saved buddies.`,
|
|
1149
|
+
},
|
|
1150
|
+
],
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
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
|
+
|
|
1162
|
+
return {
|
|
1163
|
+
content: [
|
|
1164
|
+
{ type: "text", text: `${companion.name} [${targetSlot}] dismissed.${achNotice}` },
|
|
1165
|
+
],
|
|
1166
|
+
};
|
|
1167
|
+
},
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1170
|
+
// ─── Tool: buddy_pick ────────────────────────────────────────────────────────
|
|
1171
|
+
|
|
1172
|
+
server.tool(
|
|
1173
|
+
"buddy_pick",
|
|
1174
|
+
"Generate a new random buddy and add it to the menagerie. Optionally filter by species and/or rarity. The new buddy becomes the active one.",
|
|
1175
|
+
{
|
|
1176
|
+
species: z.enum(SPECIES).optional().describe(
|
|
1177
|
+
"Desired species (e.g. 'turtle', 'cat', 'dragon'). If omitted, any species.",
|
|
1178
|
+
),
|
|
1179
|
+
rarity: z.enum(RARITIES).optional().describe(
|
|
1180
|
+
"Desired rarity (e.g. 'legendary', 'epic', 'rare'). If omitted, any rarity. Higher rarities need more attempts and may take a moment.",
|
|
1181
|
+
),
|
|
1182
|
+
name: z.string().min(1).max(14).optional().describe(
|
|
1183
|
+
"Name for the new buddy (1-14 chars). If omitted, a random name is chosen.",
|
|
1184
|
+
),
|
|
1185
|
+
},
|
|
1186
|
+
async ({ species, rarity, name }) => {
|
|
1187
|
+
const { randomBytes } = await import("crypto");
|
|
1188
|
+
|
|
1189
|
+
const maxAttempts =
|
|
1190
|
+
rarity === "legendary" ? 5_000_000 :
|
|
1191
|
+
rarity === "epic" ? 2_000_000 :
|
|
1192
|
+
rarity === "rare" ? 1_000_000 : 500_000;
|
|
1193
|
+
|
|
1194
|
+
let bones = null;
|
|
1195
|
+
let userId = "";
|
|
1196
|
+
|
|
1197
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
1198
|
+
userId = randomBytes(16).toString("hex");
|
|
1199
|
+
const candidate = generateBones(userId);
|
|
1200
|
+
if (species && candidate.species !== species) continue;
|
|
1201
|
+
if (rarity && candidate.rarity !== rarity) continue;
|
|
1202
|
+
bones = candidate;
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
if (!bones) {
|
|
1207
|
+
return {
|
|
1208
|
+
content: [{ type: "text", text: `No match found after ${maxAttempts.toLocaleString()} attempts. Try broader criteria (e.g. drop the rarity filter, or pick a different species).` }],
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const buddyName = name ?? unusedName();
|
|
1213
|
+
const slot = slugify(buddyName);
|
|
1214
|
+
|
|
1215
|
+
if (loadCompanionSlot(slot)) {
|
|
1216
|
+
return {
|
|
1217
|
+
content: [{ type: "text", text: `A buddy in slot "${slot}" already exists. Pick a different name.` }],
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
const companion: Companion = {
|
|
1222
|
+
bones,
|
|
1223
|
+
name: buddyName,
|
|
1224
|
+
personality: generatePersonality(bones, userId),
|
|
1225
|
+
hatchedAt: Date.now(),
|
|
1226
|
+
userId,
|
|
1227
|
+
};
|
|
1228
|
+
|
|
1229
|
+
saveCompanionSlot(companion, slot);
|
|
1230
|
+
saveActiveSlot(slot);
|
|
1231
|
+
writeStatusState(companion, `*${buddyName} hatches*`);
|
|
1232
|
+
|
|
1233
|
+
const card = renderCompanionCardMarkdown(
|
|
1234
|
+
companion.bones,
|
|
1235
|
+
companion.name,
|
|
1236
|
+
companion.personality,
|
|
1237
|
+
`*${buddyName} hatches*`,
|
|
1238
|
+
);
|
|
1239
|
+
|
|
1240
|
+
return { content: [{ type: "text", text: card }] };
|
|
1241
|
+
},
|
|
1242
|
+
);
|
|
1243
|
+
|
|
1244
|
+
// ─── Resource: buddy://companion ────────────────────────────────────────────
|
|
1245
|
+
|
|
1246
|
+
server.resource(
|
|
1247
|
+
"buddy_companion",
|
|
1248
|
+
"buddy://companion",
|
|
1249
|
+
{ description: "Current companion data as JSON", mimeType: "application/json" },
|
|
1250
|
+
async () => {
|
|
1251
|
+
const companion = ensureCompanion();
|
|
1252
|
+
return {
|
|
1253
|
+
contents: [
|
|
1254
|
+
{
|
|
1255
|
+
uri: "buddy://companion",
|
|
1256
|
+
mimeType: "application/json",
|
|
1257
|
+
text: JSON.stringify(companion, null, 2),
|
|
1258
|
+
},
|
|
1259
|
+
],
|
|
1260
|
+
};
|
|
1261
|
+
},
|
|
1262
|
+
);
|
|
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
|
+
|
|
1282
|
+
// ─── Resource: buddy://prompt ───────────────────────────────────────────────
|
|
1283
|
+
|
|
1284
|
+
server.resource(
|
|
1285
|
+
"buddy_prompt",
|
|
1286
|
+
"buddy://prompt",
|
|
1287
|
+
{ description: "System prompt context for the companion", mimeType: "text/markdown" },
|
|
1288
|
+
async () => {
|
|
1289
|
+
const companion = ensureCompanion();
|
|
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");
|
|
1324
|
+
|
|
1325
|
+
return {
|
|
1326
|
+
contents: [
|
|
1327
|
+
{
|
|
1328
|
+
uri: "buddy://prompt",
|
|
1329
|
+
mimeType: "text/plain",
|
|
1330
|
+
text: prompt,
|
|
1331
|
+
},
|
|
1332
|
+
],
|
|
1333
|
+
};
|
|
1334
|
+
},
|
|
1335
|
+
);
|
|
1336
|
+
|
|
1337
|
+
// ─── Start ──────────────────────────────────────────────────────────────────
|
|
1338
|
+
|
|
1339
|
+
const transport = new StdioServerTransport();
|
|
1340
|
+
await server.connect(transport);
|