@ramarivera/coding-buddy 0.4.0-alpha.8 → 0.6.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.
Files changed (92) hide show
  1. package/{adapters/claude/plugin → .claude-plugin}/marketplace.json +11 -6
  2. package/{adapters/claude/plugin → .claude-plugin}/plugin.json +9 -7
  3. package/LICENSE +1 -1
  4. package/README.md +102 -72
  5. package/adapters/omp/commands.ts +201 -0
  6. package/adapters/omp/context.ts +22 -0
  7. package/adapters/omp/events.ts +551 -0
  8. package/adapters/omp/identity.ts +10 -0
  9. package/adapters/omp/index.ts +39 -0
  10. package/adapters/omp/logger.ts +52 -0
  11. package/adapters/omp/prompt.ts +60 -0
  12. package/adapters/omp/renderers.ts +73 -0
  13. package/adapters/omp/storage.ts +25 -0
  14. package/adapters/omp/ui.ts +85 -0
  15. package/adapters/pi/commands.ts +64 -37
  16. package/adapters/pi/events.ts +147 -72
  17. package/adapters/pi/index.ts +9 -4
  18. package/adapters/pi/storage.ts +14 -285
  19. package/adapters/pi/ui.ts +56 -5
  20. package/adapters/shared/file-storage.ts +526 -0
  21. package/{adapters/claude/install → cli}/backup.ts +59 -44
  22. package/cli/biomes.ts +309 -0
  23. package/cli/buddy-shell.ts +818 -0
  24. package/{adapters/claude/install → cli}/disable.ts +14 -32
  25. package/{adapters/claude/install → cli}/doctor.ts +55 -57
  26. package/{adapters/claude/install → cli}/hunt.ts +7 -9
  27. package/cli/index.ts +36 -25
  28. package/{adapters/claude/install → cli}/install.ts +100 -145
  29. package/{adapters/claude/install → cli}/pick.ts +111 -50
  30. package/{adapters/claude/install → cli}/settings.ts +2 -2
  31. package/{adapters/claude/install → cli}/show.ts +4 -4
  32. package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
  33. package/{adapters/claude/install → cli}/test-statusline.ts +11 -11
  34. package/cli/tui.tsx +2333 -0
  35. package/{adapters/claude/install → cli}/uninstall.ts +23 -38
  36. package/cli/upgrade.ts +213 -0
  37. package/cli/validate-species.ts +209 -0
  38. package/cli/verify.ts +3 -3
  39. package/core/achievements.ts +12 -20
  40. package/core/art-data.ts +26 -0
  41. package/core/command-service.ts +99 -97
  42. package/core/engine.ts +83 -1
  43. package/core/ports.ts +10 -0
  44. package/core/reactions.ts +533 -95
  45. package/core/slot-slug.ts +22 -0
  46. package/{adapters/claude/hooks → hooks}/buddy-comment.sh +23 -2
  47. package/hooks/file-type-react.sh +401 -0
  48. package/hooks/hooks.json +52 -0
  49. package/hooks/mood-react.sh +208 -0
  50. package/{adapters/claude/hooks → hooks}/name-react.sh +5 -2
  51. package/hooks/react.sh +1368 -0
  52. package/hooks/suggest.sh +55 -0
  53. package/package.json +113 -59
  54. package/scripts/gen-emoji-widths.ts +34 -0
  55. package/scripts/paths.sh +56 -0
  56. package/scripts/postinstall.cjs +42 -0
  57. package/scripts/snapshot-statusline.sh +54 -0
  58. package/server/achievements.ts +1419 -0
  59. package/server/art.ts +450 -0
  60. package/server/award-xp.ts +48 -0
  61. package/server/check-suggestions.ts +102 -0
  62. package/server/consolidate.ts +33 -0
  63. package/server/index.ts +1340 -0
  64. package/server/mcp-launcher.sh +26 -0
  65. package/server/memory.ts +404 -0
  66. package/server/mood.ts +254 -0
  67. package/server/path.ts +82 -0
  68. package/server/shift-mood.ts +43 -0
  69. package/{adapters/claude/storage → server}/state.ts +200 -7
  70. package/server/suggestions.ts +335 -0
  71. package/server/theme.ts +53 -0
  72. package/server/xp.ts +435 -0
  73. package/{adapters/claude/skills → skills}/buddy/SKILL.md +48 -4
  74. package/statusline/buddy-status.sh +397 -0
  75. package/statusline/combined-status.sh +134 -0
  76. package/statusline/emoji-widths.data +4 -0
  77. package/adapters/claude/hooks/hooks.json +0 -35
  78. package/adapters/claude/hooks/react.sh +0 -204
  79. package/adapters/claude/popup/buddy-popup.sh +0 -92
  80. package/adapters/claude/popup/buddy-render.sh +0 -540
  81. package/adapters/claude/popup/popup-manager.sh +0 -355
  82. package/adapters/claude/rendering/art.ts +0 -264
  83. package/adapters/claude/server/index.ts +0 -752
  84. package/adapters/claude/server/instructions.ts +0 -24
  85. package/adapters/claude/server/resources.ts +0 -38
  86. package/adapters/claude/statusline/buddy-status.sh +0 -389
  87. package/adapters/claude/storage/achievements.ts +0 -253
  88. package/adapters/claude/storage/identity.ts +0 -13
  89. package/adapters/claude/storage/paths.ts +0 -24
  90. package/adapters/claude/storage/settings.ts +0 -41
  91. package/adapters/pi/README.md +0 -83
  92. package/adapters/pi/tools.ts +0 -6
