@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
@@ -1,31 +1,15 @@
1
1
  /**
2
- * claude-buddy uninstall — remove all integrations
2
+ * coding-buddy uninstall — remove all integrations
3
3
  */
4
4
 
5
5
  import { readFileSync, writeFileSync, existsSync, rmSync, readdirSync } from "fs";
6
6
  import { join } from "path";
7
- import { homedir } from "os";
8
- import { getBuddySkillDir, getClaudeConfigDir, getClaudeJsonPath, getClaudeSettingsPath } from "../storage/paths.ts";
9
-
10
- interface HookCommand {
11
- type: "command";
12
- command: string;
13
- }
14
-
15
- interface HookMatcherEntry {
16
- matcher?: string;
17
- hooks?: HookCommand[];
18
- }
19
-
20
- interface ClaudeSettings {
21
- statusLine?: { command?: string };
22
- hooks?: Record<string, HookMatcherEntry[]>;
23
- [key: string]: unknown;
24
- }
25
-
26
- function hasBuddyHook(entry: HookMatcherEntry): boolean {
27
- return (entry.hooks ?? []).some((hook) => hook.command.includes("claude-buddy"));
28
- }
7
+ import {
8
+ buddyStateDir,
9
+ claudeSettingsPath,
10
+ claudeSkillDir,
11
+ claudeUserConfigPath,
12
+ } from "../server/path.ts";
29
13
 
30
14
  const GREEN = "\x1b[32m";
31
15
  const YELLOW = "\x1b[33m";
@@ -34,13 +18,12 @@ const NC = "\x1b[0m";
34
18
  function ok(msg: string) { console.log(`${GREEN}✓${NC} ${msg}`); }
35
19
  function warn(msg: string) { console.log(`${YELLOW}⚠${NC} ${msg}`); }
36
20
 
37
- const CLAUDE_DIR = getClaudeConfigDir();
38
- const SETTINGS_FILE = getClaudeSettingsPath();
39
- const SKILL_DIR = getBuddySkillDir();
40
- const CLAUDE_JSON = getClaudeJsonPath();
41
- const STATE_DIR = join(homedir(), ".claude-buddy");
21
+ const SETTINGS_FILE = claudeSettingsPath();
22
+ const SKILL_DIR = claudeSkillDir("buddy");
23
+ const STATE_DIR = buddyStateDir();
24
+ const CLAUDE_JSON_PATH = claudeUserConfigPath();
42
25
 
43
- console.log("\nclaude-buddy uninstall\n");
26
+ console.log("\ncoding-buddy uninstall\n");
44
27
 
45
28
  // Stop all popup reopen loops and close any running popup
