@ramarivera/coding-buddy 0.4.0-alpha.7 → 0.4.0-alpha.9

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 (43) hide show
  1. package/README.md +18 -39
  2. package/adapters/claude/hooks/buddy-comment.sh +4 -1
  3. package/adapters/claude/hooks/name-react.sh +4 -1
  4. package/adapters/claude/hooks/react.sh +4 -1
  5. package/adapters/claude/install/backup.ts +36 -118
  6. package/adapters/claude/install/disable.ts +9 -14
  7. package/adapters/claude/install/doctor.ts +26 -87
  8. package/adapters/claude/install/install.ts +39 -66
  9. package/adapters/claude/install/test-statusline.ts +8 -18
  10. package/adapters/claude/install/uninstall.ts +18 -26
  11. package/adapters/claude/plugin/marketplace.json +4 -4
  12. package/adapters/claude/plugin/plugin.json +3 -5
  13. package/adapters/claude/server/index.ts +132 -5
  14. package/adapters/claude/server/path.ts +12 -0
  15. package/adapters/claude/skills/buddy/SKILL.md +16 -1
  16. package/adapters/claude/statusline/buddy-status.sh +22 -3
  17. package/adapters/claude/storage/paths.ts +9 -0
  18. package/adapters/claude/storage/settings.ts +53 -3
  19. package/adapters/claude/storage/state.ts +22 -4
  20. package/adapters/pi/README.md +19 -0
  21. package/adapters/pi/events.ts +176 -19
  22. package/adapters/pi/index.ts +3 -1
  23. package/adapters/pi/logger.ts +52 -0
  24. package/adapters/pi/prompt.ts +18 -0
  25. package/adapters/pi/storage.ts +1 -0
  26. package/cli/biomes.ts +309 -0
  27. package/cli/buddy-shell.ts +818 -0
  28. package/cli/index.ts +7 -0
  29. package/cli/tui.tsx +2244 -0
  30. package/cli/upgrade.ts +213 -0
  31. package/core/model.ts +6 -0
  32. package/package.json +78 -62
  33. package/scripts/paths.sh +40 -0
  34. package/server/achievements.ts +15 -0
  35. package/server/art.ts +1 -0
  36. package/server/engine.ts +1 -0
  37. package/server/mcp-launcher.sh +16 -0
  38. package/server/path.ts +30 -0
  39. package/server/reactions.ts +1 -0
  40. package/server/state.ts +3 -0
  41. package/adapters/claude/popup/buddy-popup.sh +0 -92
  42. package/adapters/claude/popup/buddy-render.sh +0 -540
  43. package/adapters/claude/popup/popup-manager.sh +0 -355
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-buddy",
3
- "version": "0.4.0-alpha.7"
3
+ "version": "0.4.0-alpha.9",
4
4
  "description": "Permanent coding companion for Claude Code \u2014 survives any update. MCP-based terminal pet with ASCII art, stats, reactions, and personality.",
5
5
  "author": {
6
6
  "name": "1270011"
@@ -19,10 +19,8 @@
19
19
  "mcpServers": {
20
20
  "claude-buddy": {
21
21
  "type": "stdio",
22
- "command": "bun",
23
- "args": [
24
- "adapters/claude/server/index.ts"
25
- ]
22
+ "command": "${CLAUDE_PLUGIN_ROOT}/server/mcp-launcher.sh",
23
+ "args": []
26
24
  }
27
25
  }
28
26
  }
@@ -41,7 +41,12 @@ import {
41
41
  listCompanionSlots,
42
42
  } from "../storage/state.ts";
43
43
  import { resolveUserId } from "../storage/identity.ts";
44
- import { setBuddyStatusLine, unsetBuddyStatusLine } from "../storage/settings.ts";
44
+ import { cleanupPluginState, setBuddyStatusLine, unsetBuddyStatusLine } from "../storage/settings.ts";
45
+ import {
46
+ buddyStateDir,
47
+ claudeConfigDir,
48
+ claudeSettingsPath,
49
+ } from "./path.ts";
45
50
  import {
46
51
  getReaction, generatePersonalityPrompt,
47
52
  } from "../../../core/reactions.ts";
