@ramarivera/coding-buddy 0.9.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/adapters/omp/ui.ts +13 -31
- package/adapters/pi/ui.ts +13 -31
- package/adapters/shared/file-storage.ts +1 -0
- package/adapters/shared/widget-layout.ts +125 -11
- package/core/model.ts +1 -0
- package/package.json +1 -2
- package/scripts/ci/check-statusline.sh +231 -118
- package/scripts/ci/render-statusline-pty.py +12 -4
- package/scripts/ci/render-vhs-goldens.sh +10 -19
- package/scripts/ci/statusline.tape +28 -2
- package/scripts/ci/vhs-density-fixtures.sh +27 -0
- package/scripts/ci/vhs-run-statusline.sh +13 -1
- package/scripts/ci/write-density-status.ts +86 -0
- package/server/art.ts +29 -8
- package/server/state.ts +85 -25
- package/statusline/buddy-status.sh +161 -41
- package/scripts/postinstall.cjs +0 -42
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Invoked from VHS tapes. Args: <cols> <fixture-relpath-from-repo-root>
|
|
3
|
+
# BUDDY_STATUSLINE_ROWS defaults to 50 (full density) so golden renders are not
|
|
4
|
+
# sensitive to the VHS terminal's actual row count.
|
|
3
5
|
set -euo pipefail
|
|
4
6
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
5
7
|
cols="${1:?cols}"
|
|
6
8
|
fixture_rel="${2:?fixture}"
|
|
7
9
|
payload='{"session_id":"ci","model":{"display_name":"CI"},"workspace":{"current_dir":"."}}'
|
|
10
|
+
|
|
11
|
+
if [[ "$fixture_rel" = /* ]]; then
|
|
12
|
+
cfg_dir="$fixture_rel"
|
|
13
|
+
else
|
|
14
|
+
cfg_dir="$ROOT/$fixture_rel"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
rows="${BUDDY_STATUSLINE_ROWS:-50}"
|
|
18
|
+
|
|
8
19
|
cd "$ROOT"
|
|
9
20
|
printf '%s' "$payload" \
|
|
10
21
|
| COLUMNS="$cols" \
|
|
11
22
|
BUDDY_FAKE_NOW=0 \
|
|
12
|
-
|
|
23
|
+
BUDDY_STATUSLINE_ROWS="$rows" \
|
|
24
|
+
CLAUDE_CONFIG_DIR="$cfg_dir" \
|
|
13
25
|
CLAUDE_CODE_SESSION_ID= \
|
|
14
26
|
TMUX_PANE= \
|
|
15
27
|
BUDDY_SHELL= \
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Derives a status.json fixture with full/compact/minimal frame arrays.
|
|
4
|
+
*
|
|
5
|
+
* Reads a base status object from stdin, computes frames from species/eye/hat,
|
|
6
|
+
* and writes the augmented object to the file named by the first argument.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* cat base-status.json | bun scripts/ci/write-density-status.ts dest.json
|
|
10
|
+
* bun scripts/ci/write-density-status.ts dest.json < base-status.json
|
|
11
|
+
*/
|
|
12
|
+
import { writeFileSync } from "fs";
|
|
13
|
+
import { getStatusFrames } from "../../server/art.ts";
|
|
14
|
+
import { renderFace, resolveEyeGlyph, type Species } from "../../core/engine.ts";
|
|
15
|
+
|
|
16
|
+
function deriveCompactFrames(fullFrames: string[]): string[] {
|
|
17
|
+
return fullFrames.map((body) => {
|
|
18
|
+
const lines = body.split("\n");
|
|
19
|
+
const selected: string[] = [];
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
if (selected.length >= 3) break;
|
|
22
|
+
if (line.trim() !== "") selected.push(line);
|
|
23
|
+
}
|
|
24
|
+
while (selected.length < 3) selected.push("");
|
|
25
|
+
return selected.join("\n");
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function deriveMinimalFrames(species: Species, eye: string): string[] {
|
|
30
|
+
const face = renderFace(species, resolveEyeGlyph(eye));
|
|
31
|
+
return Array.from({ length: 4 }, () => face);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function main() {
|
|
35
|
+
const dest = process.argv[2];
|
|
36
|
+
if (!dest) {
|
|
37
|
+
console.error("usage: write-density-status.ts <dest.json>");
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let input = "";
|
|
42
|
+
for await (const chunk of process.stdin) {
|
|
43
|
+
input += chunk;
|
|
44
|
+
}
|
|
45
|
+
const base = input ? JSON.parse(input) : {};
|
|
46
|
+
|
|
47
|
+
const bones = {
|
|
48
|
+
species: base.species ?? "pikachu",
|
|
49
|
+
rarity: base.rarity ?? "common",
|
|
50
|
+
eye: base.eye ?? "o",
|
|
51
|
+
hat: base.hat ?? "none",
|
|
52
|
+
shiny: base.shiny ?? false,
|
|
53
|
+
stats: base.stats ?? { DEBUGGING: 50, PATIENCE: 50, CHAOS: 50, WISDOM: 50, SNARK: 50 },
|
|
54
|
+
peak: base.peak ?? "DEBUGGING",
|
|
55
|
+
dump: base.dump ?? "PATIENCE",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const result = getStatusFrames(bones) as {
|
|
59
|
+
frames: string[];
|
|
60
|
+
frameSequence: number[];
|
|
61
|
+
compactFrames?: string[];
|
|
62
|
+
minimalFrames?: string[];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const compactFrames =
|
|
66
|
+
result.compactFrames && result.compactFrames.length > 0
|
|
67
|
+
? result.compactFrames
|
|
68
|
+
: deriveCompactFrames(result.frames);
|
|
69
|
+
|
|
70
|
+
const minimalFrames =
|
|
71
|
+
result.minimalFrames && result.minimalFrames.length > 0
|
|
72
|
+
? result.minimalFrames
|
|
73
|
+
: deriveMinimalFrames(bones.species as Species, bones.eye);
|
|
74
|
+
|
|
75
|
+
const out = {
|
|
76
|
+
...base,
|
|
77
|
+
frames: result.frames,
|
|
78
|
+
compactFrames,
|
|
79
|
+
minimalFrames,
|
|
80
|
+
frameSequence: result.frameSequence,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
writeFileSync(dest, JSON.stringify(out));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
main();
|
package/server/art.ts
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
* {E} is replaced with the eye character at render time.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { resolveEyeGlyph, type Species, type Hat, type Rarity, type StatName, type BuddyBones } from "../core/engine.ts"
|
|
9
|
+
import { renderFace, resolveEyeGlyph, type Species, type Hat, type Rarity, type StatName, type BuddyBones } from "../core/engine.ts"
|
|
10
10
|
export { resolveEyeGlyph };
|
|
11
11
|
import { HAT_ART } from "../core/art-data.ts";
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
export { HAT_ART };
|
|
14
|
+
import { getRarityColor } from "./theme.ts";
|
|
14
15
|
|
|
15
16
|
// ─── Species art: 3 frames × 5 lines each ──────────────────────────────────
|
|
16
17
|
|
|
@@ -265,6 +266,8 @@ export const STATUS_FRAME_SEQUENCE: readonly number[] = [
|
|
|
265
266
|
// \n-joined 5-line string (one jq call + mapfile in bash).
|
|
266
267
|
export function getStatusFrames(bones: BuddyBones): {
|
|
267
268
|
frames: string[];
|
|
269
|
+
compactFrames: string[];
|
|
270
|
+
minimalFrames: string[];
|
|
268
271
|
frameSequence: number[];
|
|
269
272
|
} {
|
|
270
273
|
const resolveFrame = (frameIdx: number, eyeGlyph: string): string => {
|
|
@@ -277,16 +280,34 @@ export function getStatusFrames(bones: BuddyBones): {
|
|
|
277
280
|
return art.join("\n");
|
|
278
281
|
};
|
|
279
282
|
|
|
283
|
+
// Compact: keep the first three non-empty rows of each full frame. This
|
|
284
|
+
// preserves a hat if it occupies the top row, otherwise it drops the blank
|
|
285
|
+
// placeholder and shows the face/body.
|
|
286
|
+
const deriveCompactFrames = (fullFrames: string[]): string[] =>
|
|
287
|
+
fullFrames.map((body) => {
|
|
288
|
+
const lines = body.split("\n");
|
|
289
|
+
const selected: string[] = [];
|
|
290
|
+
for (const line of lines) {
|
|
291
|
+
if (selected.length >= 3) break;
|
|
292
|
+
if (line.trim() !== "") selected.push(line);
|
|
293
|
+
}
|
|
294
|
+
while (selected.length < 3) selected.push("");
|
|
295
|
+
return selected.join("\n");
|
|
296
|
+
});
|
|
297
|
+
|
|
280
298
|
// Sanitize once at the boundary. Blink uses a literal "-" eye, which is
|
|
281
299
|
// intentionally outside EYES and must not pass through resolveEyeGlyph.
|
|
282
300
|
const eye = resolveEyeGlyph(bones.eye);
|
|
301
|
+
const frames = [
|
|
302
|
+
resolveFrame(0, eye),
|
|
303
|
+
resolveFrame(1, eye),
|
|
304
|
+
resolveFrame(2, eye),
|
|
305
|
+
resolveFrame(0, "-"),
|
|
306
|
+
];
|
|
283
307
|
return {
|
|
284
|
-
frames
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
resolveFrame(2, eye),
|
|
288
|
-
resolveFrame(0, "-"),
|
|
289
|
-
],
|
|
308
|
+
frames,
|
|
309
|
+
compactFrames: deriveCompactFrames(frames),
|
|
310
|
+
minimalFrames: Array.from({ length: frames.length }, () => renderFace(bones.species, eye)),
|
|
290
311
|
frameSequence: [...STATUS_FRAME_SEQUENCE],
|
|
291
312
|
};
|
|
292
313
|
}
|
package/server/state.ts
CHANGED
|
@@ -30,10 +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"
|
|
34
35
|
import type * as CoreEngine from "../core/engine.ts";
|
|
35
36
|
import type * as Art from "./art.ts";
|
|
36
|
-
import
|
|
37
|
+
import type * as MoodModule from "./mood.ts";
|
|
38
|
+
import type * as XpModule from "./xp.ts";
|
|
37
39
|
import {
|
|
38
40
|
buddyStateDir,
|
|
39
41
|
claudeSettingsPath,
|
|
@@ -41,9 +43,17 @@ import {
|
|
|
41
43
|
toUnixPath,
|
|
42
44
|
} from "./path.ts";
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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");
|
|
47
57
|
|
|
48
58
|
// ─── Session ID (Claude Code session isolation, tmux pane fallback) ─────────
|
|
49
59
|
//
|
|
@@ -65,7 +75,7 @@ function sessionId(): string {
|
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
function reactionFile(): string {
|
|
68
|
-
return join(
|
|
78
|
+
return join(stateDir(), `reaction.${sessionId()}.json`);
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
// ─── Manifest schema ─────────────────────────────────────────────────────────
|
|
@@ -83,7 +93,7 @@ function emptyManifest(): Manifest {
|
|
|
83
93
|
|
|
84
94
|
function loadManifest(): Manifest {
|
|
85
95
|
try {
|
|
86
|
-
const raw = readFileSync(MANIFEST_FILE, "utf8");
|
|
96
|
+
const raw = readFileSync(MANIFEST_FILE(), "utf8");
|
|
87
97
|
const m = JSON.parse(raw) as Manifest;
|
|
88
98
|
if (!m.companions) m.companions = {};
|
|
89
99
|
return m;
|
|
@@ -93,10 +103,10 @@ function loadManifest(): Manifest {
|
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
function saveManifest(m: Manifest): void {
|
|
96
|
-
mkdirSync(
|
|
97
|
-
const tmp = MANIFEST_FILE + ".tmp";
|
|
106
|
+
mkdirSync(stateDir(), { recursive: true });
|
|
107
|
+
const tmp = MANIFEST_FILE() + ".tmp";
|
|
98
108
|
writeFileSync(tmp, JSON.stringify(m, null, 2));
|
|
99
|
-
renameSync(tmp, MANIFEST_FILE); // atomic on same filesystem
|
|
109
|
+
renameSync(tmp, MANIFEST_FILE()); // atomic on same filesystem
|
|
100
110
|
}
|
|
101
111
|
|
|
102
112
|
// ─── Slot helpers ────────────────────────────────────────────────────────────
|
|
@@ -222,13 +232,13 @@ export function saveCompanion(companion: Companion): void {
|
|
|
222
232
|
// ─── Migration: legacy companion.json -> single manifest ────────────────────
|
|
223
233
|
|
|
224
234
|
function migrateIfNeeded(): void {
|
|
225
|
-
if (existsSync(MANIFEST_FILE)) return;
|
|
235
|
+
if (existsSync(MANIFEST_FILE())) return;
|
|
226
236
|
|
|
227
237
|
const companions: Record<string, Companion> = {};
|
|
228
238
|
let active = "buddy";
|
|
229
239
|
|
|
230
240
|
// Absorb menagerie/<slot>.json files
|
|
231
|
-
const menagerieDir = join(
|
|
241
|
+
const menagerieDir = join(stateDir(), "menagerie");
|
|
232
242
|
if (existsSync(menagerieDir)) {
|
|
233
243
|
try {
|
|
234
244
|
for (const f of readdirSync(menagerieDir).filter((f) =>
|
|
@@ -249,7 +259,7 @@ function migrateIfNeeded(): void {
|
|
|
249
259
|
}
|
|
250
260
|
|
|
251
261
|
// Absorb legacy companion.json
|
|
252
|
-
const legacyFile = join(
|
|
262
|
+
const legacyFile = join(stateDir(), "companion.json");
|
|
253
263
|
if (existsSync(legacyFile) && Object.keys(companions).length === 0) {
|
|
254
264
|
try {
|
|
255
265
|
const c: Companion = JSON.parse(readFileSync(legacyFile, "utf8"));
|
|
@@ -262,7 +272,7 @@ function migrateIfNeeded(): void {
|
|
|
262
272
|
}
|
|
263
273
|
|
|
264
274
|
// Read active pointer if it exists
|
|
265
|
-
const activeFile = join(
|
|
275
|
+
const activeFile = join(stateDir(), "active");
|
|
266
276
|
if (existsSync(activeFile)) {
|
|
267
277
|
try {
|
|
268
278
|
const a = readFileSync(activeFile, "utf8").trim();
|
|
@@ -322,7 +332,7 @@ export function saveReaction(
|
|
|
322
332
|
reason: string,
|
|
323
333
|
source: ReactionSource = "fallback",
|
|
324
334
|
): void {
|
|
325
|
-
mkdirSync(
|
|
335
|
+
mkdirSync(stateDir(), { recursive: true });
|
|
326
336
|
const state: ReactionState = { reaction, timestamp: Date.now(), reason, source };
|
|
327
337
|
// Atomic via tmp + rename — torn reads on the reaction file would
|
|
328
338
|
// make the Stop hook's freshness check see an absent file, pinning
|
|
@@ -353,6 +363,7 @@ export interface BuddyConfig {
|
|
|
353
363
|
bubblePosition: "top" | "left";
|
|
354
364
|
showRarity: boolean;
|
|
355
365
|
statusLineEnabled: boolean;
|
|
366
|
+
statuslineDensity: "auto" | "full" | "compact" | "minimal";
|
|
356
367
|
statuslineWidthAdjust: number;
|
|
357
368
|
bubbleWidth: number;
|
|
358
369
|
bubbleMargin: number;
|
|
@@ -366,7 +377,6 @@ export interface BuddyConfig {
|
|
|
366
377
|
suggestionsEnabled: boolean;
|
|
367
378
|
suggestionCooldown: number;
|
|
368
379
|
}
|
|
369
|
-
|
|
370
380
|
const DEFAULT_CONFIG: BuddyConfig = {
|
|
371
381
|
commentCooldown: 30,
|
|
372
382
|
// Long enough that a reaction survives the turn you were reading when it was
|
|
@@ -377,6 +387,7 @@ const DEFAULT_CONFIG: BuddyConfig = {
|
|
|
377
387
|
bubblePosition: "top",
|
|
378
388
|
showRarity: true,
|
|
379
389
|
statusLineEnabled: false,
|
|
390
|
+
statuslineDensity: "auto",
|
|
380
391
|
statuslineWidthAdjust: 0,
|
|
381
392
|
bubbleWidth: 28,
|
|
382
393
|
bubbleMargin: 8,
|
|
@@ -406,22 +417,26 @@ export function normalizeConfig(data: unknown): BuddyConfig {
|
|
|
406
417
|
if (!Number.isInteger(config.statuslineWidthAdjust)) {
|
|
407
418
|
config.statuslineWidthAdjust = DEFAULT_CONFIG.statuslineWidthAdjust;
|
|
408
419
|
}
|
|
420
|
+
const allowedDensities: BuddyConfig["statuslineDensity"][] = ["auto", "full", "compact", "minimal"];
|
|
421
|
+
if (!allowedDensities.includes(config.statuslineDensity)) {
|
|
422
|
+
config.statuslineDensity = DEFAULT_CONFIG.statuslineDensity;
|
|
423
|
+
}
|
|
409
424
|
return config;
|
|
410
425
|
}
|
|
411
426
|
|
|
412
427
|
export function loadConfig(): BuddyConfig {
|
|
413
428
|
try {
|
|
414
|
-
return normalizeConfig(JSON.parse(readFileSync(CONFIG_FILE, "utf8")));
|
|
429
|
+
return normalizeConfig(JSON.parse(readFileSync(CONFIG_FILE(), "utf8")));
|
|
415
430
|
} catch {
|
|
416
431
|
return normalizeConfig({});
|
|
417
432
|
}
|
|
418
433
|
}
|
|
419
434
|
|
|
420
435
|
export function saveConfig(config: Partial<BuddyConfig>): BuddyConfig {
|
|
421
|
-
mkdirSync(
|
|
436
|
+
mkdirSync(stateDir(), { recursive: true });
|
|
422
437
|
const current = loadConfig();
|
|
423
438
|
const merged = { ...current, ...config };
|
|
424
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2));
|
|
439
|
+
writeFileSync(CONFIG_FILE(), JSON.stringify(merged, null, 2));
|
|
425
440
|
return merged;
|
|
426
441
|
}
|
|
427
442
|
|
|
@@ -440,12 +455,36 @@ export interface StatusState {
|
|
|
440
455
|
muted: boolean;
|
|
441
456
|
achievement: string;
|
|
442
457
|
frames: string[];
|
|
458
|
+
compactFrames: string[];
|
|
459
|
+
minimalFrames: string[];
|
|
443
460
|
frameSequence: number[];
|
|
444
461
|
level: number;
|
|
445
462
|
xp: number;
|
|
446
463
|
mood: string;
|
|
447
464
|
}
|
|
448
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
|
+
}
|
|
449
488
|
export function writeStatusState(
|
|
450
489
|
companion: Companion,
|
|
451
490
|
reaction?: string,
|
|
@@ -454,18 +493,32 @@ export function writeStatusState(
|
|
|
454
493
|
level?: number,
|
|
455
494
|
xp?: number,
|
|
456
495
|
): void {
|
|
457
|
-
mkdirSync(
|
|
496
|
+
mkdirSync(stateDir(), { recursive: true });
|
|
458
497
|
const { renderFace, RARITY_STARS, resolveEyeGlyph } =
|
|
459
498
|
require("../core/engine.ts") as typeof CoreEngine;
|
|
460
499
|
const { getStatusFrames } =
|
|
461
500
|
require("./art.ts") as typeof Art;
|
|
462
501
|
const safeEye = resolveEyeGlyph(companion.bones.eye);
|
|
463
|
-
const
|
|
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);
|
|
464
517
|
let xpLevel = level ?? 1;
|
|
465
518
|
let xpTotal = xp ?? 0;
|
|
466
519
|
let moodStr = "focused";
|
|
467
520
|
try {
|
|
468
|
-
const { getXpState } = require("./xp.ts") as typeof
|
|
521
|
+
const { getXpState } = require("./xp.ts") as typeof XpModule;
|
|
469
522
|
const xpState = getXpState();
|
|
470
523
|
xpLevel = xpState.level;
|
|
471
524
|
xpTotal = xpState.totalXp;
|
|
@@ -473,7 +526,7 @@ export function writeStatusState(
|
|
|
473
526
|
// XP state is optional during first install / version skew.
|
|
474
527
|
}
|
|
475
528
|
try {
|
|
476
|
-
const { getMood } = require("./mood.ts") as typeof
|
|
529
|
+
const { getMood } = require("./mood.ts") as typeof MoodModule;
|
|
477
530
|
moodStr = getMood().current;
|
|
478
531
|
} catch {
|
|
479
532
|
// Mood state is optional during first install / version skew.
|
|
@@ -491,11 +544,18 @@ export function writeStatusState(
|
|
|
491
544
|
muted: muted ?? false,
|
|
492
545
|
achievement: achievement ?? "",
|
|
493
546
|
frames,
|
|
547
|
+
compactFrames,
|
|
548
|
+
minimalFrames,
|
|
494
549
|
frameSequence,
|
|
495
550
|
level: xpLevel,
|
|
496
551
|
xp: xpTotal,
|
|
497
552
|
mood: moodStr,
|
|
498
553
|
};
|
|
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());
|
|
499
559
|
// writeStatusState no longer calls saveReaction implicitly. Callers
|
|
500
560
|
// that need a reaction file do so explicitly with the correct
|
|
501
561
|
// `source` tag — the old implicit save had no source argument and
|
|
@@ -580,11 +640,11 @@ const TRANSIENT_PREFIXES = [
|
|
|
580
640
|
*
|
|
581
641
|
* Companion data (menagerie.json, status.json, config.json) is intentionally
|
|
582
642
|
* kept — users reinstalling get their buddy back. Call-sites that want a full
|
|
583
|
-
* wipe should delete
|
|
643
|
+
* wipe should delete stateDir() themselves after calling this.
|
|
584
644
|
*/
|
|
585
645
|
export function cleanupPluginState(
|
|
586
646
|
settingsPath: string = CLAUDE_SETTINGS_PATH,
|
|
587
|
-
stateDir: string =
|
|
647
|
+
stateDir: string = buddyStateDir(),
|
|
588
648
|
): CleanupResult {
|
|
589
649
|
const statusLineRemoved = unsetBuddyStatusLine(settingsPath);
|
|
590
650
|
|