@ramarivera/coding-buddy 0.8.0 → 0.9.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/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 +17 -6
- 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 +17 -6
- package/adapters/shared/file-storage.ts +22 -0
- package/adapters/shared/widget-layout.ts +248 -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 +3 -0
- package/core/ports.ts +4 -0
- package/core/reactions.ts +29 -2
- package/core/render-model.ts +4 -3
- package/package.json +6 -3
- package/scripts/ci/Dockerfile.vhs +125 -0
- package/scripts/ci/check-statusline.sh +441 -14
- 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 +107 -0
- package/scripts/ci/render-vhs-goldens.sh +226 -0
- package/scripts/ci/statusline.tape +28 -2
- package/scripts/ci/vhs-density-fixtures.sh +27 -0
- package/scripts/ci/vhs-run-statusline.sh +28 -0
- package/scripts/ci/write-density-status.ts +86 -0
- package/scripts/gen-emoji-widths.ts +41 -17
- package/scripts/paths.sh +3 -1
- package/server/art.ts +70 -17
- package/server/hooks/buddy-comment.ts +166 -17
- package/server/hooks/file-type-react.ts +1 -1
- package/server/hooks/mood-react.ts +1 -1
- package/server/hooks/name-react.ts +1 -1
- package/server/hooks/react.ts +1 -1
- package/server/hooks/reaction-data.ts +10 -3
- package/server/index.ts +67 -27
- package/server/path.ts +3 -0
- package/server/state.ts +147 -41
- package/skills/buddy/SKILL.md +1 -1
- package/statusline/buddy-status.sh +488 -76
- package/statusline/emoji-text.data +3 -0
- package/statusline/emoji-widths.data +2 -3
- package/statusline/substatus.sh +52 -10
- package/scripts/postinstall.cjs +0 -42
package/server/state.ts
CHANGED
|
@@ -30,7 +30,12 @@ import {
|
|
|
30
30
|
rmSync,
|
|
31
31
|
} from "fs";
|
|
32
32
|
import { join } from "path";
|
|
33
|
-
import
|
|
33
|
+
import { configuredSharedUserId } from "../core/identity.ts";
|
|
34
|
+
import type { BuddyBones, Companion } from "../core/engine.ts"
|
|
35
|
+
import type * as CoreEngine from "../core/engine.ts";
|
|
36
|
+
import type * as Art from "./art.ts";
|
|
37
|
+
import type * as MoodModule from "./mood.ts";
|
|
38
|
+
import type * as XpModule from "./xp.ts";
|
|
34
39
|
import {
|
|
35
40
|
buddyStateDir,
|
|
36
41
|
claudeSettingsPath,
|
|
@@ -38,9 +43,17 @@ import {
|
|
|
38
43
|
toUnixPath,
|
|
39
44
|
} from "./path.ts";
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
// Resolved per call, never captured at import. ES modules are cached, so a
|
|
47
|
+
// module-level `buddyStateDir()` pinned these paths to whatever
|
|
48
|
+
// CLAUDE_CONFIG_DIR happened to be at *first* import. Once any file in a test
|
|
49
|
+
// process had imported this module, every later redirect was silently ignored —
|
|
50
|
+
// which is how `bun test` came to overwrite a user's live companion with a test
|
|
51
|
+
// fixture. Individually the test files were clean; only running them together
|
|
52
|
+
// leaked. A lazy lookup makes that class of bug impossible.
|
|
53
|
+
const stateDir = () => buddyStateDir();
|
|
54
|
+
const MANIFEST_FILE = () => join(stateDir(), "menagerie.json");
|
|
55
|
+
const CONFIG_FILE = () => join(stateDir(), "config.json");
|
|
56
|
+
const STATUS_FILE = () => join(stateDir(), "status.json");
|
|
44
57
|
|
|
45
58
|
// ─── Session ID (Claude Code session isolation, tmux pane fallback) ─────────
|
|
46
59
|
//
|
|
@@ -62,7 +75,7 @@ function sessionId(): string {
|
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
function reactionFile(): string {
|
|
65
|
-
return join(
|
|
78
|
+
return join(stateDir(), `reaction.${sessionId()}.json`);
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
// ─── Manifest schema ─────────────────────────────────────────────────────────
|
|
@@ -80,7 +93,7 @@ function emptyManifest(): Manifest {
|
|
|
80
93
|
|
|
81
94
|
function loadManifest(): Manifest {
|
|
82
95
|
try {
|
|
83
|
-
const raw = readFileSync(MANIFEST_FILE, "utf8");
|
|
96
|
+
const raw = readFileSync(MANIFEST_FILE(), "utf8");
|
|
84
97
|
const m = JSON.parse(raw) as Manifest;
|
|
85
98
|
if (!m.companions) m.companions = {};
|
|
86
99
|
return m;
|
|
@@ -90,10 +103,10 @@ function loadManifest(): Manifest {
|
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
function saveManifest(m: Manifest): void {
|
|
93
|
-
mkdirSync(
|
|
94
|
-
const tmp = MANIFEST_FILE + ".tmp";
|
|
106
|
+
mkdirSync(stateDir(), { recursive: true });
|
|
107
|
+
const tmp = MANIFEST_FILE() + ".tmp";
|
|
95
108
|
writeFileSync(tmp, JSON.stringify(m, null, 2));
|
|
96
|
-
renameSync(tmp, MANIFEST_FILE); // atomic on same filesystem
|
|
109
|
+
renameSync(tmp, MANIFEST_FILE()); // atomic on same filesystem
|
|
97
110
|
}
|
|
98
111
|
|
|
99
112
|
// ─── Slot helpers ────────────────────────────────────────────────────────────
|
|
@@ -219,13 +232,13 @@ export function saveCompanion(companion: Companion): void {
|
|
|
219
232
|
// ─── Migration: legacy companion.json -> single manifest ────────────────────
|
|
220
233
|
|
|
221
234
|
function migrateIfNeeded(): void {
|
|
222
|
-
if (existsSync(MANIFEST_FILE)) return;
|
|
235
|
+
if (existsSync(MANIFEST_FILE())) return;
|
|
223
236
|
|
|
224
237
|
const companions: Record<string, Companion> = {};
|
|
225
238
|
let active = "buddy";
|
|
226
239
|
|
|
227
240
|
// Absorb menagerie/<slot>.json files
|
|
228
|
-
const menagerieDir = join(
|
|
241
|
+
const menagerieDir = join(stateDir(), "menagerie");
|
|
229
242
|
if (existsSync(menagerieDir)) {
|
|
230
243
|
try {
|
|
231
244
|
for (const f of readdirSync(menagerieDir).filter((f) =>
|
|
@@ -246,7 +259,7 @@ function migrateIfNeeded(): void {
|
|
|
246
259
|
}
|
|
247
260
|
|
|
248
261
|
// Absorb legacy companion.json
|
|
249
|
-
const legacyFile = join(
|
|
262
|
+
const legacyFile = join(stateDir(), "companion.json");
|
|
250
263
|
if (existsSync(legacyFile) && Object.keys(companions).length === 0) {
|
|
251
264
|
try {
|
|
252
265
|
const c: Companion = JSON.parse(readFileSync(legacyFile, "utf8"));
|
|
@@ -259,7 +272,7 @@ function migrateIfNeeded(): void {
|
|
|
259
272
|
}
|
|
260
273
|
|
|
261
274
|
// Read active pointer if it exists
|
|
262
|
-
const activeFile = join(
|
|
275
|
+
const activeFile = join(stateDir(), "active");
|
|
263
276
|
if (existsSync(activeFile)) {
|
|
264
277
|
try {
|
|
265
278
|
const a = readFileSync(activeFile, "utf8").trim();
|
|
@@ -278,32 +291,63 @@ function migrateIfNeeded(): void {
|
|
|
278
291
|
|
|
279
292
|
// ─── Reaction state (session-scoped for tmux isolation) ──────────────────────
|
|
280
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Where a reaction came from. Proves the bubble content isn't canned when
|
|
296
|
+
* source === "tool" or "comment"; "fallback" means a hook picked a pool line
|
|
297
|
+
* because no model-authored text surfaced; "none" is the default for legacy
|
|
298
|
+
* files written before the field existed.
|
|
299
|
+
*
|
|
300
|
+
* tool — buddy_react MCP tool call (Claude wrote it; renders nowhere)
|
|
301
|
+
* comment — old `<!-- buddy: ... -->` HTML comment (legacy / older CC)
|
|
302
|
+
* fallback — Stop hook generated it from the canned pool
|
|
303
|
+
* none — unknown / legacy file without a source field
|
|
304
|
+
*/
|
|
305
|
+
export type ReactionSource = "tool" | "comment" | "fallback" | "none";
|
|
306
|
+
|
|
281
307
|
export interface ReactionState {
|
|
282
308
|
reaction: string;
|
|
283
309
|
timestamp: number;
|
|
284
310
|
reason: string;
|
|
311
|
+
/** Provenance — see ReactionSource. Defaults to "none" on legacy files. */
|
|
312
|
+
source?: ReactionSource;
|
|
285
313
|
}
|
|
286
314
|
|
|
287
315
|
export function loadReaction(): ReactionState | null {
|
|
288
316
|
try {
|
|
289
317
|
const data: ReactionState = JSON.parse(readFileSync(reactionFile(), "utf8"));
|
|
290
318
|
const { reactionTTL } = loadConfig();
|
|
291
|
-
|
|
319
|
+
if (reactionTTL > 0 && Date.now() - data.timestamp > reactionTTL * 1000) {
|
|
320
|
+
rmSync(reactionFile(), { force: true });
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
if (data.source === undefined) data.source = "none";
|
|
292
324
|
return data;
|
|
293
325
|
} catch {
|
|
294
326
|
return null;
|
|
295
327
|
}
|
|
296
328
|
}
|
|
297
329
|
|
|
298
|
-
export function saveReaction(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
330
|
+
export function saveReaction(
|
|
331
|
+
reaction: string,
|
|
332
|
+
reason: string,
|
|
333
|
+
source: ReactionSource = "fallback",
|
|
334
|
+
): void {
|
|
335
|
+
mkdirSync(stateDir(), { recursive: true });
|
|
336
|
+
const state: ReactionState = { reaction, timestamp: Date.now(), reason, source };
|
|
337
|
+
// Atomic via tmp + rename — torn reads on the reaction file would
|
|
338
|
+
// make the Stop hook's freshness check see an absent file, pinning
|
|
339
|
+
// a stale tool reaction into the bubble forever.
|
|
340
|
+
const target = reactionFile();
|
|
341
|
+
const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
|
|
342
|
+
writeFileSync(tmp, JSON.stringify(state));
|
|
343
|
+
renameSync(tmp, target);
|
|
302
344
|
}
|
|
303
345
|
|
|
304
346
|
// ─── Identity resolution ─────────────────────────────────────────────────────
|
|
305
347
|
|
|
306
348
|
export function resolveUserId(): string {
|
|
349
|
+
const sharedUserId = configuredSharedUserId();
|
|
350
|
+
if (sharedUserId) return sharedUserId;
|
|
307
351
|
try {
|
|
308
352
|
const claudeJson = JSON.parse(readFileSync(claudeUserConfigPath(), "utf8"));
|
|
309
353
|
return claudeJson.oauthAccount?.accountUuid ?? claudeJson.userID ?? "anon";
|
|
@@ -311,9 +355,7 @@ export function resolveUserId(): string {
|
|
|
311
355
|
return "anon";
|
|
312
356
|
}
|
|
313
357
|
}
|
|
314
|
-
|
|
315
358
|
// ─── Config persistence (PR #6: tmux popup settings) ─────────────────────────
|
|
316
|
-
|
|
317
359
|
export interface BuddyConfig {
|
|
318
360
|
commentCooldown: number;
|
|
319
361
|
reactionTTL: number;
|
|
@@ -321,6 +363,8 @@ export interface BuddyConfig {
|
|
|
321
363
|
bubblePosition: "top" | "left";
|
|
322
364
|
showRarity: boolean;
|
|
323
365
|
statusLineEnabled: boolean;
|
|
366
|
+
statuslineDensity: "auto" | "full" | "compact" | "minimal";
|
|
367
|
+
statuslineWidthAdjust: number;
|
|
324
368
|
bubbleWidth: number;
|
|
325
369
|
bubbleMargin: number;
|
|
326
370
|
useCombinedStatus: boolean;
|
|
@@ -333,14 +377,18 @@ export interface BuddyConfig {
|
|
|
333
377
|
suggestionsEnabled: boolean;
|
|
334
378
|
suggestionCooldown: number;
|
|
335
379
|
}
|
|
336
|
-
|
|
337
380
|
const DEFAULT_CONFIG: BuddyConfig = {
|
|
338
381
|
commentCooldown: 30,
|
|
339
|
-
|
|
382
|
+
// Long enough that a reaction survives the turn you were reading when it was
|
|
383
|
+
// written, short enough that yesterday's bubble is never still on screen.
|
|
384
|
+
// Set to 0 to opt out and keep reactions until they are replaced.
|
|
385
|
+
reactionTTL: 900,
|
|
340
386
|
bubbleStyle: "classic",
|
|
341
387
|
bubblePosition: "top",
|
|
342
388
|
showRarity: true,
|
|
343
389
|
statusLineEnabled: false,
|
|
390
|
+
statuslineDensity: "auto",
|
|
391
|
+
statuslineWidthAdjust: 0,
|
|
344
392
|
bubbleWidth: 28,
|
|
345
393
|
bubbleMargin: 8,
|
|
346
394
|
useCombinedStatus: false,
|
|
@@ -363,22 +411,32 @@ export function normalizeConfig(data: unknown): BuddyConfig {
|
|
|
363
411
|
if (!Number.isInteger(config.subStatusRefreshSeconds) || config.subStatusRefreshSeconds <= 0) {
|
|
364
412
|
config.subStatusRefreshSeconds = DEFAULT_CONFIG.subStatusRefreshSeconds;
|
|
365
413
|
}
|
|
414
|
+
if (!Number.isInteger(config.reactionTTL) || config.reactionTTL < 0) {
|
|
415
|
+
config.reactionTTL = DEFAULT_CONFIG.reactionTTL;
|
|
416
|
+
}
|
|
417
|
+
if (!Number.isInteger(config.statuslineWidthAdjust)) {
|
|
418
|
+
config.statuslineWidthAdjust = DEFAULT_CONFIG.statuslineWidthAdjust;
|
|
419
|
+
}
|
|
420
|
+
const allowedDensities: BuddyConfig["statuslineDensity"][] = ["auto", "full", "compact", "minimal"];
|
|
421
|
+
if (!allowedDensities.includes(config.statuslineDensity)) {
|
|
422
|
+
config.statuslineDensity = DEFAULT_CONFIG.statuslineDensity;
|
|
423
|
+
}
|
|
366
424
|
return config;
|
|
367
425
|
}
|
|
368
426
|
|
|
369
427
|
export function loadConfig(): BuddyConfig {
|
|
370
428
|
try {
|
|
371
|
-
return normalizeConfig(JSON.parse(readFileSync(CONFIG_FILE, "utf8")));
|
|
429
|
+
return normalizeConfig(JSON.parse(readFileSync(CONFIG_FILE(), "utf8")));
|
|
372
430
|
} catch {
|
|
373
431
|
return normalizeConfig({});
|
|
374
432
|
}
|
|
375
433
|
}
|
|
376
434
|
|
|
377
435
|
export function saveConfig(config: Partial<BuddyConfig>): BuddyConfig {
|
|
378
|
-
mkdirSync(
|
|
436
|
+
mkdirSync(stateDir(), { recursive: true });
|
|
379
437
|
const current = loadConfig();
|
|
380
438
|
const merged = { ...current, ...config };
|
|
381
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2));
|
|
439
|
+
writeFileSync(CONFIG_FILE(), JSON.stringify(merged, null, 2));
|
|
382
440
|
return merged;
|
|
383
441
|
}
|
|
384
442
|
|
|
@@ -397,12 +455,36 @@ export interface StatusState {
|
|
|
397
455
|
muted: boolean;
|
|
398
456
|
achievement: string;
|
|
399
457
|
frames: string[];
|
|
458
|
+
compactFrames: string[];
|
|
459
|
+
minimalFrames: string[];
|
|
400
460
|
frameSequence: number[];
|
|
401
461
|
level: number;
|
|
402
462
|
xp: number;
|
|
403
463
|
mood: string;
|
|
404
464
|
}
|
|
405
465
|
|
|
466
|
+
// Density frame fallbacks used when server/art.ts has not yet been updated
|
|
467
|
+
// to emit compactFrames/minimalFrames. The art worker owns the pre-rendered
|
|
468
|
+
// density frames; state.ts only derives a degraded fallback from the full
|
|
469
|
+
// frames so old/new status.json remains renderable.
|
|
470
|
+
function deriveCompactFrames(fullFrames: string[]): string[] {
|
|
471
|
+
return fullFrames.map((body) => {
|
|
472
|
+
const lines = body.split("\n");
|
|
473
|
+
const selected: string[] = [];
|
|
474
|
+
for (const line of lines) {
|
|
475
|
+
if (selected.length >= 3) break;
|
|
476
|
+
if (line.trim() !== "") selected.push(line);
|
|
477
|
+
}
|
|
478
|
+
while (selected.length < 3) selected.push("");
|
|
479
|
+
return selected.join("\n");
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function deriveMinimalFrames(bones: BuddyBones, eye: string): string[] {
|
|
484
|
+
const { renderFace } = require("../core/engine.ts") as typeof CoreEngine;
|
|
485
|
+
const face = renderFace(bones.species, eye);
|
|
486
|
+
return Array.from({ length: 4 }, () => face);
|
|
487
|
+
}
|
|
406
488
|
export function writeStatusState(
|
|
407
489
|
companion: Companion,
|
|
408
490
|
reaction?: string,
|
|
@@ -411,17 +493,32 @@ export function writeStatusState(
|
|
|
411
493
|
level?: number,
|
|
412
494
|
xp?: number,
|
|
413
495
|
): void {
|
|
414
|
-
mkdirSync(
|
|
415
|
-
const { renderFace, RARITY_STARS } =
|
|
416
|
-
require("../core/engine.ts") as typeof
|
|
496
|
+
mkdirSync(stateDir(), { recursive: true });
|
|
497
|
+
const { renderFace, RARITY_STARS, resolveEyeGlyph } =
|
|
498
|
+
require("../core/engine.ts") as typeof CoreEngine;
|
|
417
499
|
const { getStatusFrames } =
|
|
418
|
-
require("./art.ts") as typeof
|
|
419
|
-
const
|
|
500
|
+
require("./art.ts") as typeof Art;
|
|
501
|
+
const safeEye = resolveEyeGlyph(companion.bones.eye);
|
|
502
|
+
const artResult = getStatusFrames(companion.bones) as {
|
|
503
|
+
frames: string[];
|
|
504
|
+
frameSequence: number[];
|
|
505
|
+
compactFrames?: string[];
|
|
506
|
+
minimalFrames?: string[];
|
|
507
|
+
};
|
|
508
|
+
const { frames, frameSequence } = artResult;
|
|
509
|
+
const compactFrames =
|
|
510
|
+
artResult.compactFrames && artResult.compactFrames.length > 0
|
|
511
|
+
? artResult.compactFrames
|
|
512
|
+
: deriveCompactFrames(frames);
|
|
513
|
+
const minimalFrames =
|
|
514
|
+
artResult.minimalFrames && artResult.minimalFrames.length > 0
|
|
515
|
+
? artResult.minimalFrames
|
|
516
|
+
: deriveMinimalFrames(companion.bones, safeEye);
|
|
420
517
|
let xpLevel = level ?? 1;
|
|
421
518
|
let xpTotal = xp ?? 0;
|
|
422
519
|
let moodStr = "focused";
|
|
423
520
|
try {
|
|
424
|
-
const { getXpState } = require("./xp.ts") as typeof
|
|
521
|
+
const { getXpState } = require("./xp.ts") as typeof XpModule;
|
|
425
522
|
const xpState = getXpState();
|
|
426
523
|
xpLevel = xpState.level;
|
|
427
524
|
xpTotal = xpState.totalXp;
|
|
@@ -429,7 +526,7 @@ export function writeStatusState(
|
|
|
429
526
|
// XP state is optional during first install / version skew.
|
|
430
527
|
}
|
|
431
528
|
try {
|
|
432
|
-
const { getMood } = require("./mood.ts") as typeof
|
|
529
|
+
const { getMood } = require("./mood.ts") as typeof MoodModule;
|
|
433
530
|
moodStr = getMood().current;
|
|
434
531
|
} catch {
|
|
435
532
|
// Mood state is optional during first install / version skew.
|
|
@@ -439,27 +536,36 @@ export function writeStatusState(
|
|
|
439
536
|
species: companion.bones.species,
|
|
440
537
|
rarity: companion.bones.rarity,
|
|
441
538
|
stars: RARITY_STARS[companion.bones.rarity],
|
|
442
|
-
face: renderFace(companion.bones.species,
|
|
443
|
-
eye:
|
|
539
|
+
face: renderFace(companion.bones.species, safeEye),
|
|
540
|
+
eye: safeEye,
|
|
444
541
|
shiny: companion.bones.shiny,
|
|
445
542
|
hat: companion.bones.hat,
|
|
446
543
|
reaction: "",
|
|
447
544
|
muted: muted ?? false,
|
|
448
545
|
achievement: achievement ?? "",
|
|
449
546
|
frames,
|
|
547
|
+
compactFrames,
|
|
548
|
+
minimalFrames,
|
|
450
549
|
frameSequence,
|
|
451
550
|
level: xpLevel,
|
|
452
551
|
xp: xpTotal,
|
|
453
552
|
mood: moodStr,
|
|
454
553
|
};
|
|
455
|
-
|
|
456
|
-
|
|
554
|
+
// Atomic: the statusline reads this file on a timer, so a plain write can
|
|
555
|
+
// be observed half-flushed and blank the companion for a tick.
|
|
556
|
+
const statusTmp = `${STATUS_FILE()}.tmp`;
|
|
557
|
+
writeFileSync(statusTmp, JSON.stringify(state));
|
|
558
|
+
renameSync(statusTmp, STATUS_FILE());
|
|
559
|
+
// writeStatusState no longer calls saveReaction implicitly. Callers
|
|
560
|
+
// that need a reaction file do so explicitly with the correct
|
|
561
|
+
// `source` tag — the old implicit save had no source argument and
|
|
562
|
+
// its default later flipped from "tool" to "fallback", either way
|
|
563
|
+
// overwriting provenance of hatch / pet / unmute writes with whatever
|
|
564
|
+
// happened to be the default. /buddy stats stopped lying when
|
|
565
|
+
// this call was removed.
|
|
457
566
|
}
|
|
458
567
|
|
|
459
|
-
// ─── Claude Code settings.json patching (for buddy_statusline tool) ──────────
|
|
460
|
-
|
|
461
568
|
export const CLAUDE_SETTINGS_PATH = claudeSettingsPath();
|
|
462
|
-
|
|
463
569
|
/**
|
|
464
570
|
* Write settings.statusLine pointing to the given buddy-status script.
|
|
465
571
|
* Atomic via tmp + rename. Returns false if settings.json is unreachable.
|
|
@@ -534,11 +640,11 @@ const TRANSIENT_PREFIXES = [
|
|
|
534
640
|
*
|
|
535
641
|
* Companion data (menagerie.json, status.json, config.json) is intentionally
|
|
536
642
|
* kept — users reinstalling get their buddy back. Call-sites that want a full
|
|
537
|
-
* wipe should delete
|
|
643
|
+
* wipe should delete stateDir() themselves after calling this.
|
|
538
644
|
*/
|
|
539
645
|
export function cleanupPluginState(
|
|
540
646
|
settingsPath: string = CLAUDE_SETTINGS_PATH,
|
|
541
|
-
stateDir: string =
|
|
647
|
+
stateDir: string = buddyStateDir(),
|
|
542
648
|
): CleanupResult {
|
|
543
649
|
const statusLineRemoved = unsetBuddyStatusLine(settingsPath);
|
|
544
650
|
|
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
|
|