46
29
  try {
@@ -69,22 +52,22 @@ try {
69
52
  ok("Popup stopped");
70
53
  } catch { /* not in tmux or no popup */ }
71
54
 
72
- // Remove MCP server from ~/.claude.json
55
+ // Remove MCP server from Claude's user config
73
56
  try {
74
- const claudeJson = JSON.parse(readFileSync(CLAUDE_JSON, "utf8"));
57
+ const claudeJson = JSON.parse(readFileSync(CLAUDE_JSON_PATH, "utf8"));
75
58
  if (claudeJson.mcpServers?.["claude-buddy"]) {
76
59
  delete claudeJson.mcpServers["claude-buddy"];
77
60
  if (Object.keys(claudeJson.mcpServers).length === 0) delete claudeJson.mcpServers;
78
- writeFileSync(CLAUDE_JSON, JSON.stringify(claudeJson, null, 2));
79
- ok(`MCP server removed from ${CLAUDE_JSON}`);
61
+ writeFileSync(CLAUDE_JSON_PATH, JSON.stringify(claudeJson, null, 2));
62
+ ok(`MCP server removed from ${CLAUDE_JSON_PATH}`);
80
63
  }
81
64
  } catch {
82
- warn(`Could not update ${CLAUDE_JSON}`);
65
+ warn(`Could not update ${CLAUDE_JSON_PATH}`);
83
66
  }
84
67
 
85
68
  // Remove hooks and statusline from settings.json
86
69
  try {
87
- const settings = JSON.parse(readFileSync(SETTINGS_FILE, "utf8")) as ClaudeSettings;
70
+ const settings = JSON.parse(readFileSync(SETTINGS_FILE, "utf8"));
88
71
  let changed = false;
89
72
 
90
73
  if (settings.statusLine?.command?.includes("buddy")) {
@@ -93,11 +76,11 @@ try {
93
76
  changed = true;
94
77
  }
95
78
 
96
- for (const hookType of ["PostToolUse", "Stop", "SessionStart", "SessionEnd"] as const) {
79
+ for (const hookType of ["PostToolUse", "Stop", "SessionStart", "SessionEnd", "UserPromptSubmit"] as const) {
97
80
  if (settings.hooks?.[hookType]) {
98
81
  const before = settings.hooks[hookType].length;
99
82
  settings.hooks[hookType] = settings.hooks[hookType].filter(
100
- (h) => !hasBuddyHook(h),
83
+ (h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("coding-buddy") || hh.command?.includes("claude-buddy")),
101
84
  );
102
85
  if (settings.hooks[hookType].length < before) {
103
86
  ok(`${hookType} hooks removed`);
@@ -123,7 +106,9 @@ if (existsSync(SKILL_DIR)) {
123
106
  warn("Skill not found (already removed)");
124
107
  }
125
108
 
126
- // Keep state dir (companion data) — user might want it back
109
+ // Keep state dir (companion data) — user might want it back.
110
+ // This is profile-scoped when CLAUDE_CONFIG_DIR is set, so other
111
+ // profiles' buddies are unaffected.
127
112
  if (existsSync(STATE_DIR)) {
128
113
  warn(`Companion data kept at ${STATE_DIR} — delete manually if not needed`);
129
114
  }
package/cli/upgrade.ts ADDED
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { readFileSync } from "fs";
4
+ import { execSync } from "child_process";
5
+ import { join, resolve, dirname } from "path";
6
+ import { homedir } from "os";
7
+
8
+ const CYAN = "\x1b[36m";
9
+ const GREEN = "\x1b[32m";
10
+ const YELLOW = "\x1b[33m";
11
+ const RED = "\x1b[31m";
12
+ const BOLD = "\x1b[1m";
13
+ const DIM = "\x1b[2m";
14
+ const NC = "\x1b[0m";
15
+
16
+ const PROJECT_ROOT = resolve(dirname(import.meta.dir));
17
+
18
+ function ok(msg: string) { console.log(`${GREEN}✓${NC} ${msg}`); }
19
+ function info(msg: string) { console.log(`${CYAN}→${NC} ${msg}`); }
20
+ function warn(msg: string) { console.log(`${YELLOW}⚠${NC} ${msg}`); }
21
+ function err(msg: string) { console.log(`${RED}✗${NC} ${msg}`); }
22
+
23
+ function tryExec(cmd: string, fallback = ""): string {
24
+ try {
25
+ return execSync(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
26
+ } catch {
27
+ return fallback;
28
+ }
29
+ }
30
+
31
+ function getCurrentVersion(): string {
32
+ try {
33
+ const pkg = JSON.parse(readFileSync(join(PROJECT_ROOT, "package.json"), "utf8"));
34
+ return pkg.version ?? "unknown";
35
+ } catch {
36
+ return "unknown";
37
+ }
38
+ }
39
+
40
+ function banner() {
41
+ console.log(`
42
+ ${CYAN}╔══════════════════════════════════════════════════════════╗${NC}
43
+ ${CYAN}║${NC} ${BOLD}coding-buddy upgrade${NC} ${CYAN}║${NC}
44
+ ${CYAN}╚══════════════════════════════════════════════════════════╝${NC}
45
+ `);
46
+ }
47
+
48
+ function checkGitRepo(): boolean {
49
+ const isRepo = tryExec("git rev-parse --is-inside-work-tree 2>/dev/null");
50
+ if (isRepo !== "true") {
51
+ err("Not inside a git repository. Upgrade requires a git clone of coding-buddy.");
52
+ return false;
53
+ }
54
+ return true;
55
+ }
56
+
57
+ function getRemoteBranch(): string {
58
+ const branch = tryExec("git rev-parse --abbrev-ref HEAD 2>/dev/null", "main");
59
+ return branch === "HEAD" ? "main" : branch;
60
+ }
61
+
62
+ function checkForUpdates(branch: string): { hasUpdate: boolean; local: string; remote: string; commits: string[] } {
63
+ info("Fetching latest from remote...\n");
64
+ try {
65
+ execSync("git fetch --quiet 2>/dev/null", { cwd: PROJECT_ROOT, stdio: "ignore" });
66
+ } catch {
67
+ warn("git fetch failed — proceeding with cached remote state");
68
+ }
69
+
70
+ const local = tryExec("git rev-parse HEAD 2>/dev/null");
71
+ const upstream = tryExec(`git rev-parse '@{upstream}' 2>/dev/null`);
72
+ const remote = upstream || tryExec(`git rev-parse origin/${branch} 2>/dev/null`);
73
+
74
+ if (!local || !remote) {
75
+ warn("Could not determine remote HEAD — no tracking branch configured");
76
+ info(`To set one: git branch --set-upstream-to=origin/${branch} ${branch}`);
77
+ return { hasUpdate: false, local, remote, commits: [] };
78
+ }
79
+
80
+ if (local === remote) {
81
+ return { hasUpdate: false, local, remote, commits: [] };
82
+ }
83
+
84
+ const commits = tryExec(
85
+ `git log --oneline ${local}..origin/${branch} 2>/dev/null`,
86
+ ).split("\n").filter(Boolean);
87
+
88
+ return { hasUpdate: true, local, remote, commits };
89
+ }
90
+
91
+ function pullLatest(branch: string): boolean {
92
+ info(`Pulling latest from origin/${branch}...`);
93
+ try {
94
+ const output = execSync(`git pull --ff-only origin ${branch} 2>&1`, {
95
+ cwd: PROJECT_ROOT,
96
+ encoding: "utf8",
97
+ });
98
+ ok("Git pull successful");
99
+ return true;
100
+ } catch (e: any) {
101
+ err("git pull failed — you may have local changes that conflict");
102
+ err(e.message?.split("\n")[0] || "unknown error");
103
+ info("Stash or commit your local changes, then re-run upgrade");
104
+ return false;
105
+ }
106
+ }
107
+
108
+ function installDeps(): boolean {
109
+ info("Installing dependencies...");
110
+ try {
111
+ execSync("bun install 2>&1", { cwd: PROJECT_ROOT, stdio: "ignore" });
112
+ ok("Dependencies installed");
113
+ return true;
114
+ } catch {
115
+ err("bun install failed");
116
+ return false;
117
+ }
118
+ }
119
+
120
+ function reinstallBuddy(): boolean {
121
+ info("Re-running install-buddy to update integrations...\n");
122
+ try {
123
+ execSync("bun run install-buddy 2>&1", { cwd: PROJECT_ROOT, stdio: "inherit" });
124
+ return true;
125
+ } catch {
126
+ err("install-buddy failed");
127
+ return false;
128
+ }
129
+ }
130
+
131
+ function printSummary(oldVersion: string, commits: string[]) {
132
+ const newVersion = getCurrentVersion();
133
+
134
+ console.log("");
135
+ console.log(`${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}`);
136
+ console.log(`${GREEN} Upgrade complete!${NC}`);
137
+ console.log(`${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}`);
138
+ console.log("");
139
+ console.log(` ${BOLD}Version:${NC} ${oldVersion} → ${BOLD}${newVersion}${NC}`);
140
+
141
+ if (commits.length > 0) {
142
+ console.log(` ${BOLD}Changes:${NC}`);
143
+ const display = commits.slice(0, 15);
144
+ for (const c of display) {
145
+ console.log(` ${DIM}${c}${NC}`);
146
+ }
147
+ if (commits.length > 15) {
148
+ console.log(` ${DIM}... and ${commits.length - 15} more${NC}`);
149
+ }
150
+ }
151
+
152
+ console.log("");
153
+ console.log(`${DIM} Restart Claude Code for changes to take effect.${NC}`);
154
+ console.log("");
155
+ }
156
+
157
+ const args = process.argv.slice(2);
158
+ const checkOnly = args.includes("--check");
159
+
160
+ banner();
161
+
162
+ const oldVersion = getCurrentVersion();
163
+ info(`Current version: ${oldVersion}\n`);
164
+
165
+ if (!checkGitRepo()) {
166
+ process.exit(1);
167
+ }
168
+
169
+ const branch = getRemoteBranch();
170
+ const { hasUpdate, commits } = checkForUpdates(branch);
171
+
172
+ if (!hasUpdate) {
173
+ ok(`Already up to date (v${oldVersion})`);
174
+ console.log("");
175
+ process.exit(0);
176
+ }
177
+
178
+ if (checkOnly) {
179
+ warn(`Update available! ${commits.length} new commit${commits.length === 1 ? "" : "s"}:`);
180
+ for (const c of commits.slice(0, 10)) {
181
+ console.log(` ${DIM}${c}${NC}`);
182
+ }
183
+ if (commits.length > 10) {
184
+ console.log(` ${DIM}... and ${commits.length - 10} more${NC}`);
185
+ }
186
+ console.log("");
187
+ info("Run without --check to apply the update");
188
+ console.log("");
189
+ process.exit(0);
190
+ }
191
+
192
+ info(`${commits.length} new commit${commits.length === 1 ? "" : ""} available:`);
193
+ for (const c of commits.slice(0, 10)) {
194
+ console.log(` ${DIM}${c}${NC}`);
195
+ }
196
+ if (commits.length > 10) {
197
+ console.log(` ${DIM}... and ${commits.length - 10} more${NC}`);
198
+ }
199
+ console.log("");
200
+
201
+ if (!pullLatest(branch)) {
202
+ process.exit(1);
203
+ }
204
+
205
+ if (!installDeps()) {
206
+ process.exit(1);
207
+ }
208
+
209
+ if (!reinstallBuddy()) {
210
+ process.exit(1);
211
+ }
212
+
213
+ printSummary(oldVersion, commits);
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Validate species art format.
4
+ *
5
+ * Usage:
6
+ * bun run cli/validate-species.ts <path>
7
+ * bun run cli/validate-species.ts --check-species <name>
8
+ *
9
+ * Exits 0 if valid, 1 if invalid (with error messages to stderr).
10
+ */
11
+
12
+ import { readFileSync } from "fs";
13
+ import { join } from "path";
14
+
15
+ interface ValidationResult {
16
+ valid: boolean;
17
+ errors: string[];
18
+ warnings: string[];
19
+ }
20
+
21
+ const EYE_PLACEHOLDER = "{E}";
22
+
23
+ /** Check a string for ANSI escape sequences */
24
+ function hasAnsiCodes(s: string): boolean {
25
+ return s.includes("\x1b[");
26
+ }
27
+
28
+ /** Strip ANSI codes from a string */
29
+ function stripAnsi(s: string): string {
30
+ return s.replace(/\x1b\[[^m]*m/g, "");
31
+ }
32
+
33
+ /** Validate a single art frame (must be exactly 5 lines) */
34
+ function validateFrame(
35
+ species: string,
36
+ frameIndex: number,
37
+ lines: string[],
38
+ ): ValidationResult {
39
+ const errors: string[] = [];
40
+ const warnings: string[] = [];
41
+
42
+ if (lines.length !== 5) {
43
+ errors.push(
44
+ ` Species "${species}" frame ${frameIndex}: expected 5 lines, got ${lines.length}`,
45
+ );
46
+ }
47
+
48
+ for (let i = 0; i < lines.length; i++) {
49
+ const line = lines[i];
50
+ const rawLen = line.length;
51
+ const displayLen = stripAnsi(line).length;
52
+
53
+ if (hasAnsiCodes(line)) {
54
+ errors.push(
55
+ ` Species "${species}" frame ${frameIndex} line ${i + 1}: contains ANSI escape sequences (not allowed)`,
56
+ );
57
+ }
58
+
59
+ if (displayLen > 14) {
60
+ errors.push(
61
+ ` Species "${species}" frame ${frameIndex} line ${i + 1}: display width ${displayLen} exceeds max 14 chars`,
62
+ );
63
+ }
64
+
65
+ // Each species art should have the eye placeholder in at least one line
66
+ // (not strictly required but warn if missing)
67
+ if (!line.includes(EYE_PLACEHOLDER) && i === 1) {
68
+ warnings.push(
69
+ ` Species "${species}" frame ${frameIndex} line ${i + 1}: missing ${EYE_PLACEHOLDER} eye placeholder (recommended for line 2)`,
70
+ );
71
+ }
72
+ }
73
+
74
+ return { valid: errors.length === 0, errors, warnings };
75
+ }
76
+
77
+ /** Validate art structure from parsed JSON */
78
+ function validateSpeciesArt(
79
+ species: string,
80
+ art: string[][],
81
+ ): ValidationResult {
82
+ const errors: string[] = [];
83
+ const warnings: string[] = [];
84
+
85
+ if (!Array.isArray(art)) {
86
+ errors.push(`Species "${species}": art must be an array of frames`);
87
+ return { valid: false, errors, warnings };
88
+ }
89
+
90
+ if (art.length !== 3) {
91
+ errors.push(
92
+ `Species "${species}": expected 3 animation frames, got ${art.length}`,
93
+ );
94
+ }
95
+
96
+ for (let i = 0; i < art.length; i++) {
97
+ const frame = art[i];
98
+ const result = validateFrame(species, i, frame);
99
+ errors.push(...result.errors);
100
+ warnings.push(...result.warnings);
101
+ }
102
+
103
+ return { valid: errors.length === 0, errors, warnings };
104
+ }
105
+
106
+ /** Validate a face template — must contain at least one eye placeholder */
107
+ function validateFaceTemplate(
108
+ species: string,
109
+ face: string,
110
+ ): ValidationResult {
111
+ const errors: string[] = [];
112
+ const warnings: string[] = [];
113
+
114
+ if (face.length > 20) {
115
+ errors.push(
116
+ `Species "${species}" face template: display width ${face.length} exceeds max 20 chars`,
117
+ );
118
+ }
119
+
120
+ if (!face.includes(EYE_PLACEHOLDER)) {
121
+ errors.push(
122
+ `Species "${species}" face template: missing ${EYE_PLACEHOLDER} eye placeholder`,
123
+ );
124
+ }
125
+
126
+ return { valid: errors.length === 0, errors, warnings };
127
+ }
128
+
129
+ /**
130
+ * Main validation function — validates a species JSON file or inline art.
131
+ * Returns exit code (0 = valid, 1 = invalid).
132
+ */
133
+ export function validateSpeciesFile(filePath: string): ValidationResult {
134
+ const errors: string[] = [];
135
+ const warnings: string[] = [];
136
+
137
+ let data: Record<string, unknown>;
138
+ try {
139
+ const raw = readFileSync(filePath, "utf8");
140
+ data = JSON.parse(raw);
141
+ } catch (e) {
142
+ return {
143
+ valid: false,
144
+ errors: [`Failed to read or parse JSON file: ${filePath}`],
145
+ warnings: [],
146
+ };
147
+ }
148
+
149
+ const species = data.name as string | undefined;
150
+ if (!species || typeof species !== "string") {
151
+ errors.push("Species must have a 'name' field (string)");
152
+ }
153
+
154
+ const art = data.art as string[][] | undefined;
155
+ if (!art) {
156
+ errors.push("Species must have an 'art' field (string[][] — 3 frames × 5 lines)");
157
+ } else {
158
+ const artResult = validateSpeciesArt(species ?? "unknown", art);
159
+ errors.push(...artResult.errors);
160
+ warnings.push(...artResult.warnings);
161
+ }
162
+
163
+ const face = data.face as string | undefined;
164
+ if (face) {
165
+ const faceResult = validateFaceTemplate(species ?? "unknown", face);
166
+ errors.push(...faceResult.errors);
167
+ warnings.push(...faceResult.warnings);
168
+ }
169
+
170
+ return { valid: errors.length === 0, errors, warnings };
171
+ }
172
+
173
+ /** Print results and exit with appropriate code */
174
+ function main(): never {
175
+ const args = process.argv.slice(2);
176
+
177
+ if (args.length === 0) {
178
+ console.error("Usage: bun run cli/validate-species.ts <path-to-species-json>");
179
+ console.error(" Validates art format, ANSI codes, and eye placeholder.");
180
+ console.error("");
181
+ console.error("Example:");
182
+ console.error(" bun run cli/validate-species.ts ./my-species.json");
183
+ process.exit(1);
184
+ }
185
+
186
+ const filePath = args[0].startsWith("/") ? args[0] : join(process.cwd(), args[0]);
187
+ const result = validateSpeciesFile(filePath);
188
+
189
+ if (result.warnings.length > 0) {
190
+ console.warn("Warnings:");
191
+ for (const w of result.warnings) {
192
+ console.warn(w);
193
+ }
194
+ console.warn("");
195
+ }
196
+
197
+ if (result.valid) {
198
+ console.log(`✓ ${args[0]}: valid`);
199
+ process.exit(0);
200
+ } else {
201
+ console.error("Errors:");
202
+ for (const e of result.errors) {
203
+ console.error(e);
204
+ }
205
+ process.exit(1);
206
+ }
207
+ }
208
+
209
+ main();
package/cli/verify.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
- * claude-buddy verify — show what buddy a user ID produces
2
+ * coding-buddy verify — show what buddy a user ID produces
3
3
  */
4
4
 
5
- import { generateBones, renderBuddy, STAT_NAMES } from "../core/engine.ts";
6
- import { resolveUserId } from "../adapters/claude/storage/identity.ts";
5
+ import { generateBones, renderBuddy, STAT_NAMES } from "../core/engine.ts"
6
+ import { resolveUserId } from "../server/state.ts";
7
7
 
8
8
  const userId = process.argv[3] || resolveUserId();
9
9
 
@@ -1,24 +1,6 @@
1
- import type { UnlockedAchievement } from "./model.ts";
1
+ import type { UnlockedAchievement, GlobalCounters, SlotCounters, EventCounters } from "./model.ts";
2
+ export type { GlobalCounters, SlotCounters, EventCounters };
2
3
 
3
- export interface GlobalCounters {
4
- errors_seen: number;
5
- tests_failed: number;
6
- large_diffs: number;
7
- sessions: number;
8
- commands_run: number;
9
- days_active: number;
10
- turns: number;
11
- }
12
-
13
- export interface SlotCounters {
14
- pets: number;
15
- reactions_given: number;
16
- }
17
-
18
- export interface EventCounters extends GlobalCounters {
19
- pets: number;
20
- reactions_given: number;
21
- }
22
4
 
23
5
  export const GLOBAL_KEYS: (keyof GlobalCounters)[] = [
24
6
  "errors_seen", "tests_failed", "large_diffs",
@@ -189,12 +171,22 @@ export function getUnlockedAchievements(
189
171
  ): Achievement[] {
190
172
  const newlyUnlocked: Achievement[] = [];
191
173
  for (const ach of ACHIEVEMENTS) {
174
+ if (ach.id === "first_steps") continue;
192
175
  if (unlockedIds.has(ach.id)) continue;
193
176
  if (ach.check(events)) newlyUnlocked.push(ach);
194
177
  }
195
178
  return newlyUnlocked;
196
179
  }
197
180
 
181
+ export function getCreationAchievements(
182
+ _events: EventCounters,
183
+ unlockedIds: Set<string>,
184
+ ): Achievement[] {
185
+ if (unlockedIds.has("first_steps")) return [];
186
+ const first = ACHIEVEMENTS.find((ach) => ach.id === "first_steps");
187
+ return first ? [first] : [];
188
+ }
189
+
198
190
  export function isAchievementUnlocked(
199
191
  id: string,
200
192
  unlocked: UnlockedAchievement[],
package/core/art-data.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { Species, Hat } from "./engine.ts";
2
2
 
3
+ // ─── Species art: 3 frames × 5 lines each ──────────────────────────────────
4
+
3
5
  export const SPECIES_ART: Record<Species, string[][]> = {
4
6
  duck: [
5
7
  [" ", " __ ", " <({E} )___ ", " ( ._> ", " `--' "],
@@ -91,8 +93,32 @@ export const SPECIES_ART: Record<Species, string[][]> = {
91
93
  [" ", " /\\ /| ", " ( {E} {E} ) ", " ( .. ) ", " `------' "],
92
94
  [" ", " /\\ /\\ ", " ( {E} {E} ) ", " ( .. ) ", " `------'~ "],
93
95
  ],
96
+ wyvern: [
97
+ ["} {",
98
+ "|\\^```^/|",
99
+ "\\ {E}' '{E} /",
100
+ " ≈(° °)≈",
101
+ " '-'"],
102
+ ["} {",
103
+ "|\\^```^/|",
104
+ "\\ {E}' '{E} /",
105
+ " ≈(° °)≈",
106
+ " '~'"],
107
+ ["} {",
108
+ "|\\^```^/|",
109
+ "\\ {E}' '{E} /",
110
+ " ≈(° °)≈",
111
+ " 'v'"],
112
+ ],
113
+ pikachu: [
114
+ [" ", " /\\_/\\ ", " ({E} {E}) ", " ( ω ) ", " (__) "],
115
+ [" ", " /\\_/\\ ", " (- -) ", " ( ω ) ", " (__) "],
116
+ [" ", " /\\_/\\ ", " ({E} {E}) ", " ( ~ ) ", " (__) "],
117
+ ],
94
118
  };
95
119
 
120
+ // ─── Hat art ────────────────────────────────────────────────────────────────
121
+
96
122
  export const HAT_ART: Record<Hat, string> = {
97
123
  none: "",
98
124
  crown: " \\^^^/ ",