@ramarivera/coding-buddy 0.8.0 → 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/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/package.json +6 -2
- package/scripts/ci/Dockerfile.vhs +125 -0
- package/scripts/ci/check-statusline.sh +328 -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 +99 -0
- package/scripts/ci/render-vhs-goldens.sh +235 -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/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 +63 -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 +52 -10
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,6 +353,7 @@ 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;
|
|
@@ -336,11 +369,15 @@ export interface BuddyConfig {
|
|
|
336
369
|
|
|
337
370
|
const DEFAULT_CONFIG: BuddyConfig = {
|
|
338
371
|
commentCooldown: 30,
|
|
339
|
-
|
|
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,
|
|
340
376
|
bubbleStyle: "classic",
|
|
341
377
|
bubblePosition: "top",
|
|
342
378
|
showRarity: true,
|
|
343
379
|
statusLineEnabled: false,
|
|
380
|
+
statuslineWidthAdjust: 0,
|
|
344
381
|
bubbleWidth: 28,
|
|
345
382
|
bubbleMargin: 8,
|
|
346
383
|
useCombinedStatus: false,
|
|
@@ -363,6 +400,12 @@ export function normalizeConfig(data: unknown): BuddyConfig {
|
|
|
363
400
|
if (!Number.isInteger(config.subStatusRefreshSeconds) || config.subStatusRefreshSeconds <= 0) {
|
|
364
401
|
config.subStatusRefreshSeconds = DEFAULT_CONFIG.subStatusRefreshSeconds;
|
|
365
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
|
+
}
|
|
366
409
|
return config;
|
|
367
410
|
}
|
|
368
411
|
|
|
@@ -412,10 +455,11 @@ export function writeStatusState(
|
|
|
412
455
|
xp?: number,
|
|
413
456
|
): void {
|
|
414
457
|
mkdirSync(STATE_DIR, { recursive: true });
|
|
415
|
-
const { renderFace, RARITY_STARS } =
|
|
416
|
-
require("../core/engine.ts") as typeof
|
|
458
|
+
const { renderFace, RARITY_STARS, resolveEyeGlyph } =
|
|
459
|
+
require("../core/engine.ts") as typeof CoreEngine;
|
|
417
460
|
const { getStatusFrames } =
|
|
418
|
-
require("./art.ts") as typeof
|
|
461
|
+
require("./art.ts") as typeof Art;
|
|
462
|
+
const safeEye = resolveEyeGlyph(companion.bones.eye);
|
|
419
463
|
const { frames, frameSequence } = getStatusFrames(companion.bones);
|
|
420
464
|
let xpLevel = level ?? 1;
|
|
421
465
|
let xpTotal = xp ?? 0;
|
|
@@ -439,8 +483,8 @@ export function writeStatusState(
|
|
|
439
483
|
species: companion.bones.species,
|
|
440
484
|
rarity: companion.bones.rarity,
|
|
441
485
|
stars: RARITY_STARS[companion.bones.rarity],
|
|
442
|
-
face: renderFace(companion.bones.species,
|
|
443
|
-
eye:
|
|
486
|
+
face: renderFace(companion.bones.species, safeEye),
|
|
487
|
+
eye: safeEye,
|
|
444
488
|
shiny: companion.bones.shiny,
|
|
445
489
|
hat: companion.bones.hat,
|
|
446
490
|
reaction: "",
|
|
@@ -452,14 +496,16 @@ export function writeStatusState(
|
|
|
452
496
|
xp: xpTotal,
|
|
453
497
|
mood: moodStr,
|
|
454
498
|
};
|
|
455
|
-
|
|
456
|
-
|
|
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.
|
|
457
506
|
}
|
|
458
507
|
|
|
459
|
-
// ─── Claude Code settings.json patching (for buddy_statusline tool) ──────────
|
|
460
|
-
|
|
461
508
|
export const CLAUDE_SETTINGS_PATH = claudeSettingsPath();
|
|
462
|
-
|
|
463
509
|
/**
|
|
464
510
|
* Write settings.statusLine pointing to the given buddy-status script.
|
|
465
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
|
|