@ramarivera/coding-buddy 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +6 -6
- package/.claude-plugin/plugin.json +4 -4
- package/README.md +12 -11
- package/adapters/omp/renderers.ts +61 -4
- package/adapters/omp/ui.ts +1 -1
- package/cli/install.ts +12 -6
- package/cli/pick.ts +26 -8
- package/cli/upgrade.ts +44 -11
- package/hooks/buddy-comment.sh +10 -85
- package/hooks/file-type-react.sh +10 -400
- package/hooks/hooks.json +19 -5
- package/hooks/legacy/file-type-react.sh +403 -0
- package/hooks/legacy/mood-react.sh +210 -0
- package/hooks/legacy/name-react.sh +178 -0
- package/hooks/legacy/react.sh +1364 -0
- package/hooks/mood-react.sh +10 -207
- package/hooks/name-react.sh +10 -178
- package/hooks/react.sh +10 -1367
- package/hooks/suggest.sh +10 -54
- package/package.json +3 -3
- package/scripts/paths.sh +16 -1
- package/server/art.ts +1 -1
- package/server/hooks/buddy-comment.ts +93 -0
- package/server/hooks/common.ts +95 -0
- package/server/hooks/legacy-bash.ts +34 -0
- package/server/hooks/suggest.ts +60 -0
- package/server/state.ts +14 -2
- package/statusline/buddy-status.sh +20 -12
- package/statusline/combined-status.sh +9 -8
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coding-buddy",
|
|
3
3
|
"owner": {
|
|
4
|
-
"name": "
|
|
4
|
+
"name": "ramarivera"
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "Persistent coding companion for Claude Code, Pi, and Oh My Pi (requires bun: https://bun.sh/install)",
|
|
8
|
-
"version": "0.
|
|
8
|
+
"version": "0.7.1"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "coding-buddy",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"description": "Persistent coding companion for Claude Code, Pi, and Oh My Pi. MCP-based terminal pet with ASCII art, stats, reactions, and personality. Requires bun runtime (https://bun.sh/install).",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.7.1",
|
|
16
16
|
"author": {
|
|
17
|
-
"name": "
|
|
17
|
+
"name": "ramarivera"
|
|
18
18
|
},
|
|
19
|
-
"homepage": "https://github.com/
|
|
20
|
-
"repository": "https://github.com/
|
|
19
|
+
"homepage": "https://github.com/ramarivera/coding-buddy#readme",
|
|
20
|
+
"repository": "https://github.com/ramarivera/coding-buddy",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"keywords": [
|
|
23
23
|
"coding-buddy",
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coding-buddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Persistent coding companion for Claude Code, Pi, and Oh My Pi. MCP-based terminal pet with ASCII art, stats, reactions, and personality.",
|
|
5
5
|
"author": {
|
|
6
|
-
"name": "
|
|
6
|
+
"name": "ramarivera"
|
|
7
7
|
},
|
|
8
8
|
"license": "MIT",
|
|
9
|
-
"repository": "https://github.com/
|
|
10
|
-
"homepage": "https://github.com/
|
|
9
|
+
"repository": "https://github.com/ramarivera/coding-buddy",
|
|
10
|
+
"homepage": "https://github.com/ramarivera/coding-buddy#readme",
|
|
11
11
|
"keywords": [
|
|
12
12
|
"coding-buddy",
|
|
13
13
|
"claude-code",
|
package/README.md
CHANGED
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
<!-- BADGES -->
|
|
14
14
|
<!-- ============================================================ -->
|
|
15
15
|
|
|
16
|
-
[](https://github.com/ramarivera/coding-buddy/releases)
|
|
17
|
+
[](LICENSE)
|
|
18
|
+
[](https://github.com/ramarivera/coding-buddy/stargazers)
|
|
19
19
|
[](https://claude.ai/code)
|
|
20
20
|
[](#requirements)
|
|
21
21
|
[](https://modelcontextprotocol.io)
|
|
@@ -103,7 +103,7 @@ The `claude-buddy` package ended at **v0.5.2**.
|
|
|
103
103
|
npx -y @ramarivera/coding-buddy
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
This installs the MCP server, skill, hooks, and status line. Restart Claude Code and type `/buddy`.
|
|
106
|
+
This installs the MCP server, skill, hooks, and status line. Claude Code hooks are registered as thin bash wrappers with 15s timeouts; the wrappers locate Bun in bare hook environments and delegate behavior to TypeScript or the phase-2 legacy bash bridge. Restart Claude Code and type `/buddy`.
|
|
107
107
|
|
|
108
108
|
### Pi
|
|
109
109
|
|
|
@@ -124,7 +124,7 @@ OMP registers the package's native extension and stores companion state under `~
|
|
|
124
124
|
### From source
|
|
125
125
|
|
|
126
126
|
```bash
|
|
127
|
-
git clone https://github.com/
|
|
127
|
+
git clone https://github.com/ramarivera/coding-buddy coding-buddy
|
|
128
128
|
cd coding-buddy
|
|
129
129
|
bun install
|
|
130
130
|
bun run install-buddy
|
|
@@ -255,7 +255,8 @@ MCP is an industry-standard protocol. Skills are Markdown files. Hooks and statu
|
|
|
255
255
|
coding-buddy/
|
|
256
256
|
├── server/ # MCP server — tools, engine, art, reactions, state
|
|
257
257
|
├── skills/buddy/ # /buddy slash command
|
|
258
|
-
├── hooks/ #
|
|
258
|
+
├── hooks/ # Thin Claude Code hook wrappers and phase-2 legacy bash hooks
|
|
259
|
+
├── server/hooks/ # Hook behavior implemented in TypeScript
|
|
259
260
|
├── statusline/ # Animated right-aligned buddy display
|
|
260
261
|
└── cli/ # install, show, hunt, verify, doctor, backup, uninstall
|
|
261
262
|
```
|
|
@@ -389,7 +390,7 @@ jq '.mcpServers["claude-buddy"]' ~/.claude.json
|
|
|
389
390
|
Known MVP issue on some terminal/platform combos — different terminals render Braille Pattern Blank (U+2800) at different widths.
|
|
390
391
|
|
|
391
392
|
To help us fix it:
|
|
392
|
-
1. Run `bun run doctor` and paste output in a [new issue](https://github.com/
|
|
393
|
+
1. Run `bun run doctor` and paste output in a [new issue](https://github.com/ramarivera/coding-buddy/issues/new)
|
|
393
394
|
2. Run `bun run test-statusline` and screenshot the result
|
|
394
395
|
3. Then `bun run test-statusline restore`
|
|
395
396
|
|
|
@@ -443,15 +444,15 @@ bun run uninstall # full clean removal
|
|
|
443
444
|
|
|
444
445
|
Thank you to everyone who helped bring buddies back to life.
|
|
445
446
|
|
|
446
|
-
<a href="https://github.com/
|
|
447
|
-
<img src="https://contrib.rocks/image?repo=
|
|
447
|
+
<a href="https://github.com/ramarivera/coding-buddy/graphs/contributors">
|
|
448
|
+
<img src="https://contrib.rocks/image?repo=ramarivera/coding-buddy" alt="Contributors" />
|
|
448
449
|
</a>
|
|
449
450
|
|
|
450
|
-
<sub>Automatically generated from the [contributors graph](https://github.com/
|
|
451
|
+
<sub>Automatically generated from the [contributors graph](https://github.com/ramarivera/coding-buddy/graphs/contributors) via [contrib.rocks](https://contrib.rocks).</sub>
|
|
451
452
|
|
|
452
453
|
<br>
|
|
453
454
|
|
|
454
|
-
Want to help? New species, better reactions, bugfixes, wild new features — [PRs welcome](https://github.com/
|
|
455
|
+
Want to help? New species, better reactions, bugfixes, wild new features — [PRs welcome](https://github.com/ramarivera/coding-buddy/pulls).
|
|
455
456
|
|
|
456
457
|
<br>
|
|
457
458
|
|
|
@@ -7,11 +7,68 @@ function trimArt(line: string): string {
|
|
|
7
7
|
return line.replace(/\s+$/g, "");
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const REACTION_WIDTH = 44;
|
|
11
|
+
|
|
12
|
+
function displayWidth(text: string): number {
|
|
13
|
+
let width = 0;
|
|
14
|
+
for (const character of text) {
|
|
15
|
+
width += /\p{Emoji_Presentation}/u.test(character) ? 2 : 1;
|
|
16
|
+
}
|
|
17
|
+
return width;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function truncateToWidth(text: string, width: number): string {
|
|
21
|
+
let result = "";
|
|
22
|
+
let currentWidth = 0;
|
|
23
|
+
for (const character of text) {
|
|
24
|
+
const characterWidth = /\p{Emoji_Presentation}/u.test(character) ? 2 : 1;
|
|
25
|
+
if (currentWidth + characterWidth > width) break;
|
|
26
|
+
result += character;
|
|
27
|
+
currentWidth += characterWidth;
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function wrapReaction(reaction: string): string[] {
|
|
33
|
+
const lines: string[] = [];
|
|
34
|
+
let current = "";
|
|
35
|
+
|
|
36
|
+
for (const word of reaction.trim().split(/\s+/)) {
|
|
37
|
+
if (!word) continue;
|
|
38
|
+
if (displayWidth(word) > REACTION_WIDTH) {
|
|
39
|
+
if (current) lines.push(current);
|
|
40
|
+
lines.push(`${truncateToWidth(word, REACTION_WIDTH - 1)}…`);
|
|
41
|
+
current = "";
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const candidate = current ? `${current} ${word}` : word;
|
|
46
|
+
if (displayWidth(candidate) > REACTION_WIDTH) {
|
|
47
|
+
lines.push(current);
|
|
48
|
+
current = word;
|
|
49
|
+
} else {
|
|
50
|
+
current = candidate;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (current) lines.push(current);
|
|
55
|
+
return lines;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderReactionBubble(reaction: string): string[] {
|
|
59
|
+
const lines = wrapReaction(`💬 ${reaction}`);
|
|
60
|
+
const border = `╭${"─".repeat(REACTION_WIDTH + 2)}╮`;
|
|
61
|
+
return [
|
|
62
|
+
border,
|
|
63
|
+
...lines.map((line) => `│ ${line}${" ".repeat(Math.max(0, REACTION_WIDTH - displayWidth(line)))} │`),
|
|
64
|
+
`╰${"─".repeat(REACTION_WIDTH + 2)}╯`,
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function renderBuddyStatus(companion: Companion): string {
|
|
11
69
|
const shiny = companion.bones.shiny ? " ✨" : "";
|
|
12
70
|
const stars = RARITY_STARS[companion.bones.rarity];
|
|
13
|
-
|
|
14
|
-
return `${trimArt(getArtFrame(companion.bones.species, companion.bones.eye, Date.now())[0])} ${companion.name} ${stars}${shiny}${suffix}`;
|
|
71
|
+
return `${trimArt(getArtFrame(companion.bones.species, companion.bones.eye, Date.now())[0])} ${companion.name} ${stars}${shiny}`;
|
|
15
72
|
}
|
|
16
73
|
|
|
17
74
|
export function renderBuddyWidget(
|
|
@@ -33,7 +90,7 @@ export function renderBuddyWidget(
|
|
|
33
90
|
];
|
|
34
91
|
|
|
35
92
|
if (reaction?.reaction) {
|
|
36
|
-
lines.push("",
|
|
93
|
+
lines.push("", ...renderReactionBubble(reaction.reaction));
|
|
37
94
|
}
|
|
38
95
|
|
|
39
96
|
if (achievements.length > 0) {
|
package/adapters/omp/ui.ts
CHANGED
|
@@ -27,7 +27,7 @@ export class OmpBuddyUI {
|
|
|
27
27
|
): void {
|
|
28
28
|
const currentReaction = reaction ?? null;
|
|
29
29
|
const muted = this.storage.isMuted();
|
|
30
|
-
const status = renderBuddyStatus(companion
|
|
30
|
+
const status = renderBuddyStatus(companion);
|
|
31
31
|
ctx.ui.setStatus("buddy", muted ? `${status} [muted]` : status);
|
|
32
32
|
ctx.ui.setWidget(
|
|
33
33
|
"buddy",
|
package/cli/install.ts
CHANGED
|
@@ -35,6 +35,7 @@ const SETTINGS_FILE = claudeSettingsPath();
|
|
|
35
35
|
const BUDDY_DIR = claudeSkillDir("buddy");
|
|
36
36
|
const CLAUDE_JSON_PATH = claudeUserConfigPath();
|
|
37
37
|
const PROJECT_ROOT = resolve(dirname(import.meta.dir));
|
|
38
|
+
const HOOK_TIMEOUT_SECONDS = 15;
|
|
38
39
|
|
|
39
40
|
function banner() {
|
|
40
41
|
console.log(`
|
|
@@ -187,6 +188,11 @@ function installHooks(settings: Record<string, any>) {
|
|
|
187
188
|
const suggestHook = join(PROJECT_ROOT, "hooks", "suggest.sh");
|
|
188
189
|
const nameHook = join(PROJECT_ROOT, "hooks", "name-react.sh");
|
|
189
190
|
const moodHook = join(PROJECT_ROOT, "hooks", "mood-react.sh");
|
|
191
|
+
const commandHook = (command: string) => ({
|
|
192
|
+
type: "command",
|
|
193
|
+
command: toUnixPath(command),
|
|
194
|
+
timeout: HOOK_TIMEOUT_SECONDS,
|
|
195
|
+
});
|
|
190
196
|
|
|
191
197
|
if (!settings.hooks) settings.hooks = {};
|
|
192
198
|
|
|
@@ -198,11 +204,11 @@ function installHooks(settings: Record<string, any>) {
|
|
|
198
204
|
);
|
|
199
205
|
settings.hooks.PostToolUse.push({
|
|
200
206
|
matcher: "Bash",
|
|
201
|
-
hooks: [
|
|
207
|
+
hooks: [commandHook(reactHook)],
|
|
202
208
|
});
|
|
203
209
|
settings.hooks.PostToolUse.push({
|
|
204
210
|
matcher: "Write|Edit",
|
|
205
|
-
hooks: [
|
|
211
|
+
hooks: [commandHook(fileTypeHook)],
|
|
206
212
|
});
|
|
207
213
|
|
|
208
214
|
// Stop: extract <!-- buddy: --> comment from Claude's response
|
|
@@ -211,10 +217,10 @@ function installHooks(settings: Record<string, any>) {
|
|
|
211
217
|
(h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("coding-buddy") || hh.command?.includes("claude-buddy")),
|
|
212
218
|
);
|
|
213
219
|
settings.hooks.Stop.push({
|
|
214
|
-
hooks: [
|
|
220
|
+
hooks: [commandHook(commentHook)],
|
|
215
221
|
});
|
|
216
222
|
settings.hooks.Stop.push({
|
|
217
|
-
hooks: [
|
|
223
|
+
hooks: [commandHook(suggestHook)],
|
|
218
224
|
});
|
|
219
225
|
|
|
220
226
|
// UserPromptSubmit: detect buddy's name in user message → instant status line
|
|
@@ -224,10 +230,10 @@ function installHooks(settings: Record<string, any>) {
|
|
|
224
230
|
(h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("coding-buddy") || hh.command?.includes("claude-buddy")),
|
|
225
231
|
);
|
|
226
232
|
settings.hooks.UserPromptSubmit.push({
|
|
227
|
-
hooks: [
|
|
233
|
+
hooks: [commandHook(nameHook)],
|
|
228
234
|
});
|
|
229
235
|
settings.hooks.UserPromptSubmit.push({
|
|
230
|
-
hooks: [
|
|
236
|
+
hooks: [commandHook(moodHook)],
|
|
231
237
|
});
|
|
232
238
|
|
|
233
239
|
ok("Hooks registered: PostToolUse (Bash + Write/Edit) + Stop (comment + suggest) + UserPromptSubmit (name + mood)");
|
package/cli/pick.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
19
|
loadActiveSlot, saveActiveSlot, listCompanionSlots,
|
|
20
|
-
loadCompanionSlot, saveCompanionSlot, slugify, unusedName, writeStatusState,
|
|
20
|
+
loadCompanionSlot, saveCompanionSlot, deleteCompanionSlot, slugify, unusedName, writeStatusState,
|
|
21
21
|
} from "../server/state.ts";
|
|
22
22
|
import { generateBones, generatePersonality, SPECIES, RARITIES, STAT_NAMES, RARITY_STARS, EYES, HATS,
|
|
23
23
|
type Species, type Rarity, type StatName, type Eye, type Hat,
|
|
@@ -123,6 +123,7 @@ interface State {
|
|
|
123
123
|
searchStatus: string;
|
|
124
124
|
nameInput: string;
|
|
125
125
|
pendingResult: BuddyResult | null;
|
|
126
|
+
confirmDelete: boolean;
|
|
126
127
|
message: string;
|
|
127
128
|
}
|
|
128
129
|
|
|
@@ -141,6 +142,7 @@ function fresh(): State {
|
|
|
141
142
|
searchStatus: "",
|
|
142
143
|
nameInput: "",
|
|
143
144
|
pendingResult: null,
|
|
145
|
+
confirmDelete: false,
|
|
144
146
|
message: "",
|
|
145
147
|
};
|
|
146
148
|
}
|
|
@@ -151,7 +153,7 @@ const LEFT_W = 36;
|
|
|
151
153
|
|
|
152
154
|
function savedPane(s: State): string[] {
|
|
153
155
|
const lines: string[] = [];
|
|
154
|
-
lines.push(`${B} Your Menagerie${N} ${GR}[s] search${N}`);
|
|
156
|
+
lines.push(`${B} Your Menagerie${N} ${GR}[s] search [d] remove${N}`);
|
|
155
157
|
lines.push(GR + " " + "─".repeat(LEFT_W - 2) + N);
|
|
156
158
|
|
|
157
159
|
if (s.savedSlots.length === 0) {
|
|
@@ -174,6 +176,10 @@ function savedPane(s: State): string[] {
|
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
lines.push(GR + " " + "─".repeat(LEFT_W - 2) + N);
|
|
179
|
+
if (s.confirmDelete) {
|
|
180
|
+
const entry = s.savedSlots[s.savedCursor];
|
|
181
|
+
lines.push(` ${YL}remove ${B}${entry?.companion.name ?? "?"}${N}${YL}? [d/y] yes [any] no${N}`);
|
|
182
|
+
}
|
|
177
183
|
return lines;
|
|
178
184
|
}
|
|
179
185
|
|
|
@@ -273,9 +279,7 @@ function previewPane(s: State): string[] {
|
|
|
273
279
|
}
|
|
274
280
|
|
|
275
281
|
if (!c) return [` ${GR}no preview${N}`];
|
|
276
|
-
|
|
277
|
-
const cols = Math.max(80, process.stdout.columns || 80);
|
|
278
|
-
const rightW = cols - LEFT_W - 3;
|
|
282
|
+
const rightW = 34;
|
|
279
283
|
return renderCompanionCard(c.bones, c.name, c.personality, undefined, 0, rightW).split("\n");
|
|
280
284
|
}
|
|
281
285
|
|
|
@@ -309,7 +313,7 @@ function drawScreen(s: State): void {
|
|
|
309
313
|
|
|
310
314
|
// Footer — mode-specific help
|
|
311
315
|
const helpText =
|
|
312
|
-
s.mode === "saved" ? "↑↓ navigate enter summon r random s search q quit" :
|
|
316
|
+
s.mode === "saved" ? "↑↓ navigate enter summon r random s search d remove q quit" :
|
|
313
317
|
s.mode === "criteria" ? "↑↓ field ←→ value enter search esc back" :
|
|
314
318
|
s.mode === "searching" ? "any key to stop and show results so far" :
|
|
315
319
|
s.mode === "results" ? "↑↓ navigate enter name+save esc back q quit" :
|
|
@@ -349,7 +353,7 @@ async function runSearch(s: State): Promise<void> {
|
|
|
349
353
|
|
|
350
354
|
// Scale attempt budget to rarity difficulty
|
|
351
355
|
const maxAttempts =
|
|
352
|
-
wantRa === "legendary" ?
|
|
356
|
+
wantRa === "legendary" ? 10_000_000_000 :
|
|
353
357
|
wantRa === "epic" ? 50_000_000 :
|
|
354
358
|
wantRa === "rare" ? 20_000_000 : 10_000_000;
|
|
355
359
|
|
|
@@ -438,12 +442,26 @@ function onKey(key: string, s: State): boolean {
|
|
|
438
442
|
}
|
|
439
443
|
|
|
440
444
|
case "saved": {
|
|
445
|
+
if (s.confirmDelete) {
|
|
446
|
+
if (key === "d" || key === "y") {
|
|
447
|
+
const entry = s.savedSlots[s.savedCursor];
|
|
448
|
+
if (entry) {
|
|
449
|
+
deleteCompanionSlot(entry.slot);
|
|
450
|
+
s.savedSlots = listCompanionSlots();
|
|
451
|
+
s.activeSlot = loadActiveSlot();
|
|
452
|
+
s.savedCursor = clamp(s.savedCursor, 0, Math.max(0, s.savedSlots.length - 1));
|
|
453
|
+
s.message = `✗ ${entry.companion.name} removed`;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
s.confirmDelete = false;
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
441
459
|
if (key === "q") return true;
|
|
442
460
|
if (key === "s") { s.mode = "criteria"; break; }
|
|
461
|
+
if (key === "d" && s.savedSlots.length > 0) { s.confirmDelete = true; break; }
|
|
443
462
|
if (key === "\x1b[A" || key === "k") s.savedCursor = clamp(s.savedCursor - 1, 0, s.savedSlots.length - 1);
|
|
444
463
|
else if (key === "\x1b[B" || key === "j") s.savedCursor = clamp(s.savedCursor + 1, 0, s.savedSlots.length - 1);
|
|
445
464
|
else if (key === "r") {
|
|
446
|
-
// Random pick from menagerie
|
|
447
465
|
if (s.savedSlots.length > 0) {
|
|
448
466
|
const entry = s.savedSlots[Math.floor(Math.random() * s.savedSlots.length)];
|
|
449
467
|
s.savedCursor = s.savedSlots.indexOf(entry);
|
package/cli/upgrade.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { readFileSync } from "fs";
|
|
4
|
-
import { execSync } from "child_process";
|
|
4
|
+
import { execSync, spawnSync } from "child_process";
|
|
5
5
|
import { join, resolve, dirname } from "path";
|
|
6
|
-
import { homedir } from "os";
|
|
7
6
|
|
|
8
7
|
const CYAN = "\x1b[36m";
|
|
9
8
|
const GREEN = "\x1b[32m";
|
|
@@ -28,6 +27,37 @@ function tryExec(cmd: string, fallback = ""): string {
|
|
|
28
27
|
}
|
|
29
28
|
}
|
|
30
29
|
|
|
30
|
+
function trySpawn(
|
|
31
|
+
command: string,
|
|
32
|
+
args: string[],
|
|
33
|
+
fallback = "",
|
|
34
|
+
cwd: string = PROJECT_ROOT,
|
|
35
|
+
): string {
|
|
36
|
+
const result = spawnSync(command, args, {
|
|
37
|
+
cwd,
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
40
|
+
});
|
|
41
|
+
if (result.status !== 0) return fallback;
|
|
42
|
+
return result.stdout.trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function runCommand(
|
|
46
|
+
command: string,
|
|
47
|
+
args: string[],
|
|
48
|
+
options: {
|
|
49
|
+
cwd?: string;
|
|
50
|
+
stdio?: "ignore" | "inherit";
|
|
51
|
+
} = {},
|
|
52
|
+
): boolean {
|
|
53
|
+
const result = spawnSync(command, args, {
|
|
54
|
+
cwd: options.cwd ?? PROJECT_ROOT,
|
|
55
|
+
encoding: "utf8",
|
|
56
|
+
stdio: ["ignore", options.stdio ?? "ignore", options.stdio ?? "ignore"],
|
|
57
|
+
});
|
|
58
|
+
return result.status === 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
31
61
|
function getCurrentVersion(): string {
|
|
32
62
|
try {
|
|
33
63
|
const pkg = JSON.parse(readFileSync(join(PROJECT_ROOT, "package.json"), "utf8"));
|
|
@@ -61,15 +91,13 @@ function getRemoteBranch(): string {
|
|
|
61
91
|
|
|
62
92
|
function checkForUpdates(branch: string): { hasUpdate: boolean; local: string; remote: string; commits: string[] } {
|
|
63
93
|
info("Fetching latest from remote...\n");
|
|
64
|
-
|
|
65
|
-
execSync("git fetch --quiet 2>/dev/null", { cwd: PROJECT_ROOT, stdio: "ignore" });
|
|
66
|
-
} catch {
|
|
94
|
+
if (!runCommand("git", ["fetch", "--quiet"])) {
|
|
67
95
|
warn("git fetch failed — proceeding with cached remote state");
|
|
68
96
|
}
|
|
69
97
|
|
|
70
98
|
const local = tryExec("git rev-parse HEAD 2>/dev/null");
|
|
71
99
|
const upstream = tryExec(`git rev-parse '@{upstream}' 2>/dev/null`);
|
|
72
|
-
const remote = upstream ||
|
|
100
|
+
const remote = upstream || trySpawn("git", ["rev-parse", `origin/${branch}`]);
|
|
73
101
|
|
|
74
102
|
if (!local || !remote) {
|
|
75
103
|
warn("Could not determine remote HEAD — no tracking branch configured");
|
|
@@ -81,8 +109,9 @@ function checkForUpdates(branch: string): { hasUpdate: boolean; local: string; r
|
|
|
81
109
|
return { hasUpdate: false, local, remote, commits: [] };
|
|
82
110
|
}
|
|
83
111
|
|
|
84
|
-
const commits =
|
|
85
|
-
|
|
112
|
+
const commits = trySpawn(
|
|
113
|
+
"git",
|
|
114
|
+
["log", "--oneline", `${local}..origin/${branch}`],
|
|
86
115
|
).split("\n").filter(Boolean);
|
|
87
116
|
|
|
88
117
|
return { hasUpdate: true, local, remote, commits };
|
|
@@ -91,10 +120,12 @@ function checkForUpdates(branch: string): { hasUpdate: boolean; local: string; r
|
|
|
91
120
|
function pullLatest(branch: string): boolean {
|
|
92
121
|
info(`Pulling latest from origin/${branch}...`);
|
|
93
122
|
try {
|
|
94
|
-
const
|
|
123
|
+
const result = spawnSync("git", ["pull", "--ff-only", "origin", branch], {
|
|
95
124
|
cwd: PROJECT_ROOT,
|
|
96
125
|
encoding: "utf8",
|
|
126
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
97
127
|
});
|
|
128
|
+
if (result.status !== 0) throw new Error(result.stderr || result.stdout || "unknown error");
|
|
98
129
|
ok("Git pull successful");
|
|
99
130
|
return true;
|
|
100
131
|
} catch (e: any) {
|
|
@@ -108,7 +139,7 @@ function pullLatest(branch: string): boolean {
|
|
|
108
139
|
function installDeps(): boolean {
|
|
109
140
|
info("Installing dependencies...");
|
|
110
141
|
try {
|
|
111
|
-
|
|
142
|
+
if (!runCommand("bun", ["install"])) throw new Error("bun install failed");
|
|
112
143
|
ok("Dependencies installed");
|
|
113
144
|
return true;
|
|
114
145
|
} catch {
|
|
@@ -120,7 +151,9 @@ function installDeps(): boolean {
|
|
|
120
151
|
function reinstallBuddy(): boolean {
|
|
121
152
|
info("Re-running install-buddy to update integrations...\n");
|
|
122
153
|
try {
|
|
123
|
-
|
|
154
|
+
if (!runCommand("bun", ["run", "install-buddy"], { stdio: "inherit" })) {
|
|
155
|
+
throw new Error("install-buddy failed");
|
|
156
|
+
}
|
|
124
157
|
return true;
|
|
125
158
|
} catch {
|
|
126
159
|
err("install-buddy failed");
|
package/hooks/buddy-comment.sh
CHANGED
|
@@ -1,86 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
# Session ID: sanitized tmux pane number, or "default" outside tmux
|
|
13
|
-
SID="${TMUX_PANE#%}"
|
|
14
|
-
SID="${SID:-default}"
|
|
15
|
-
STATUS_FILE="$STATE_DIR/status.json"
|
|
16
|
-
COOLDOWN_FILE="$STATE_DIR/.last_comment.$SID"
|
|
17
|
-
CONFIG_FILE="$STATE_DIR/config.json"
|
|
18
|
-
EVENTS_FILE="$STATE_DIR/events.json"
|
|
19
|
-
|
|
20
|
-
[ -f "$STATUS_FILE" ] || exit 0
|
|
21
|
-
|
|
22
|
-
# Read cooldown from config (default 30s, 0 = disabled)
|
|
23
|
-
COOLDOWN=30
|
|
24
|
-
if [ -f "$CONFIG_FILE" ]; then
|
|
25
|
-
_cd=$(jq -r '.commentCooldown // 30' "$CONFIG_FILE" 2>/dev/null || echo 30)
|
|
26
|
-
# Accept any non-negative integer (including 0 to disable cooldown)
|
|
27
|
-
[[ "$_cd" =~ ^[0-9]+$ ]] && COOLDOWN=$_cd
|
|
28
|
-
fi
|
|
29
|
-
|
|
30
|
-
INPUT=$(cat)
|
|
31
|
-
|
|
32
|
-
# Extract last_assistant_message from hook input
|
|
33
|
-
MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""' 2>/dev/null)
|
|
34
|
-
[ -z "$MSG" ] && exit 0
|
|
35
|
-
|
|
36
|
-
# Extract <!-- buddy: ... --> comment (portable, no grep -P)
|
|
37
|
-
COMMENT=$(echo "$MSG" | sed -n 's/.*<!-- *buddy: *\(.*[^ ]\) *-->.*/\1/p' | tail -1)
|
|
38
|
-
[ -z "$COMMENT" ] && exit 0
|
|
39
|
-
|
|
40
|
-
# Cooldown: configurable (default 30s)
|
|
41
|
-
if [ -f "$COOLDOWN_FILE" ]; then
|
|
42
|
-
LAST=$(cat "$COOLDOWN_FILE" 2>/dev/null)
|
|
43
|
-
NOW=$(date +%s)
|
|
44
|
-
[ $(( NOW - ${LAST:-0} )) -lt "$COOLDOWN" ] && exit 0
|
|
45
|
-
fi
|
|
46
|
-
|
|
47
|
-
mkdir -p "$STATE_DIR"
|
|
48
|
-
date +%s > "$COOLDOWN_FILE"
|
|
49
|
-
|
|
50
|
-
# Update status.json with the reaction
|
|
51
|
-
TMP=$(mktemp)
|
|
52
|
-
jq --arg r "$COMMENT" '.reaction = $r' "$STATUS_FILE" > "$TMP" 2>/dev/null && mv "$TMP" "$STATUS_FILE"
|
|
53
|
-
|
|
54
|
-
# Also write reaction file (use jq for safe JSON encoding)
|
|
55
|
-
jq -n --arg r "$COMMENT" --arg ts "$(date +%s)000" \
|
|
56
|
-
'{reaction: $r, timestamp: ($ts | tonumber), reason: "turn"}' \
|
|
57
|
-
> "$STATE_DIR/reaction.$SID.json"
|
|
58
|
-
|
|
59
|
-
# Increment achievement event counters and award XP
|
|
60
|
-
if command -v jq >/dev/null 2>&1; then
|
|
61
|
-
if [ ! -f "$EVENTS_FILE" ]; then
|
|
62
|
-
echo '{}' > "$EVENTS_FILE"
|
|
63
|
-
fi
|
|
64
|
-
TMP=$(mktemp)
|
|
65
|
-
jq '.turns = (.turns // 0 + 1)' "$EVENTS_FILE" > "$TMP" 2>/dev/null && mv "$TMP" "$EVENTS_FILE"
|
|
66
|
-
fi
|
|
67
|
-
|
|
68
|
-
# Award XP for turn (async, non-blocking)
|
|
69
|
-
if [ -x "$(command -v bun)" ]; then
|
|
70
|
-
PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
71
|
-
bun run "$PLUGIN_ROOT/server/award-xp.ts" "turn" >/dev/null 2>&1 &
|
|
72
|
-
fi
|
|
73
|
-
|
|
74
|
-
# Consolidate memory (async, non-blocking)
|
|
75
|
-
# Extract project, bug, and preference signals from conversation
|
|
76
|
-
if [ -x "$(command -v bun)" ]; then
|
|
77
|
-
PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
78
|
-
# Pass the assistant message for analysis (pass empty string for user prompt if unavailable)
|
|
79
|
-
USER_MSG=$(echo "$INPUT" | jq -r '.last_user_message // ""' 2>/dev/null)
|
|
80
|
-
bun run "$PLUGIN_ROOT/server/consolidate.ts" \
|
|
81
|
-
"$(echo "$MSG" | jq -Rs .)" \
|
|
82
|
-
"$(echo "$USER_MSG" | jq -Rs .)" \
|
|
83
|
-
>/dev/null 2>&1 &
|
|
84
|
-
fi
|
|
85
|
-
|
|
86
|
-
exit 0
|
|
2
|
+
set +e
|
|
3
|
+
SCRIPT_DIR="${BASH_SOURCE[0]%/*}"
|
|
4
|
+
ROOT="$(cd "$SCRIPT_DIR/.." 2>/dev/null && pwd)" || exit 0
|
|
5
|
+
[ -n "$ROOT" ] || exit 0
|
|
6
|
+
BUN="$(command -v bun 2>/dev/null || true)"
|
|
7
|
+
[ -x "$BUN" ] || BUN="$HOME/.bun/bin/bun"
|
|
8
|
+
[ -x "$BUN" ] || BUN="/opt/homebrew/bin/bun"
|
|
9
|
+
[ -x "$BUN" ] || BUN="/usr/local/bin/bun"
|
|
10
|
+
[ -x "$BUN" ] || exit 0
|
|
11
|
+
exec "$BUN" run "$ROOT/server/hooks/buddy-comment.ts" || exit 0
|