@ramarivera/coding-buddy 0.4.0-alpha.2 → 0.4.0-alpha.4
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 +4 -5
- package/{hooks → adapters/claude/hooks}/hooks.json +3 -3
- package/{cli → adapters/claude/install}/backup.ts +10 -9
- package/{cli → adapters/claude/install}/disable.ts +27 -6
- package/{cli → adapters/claude/install}/doctor.ts +44 -31
- package/{cli → adapters/claude/install}/hunt.ts +4 -4
- package/{cli → adapters/claude/install}/install.ts +81 -42
- package/{cli → adapters/claude/install}/pick.ts +3 -3
- package/{cli → adapters/claude/install}/settings.ts +1 -1
- package/{cli → adapters/claude/install}/show.ts +2 -2
- package/{cli → adapters/claude/install}/test-statusline.ts +3 -2
- package/{cli → adapters/claude/install}/uninstall.ts +31 -10
- package/{.claude-plugin → adapters/claude/plugin}/plugin.json +1 -1
- package/adapters/claude/popup/buddy-popup.sh +92 -0
- package/adapters/claude/popup/buddy-render.sh +540 -0
- package/adapters/claude/popup/popup-manager.sh +355 -0
- package/{server → adapters/claude/rendering}/art.ts +3 -115
- package/{server → adapters/claude/server}/index.ts +49 -71
- package/adapters/claude/server/instructions.ts +24 -0
- package/adapters/claude/server/resources.ts +38 -0
- package/adapters/claude/storage/achievements.ts +253 -0
- package/adapters/claude/storage/identity.ts +13 -0
- package/adapters/claude/storage/paths.ts +24 -0
- package/adapters/claude/storage/settings.ts +41 -0
- package/{server → adapters/claude/storage}/state.ts +3 -65
- package/adapters/pi/README.md +64 -0
- package/adapters/pi/commands.ts +173 -0
- package/adapters/pi/events.ts +150 -0
- package/adapters/pi/identity.ts +10 -0
- package/adapters/pi/index.ts +25 -0
- package/adapters/pi/renderers.ts +73 -0
- package/adapters/pi/storage.ts +295 -0
- package/adapters/pi/tools.ts +6 -0
- package/adapters/pi/ui.ts +39 -0
- package/cli/index.ts +11 -11
- package/cli/verify.ts +2 -2
- package/core/achievements.ts +203 -0
- package/core/art-data.ts +105 -0
- package/core/command-service.ts +338 -0
- package/core/model.ts +59 -0
- package/core/ports.ts +40 -0
- package/core/render-model.ts +10 -0
- package/package.json +23 -19
- package/server/achievements.ts +0 -445
- /package/{hooks → adapters/claude/hooks}/buddy-comment.sh +0 -0
- /package/{hooks → adapters/claude/hooks}/name-react.sh +0 -0
- /package/{hooks → adapters/claude/hooks}/react.sh +0 -0
- /package/{cli → adapters/claude/install}/test-statusline.sh +0 -0
- /package/{.claude-plugin → adapters/claude/plugin}/marketplace.json +0 -0
- /package/{skills → adapters/claude/skills}/buddy/SKILL.md +0 -0
- /package/{statusline → adapters/claude/statusline}/buddy-status.sh +0 -0
- /package/{server → core}/engine.ts +0 -0
- /package/{server → core}/reactions.ts +0 -0
package/README.md
CHANGED
|
@@ -196,11 +196,10 @@ MCP is an industry-standard protocol. Skills are Markdown files. Hooks and statu
|
|
|
196
196
|
|
|
197
197
|
```
|
|
198
198
|
claude-buddy/
|
|
199
|
-
├──
|
|
200
|
-
├──
|
|
201
|
-
├──
|
|
202
|
-
|
|
203
|
-
└── cli/ # install, show, hunt, verify, doctor, backup, uninstall
|
|
199
|
+
├── core/ # shared buddy domain logic — engine, reactions, achievements, models
|
|
200
|
+
├── adapters/claude/ # Claude adapter — MCP server, storage, skills, hooks, UI, installer
|
|
201
|
+
├── adapters/pi/ # pi adapter — native pi extension (planned)
|
|
202
|
+
└── cli/ # host-agnostic CLI entrypoints/utilities
|
|
204
203
|
```
|
|
205
204
|
|
|
206
205
|
</details>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "./hooks/react.sh"
|
|
9
|
+
"command": "./adapters/claude/hooks/react.sh"
|
|
10
10
|
}
|
|
11
11
|
]
|
|
12
12
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"hooks": [
|
|
17
17
|
{
|
|
18
18
|
"type": "command",
|
|
19
|
-
"command": "./hooks/buddy-comment.sh"
|
|
19
|
+
"command": "./adapters/claude/hooks/buddy-comment.sh"
|
|
20
20
|
}
|
|
21
21
|
]
|
|
22
22
|
}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"hooks": [
|
|
27
27
|
{
|
|
28
28
|
"type": "command",
|
|
29
|
-
"command": "./hooks/name-react.sh"
|
|
29
|
+
"command": "./adapters/claude/hooks/name-react.sh"
|
|
30
30
|
}
|
|
31
31
|
]
|
|
32
32
|
}
|
|
@@ -25,14 +25,15 @@ import {
|
|
|
25
25
|
readFileSync, writeFileSync, mkdirSync, existsSync,
|
|
26
26
|
readdirSync, statSync, rmSync, copyFileSync,
|
|
27
27
|
} from "fs";
|
|
28
|
-
import { join } from "path";
|
|
28
|
+
import { dirname, join } from "path";
|
|
29
29
|
import { homedir } from "os";
|
|
30
|
+
import { getBuddySkillDir, getClaudeJsonPath, getClaudeSettingsPath } from "../storage/paths.ts";
|
|
30
31
|
|
|
31
32
|
const HOME = homedir();
|
|
32
33
|
const BACKUPS_DIR = join(HOME, ".claude-buddy", "backups");
|
|
33
|
-
const SETTINGS =
|
|
34
|
-
const CLAUDE_JSON =
|
|
35
|
-
const SKILL = join(
|
|
34
|
+
const SETTINGS = getClaudeSettingsPath();
|
|
35
|
+
const CLAUDE_JSON = getClaudeJsonPath();
|
|
36
|
+
const SKILL = join(getBuddySkillDir(), "SKILL.md");
|
|
36
37
|
const STATE_DIR = join(HOME, ".claude-buddy");
|
|
37
38
|
|
|
38
39
|
const RED = "\x1b[31m";
|
|
@@ -73,7 +74,7 @@ function createBackup(): string {
|
|
|
73
74
|
const dir = join(BACKUPS_DIR, ts);
|
|
74
75
|
mkdirSync(dir, { recursive: true });
|
|
75
76
|
|
|
76
|
-
const manifest:
|
|
77
|
+
const manifest: { timestamp: string; files: string[] } = { timestamp: ts, files: [] };
|
|
77
78
|
|
|
78
79
|
// 1. settings.json
|
|
79
80
|
const settings = tryRead(SETTINGS);
|
|
@@ -193,7 +194,7 @@ function restoreBackup(ts: string) {
|
|
|
193
194
|
// 1. settings.json — overwrite
|
|
194
195
|
const settingsBak = join(dir, "settings.json");
|
|
195
196
|
if (existsSync(settingsBak)) {
|
|
196
|
-
mkdirSync(
|
|
197
|
+
mkdirSync(dirname(SETTINGS), { recursive: true });
|
|
197
198
|
copyFileSync(settingsBak, SETTINGS);
|
|
198
199
|
ok("Restored: ~/.claude/settings.json");
|
|
199
200
|
}
|
|
@@ -202,9 +203,9 @@ function restoreBackup(ts: string) {
|
|
|
202
203
|
const mcpBak = join(dir, "mcpserver.json");
|
|
203
204
|
if (existsSync(mcpBak)) {
|
|
204
205
|
const ourMcp = JSON.parse(readFileSync(mcpBak, "utf8"));
|
|
205
|
-
let claudeJson: Record<string,
|
|
206
|
+
let claudeJson: { mcpServers?: Record<string, unknown> } = {};
|
|
206
207
|
try {
|
|
207
|
-
claudeJson = JSON.parse(readFileSync(CLAUDE_JSON, "utf8"));
|
|
208
|
+
claudeJson = JSON.parse(readFileSync(CLAUDE_JSON, "utf8")) as { mcpServers?: Record<string, unknown> };
|
|
208
209
|
} catch { /* empty */ }
|
|
209
210
|
if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
|
|
210
211
|
claudeJson.mcpServers["claude-buddy"] = ourMcp;
|
|
@@ -215,7 +216,7 @@ function restoreBackup(ts: string) {
|
|
|
215
216
|
// 3. SKILL.md
|
|
216
217
|
const skillBak = join(dir, "SKILL.md");
|
|
217
218
|
if (existsSync(skillBak)) {
|
|
218
|
-
mkdirSync(
|
|
219
|
+
mkdirSync(dirname(SKILL), { recursive: true });
|
|
219
220
|
copyFileSync(skillBak, SKILL);
|
|
220
221
|
ok("Restored: ~/.claude/skills/buddy/SKILL.md");
|
|
221
222
|
}
|
|
@@ -11,6 +11,27 @@
|
|
|
11
11
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
12
12
|
import { join } from "path";
|
|
13
13
|
import { homedir } from "os";
|
|
14
|
+
import { getClaudeJsonPath, getClaudeSettingsPath } from "../storage/paths.ts";
|
|
15
|
+
|
|
16
|
+
interface HookCommand {
|
|
17
|
+
type: "command";
|
|
18
|
+
command: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface HookMatcherEntry {
|
|
22
|
+
matcher?: string;
|
|
23
|
+
hooks?: HookCommand[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ClaudeSettings {
|
|
27
|
+
statusLine?: { command?: string };
|
|
28
|
+
hooks?: Record<string, HookMatcherEntry[]>;
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hasBuddyHook(entry: HookMatcherEntry): boolean {
|
|
33
|
+
return (entry.hooks ?? []).some((hook) => hook.command.includes("claude-buddy"));
|
|
34
|
+
}
|
|
14
35
|
|
|
15
36
|
const GREEN = "\x1b[32m";
|
|
16
37
|
const YELLOW = "\x1b[33m";
|
|
@@ -22,8 +43,8 @@ function ok(msg: string) { console.log(`${GREEN}✓${NC} ${msg}`); }
|
|
|
22
43
|
function warn(msg: string) { console.log(`${YELLOW}⚠${NC} ${msg}`); }
|
|
23
44
|
|
|
24
45
|
const HOME = homedir();
|
|
25
|
-
const CLAUDE_JSON =
|
|
26
|
-
const SETTINGS =
|
|
46
|
+
const CLAUDE_JSON = getClaudeJsonPath();
|
|
47
|
+
const SETTINGS = getClaudeSettingsPath();
|
|
27
48
|
|
|
28
49
|
console.log(`\n${BOLD}Disabling claude-buddy...${NC}\n`);
|
|
29
50
|
|
|
@@ -34,17 +55,17 @@ try {
|
|
|
34
55
|
delete claudeJson.mcpServers["claude-buddy"];
|
|
35
56
|
if (Object.keys(claudeJson.mcpServers).length === 0) delete claudeJson.mcpServers;
|
|
36
57
|
writeFileSync(CLAUDE_JSON, JSON.stringify(claudeJson, null, 2));
|
|
37
|
-
ok(
|
|
58
|
+
ok(`MCP server removed from ${CLAUDE_JSON}`);
|
|
38
59
|
} else {
|
|
39
60
|
warn("MCP server was not registered");
|
|
40
61
|
}
|
|
41
62
|
} catch {
|
|
42
|
-
warn(
|
|
63
|
+
warn(`Could not update ${CLAUDE_JSON}`);
|
|
43
64
|
}
|
|
44
65
|
|
|
45
66
|
// 2. Remove status line + hooks from settings.json
|
|
46
67
|
try {
|
|
47
|
-
const settings = JSON.parse(readFileSync(SETTINGS, "utf8"));
|
|
68
|
+
const settings = JSON.parse(readFileSync(SETTINGS, "utf8")) as ClaudeSettings;
|
|
48
69
|
let changed = false;
|
|
49
70
|
|
|
50
71
|
if (settings.statusLine?.command?.includes("buddy")) {
|
|
@@ -58,7 +79,7 @@ try {
|
|
|
58
79
|
if (settings.hooks[hookType]) {
|
|
59
80
|
const before = settings.hooks[hookType].length;
|
|
60
81
|
settings.hooks[hookType] = settings.hooks[hookType].filter(
|
|
61
|
-
(h
|
|
82
|
+
(h) => !hasBuddyHook(h),
|
|
62
83
|
);
|
|
63
84
|
if (settings.hooks[hookType].length < before) changed = true;
|
|
64
85
|
if (settings.hooks[hookType].length === 0) delete settings.hooks[hookType];
|
|
@@ -10,12 +10,17 @@
|
|
|
10
10
|
|
|
11
11
|
import { readFileSync, existsSync, statSync } from "fs";
|
|
12
12
|
import { execSync } from "child_process";
|
|
13
|
-
import { join, resolve
|
|
13
|
+
import { join, resolve } from "path";
|
|
14
14
|
import { homedir } from "os";
|
|
15
|
+
import { getBuddySkillDir, getClaudeConfigDir, getClaudeJsonPath, getClaudeSettingsPath } from "../storage/paths.ts";
|
|
15
16
|
|
|
16
|
-
const PROJECT_ROOT = resolve(
|
|
17
|
+
const PROJECT_ROOT = resolve(import.meta.dir, "../../..");
|
|
17
18
|
const HOME = homedir();
|
|
18
|
-
const
|
|
19
|
+
const CLAUDE_DIR = getClaudeConfigDir();
|
|
20
|
+
const CLAUDE_JSON = getClaudeJsonPath();
|
|
21
|
+
const SETTINGS = getClaudeSettingsPath();
|
|
22
|
+
const SKILL_PATH = join(getBuddySkillDir(), "SKILL.md");
|
|
23
|
+
const STATUS_SCRIPT = join(PROJECT_ROOT, "adapters", "claude", "statusline", "buddy-status.sh");
|
|
19
24
|
|
|
20
25
|
const RED = "\x1b[31m";
|
|
21
26
|
const GREEN = "\x1b[32m";
|
|
@@ -49,9 +54,13 @@ function tryRead(path: string): string | null {
|
|
|
49
54
|
try { return readFileSync(path, "utf8"); } catch { return null; }
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
function tryParseJson(text: string | null):
|
|
57
|
+
function tryParseJson(text: string | null): unknown {
|
|
53
58
|
if (!text) return null;
|
|
54
|
-
try { return JSON.parse(text); } catch { return null; }
|
|
59
|
+
try { return JSON.parse(text) as unknown; } catch { return null; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
63
|
+
return typeof value === "object" && value !== null;
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
// ─── Header ─────────────────────────────────────────────────────────────────
|
|
@@ -91,8 +100,10 @@ row("tput cols", tryExec("tput cols 2>/dev/null", "(failed)"));
|
|
|
91
100
|
section("Filesystem");
|
|
92
101
|
const procExists = existsSync("/proc");
|
|
93
102
|
row("/proc exists", procExists ? `${GREEN}yes${NC} (Linux)` : `${RED}no${NC} (macOS/BSD)`);
|
|
94
|
-
row("
|
|
95
|
-
row("
|
|
103
|
+
row("Claude config dir", CLAUDE_DIR);
|
|
104
|
+
row("Claude config dir exists", existsSync(CLAUDE_DIR) ? "yes" : "no");
|
|
105
|
+
row("Claude config file", CLAUDE_JSON);
|
|
106
|
+
row("Claude config file exists", existsSync(CLAUDE_JSON) ? "yes" : "no");
|
|
96
107
|
row("~/.claude-buddy/ exists", existsSync(join(HOME, ".claude-buddy")) ? "yes" : "no");
|
|
97
108
|
row("Project root", PROJECT_ROOT);
|
|
98
109
|
row("Status script exists", existsSync(STATUS_SCRIPT) ? "yes" : `${RED}no${NC}`);
|
|
@@ -103,18 +114,20 @@ section("claude-buddy state");
|
|
|
103
114
|
const menagerie = tryParseJson(tryRead(join(HOME, ".claude-buddy", "menagerie.json")));
|
|
104
115
|
const status = tryParseJson(tryRead(join(HOME, ".claude-buddy", "status.json")));
|
|
105
116
|
|
|
106
|
-
if (menagerie) {
|
|
107
|
-
const activeSlot = menagerie.active
|
|
108
|
-
const
|
|
117
|
+
if (isRecord(menagerie)) {
|
|
118
|
+
const activeSlot = typeof menagerie.active === "string" ? menagerie.active : "buddy";
|
|
119
|
+
const companions = isRecord(menagerie.companions) ? menagerie.companions : {};
|
|
120
|
+
const companion = companions[activeSlot];
|
|
109
121
|
row("Active slot", activeSlot);
|
|
110
|
-
row("Total slots", String(Object.keys(
|
|
111
|
-
if (companion) {
|
|
112
|
-
|
|
113
|
-
row("
|
|
114
|
-
row("
|
|
115
|
-
row("
|
|
116
|
-
row("
|
|
117
|
-
row("
|
|
122
|
+
row("Total slots", String(Object.keys(companions).length));
|
|
123
|
+
if (isRecord(companion)) {
|
|
124
|
+
const bones = isRecord(companion.bones) ? companion.bones : {};
|
|
125
|
+
row("Companion name", typeof companion.name === "string" ? companion.name : "(none)");
|
|
126
|
+
row("Species", typeof bones.species === "string" ? bones.species : "(none)");
|
|
127
|
+
row("Rarity", typeof bones.rarity === "string" ? bones.rarity : "(none)");
|
|
128
|
+
row("Hat", typeof bones.hat === "string" ? bones.hat : "(none)");
|
|
129
|
+
row("Eye", typeof bones.eye === "string" ? bones.eye : "(none)");
|
|
130
|
+
row("Shiny", String(typeof bones.shiny === "boolean" ? bones.shiny : false));
|
|
118
131
|
} else {
|
|
119
132
|
err(`No companion found in active slot "${activeSlot}"`);
|
|
120
133
|
}
|
|
@@ -122,9 +135,9 @@ if (menagerie) {
|
|
|
122
135
|
err("No manifest found at ~/.claude-buddy/menagerie.json");
|
|
123
136
|
}
|
|
124
137
|
|
|
125
|
-
if (status) {
|
|
126
|
-
row("Status muted", String(status.muted
|
|
127
|
-
row("Current reaction", status.reaction
|
|
138
|
+
if (isRecord(status)) {
|
|
139
|
+
row("Status muted", String(typeof status.muted === "boolean" ? status.muted : false));
|
|
140
|
+
row("Current reaction", typeof status.reaction === "string" && status.reaction.length > 0 ? status.reaction : "(none)");
|
|
128
141
|
} else {
|
|
129
142
|
warn("No status state at ~/.claude-buddy/status.json");
|
|
130
143
|
}
|
|
@@ -132,38 +145,38 @@ if (status) {
|
|
|
132
145
|
// ─── settings.json ──────────────────────────────────────────────────────────
|
|
133
146
|
|
|
134
147
|
section("Claude Code config");
|
|
135
|
-
const settings = tryParseJson(tryRead(
|
|
136
|
-
const claudeJson = tryParseJson(tryRead(
|
|
148
|
+
const settings = tryParseJson(tryRead(SETTINGS));
|
|
149
|
+
const claudeJson = tryParseJson(tryRead(CLAUDE_JSON));
|
|
137
150
|
|
|
138
|
-
if (settings
|
|
151
|
+
if (isRecord(settings) && "statusLine" in settings && settings.statusLine !== undefined) {
|
|
139
152
|
console.log(` ${DIM}statusLine:${NC}`);
|
|
140
153
|
console.log(` ${JSON.stringify(settings.statusLine, null, 2).split("\n").join("\n ")}`);
|
|
141
154
|
} else {
|
|
142
155
|
warn("No statusLine in ~/.claude/settings.json");
|
|
143
156
|
}
|
|
144
157
|
|
|
145
|
-
if (settings
|
|
158
|
+
if (isRecord(settings) && isRecord(settings.hooks)) {
|
|
146
159
|
console.log(` ${DIM}hooks:${NC}`);
|
|
147
160
|
for (const event of Object.keys(settings.hooks)) {
|
|
148
|
-
const
|
|
161
|
+
const hookEntries = settings.hooks[event];
|
|
162
|
+
const count = Array.isArray(hookEntries) ? hookEntries.length : 0;
|
|
149
163
|
row(` ${event}`, `${count} entr${count === 1 ? "y" : "ies"}`);
|
|
150
164
|
}
|
|
151
165
|
} else {
|
|
152
166
|
warn("No hooks configured");
|
|
153
167
|
}
|
|
154
168
|
|
|
155
|
-
if (claudeJson
|
|
169
|
+
if (isRecord(claudeJson) && isRecord(claudeJson.mcpServers) && isRecord(claudeJson.mcpServers["claude-buddy"])) {
|
|
156
170
|
ok("MCP server registered in ~/.claude.json");
|
|
157
171
|
console.log(` ${JSON.stringify(claudeJson.mcpServers["claude-buddy"], null, 2).split("\n").join("\n ")}`);
|
|
158
172
|
} else {
|
|
159
173
|
err("MCP server NOT registered in ~/.claude.json");
|
|
160
174
|
}
|
|
161
175
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
ok(`Skill installed: ${skillPath}`);
|
|
176
|
+
if (existsSync(SKILL_PATH)) {
|
|
177
|
+
ok(`Skill installed: ${SKILL_PATH}`);
|
|
165
178
|
} else {
|
|
166
|
-
err(`Skill missing: ${
|
|
179
|
+
err(`Skill missing: ${SKILL_PATH}`);
|
|
167
180
|
}
|
|
168
181
|
|
|
169
182
|
// ─── Live status line test ──────────────────────────────────────────────────
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
import {
|
|
11
11
|
searchBuddy, renderBuddy, SPECIES, RARITIES, STAT_NAMES,
|
|
12
12
|
type Species, type Rarity, type StatName, type SearchCriteria,
|
|
13
|
-
} from "
|
|
13
|
+
} from "../../../core/engine.ts";
|
|
14
14
|
import {
|
|
15
15
|
saveCompanionSlot, saveActiveSlot, writeStatusState,
|
|
16
16
|
slugify, unusedName, listCompanionSlots,
|
|
17
|
-
} from "../
|
|
17
|
+
} from "../storage/state.ts";
|
|
18
18
|
import { createInterface } from "readline";
|
|
19
19
|
|
|
20
20
|
const CYAN = "\x1b[36m";
|
|
@@ -67,8 +67,8 @@ ${CYAN}╚═══════════════════════
|
|
|
67
67
|
const statsAns = await ask(`\n Configure stats? [Y/n]: `);
|
|
68
68
|
if (statsAns.toLowerCase() !== "n") {
|
|
69
69
|
wantPeak = await pickFromList("Peak stat (highest):", STAT_NAMES);
|
|
70
|
-
const dumpOptions = STAT_NAMES.filter((s) => s !== wantPeak);
|
|
71
|
-
wantDump = await pickFromList("Dump stat (lowest):", dumpOptions
|
|
70
|
+
const dumpOptions = STAT_NAMES.filter((s): s is Exclude<StatName, typeof wantPeak> => s !== wantPeak);
|
|
71
|
+
wantDump = await pickFromList("Dump stat (lowest):", dumpOptions);
|
|
72
72
|
console.log(`${GREEN}✓${NC} peak=${wantPeak} dump=${wantDump}`);
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
* Checks: bun, jq, ~/.claude/ directory
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, cpSync } from "fs";
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, cpSync, statSync } from "fs";
|
|
9
9
|
import { execSync } from "child_process";
|
|
10
|
-
import { join, resolve
|
|
11
|
-
import { homedir } from "os";
|
|
10
|
+
import { join, resolve } from "path";
|
|
12
11
|
|
|
13
|
-
import { generateBones, renderBuddy, renderFace, RARITY_STARS } from "
|
|
14
|
-
import { loadCompanion, saveCompanion,
|
|
15
|
-
import {
|
|
12
|
+
import { generateBones, renderBuddy, renderFace, RARITY_STARS } from "../../../core/engine.ts";
|
|
13
|
+
import { loadCompanion, saveCompanion, writeStatusState } from "../storage/state.ts";
|
|
14
|
+
import { resolveUserId } from "../storage/identity.ts";
|
|
15
|
+
import { generateFallbackName } from "../../../core/reactions.ts";
|
|
16
|
+
import { getBuddySkillDir, getClaudeConfigDir, getClaudeJsonPath, getClaudeSettingsPath } from "../storage/paths.ts";
|
|
16
17
|
|
|
17
18
|
const CYAN = "\x1b[36m";
|
|
18
19
|
const GREEN = "\x1b[32m";
|
|
@@ -22,10 +23,11 @@ const BOLD = "\x1b[1m";
|
|
|
22
23
|
const DIM = "\x1b[2m";
|
|
23
24
|
const NC = "\x1b[0m";
|
|
24
25
|
|
|
25
|
-
const CLAUDE_DIR =
|
|
26
|
-
const SETTINGS_FILE =
|
|
27
|
-
const BUDDY_DIR =
|
|
28
|
-
const
|
|
26
|
+
const CLAUDE_DIR = getClaudeConfigDir();
|
|
27
|
+
const SETTINGS_FILE = getClaudeSettingsPath();
|
|
28
|
+
const BUDDY_DIR = getBuddySkillDir();
|
|
29
|
+
const CLAUDE_JSON = getClaudeJsonPath();
|
|
30
|
+
const PROJECT_ROOT = resolve(import.meta.dir, "../../..");
|
|
29
31
|
|
|
30
32
|
function banner() {
|
|
31
33
|
console.log(`
|
|
@@ -70,21 +72,23 @@ function preflight(): boolean {
|
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
// Check
|
|
75
|
+
// Check Claude config dir exists and is actually a directory
|
|
74
76
|
if (!existsSync(CLAUDE_DIR)) {
|
|
75
|
-
err(
|
|
77
|
+
err(`Claude config dir not found: ${CLAUDE_DIR}`);
|
|
78
|
+
pass = false;
|
|
79
|
+
} else if (!statSync(CLAUDE_DIR).isDirectory()) {
|
|
80
|
+
err(`Claude config path is not a directory: ${CLAUDE_DIR}`);
|
|
76
81
|
pass = false;
|
|
77
82
|
} else {
|
|
78
|
-
ok(
|
|
83
|
+
ok(`Claude config dir found: ${CLAUDE_DIR}`);
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
// Check
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
err("~/.claude.json not found. Start Claude Code once first, then re-run.");
|
|
86
|
+
// Check .claude.json exists in the active Claude config location
|
|
87
|
+
if (!existsSync(CLAUDE_JSON)) {
|
|
88
|
+
err(`Claude config file not found: ${CLAUDE_JSON}`);
|
|
85
89
|
pass = false;
|
|
86
90
|
} else {
|
|
87
|
-
ok(
|
|
91
|
+
ok(`Claude config file found: ${CLAUDE_JSON}`);
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
return pass;
|
|
@@ -92,15 +96,50 @@ function preflight(): boolean {
|
|
|
92
96
|
|
|
93
97
|
// ─── Load / update settings.json ────────────────────────────────────────────
|
|
94
98
|
|
|
95
|
-
|
|
99
|
+
interface HookCommand {
|
|
100
|
+
type: "command";
|
|
101
|
+
command: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface HookMatcherEntry {
|
|
105
|
+
matcher?: string;
|
|
106
|
+
hooks?: HookCommand[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface ClaudeStatusLineConfig {
|
|
110
|
+
type: "command";
|
|
111
|
+
command: string;
|
|
112
|
+
padding: number;
|
|
113
|
+
refreshInterval: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface ClaudeSettings {
|
|
117
|
+
statusLine?: ClaudeStatusLineConfig;
|
|
118
|
+
hooks?: Partial<Record<"SessionStart" | "SessionEnd" | "PostToolUse" | "Stop" | "UserPromptSubmit", HookMatcherEntry[]>>;
|
|
119
|
+
permissions?: {
|
|
120
|
+
allow?: string[];
|
|
121
|
+
};
|
|
122
|
+
[key: string]: unknown;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface ClaudeJsonConfig {
|
|
126
|
+
mcpServers?: Record<string, { command: string; args: string[]; cwd: string }>;
|
|
127
|
+
[key: string]: unknown;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function hasBuddyHook(entry: HookMatcherEntry): boolean {
|
|
131
|
+
return (entry.hooks ?? []).some((hook) => hook.command.includes("claude-buddy"));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function loadSettings(): ClaudeSettings {
|
|
96
135
|
try {
|
|
97
|
-
return JSON.parse(readFileSync(SETTINGS_FILE, "utf8"));
|
|
136
|
+
return JSON.parse(readFileSync(SETTINGS_FILE, "utf8")) as ClaudeSettings;
|
|
98
137
|
} catch {
|
|
99
138
|
return {};
|
|
100
139
|
}
|
|
101
140
|
}
|
|
102
141
|
|
|
103
|
-
function saveSettings(settings:
|
|
142
|
+
function saveSettings(settings: ClaudeSettings) {
|
|
104
143
|
mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
105
144
|
writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + "\n");
|
|
106
145
|
}
|
|
@@ -108,12 +147,12 @@ function saveSettings(settings: Record<string, any>) {
|
|
|
108
147
|
// ─── Step 1: Register MCP server (in ~/.claude.json) ────────────────────────
|
|
109
148
|
|
|
110
149
|
function installMcp() {
|
|
111
|
-
const serverPath = join(PROJECT_ROOT, "server", "index.ts");
|
|
112
|
-
const claudeJsonPath =
|
|
150
|
+
const serverPath = join(PROJECT_ROOT, "adapters", "claude", "server", "index.ts");
|
|
151
|
+
const claudeJsonPath = CLAUDE_JSON;
|
|
113
152
|
|
|
114
|
-
let claudeJson:
|
|
153
|
+
let claudeJson: ClaudeJsonConfig = {};
|
|
115
154
|
try {
|
|
116
|
-
claudeJson = JSON.parse(readFileSync(claudeJsonPath, "utf8"));
|
|
155
|
+
claudeJson = JSON.parse(readFileSync(claudeJsonPath, "utf8")) as ClaudeJsonConfig;
|
|
117
156
|
} catch { /* fresh config */ }
|
|
118
157
|
|
|
119
158
|
if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
|
|
@@ -125,22 +164,22 @@ function installMcp() {
|
|
|
125
164
|
};
|
|
126
165
|
|
|
127
166
|
writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
|
|
128
|
-
ok(
|
|
167
|
+
ok(`MCP server registered in ${claudeJsonPath}`);
|
|
129
168
|
}
|
|
130
169
|
|
|
131
170
|
// ─── Step 2: Install skill ──────────────────────────────────────────────────
|
|
132
171
|
|
|
133
172
|
function installSkill() {
|
|
134
|
-
const srcSkill = join(PROJECT_ROOT, "skills", "buddy", "SKILL.md");
|
|
173
|
+
const srcSkill = join(PROJECT_ROOT, "adapters", "claude", "skills", "buddy", "SKILL.md");
|
|
135
174
|
mkdirSync(BUDDY_DIR, { recursive: true });
|
|
136
175
|
cpSync(srcSkill, join(BUDDY_DIR, "SKILL.md"), { force: true });
|
|
137
|
-
ok(
|
|
176
|
+
ok(`Skill installed: ${join(BUDDY_DIR, "SKILL.md")}`);
|
|
138
177
|
}
|
|
139
178
|
|
|
140
179
|
// ─── Step 3: Configure status line (with animation refresh) ─────────────────
|
|
141
180
|
|
|
142
|
-
function installStatusLine(settings:
|
|
143
|
-
const statusScript = join(PROJECT_ROOT, "statusline", "buddy-status.sh");
|
|
181
|
+
function installStatusLine(settings: ClaudeSettings) {
|
|
182
|
+
const statusScript = join(PROJECT_ROOT, "adapters", "claude", "statusline", "buddy-status.sh");
|
|
144
183
|
|
|
145
184
|
settings.statusLine = {
|
|
146
185
|
type: "command",
|
|
@@ -167,15 +206,15 @@ function detectTmux(): boolean {
|
|
|
167
206
|
}
|
|
168
207
|
}
|
|
169
208
|
|
|
170
|
-
function installPopupHooks(settings:
|
|
171
|
-
const popupManager = join(PROJECT_ROOT, "popup", "popup-manager.sh");
|
|
209
|
+
function installPopupHooks(settings: ClaudeSettings) {
|
|
210
|
+
const popupManager = join(PROJECT_ROOT, "adapters", "claude", "popup", "popup-manager.sh");
|
|
172
211
|
|
|
173
212
|
if (!settings.hooks) settings.hooks = {};
|
|
174
213
|
|
|
175
214
|
// SessionStart: open popup
|
|
176
215
|
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
177
216
|
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(
|
|
178
|
-
(h
|
|
217
|
+
(h) => !hasBuddyHook(h),
|
|
179
218
|
);
|
|
180
219
|
settings.hooks.SessionStart.push({
|
|
181
220
|
hooks: [{ type: "command", command: `${popupManager} start` }],
|
|
@@ -184,7 +223,7 @@ function installPopupHooks(settings: Record<string, any>) {
|
|
|
184
223
|
// SessionEnd: close popup
|
|
185
224
|
if (!settings.hooks.SessionEnd) settings.hooks.SessionEnd = [];
|
|
186
225
|
settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(
|
|
187
|
-
(h
|
|
226
|
+
(h) => !hasBuddyHook(h),
|
|
188
227
|
);
|
|
189
228
|
settings.hooks.SessionEnd.push({
|
|
190
229
|
hooks: [{ type: "command", command: `${popupManager} stop` }],
|
|
@@ -195,17 +234,17 @@ function installPopupHooks(settings: Record<string, any>) {
|
|
|
195
234
|
|
|
196
235
|
// ─── Step 4: Register hooks ─────────────────────────────────────────────────
|
|
197
236
|
|
|
198
|
-
function installHooks(settings:
|
|
199
|
-
const reactHook = join(PROJECT_ROOT, "hooks", "react.sh");
|
|
200
|
-
const commentHook = join(PROJECT_ROOT, "hooks", "buddy-comment.sh");
|
|
201
|
-
const nameHook = join(PROJECT_ROOT, "hooks", "name-react.sh");
|
|
237
|
+
function installHooks(settings: ClaudeSettings) {
|
|
238
|
+
const reactHook = join(PROJECT_ROOT, "adapters", "claude", "hooks", "react.sh");
|
|
239
|
+
const commentHook = join(PROJECT_ROOT, "adapters", "claude", "hooks", "buddy-comment.sh");
|
|
240
|
+
const nameHook = join(PROJECT_ROOT, "adapters", "claude", "hooks", "name-react.sh");
|
|
202
241
|
|
|
203
242
|
if (!settings.hooks) settings.hooks = {};
|
|
204
243
|
|
|
205
244
|
// PostToolUse: detect errors/test failures/successes in Bash output
|
|
206
245
|
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
207
246
|
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(
|
|
208
|
-
(h
|
|
247
|
+
(h) => !hasBuddyHook(h),
|
|
209
248
|
);
|
|
210
249
|
settings.hooks.PostToolUse.push({
|
|
211
250
|
matcher: "Bash",
|
|
@@ -215,7 +254,7 @@ function installHooks(settings: Record<string, any>) {
|
|
|
215
254
|
// Stop: extract <!-- buddy: --> comment from Claude's response
|
|
216
255
|
if (!settings.hooks.Stop) settings.hooks.Stop = [];
|
|
217
256
|
settings.hooks.Stop = settings.hooks.Stop.filter(
|
|
218
|
-
(h
|
|
257
|
+
(h) => !hasBuddyHook(h),
|
|
219
258
|
);
|
|
220
259
|
settings.hooks.Stop.push({
|
|
221
260
|
hooks: [{ type: "command", command: commentHook }],
|
|
@@ -224,7 +263,7 @@ function installHooks(settings: Record<string, any>) {
|
|
|
224
263
|
// UserPromptSubmit: detect buddy's name in user message → instant status line reaction
|
|
225
264
|
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
|
|
226
265
|
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
|
|
227
|
-
(h
|
|
266
|
+
(h) => !hasBuddyHook(h),
|
|
228
267
|
);
|
|
229
268
|
settings.hooks.UserPromptSubmit.push({
|
|
230
269
|
hooks: [{ type: "command", command: nameHook }],
|
|
@@ -235,7 +274,7 @@ function installHooks(settings: Record<string, any>) {
|
|
|
235
274
|
|
|
236
275
|
// ─── Step 5: Ensure MCP tools are allowed ───────────────────────────────────
|
|
237
276
|
|
|
238
|
-
function ensurePermissions(settings:
|
|
277
|
+
function ensurePermissions(settings: ClaudeSettings) {
|
|
239
278
|
if (!settings.permissions) settings.permissions = {};
|
|
240
279
|
if (!settings.permissions.allow) settings.permissions.allow = [];
|
|
241
280
|
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
import {
|
|
19
19
|
loadActiveSlot, saveActiveSlot, listCompanionSlots,
|
|
20
20
|
loadCompanionSlot, saveCompanionSlot, slugify, unusedName, writeStatusState,
|
|
21
|
-
} from "../
|
|
21
|
+
} from "../storage/state.ts";
|
|
22
22
|
import {
|
|
23
23
|
generateBones, SPECIES, RARITIES, STAT_NAMES, RARITY_STARS,
|
|
24
24
|
type Species, type Rarity, type StatName, type BuddyBones, type Companion,
|
|
25
|
-
} from "
|
|
26
|
-
import { renderCompanionCard } from "../
|
|
25
|
+
} from "../../../core/engine.ts";
|
|
26
|
+
import { renderCompanionCard } from "../rendering/art.ts";
|
|
27
27
|
import { randomBytes } from "crypto";
|
|
28
28
|
|
|
29
29
|
// ─── ANSI ─────────────────────────────────────────────────────────────────────
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* bun run settings cooldown 0 Set comment cooldown (0-300 seconds)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { loadConfig, saveConfig } from "../
|
|
10
|
+
import { loadConfig, saveConfig } from "../storage/state.ts";
|
|
11
11
|
|
|
12
12
|
const args = process.argv.slice(2);
|
|
13
13
|
const key = args[0];
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* claude-buddy show — display current companion in terminal
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { renderBuddy, renderFace, RARITY_STARS } from "
|
|
6
|
-
import { loadCompanion, loadReaction } from "../
|
|
5
|
+
import { renderBuddy, renderFace, RARITY_STARS } from "../../../core/engine.ts";
|
|
6
|
+
import { loadCompanion, loadReaction } from "../storage/state.ts";
|
|
7
7
|
|
|
8
8
|
const BOLD = "\x1b[1m";
|
|
9
9
|
const DIM = "\x1b[2m";
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
import { readFileSync, writeFileSync, existsSync, copyFileSync, chmodSync, mkdirSync } from "fs";
|
|
15
15
|
import { join, dirname, resolve } from "path";
|
|
16
16
|
import { homedir } from "os";
|
|
17
|
+
import { getClaudeSettingsPath } from "../storage/paths.ts";
|
|
17
18
|
|
|
18
19
|
const HOME = homedir();
|
|
19
|
-
const SETTINGS =
|
|
20
|
+
const SETTINGS = getClaudeSettingsPath();
|
|
20
21
|
const BACKUP = join(HOME, ".claude-buddy", "statusline.bak");
|
|
21
22
|
const TEST_SCRIPT = join(HOME, ".claude-buddy", "test-statusline.sh");
|
|
22
23
|
const SOURCE_SCRIPT = resolve(import.meta.dir, "test-statusline.sh");
|
|
@@ -41,7 +42,7 @@ if (action === "install") {
|
|
|
41
42
|
console.log(`\n${BOLD}claude-buddy test status line installer${NC}\n`);
|
|
42
43
|
|
|
43
44
|
if (!existsSync(SETTINGS)) {
|
|
44
|
-
err(
|
|
45
|
+
err(`Claude settings not found: ${SETTINGS}`);
|
|
45
46
|
process.exit(1);
|
|
46
47
|
}
|
|
47
48
|
if (!existsSync(SOURCE_SCRIPT)) {
|