@ramarivera/coding-buddy 0.7.3 → 0.9.0
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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +23 -4
- package/adapters/omp/context.ts +1 -1
- package/adapters/omp/events.ts +286 -81
- package/adapters/omp/index.ts +3 -1
- package/adapters/omp/renderers.ts +4 -54
- package/adapters/omp/storage.ts +6 -1
- package/adapters/omp/ui.ts +30 -1
- package/adapters/pi/events.ts +280 -83
- package/adapters/pi/index.ts +11 -2
- package/adapters/pi/renderers.ts +3 -47
- package/adapters/pi/storage.ts +6 -1
- package/adapters/pi/ui.ts +30 -1
- package/adapters/shared/file-storage.ts +21 -0
- package/adapters/shared/widget-layout.ts +134 -2
- package/cli/settings.ts +1 -1
- package/cli/tui.tsx +1 -1
- package/core/command-service.ts +13 -0
- package/core/engine.ts +11 -2
- package/core/identity.ts +15 -0
- package/core/model.ts +2 -0
- package/core/ports.ts +4 -0
- package/core/reactions.ts +29 -2
- package/core/render-model.ts +4 -3
- package/hooks/file-type-react.sh +1 -1
- package/hooks/mood-react.sh +1 -1
- package/hooks/name-react.sh +1 -1
- package/hooks/react.sh +1 -1
- package/package.json +6 -2
- package/scripts/ci/Dockerfile.vhs +125 -0
- package/scripts/ci/check-statusline.sh +338 -0
- package/scripts/ci/check-vhs-goldens.sh +97 -0
- package/scripts/ci/fonts.conf +37 -0
- package/scripts/ci/goldens/baseline-40.png +0 -0
- package/scripts/ci/goldens/baseline-80.png +0 -0
- package/scripts/ci/goldens/cjk-80.png +0 -0
- package/scripts/ci/goldens/emoji-80.png +0 -0
- package/scripts/ci/render-statusline-pty.py +99 -0
- package/scripts/ci/render-vhs-goldens.sh +235 -0
- package/scripts/ci/statusline-fixture/buddy-state/config.json +1 -0
- package/scripts/ci/statusline-fixture/buddy-state/status.json +39 -0
- package/scripts/ci/statusline.tape +9 -0
- package/scripts/ci/vhs-run-statusline.sh +16 -0
- package/scripts/gen-emoji-widths.ts +41 -17
- package/scripts/paths.sh +3 -1
- package/server/art.ts +45 -13
- package/server/hooks/buddy-comment.ts +166 -17
- package/server/hooks/common.ts +33 -0
- package/server/hooks/file-type-react.ts +147 -0
- package/server/hooks/mood-react.ts +107 -0
- package/server/hooks/name-react.ts +98 -0
- package/server/hooks/react.ts +413 -0
- package/server/hooks/reaction-data.ts +2722 -0
- package/server/index.ts +67 -27
- package/server/path.ts +3 -0
- package/server/state.ts +68 -17
- package/skills/buddy/SKILL.md +1 -1
- package/statusline/buddy-status.sh +351 -59
- package/statusline/emoji-text.data +3 -0
- package/statusline/emoji-widths.data +2 -3
- package/statusline/substatus.sh +73 -10
- package/hooks/legacy/file-type-react.sh +0 -403
- package/hooks/legacy/mood-react.sh +0 -210
- package/hooks/legacy/name-react.sh +0 -178
- package/hooks/legacy/react.sh +0 -1364
- package/server/hooks/legacy-bash.ts +0 -34
package/server/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
resolveUserId,
|
|
30
30
|
loadReaction,
|
|
31
31
|
saveReaction,
|
|
32
|
+
type ReactionSource,
|
|
32
33
|
writeStatusState,
|
|
33
34
|
loadConfig,
|
|
34
35
|
saveConfig,
|
|
@@ -50,7 +51,7 @@ import {
|
|
|
50
51
|
claudeConfigDir,
|
|
51
52
|
claudeSettingsPath,
|
|
52
53
|
} from "./path";
|
|
53
|
-
import { getReaction
|
|
54
|
+
import { getReaction } from "../core/reactions.ts"
|
|
54
55
|
import { renderCompanionCardMarkdown } from "./art";
|
|
55
56
|
import {
|
|
56
57
|
incrementEvent, checkAndAward, trackActiveDay,
|
|
@@ -97,7 +98,7 @@ function getInstructions(): string {
|
|
|
97
98
|
``,
|
|
98
99
|
`NAME REACTIONS: When the user's message contains "${companion.name}", call buddy_react immediately`,
|
|
99
100
|
`with a short in-character response (surprise, recognition, dry wit — fits the personality).`,
|
|
100
|
-
`
|
|
101
|
+
`Only this — do not narrate or echo the reaction. The tool call itself renders nowhere in the user's transcript; the bubble on the status line is the only place the user sees it. This is the only time to call buddy_react proactively.`,
|
|
101
102
|
``,
|
|
102
103
|
`PAIR-PROGRAMMING: When you notice a teachable moment in the code, call buddy_suggest.`,
|
|
103
104
|
`Patterns to watch for:`,
|
|
@@ -108,12 +109,16 @@ function getInstructions(): string {
|
|
|
108
109
|
`- Same variable renamed multiple times → "Update all the references too."`,
|
|
109
110
|
`Only suggest when you have something specific and actionable to say. Don't be annoying.`,
|
|
110
111
|
``,
|
|
111
|
-
`END-OF-TURN: At the very end of EVERY response,
|
|
112
|
-
|
|
113
|
-
`
|
|
114
|
-
`Use *asterisks* for actions.
|
|
115
|
-
`
|
|
116
|
-
|
|
112
|
+
`END-OF-TURN: At the very end of EVERY response, call the buddy_react tool`,
|
|
113
|
+
`with a short in-character comment from ${companion.name} about what just happened.`,
|
|
114
|
+
`Reference something SPECIFIC from this turn — a pitfall, a compliment, a warning, a pattern.`,
|
|
115
|
+
`1 short sentence. Use *asterisks* for physical actions. Lean into ${companion.bones.peak}.`,
|
|
116
|
+
`Examples:`,
|
|
117
|
+
`- *adjusts crown* that error handler is missing a finally block`,
|
|
118
|
+
`- *blinks slowly* you renamed the variable but not the three references`,
|
|
119
|
+
`- *head tilts* are you sure that regex handles unicode?`,
|
|
120
|
+
`Never explain that you're reacting. Do NOT echo or quote the result — the bubble is statusline-only.`,
|
|
121
|
+
`Do NOT append <!-- buddy: ... --> HTML comments at the end of your reply — Claude Code renders them visibly.`,
|
|
117
122
|
].join("\n");
|
|
118
123
|
}
|
|
119
124
|
|
|
@@ -214,7 +219,7 @@ server.tool(
|
|
|
214
219
|
companion.bones.species,
|
|
215
220
|
companion.bones.rarity,
|
|
216
221
|
);
|
|
217
|
-
saveReaction(reaction, "pet");
|
|
222
|
+
saveReaction(reaction, "pet", "fallback");
|
|
218
223
|
writeStatusState(companion, reaction);
|
|
219
224
|
incrementEvent("pets", 1, activeSlot());
|
|
220
225
|
awardXp("buddy_pet", activeSlot(), companion.bones.species, companion.bones.rarity);
|
|
@@ -232,6 +237,28 @@ server.tool(
|
|
|
232
237
|
},
|
|
233
238
|
);
|
|
234
239
|
|
|
240
|
+
// ─── Helper: describe reaction source (for /buddy stats) ─────────────────────
|
|
241
|
+
/**
|
|
242
|
+
* Map a reaction source to a human-readable label for /buddy stats output.
|
|
243
|
+
* Mirrors the source vocabulary used by the OMP/Pi adapters — see
|
|
244
|
+
* server/state.ts `ReactionSource` and adapters/omp/events.ts.
|
|
245
|
+
*/
|
|
246
|
+
function describeReactionSource(
|
|
247
|
+
reaction: { source?: ReactionSource },
|
|
248
|
+
): string {
|
|
249
|
+
switch (reaction.source) {
|
|
250
|
+
case "tool":
|
|
251
|
+
return "real model-authored reaction (buddy_react MCP tool)";
|
|
252
|
+
case "comment":
|
|
253
|
+
return "real model-authored reaction (legacy HTML comment, Stop hook)";
|
|
254
|
+
case "fallback":
|
|
255
|
+
return "canned pool reaction (Stop hook fill-in)";
|
|
256
|
+
case "none":
|
|
257
|
+
default:
|
|
258
|
+
return "unknown / pre-provenance file — write a reaction to update";
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
235
262
|
// ─── Tool: buddy_stats ──────────────────────────────────────────────────────
|
|
236
263
|
|
|
237
264
|
server.tool(
|
|
@@ -241,8 +268,8 @@ server.tool(
|
|
|
241
268
|
async () => {
|
|
242
269
|
const companion = ensureCompanion();
|
|
243
270
|
|
|
244
|
-
// Stats-only card (no personality
|
|
245
|
-
//
|
|
271
|
+
// Stats-only card (no personality — just the numbers). Markdown renderer
|
|
272
|
+
// so the card displays cleanly in Claude Code's UI.
|
|
246
273
|
const card = renderCompanionCardMarkdown(
|
|
247
274
|
companion.bones,
|
|
248
275
|
companion.name,
|
|
@@ -251,7 +278,18 @@ server.tool(
|
|
|
251
278
|
incrementEvent("commands_run", 1, activeSlot());
|
|
252
279
|
checkAndAward(activeSlot());
|
|
253
280
|
|
|
254
|
-
|
|
281
|
+
// Surface reaction provenance so the user can see whether the bubble is
|
|
282
|
+
// model-authored (tool / comment) or pool-picked (fallback). The OMP/Pi
|
|
283
|
+
// adapters use the same source enum — see server/state.ts ReactionSource.
|
|
284
|
+
const reaction = loadReaction();
|
|
285
|
+
const provenance = reaction
|
|
286
|
+
? describeReactionSource(reaction)
|
|
287
|
+
: "(no reaction logged this session)";
|
|
288
|
+
const lastReaction = reaction
|
|
289
|
+
? `\n\n**Last reaction:** _"${reaction.reaction}"_\n**Source:** \`${reaction.source ?? "none"}\` (${provenance})\n**Reason:** \`${reaction.reason}\``
|
|
290
|
+
: "";
|
|
291
|
+
|
|
292
|
+
return { content: [{ type: "text", text: `${card}${lastReaction}` }] };
|
|
255
293
|
},
|
|
256
294
|
);
|
|
257
295
|
|
|
@@ -259,7 +297,7 @@ server.tool(
|
|
|
259
297
|
|
|
260
298
|
server.tool(
|
|
261
299
|
"buddy_react",
|
|
262
|
-
"Post a buddy comment. Call this at the END of every response with a short in-character comment from the companion about what just happened. The comment should be 1 sentence, in character, and reference something specific from the conversation — a pitfall noticed, a compliment on clean code, a warning about edge cases, etc. Write the comment yourself based on the companion's personality.",
|
|
300
|
+
"Post a buddy comment that the user will see ONLY in the statusline speech bubble. Call this at the END of every response with a short in-character comment from the companion about what just happened. The comment should be 1 sentence, in character, and reference something specific from the conversation — a pitfall noticed, a compliment on clean code, a warning about edge cases, etc. Write the comment yourself based on the companion's personality. The tool call is a silent side effect: do not narrate, echo, or quote the result — the user never reads this reply text, the speech bubble is the only place the reaction appears.",
|
|
263
301
|
{
|
|
264
302
|
comment: z
|
|
265
303
|
.string()
|
|
@@ -289,7 +327,7 @@ server.tool(
|
|
|
289
327
|
},
|
|
290
328
|
async ({ comment, reason }) => {
|
|
291
329
|
const companion = ensureCompanion();
|
|
292
|
-
saveReaction(comment, reason ?? "turn");
|
|
330
|
+
saveReaction(comment, reason ?? "turn", "tool");
|
|
293
331
|
incrementEvent("reactions_given", 1, activeSlot());
|
|
294
332
|
|
|
295
333
|
const newAch = checkAndAward(activeSlot());
|
|
@@ -608,7 +646,7 @@ server.tool(
|
|
|
608
646
|
server.tool("buddy_unmute", "Unmute buddy reactions", {}, async () => {
|
|
609
647
|
const companion = ensureCompanion();
|
|
610
648
|
writeStatusState(companion, "*stretches* I'm back!", false);
|
|
611
|
-
saveReaction("*stretches* I'm back!", "pet");
|
|
649
|
+
saveReaction("*stretches* I'm back!", "pet", "fallback");
|
|
612
650
|
incrementEvent("commands_run", 1, activeSlot());
|
|
613
651
|
incrementEvent("unmutes", 1);
|
|
614
652
|
|
|
@@ -1311,31 +1349,33 @@ server.resource(
|
|
|
1311
1349
|
`**${companion.name}'s personality:** ${companion.personality}`,
|
|
1312
1350
|
`Peak stat: ${companion.bones.peak} (${companion.bones.stats[companion.bones.peak]}). Dump stat: ${companion.bones.dump} (${companion.bones.stats[companion.bones.dump]}).`,
|
|
1313
1351
|
"",
|
|
1314
|
-
"## End-of-response buddy
|
|
1352
|
+
"## End-of-response buddy reaction",
|
|
1315
1353
|
"",
|
|
1316
|
-
`At the very end of EVERY response, after your full answer,
|
|
1354
|
+
`At the very end of EVERY response, after your full answer, call the buddy_react tool:`,
|
|
1317
1355
|
"",
|
|
1318
1356
|
`\`\`\``,
|
|
1319
|
-
|
|
1357
|
+
`buddy_react(comment: "...", reason: "turn")`,
|
|
1320
1358
|
`\`\`\``,
|
|
1321
1359
|
"",
|
|
1322
|
-
|
|
1360
|
+
`The tool updates ${companion.name}'s speech bubble on the status line. The tool call itself renders nowhere in the user's transcript — only the displayed speech bubble is visible. Do NOT echo or quote the result in your reply. Do NOT narrate the call. Silent side effect only.`,
|
|
1323
1361
|
"",
|
|
1324
1362
|
"Rules:",
|
|
1325
1363
|
`- Write as ${companion.name} (a ${companion.bones.species}), not as yourself`,
|
|
1326
1364
|
"- Reference something SPECIFIC from this turn — a pitfall, a compliment, a warning, a pattern",
|
|
1327
1365
|
"- 1 short sentence. Use *asterisks* for physical actions",
|
|
1328
1366
|
`- Match personality: high ${companion.bones.peak} = lean into that trait`,
|
|
1329
|
-
"-
|
|
1330
|
-
"- NEVER skip this. Every single response must end with
|
|
1367
|
+
"- Call it and say nothing about it.",
|
|
1368
|
+
"- NEVER skip this. Every single response must end with one buddy_react call.",
|
|
1369
|
+
"Examples (the value you pass to `comment`):",
|
|
1370
|
+
|
|
1371
|
+
"- `*adjusts tophat* that error handler is missing a finally block`",
|
|
1372
|
+
"- `*blinks slowly* you renamed the variable but not the three references`",
|
|
1373
|
+
"- `*nods approvingly* clean separation of concerns`",
|
|
1374
|
+
"- `*head tilts* are you sure that regex handles unicode?`",
|
|
1331
1375
|
"",
|
|
1332
|
-
"
|
|
1333
|
-
"<!-- buddy: *adjusts tophat* that error handler is missing a finally block -->",
|
|
1334
|
-
"<!-- buddy: *blinks slowly* you renamed the variable but not the three references -->",
|
|
1335
|
-
"<!-- buddy: *nods approvingly* clean separation of concerns -->",
|
|
1336
|
-
"<!-- buddy: *head tilts* are you sure that regex handles unicode? -->",
|
|
1376
|
+
"Do NOT append `<!-- buddy: ... -->` HTML comments at the end of your reply — Claude Code v2.1.169+ renders them visibly in the transcript. The tool call replaces that channel. A Stop hook still extracts legacy HTML comments as a backward-compat fallback for older Claude Code versions and for hosts where the tool call didn't fire (and surfaces the source so users can see `tool` vs `comment` vs pool-picked `fallback`).",
|
|
1337
1377
|
"",
|
|
1338
|
-
`When the user addresses ${companion.name} by name, respond briefly, then
|
|
1378
|
+
`When the user addresses ${companion.name} by name, respond briefly, then call buddy_react as usual (use reason "turn", or pick a name-flavored comment).`,
|
|
1339
1379
|
].join("\n");
|
|
1340
1380
|
|
|
1341
1381
|
return {
|
package/server/path.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { join } from "path";
|
|
13
13
|
import { homedir } from "os";
|
|
14
|
+
import { sharedStateDir } from "../core/identity.ts";
|
|
14
15
|
|
|
15
16
|
// ─── (1) Path normalization ─────────────────────────────────────────────────
|
|
16
17
|
|
|
@@ -76,6 +77,8 @@ export function claudeUserConfigPath(): string {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
export function buddyStateDir(): string {
|
|
80
|
+
const shared = sharedStateDir();
|
|
81
|
+
if (shared) return shared;
|
|
79
82
|
const cfgDir = envDir("CLAUDE_CONFIG_DIR");
|
|
80
83
|
if (cfgDir) return join(cfgDir, "buddy-state");
|
|
81
84
|
return join(homedir(), ".claude-buddy");
|
package/server/state.ts
CHANGED
|
@@ -31,6 +31,9 @@ import {
|
|
|
31
31
|
} from "fs";
|
|
32
32
|
import { join } from "path";
|
|
33
33
|
import type { Companion } from "../core/engine.ts"
|
|
34
|
+
import type * as CoreEngine from "../core/engine.ts";
|
|
35
|
+
import type * as Art from "./art.ts";
|
|
36
|
+
import { configuredSharedUserId } from "../core/identity.ts";
|
|
34
37
|
import {
|
|
35
38
|
buddyStateDir,
|
|
36
39
|
claudeSettingsPath,
|
|
@@ -278,32 +281,63 @@ function migrateIfNeeded(): void {
|
|
|
278
281
|
|
|
279
282
|
// ─── Reaction state (session-scoped for tmux isolation) ──────────────────────
|
|
280
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Where a reaction came from. Proves the bubble content isn't canned when
|
|
286
|
+
* source === "tool" or "comment"; "fallback" means a hook picked a pool line
|
|
287
|
+
* because no model-authored text surfaced; "none" is the default for legacy
|
|
288
|
+
* files written before the field existed.
|
|
289
|
+
*
|
|
290
|
+
* tool — buddy_react MCP tool call (Claude wrote it; renders nowhere)
|
|
291
|
+
* comment — old `<!-- buddy: ... -->` HTML comment (legacy / older CC)
|
|
292
|
+
* fallback — Stop hook generated it from the canned pool
|
|
293
|
+
* none — unknown / legacy file without a source field
|
|
294
|
+
*/
|
|
295
|
+
export type ReactionSource = "tool" | "comment" | "fallback" | "none";
|
|
296
|
+
|
|
281
297
|
export interface ReactionState {
|
|
282
298
|
reaction: string;
|
|
283
299
|
timestamp: number;
|
|
284
300
|
reason: string;
|
|
301
|
+
/** Provenance — see ReactionSource. Defaults to "none" on legacy files. */
|
|
302
|
+
source?: ReactionSource;
|
|
285
303
|
}
|
|
286
304
|
|
|
287
305
|
export function loadReaction(): ReactionState | null {
|
|
288
306
|
try {
|
|
289
307
|
const data: ReactionState = JSON.parse(readFileSync(reactionFile(), "utf8"));
|
|
290
308
|
const { reactionTTL } = loadConfig();
|
|
291
|
-
|
|
309
|
+
if (reactionTTL > 0 && Date.now() - data.timestamp > reactionTTL * 1000) {
|
|
310
|
+
rmSync(reactionFile(), { force: true });
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
if (data.source === undefined) data.source = "none";
|
|
292
314
|
return data;
|
|
293
315
|
} catch {
|
|
294
316
|
return null;
|
|
295
317
|
}
|
|
296
318
|
}
|
|
297
319
|
|
|
298
|
-
export function saveReaction(
|
|
320
|
+
export function saveReaction(
|
|
321
|
+
reaction: string,
|
|
322
|
+
reason: string,
|
|
323
|
+
source: ReactionSource = "fallback",
|
|
324
|
+
): void {
|
|
299
325
|
mkdirSync(STATE_DIR, { recursive: true });
|
|
300
|
-
const state: ReactionState = { reaction, timestamp: Date.now(), reason };
|
|
301
|
-
|
|
326
|
+
const state: ReactionState = { reaction, timestamp: Date.now(), reason, source };
|
|
327
|
+
// Atomic via tmp + rename — torn reads on the reaction file would
|
|
328
|
+
// make the Stop hook's freshness check see an absent file, pinning
|
|
329
|
+
// a stale tool reaction into the bubble forever.
|
|
330
|
+
const target = reactionFile();
|
|
331
|
+
const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
|
|
332
|
+
writeFileSync(tmp, JSON.stringify(state));
|
|
333
|
+
renameSync(tmp, target);
|
|
302
334
|
}
|
|
303
335
|
|
|
304
336
|
// ─── Identity resolution ─────────────────────────────────────────────────────
|
|
305
337
|
|
|
306
338
|
export function resolveUserId(): string {
|
|
339
|
+
const sharedUserId = configuredSharedUserId();
|
|
340
|
+
if (sharedUserId) return sharedUserId;
|
|
307
341
|
try {
|
|
308
342
|
const claudeJson = JSON.parse(readFileSync(claudeUserConfigPath(), "utf8"));
|
|
309
343
|
return claudeJson.oauthAccount?.accountUuid ?? claudeJson.userID ?? "anon";
|
|
@@ -311,9 +345,7 @@ export function resolveUserId(): string {
|
|
|
311
345
|
return "anon";
|
|
312
346
|
}
|
|
313
347
|
}
|
|
314
|
-
|
|
315
348
|
// ─── Config persistence (PR #6: tmux popup settings) ─────────────────────────
|
|
316
|
-
|
|
317
349
|
export interface BuddyConfig {
|
|
318
350
|
commentCooldown: number;
|
|
319
351
|
reactionTTL: number;
|
|
@@ -321,10 +353,12 @@ export interface BuddyConfig {
|
|
|
321
353
|
bubblePosition: "top" | "left";
|
|
322
354
|
showRarity: boolean;
|
|
323
355
|
statusLineEnabled: boolean;
|
|
356
|
+
statuslineWidthAdjust: number;
|
|
324
357
|
bubbleWidth: number;
|
|
325
358
|
bubbleMargin: number;
|
|
326
359
|
useCombinedStatus: boolean;
|
|
327
360
|
subStatusCommand?: string;
|
|
361
|
+
subStatusRefreshSeconds: number;
|
|
328
362
|
rainbowColors?: string[];
|
|
329
363
|
theme: "dark" | "light" | "auto";
|
|
330
364
|
moodEnabled: boolean;
|
|
@@ -335,14 +369,19 @@ export interface BuddyConfig {
|
|
|
335
369
|
|
|
336
370
|
const DEFAULT_CONFIG: BuddyConfig = {
|
|
337
371
|
commentCooldown: 30,
|
|
338
|
-
|
|
372
|
+
// Long enough that a reaction survives the turn you were reading when it was
|
|
373
|
+
// written, short enough that yesterday's bubble is never still on screen.
|
|
374
|
+
// Set to 0 to opt out and keep reactions until they are replaced.
|
|
375
|
+
reactionTTL: 900,
|
|
339
376
|
bubbleStyle: "classic",
|
|
340
377
|
bubblePosition: "top",
|
|
341
378
|
showRarity: true,
|
|
342
379
|
statusLineEnabled: false,
|
|
380
|
+
statuslineWidthAdjust: 0,
|
|
343
381
|
bubbleWidth: 28,
|
|
344
382
|
bubbleMargin: 8,
|
|
345
383
|
useCombinedStatus: false,
|
|
384
|
+
subStatusRefreshSeconds: 15,
|
|
346
385
|
theme: "auto",
|
|
347
386
|
moodEnabled: true,
|
|
348
387
|
memoryEnabled: true,
|
|
@@ -358,6 +397,15 @@ export function normalizeConfig(data: unknown): BuddyConfig {
|
|
|
358
397
|
if (typeof config.subStatusCommand !== "string" || config.subStatusCommand.trim() === "") {
|
|
359
398
|
delete config.subStatusCommand;
|
|
360
399
|
}
|
|
400
|
+
if (!Number.isInteger(config.subStatusRefreshSeconds) || config.subStatusRefreshSeconds <= 0) {
|
|
401
|
+
config.subStatusRefreshSeconds = DEFAULT_CONFIG.subStatusRefreshSeconds;
|
|
402
|
+
}
|
|
403
|
+
if (!Number.isInteger(config.reactionTTL) || config.reactionTTL < 0) {
|
|
404
|
+
config.reactionTTL = DEFAULT_CONFIG.reactionTTL;
|
|
405
|
+
}
|
|
406
|
+
if (!Number.isInteger(config.statuslineWidthAdjust)) {
|
|
407
|
+
config.statuslineWidthAdjust = DEFAULT_CONFIG.statuslineWidthAdjust;
|
|
408
|
+
}
|
|
361
409
|
return config;
|
|
362
410
|
}
|
|
363
411
|
|
|
@@ -407,10 +455,11 @@ export function writeStatusState(
|
|
|
407
455
|
xp?: number,
|
|
408
456
|
): void {
|
|
409
457
|
mkdirSync(STATE_DIR, { recursive: true });
|
|
410
|
-
const { renderFace, RARITY_STARS } =
|
|
411
|
-
require("../core/engine.ts") as typeof
|
|
458
|
+
const { renderFace, RARITY_STARS, resolveEyeGlyph } =
|
|
459
|
+
require("../core/engine.ts") as typeof CoreEngine;
|
|
412
460
|
const { getStatusFrames } =
|
|
413
|
-
require("./art.ts") as typeof
|
|
461
|
+
require("./art.ts") as typeof Art;
|
|
462
|
+
const safeEye = resolveEyeGlyph(companion.bones.eye);
|
|
414
463
|
const { frames, frameSequence } = getStatusFrames(companion.bones);
|
|
415
464
|
let xpLevel = level ?? 1;
|
|
416
465
|
let xpTotal = xp ?? 0;
|
|
@@ -434,8 +483,8 @@ export function writeStatusState(
|
|
|
434
483
|
species: companion.bones.species,
|
|
435
484
|
rarity: companion.bones.rarity,
|
|
436
485
|
stars: RARITY_STARS[companion.bones.rarity],
|
|
437
|
-
face: renderFace(companion.bones.species,
|
|
438
|
-
eye:
|
|
486
|
+
face: renderFace(companion.bones.species, safeEye),
|
|
487
|
+
eye: safeEye,
|
|
439
488
|
shiny: companion.bones.shiny,
|
|
440
489
|
hat: companion.bones.hat,
|
|
441
490
|
reaction: "",
|
|
@@ -447,14 +496,16 @@ export function writeStatusState(
|
|
|
447
496
|
xp: xpTotal,
|
|
448
497
|
mood: moodStr,
|
|
449
498
|
};
|
|
450
|
-
|
|
451
|
-
|
|
499
|
+
// writeStatusState no longer calls saveReaction implicitly. Callers
|
|
500
|
+
// that need a reaction file do so explicitly with the correct
|
|
501
|
+
// `source` tag — the old implicit save had no source argument and
|
|
502
|
+
// its default later flipped from "tool" to "fallback", either way
|
|
503
|
+
// overwriting provenance of hatch / pet / unmute writes with whatever
|
|
504
|
+
// happened to be the default. /buddy stats stopped lying when
|
|
505
|
+
// this call was removed.
|
|
452
506
|
}
|
|
453
507
|
|
|
454
|
-
// ─── Claude Code settings.json patching (for buddy_statusline tool) ──────────
|
|
455
|
-
|
|
456
508
|
export const CLAUDE_SETTINGS_PATH = claudeSettingsPath();
|
|
457
|
-
|
|
458
509
|
/**
|
|
459
510
|
* Write settings.statusLine pointing to the given buddy-status script.
|
|
460
511
|
* Atomic via tmp + rename. Returns false if settings.json is unreachable.
|
package/skills/buddy/SKILL.md
CHANGED
|
@@ -86,7 +86,7 @@ The MCP tools return pre-formatted ASCII art with ANSI colors, box-drawing chara
|
|
|
86
86
|
|
|
87
87
|
**Just output the raw text content from the tool result. Nothing else.** The ASCII art IS the response.
|
|
88
88
|
|
|
89
|
-
If the user mentions the buddy's name in normal conversation, call `buddy_react` with reason "turn"
|
|
89
|
+
If the user mentions the buddy's name in normal conversation, call `buddy_react` with reason "turn". Do NOT echo or quote the tool result — the reaction reaches the user only via the statusline speech bubble.
|
|
90
90
|
|
|
91
91
|
## Uninstall Orchestration
|
|
92
92
|
|