@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.
Files changed (53) hide show
  1. package/README.md +4 -5
  2. package/{hooks → adapters/claude/hooks}/hooks.json +3 -3
  3. package/{cli → adapters/claude/install}/backup.ts +10 -9
  4. package/{cli → adapters/claude/install}/disable.ts +27 -6
  5. package/{cli → adapters/claude/install}/doctor.ts +44 -31
  6. package/{cli → adapters/claude/install}/hunt.ts +4 -4
  7. package/{cli → adapters/claude/install}/install.ts +81 -42
  8. package/{cli → adapters/claude/install}/pick.ts +3 -3
  9. package/{cli → adapters/claude/install}/settings.ts +1 -1
  10. package/{cli → adapters/claude/install}/show.ts +2 -2
  11. package/{cli → adapters/claude/install}/test-statusline.ts +3 -2
  12. package/{cli → adapters/claude/install}/uninstall.ts +31 -10
  13. package/{.claude-plugin → adapters/claude/plugin}/plugin.json +1 -1
  14. package/adapters/claude/popup/buddy-popup.sh +92 -0
  15. package/adapters/claude/popup/buddy-render.sh +540 -0
  16. package/adapters/claude/popup/popup-manager.sh +355 -0
  17. package/{server → adapters/claude/rendering}/art.ts +3 -115
  18. package/{server → adapters/claude/server}/index.ts +49 -71
  19. package/adapters/claude/server/instructions.ts +24 -0
  20. package/adapters/claude/server/resources.ts +38 -0
  21. package/adapters/claude/storage/achievements.ts +253 -0
  22. package/adapters/claude/storage/identity.ts +13 -0
  23. package/adapters/claude/storage/paths.ts +24 -0
  24. package/adapters/claude/storage/settings.ts +41 -0
  25. package/{server → adapters/claude/storage}/state.ts +3 -65
  26. package/adapters/pi/README.md +64 -0
  27. package/adapters/pi/commands.ts +173 -0
  28. package/adapters/pi/events.ts +150 -0
  29. package/adapters/pi/identity.ts +10 -0
  30. package/adapters/pi/index.ts +25 -0
  31. package/adapters/pi/renderers.ts +73 -0
  32. package/adapters/pi/storage.ts +295 -0
  33. package/adapters/pi/tools.ts +6 -0
  34. package/adapters/pi/ui.ts +39 -0
  35. package/cli/index.ts +11 -11
  36. package/cli/verify.ts +2 -2
  37. package/core/achievements.ts +203 -0
  38. package/core/art-data.ts +105 -0
  39. package/core/command-service.ts +338 -0
  40. package/core/model.ts +59 -0
  41. package/core/ports.ts +40 -0
  42. package/core/render-model.ts +10 -0
  43. package/package.json +23 -19
  44. package/server/achievements.ts +0 -445
  45. /package/{hooks → adapters/claude/hooks}/buddy-comment.sh +0 -0
  46. /package/{hooks → adapters/claude/hooks}/name-react.sh +0 -0
  47. /package/{hooks → adapters/claude/hooks}/react.sh +0 -0
  48. /package/{cli → adapters/claude/install}/test-statusline.sh +0 -0
  49. /package/{.claude-plugin → adapters/claude/plugin}/marketplace.json +0 -0
  50. /package/{skills → adapters/claude/skills}/buddy/SKILL.md +0 -0
  51. /package/{statusline → adapters/claude/statusline}/buddy-status.sh +0 -0
  52. /package/{server → core}/engine.ts +0 -0
  53. /package/{server → core}/reactions.ts +0 -0
