@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.
- package/README.md +18 -39
- package/adapters/claude/hooks/buddy-comment.sh +4 -1
- package/adapters/claude/hooks/name-react.sh +4 -1
- package/adapters/claude/hooks/react.sh +4 -1
- package/adapters/claude/install/backup.ts +36 -118
- package/adapters/claude/install/disable.ts +9 -14
- package/adapters/claude/install/doctor.ts +26 -87
- package/adapters/claude/install/install.ts +39 -66
- package/adapters/claude/install/test-statusline.ts +8 -18
- package/adapters/claude/install/uninstall.ts +18 -26
- package/adapters/claude/plugin/marketplace.json +4 -4
- package/adapters/claude/plugin/plugin.json +3 -5
- package/adapters/claude/server/index.ts +132 -5
- package/adapters/claude/server/path.ts +12 -0
- package/adapters/claude/skills/buddy/SKILL.md +16 -1
- package/adapters/claude/statusline/buddy-status.sh +22 -3
- package/adapters/claude/storage/paths.ts +9 -0
- package/adapters/claude/storage/settings.ts +53 -3
- package/adapters/claude/storage/state.ts +22 -4
- package/adapters/pi/README.md +19 -0
- package/adapters/pi/events.ts +176 -19
- package/adapters/pi/index.ts +3 -1
- package/adapters/pi/logger.ts +52 -0
- package/adapters/pi/prompt.ts +18 -0
- package/adapters/pi/storage.ts +1 -0
- package/cli/biomes.ts +309 -0
- package/cli/buddy-shell.ts +818 -0
- package/cli/index.ts +7 -0
- package/cli/tui.tsx +2244 -0
- package/cli/upgrade.ts +213 -0
- package/core/model.ts +6 -0
- package/package.json +78 -62
- package/scripts/paths.sh +40 -0
- package/server/achievements.ts +15 -0
- package/server/art.ts +1 -0
- package/server/engine.ts +1 -0
- package/server/mcp-launcher.sh +16 -0
- package/server/path.ts +30 -0
- package/server/reactions.ts +1 -0
- package/server/state.ts +3 -0
- package/adapters/claude/popup/buddy-popup.sh +0 -92
- package/adapters/claude/popup/buddy-render.sh +0 -540
- 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.
|
|
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": "
|
|
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
|
|
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
|
|
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:
|
|
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="$
|
|
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="$
|
|
237
|
+
REACTION_FILE="$BUDDY_STATE_DIR/reaction.$SID.json"
|
|
219
238
|
REACTION_TTL=0
|
|
220
|
-
CONFIG_FILE="$
|
|
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,
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
*
|
|
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 =
|
|
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];
|
package/adapters/pi/README.md
CHANGED
|
@@ -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.
|