package/server/path.ts ADDED
@@ -0,0 +1,82 @@
1
+ // Path utilities and helpers
2
+ //
3
+ // Two related concerns live here:
4
+ // 1. Path normalization (Windows compat) — toUnixPath().
5
+ // 2. Resolution of Claude Code config / state paths — claudeConfigDir,
6
+ // claudeSettingsPath, claudeSkillDir, claudeUserConfigPath, buddyStateDir.
7
+ // These honor CLAUDE_CONFIG_DIR so coding-buddy works with Claude Code's
8
+ // multi-account layout (one CLAUDE_CONFIG_DIR per profile).
9
+ //
10
+ // The shell counterpart of (2) lives in scripts/paths.sh and MUST stay in sync.
11
+
12
+ import { join } from "path";
13
+ import { homedir } from "os";
14
+
15
+ // ─── (1) Path normalization ─────────────────────────────────────────────────
16
+
17
+ // Node's path.join() produces backslash paths on Windows, which bash treats as
18
+ // escape sequences, stripping them entirely (e.g. C:\Users -> C:Users).
19
+ // Use forward slashes in all paths written to config files.
20
+
21
+ /**
22
+ * Converts all backslashes in a file path to forward slashes, producing a Unix-style path.
23
+ *
24
+ * @param p - The file path to convert.
25
+ * @returns The converted path with forward slashes.
26
+ */
27
+ export function toUnixPath(p: string): string {
28
+ return p.replace(/\\/g, "/");
29
+ }
30
+
31
+ // ─── (2) Claude config / state path resolvers ───────────────────────────────
32
+ //
33
+ // Resolution rules:
34
+ // - If CLAUDE_CONFIG_DIR is set: everything lives under
35
+ // $CLAUDE_CONFIG_DIR/. .claude.json is preferred inside the config dir
36
+ // when present, falling back to $HOME/.claude.json for setups that
37
+ // keep it at $HOME. Buddy state goes to $CLAUDE_CONFIG_DIR/buddy-state.
38
+ // - If CLAUDE_CONFIG_DIR is NOT set: the single-profile defaults —
39
+ // ~/.claude/, ~/.claude.json, ~/.claude-buddy/.
40
+
41
+ function envDir(name: string): string | undefined {
42
+ const v = process.env[name];
43
+ return v && v.length > 0 ? v : undefined;
44
+ }
45
+
46
+ export function claudeConfigDir(): string {
47
+ return envDir("CLAUDE_CONFIG_DIR") ?? join(homedir(), ".claude");
48
+ }
49
+
50
+ export function claudeSettingsPath(): string {
51
+ return join(claudeConfigDir(), "settings.json");
52
+ }
53
+
54
+ export function claudeSkillDir(name: string): string {
55
+ return join(claudeConfigDir(), "skills", name);
56
+ }
57
+
58
+ /**
59
+ * Resolve the path to Claude Code's user-config file (.claude.json).
60
+ *
61
+ * When CLAUDE_CONFIG_DIR is set the file lives inside it — we always
62
+ * resolve there so every read and write stays profile-scoped. Falling
63
+ * back to $HOME/.claude.json when the in-dir file is missing would
64
+ * break that isolation: installs in one profile could mutate the
65
+ * home-level file that a different profile reads. If the in-dir file
66
+ * doesn't exist yet, callers surface a clear "Start Claude Code once
67
+ * first" preflight error instead of silently writing to $HOME.
68
+ *
69
+ * When CLAUDE_CONFIG_DIR is unset we use $HOME/.claude.json — the
70
+ * single-profile default.
71
+ */
72
+ export function claudeUserConfigPath(): string {
73
+ const cfgDir = envDir("CLAUDE_CONFIG_DIR");
74
+ if (cfgDir) return join(cfgDir, ".claude.json");
75
+ return join(homedir(), ".claude.json");
76
+ }
77
+
78
+ export function buddyStateDir(): string {
79
+ const cfgDir = envDir("CLAUDE_CONFIG_DIR");
80
+ if (cfgDir) return join(cfgDir, "buddy-state");
81
+ return join(homedir(), ".claude-buddy");
82
+ }
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Lightweight mood shifting script — called from shell hooks.
4
+ * Shifts mood based on coding events.
5
+ *
6
+ * Usage:
7
+ * bun run server/shift-mood.ts <trigger> [slot]
8
+ *
9
+ * Triggers: tests_pass | tests_fail | error | large_diff | achievement | long_session | idle | rest
10
+ */
11
+
12
+ import { shiftMood, MOOD_NAMES, MOOD_COLORS } from "./mood";
13
+ import type { MoodTrigger } from "./mood";
14
+
15
+ const VALID_TRIGGERS = new Set([
16
+ "tests_pass",
17
+ "tests_fail",
18
+ "error",
19
+ "large_diff",
20
+ "achievement",
21
+ "long_session",
22
+ "idle",
23
+ "rest",
24
+ ]);
25
+
26
+ function main(): void {
27
+ const trigger = process.argv[2] as string;
28
+
29
+ if (!trigger || !VALID_TRIGGERS.has(trigger)) {
30
+ console.error(
31
+ `Usage: bun run server/shift-mood.ts <trigger>\nValid triggers: ${[...VALID_TRIGGERS].join(" | ")}`,
32
+ );
33
+ process.exit(1);
34
+ }
35
+
36
+ const state = shiftMood(trigger as MoodTrigger);
37
+ const mood = state.current;
38
+ const color = MOOD_COLORS[mood] ?? "💫";
39
+ const name = MOOD_NAMES[mood] ?? mood;
40
+ console.log(`Mood: ${color} ${name} (intensity ${state.intensity}/3)`);
41
+ }
42
+
43
+ main();
@@ -1,8 +1,12 @@
1
1
  /**
2
- * State management — reads/writes companion data to ~/.claude-buddy/
2
+ * State management — reads/writes companion data to the buddy state dir.
3
+ *
4
+ * The state dir resolves via server/paths.ts (honors CLAUDE_CONFIG_DIR).
5
+ * Default: ~/.claude-buddy/. With CLAUDE_CONFIG_DIR set:
6
+ * $CLAUDE_CONFIG_DIR/buddy-state/.
3
7
  *
4
8
  * Storage layout (v3 — single manifest):
5
- * ~/.claude-buddy/
9
+ * <state-dir>/
6
10
  * menagerie.json <- SSOT: { active, companions: { [slot]: Companion } }
7
11
  * reaction.$SID.json <- transient reaction state (session-scoped)
8
12
  * status.json <- compact state for the status-line shell script
@@ -23,12 +27,18 @@ import {
23
27
  existsSync,
24
28
  readdirSync,
25
29
  renameSync,
30
+ rmSync,
26
31
  } from "fs";
27
32
  import { join } from "path";
28
- import { homedir } from "os";
29
- import type { Companion } from "../../../core/engine.ts";
33
+ import type { Companion } from "../core/engine.ts"
34
+ import {
35
+ buddyStateDir,
36
+ claudeSettingsPath,
37
+ claudeUserConfigPath,
38
+ toUnixPath,
39
+ } from "./path.ts";
30
40
 
31
- const STATE_DIR = join(homedir(), ".claude-buddy");
41
+ export const STATE_DIR = buddyStateDir();
32
42
  const MANIFEST_FILE = join(STATE_DIR, "menagerie.json");
33
43
  const CONFIG_FILE = join(STATE_DIR, "config.json");
34
44
 
@@ -95,7 +105,7 @@ export function slugify(name: string): string {
95
105
  */