@@ -329,8 +334,8 @@ server.tool(
329
334
  " /buddy summon Summon a saved buddy (omit slot for random)",
330
335
  " /buddy save Save current buddy to a named slot",
331
336
  " /buddy list List all saved buddies",
337
+ " /buddy pick Generate a new random buddy (optional: species, rarity)",
332
338
  " /buddy dismiss Remove a saved buddy slot",
333
- " /buddy pick Launch interactive TUI picker (! bun run pick)",
334
339
  " /buddy frequency Show or set comment cooldown (tmux only)",
335
340
  " /buddy style Show or set bubble style (tmux only)",
336
341
  " /buddy position Show or set bubble position (tmux only)",
@@ -386,7 +391,7 @@ server.tool(
386
391
 
387
392
  server.tool(
388
393
  "buddy_style",
389
- "Configure the popup appearance. Returns current settings if called without arguments.",
394
+ "Configure the buddy bubble appearance. Returns current settings if called without arguments.",
390
395
  {
391
396
  style: z
392
397
  .enum(["classic", "round"])
@@ -403,7 +408,7 @@ server.tool(
403
408
  showRarity: z
404
409
  .boolean()
405
410
  .optional()
406
- .describe("Show or hide the stars + rarity line in the popup"),
411
+ .describe("Show or hide the stars + rarity line in the status line"),
407
412
  },
408
413
  async ({ style, position, showRarity }) => {
409
414
  if (
@@ -500,7 +505,10 @@ server.tool(
500
505
  content: [
501
506
  {
502
507
  type: "text",
503
- text: "Status line enabled! Restart Claude Code to see your buddy in the status line.",
508
+ text:
509
+ "Status line enabled! Restart Claude Code to see your buddy in the status line.\n\n" +
510
+ `Note: this writes an entry to ${claudeSettingsPath()} that \`claude plugin uninstall\` does not remove. ` +
511
+ "Run `/buddy uninstall` before uninstalling the plugin to clean it up.",
504
512
  },
505
513
  ],
506
514
  };
@@ -518,6 +526,51 @@ server.tool(
518
526
  },
519
527
  );
520
528
 
529
+ // ─── Tool: buddy_uninstall ───────────────────────────────────────────────────
530
+
531
+ server.tool(
532
+ "buddy_uninstall",
533
+ "Clean up claude-buddy's writes to Claude Code's settings.json and transient session files in the buddy state dir (resolved via CLAUDE_CONFIG_DIR), in preparation for `claude plugin uninstall`. Companion data (menagerie, status, config) is intentionally preserved so reinstalling restores the buddy. The tool only cleans the plugin's own settings — it never removes a foreign statusLine.",
534
+ {},
535
+ async () => {
536
+ const result = cleanupPluginState();
537
+
538
+ const settingsPath = claudeSettingsPath();
539
+ const stateDir = buddyStateDir();
540
+ const pluginsCacheDir = join(claudeConfigDir(), "plugins", "cache", "claude-buddy");
541
+
542
+ const lines: string[] = [];
543
+ lines.push("claude-buddy: settings.json cleanup complete.");
544
+ lines.push("");
545
+ lines.push(
546
+ result.statusLineRemoved
547
+ ? ` \u2713 statusLine entry removed from ${settingsPath}`
548
+ : " \u2014 no buddy statusLine was present (nothing to remove)",
549
+ );
550
+ if (result.foreignStatusLineKept) {
551
+ lines.push(
552
+ " \u2713 a non-buddy statusLine was detected and left untouched",
553
+ );
554
+ }
555
+ lines.push(
556
+ ` \u2713 ${result.transientFilesRemoved} transient session file(s) removed from ${stateDir}`,
557
+ );
558
+ lines.push(` \u2014 companion data at ${stateDir} preserved`);
559
+ lines.push("");
560
+ lines.push("Now run these commands via the Bash tool, in order:");
561
+ lines.push("");
562
+ lines.push(" claude plugin uninstall claude-buddy@claude-buddy");
563
+ lines.push(" claude plugin marketplace remove claude-buddy");
564
+ lines.push(` rm -rf ${pluginsCacheDir}`);
565
+ lines.push("");
566
+ lines.push(
567
+ "After those three commands the plugin is fully removed. Restart Claude Code to apply.",
568
+ );
569
+
570
+ return { content: [{ type: "text", text: lines.join("\n") }] };
571
+ },
572
+ );
573
+
521
574
  // ─── Tool: buddy_achievements ────────────────────────────────────────────────
522
575
 
523
576
  server.tool(
@@ -704,6 +757,80 @@ server.tool(
704
757
  },
705
758
  );
706
759
 
760
+ // ─── Tool: buddy_pick ────────────────────────────────────────────────────────
761
+
762
+ server.tool(
763
+ "buddy_pick",
764
+ "Generate a new random buddy and add it to the menagerie. Optionally filter by species and/or rarity. The new buddy becomes the active one.",
765
+ {
766
+ species: z.enum(SPECIES).optional().describe(
767
+ "Desired species (e.g. 'turtle', 'cat', 'dragon'). If omitted, any species.",
768
+ ),
769
+ rarity: z.enum(RARITIES).optional().describe(
770
+ "Desired rarity (e.g. 'legendary', 'epic', 'rare'). If omitted, any rarity. Higher rarities need more attempts and may take a moment.",
771
+ ),
772
+ name: z.string().min(1).max(14).optional().describe(
773
+ "Name for the new buddy (1-14 chars). If omitted, a random name is chosen.",
774
+ ),
775
+ },
776
+ async ({ species, rarity, name }) => {
777
+ const { randomBytes } = await import("crypto");
778
+
779
+ const maxAttempts =
780
+ rarity === "legendary" ? 5_000_000 :
781
+ rarity === "epic" ? 2_000_000 :
782
+ rarity === "rare" ? 1_000_000 : 500_000;
783
+
784
+ let bones = null;
785
+ let userId = "";
786
+
787
+ for (let i = 0; i < maxAttempts; i++) {
788
+ userId = randomBytes(16).toString("hex");
789
+ const candidate = generateBones(userId);
790
+ if (species && candidate.species !== species) continue;
791
+ if (rarity && candidate.rarity !== rarity) continue;
792
+ bones = candidate;
793
+ break;
794
+ }
795
+
796
+ if (!bones) {
797
+ return {
798
+ content: [{ type: "text", text: `No match found after ${maxAttempts.toLocaleString()} attempts. Try broader criteria (e.g. drop the rarity filter, or pick a different species).` }],
799
+ };
800
+ }
801
+
802
+ const buddyName = name ?? unusedName();
803
+ const slot = slugify(buddyName);
804
+
805
+ if (loadCompanionSlot(slot)) {
806
+ return {
807
+ content: [{ type: "text", text: `A buddy in slot "${slot}" already exists. Pick a different name.` }],
808
+ };
809
+ }
810
+
811
+ const companion: Companion = {
812
+ bones,
813
+ name: buddyName,
814
+ personality: `A ${bones.rarity} ${bones.species} who watches code with quiet intensity.`,
815
+ hatchedAt: Date.now(),
816
+ userId,
817
+ };
818
+
819
+ saveCompanionSlot(companion, slot);
820
+ saveActiveSlot(slot);
821
+ writeStatusState(companion, `*${buddyName} hatches*`);
822
+
823
+ const card = renderCompanionCardMarkdown(
824
+ companion.bones,
825
+ companion.name,
826
+ companion.personality,
827
+ `*${buddyName} hatches*`,
828
+ );
829
+
830
+ return { content: [{ type: "text", text: card }] };
831
+ },
832
+ );
833
+
707
834
  // ─── Resource: buddy://companion ────────────────────────────────────────────
708
835
 
709
836
  server.resource(
@@ -0,0 +1,12 @@
1
+ export {
2
+ getBuddyStateDir as buddyStateDir,
3
+ getClaudeConfigDir as claudeConfigDir,
4
+ getClaudeJsonPath as claudeUserConfigPath,
5
+ getClaudeSettingsPath as claudeSettingsPath,
6
+ toUnixPath,
7
+ } from "../storage/paths.ts";
8
+
9
+ export function claudeSkillDir(_name: string): string {
10
+ const { getBuddySkillDir } = require("../storage/paths.ts") as typeof import("../storage/paths.ts");
11
+ return getBuddySkillDir();
12
+ }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: buddy
3
3
  description: "Show, pet, or manage your coding companion. Use when the user types /buddy or mentions their companion by name."
4
- argument-hint: "[show|pet|stats|help|off|on|rename <name>|personality <text>|achievements|summon [slot]|save [slot]|list|dismiss <slot>|pick|frequency [seconds]|style [classic|round]|position [top|left]|rarity [on|off]|statusline [on|off]]"
4
+ argument-hint: "[show|pet|stats|help|off|on|rename <name>|personality <text>|achievements|summon [slot]|save [slot]|list|dismiss <slot>|pick|frequency [seconds]|style [classic|round]|position [top|left]|rarity [on|off]|statusline [on|off]|uninstall]"
5
5
  allowed-tools: mcp__claude_buddy__*
6
6
  ---
7
7
 
@@ -41,6 +41,7 @@ Based on `$ARGUMENTS`:
41
41
  | `statusline` | Call `buddy_statusline` with no args (show current) |
42
42
  | `statusline on` | Call `buddy_statusline` with enabled=true |
43
43
  | `statusline off` | Call `buddy_statusline` with enabled=false |
44
+ | `uninstall` | Run the uninstall sequence (see **Uninstall Orchestration** below) |
44
45
 
45
46
  ## CRITICAL OUTPUT RULES
46
47
 
@@ -57,3 +58,17 @@ The MCP tools return pre-formatted ASCII art with ANSI colors, box-drawing chara
57
58
  **Just output the raw text content from the tool result. Nothing else.** The ASCII art IS the response.
58
59
 
59
60
  If the user mentions the buddy's name in normal conversation, call `buddy_react` with reason "turn" and display the result verbatim.
61
+
62
+ ## Uninstall Orchestration
63
+
64
+ When the user invokes `/buddy uninstall`, run this sequence **in order** — do not skip steps, do not ask for confirmation between steps:
65
+
66
+ 1. Call the MCP tool `buddy_uninstall`. Display its output verbatim.
67
+ 2. Run via Bash tool: `claude plugin uninstall claude-buddy@claude-buddy`
68
+ 3. Run via Bash tool: `claude plugin marketplace remove claude-buddy`
69
+ 4. Run via Bash tool: `rm -rf ~/.claude/plugins/cache/claude-buddy`
70
+ 5. Tell the user: uninstall is complete; companion data is kept at `~/.claude-buddy/`; restart Claude Code to release the plugin.
71
+
72
+ If any Bash step fails (non-zero exit), report the error but continue with the remaining steps — each step is independent and always-safe to run.
73
+
74
+ Do not call `buddy_uninstall` for any other command than `/buddy uninstall`. Never call it proactively.
@@ -8,8 +8,15 @@
8
8
  # - refreshInterval: 1s in settings.json cycles the animation
9
9
  #
10
10
  # Uses Braille Blank (U+2800) for padding — survives JS .trim()
11
+ #
12
+ # When running inside buddy-shell (the PTY wrapper), skip status line rendering
13
+ # so the buddy doesn't show up twice (once in status line, once in wrapper panel).
14
+ [ "$BUDDY_SHELL" = "1" ] && exit 0
15
+
16
+ # shellcheck source=../scripts/paths.sh
17
+ source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh"
11
18
 
12
- STATE="$HOME/.claude-buddy/status.json"
19
+ STATE="$BUDDY_STATE_DIR/status.json"
13
20
  # Session ID: sanitized tmux pane number, or "default" outside tmux
14
21
  SID="${TMUX_PANE#%}"
15
22
  SID="${SID:-default}"
@@ -66,11 +73,23 @@ PID=$$
66
73
  for _ in 1 2 3 4 5; do
67
74
  PID=$(ps -o ppid= -p "$PID" 2>/dev/null | tr -d ' ')
68
75
  [ -z "$PID" ] || [ "$PID" = "1" ] && break
76
+
77
+ # Linux: read PTY device from /proc
69
78
  PTY=$(readlink "/proc/${PID}/fd/0" 2>/dev/null)
70
79
  if [ -c "$PTY" ] 2>/dev/null; then
71
80
  COLS=$(stty size < "$PTY" 2>/dev/null | awk '{print $2}')
72
81
  [ "${COLS:-0}" -gt 40 ] 2>/dev/null && break
73
82
  fi
83
+
84
+ # macOS: /proc doesn't exist — get TTY name from process table
85
+ TTY_NAME=$(ps -o tty= -p "$PID" 2>/dev/null | tr -d ' ')
86
+ if [ -n "$TTY_NAME" ] && [ "$TTY_NAME" != "??" ] && [ "$TTY_NAME" != "?" ]; then
87
+ TTY_DEV="/dev/$TTY_NAME"
88
+ if [ -c "$TTY_DEV" ] 2>/dev/null; then
89
+ COLS=$(stty size < "$TTY_DEV" 2>/dev/null | awk '{print $2}')
90
+ [ "${COLS:-0}" -gt 40 ] 2>/dev/null && break
91
+ fi
92
+ fi
74
93
  done
75
94
  [ "${COLS:-0}" -lt 40 ] 2>/dev/null && COLS=${COLUMNS:-0}
76
95
  [ "${COLS:-0}" -lt 40 ] 2>/dev/null && COLS=125
@@ -215,9 +234,9 @@ BUBBLE=""
215
234
  if [ -n "$ACHIEVEMENT" ] && [ "$ACHIEVEMENT" != "null" ] && [ "$ACHIEVEMENT" != "" ]; then
216
235
  BUBBLE=$'\xf0\x9f\x8f\x86'" $ACHIEVEMENT"
217
236
  fi
218
- REACTION_FILE="$HOME/.claude-buddy/reaction.$SID.json"
237
+ REACTION_FILE="$BUDDY_STATE_DIR/reaction.$SID.json"
219
238
  REACTION_TTL=0
220
- CONFIG_FILE="$HOME/.claude-buddy/config.json"
239
+ CONFIG_FILE="$BUDDY_STATE_DIR/config.json"
221
240
  if [ -f "$CONFIG_FILE" ]; then
222
241
  _ttl=$(jq -r '.reactionTTL // 0' "$CONFIG_FILE" 2>/dev/null || echo 0)
223
242
  case "$_ttl" in ''|*[!0-9]*) ;; *) REACTION_TTL="$_ttl" ;; esac
@@ -1,6 +1,10 @@
1
1
  import { homedir } from "os";
2
2
  import { join, resolve } from "path";
3
3
 
4
+ export function toUnixPath(path: string): string {
5
+ return path.replace(/\\/g, "/");
6
+ }
7
+
4
8
  function claudeConfigDirEnv(): string | null {
5
9
  const value = process.env.CLAUDE_CONFIG_DIR?.trim();
6
10
  return value ? resolve(value) : null;
@@ -22,3 +26,8 @@ export function getClaudeSettingsPath(): string {
22
26
  export function getBuddySkillDir(): string {
23
27
  return join(getClaudeConfigDir(), "skills", "buddy");
24
28
  }
29
+
30
+ export function getBuddyStateDir(): string {
31
+ const configDir = claudeConfigDirEnv();
32
+ return configDir ? join(configDir, "buddy-state") : join(homedir(), ".claude-buddy");
33
+ }
@@ -1,5 +1,6 @@
1
- import { readFileSync, writeFileSync, renameSync } from "fs";
2
- import { getClaudeSettingsPath } from "./paths.ts";
1
+ import { existsSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { getBuddyStateDir, getClaudeSettingsPath, toUnixPath } from "./paths.ts";
3
4
 
4
5
  export const CLAUDE_SETTINGS_PATH = getClaudeSettingsPath();
5
6
 
@@ -11,7 +12,7 @@ export function setBuddyStatusLine(
11
12
  const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
12
13
  settings.statusLine = {
13
14
  type: "command",
14
- command: statusScript,
15
+ command: toUnixPath(statusScript),
15
16
  padding: 1,
16
17
  refreshInterval: 1,
17
18
  };
@@ -39,3 +40,52 @@ export function unsetBuddyStatusLine(
39
40
  return false;
40
41
  }
41
42
  }
43
+
44
+ export interface CleanupResult {
45
+ statusLineRemoved: boolean;
46
+ foreignStatusLineKept: boolean;
47
+ transientFilesRemoved: number;
48
+ }
49
+
50
+ const TRANSIENT_PREFIXES = [
51
+ "popup-stop.",
52
+ "popup-resize.",
53
+ "popup-env.",
54
+ "popup-scroll.",
55
+ "popup-reopen-pid.",
56
+ "reaction.",
57
+ ".last_reaction.",
58
+ ".last_comment.",
59
+ ];
60
+
61
+ export function cleanupPluginState(
62
+ settingsPath: string = CLAUDE_SETTINGS_PATH,
63
+ stateDir: string = getBuddyStateDir(),
64
+ ): CleanupResult {
65
+ const statusLineRemoved = unsetBuddyStatusLine(settingsPath);
66
+
67
+ let foreignStatusLineKept = false;
68
+ try {
69
+ const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
70
+ const cmd = settings.statusLine?.command;
71
+ if (cmd && !cmd.includes("buddy-status.sh")) foreignStatusLineKept = true;
72
+ } catch {
73
+ // ignore missing settings
74
+ }
75
+
76
+ let transientFilesRemoved = 0;
77
+ try {
78
+ if (existsSync(stateDir)) {
79
+ for (const file of readdirSync(stateDir)) {
80
+ if (TRANSIENT_PREFIXES.some((prefix) => file.startsWith(prefix))) {
81
+ rmSync(join(stateDir, file), { force: true });
82
+ transientFilesRemoved++;
83
+ }
84
+ }
85
+ }
86
+ } catch {
87
+ // ignore unreadable state dir
88
+ }
89
+
90
+ return { statusLineRemoved, foreignStatusLineKept, transientFilesRemoved };
91
+ }
@@ -1,8 +1,12 @@
1
1
  /**
2
- * State management — reads/writes companion data to ~/.claude-buddy/
2
+ * State management — reads/writes companion data to the buddy state dir.
3
+ *
4
+ * The state dir resolves via server/paths.ts (honors CLAUDE_CONFIG_DIR).
5
+ * Default: ~/.claude-buddy/. With CLAUDE_CONFIG_DIR set:
6
+ * $CLAUDE_CONFIG_DIR/buddy-state/.
3
7
  *
4
8
  * Storage layout (v3 — single manifest):
5
- * ~/.claude-buddy/
9
+ * <state-dir>/
6
10
  * menagerie.json <- SSOT: { active, companions: { [slot]: Companion } }
7
11
  * reaction.$SID.json <- transient reaction state (session-scoped)
8
12
  * status.json <- compact state for the status-line shell script
@@ -23,12 +27,13 @@ import {
23
27
  existsSync,
24
28
  readdirSync,
25
29
  renameSync,
30
+ rmSync,
26
31
  } from "fs";
27
32
  import { join } from "path";
28
- import { homedir } from "os";
29
33
  import type { Companion } from "../../../core/engine.ts";
34
+ import { getBuddyStateDir } from "./paths.ts";
30
35
 
31
- const STATE_DIR = join(homedir(), ".claude-buddy");
36
+ export const STATE_DIR = getBuddyStateDir();
32
37
  const MANIFEST_FILE = join(STATE_DIR, "menagerie.json");
33
38
  const CONFIG_FILE = join(STATE_DIR, "config.json");
34
39
 
@@ -145,6 +150,19 @@ export function saveCompanionSlot(companion: Companion, slot: string): void {
145
150
  saveManifest(m);
146
151
  }
147
152
 
153
+ /**
154
+ * UPDATE an existing (possibly non-active) companion slot.
155
+ * Throws if the slot does not exist.
156
+ */
157
+ export function updateCompanionSlot(slot: string, companion: Companion): void {
158
+ const m = loadManifest();
159
+ if (!m.companions[slot]) {
160
+ throw new Error(`Slot "${slot}" does not exist.`);
161
+ }
162
+ m.companions[slot] = companion;
163
+ saveManifest(m);
164
+ }
165
+
148
166
  export function deleteCompanionSlot(slot: string): void {
149
167
  const m = loadManifest();
150
168
  delete m.companions[slot];
@@ -62,3 +62,22 @@ Passive behavior:
62
62
  Persistence lives under:
63
63
 
64
64
  - `~/.pi/agent/buddy/`
65
+
66
+ ## Config
67
+
68
+ Buddy config is stored in:
69
+
70
+ - `~/.pi/agent/buddy/config.json`
71
+
72
+ You can optionally force the end-of-turn buddy comment generator to use a different model than the main pi session:
73
+
74
+ ```json
75
+ {
76
+ "turnCommentModel": {
77
+ "provider": "google",
78
+ "model": "gemini-2.5-flash"
79
+ }
80
+ }
81
+ ```
82
+
83
+ If `turnCommentModel` is unset, or the configured model cannot be found, buddy falls back to the active session model.