@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.
Files changed (94) hide show
  1. package/.claude-plugin/marketplace.json +45 -0
  2. package/.claude-plugin/plugin.json +30 -0
  3. package/LICENSE +1 -1
  4. package/README.md +96 -45
  5. package/adapters/omp/commands.ts +201 -0
  6. package/adapters/omp/context.ts +22 -0
  7. package/adapters/omp/events.ts +551 -0
  8. package/adapters/omp/identity.ts +10 -0
  9. package/adapters/omp/index.ts +39 -0
  10. package/adapters/omp/logger.ts +52 -0
  11. package/adapters/omp/prompt.ts +60 -0
  12. package/adapters/omp/renderers.ts +73 -0
  13. package/adapters/omp/storage.ts +25 -0
  14. package/adapters/omp/ui.ts +85 -0
  15. package/adapters/pi/commands.ts +64 -37
  16. package/adapters/pi/events.ts +147 -72
  17. package/adapters/pi/index.ts +9 -4
  18. package/adapters/pi/storage.ts +14 -285
  19. package/adapters/pi/ui.ts +56 -5
  20. package/adapters/shared/file-storage.ts +526 -0
  21. package/{adapters/claude/install → cli}/backup.ts +130 -33
  22. package/cli/buddy-shell.ts +1 -1
  23. package/{adapters/claude/install → cli}/disable.ts +19 -32
  24. package/cli/doctor.ts +231 -0
  25. package/{adapters/claude/install → cli}/hunt.ts +7 -9
  26. package/cli/index.ts +30 -26
  27. package/{adapters/claude/install → cli}/install.ts +65 -83
  28. package/{adapters/claude/install → cli}/pick.ts +136 -57
  29. package/{adapters/claude/install → cli}/settings.ts +2 -2
  30. package/{adapters/claude/install → cli}/show.ts +4 -4
  31. package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
  32. package/{adapters/claude/install → cli}/test-statusline.ts +21 -11
  33. package/cli/tui.tsx +106 -17
  34. package/{adapters/claude/install → cli}/uninstall.ts +40 -47
  35. package/cli/upgrade.ts +46 -13
  36. package/cli/validate-species.ts +209 -0
  37. package/cli/verify.ts +3 -3
  38. package/core/achievements.ts +12 -20
  39. package/core/art-data.ts +26 -0
  40. package/core/command-service.ts +99 -97
  41. package/core/engine.ts +83 -1
  42. package/core/ports.ts +10 -0
  43. package/core/reactions.ts +533 -95
  44. package/core/slot-slug.ts +22 -0
  45. package/{adapters/claude/hooks → hooks}/buddy-comment.sh +22 -9
  46. package/hooks/file-type-react.sh +403 -0
  47. package/hooks/hooks.json +52 -0
  48. package/hooks/mood-react.sh +210 -0
  49. package/{adapters/claude/hooks → hooks}/name-react.sh +3 -4
  50. package/hooks/react.sh +1364 -0
  51. package/hooks/suggest.sh +55 -0
  52. package/package.json +70 -31
  53. package/scripts/gen-emoji-widths.ts +34 -0
  54. package/scripts/paths.sh +33 -2
  55. package/scripts/postinstall.cjs +42 -0
  56. package/scripts/snapshot-statusline.sh +54 -0
  57. package/server/achievements.ts +1419 -15
  58. package/server/art.ts +450 -1
  59. package/server/award-xp.ts +48 -0
  60. package/server/check-suggestions.ts +102 -0
  61. package/server/consolidate.ts +33 -0
  62. package/{adapters/claude/server → server}/index.ts +556 -95
  63. package/server/mcp-launcher.sh +16 -6
  64. package/server/memory.ts +404 -0
  65. package/server/mood.ts +254 -0
  66. package/server/path.ts +70 -18
  67. package/server/shift-mood.ts +43 -0
  68. package/server/state.ts +552 -3
  69. package/server/suggestions.ts +335 -0
  70. package/server/theme.ts +53 -0
  71. package/server/xp.ts +435 -0
  72. package/{adapters/claude/skills → skills}/buddy/SKILL.md +36 -7
  73. package/statusline/buddy-status.sh +402 -0
  74. package/statusline/combined-status.sh +135 -0
  75. package/statusline/emoji-widths.data +4 -0
  76. package/adapters/claude/hooks/hooks.json +0 -35
  77. package/adapters/claude/hooks/react.sh +0 -207
  78. package/adapters/claude/install/doctor.ts +0 -172
  79. package/adapters/claude/plugin/marketplace.json +0 -40
  80. package/adapters/claude/plugin/plugin.json +0 -26
  81. package/adapters/claude/rendering/art.ts +0 -264
  82. package/adapters/claude/server/instructions.ts +0 -24
  83. package/adapters/claude/server/path.ts +0 -12
  84. package/adapters/claude/server/resources.ts +0 -38
  85. package/adapters/claude/statusline/buddy-status.sh +0 -408
  86. package/adapters/claude/storage/achievements.ts +0 -253
  87. package/adapters/claude/storage/identity.ts +0 -13
  88. package/adapters/claude/storage/paths.ts +0 -33
  89. package/adapters/claude/storage/settings.ts +0 -91
  90. package/adapters/claude/storage/state.ts +0 -365
  91. package/adapters/pi/README.md +0 -83
  92. package/adapters/pi/tools.ts +0 -6
  93. package/server/engine.ts +0 -1
  94. package/server/reactions.ts +0 -1
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * claude-buddy MCP server
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
- generateBones,
16
- renderFace,
17
- SPECIES,
18
- RARITIES,
19
- STAT_NAMES,
20
- RARITY_STARS,
21
- type Species,
22
- type Rarity,
23
- type StatName,
24
- type Companion,
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
- } from "../storage/state.ts";
43
- import { resolveUserId } from "../storage/identity.ts";
44
- import { cleanupPluginState, setBuddyStatusLine, unsetBuddyStatusLine } from "../storage/settings.ts";
42
+ setBuddyStatusLine,
43
+ unsetBuddyStatusLine,
44
+ cleanupPluginState,
45
+ } from "./state";
45
46
  import {
46
47
  buddyStateDir,
47
48
  claudeConfigDir,
48
49
  claudeSettingsPath,
49
- } from "./path.ts";
50
- import {
51
- getReaction, generatePersonalityPrompt,
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
- loadEvents,
58
- loadUnlocked,
59
- saveUnlocked,
60
- } from "../storage/achievements.ts";
61
- import { BuddyCommandService } from "../../../core/command-service.ts";
62
- import { getInstructions } from "./instructions.ts";
63
- import { buildPromptResource } from "./resources.ts";
64
-
65
- const buddyService = new BuddyCommandService({
66
- identity: { getStableUserId: () => resolveUserId() },
67
- buddies: {
68
- loadActive: () => loadCompanion(),
69
- saveActive: (companion) => saveCompanion(companion),
70
- loadSlot: (slot) => loadCompanionSlot(slot),
71
- saveSlot: (slot, companion) => saveCompanionSlot(companion, slot),
72
- deleteSlot: (slot) => deleteCompanionSlot(slot),
73
- listSlots: () => listCompanionSlots(),
74
- loadActiveSlot: () => loadActiveSlot(),
75
- saveActiveSlot: (slot) => saveActiveSlot(slot),
76
- },
77
- reactions: {
78
- loadLatest: () => loadReaction(),
79
- saveLatest: (reaction) => saveReaction(reaction.reaction, reaction.reason),
80
- },
81
- config: {
82
- loadConfig: () => loadConfig(),
83
- saveConfig: (config) => saveConfig(config),
84
- },
85
- events: {
86
- loadCounters: (scope) => loadEvents(scope),
87
- increment: (key, amount, scope) => incrementEvent(key, amount, scope),
88
- loadUnlocked: () => loadUnlocked(),
89
- saveUnlocked: (unlocked) => saveUnlocked(unlocked),
90
- trackActiveDay: () => trackActiveDay(),
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: "claude-buddy",
97
- version: "0.4.0-alpha.7",
120
+ name: "coding-buddy",
121
+ version: "0.6.0",
98
122
  },
99
123
  {
100
- instructions: getInstructions(buddyService.loadActiveCompanion()),
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 = buddyService.getStableUserId();
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: `A ${bones.rarity} ${bones.species} who watches code with quiet intensity.`,
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 = buddyService.loadLatestReaction();
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(["error", "test-fail", "large-diff", "turn"])
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
- "claude-buddy commands",
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> to change.`,
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: Record<string, string | boolean> = {};
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
- return { content: [{ type: "text", text: `${companion.name} is back!` }] };
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. When enabled, configures Claude Code's status line to show your buddy with animation and reactions. When disabled, the status line is released for other use. Returns current status if called without arguments.",
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 or /buddy statusline off to change.\nRestart Claude Code after enabling for it to take effect.`,
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 (enabled) {
501
- const projectRoot = resolve(import.meta.dir, "../../..");
502
- const statusScript = join(projectRoot, "adapters", "claude", "statusline", "buddy-status.sh");
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
- "Status line enabled! Restart Claude Code to see your buddy in the status line.\n\n" +
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 claude-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.",
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", "claude-buddy");
706
+ const pluginsCacheDir = join(claudeConfigDir(), "plugins", "cache", "coding-buddy");
541
707
 
542
708
  const lines: string[] = [];
543
- lines.push("claude-buddy: settings.json cleanup complete.");
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 claude-buddy@claude-buddy");
563
- lines.push(" claude plugin marketplace remove claude-buddy");
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 = buddyService.getStableUserId();
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: `A ${bones.rarity} ${bones.species} who watches code with quiet intensity.`,
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 = buildPromptResource(companion);
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: [