@ramarivera/coding-buddy 0.4.0-alpha.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/cli/pick.ts ADDED
@@ -0,0 +1,492 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * cli/pick.ts — interactive two-pane buddy picker
4
+ *
5
+ * Left pane │ Right pane
6
+ * ───────────────────────── │ ──────────────────────
7
+ * Saved: list of slots │ full companion card
8
+ * Criteria: search form │
9
+ * Results: matched buddies │ preview of highlighted
10
+ * Naming: name + save prompt │ card with live name
11
+ *
12
+ * Keys — Saved: ↑↓ navigate [enter] summon [r] random [s] search [q] quit
13
+ * Keys — Criteria: ↑↓ field ←→ value [enter] run [esc] back
14
+ * Keys — Results: ↑↓ navigate [enter] pick [esc] back [q] quit
15
+ * Keys — Naming: type name [enter] save [esc] cancel
16
+ */
17
+
18
+ import {
19
+ loadActiveSlot, saveActiveSlot, listCompanionSlots,
20
+ loadCompanionSlot, saveCompanionSlot, slugify, unusedName, writeStatusState,
21
+ } from "../server/state.ts";
22
+ import {
23
+ generateBones, SPECIES, RARITIES, STAT_NAMES, RARITY_STARS,
24
+ type Species, type Rarity, type StatName, type BuddyBones, type Companion,
25
+ } from "../server/engine.ts";
26
+ import { renderCompanionCard } from "../server/art.ts";
27
+ import { randomBytes } from "crypto";
28
+
29
+ // ─── ANSI ─────────────────────────────────────────────────────────────────────
30
+
31
+ const RARITY_CLR: Record<string, string> = {
32
+ common: "\x1b[38;2;153;153;153m",
33
+ uncommon: "\x1b[38;2;78;186;101m",
34
+ rare: "\x1b[38;2;177;185;249m",
35
+ epic: "\x1b[38;2;175;135;255m",
36
+ legendary: "\x1b[38;2;255;193;7m",
37
+ };
38
+ const B = "\x1b[1m";
39
+ const D = "\x1b[2m";
40
+ const RV = "\x1b[7m";
41
+ const N = "\x1b[0m";
42
+ const CY = "\x1b[36m";
43
+ const GR = "\x1b[90m";
44
+ const YL = "\x1b[33m";
45
+ const GN = "\x1b[32m";
46
+
47
+ function stripAnsi(s: string): string { return s.replace(/\x1b\[[^m]*m/g, ""); }
48
+
49
+ // Wide characters: emojis, some Unicode symbols take 2 display columns
50
+ function charWidth(cp: number): number {
51
+ // Emoji modifiers, variation selectors, ZWJ
52
+ if (cp >= 0xFE00 && cp <= 0xFE0F) return 0;
53
+ if (cp === 0x200D) return 0;
54
+ // Common wide ranges: CJK, emoji, fullwidth, braille, box-drawing stars etc.
55
+ if (cp >= 0x1F000) return 2; // Most emoji (sparkles ✨ = U+2728 is below this)
56
+ if (cp === 0x2728) return 2; // ✨ Sparkles
57
+ if (cp >= 0x2600 && cp <= 0x27BF) return 1; // Misc symbols (★ ☆ etc.) — typically 1 col
58
+ if (cp >= 0x2500 && cp <= 0x257F) return 1; // Box drawing
59
+ if (cp >= 0x2580 && cp <= 0x259F) return 1; // Block elements (█░)
60
+ if (cp >= 0x3000 && cp <= 0x9FFF) return 2; // CJK
61
+ if (cp >= 0xF900 && cp <= 0xFAFF) return 2; // CJK compat
62
+ if (cp >= 0xFF01 && cp <= 0xFF60) return 2; // Fullwidth
63
+ return 1;
64
+ }
65
+
66
+ function vlen(s: string): number {
67
+ const clean = stripAnsi(s);
68
+ let w = 0;
69
+ for (const ch of clean) {
70
+ w += charWidth(ch.codePointAt(0)!);
71
+ }
72
+ return w;
73
+ }
74
+
75
+ function rpad(s: string, w: number): string {
76
+ const v = vlen(s);
77
+ return v < w ? s + " ".repeat(w - v) : s;
78
+ }
79
+
80
+ // ─── Option lists ─────────────────────────────────────────────────────────────
81
+
82
+ const SP_OPTS = ["any", ...SPECIES] as const;
83
+ const RA_OPTS = ["any", ...RARITIES] as const;
84
+ const SH_OPTS = ["any", "yes", "no"] as const;
85
+ const ST_OPTS = ["any", ...STAT_NAMES] as const;
86
+
87
+ const CRITERIA_ROWS: Array<{ label: string; opts: readonly string[] }> = [
88
+ { label: "Species", opts: SP_OPTS },
89
+ { label: "Rarity ", opts: RA_OPTS },
90
+ { label: "Shiny ", opts: SH_OPTS },
91
+ { label: "Peak ", opts: ST_OPTS },
92
+ { label: "Dump ", opts: ST_OPTS },
93
+ ];
94
+
95
+ // ─── State ────────────────────────────────────────────────────────────────────
96
+
97
+ type Mode = "saved" | "criteria" | "results" | "naming";
98
+ interface SlotEntry { slot: string; companion: Companion; }
99
+ interface BuddyResult { userId: string; bones: BuddyBones; }
100
+
101
+ interface State {
102
+ mode: Mode;
103
+ savedSlots: SlotEntry[];
104
+ savedCursor: number;
105
+ activeSlot: string;
106
+ criteriaFocus: number;
107
+ ci: number[]; // [speciesIdx, rarityIdx, shinyIdx, peakIdx, dumpIdx]
108
+ results: BuddyResult[];
109
+ resultCursor: number;
110
+ searchStatus: string;
111
+ nameInput: string;
112
+ pendingResult: BuddyResult | null;
113
+ message: string;
114
+ }
115
+
116
+ function fresh(): State {
117
+ return {
118
+ mode: "saved",
119
+ savedSlots: listCompanionSlots(),
120
+ savedCursor: 0,
121
+ activeSlot: loadActiveSlot(),
122
+ criteriaFocus: 0,
123
+ // Default criteria: legendary, any species
124
+ ci: [0, RA_OPTS.indexOf("legendary"), 0, 0, 0],
125
+ results: [],
126
+ resultCursor: 0,
127
+ searchStatus: "",
128
+ nameInput: "",
129
+ pendingResult: null,
130
+ message: "",
131
+ };
132
+ }
133
+
134
+ // ─── Pane builders ────────────────────────────────────────────────────────────
135
+
136
+ const LEFT_W = 36;
137
+
138
+ function savedPane(s: State): string[] {
139
+ const lines: string[] = [];
140
+ lines.push(`${B} Your Menagerie${N} ${GR}[s] search${N}`);
141
+ lines.push(GR + " " + "─".repeat(LEFT_W - 2) + N);
142
+
143
+ if (s.savedSlots.length === 0) {
144
+ lines.push(` ${GR}your menagerie is empty${N}`);
145
+ lines.push(` ${GR}press [s] to search${N}`);
146
+ }
147
+
148
+ for (let i = 0; i < s.savedSlots.length; i++) {
149
+ const { slot, companion: c } = s.savedSlots[i];
150
+ const isActive = slot === s.activeSlot;
151
+ const isCursor = i === s.savedCursor;
152
+ const dot = isActive ? `${GN}●${N}` : " ";
153
+ const clr = RARITY_CLR[c.bones.rarity] ?? "";
154
+ const star = RARITY_STARS[c.bones.rarity];
155
+ const shiny = c.bones.shiny ? "✨" : " ";
156
+ const name = c.name.slice(0, 11).padEnd(11);
157
+ const sp = c.bones.species.slice(0, 7).padEnd(7);
158
+ const row = ` ${dot} ${clr}${name}${N} ${GR}${sp}${N} ${clr}${star}${N} ${shiny}`;
159
+ lines.push(isCursor ? RV + row + N : row);
160
+ }
161
+
162
+ lines.push(GR + " " + "─".repeat(LEFT_W - 2) + N);
163
+ return lines;
164
+ }
165
+
166
+ function criteriaPane(s: State): string[] {
167
+ const lines: string[] = [];
168
+ lines.push(`${B} Search Criteria${N}`);
169
+ lines.push(GR + " " + "─".repeat(LEFT_W - 2) + N);
170
+
171
+ for (let i = 0; i < CRITERIA_ROWS.length; i++) {
172
+ const { label, opts } = CRITERIA_ROWS[i];
173
+ const val = opts[s.ci[i]];
174
+ const focus = i === s.criteriaFocus;
175
+ const clr = RARITY_CLR[val] ?? "";
176
+ const arrow = focus ? `${YL}>${N}` : " ";
177
+ const valDisp = focus
178
+ ? `${RV}${B} ${val.padEnd(11)} ${N}`
179
+ : `${D}${clr} ${val.padEnd(11)} ${N}`;
180
+ lines.push(` ${arrow} ${GR}${label}${N} ${valDisp} ${GR}←→${N}`);
181
+ }
182
+
183
+ lines.push(GR + " " + "─".repeat(LEFT_W - 2) + N);
184
+ if (s.searchStatus) lines.push(` ${YL}${s.searchStatus}${N}`);
185
+ return lines;
186
+ }
187
+
188
+ function resultsPane(s: State): string[] {
189
+ const lines: string[] = [];
190
+ lines.push(`${B} Results${N} ${GR}${s.results.length} found${N}`);
191
+ lines.push(GR + " " + "─".repeat(LEFT_W - 2) + N);
192
+
193
+ if (s.results.length === 0) {
194
+ lines.push(` ${GR}no matches — try broader criteria${N}`);
195
+ }
196
+
197
+ // Scrolling window
198
+ const viewH = 12;
199
+ const offset = Math.max(0, s.resultCursor - Math.floor(viewH / 2));
200
+ for (let i = offset; i < Math.min(s.results.length, offset + viewH); i++) {
201
+ const b = s.results[i].bones;
202
+ const sel = i === s.resultCursor;
203
+ const clr = RARITY_CLR[b.rarity] ?? "";
204
+ const star = RARITY_STARS[b.rarity];
205
+ const shiny = b.shiny ? "✨" : " ";
206
+ const ra = b.rarity.slice(0, 3);
207
+ const sp = b.species.padEnd(8);
208
+ const eye = `e:${b.eye}`;
209
+ const hat = `h:${b.hat.slice(0, 6).padEnd(6)}`;
210
+ const row = ` ${clr}${ra}${N} ${sp} ${GR}${eye} ${hat}${N} ${shiny}`;
211
+ lines.push(sel ? RV + row + N : row);
212
+ }
213
+
214
+ lines.push(GR + " " + "─".repeat(LEFT_W - 2) + N);
215
+ return lines;
216
+ }
217
+
218
+ function namingPane(s: State): string[] {
219
+ const b = s.pendingResult?.bones;
220
+ const clr = b ? (RARITY_CLR[b.rarity] ?? "") : "";
221
+ const lines: string[] = [];
222
+ lines.push(`${B} Name this buddy${N}`);
223
+ if (b) lines.push(` ${clr}${b.rarity} ${b.species}${N}`);
224
+ lines.push(GR + " " + "─".repeat(LEFT_W - 2) + N);
225
+ lines.push(` ${B}Name:${N} ${s.nameInput}${YL}▌${N}`);
226
+ lines.push(` ${GR}(type a name, or enter for random)${N}`);
227
+ return lines;
228
+ }
229
+
230
+ function previewPane(s: State): string[] {
231
+ let c: Companion | null = null;
232
+
233
+ if (s.mode === "saved") {
234
+ c = s.savedSlots[s.savedCursor]?.companion ?? null;
235
+ } else if (s.mode === "results") {
236
+ const r = s.results[s.resultCursor];
237
+ if (r) c = {
238
+ bones: r.bones, name: "???",
239
+ personality: `A ${r.bones.rarity} ${r.bones.species}`,
240
+ hatchedAt: Date.now(), userId: r.userId,
241
+ };
242
+ } else if (s.mode === "naming" && s.pendingResult) {
243
+ const r = s.pendingResult;
244
+ c = {
245
+ bones: r.bones, name: s.nameInput || "???",
246
+ personality: `A ${r.bones.rarity} ${r.bones.species} who watches code with quiet intensity.`,
247
+ hatchedAt: Date.now(), userId: r.userId,
248
+ };
249
+ }
250
+
251
+ if (!c) return [` ${GR}no preview${N}`];
252
+ // Calculate available width for the right pane (total cols - left pane - separator)
253
+ const cols = Math.max(80, process.stdout.columns || 80);
254
+ const rightW = cols - LEFT_W - 3;
255
+ return renderCompanionCard(c.bones, c.name, c.personality, undefined, 0, rightW).split("\n");
256
+ }
257
+
258
+ // ─── Screen render ────────────────────────────────────────────────────────────
259
+
260
+ function drawScreen(s: State): void {
261
+ const cols = Math.max(80, process.stdout.columns || 80);
262
+ const rows = Math.max(20, process.stdout.rows || 24);
263
+
264
+ const leftLines = s.mode === "saved" ? savedPane(s)
265
+ : s.mode === "criteria" ? criteriaPane(s)
266
+ : s.mode === "results" ? resultsPane(s)
267
+ : namingPane(s);
268
+ const rightLines = previewPane(s);
269
+ const contentH = rows - 2;
270
+
271
+ let out = "\x1b[2J\x1b[H"; // clear + home
272
+
273
+ // Title bar
274
+ const title = ` claude-buddy pick `;
275
+ const fill = "─".repeat(Math.max(0, cols - title.length - 2));
276
+ out += `${CY}─${B}${title}${N}${CY}${fill}─${N}\n`;
277
+
278
+ // Content rows
279
+ for (let i = 0; i < contentH; i++) {
280
+ const l = rpad(leftLines[i] ?? "", LEFT_W);
281
+ const r = rightLines[i] ?? "";
282
+ out += l + GR + "│" + N + " " + r + "\n";
283
+ }
284
+
285
+ // Footer — mode-specific help
286
+ const helpText =
287
+ s.mode === "saved" ? "↑↓ navigate enter summon r random s search q quit" :
288
+ s.mode === "criteria" ? "↑↓ field ←→ value enter search esc back" :
289
+ s.mode === "results" ? "↑↓ navigate enter name+save esc back q quit" :
290
+ s.mode === "naming" ? "type name enter save esc cancel" : "";
291
+ out += `${GR}─${N} ${GR}${helpText}${N} ${GR}${"─".repeat(Math.max(0, cols - helpText.length - 4))}${N}`;
292
+
293
+ // Message overlay on last line
294
+ if (s.message) {
295
+ out += `\x1b[${rows};1H ${GN}${B}${s.message}${N}`;
296
+ }
297
+
298
+ process.stdout.write(out);
299
+ }
300
+
301
+ // ─── Search ───────────────────────────────────────────────────────────────────
302
+
303
+ function runSearch(s: State): void {
304
+ const wantSp = SP_OPTS[s.ci[0]] !== "any" ? SP_OPTS[s.ci[0]] as Species : null;
305
+ const wantRa = RA_OPTS[s.ci[1]] !== "any" ? RA_OPTS[s.ci[1]] as Rarity : null;
306
+ const wantShiny = SH_OPTS[s.ci[2]] === "yes" ? true
307
+ : SH_OPTS[s.ci[2]] === "no" ? false : null;
308
+ const wantPeak = ST_OPTS[s.ci[3]] !== "any" ? ST_OPTS[s.ci[3]] as StatName : null;
309
+ const wantDump = ST_OPTS[s.ci[4]] !== "any" ? ST_OPTS[s.ci[4]] as StatName : null;
310
+
311
+ // Scale attempt budget to rarity difficulty
312
+ const maxAttempts =
313
+ wantRa === "legendary" ? 200_000_000 :
314
+ wantRa === "epic" ? 50_000_000 :
315
+ wantRa === "rare" ? 20_000_000 : 10_000_000;
316
+
317
+ const results: BuddyResult[] = [];
318
+ const progressRow = (process.stdout.rows || 24) - 1;
319
+
320
+ for (let i = 0; i < maxAttempts && results.length < 20; i++) {
321
+ if (i > 0 && i % 1_000_000 === 0) {
322
+ s.searchStatus = `${(i / 1e6).toFixed(0)}M checked — ${results.length} found`;
323
+ process.stdout.write(
324
+ `\x1b[${progressRow};1H ${YL}${s.searchStatus}${N} \x1b[K`,
325
+ );
326
+ }
327
+
328
+ const userId = randomBytes(16).toString("hex");
329
+ const bones = generateBones(userId);
330
+
331
+ if (wantSp !== null && bones.species !== wantSp) continue;
332
+ if (wantRa !== null && bones.rarity !== wantRa) continue;
333
+ if (wantShiny !== null && bones.shiny !== wantShiny) continue;
334
+ if (wantPeak !== null && bones.peak !== wantPeak) continue;
335
+ if (wantDump !== null && bones.dump !== wantDump) continue;
336
+
337
+ results.push({ userId, bones });
338
+ }
339
+
340
+ s.searchStatus = `${results.length} found`;
341
+ s.results = results;
342
+ s.resultCursor = 0;
343
+ s.mode = "results";
344
+ }
345
+
346
+ // ─── Key handlers ─────────────────────────────────────────────────────────────
347
+
348
+ function clamp(v: number, lo: number, hi: number) { return Math.max(lo, Math.min(hi, v)); }
349
+
350
+ /** Returns true if the TUI should exit. */
351
+ function onKey(key: string, s: State): boolean {
352
+ if (key === "\x03") return true; // Ctrl+C always quits
353
+
354
+ switch (s.mode) {
355
+ case "naming": {
356
+ if (key === "\x1b") {
357
+ s.mode = "results"; s.nameInput = ""; s.pendingResult = null;
358
+ } else if (key === "\r" || key === "\n") {
359
+ // Empty input → auto-pick a random unused name
360
+ const name = s.nameInput.trim() || unusedName();
361
+ const slot = slugify(name);
362
+ if (loadCompanionSlot(slot)) {
363
+ s.message = `"${slot}" already taken — type a different name`;
364
+ s.nameInput = "";
365
+ break;
366
+ }
367
+ const r = s.pendingResult!;
368
+ const companion: Companion = {
369
+ bones: r.bones, name,
370
+ personality: `A ${r.bones.rarity} ${r.bones.species} who watches code with quiet intensity.`,
371
+ hatchedAt: Date.now(), userId: r.userId,
372
+ };
373
+ saveCompanionSlot(companion, slot);
374
+ saveActiveSlot(slot);
375
+ writeStatusState(companion, `*${name} arrives*`);
376
+ s.message = `✓ ${name} saved to slot "${slot}" and set as active!`;
377
+ return true;
378
+ } else if (key === "\u007f" || key === "\b") {
379
+ s.nameInput = s.nameInput.slice(0, -1);
380
+ } else if (key.length === 1 && key >= " " && s.nameInput.length < 14) {
381
+ s.nameInput += key;
382
+ }
383
+ break;
384
+ }
385
+
386
+ case "saved": {
387
+ if (key === "q") return true;
388
+ if (key === "s") { s.mode = "criteria"; break; }
389
+ if (key === "\x1b[A" || key === "k") s.savedCursor = clamp(s.savedCursor - 1, 0, s.savedSlots.length - 1);
390
+ else if (key === "\x1b[B" || key === "j") s.savedCursor = clamp(s.savedCursor + 1, 0, s.savedSlots.length - 1);
391
+ else if (key === "r") {
392
+ // Random pick from menagerie
393
+ if (s.savedSlots.length > 0) {
394
+ const entry = s.savedSlots[Math.floor(Math.random() * s.savedSlots.length)];
395
+ s.savedCursor = s.savedSlots.indexOf(entry);
396
+ saveActiveSlot(entry.slot);
397
+ writeStatusState(entry.companion, `*${entry.companion.name} arrives*`);
398
+ s.message = `✓ ${entry.companion.name} summoned at random!`;
399
+ return true;
400
+ }
401
+ } else if (key === "\r" || key === "\n") {
402
+ const entry = s.savedSlots[s.savedCursor];
403
+ if (entry) {
404
+ saveActiveSlot(entry.slot);
405
+ writeStatusState(entry.companion, `*${entry.companion.name} arrives*`);
406
+ s.message = `✓ ${entry.companion.name} summoned!`;
407
+ return true;
408
+ }
409
+ }
410
+ break;
411
+ }
412
+
413
+ case "criteria": {
414
+ if (key === "q") return true;
415
+ if (key === "\x1b") { s.mode = "saved"; break; }
416
+ if (key === "\x1b[A" || key === "k") s.criteriaFocus = clamp(s.criteriaFocus - 1, 0, CRITERIA_ROWS.length - 1);
417
+ else if (key === "\x1b[B" || key === "j") s.criteriaFocus = clamp(s.criteriaFocus + 1, 0, CRITERIA_ROWS.length - 1);
418
+ else if (key === "\x1b[C" || key === "l") {
419
+ const len = CRITERIA_ROWS[s.criteriaFocus].opts.length;
420
+ s.ci[s.criteriaFocus] = (s.ci[s.criteriaFocus] + 1) % len;
421
+ } else if (key === "\x1b[D" || key === "h") {
422
+ const len = CRITERIA_ROWS[s.criteriaFocus].opts.length;
423
+ s.ci[s.criteriaFocus] = (s.ci[s.criteriaFocus] - 1 + len) % len;
424
+ } else if (key === "\r" || key === "\n") {
425
+ s.searchStatus = "Starting search...";
426
+ drawScreen(s);
427
+ runSearch(s);
428
+ }
429
+ break;
430
+ }
431
+
432
+ case "results": {
433
+ if (key === "q") return true;
434
+ if (key === "\x1b") { s.mode = "criteria"; break; }
435
+ if (key === "\x1b[A" || key === "k") s.resultCursor = clamp(s.resultCursor - 1, 0, s.results.length - 1);
436
+ else if (key === "\x1b[B" || key === "j") s.resultCursor = clamp(s.resultCursor + 1, 0, s.results.length - 1);
437
+ else if (key === "\r" || key === "\n") {
438
+ const r = s.results[s.resultCursor];
439
+ if (r) {
440
+ s.pendingResult = r;
441
+ s.nameInput = ""; // empty — user types name or presses Enter for auto
442
+ s.mode = "naming";
443
+ }
444
+ }
445
+ break;
446
+ }
447
+ }
448
+ return false;
449
+ }
450
+
451
+ // ─── Entry point ──────────────────────────────────────────────────────────────
452
+
453
+ function cleanup(): void {
454
+ process.stdout.write("\x1b[?25h"); // show cursor
455
+ try { process.stdin.setRawMode(false); } catch {}
456
+ process.stdin.pause();
457
+ }
458
+
459
+ async function main(): Promise<void> {
460
+ if (!process.stdin.isTTY) {
461
+ console.error("buddy pick requires an interactive terminal (TTY)");
462
+ process.exit(1);
463
+ }
464
+
465
+ process.stdout.write("\x1b[?25l"); // hide cursor
466
+ process.stdin.setRawMode(true);
467
+ process.stdin.resume();
468
+ process.stdin.setEncoding("utf8");
469
+
470
+ process.on("exit", cleanup);
471
+ process.on("SIGINT", () => { cleanup(); process.exit(0); });
472
+
473
+ const s = fresh();
474
+ drawScreen(s);
475
+
476
+ await new Promise<void>((resolve) => {
477
+ process.stdin.on("data", (key: string) => {
478
+ const quit = onKey(key, s);
479
+ drawScreen(s);
480
+ if (quit) {
481
+ cleanup();
482
+ process.stdout.write("\x1b[2J\x1b[H");
483
+ if (s.message) console.log(`\n ${s.message}\n`);
484
+ resolve();
485
+ }
486
+ });
487
+ });
488
+
489
+ process.exit(0);
490
+ }
491
+
492
+ main();
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * cli/settings.ts — View and update buddy settings
4
+ *
5
+ * Usage:
6
+ * bun run settings Show current settings
7
+ * bun run settings cooldown 0 Set comment cooldown (0-300 seconds)
8
+ */
9
+
10
+ import { loadConfig, saveConfig } from "../server/state.ts";
11
+
12
+ const args = process.argv.slice(2);
13
+ const key = args[0];
14
+ const value = args[1];
15
+
16
+ if (!key) {
17
+ const cfg = loadConfig();
18
+ console.log(`
19
+ claude-buddy settings
20
+ ─────────────────────
21
+ Comment cooldown: ${cfg.commentCooldown}s (0 = no throttling, default 30)
22
+ Reaction TTL: ${cfg.reactionTTL}s (0 = permanent, default 0)
23
+
24
+ Change: bun run settings cooldown <seconds>
25
+ bun run settings ttl <seconds>
26
+ `);
27
+ process.exit(0);
28
+ }
29
+
30
+ if (key === "cooldown") {
31
+ if (value === undefined) {
32
+ const cfg = loadConfig();
33
+ console.log(`Comment cooldown: ${cfg.commentCooldown}s`);
34
+ process.exit(0);
35
+ }
36
+
37
+ const n = parseInt(value, 10);
38
+ if (isNaN(n) || n < 0 || n > 300) {
39
+ console.error("Error: cooldown must be 0-300 (seconds)");
40
+ process.exit(1);
41
+ }
42
+
43
+ const cfg = saveConfig({ commentCooldown: n });
44
+ console.log(`Updated: comment cooldown → ${cfg.commentCooldown}s`);
45
+ process.exit(0);
46
+ }
47
+
48
+ if (key === "ttl") {
49
+ if (value === undefined) {
50
+ const cfg = loadConfig();
51
+ console.log(`Reaction TTL: ${cfg.reactionTTL}s`);
52
+ process.exit(0);
53
+ }
54
+
55
+ const n = parseInt(value, 10);
56
+ if (isNaN(n) || n < 0 || n > 300) {
57
+ console.error("Error: ttl must be 0-300 (seconds, 0 = permanent)");
58
+ process.exit(1);
59
+ }
60
+
61
+ const cfg = saveConfig({ reactionTTL: n });
62
+ console.log(`Updated: reaction TTL → ${cfg.reactionTTL}s${n === 0 ? " (permanent)" : ""}`);
63
+ process.exit(0);
64
+ }
65
+
66
+ console.error(`Unknown setting: ${key}`);
67
+ console.error("Available: cooldown, ttl");
68
+ process.exit(1);
package/cli/show.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * claude-buddy show — display current companion in terminal
3
+ */
4
+
5
+ import { renderBuddy, renderFace, RARITY_STARS } from "../server/engine.ts";
6
+ import { loadCompanion, loadReaction } from "../server/state.ts";
7
+
8
+ const BOLD = "\x1b[1m";
9
+ const DIM = "\x1b[2m";
10
+ const NC = "\x1b[0m";
11
+
12
+ const companion = loadCompanion();
13
+
14
+ if (!companion) {
15
+ console.log("No companion found. Run 'claude-buddy install' first.");
16
+ process.exit(1);
17
+ }
18
+
19
+ console.log("");
20
+ console.log(renderBuddy(companion.bones));
21
+ console.log("");
22
+ console.log(` ${BOLD}${companion.name}${NC}`);
23
+ console.log(` ${DIM}${companion.personality}${NC}`);
24
+ console.log("");
25
+
26
+ const reaction = loadReaction();
27
+ if (reaction) {
28
+ const face = renderFace(companion.bones.species, companion.bones.eye);
29
+ console.log(` ${face} "${reaction.reaction}"`);
30
+ console.log("");
31
+ }
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env bash
2
+ # claude-buddy diagnostic test status line
3
+ # Outputs multiple padding strategies + multi-line check + width check
4
+
5
+ cat > /dev/null # drain stdin
6
+
7
+ # ANSI colors
8
+ NC=$'\033[0m'
9
+ DIM=$'\033[2m'
10
+ GOLD=$'\033[38;2;255;193;7m'
11
+ GREEN=$'\033[38;2;78;186;101m'
12
+ BLUE=$'\033[38;2;87;105;247m'
13
+
14
+ # Header
15
+ echo "${DIM}--- claude-buddy statusline test ---${NC}"
16
+
17
+ # Multi-line check
18
+ echo "LINE_1_top"
19
+ echo "LINE_2_middle"
20
+ echo "LINE_3_bottom"
21
+
22
+ # Padding strategies (each marker should align if strategy works)
23
+ PAD=30
24
+ echo "$(printf '%*s' "$PAD" '')|SPACE_${PAD}_END"
25
+ B=$'\xe2\xa0\x80'
26
+ braille=""
27
+ for ((i=0; i<PAD; i++)); do braille="${braille}${B}"; done
28
+ echo "${braille}|BRAILLE_${PAD}_END"
29
+ NBSP=$'\xc2\xa0'
30
+ nbsp=""
31
+ for ((i=0; i<PAD; i++)); do nbsp="${nbsp}${NBSP}"; done
32
+ echo "${nbsp}|NBSP_${PAD}_END"
33
+
34
+ # Mini buddy art (mushroom)
35
+ echo "${GOLD}-o-OO-o-${NC}"
36
+ echo "${GOLD}(________)${NC}"
37
+ echo "${GOLD} |° °|${NC}"
38
+ echo "${GOLD} |__|${NC}"
39
+ echo "${DIM}MUSHROOM${NC}"
40
+
41
+ exit 0