@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.
Files changed (92) hide show
  1. package/{adapters/claude/plugin → .claude-plugin}/marketplace.json +11 -6
  2. package/{adapters/claude/plugin → .claude-plugin}/plugin.json +9 -7
  3. package/LICENSE +1 -1
  4. package/README.md +102 -72
  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 +59 -44
  22. package/cli/biomes.ts +309 -0
  23. package/cli/buddy-shell.ts +818 -0
  24. package/{adapters/claude/install → cli}/disable.ts +14 -32
  25. package/{adapters/claude/install → cli}/doctor.ts +55 -57
  26. package/{adapters/claude/install → cli}/hunt.ts +7 -9
  27. package/cli/index.ts +36 -25
  28. package/{adapters/claude/install → cli}/install.ts +100 -145
  29. package/{adapters/claude/install → cli}/pick.ts +111 -50
  30. package/{adapters/claude/install → cli}/settings.ts +2 -2
  31. package/{adapters/claude/install → cli}/show.ts +4 -4
  32. package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
  33. package/{adapters/claude/install → cli}/test-statusline.ts +11 -11
  34. package/cli/tui.tsx +2333 -0
  35. package/{adapters/claude/install → cli}/uninstall.ts +23 -38
  36. package/cli/upgrade.ts +213 -0
  37. package/cli/validate-species.ts +209 -0
  38. package/cli/verify.ts +3 -3
  39. package/core/achievements.ts +12 -20
  40. package/core/art-data.ts +26 -0
  41. package/core/command-service.ts +99 -97
  42. package/core/engine.ts +83 -1
  43. package/core/ports.ts +10 -0
  44. package/core/reactions.ts +533 -95
  45. package/core/slot-slug.ts +22 -0
  46. package/{adapters/claude/hooks → hooks}/buddy-comment.sh +23 -2
  47. package/hooks/file-type-react.sh +401 -0
  48. package/hooks/hooks.json +52 -0
  49. package/hooks/mood-react.sh +208 -0
  50. package/{adapters/claude/hooks → hooks}/name-react.sh +5 -2
  51. package/hooks/react.sh +1368 -0
  52. package/hooks/suggest.sh +55 -0
  53. package/package.json +113 -59
  54. package/scripts/gen-emoji-widths.ts +34 -0
  55. package/scripts/paths.sh +56 -0
  56. package/scripts/postinstall.cjs +42 -0
  57. package/scripts/snapshot-statusline.sh +54 -0
  58. package/server/achievements.ts +1419 -0
  59. package/server/art.ts +450 -0
  60. package/server/award-xp.ts +48 -0
  61. package/server/check-suggestions.ts +102 -0
  62. package/server/consolidate.ts +33 -0
  63. package/server/index.ts +1340 -0
  64. package/server/mcp-launcher.sh +26 -0
  65. package/server/memory.ts +404 -0
  66. package/server/mood.ts +254 -0
  67. package/server/path.ts +82 -0
  68. package/server/shift-mood.ts +43 -0
  69. package/{adapters/claude/storage → server}/state.ts +200 -7
  70. package/server/suggestions.ts +335 -0
  71. package/server/theme.ts +53 -0
  72. package/server/xp.ts +435 -0
  73. package/{adapters/claude/skills → skills}/buddy/SKILL.md +48 -4
  74. package/statusline/buddy-status.sh +397 -0
  75. package/statusline/combined-status.sh +134 -0
  76. package/statusline/emoji-widths.data +4 -0
  77. package/adapters/claude/hooks/hooks.json +0 -35
  78. package/adapters/claude/hooks/react.sh +0 -204
  79. package/adapters/claude/popup/buddy-popup.sh +0 -92
  80. package/adapters/claude/popup/buddy-render.sh +0 -540
  81. package/adapters/claude/popup/popup-manager.sh +0 -355
  82. package/adapters/claude/rendering/art.ts +0 -264
  83. package/adapters/claude/server/index.ts +0 -752
  84. package/adapters/claude/server/instructions.ts +0 -24
  85. package/adapters/claude/server/resources.ts +0 -38
  86. package/adapters/claude/statusline/buddy-status.sh +0 -389
  87. package/adapters/claude/storage/achievements.ts +0 -253
  88. package/adapters/claude/storage/identity.ts +0 -13
  89. package/adapters/claude/storage/paths.ts +0 -24
  90. package/adapters/claude/storage/settings.ts +0 -41
  91. package/adapters/pi/README.md +0 -83
  92. package/adapters/pi/tools.ts +0 -6