96
106
  export function unusedName(): string {
97
107
  const { generateFallbackName } =
98
- require("../../../core/reactions.ts") as typeof import("../../../core/reactions.ts");
108
+ require("../core/reactions.ts") as typeof import("../core/reactions.ts");
99
109
  const taken = new Set(Object.keys(loadManifest().companions));
100
110
  for (let i = 0; i < 50; i++) {
101
111
  const n = generateFallbackName();
@@ -145,6 +155,19 @@ export function saveCompanionSlot(companion: Companion, slot: string): void {
145
155
  saveManifest(m);
146
156
  }
147
157
 
158
+ /**
159
+ * UPDATE an existing (possibly non-active) companion slot.
160
+ * Throws if the slot does not exist.
161
+ */
162
+ export function updateCompanionSlot(slot: string, companion: Companion): void {
163
+ const m = loadManifest();
164
+ if (!m.companions[slot]) {
165
+ throw new Error(`Slot "${slot}" does not exist.`);
166
+ }
167
+ m.companions[slot] = companion;
168
+ saveManifest(m);
169
+ }
170
+
148
171
  export function deleteCompanionSlot(slot: string): void {
149
172
  const m = loadManifest();
150
173
  delete m.companions[slot];
@@ -267,6 +290,17 @@ export function saveReaction(reaction: string, reason: string): void {
267
290
  writeFileSync(reactionFile(), JSON.stringify(state));
268
291
  }
269
292
 
293
+ // ─── Identity resolution ─────────────────────────────────────────────────────
294
+
295
+ export function resolveUserId(): string {
296
+ try {
297
+ const claudeJson = JSON.parse(readFileSync(claudeUserConfigPath(), "utf8"));
298
+ return claudeJson.oauthAccount?.accountUuid ?? claudeJson.userID ?? "anon";
299
+ } catch {
300
+ return "anon";
301
+ }
302
+ }
303
+
270
304
  // ─── Config persistence (PR #6: tmux popup settings) ─────────────────────────
271
305
 
272
306
  export interface BuddyConfig {
@@ -276,6 +310,15 @@ export interface BuddyConfig {
276
310
  bubblePosition: "top" | "left";
277
311
  showRarity: boolean;
278
312
  statusLineEnabled: boolean;
313
+ bubbleWidth: number;
314
+ bubbleMargin: number;
315
+ useCombinedStatus: boolean;
316
+ rainbowColors?: string[];
317
+ theme: "dark" | "light" | "auto";
318
+ moodEnabled: boolean;
319
+ memoryEnabled: boolean;
320
+ suggestionsEnabled: boolean;
321
+ suggestionCooldown: number;
279
322
  }
280
323
 
281
324
  const DEFAULT_CONFIG: BuddyConfig = {
@@ -285,6 +328,14 @@ const DEFAULT_CONFIG: BuddyConfig = {
285
328
  bubblePosition: "top",
286
329
  showRarity: true,
287
330
  statusLineEnabled: false,
331
+ bubbleWidth: 28,
332
+ bubbleMargin: 8,
333
+ useCombinedStatus: false,
334
+ theme: "auto",
335
+ moodEnabled: true,
336
+ memoryEnabled: true,
337
+ suggestionsEnabled: true,
338
+ suggestionCooldown: 180,
288
339
  };
289
340
 
290
341
  export function loadConfig(): BuddyConfig {
@@ -318,6 +369,11 @@ export interface StatusState {
318
369
  reaction: string;
319
370
  muted: boolean;
320
371
  achievement: string;
372
+ frames: string[];
373
+ frameSequence: number[];
374
+ level: number;
375
+ xp: number;
376
+ mood: string;
321
377
  }
322
378
 
323
379
  export function writeStatusState(
@@ -325,10 +381,32 @@ export function writeStatusState(
325
381
  reaction?: string,
326
382
  muted?: boolean,
327
383
  achievement?: string,
384
+ level?: number,
385
+ xp?: number,
328
386
  ): void {
329
387
  mkdirSync(STATE_DIR, { recursive: true });
330
388
  const { renderFace, RARITY_STARS } =
331
- require("../../../core/engine.ts") as typeof import("../../../core/engine.ts");
389
+ require("../core/engine.ts") as typeof import("../core/engine.ts");
390
+ const { getStatusFrames } =
391
+ require("./art.ts") as typeof import("./art.ts");
392
+ const { frames, frameSequence } = getStatusFrames(companion.bones);
393
+ let xpLevel = level ?? 1;
394
+ let xpTotal = xp ?? 0;
395
+ let moodStr = "focused";
396
+ try {
397
+ const { getXpState } = require("./xp.ts") as typeof import("./xp.ts");
398
+ const xpState = getXpState();
399
+ xpLevel = xpState.level;
400
+ xpTotal = xpState.totalXp;
401
+ } catch {
402
+ // XP state is optional during first install / version skew.
403
+ }
404
+ try {
405
+ const { getMood } = require("./mood.ts") as typeof import("./mood.ts");
406
+ moodStr = getMood().current;
407
+ } catch {
408
+ // Mood state is optional during first install / version skew.
409
+ }
332
410
  const state: StatusState = {
333
411
  name: companion.name,
334
412
  species: companion.bones.species,
@@ -341,7 +419,122 @@ export function writeStatusState(
341
419
  reaction: reaction ?? "",
342
420
  muted: muted ?? false,
343
421
  achievement: achievement ?? "",
422
+ frames,
423
+ frameSequence,
424
+ level: xpLevel,
425
+ xp: xpTotal,
426
+ mood: moodStr,
344
427
  };
345
428
  writeFileSync(join(STATE_DIR, "status.json"), JSON.stringify(state));
346
429
  }
347
430
 
431
+ // ─── Claude Code settings.json patching (for buddy_statusline tool) ──────────
432
+
433
+ export const CLAUDE_SETTINGS_PATH = claudeSettingsPath();
434
+
435
+ /**
436
+ * Write settings.statusLine pointing to the given buddy-status script.
437
+ * Atomic via tmp + rename. Returns false if settings.json is unreachable.
438
+ */
439
+ export function setBuddyStatusLine(
440
+ statusScript: string,
441
+ settingsPath: string = CLAUDE_SETTINGS_PATH,
442
+ ): boolean {
443
+ try {
444
+ const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
445
+ settings.statusLine = {
446
+ type: "command",
447
+ command: toUnixPath(statusScript),
448
+ padding: 1,
449
+ refreshInterval: 1,
450
+ };
451
+ const tmp = settingsPath + ".tmp";
452
+ writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
453
+ renameSync(tmp, settingsPath);
454
+ return true;
455
+ } catch {
456
+ return false;
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Remove settings.statusLine — but only if it points to buddy-status.sh.
462
+ * Leaves foreign statusLines untouched. Returns false if no buddy line was
463
+ * present or settings.json is unreachable.
464
+ */
465
+ export function unsetBuddyStatusLine(
466
+ settingsPath: string = CLAUDE_SETTINGS_PATH,
467
+ ): boolean {
468
+ try {
469
+ const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
470
+ if (!settings.statusLine?.command?.includes("buddy-status.sh")) return false;
471
+ delete settings.statusLine;
472
+ const tmp = settingsPath + ".tmp";
473
+ writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
474
+ renameSync(tmp, settingsPath);
475
+ return true;
476
+ } catch {
477
+ return false;
478
+ }
479
+ }
480
+
481
+ // ─── Plugin uninstall cleanup ───────────────────────────────────────────────
482
+
483
+ export interface CleanupResult {
484
+ statusLineRemoved: boolean;
485
+ foreignStatusLineKept: boolean;
486
+ transientFilesRemoved: number;
487
+ }
488
+
489
+ const TRANSIENT_PREFIXES = [
490
+ "popup-stop.",
491
+ "popup-resize.",
492
+ "popup-env.",
493
+ "popup-scroll.",
494
+ "popup-reopen-pid.",
495
+ "reaction.",
496
+ ".last_reaction.",
497
+ ".last_comment.",
498
+ ];
499
+
500
+ /**
501
+ * Clean up plugin-owned writes to the user's global state so that
502
+ * `claude plugin uninstall` leaves no orphaned entries behind. Specifically:
503
+ * - remove settings.statusLine if it points to buddy-status.sh
504
+ * - delete session-scoped transient files under ~/.claude-buddy/
505
+ *
506
+ * Companion data (menagerie.json, status.json, config.json) is intentionally
507
+ * kept — users reinstalling get their buddy back. Call-sites that want a full
508
+ * wipe should delete STATE_DIR themselves after calling this.
509
+ */
510
+ export function cleanupPluginState(
511
+ settingsPath: string = CLAUDE_SETTINGS_PATH,
512
+ stateDir: string = STATE_DIR,
513
+ ): CleanupResult {
514
+ const statusLineRemoved = unsetBuddyStatusLine(settingsPath);
515
+
516
+ let foreignStatusLineKept = false;
517
+ try {
518
+ const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
519
+ const cmd = settings.statusLine?.command;
520
+ if (cmd && !cmd.includes("buddy-status.sh")) foreignStatusLineKept = true;
521
+ } catch {
522
+ /* missing settings.json is fine */
523
+ }
524
+
525
+ let transientFilesRemoved = 0;
526
+ try {
527
+ if (existsSync(stateDir)) {
528
+ for (const f of readdirSync(stateDir)) {
529
+ if (TRANSIENT_PREFIXES.some(p => f.startsWith(p))) {
530
+ rmSync(join(stateDir, f), { force: true });
531
+ transientFilesRemoved++;
532
+ }
533
+ }
534
+ }
535
+ } catch {
536
+ /* state dir unreadable is fine */
537
+ }
538
+
539
+ return { statusLineRemoved, foreignStatusLineKept, transientFilesRemoved };
540
+ }