@ramarivera/coding-buddy 0.4.0-alpha.9 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +45 -0
- package/.claude-plugin/plugin.json +30 -0
- package/LICENSE +1 -1
- package/README.md +96 -45
- package/adapters/omp/commands.ts +201 -0
- package/adapters/omp/context.ts +22 -0
- package/adapters/omp/events.ts +551 -0
- package/adapters/omp/identity.ts +10 -0
- package/adapters/omp/index.ts +39 -0
- package/adapters/omp/logger.ts +52 -0
- package/adapters/omp/prompt.ts +60 -0
- package/adapters/omp/renderers.ts +73 -0
- package/adapters/omp/storage.ts +25 -0
- package/adapters/omp/ui.ts +85 -0
- package/adapters/pi/commands.ts +64 -37
- package/adapters/pi/events.ts +147 -72
- package/adapters/pi/index.ts +9 -4
- package/adapters/pi/storage.ts +14 -285
- package/adapters/pi/ui.ts +56 -5
- package/adapters/shared/file-storage.ts +526 -0
- package/{adapters/claude/install → cli}/backup.ts +130 -33
- package/cli/buddy-shell.ts +1 -1
- package/{adapters/claude/install → cli}/disable.ts +19 -32
- package/cli/doctor.ts +231 -0
- package/{adapters/claude/install → cli}/hunt.ts +7 -9
- package/cli/index.ts +30 -26
- package/{adapters/claude/install → cli}/install.ts +65 -83
- package/{adapters/claude/install → cli}/pick.ts +136 -57
- package/{adapters/claude/install → cli}/settings.ts +2 -2
- package/{adapters/claude/install → cli}/show.ts +4 -4
- package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
- package/{adapters/claude/install → cli}/test-statusline.ts +21 -11
- package/cli/tui.tsx +106 -17
- package/{adapters/claude/install → cli}/uninstall.ts +40 -47
- package/cli/upgrade.ts +46 -13
- package/cli/validate-species.ts +209 -0
- package/cli/verify.ts +3 -3
- package/core/achievements.ts +12 -20
- package/core/art-data.ts +26 -0
- package/core/command-service.ts +99 -97
- package/core/engine.ts +83 -1
- package/core/ports.ts +10 -0
- package/core/reactions.ts +533 -95
- package/core/slot-slug.ts +22 -0
- package/{adapters/claude/hooks → hooks}/buddy-comment.sh +22 -9
- package/hooks/file-type-react.sh +403 -0
- package/hooks/hooks.json +52 -0
- package/hooks/mood-react.sh +210 -0
- package/{adapters/claude/hooks → hooks}/name-react.sh +3 -4
- package/hooks/react.sh +1364 -0
- package/hooks/suggest.sh +55 -0
- package/package.json +70 -31
- package/scripts/gen-emoji-widths.ts +34 -0
- package/scripts/paths.sh +33 -2
- package/scripts/postinstall.cjs +42 -0
- package/scripts/snapshot-statusline.sh +54 -0
- package/server/achievements.ts +1419 -15
- package/server/art.ts +450 -1
- package/server/award-xp.ts +48 -0
- package/server/check-suggestions.ts +102 -0
- package/server/consolidate.ts +33 -0
- package/{adapters/claude/server → server}/index.ts +556 -95
- package/server/mcp-launcher.sh +16 -6
- package/server/memory.ts +404 -0
- package/server/mood.ts +254 -0
- package/server/path.ts +70 -18
- package/server/shift-mood.ts +43 -0
- package/server/state.ts +552 -3
- package/server/suggestions.ts +335 -0
- package/server/theme.ts +53 -0
- package/server/xp.ts +435 -0
- package/{adapters/claude/skills → skills}/buddy/SKILL.md +36 -7
- package/statusline/buddy-status.sh +402 -0
- package/statusline/combined-status.sh +135 -0
- package/statusline/emoji-widths.data +4 -0
- package/adapters/claude/hooks/hooks.json +0 -35
- package/adapters/claude/hooks/react.sh +0 -207
- package/adapters/claude/install/doctor.ts +0 -172
- package/adapters/claude/plugin/marketplace.json +0 -40
- package/adapters/claude/plugin/plugin.json +0 -26
- package/adapters/claude/rendering/art.ts +0 -264
- package/adapters/claude/server/instructions.ts +0 -24
- package/adapters/claude/server/path.ts +0 -12
- package/adapters/claude/server/resources.ts +0 -38
- package/adapters/claude/statusline/buddy-status.sh +0 -408
- package/adapters/claude/storage/achievements.ts +0 -253
- package/adapters/claude/storage/identity.ts +0 -13
- package/adapters/claude/storage/paths.ts +0 -33
- package/adapters/claude/storage/settings.ts +0 -91
- package/adapters/claude/storage/state.ts +0 -365
- package/adapters/pi/README.md +0 -83
- package/adapters/pi/tools.ts +0 -6
- package/server/engine.ts +0 -1
- package/server/reactions.ts +0 -1
|
@@ -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
|
+
}
|
package/adapters/pi/commands.ts
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
29
|
-
if (
|
|
30
|
-
ctx.ui.notify(`Meet ${
|
|
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(
|
|
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
|
-
|
|
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,
|
|
45
|
+
deps.ui.notifyAchievements(ctx, achievements);
|
|
43
46
|
return;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
case "stats": {
|
|
47
|
-
deps.service.
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
ctx.ui.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 ? "
|
|
120
|
-
return `${companion.name} [${slot}]
|
|
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
|
|
132
|
-
|
|
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
|
}
|