@@ -0,0 +1,60 @@
1
+ import type { Companion } from "../../core/model.ts";
2
+
3
+ export function buildBuddyReactionSystemPrompt(companion: Companion): string {
4
+ return [
5
+ `A small ${companion.bones.rarity} ${companion.bones.species} named ${companion.name} watches from the status line. You are not ${companion.name} — it is a separate creature.`,
6
+ `Personality: ${companion.personality}`,
7
+ `Peak stat: ${companion.bones.peak} (${companion.bones.stats[companion.bones.peak]}). Dump stat: ${companion.bones.dump} (${companion.bones.stats[companion.bones.dump]}).`,
8
+ "",
9
+ `Write as ${companion.name} (a ${companion.bones.species}), not as the assistant.`,
10
+ "Reference something SPECIFIC from this turn — a pitfall, compliment, warning, pattern, file, test, or edge case.",
11
+ "Return exactly one short sentence, max 150 chars.",
12
+ "Use *asterisks* for physical actions when useful.",
13
+ "Do not explain yourself. Do not use markdown fences. Output only the buddy reaction line.",
14
+ ].join("\n");
15
+ }
16
+
17
+ export function buildBuddyReactionPrompt(
18
+ companion: Companion,
19
+ assistantText: string,
20
+ toolText: string,
21
+ userText: string,
22
+ ): string {
23
+ return [
24
+ `You are generating a single end-of-turn buddy reaction for ${companion.name}, a ${companion.bones.rarity} ${companion.bones.species}.`,
25
+ `Buddy personality: ${companion.personality}`,
26
+ `Peak stat: ${companion.bones.peak} (${companion.bones.stats[companion.bones.peak]}). Dump stat: ${companion.bones.dump} (${companion.bones.stats[companion.bones.dump]}).`,
27
+ "",
28
+ "Write exactly one short in-character reaction sentence as the buddy.",
29
+ "Rules:",
30
+ `- Write as ${companion.name}, not as the assistant.`,
31
+ "- Reference something specific from the turn.",
32
+ "- Max 150 characters.",
33
+ "- Use *asterisks* for physical actions when useful.",
34
+ "- Output only the reaction line, with no quotes, labels, markdown fences, or explanation.",
35
+ "",
36
+ "User prompt:",
37
+ userText || "(unknown)",
38
+ "",
39
+ "Assistant response:",
40
+ assistantText || "(empty)",
41
+ "",
42
+ "Tool result summary:",
43
+ toolText || "(none)",
44
+ ].join("\n");
45
+ }
46
+
47
+ export function stripBuddyComments(text: string): string {
48
+ return text.replace(/<!--\s*buddy:\s*[\s\S]*?-->/gi, "").trimEnd();
49
+ }
50
+
51
+ export function normalizeBuddyComment(text: string): string {
52
+ return text
53
+ .replace(/<!--\s*buddy:\s*([\s\S]*?)\s*-->/gi, "$1")
54
+ .replace(/^buddy\s*:\s*/i, "")
55
+ .replace(/^['"`]+|['"`]+$/g, "")
56
+ .replace(/\s+/g, " ")
57
+ .trim()
58
+ .slice(0, 150)
59
+ .trim();
60
+ }
@@ -0,0 +1,73 @@
1
+ import { RARITY_STARS } from "../../core/engine.ts";
2
+ import { getArtFrame, HAT_ART } from "../../core/render-model.ts";
3
+ import type { Companion, ReactionState } from "../../core/model.ts";
4
+ import type { Achievement } from "../../core/achievements.ts";
5
+
6
+ function trimArt(line: string): string {
7
+ return line.replace(/\s+$/g, "");
8
+ }
9
+
10
+ export function renderBuddyStatus(companion: Companion, reaction?: ReactionState | null): string {
11
+ const shiny = companion.bones.shiny ? " ✨" : "";
12
+ const stars = RARITY_STARS[companion.bones.rarity];
13
+ const suffix = reaction?.reaction ? ` — ${reaction.reaction}` : "";
14
+ return `${trimArt(getArtFrame(companion.bones.species, companion.bones.eye, Date.now())[0])} ${companion.name} ${stars}${shiny}${suffix}`;
15
+ }
16
+
17
+ export function renderBuddyWidget(
18
+ companion: Companion,
19
+ reaction?: ReactionState | null,
20
+ achievements: Achievement[] = [],
21
+ ): string[] {
22
+ const frame = Math.floor(Date.now() / 700);
23
+ const art = getArtFrame(companion.bones.species, companion.bones.eye, frame).map(trimArt);
24
+ const hat = companion.bones.hat === "none" ? [] : [trimArt(HAT_ART[companion.bones.hat])];
25
+ const stars = RARITY_STARS[companion.bones.rarity];
26
+ const shiny = companion.bones.shiny ? " ✨" : "";
27
+ const lines = [
28
+ `buddy · ${companion.name}`,
29
+ `${companion.bones.rarity} ${companion.bones.species} ${stars}${shiny}`,
30
+ "",
31
+ ...hat,
32
+ ...art,
33
+ ];
34
+
35
+ if (reaction?.reaction) {
36
+ lines.push("", `“${reaction.reaction}”`);
37
+ }
38
+
39
+ if (achievements.length > 0) {
40
+ lines.push("", ...achievements.map((achievement) => `🏆 ${achievement.name}`));
41
+ }
42
+
43
+ return lines;
44
+ }
45
+
46
+ export function renderBuddyStats(companion: Companion): string[] {
47
+ return [
48
+ `name: ${companion.name}`,
49
+ `species: ${companion.bones.species}`,
50
+ `rarity: ${companion.bones.rarity}`,
51
+ `peak: ${companion.bones.peak}`,
52
+ `dump: ${companion.bones.dump}`,
53
+ "",
54
+ ...Object.entries(companion.bones.stats).map(([stat, value]) => `${stat.padEnd(9)} ${String(value).padStart(3)}`),
55
+ ];
56
+ }
57
+
58
+ export function renderAchievementsSummary(
59
+ unlocked: Array<{ achievement: Achievement; unlockedAt: number; slot?: string }>,
60
+ remaining: Achievement[],
61
+ ): string[] {
62
+ const lines = [`achievements unlocked: ${unlocked.length}`];
63
+
64
+ if (unlocked.length > 0) {
65
+ lines.push("", ...unlocked.map(({ achievement }) => `${achievement.icon} ${achievement.name}`));
66
+ }
67
+
68
+ if (remaining.length > 0) {
69
+ lines.push("", `next up: ${remaining[0].icon} ${remaining[0].name}`);
70
+ }
71
+
72
+ return lines;
73
+ }
@@ -0,0 +1,25 @@
1
+ import { getAgentDir } from "@oh-my-pi/pi-utils";
2
+ import { join } from "node:path";
3
+ import {
4
+ FileBuddyStorage,
5
+ slugifySlot,
6
+ type FileBuddyConfig,
7
+ } from "../shared/file-storage.ts";
8
+
9
+ export const DEFAULT_OMP_BUDDY_STATE_DIR = join(getAgentDir(), "buddy");
10
+
11
+ export class OmpBuddyStorage extends FileBuddyStorage {
12
+ constructor(stateDir = DEFAULT_OMP_BUDDY_STATE_DIR) {
13
+ super(stateDir);
14
+ }
15
+
16
+ loadOmpConfig(): FileBuddyConfig {
17
+ return this.loadHostConfig();
18
+ }
19
+
20
+ saveOmpConfig(config: Partial<FileBuddyConfig>): FileBuddyConfig {
21
+ return this.saveHostConfig(config);
22
+ }
23
+ }
24
+
25
+ export { slugifySlot };
@@ -0,0 +1,85 @@
1
+ import type { OmpBuddyUiContext } from "./context.ts";
2
+ import type { Achievement } from "../../core/achievements.ts";
3
+ import type { Companion, ReactionState } from "../../core/model.ts";
4
+ import { renderAchievementsSummary, renderBuddyStatus, renderBuddyWidget } from "./renderers.ts";
5
+ import { OmpBuddyStorage } from "./storage.ts";
6
+
7
+ export class OmpBuddyUI {
8
+ private readonly timers: Array<{ timer: Timer; clear: (timer: Timer) => void }> = [];
9
+
10
+ constructor(private readonly storage: OmpBuddyStorage) {}
11
+
12
+ cancelTimers(): void {
13
+ for (const { timer, clear } of this.timers.splice(0)) {
14
+ try {
15
+ clear(timer);
16
+ } catch {
17
+ // Ignore cleanup errors.
18
+ }
19
+ }
20
+ }
21
+
22
+ refresh(
23
+ ctx: OmpBuddyUiContext,
24
+ companion: Companion,
25
+ reaction?: ReactionState | null,
26
+ achievements: Achievement[] = [],
27
+ ): void {
28
+ const currentReaction = reaction ?? null;
29
+ const muted = this.storage.isMuted();
30
+ const status = renderBuddyStatus(companion, muted ? null : currentReaction);
31
+ ctx.ui.setStatus("buddy", muted ? `${status} [muted]` : status);
32
+ ctx.ui.setWidget(
33
+ "buddy",
34
+ renderBuddyWidget(companion, muted ? null : currentReaction, achievements),
35
+ );
36
+
37
+ this.cancelTimers();
38
+ if (!currentReaction) return;
39
+
40
+ const ttl = this.storage.loadConfig().reactionTTL;
41
+ if (ttl <= 0) return;
42
+
43
+ const scheduler = getScheduler(ctx);
44
+ if (!scheduler) return;
45
+
46
+ const reactionTimestamp = currentReaction.timestamp;
47
+ const delay = Math.max(0, reactionTimestamp + ttl * 1000 + 1 - Date.now());
48
+ const timer = scheduler.setTimeout(() => {
49
+ const latest = this.storage.loadLatest();
50
+ if (latest && latest.timestamp >= reactionTimestamp) {
51
+ this.refresh(ctx, companion, latest, []);
52
+ } else {
53
+ this.refresh(ctx, companion, null, []);
54
+ }
55
+ }, delay);
56
+ this.timers.push({ timer, clear: scheduler.clearTimer });
57
+ }
58
+
59
+ notifyAchievements(ctx: OmpBuddyUiContext, achievements: Achievement[]): void {
60
+ for (const achievement of achievements) {
61
+ ctx.ui.notify(`Unlocked: ${achievement.icon} ${achievement.name}`, "info");
62
+ }
63
+ }
64
+
65
+ showAchievements(
66
+ ctx: OmpBuddyUiContext,
67
+ unlocked: Array<{ achievement: Achievement; unlockedAt: number; slot?: string }>,
68
+ remaining: Achievement[],
69
+ ): void {
70
+ ctx.ui.setWidget("buddy", renderAchievementsSummary(unlocked, remaining));
71
+ ctx.ui.notify(`Achievements: ${unlocked.length} unlocked`, "info");
72
+ }
73
+ }
74
+
75
+ function getScheduler(
76
+ ctx: OmpBuddyUiContext,
77
+ ): { setTimeout: (callback: () => void, ms?: number) => Timer; clearTimer: (timer: Timer) => void } | null {
78
+ if (typeof ctx.setTimeout === "function" && typeof ctx.clearTimer === "function") {
79
+ return { setTimeout: ctx.setTimeout, clearTimer: ctx.clearTimer };
80
+ }
81
+ if (typeof globalThis.setTimeout === "function" && typeof globalThis.clearTimeout === "function") {
82
+ return { setTimeout: globalThis.setTimeout, clearTimer: globalThis.clearTimeout };
83
+ }
84
+ return null;
85
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
2
- import { BuddyCommandService } from "../../core/command-service.ts";
2
+ import { BuddyCommandService, mergeAchievements } from "../../core/command-service.ts";
3
3
  import { renderBuddyStats } from "./renderers.ts";
4
4
  import { PiBuddyStorage, slugifySlot } from "./storage.ts";
5
5
  import { PiBuddyUI } from "./ui.ts";
@@ -12,7 +12,7 @@ interface RegisterBuddyCommandsDeps {
12
12
 
13
13
  export function registerBuddyCommands(pi: ExtensionAPI, deps: RegisterBuddyCommandsDeps): void {
14
14
  pi.registerCommand("buddy", {
15
- description: "Manage your coding buddy",
15
+ description: "Manage your coding companion",
16
16
  handler: async (args: string, ctx: ExtensionCommandContext) => {
17
17
  const input = args.trim();
18
18
  const [command, ...rest] = input ? input.split(/\s+/) : [];
@@ -22,128 +22,154 @@ export function registerBuddyCommands(pi: ExtensionAPI, deps: RegisterBuddyComma
22
22
  switch (command) {
23
23
  case undefined:
24
24
  case "show": {
25
- const result = deps.service.ensureCompanion();
26
- deps.service.incrementCommandsRun();
25
+ const ensured = deps.service.ensureCompanion();
26
+ const commandAchievements = deps.service.incrementCommandsRun();
27
+ const achievements = mergeAchievements(ensured.achievements, commandAchievements);
27
28
  const reaction = deps.storage.loadLatest();
28
- deps.ui.refresh(ctx, result.companion, reaction, result.achievements);
29
- if (result.created) {
30
- ctx.ui.notify(`Meet ${result.companion.name}!`, "info");
29
+ deps.ui.refresh(ctx, ensured.companion, reaction, achievements);
30
+ if (ensured.created) {
31
+ ctx.ui.notify(`Meet ${ensured.companion.name}!`, "info");
31
32
  } else {
32
- ctx.ui.notify(deps.service.formatCompanionSummary(result.companion), "info");
33
+ ctx.ui.notify(deps.service.formatCompanionSummary(ensured.companion), "info");
33
34
  }
35
+ deps.ui.notifyAchievements(ctx, achievements);
34
36
  return;
35
37
  }
36
38
 
37
39
  case "pet": {
38
- deps.service.incrementCommandsRun();
40
+ const commandAchievements = deps.service.incrementCommandsRun();
39
41
  const result = deps.service.petBuddy();
40
- deps.ui.refresh(ctx, result.companion, result.state, result.achievements);
42
+ const achievements = mergeAchievements(commandAchievements, result.achievements);
43
+ deps.ui.refresh(ctx, result.companion, result.state, achievements);
41
44
  ctx.ui.notify(`${result.companion.name} seems pleased.`, "info");
42
- deps.ui.notifyAchievements(ctx, result.achievements);
45
+ deps.ui.notifyAchievements(ctx, achievements);
43
46
  return;
44
47
  }
45
48
 
46
49
  case "stats": {
47
- deps.service.incrementCommandsRun();
48
- const result = deps.service.ensureCompanion();
49
- ctx.ui.setWidget("buddy", renderBuddyStats(result.companion));
50
- ctx.ui.notify(`${result.companion.name}'s stats`, "info");
50
+ const ensured = deps.service.ensureCompanion();
51
+ const commandAchievements = deps.service.incrementCommandsRun();
52
+ const achievements = mergeAchievements(ensured.achievements, commandAchievements);
53
+ ctx.ui.setWidget("buddy", renderBuddyStats(ensured.companion));
54
+ ctx.ui.notify(`${ensured.companion.name}'s stats`, "info");
55
+ deps.ui.notifyAchievements(ctx, achievements);
51
56
  return;
52
57
  }
53
58
 
54
59
  case "rename": {
55
60
  if (!remainder) throw new Error("Usage: /buddy rename <name>");
56
- deps.service.incrementCommandsRun();
61
+ const commandAchievements = deps.service.incrementCommandsRun();
57
62
  const companion = deps.service.renameBuddy(remainder);
58
- deps.ui.refresh(ctx, companion, deps.storage.loadLatest());
63
+ const ensured = deps.service.ensureCompanion();
64
+ const achievements = mergeAchievements(commandAchievements, ensured.achievements);
65
+ deps.ui.refresh(ctx, companion, deps.storage.loadLatest(), achievements);
59
66
  ctx.ui.notify(`Buddy renamed to ${companion.name}.`, "info");
67
+ deps.ui.notifyAchievements(ctx, achievements);
60
68
  return;
61
69
  }
62
70
 
63
71
  case "personality": {
64
72
  if (!remainder) throw new Error("Usage: /buddy personality <text>");
65
- deps.service.incrementCommandsRun();
73
+ const commandAchievements = deps.service.incrementCommandsRun();
66
74
  const companion = deps.service.setPersonality(remainder);
67
- deps.ui.refresh(ctx, companion, deps.storage.loadLatest());
75
+ const ensured = deps.service.ensureCompanion();
76
+ const achievements = mergeAchievements(commandAchievements, ensured.achievements);
77
+ deps.ui.refresh(ctx, companion, deps.storage.loadLatest(), achievements);
68
78
  ctx.ui.notify(`${companion.name}'s personality was updated.`, "info");
79
+ deps.ui.notifyAchievements(ctx, achievements);
69
80
  return;
70
81
  }
71
82
 
72
83
  case "off": {
73
- deps.service.incrementCommandsRun();
84
+ const commandAchievements = deps.service.incrementCommandsRun();
74
85
  deps.storage.setMuted(true);
75
- const result = deps.service.ensureCompanion();
76
- deps.ui.refresh(ctx, result.companion, null);
77
- ctx.ui.notify(`${result.companion.name} goes quiet.`, "info");
86
+ const ensured = deps.service.ensureCompanion();
87
+ const achievements = mergeAchievements(commandAchievements, ensured.achievements);
88
+ deps.ui.refresh(ctx, ensured.companion, null, achievements);
89
+ ctx.ui.notify(`${ensured.companion.name} goes quiet.`, "info");
90
+ deps.ui.notifyAchievements(ctx, achievements);
78
91
  return;
79
92
  }
80
93
 
81
94
  case "on": {
82
- deps.service.incrementCommandsRun();
95
+ const commandAchievements = deps.service.incrementCommandsRun();
83
96
  deps.storage.setMuted(false);
84
97
  const result = deps.service.recordComment("*stretches* I'm back!");
85
- deps.ui.refresh(ctx, result.companion, result.state, result.achievements);
98
+ const achievements = mergeAchievements(commandAchievements, result.achievements);
99
+ deps.ui.refresh(ctx, result.companion, result.state, achievements);
86
100
  ctx.ui.notify(`${result.companion.name} is back.`, "info");
87
- deps.ui.notifyAchievements(ctx, result.achievements);
101
+ deps.ui.notifyAchievements(ctx, achievements);
88
102
  return;
89
103
  }
90
104
 
91
105
  case "save": {
92
- deps.service.incrementCommandsRun();
106
+ const commandAchievements = deps.service.incrementCommandsRun();
93
107
  const result = deps.service.saveBuddy(remainder || undefined);
94
- deps.ui.refresh(ctx, result.companion, deps.storage.loadLatest());
108
+ const ensured = deps.service.ensureCompanion();
109
+ const achievements = mergeAchievements(commandAchievements, ensured.achievements);
110
+ deps.ui.refresh(ctx, result.companion, deps.storage.loadLatest(), achievements);
95
111
  ctx.ui.notify(`Saved ${result.companion.name} to [${result.slot}].`, "info");
112
+ deps.ui.notifyAchievements(ctx, achievements);
96
113
  return;
97
114
  }
98
115
 
99
116
  case "summon": {
100
- deps.service.incrementCommandsRun();
117
+ const commandAchievements = deps.service.incrementCommandsRun();
101
118
  const result = deps.service.summonBuddy(remainder || undefined);
102
119
  if (!result) throw new Error("No saved buddy found for that slot.");
103
- deps.ui.refresh(ctx, result.companion, deps.storage.loadLatest());
120
+ const ensured = deps.service.ensureCompanion();
121
+ const achievements = mergeAchievements(commandAchievements, ensured.achievements);
122
+ deps.ui.refresh(ctx, result.companion, deps.storage.loadLatest(), achievements);
104
123
  ctx.ui.notify(`${result.companion.name} arrives from [${result.slot}].`, "info");
124
+ deps.ui.notifyAchievements(ctx, achievements);
105
125
  return;
106
126
  }
107
127
 
108
128
  case "list": {
109
- deps.service.incrementCommandsRun();
129
+ const commandAchievements = deps.service.incrementCommandsRun();
110
130
  const active = deps.storage.loadActiveSlot();
111
131
  const companions = deps.service.listCompanions();
112
132
  if (companions.length === 0) {
113
133
  ctx.ui.notify("No saved buddies yet.", "info");
134
+ deps.ui.notifyAchievements(ctx, commandAchievements);
114
135
  return;
115
136
  }
116
137
  ctx.ui.setWidget(
117
138
  "buddy",
118
139
  companions.map(({ slot, companion }) => {
119
- const marker = slot === active ? " ← active" : "";
120
- return `${companion.name} [${slot}] — ${companion.bones.rarity} ${companion.bones.species}${marker}`;
140
+ const marker = slot === active ? " <- active" : "";
141
+ return `${companion.name} [${slot}] - ${companion.bones.rarity} ${companion.bones.species}${marker}`;
121
142
  }),
122
143
  );
123
144
  ctx.ui.notify(`Saved buddies: ${companions.length}`, "info");
145
+ deps.ui.notifyAchievements(ctx, commandAchievements);
124
146
  return;
125
147
  }
126
148
 
127
149
  case "dismiss": {
128
150
  if (!remainder) throw new Error("Usage: /buddy dismiss <slot>");
129
- deps.service.incrementCommandsRun();
151
+ const commandAchievements = deps.service.incrementCommandsRun();
130
152
  const result = deps.service.dismissBuddy(remainder);
131
- const active = deps.service.ensureCompanion();
132
- deps.ui.refresh(ctx, active.companion, deps.storage.loadLatest());
153
+ const ensured = deps.service.ensureCompanion();
154
+ const achievements = mergeAchievements(commandAchievements, ensured.achievements);
155
+ deps.ui.refresh(ctx, ensured.companion, deps.storage.loadLatest(), achievements);
133
156
  ctx.ui.notify(`Dismissed ${result.companion.name} [${result.slot}].`, "info");
157
+ deps.ui.notifyAchievements(ctx, achievements);
134
158
  return;
135
159
  }
136
160
 
137
161
  case "achievements": {
138
- deps.service.incrementCommandsRun();
162
+ const commandAchievements = deps.service.incrementCommandsRun();
139
163
  const activeSlot = deps.storage.loadActiveSlot() ?? undefined;
140
164
  const progress = deps.service.getAchievementProgress(activeSlot);
141
165
  deps.ui.showAchievements(ctx, progress.unlocked, progress.remaining);
166
+ deps.ui.notifyAchievements(ctx, commandAchievements);
142
167
  return;
143
168
  }
144
169
 
145
170
  case "help":
146
171
  default: {
172
+ const commandAchievements = deps.service.incrementCommandsRun();
147
173
  ctx.ui.setWidget("buddy", [
148
174
  "/buddy",
149
175
  "/buddy pet",
@@ -158,6 +184,7 @@ export function registerBuddyCommands(pi: ExtensionAPI, deps: RegisterBuddyComma
158
184
  "/buddy achievements",
159
185
  ]);
160
186
  ctx.ui.notify("Buddy commands ready.", "info");
187
+ deps.ui.notifyAchievements(ctx, commandAchievements);
161
188
  return;
162
189
  }
163
190
  }