@ramarivera/coding-buddy 0.4.0-alpha.2 → 0.4.0-alpha.4
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/README.md +4 -5
- package/{hooks → adapters/claude/hooks}/hooks.json +3 -3
- package/{cli → adapters/claude/install}/backup.ts +10 -9
- package/{cli → adapters/claude/install}/disable.ts +27 -6
- package/{cli → adapters/claude/install}/doctor.ts +44 -31
- package/{cli → adapters/claude/install}/hunt.ts +4 -4
- package/{cli → adapters/claude/install}/install.ts +81 -42
- package/{cli → adapters/claude/install}/pick.ts +3 -3
- package/{cli → adapters/claude/install}/settings.ts +1 -1
- package/{cli → adapters/claude/install}/show.ts +2 -2
- package/{cli → adapters/claude/install}/test-statusline.ts +3 -2
- package/{cli → adapters/claude/install}/uninstall.ts +31 -10
- package/{.claude-plugin → adapters/claude/plugin}/plugin.json +1 -1
- package/adapters/claude/popup/buddy-popup.sh +92 -0
- package/adapters/claude/popup/buddy-render.sh +540 -0
- package/adapters/claude/popup/popup-manager.sh +355 -0
- package/{server → adapters/claude/rendering}/art.ts +3 -115
- package/{server → adapters/claude/server}/index.ts +49 -71
- package/adapters/claude/server/instructions.ts +24 -0
- package/adapters/claude/server/resources.ts +38 -0
- package/adapters/claude/storage/achievements.ts +253 -0
- package/adapters/claude/storage/identity.ts +13 -0
- package/adapters/claude/storage/paths.ts +24 -0
- package/adapters/claude/storage/settings.ts +41 -0
- package/{server → adapters/claude/storage}/state.ts +3 -65
- package/adapters/pi/README.md +64 -0
- package/adapters/pi/commands.ts +173 -0
- package/adapters/pi/events.ts +150 -0
- package/adapters/pi/identity.ts +10 -0
- package/adapters/pi/index.ts +25 -0
- package/adapters/pi/renderers.ts +73 -0
- package/adapters/pi/storage.ts +295 -0
- package/adapters/pi/tools.ts +6 -0
- package/adapters/pi/ui.ts +39 -0
- package/cli/index.ts +11 -11
- package/cli/verify.ts +2 -2
- package/core/achievements.ts +203 -0
- package/core/art-data.ts +105 -0
- package/core/command-service.ts +338 -0
- package/core/model.ts +59 -0
- package/core/ports.ts +40 -0
- package/core/render-model.ts +10 -0
- package/package.json +23 -19
- package/server/achievements.ts +0 -445
- /package/{hooks → adapters/claude/hooks}/buddy-comment.sh +0 -0
- /package/{hooks → adapters/claude/hooks}/name-react.sh +0 -0
- /package/{hooks → adapters/claude/hooks}/react.sh +0 -0
- /package/{cli → adapters/claude/install}/test-statusline.sh +0 -0
- /package/{.claude-plugin → adapters/claude/plugin}/marketplace.json +0 -0
- /package/{skills → adapters/claude/skills}/buddy/SKILL.md +0 -0
- /package/{statusline → adapters/claude/statusline}/buddy-status.sh +0 -0
- /package/{server → core}/engine.ts +0 -0
- /package/{server → core}/reactions.ts +0 -0
package/core/art-data.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Species, Hat } from "./engine.ts";
|
|
2
|
+
|
|
3
|
+
export const SPECIES_ART: Record<Species, string[][]> = {
|
|
4
|
+
duck: [
|
|
5
|
+
[" ", " __ ", " <({E} )___ ", " ( ._> ", " `--' "],
|
|
6
|
+
[" ", " __ ", " <({E} )___ ", " ( ._> ", " `--'~ "],
|
|
7
|
+
[" ", " __ ", " <({E} )___ ", " ( .__> ", " `--' "],
|
|
8
|
+
],
|
|
9
|
+
goose: [
|
|
10
|
+
[" ", " ({E}> ", " || ", " _(__)_ ", " ^^^^ "],
|
|
11
|
+
[" ", " ({E}> ", " || ", " _(__)_ ", " ^^^^ "],
|
|
12
|
+
[" ", " ({E}>> ", " || ", " _(__)_ ", " ^^^^ "],
|
|
13
|
+
],
|
|
14
|
+
blob: [
|
|
15
|
+
[" ", " .----. ", " ( {E} {E} ) ", " ( ) ", " `----' "],
|
|
16
|
+
[" ", " .------. ", " ( {E} {E} ) ", " ( ) ", " `------' "],
|
|
17
|
+
[" ", " .--. ", " ({E} {E}) ", " ( ) ", " `--' "],
|
|
18
|
+
],
|
|
19
|
+
cat: [
|
|
20
|
+
[" ", " /\\_/\\ ", " ( {E} {E}) ", " ( \u03c9 ) ", " (\")_(\") "],
|
|
21
|
+
[" ", " /\\_/\\ ", " ( {E} {E}) ", " ( \u03c9 ) ", " (\")_(\")~ "],
|
|
22
|
+
[" ", " /\\-/\\ ", " ( {E} {E}) ", " ( \u03c9 ) ", " (\")_(\") "],
|
|
23
|
+
],
|
|
24
|
+
dragon: [
|
|
25
|
+
[" ", " /^\\ /^\\ ", " < {E} {E} > ", " ( ~~ ) ", " `-vvvv-' "],
|
|
26
|
+
[" ", " /^\\ /^\\ ", " < {E} {E} > ", " ( ) ", " `-vvvv-' "],
|
|
27
|
+
[" ~ ~ ", " /^\\ /^\\ ", " < {E} {E} > ", " ( ~~ ) ", " `-vvvv-' "],
|
|
28
|
+
],
|
|
29
|
+
octopus: [
|
|
30
|
+
[" ", " .----. ", " ( {E} {E} ) ", " (______) ", " /\\/\\/\\/\\ "],
|
|
31
|
+
[" ", " .----. ", " ( {E} {E} ) ", " (______) ", " \\/\\/\\/\\/ "],
|
|
32
|
+
[" o ", " .----. ", " ( {E} {E} ) ", " (______) ", " /\\/\\/\\/\\ "],
|
|
33
|
+
],
|
|
34
|
+
owl: [
|
|
35
|
+
[" ", " /\\ /\\ ", " (({E})({E})) ", " ( >< ) ", " `----' "],
|
|
36
|
+
[" ", " /\\ /\\ ", " (({E})({E})) ", " ( >< ) ", " .----. "],
|
|
37
|
+
[" ", " /\\ /\\ ", " (({E})(-)) ", " ( >< ) ", " `----' "],
|
|
38
|
+
],
|
|
39
|
+
penguin: [
|
|
40
|
+
[" ", " .---. ", " ({E}>{E}) ", " /( )\\ ", " `---' "],
|
|
41
|
+
[" ", " .---. ", " ({E}>{E}) ", " |( )| ", " `---' "],
|
|
42
|
+
[" .---. ", " ({E}>{E}) ", " /( )\\ ", " `---' ", " ~ ~ "],
|
|
43
|
+
],
|
|
44
|
+
turtle: [
|
|
45
|
+
[" ", " _,--._ ", " ( {E} {E} ) ", " /[______]\\ ", " `` `` "],
|
|
46
|
+
[" ", " _,--._ ", " ( {E} {E} ) ", " /[______]\\ ", " `` `` "],
|
|
47
|
+
[" ", " _,--._ ", " ( {E} {E} ) ", " /[======]\\ ", " `` `` "],
|
|
48
|
+
],
|
|
49
|
+
snail: [
|
|
50
|
+
[" ", " {E} .--. ", " \\ ( @ ) ", " \\_`--' ", " ~~~~~~~ "],
|
|
51
|
+
[" ", " {E} .--. ", " | ( @ ) ", " \\_`--' ", " ~~~~~~~ "],
|
|
52
|
+
[" ", " {E} .--. ", " \\ ( @ ) ", " \\_`--' ", " ~~~~~~ "],
|
|
53
|
+
],
|
|
54
|
+
ghost: [
|
|
55
|
+
[" ", " .----. ", " / {E} {E} \\ ", " | | ", " ~`~``~`~ "],
|
|
56
|
+
[" ", " .----. ", " / {E} {E} \\ ", " | | ", " `~`~~`~` "],
|
|
57
|
+
[" ~ ~ ", " .----. ", " / {E} {E} \\ ", " | | ", " ~~`~~`~~ "],
|
|
58
|
+
],
|
|
59
|
+
axolotl: [
|
|
60
|
+
[" ", "}~(______)~{", "}~({E} .. {E})~{", " ( .--. ) ", " (_/ \\_) "],
|
|
61
|
+
[" ", "~}(______){~", "~}({E} .. {E}){~", " ( .--. ) ", " (_/ \\_) "],
|
|
62
|
+
[" ", "}~(______)~{", "}~({E} .. {E})~{", " ( -- ) ", " ~_/ \\_~ "],
|
|
63
|
+
],
|
|
64
|
+
capybara: [
|
|
65
|
+
[" ", " n______n ", " ( {E} {E} ) ", " ( oo ) ", " `------' "],
|
|
66
|
+
[" ", " n______n ", " ( {E} {E} ) ", " ( Oo ) ", " `------' "],
|
|
67
|
+
[" ~ ~ ", " u______n ", " ( {E} {E} ) ", " ( oo ) ", " `------' "],
|
|
68
|
+
],
|
|
69
|
+
cactus: [
|
|
70
|
+
[" ", " n ____ n ", " | |{E} {E}| | ", " |_| |_| ", " | | "],
|
|
71
|
+
[" ", " ____ ", " n |{E} {E}| n ", " |_| |_| ", " | | "],
|
|
72
|
+
[" n n ", " | ____ | ", " | |{E} {E}| | ", " |_| |_| ", " | | "],
|
|
73
|
+
],
|
|
74
|
+
robot: [
|
|
75
|
+
[" ", " .[||]. ", " [ {E} {E} ] ", " [ ==== ] ", " `------' "],
|
|
76
|
+
[" ", " .[||]. ", " [ {E} {E} ] ", " [ -==- ] ", " `------' "],
|
|
77
|
+
[" * ", " .[||]. ", " [ {E} {E} ] ", " [ ==== ] ", " `------' "],
|
|
78
|
+
],
|
|
79
|
+
rabbit: [
|
|
80
|
+
[" ", " (\\__/) ", " ( {E} {E} ) ", " =( .. )= ", " (\")__(\")" ],
|
|
81
|
+
[" ", " (|__/) ", " ( {E} {E} ) ", " =( .. )= ", " (\")__(\")" ],
|
|
82
|
+
[" ", " (\\__/) ", " ( {E} {E} ) ", " =( . . )= ", " (\")__(\")" ],
|
|
83
|
+
],
|
|
84
|
+
mushroom: [
|
|
85
|
+
[" ", " .-o-OO-o-. ", "(__________)"," |{E} {E}| ", " |____| "],
|
|
86
|
+
[" ", " .-O-oo-O-. ", "(__________)"," |{E} {E}| ", " |____| "],
|
|
87
|
+
[" . o . ", " .-o-OO-o-. ", "(__________)"," |{E} {E}| ", " |____| "],
|
|
88
|
+
],
|
|
89
|
+
chonk: [
|
|
90
|
+
[" ", " /\\ /\\ ", " ( {E} {E} ) ", " ( .. ) ", " `------' "],
|
|
91
|
+
[" ", " /\\ /| ", " ( {E} {E} ) ", " ( .. ) ", " `------' "],
|
|
92
|
+
[" ", " /\\ /\\ ", " ( {E} {E} ) ", " ( .. ) ", " `------'~ "],
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const HAT_ART: Record<Hat, string> = {
|
|
97
|
+
none: "",
|
|
98
|
+
crown: " \\^^^/ ",
|
|
99
|
+
tophat: " [___] ",
|
|
100
|
+
propeller: " -+- ",
|
|
101
|
+
halo: " ( ) ",
|
|
102
|
+
wizard: " /^\\ ",
|
|
103
|
+
beanie: " (___) ",
|
|
104
|
+
tinyduck: " ,> ",
|
|
105
|
+
};
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateBones,
|
|
3
|
+
RARITY_STARS,
|
|
4
|
+
type Companion,
|
|
5
|
+
} from "./engine.ts";
|
|
6
|
+
import { getUnlockedAchievements, type Achievement } from "./achievements.ts";
|
|
7
|
+
import { generateFallbackName, getReaction } from "./reactions.ts";
|
|
8
|
+
import type {
|
|
9
|
+
BuddyConfig,
|
|
10
|
+
ReactionState,
|
|
11
|
+
} from "./model.ts";
|
|
12
|
+
import type {
|
|
13
|
+
BuddyConfigRepository,
|
|
14
|
+
BuddyEventRepository,
|
|
15
|
+
BuddyRepository,
|
|
16
|
+
IdentityProvider,
|
|
17
|
+
ReactionRepository,
|
|
18
|
+
} from "./ports.ts";
|
|
19
|
+
|
|
20
|
+
export interface BuddyCommandServiceDeps {
|
|
21
|
+
identity: IdentityProvider;
|
|
22
|
+
buddies: BuddyRepository;
|
|
23
|
+
reactions: ReactionRepository;
|
|
24
|
+
config: BuddyConfigRepository;
|
|
25
|
+
events: BuddyEventRepository;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface EnsureCompanionResult {
|
|
29
|
+
companion: Companion;
|
|
30
|
+
slot: string;
|
|
31
|
+
created: boolean;
|
|
32
|
+
achievements: Achievement[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ReactionResult {
|
|
36
|
+
companion: Companion;
|
|
37
|
+
slot: string;
|
|
38
|
+
reaction: string;
|
|
39
|
+
state: ReactionState;
|
|
40
|
+
achievements: Achievement[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SaveBuddyResult {
|
|
44
|
+
companion: Companion;
|
|
45
|
+
slot: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SummonBuddyResult {
|
|
49
|
+
companion: Companion;
|
|
50
|
+
slot: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Shared application-service seam for buddy operations.
|
|
55
|
+
*
|
|
56
|
+
* Host adapters should depend on this service for buddy domain behavior,
|
|
57
|
+
* while keeping transport, UI, and host-specific persistence concerns outside.
|
|
58
|
+
*/
|
|
59
|
+
export class BuddyCommandService {
|
|
60
|
+
constructor(private readonly deps: BuddyCommandServiceDeps) {}
|
|
61
|
+
|
|
62
|
+
getStableUserId(): string {
|
|
63
|
+
return this.deps.identity.getStableUserId();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
loadActiveCompanion(): Companion | null {
|
|
67
|
+
return this.deps.buddies.loadActive();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
loadLatestReaction(scope?: string): ReactionState | null {
|
|
71
|
+
return this.deps.reactions.loadLatest(scope);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
loadConfig(): BuddyConfig {
|
|
75
|
+
return this.deps.config.loadConfig();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
listCompanions(): Array<{ slot: string; companion: Companion }> {
|
|
79
|
+
return this.deps.buddies.listSlots();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
ensureCompanion(): EnsureCompanionResult {
|
|
83
|
+
const active = this.deps.buddies.loadActive();
|
|
84
|
+
if (active) {
|
|
85
|
+
return {
|
|
86
|
+
companion: active,
|
|
87
|
+
slot: this.getActiveSlot(),
|
|
88
|
+
created: false,
|
|
89
|
+
achievements: [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const saved = this.deps.buddies.listSlots();
|
|
94
|
+
if (saved.length > 0) {
|
|
95
|
+
const rescued = saved[0];
|
|
96
|
+
this.deps.buddies.saveActiveSlot(rescued.slot);
|
|
97
|
+
return {
|
|
98
|
+
companion: rescued.companion,
|
|
99
|
+
slot: rescued.slot,
|
|
100
|
+
created: false,
|
|
101
|
+
achievements: [],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const userId = this.getStableUserId();
|
|
106
|
+
const companion = this.createCompanion(userId);
|
|
107
|
+
const slot = slugify(companion.name);
|
|
108
|
+
this.deps.buddies.saveSlot(slot, companion);
|
|
109
|
+
this.deps.buddies.saveActiveSlot(slot);
|
|
110
|
+
this.deps.events.trackActiveDay();
|
|
111
|
+
this.deps.events.increment("sessions", 1);
|
|
112
|
+
const achievements = this.unlockAchievements(slot);
|
|
113
|
+
|
|
114
|
+
return { companion, slot, created: true, achievements };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
petBuddy(scope?: string): ReactionResult {
|
|
118
|
+
const { companion, slot } = this.ensureCompanion();
|
|
119
|
+
this.deps.events.increment("pets", 1, slot);
|
|
120
|
+
return this.saveReaction("pet", companion, slot, scope);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
renameBuddy(name: string): Companion {
|
|
124
|
+
const trimmed = name.trim();
|
|
125
|
+
if (!trimmed) throw new Error("Buddy name cannot be empty.");
|
|
126
|
+
const { companion } = this.ensureCompanion();
|
|
127
|
+
const updated: Companion = { ...companion, name: trimmed.slice(0, 14) };
|
|
128
|
+
this.deps.buddies.saveActive(updated);
|
|
129
|
+
return updated;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
setPersonality(personality: string): Companion {
|
|
133
|
+
const trimmed = personality.trim();
|
|
134
|
+
if (!trimmed) throw new Error("Buddy personality cannot be empty.");
|
|
135
|
+
const { companion } = this.ensureCompanion();
|
|
136
|
+
const updated: Companion = { ...companion, personality: trimmed };
|
|
137
|
+
this.deps.buddies.saveActive(updated);
|
|
138
|
+
return updated;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
saveBuddy(slot?: string): SaveBuddyResult {
|
|
142
|
+
const { companion } = this.ensureCompanion();
|
|
143
|
+
const targetSlot = slugify(slot?.trim() || companion.name);
|
|
144
|
+
this.deps.buddies.saveSlot(targetSlot, companion);
|
|
145
|
+
this.deps.buddies.saveActiveSlot(targetSlot);
|
|
146
|
+
return { companion, slot: targetSlot };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
summonBuddy(slot?: string): SummonBuddyResult | null {
|
|
150
|
+
const targetSlot = slot ? slugify(slot) : undefined;
|
|
151
|
+
|
|
152
|
+
if (!targetSlot) {
|
|
153
|
+
const companions = this.deps.buddies.listSlots();
|
|
154
|
+
if (companions.length === 0) return null;
|
|
155
|
+
const pick = companions[Math.floor(Math.random() * companions.length)];
|
|
156
|
+
this.deps.buddies.saveActiveSlot(pick.slot);
|
|
157
|
+
return { companion: pick.companion, slot: pick.slot };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const companion = this.deps.buddies.loadSlot(targetSlot);
|
|
161
|
+
if (!companion) return null;
|
|
162
|
+
|
|
163
|
+
this.deps.buddies.saveActiveSlot(targetSlot);
|
|
164
|
+
return { companion, slot: targetSlot };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
dismissBuddy(slot: string): SaveBuddyResult {
|
|
168
|
+
const targetSlot = slugify(slot);
|
|
169
|
+
const activeSlot = this.getActiveSlot();
|
|
170
|
+
if (targetSlot === activeSlot) {
|
|
171
|
+
throw new Error("Cannot dismiss the active buddy.");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const companion = this.deps.buddies.loadSlot(targetSlot);
|
|
175
|
+
if (!companion) {
|
|
176
|
+
throw new Error(`No buddy found in slot \"${targetSlot}\".`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.deps.buddies.deleteSlot(targetSlot);
|
|
180
|
+
return { companion, slot: targetSlot };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
recordNameMention(scope?: string): ReactionResult {
|
|
184
|
+
const { companion, slot } = this.ensureCompanion();
|
|
185
|
+
return this.saveReaction("turn", companion, slot, scope);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
recordToolError(scope?: string, line?: number): ReactionResult {
|
|
189
|
+
const { companion, slot } = this.ensureCompanion();
|
|
190
|
+
this.deps.events.increment("errors_seen", 1);
|
|
191
|
+
return this.saveReaction("error", companion, slot, scope, { line });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
recordTestFailure(scope?: string, count?: number): ReactionResult {
|
|
195
|
+
const { companion, slot } = this.ensureCompanion();
|
|
196
|
+
this.deps.events.increment("tests_failed", 1);
|
|
197
|
+
return this.saveReaction("test-fail", companion, slot, scope, { count });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
recordLargeDiff(lines: number, scope?: string): ReactionResult {
|
|
201
|
+
const { companion, slot } = this.ensureCompanion();
|
|
202
|
+
this.deps.events.increment("large_diffs", 1);
|
|
203
|
+
return this.saveReaction("large-diff", companion, slot, scope, { lines });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
recordTurn(scope?: string): ReactionResult {
|
|
207
|
+
const { companion, slot } = this.ensureCompanion();
|
|
208
|
+
this.deps.events.increment("turns", 1);
|
|
209
|
+
return this.saveReaction("turn", companion, slot, scope);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
incrementCommandsRun(): Achievement[] {
|
|
213
|
+
this.deps.events.increment("commands_run", 1, this.getActiveSlotOrUndefined());
|
|
214
|
+
return this.unlockAchievements(this.getActiveSlotOrUndefined());
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
getAchievementProgress(slot?: string): {
|
|
218
|
+
unlocked: Array<{ achievement: Achievement; unlockedAt: number; slot?: string }>;
|
|
219
|
+
remaining: Achievement[];
|
|
220
|
+
} {
|
|
221
|
+
const unlocked = this.deps.events.loadUnlocked();
|
|
222
|
+
const unlockedMap = new Map(unlocked.map((entry) => [entry.id, entry]));
|
|
223
|
+
|
|
224
|
+
const unlockedAchievements = getUnlockedAchievements(
|
|
225
|
+
this.deps.events.loadCounters(slot),
|
|
226
|
+
new Set<string>(),
|
|
227
|
+
).filter((achievement) => unlockedMap.has(achievement.id));
|
|
228
|
+
|
|
229
|
+
const remaining = getUnlockedAchievements(
|
|
230
|
+
this.deps.events.loadCounters(slot),
|
|
231
|
+
new Set(unlocked.map((entry) => entry.id)),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
unlocked: unlockedAchievements.map((achievement) => ({
|
|
236
|
+
achievement,
|
|
237
|
+
unlockedAt: unlockedMap.get(achievement.id)?.unlockedAt ?? 0,
|
|
238
|
+
slot: unlockedMap.get(achievement.id)?.slot,
|
|
239
|
+
})),
|
|
240
|
+
remaining,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
formatCompanionSummary(companion: Companion): string {
|
|
245
|
+
const shiny = companion.bones.shiny ? " ✨" : "";
|
|
246
|
+
return `${companion.name} — ${companion.bones.rarity} ${companion.bones.species} ${RARITY_STARS[companion.bones.rarity]}${shiny}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private getActiveSlot(): string {
|
|
250
|
+
return this.deps.buddies.loadActiveSlot() || "buddy";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private getActiveSlotOrUndefined(): string | undefined {
|
|
254
|
+
const slot = this.deps.buddies.loadActiveSlot();
|
|
255
|
+
return slot || undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private createCompanion(userId: string): Companion {
|
|
259
|
+
const bones = generateBones(userId);
|
|
260
|
+
const name = this.generateUnusedName();
|
|
261
|
+
return {
|
|
262
|
+
bones,
|
|
263
|
+
name,
|
|
264
|
+
personality: `A ${bones.rarity} ${bones.species} who watches code with quiet intensity.`,
|
|
265
|
+
hatchedAt: Date.now(),
|
|
266
|
+
userId,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private generateUnusedName(): string {
|
|
271
|
+
const taken = new Set(this.deps.buddies.listSlots().map(({ slot }) => slot));
|
|
272
|
+
for (let i = 0; i < 50; i++) {
|
|
273
|
+
const candidate = generateFallbackName();
|
|
274
|
+
if (!taken.has(slugify(candidate))) return candidate;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let suffix = 0;
|
|
278
|
+
while (taken.has(`buddy-${suffix}`)) suffix++;
|
|
279
|
+
return `buddy-${suffix}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private saveReaction(
|
|
283
|
+
reason: "pet" | "turn" | "error" | "test-fail" | "large-diff",
|
|
284
|
+
companion: Companion,
|
|
285
|
+
slot: string,
|
|
286
|
+
scope?: string,
|
|
287
|
+
context?: { line?: number; count?: number; lines?: number },
|
|
288
|
+
): ReactionResult {
|
|
289
|
+
const reaction = getReaction(
|
|
290
|
+
reason,
|
|
291
|
+
companion.bones.species,
|
|
292
|
+
companion.bones.rarity,
|
|
293
|
+
context,
|
|
294
|
+
);
|
|
295
|
+
const state: ReactionState = {
|
|
296
|
+
reaction,
|
|
297
|
+
reason,
|
|
298
|
+
timestamp: Date.now(),
|
|
299
|
+
};
|
|
300
|
+
this.deps.reactions.saveLatest(state, scope);
|
|
301
|
+
this.deps.events.increment("reactions_given", 1, slot);
|
|
302
|
+
const achievements = this.unlockAchievements(slot);
|
|
303
|
+
return { companion, slot, reaction, state, achievements };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private unlockAchievements(slot?: string): Achievement[] {
|
|
307
|
+
const unlocked = this.deps.events.loadUnlocked();
|
|
308
|
+
const unlockedIds = new Set(unlocked.map((entry) => entry.id));
|
|
309
|
+
const newlyUnlocked = getUnlockedAchievements(
|
|
310
|
+
this.deps.events.loadCounters(slot),
|
|
311
|
+
unlockedIds,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
if (newlyUnlocked.length === 0) return [];
|
|
315
|
+
|
|
316
|
+
this.deps.events.saveUnlocked([
|
|
317
|
+
...unlocked,
|
|
318
|
+
...newlyUnlocked.map((achievement) => ({
|
|
319
|
+
id: achievement.id,
|
|
320
|
+
unlockedAt: Date.now(),
|
|
321
|
+
slot,
|
|
322
|
+
})),
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
return newlyUnlocked;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function slugify(name: string): string {
|
|
330
|
+
return (
|
|
331
|
+
name
|
|
332
|
+
.toLowerCase()
|
|
333
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
334
|
+
.replace(/-+/g, "-")
|
|
335
|
+
.replace(/^-|-$/g, "")
|
|
336
|
+
.slice(0, 14) || "buddy"
|
|
337
|
+
);
|
|
338
|
+
}
|
package/core/model.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BuddyBones,
|
|
3
|
+
BuddyStats,
|
|
4
|
+
Companion,
|
|
5
|
+
Species,
|
|
6
|
+
Rarity,
|
|
7
|
+
Eye,
|
|
8
|
+
Hat,
|
|
9
|
+
StatName,
|
|
10
|
+
} from "./engine.ts";
|
|
11
|
+
|
|
12
|
+
export type {
|
|
13
|
+
BuddyBones,
|
|
14
|
+
BuddyStats,
|
|
15
|
+
Companion,
|
|
16
|
+
Species,
|
|
17
|
+
Rarity,
|
|
18
|
+
Eye,
|
|
19
|
+
Hat,
|
|
20
|
+
StatName,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface ReactionState {
|
|
24
|
+
reaction: string;
|
|
25
|
+
timestamp: number;
|
|
26
|
+
reason: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BuddyConfig {
|
|
30
|
+
commentCooldown: number;
|
|
31
|
+
reactionTTL: number;
|
|
32
|
+
bubbleStyle: "classic" | "round";
|
|
33
|
+
bubblePosition: "top" | "left";
|
|
34
|
+
showRarity: boolean;
|
|
35
|
+
statusLineEnabled: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GlobalCounters {
|
|
39
|
+
errors_seen: number;
|
|
40
|
+
tests_failed: number;
|
|
41
|
+
large_diffs: number;
|
|
42
|
+
sessions: number;
|
|
43
|
+
commands_run: number;
|
|
44
|
+
days_active: number;
|
|
45
|
+
turns: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SlotCounters {
|
|
49
|
+
pets: number;
|
|
50
|
+
reactions_given: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface EventCounters extends GlobalCounters, SlotCounters {}
|
|
54
|
+
|
|
55
|
+
export interface UnlockedAchievement {
|
|
56
|
+
id: string;
|
|
57
|
+
unlockedAt: number;
|
|
58
|
+
slot?: string;
|
|
59
|
+
}
|
package/core/ports.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BuddyConfig,
|
|
3
|
+
Companion,
|
|
4
|
+
EventCounters,
|
|
5
|
+
ReactionState,
|
|
6
|
+
UnlockedAchievement,
|
|
7
|
+
} from "./model.ts";
|
|
8
|
+
|
|
9
|
+
export interface IdentityProvider {
|
|
10
|
+
getStableUserId(): string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BuddyRepository {
|
|
14
|
+
loadActive(): Companion | null;
|
|
15
|
+
saveActive(companion: Companion): void;
|
|
16
|
+
loadSlot(slot: string): Companion | null;
|
|
17
|
+
saveSlot(slot: string, companion: Companion): void;
|
|
18
|
+
deleteSlot(slot: string): void;
|
|
19
|
+
listSlots(): Array<{ slot: string; companion: Companion }>;
|
|
20
|
+
loadActiveSlot(): string | null;
|
|
21
|
+
saveActiveSlot(slot: string): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ReactionRepository {
|
|
25
|
+
loadLatest(scope?: string): ReactionState | null;
|
|
26
|
+
saveLatest(reaction: ReactionState, scope?: string): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BuddyConfigRepository {
|
|
30
|
+
loadConfig(): BuddyConfig;
|
|
31
|
+
saveConfig(config: Partial<BuddyConfig>): BuddyConfig;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface BuddyEventRepository {
|
|
35
|
+
loadCounters(scope?: string): EventCounters;
|
|
36
|
+
increment(key: keyof EventCounters, amount?: number, scope?: string): EventCounters;
|
|
37
|
+
loadUnlocked(): UnlockedAchievement[];
|
|
38
|
+
saveUnlocked(unlocked: UnlockedAchievement[]): void;
|
|
39
|
+
trackActiveDay(): void;
|
|
40
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Eye, Species } from "./engine.ts";
|
|
2
|
+
import { HAT_ART, SPECIES_ART } from "./art-data.ts";
|
|
3
|
+
|
|
4
|
+
export function getArtFrame(species: Species, eye: Eye, frame: number = 0): string[] {
|
|
5
|
+
const frames = SPECIES_ART[species];
|
|
6
|
+
const f = frames[frame % frames.length];
|
|
7
|
+
return f.map((line) => line.replace(/\{E\}/g, eye));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export { HAT_ART, SPECIES_ART };
|
package/package.json
CHANGED
|
@@ -1,35 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ramarivera/coding-buddy",
|
|
3
|
-
"version": "0.4.0-alpha.
|
|
3
|
+
"version": "0.4.0-alpha.4",
|
|
4
4
|
"description": "Persistent coding companion for Claude Code and pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"coding-buddy": "./cli/index.ts"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"server": "bun run server/index.ts",
|
|
11
|
-
"pick": "bun run
|
|
12
|
-
"hunt": "bun run
|
|
13
|
-
"install-buddy": "bun run
|
|
14
|
-
"show": "bun run
|
|
15
|
-
"doctor": "bun run
|
|
16
|
-
"test-statusline": "bun run
|
|
17
|
-
"backup": "bun run
|
|
18
|
-
"settings": "bun run
|
|
19
|
-
"disable": "bun run
|
|
20
|
-
"enable": "bun run
|
|
21
|
-
"uninstall": "bun run
|
|
10
|
+
"server": "bun run adapters/claude/server/index.ts",
|
|
11
|
+
"pick": "bun run adapters/claude/install/pick.ts",
|
|
12
|
+
"hunt": "bun run adapters/claude/install/hunt.ts",
|
|
13
|
+
"install-buddy": "bun run adapters/claude/install/install.ts",
|
|
14
|
+
"show": "bun run adapters/claude/install/show.ts",
|
|
15
|
+
"doctor": "bun run adapters/claude/install/doctor.ts",
|
|
16
|
+
"test-statusline": "bun run adapters/claude/install/test-statusline.ts",
|
|
17
|
+
"backup": "bun run adapters/claude/install/backup.ts",
|
|
18
|
+
"settings": "bun run adapters/claude/install/settings.ts",
|
|
19
|
+
"disable": "bun run adapters/claude/install/disable.ts",
|
|
20
|
+
"enable": "bun run adapters/claude/install/install.ts",
|
|
21
|
+
"uninstall": "bun run adapters/claude/install/uninstall.ts",
|
|
22
22
|
"help": "bun run cli/index.ts help",
|
|
23
23
|
"test": "bun test",
|
|
24
24
|
"typecheck": "tsc --noEmit"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
|
-
"
|
|
27
|
+
"core/",
|
|
28
|
+
"adapters/",
|
|
28
29
|
"cli/",
|
|
29
|
-
"skills/",
|
|
30
|
-
"hooks/",
|
|
31
|
-
"statusline/",
|
|
32
|
-
".claude-plugin/",
|
|
33
30
|
"!**/*.test.ts"
|
|
34
31
|
],
|
|
35
32
|
"keywords": [
|
|
@@ -38,7 +35,8 @@
|
|
|
38
35
|
"companion",
|
|
39
36
|
"mcp",
|
|
40
37
|
"terminal-pet",
|
|
41
|
-
"tamagotchi"
|
|
38
|
+
"tamagotchi",
|
|
39
|
+
"pi-package"
|
|
42
40
|
],
|
|
43
41
|
"repository": {
|
|
44
42
|
"type": "git",
|
|
@@ -50,7 +48,13 @@
|
|
|
50
48
|
"access": "public",
|
|
51
49
|
"registry": "https://registry.npmjs.org/"
|
|
52
50
|
},
|
|
51
|
+
"pi": {
|
|
52
|
+
"extensions": [
|
|
53
|
+
"./adapters/pi/index.ts"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
53
56
|
"dependencies": {
|
|
57
|
+
"@mariozechner/pi-coding-agent": "0.66.1",
|
|
54
58
|
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
55
59
|
},
|
|
56
60
|
"devDependencies": {
|