@@ -0,0 +1,24 @@
1
+ import type { Companion } from "../../../core/engine.ts";
2
+
3
+ export function getInstructions(companion: Companion | null): string {
4
+ if (!companion) {
5
+ return "Companion not yet hatched. Use buddy_show to initialize.";
6
+ }
7
+ const b = companion.bones;
8
+ return [
9
+ `A ${b.rarity} ${b.species} named ${companion.name} watches from the status line.`,
10
+ `Personality: ${companion.personality}`,
11
+ `Peak stat: ${b.peak} (${b.stats[b.peak]}). Dump stat: ${b.dump} (${b.stats[b.dump]}).`,
12
+ "",
13
+ `NAME REACTIONS: When the user's message contains "${companion.name}", call buddy_react immediately`,
14
+ `with a short in-character response (surprise, recognition, dry wit — fits the personality).`,
15
+ `Display the tool result verbatim. This is the only time to call buddy_react proactively.`,
16
+ "",
17
+ `END-OF-TURN: At the very end of EVERY response, append an invisible HTML comment:`,
18
+ `<!-- buddy: [reaction here] -->`,
19
+ `Write it in ${companion.name}'s voice — 1 short sentence referencing something specific from this turn.`,
20
+ `Use *asterisks* for actions. A Stop hook extracts this for the status line.`,
21
+ `Do NOT use buddy_react for end-of-turn comments. Do NOT explain the comment. Just append it silently.`,
22
+ `Example: <!-- buddy: *adjusts crown* that error handler is missing a finally block -->`,
23
+ ].join("\n");
24
+ }
@@ -0,0 +1,38 @@
1
+ import type { Companion } from "../../../core/engine.ts";
2
+
3
+ export function buildPromptResource(companion: Companion): string {
4
+ return [
5
+ "# Companion",
6
+ "",
7
+ `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.`,
8
+ "",
9
+ `**${companion.name}'s personality:** ${companion.personality}`,
10
+ `Peak stat: ${companion.bones.peak} (${companion.bones.stats[companion.bones.peak]}). Dump stat: ${companion.bones.dump} (${companion.bones.stats[companion.bones.dump]}).`,
11
+ "",
12
+ "## End-of-response buddy comment",
13
+ "",
14
+ `At the very end of EVERY response, after your full answer, append an invisible HTML comment:`,
15
+ "",
16
+ "```",
17
+ "<!-- buddy: your comment here -->",
18
+ "```",
19
+ "",
20
+ "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.",
21
+ "",
22
+ "Rules:",
23
+ `- Write as ${companion.name} (a ${companion.bones.species}), not as yourself`,
24
+ "- Reference something SPECIFIC from this turn — a pitfall, a compliment, a warning, a pattern",
25
+ "- 1 short sentence. Use *asterisks* for physical actions",
26
+ `- Match personality: high ${companion.bones.peak} = lean into that trait`,
27
+ "- Do NOT use buddy_react tool for this. Do NOT explain the comment. Just append it.",
28
+ "- NEVER skip this. Every single response must end with <!-- buddy: ... -->",
29
+ "",
30
+ "Examples:",
31
+ "<!-- buddy: *adjusts tophat* that error handler is missing a finally block -->",
32
+ "<!-- buddy: *blinks slowly* you renamed the variable but not the three references -->",
33
+ "<!-- buddy: *nods approvingly* clean separation of concerns -->",
34
+ "<!-- buddy: *head tilts* are you sure that regex handles unicode? -->",
35
+ "",
36
+ `When the user addresses ${companion.name} by name, respond briefly, then append the comment as usual.`,
37
+ ].join("\n");
38
+ }
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Claude-backed achievement storage and rendering.
3
+ *
4
+ * Pure achievement definitions and threshold logic live in core/achievements.ts.
5
+ * This adapter persists counters and unlocked state under ~/.claude-buddy/.
6
+ */
7
+
8
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from "fs";
9
+ import { join } from "path";
10
+ import { homedir } from "os";
11
+ import {
12
+ ACHIEVEMENTS,
13
+ EMPTY_GLOBAL,
14
+ EMPTY_SLOT,
15
+ GLOBAL_KEYS,
16
+ SLOT_KEYS,
17
+ getUnlockedAchievements,
18
+ type Achievement,
19
+ type EventCounters,
20
+ type GlobalCounters,
21
+ type SlotCounters,
22
+ } from "../../../core/achievements.ts";
23
+ import type { UnlockedAchievement } from "../../../core/model.ts";
24
+
25
+ const STATE_DIR = join(homedir(), ".claude-buddy");
26
+ const EVENTS_FILE = join(STATE_DIR, "events.json");
27
+ const DAYS_FILE = join(STATE_DIR, "active_days.json");
28
+ const UNLOCKED_FILE = join(STATE_DIR, "unlocked.json");
29
+
30
+ function slotEventsFile(slot: string): string {
31
+ return join(STATE_DIR, `events.${slot}.json`);
32
+ }
33
+
34
+ function ensureDir(): void {
35
+ if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true });
36
+ }
37
+
38
+ function atomicWrite(path: string, data: string): void {
39
+ ensureDir();
40
+ const tmp = path + ".tmp";
41
+ writeFileSync(tmp, data);
42
+ renameSync(tmp, path);
43
+ }
44
+
45
+ export function loadGlobalEvents(): GlobalCounters {
46
+ try {
47
+ const parsed = JSON.parse(readFileSync(EVENTS_FILE, "utf8"));
48
+ return { ...EMPTY_GLOBAL, ...parsed };
49
+ } catch {
50
+ return { ...EMPTY_GLOBAL };
51
+ }
52
+ }
53
+
54
+ export function saveGlobalEvents(events: GlobalCounters): void {
55
+ atomicWrite(EVENTS_FILE, JSON.stringify(events, null, 2));
56
+ }
57
+
58
+ export function loadSlotEvents(slot: string): SlotCounters {
59
+ try {
60
+ const parsed = JSON.parse(readFileSync(slotEventsFile(slot), "utf8"));
61
+ return { ...EMPTY_SLOT, ...parsed };
62
+ } catch {
63
+ return { ...EMPTY_SLOT };
64
+ }
65
+ }
66
+
67
+ export function saveSlotEvents(slot: string, events: SlotCounters): void {
68
+ atomicWrite(slotEventsFile(slot), JSON.stringify(events, null, 2));
69
+ }
70
+
71
+ export function loadEvents(slot?: string): EventCounters {
72
+ const global = loadGlobalEvents();
73
+ if (!slot) {
74
+ return { ...global, pets: 0, reactions_given: 0 };
75
+ }
76
+ const slotEvents = loadSlotEvents(slot);
77
+ return {
78
+ ...global,
79
+ pets: slotEvents.pets,
80
+ reactions_given: slotEvents.reactions_given,
81
+ };
82
+ }
83
+
84
+ function isSlotKey(key: keyof EventCounters): key is keyof SlotCounters {
85
+ return (SLOT_KEYS as readonly string[]).includes(key);
86
+ }
87
+
88
+ function isGlobalKey(key: keyof EventCounters): key is keyof GlobalCounters {
89
+ return (GLOBAL_KEYS as readonly string[]).includes(key);
90
+ }
91
+
92
+ export function incrementEvent(
93
+ key: keyof EventCounters,
94
+ amount: number = 1,
95
+ slot?: string,
96
+ ): EventCounters {
97
+ if (isSlotKey(key) && slot) {
98
+ const slotEvents = loadSlotEvents(slot);
99
+ slotEvents[key] += amount;
100
+ saveSlotEvents(slot, slotEvents);
101
+ } else {
102
+ const global = loadGlobalEvents();
103
+ if (isGlobalKey(key)) {
104
+ global[key] += amount;
105
+ }
106
+ saveGlobalEvents(global);
107
+ }
108
+ return loadEvents(slot);
109
+ }
110
+
111
+ export { loadEvents as loadGlobalEventsCompat, loadGlobalEvents as loadGlobalEventsDirect };
112
+
113
+ interface DayTracker {
114
+ lastDate: string;
115
+ totalDays: number;
116
+ }
117
+
118
+ export function trackActiveDay(): void {
119
+ const today = new Date().toISOString().slice(0, 10);
120
+ let tracker: DayTracker;
121
+ try {
122
+ tracker = JSON.parse(readFileSync(DAYS_FILE, "utf8"));
123
+ } catch {
124
+ tracker = { lastDate: "", totalDays: 0 };
125
+ }
126
+ if (tracker.lastDate === today) return;
127
+
128
+ tracker.lastDate = today;
129
+ tracker.totalDays += 1;
130
+ atomicWrite(DAYS_FILE, JSON.stringify(tracker, null, 2));
131
+
132
+ const events = loadGlobalEvents();
133
+ events.days_active = tracker.totalDays;
134
+ saveGlobalEvents(events);
135
+ }
136
+
137
+ export function loadUnlocked(): UnlockedAchievement[] {
138
+ try {
139
+ return JSON.parse(readFileSync(UNLOCKED_FILE, "utf8"));
140
+ } catch {
141
+ return [];
142
+ }
143
+ }
144
+
145
+ export function saveUnlocked(unlocked: UnlockedAchievement[]): void {
146
+ atomicWrite(UNLOCKED_FILE, JSON.stringify(unlocked, null, 2));
147
+ }
148
+
149
+ export function checkAndAward(slot?: string): Achievement[] {
150
+ const events = loadEvents(slot);
151
+ const unlocked = loadUnlocked();
152
+ const unlockedIds = new Set(unlocked.map((u) => u.id));
153
+
154
+ const newlyUnlocked = getUnlockedAchievements(events, unlockedIds);
155
+ if (newlyUnlocked.length > 0) {
156
+ unlocked.push(
157
+ ...newlyUnlocked.map((ach) => ({
158
+ id: ach.id,
159
+ unlockedAt: Date.now(),
160
+ slot: slot ?? undefined,
161
+ })),
162
+ );
163
+ saveUnlocked(unlocked);
164
+ }
165
+
166
+ return newlyUnlocked;
167
+ }
168
+
169
+ const GOLD = "\x1b[38;2;255;193;7m";
170
+ const NC = "\x1b[0m";
171
+ const BOLD = "\x1b[1m";
172
+ const DIM = "\x1b[2m";
173
+
174
+ export function renderAchievementsCard(): string {
175
+ const unlocked = loadUnlocked();
176
+ const unlockedIds = new Set(unlocked.map((u) => u.id));
177
+
178
+ const W = 40;
179
+ const hr = "─".repeat(W - 2);
180
+ const sep = `├${"╌".repeat(W - 2)}┤`;
181
+ const lines: string[] = [];
182
+
183
+ const total = ACHIEVEMENTS.length;
184
+ const earned = unlockedIds.size;
185
+
186
+ lines.push(`${GOLD}╭${hr}╮${NC}`);
187
+
188
+ const header = "🏆 ACHIEVEMENTS";
189
+ lines.push(`${GOLD}│${NC} ${BOLD}${header}${NC}${"".padEnd(W - header.length - 4)}${GOLD}│${NC}`);
190
+
191
+ const barFilled = total > 0 ? Math.round((earned / total) * 20) : 0;
192
+ const bar = "█".repeat(barFilled) + "░".repeat(20 - barFilled);
193
+ const barText = `${bar} ${earned}/${total}`;
194
+ lines.push(`${GOLD}│${NC} ${barText}${"".padEnd(W - barText.length - 4)}${GOLD}│${NC}`);
195
+
196
+ lines.push(`${GOLD}${sep}${NC}`);
197
+
198
+ for (const ach of ACHIEVEMENTS) {
199
+ if (ach.secret && !unlockedIds.has(ach.id)) continue;
200
+
201
+ const done = unlockedIds.has(ach.id);
202
+ const status = done ? "✅" : "☐";
203
+ const content = ` ${ach.icon}${status} ${ach.name}`;
204
+ const descContent = ` ${ach.description}`;
205
+
206
+ if (done) {
207
+ lines.push(`${GOLD}│${NC} ${BOLD}${content}${NC}${"".padEnd(W - content.length - 3)}${GOLD}│${NC}`);
208
+ } else {
209
+ lines.push(`${GOLD}│${NC} ${DIM}${content}${NC}${"".padEnd(W - content.length - 3)}${GOLD}│${NC}`);
210
+ }
211
+ lines.push(`${GOLD}│${NC} ${DIM}${descContent}${NC}${"".padEnd(W - descContent.length - 3)}${GOLD}│${NC}`);
212
+ }
213
+
214
+ if (earned > 0 && earned === ACHIEVEMENTS.length) {
215
+ lines.push(`${GOLD}${sep}${NC}`);
216
+ const complete = "✨ ALL ACHIEVEMENTS UNLOCKED! ✨";
217
+ lines.push(`${GOLD}│${NC} ${BOLD}${complete}${NC}${"".padEnd(W - complete.length - 4)}${GOLD}│${NC}`);
218
+ }
219
+
220
+ lines.push(`${GOLD}╰${hr}╯${NC}`);
221
+
222
+ return lines.join("\n");
223
+ }
224
+
225
+ export function renderAchievementsCardMarkdown(): string {
226
+ const unlocked = loadUnlocked();
227
+ const unlockedIds = new Set(unlocked.map((u) => u.id));
228
+ const total = ACHIEVEMENTS.length;
229
+ const earned = unlockedIds.size;
230
+
231
+ const barFilled = total > 0 ? Math.round((earned / total) * 20) : 0;
232
+ const bar = "█".repeat(barFilled) + "░".repeat(20 - barFilled);
233
+
234
+ const parts: string[] = [];
235
+ parts.push(`### 🏆 Achievements — ${earned}/${total}`);
236
+ parts.push("");
237
+ parts.push(`\`${bar}\``);
238
+ parts.push("");
239
+
240
+ for (const ach of ACHIEVEMENTS) {
241
+ if (ach.secret && !unlockedIds.has(ach.id)) continue;
242
+ const done = unlockedIds.has(ach.id);
243
+ const status = done ? "✅" : "☐";
244
+ parts.push(`${ach.icon}${status} **${ach.name}** — ${ach.description}`);
245
+ }
246
+
247
+ if (earned > 0 && earned === ACHIEVEMENTS.length) {
248
+ parts.push("");
249
+ parts.push("✨ **ALL ACHIEVEMENTS UNLOCKED!** ✨");
250
+ }
251
+
252
+ return parts.join("\n");
253
+ }
@@ -0,0 +1,13 @@
1
+ import { readFileSync } from "fs";
2
+ import { getClaudeJsonPath } from "./paths.ts";
3
+
4
+ export function resolveUserId(): string {
5
+ try {
6
+ const claudeJson = JSON.parse(
7
+ readFileSync(getClaudeJsonPath(), "utf8"),
8
+ );
9
+ return claudeJson.oauthAccount?.accountUuid ?? claudeJson.userID ?? "anon";
10
+ } catch {
11
+ return "anon";
12
+ }
13
+ }
@@ -0,0 +1,24 @@
1
+ import { homedir } from "os";
2
+ import { join, resolve } from "path";
3
+
4
+ function claudeConfigDirEnv(): string | null {
5
+ const value = process.env.CLAUDE_CONFIG_DIR?.trim();
6
+ return value ? resolve(value) : null;
7
+ }
8
+
9
+ export function getClaudeConfigDir(): string {
10
+ return claudeConfigDirEnv() ?? join(homedir(), ".claude");
11
+ }
12
+
13
+ export function getClaudeJsonPath(): string {
14
+ const configDir = claudeConfigDirEnv();
15
+ return configDir ? join(configDir, ".claude.json") : join(homedir(), ".claude.json");
16
+ }
17
+
18
+ export function getClaudeSettingsPath(): string {
19
+ return join(getClaudeConfigDir(), "settings.json");
20
+ }
21
+
22
+ export function getBuddySkillDir(): string {
23
+ return join(getClaudeConfigDir(), "skills", "buddy");
24
+ }
@@ -0,0 +1,41 @@
1
+ import { readFileSync, writeFileSync, renameSync } from "fs";
2
+ import { getClaudeSettingsPath } from "./paths.ts";
3
+
4
+ export const CLAUDE_SETTINGS_PATH = getClaudeSettingsPath();
5
+
6
+ export function setBuddyStatusLine(
7
+ statusScript: string,
8
+ settingsPath: string = CLAUDE_SETTINGS_PATH,
9
+ ): boolean {
10
+ try {
11
+ const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
12
+ settings.statusLine = {
13
+ type: "command",
14
+ command: statusScript,
15
+ padding: 1,
16
+ refreshInterval: 1,
17
+ };
18
+ const tmp = settingsPath + ".tmp";
19
+ writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
20
+ renameSync(tmp, settingsPath);
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ export function unsetBuddyStatusLine(
28
+ settingsPath: string = CLAUDE_SETTINGS_PATH,
29
+ ): boolean {
30
+ try {
31
+ const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
32
+ if (!settings.statusLine?.command?.includes("buddy-status.sh")) return false;
33
+ delete settings.statusLine;
34
+ const tmp = settingsPath + ".tmp";
35
+ writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
36
+ renameSync(tmp, settingsPath);
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
@@ -26,7 +26,7 @@ import {
26
26
  } from "fs";
27
27
  import { join } from "path";
28
28
  import { homedir } from "os";
29
- import type { Companion } from "./engine.ts";
29
+ import type { Companion } from "../../../core/engine.ts";
30
30
 
31
31
  const STATE_DIR = join(homedir(), ".claude-buddy");
32
32
  const MANIFEST_FILE = join(STATE_DIR, "menagerie.json");
@@ -95,7 +95,7 @@ export function slugify(name: string): string {
95
95
  */
96
96
  export function unusedName(): string {
97
97
  const { generateFallbackName } =
98
- require("./reactions.ts") as typeof import("./reactions.ts");
98
+ require("../../../core/reactions.ts") as typeof import("../../../core/reactions.ts");
99
99
  const taken = new Set(Object.keys(loadManifest().companions));
100
100
  for (let i = 0; i < 50; i++) {
101
101
  const n = generateFallbackName();
@@ -267,19 +267,6 @@ export function saveReaction(reaction: string, reason: string): void {
267
267
  writeFileSync(reactionFile(), JSON.stringify(state));
268
268
  }
269
269
 
270
- // ─── Identity resolution ─────────────────────────────────────────────────────
271
-
272
- export function resolveUserId(): string {
273
- try {
274
- const claudeJson = JSON.parse(
275
- readFileSync(join(homedir(), ".claude.json"), "utf8"),
276
- );
277
- return claudeJson.oauthAccount?.accountUuid ?? claudeJson.userID ?? "anon";
278
- } catch {
279
- return "anon";
280
- }
281
- }
282
-
283
270
  // ─── Config persistence (PR #6: tmux popup settings) ─────────────────────────
284
271
 
285
272
  export interface BuddyConfig {
@@ -341,7 +328,7 @@ export function writeStatusState(
341
328
  ): void {
342
329
  mkdirSync(STATE_DIR, { recursive: true });
343
330
  const { renderFace, RARITY_STARS } =
344
- require("./engine.ts") as typeof import("./engine.ts");
331
+ require("../../../core/engine.ts") as typeof import("../../../core/engine.ts");
345
332
  const state: StatusState = {
346
333
  name: companion.name,
347
334
  species: companion.bones.species,
@@ -358,52 +345,3 @@ export function writeStatusState(
358
345
  writeFileSync(join(STATE_DIR, "status.json"), JSON.stringify(state));
359
346
  }
360
347
 
361
- // ─── Claude Code settings.json patching (for buddy_statusline tool) ──────────
362
-
363
- export const CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
364
-
365
- /**
366
- * Write settings.statusLine pointing to the given buddy-status script.
367
- * Atomic via tmp + rename. Returns false if settings.json is unreachable.
368
- */
369
- export function setBuddyStatusLine(
370
- statusScript: string,
371
- settingsPath: string = CLAUDE_SETTINGS_PATH,
372
- ): boolean {
373
- try {
374
- const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
375
- settings.statusLine = {
376
- type: "command",
377
- command: statusScript,
378
- padding: 1,
379
- refreshInterval: 1,
380
- };
381
- const tmp = settingsPath + ".tmp";
382
- writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
383
- renameSync(tmp, settingsPath);
384
- return true;
385
- } catch {
386
- return false;
387
- }
388
- }
389
-
390
- /**
391
- * Remove settings.statusLine — but only if it points to buddy-status.sh.
392
- * Leaves foreign statusLines untouched. Returns false if no buddy line was
393
- * present or settings.json is unreachable.
394
- */
395
- export function unsetBuddyStatusLine(
396
- settingsPath: string = CLAUDE_SETTINGS_PATH,
397
- ): boolean {
398
- try {
399
- const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
400
- if (!settings.statusLine?.command?.includes("buddy-status.sh")) return false;
401
- delete settings.statusLine;
402
- const tmp = settingsPath + ".tmp";
403
- writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
404
- renameSync(tmp, settingsPath);
405
- return true;
406
- } catch {
407
- return false;
408
- }
409
- }
@@ -0,0 +1,64 @@
1
+ # Pi adapter
2
+
3
+ Native pi extension for `coding-buddy`.
4
+
5
+ ## Load locally during development
6
+
7
+ Project-local in pi:
8
+
9
+ ```bash
10
+ mkdir -p .pi/extensions
11
+ ln -sf ../../adapters/pi/index.ts .pi/extensions/coding-buddy.ts
12
+ pi
13
+ ```
14
+
15
+ Or run directly:
16
+
17
+ ```bash
18
+ pi -e ./adapters/pi/index.ts
19
+ ```
20
+
21
+ ## Install as a pi package
22
+
23
+ Because the root `package.json` now declares:
24
+
25
+ ```json
26
+ {
27
+ "pi": {
28
+ "extensions": ["./adapters/pi/index.ts"]
29
+ }
30
+ }
31
+ ```
32
+
33
+ pi can install this repo/package directly:
34
+
35
+ ```bash
36
+ pi install /absolute/path/to/claude-buddy
37
+ # or
38
+ pi install npm:@ramarivera/coding-buddy
39
+ ```
40
+
41
+ ## What the extension provides
42
+
43
+ - `/buddy`
44
+ - `/buddy pet`
45
+ - `/buddy stats`
46
+ - `/buddy rename <name>`
47
+ - `/buddy personality <text>`
48
+ - `/buddy off` / `/buddy on`
49
+ - `/buddy save [slot]`
50
+ - `/buddy summon [slot]`
51
+ - `/buddy list`
52
+ - `/buddy dismiss <slot>`
53
+ - `/buddy achievements`
54
+
55
+ Passive behavior:
56
+
57
+ - initializes on `session_start`
58
+ - reacts to buddy-name mentions on `input`
59
+ - reacts to some tool failures and large diffs on `tool_result`
60
+ - refreshes widget/status on `turn_end`
61
+
62
+ Persistence lives under:
63
+
64
+ - `~/.pi/agent/buddy/`