@ramarivera/coding-buddy 0.4.0-alpha.1 → 0.4.0-alpha.3
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 +3 -3
- package/{cli → adapters/claude/install}/disable.ts +22 -2
- package/{cli → adapters/claude/install}/doctor.ts +30 -23
- package/{cli → adapters/claude/install}/hunt.ts +4 -4
- package/{cli → adapters/claude/install}/install.ts +62 -26
- 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}/uninstall.ts +22 -2
- 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 +14 -0
- package/adapters/claude/storage/settings.ts +42 -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/{cli → adapters/claude/install}/test-statusline.ts +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
|
}
|
|
@@ -73,7 +73,7 @@ function createBackup(): string {
|
|
|
73
73
|
const dir = join(BACKUPS_DIR, ts);
|
|
74
74
|
mkdirSync(dir, { recursive: true });
|
|
75
75
|
|
|
76
|
-
const manifest:
|
|
76
|
+
const manifest: { timestamp: string; files: string[] } = { timestamp: ts, files: [] };
|
|
77
77
|
|
|
78
78
|
// 1. settings.json
|
|
79
79
|
const settings = tryRead(SETTINGS);
|
|
@@ -202,9 +202,9 @@ function restoreBackup(ts: string) {
|
|
|
202
202
|
const mcpBak = join(dir, "mcpserver.json");
|
|
203
203
|
if (existsSync(mcpBak)) {
|
|
204
204
|
const ourMcp = JSON.parse(readFileSync(mcpBak, "utf8"));
|
|
205
|
-
let claudeJson: Record<string,
|
|
205
|
+
let claudeJson: { mcpServers?: Record<string, unknown> } = {};
|
|
206
206
|
try {
|
|
207
|
-
claudeJson = JSON.parse(readFileSync(CLAUDE_JSON, "utf8"));
|
|
207
|
+
claudeJson = JSON.parse(readFileSync(CLAUDE_JSON, "utf8")) as { mcpServers?: Record<string, unknown> };
|
|
208
208
|
} catch { /* empty */ }
|
|
209
209
|
if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
|
|
210
210
|
claudeJson.mcpServers["claude-buddy"] = ourMcp;
|
|
@@ -12,6 +12,26 @@ import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
|
12
12
|
import { join } from "path";
|
|
13
13
|
import { homedir } from "os";
|
|
14
14
|
|
|
15
|
+
interface HookCommand {
|
|
16
|
+
type: "command";
|
|
17
|
+
command: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface HookMatcherEntry {
|
|
21
|
+
matcher?: string;
|
|
22
|
+
hooks?: HookCommand[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ClaudeSettings {
|
|
26
|
+
statusLine?: { command?: string };
|
|
27
|
+
hooks?: Record<string, HookMatcherEntry[]>;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hasBuddyHook(entry: HookMatcherEntry): boolean {
|
|
32
|
+
return (entry.hooks ?? []).some((hook) => hook.command.includes("claude-buddy"));
|
|
33
|
+
}
|
|
34
|
+
|
|
15
35
|
const GREEN = "\x1b[32m";
|
|
16
36
|
const YELLOW = "\x1b[33m";
|
|
17
37
|
const BOLD = "\x1b[1m";
|
|
@@ -44,7 +64,7 @@ try {
|
|
|
44
64
|
|
|
45
65
|
// 2. Remove status line + hooks from settings.json
|
|
46
66
|
try {
|
|
47
|
-
const settings = JSON.parse(readFileSync(SETTINGS, "utf8"));
|
|
67
|
+
const settings = JSON.parse(readFileSync(SETTINGS, "utf8")) as ClaudeSettings;
|
|
48
68
|
let changed = false;
|
|
49
69
|
|
|
50
70
|
if (settings.statusLine?.command?.includes("buddy")) {
|
|
@@ -58,7 +78,7 @@ try {
|
|
|
58
78
|
if (settings.hooks[hookType]) {
|
|
59
79
|
const before = settings.hooks[hookType].length;
|
|
60
80
|
settings.hooks[hookType] = settings.hooks[hookType].filter(
|
|
61
|
-
(h
|
|
81
|
+
(h) => !hasBuddyHook(h),
|
|
62
82
|
);
|
|
63
83
|
if (settings.hooks[hookType].length < before) changed = true;
|
|
64
84
|
if (settings.hooks[hookType].length === 0) delete settings.hooks[hookType];
|
|
@@ -10,12 +10,12 @@
|
|
|
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
15
|
|
|
16
|
-
const PROJECT_ROOT = resolve(
|
|
16
|
+
const PROJECT_ROOT = resolve(import.meta.dir, "../../..");
|
|
17
17
|
const HOME = homedir();
|
|
18
|
-
const STATUS_SCRIPT = join(PROJECT_ROOT, "statusline", "buddy-status.sh");
|
|
18
|
+
const STATUS_SCRIPT = join(PROJECT_ROOT, "adapters", "claude", "statusline", "buddy-status.sh");
|
|
19
19
|
|
|
20
20
|
const RED = "\x1b[31m";
|
|
21
21
|
const GREEN = "\x1b[32m";
|
|
@@ -49,9 +49,13 @@ function tryRead(path: string): string | null {
|
|
|
49
49
|
try { return readFileSync(path, "utf8"); } catch { return null; }
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function tryParseJson(text: string | null):
|
|
52
|
+
function tryParseJson(text: string | null): unknown {
|
|
53
53
|
if (!text) return null;
|
|
54
|
-
try { return JSON.parse(text); } catch { return null; }
|
|
54
|
+
try { return JSON.parse(text) as unknown; } catch { return null; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
58
|
+
return typeof value === "object" && value !== null;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
// ─── Header ─────────────────────────────────────────────────────────────────
|
|
@@ -103,18 +107,20 @@ section("claude-buddy state");
|
|
|
103
107
|
const menagerie = tryParseJson(tryRead(join(HOME, ".claude-buddy", "menagerie.json")));
|
|
104
108
|
const status = tryParseJson(tryRead(join(HOME, ".claude-buddy", "status.json")));
|
|
105
109
|
|
|
106
|
-
if (menagerie) {
|
|
107
|
-
const activeSlot = menagerie.active
|
|
108
|
-
const
|
|
110
|
+
if (isRecord(menagerie)) {
|
|
111
|
+
const activeSlot = typeof menagerie.active === "string" ? menagerie.active : "buddy";
|
|
112
|
+
const companions = isRecord(menagerie.companions) ? menagerie.companions : {};
|
|
113
|
+
const companion = companions[activeSlot];
|
|
109
114
|
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("
|
|
115
|
+
row("Total slots", String(Object.keys(companions).length));
|
|
116
|
+
if (isRecord(companion)) {
|
|
117
|
+
const bones = isRecord(companion.bones) ? companion.bones : {};
|
|
118
|
+
row("Companion name", typeof companion.name === "string" ? companion.name : "(none)");
|
|
119
|
+
row("Species", typeof bones.species === "string" ? bones.species : "(none)");
|
|
120
|
+
row("Rarity", typeof bones.rarity === "string" ? bones.rarity : "(none)");
|
|
121
|
+
row("Hat", typeof bones.hat === "string" ? bones.hat : "(none)");
|
|
122
|
+
row("Eye", typeof bones.eye === "string" ? bones.eye : "(none)");
|
|
123
|
+
row("Shiny", String(typeof bones.shiny === "boolean" ? bones.shiny : false));
|
|
118
124
|
} else {
|
|
119
125
|
err(`No companion found in active slot "${activeSlot}"`);
|
|
120
126
|
}
|
|
@@ -122,9 +128,9 @@ if (menagerie) {
|
|
|
122
128
|
err("No manifest found at ~/.claude-buddy/menagerie.json");
|
|
123
129
|
}
|
|
124
130
|
|
|
125
|
-
if (status) {
|
|
126
|
-
row("Status muted", String(status.muted
|
|
127
|
-
row("Current reaction", status.reaction
|
|
131
|
+
if (isRecord(status)) {
|
|
132
|
+
row("Status muted", String(typeof status.muted === "boolean" ? status.muted : false));
|
|
133
|
+
row("Current reaction", typeof status.reaction === "string" && status.reaction.length > 0 ? status.reaction : "(none)");
|
|
128
134
|
} else {
|
|
129
135
|
warn("No status state at ~/.claude-buddy/status.json");
|
|
130
136
|
}
|
|
@@ -135,24 +141,25 @@ section("Claude Code config");
|
|
|
135
141
|
const settings = tryParseJson(tryRead(join(HOME, ".claude", "settings.json")));
|
|
136
142
|
const claudeJson = tryParseJson(tryRead(join(HOME, ".claude.json")));
|
|
137
143
|
|
|
138
|
-
if (settings
|
|
144
|
+
if (isRecord(settings) && "statusLine" in settings && settings.statusLine !== undefined) {
|
|
139
145
|
console.log(` ${DIM}statusLine:${NC}`);
|
|
140
146
|
console.log(` ${JSON.stringify(settings.statusLine, null, 2).split("\n").join("\n ")}`);
|
|
141
147
|
} else {
|
|
142
148
|
warn("No statusLine in ~/.claude/settings.json");
|
|
143
149
|
}
|
|
144
150
|
|
|
145
|
-
if (settings
|
|
151
|
+
if (isRecord(settings) && isRecord(settings.hooks)) {
|
|
146
152
|
console.log(` ${DIM}hooks:${NC}`);
|
|
147
153
|
for (const event of Object.keys(settings.hooks)) {
|
|
148
|
-
const
|
|
154
|
+
const hookEntries = settings.hooks[event];
|
|
155
|
+
const count = Array.isArray(hookEntries) ? hookEntries.length : 0;
|
|
149
156
|
row(` ${event}`, `${count} entr${count === 1 ? "y" : "ies"}`);
|
|
150
157
|
}
|
|
151
158
|
} else {
|
|
152
159
|
warn("No hooks configured");
|
|
153
160
|
}
|
|
154
161
|
|
|
155
|
-
if (claudeJson
|
|
162
|
+
if (isRecord(claudeJson) && isRecord(claudeJson.mcpServers) && isRecord(claudeJson.mcpServers["claude-buddy"])) {
|
|
156
163
|
ok("MCP server registered in ~/.claude.json");
|
|
157
164
|
console.log(` ${JSON.stringify(claudeJson.mcpServers["claude-buddy"], null, 2).split("\n").join("\n ")}`);
|
|
158
165
|
} else {
|
|
@@ -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
|
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, cpSync } from "fs";
|
|
9
9
|
import { execSync } from "child_process";
|
|
10
|
-
import { join, resolve
|
|
10
|
+
import { join, resolve } from "path";
|
|
11
11
|
import { homedir } from "os";
|
|
12
12
|
|
|
13
|
-
import { generateBones, renderBuddy, renderFace, RARITY_STARS } from "
|
|
14
|
-
import { loadCompanion, saveCompanion,
|
|
15
|
-
import {
|
|
13
|
+
import { generateBones, renderBuddy, renderFace, RARITY_STARS } from "../../../core/engine.ts";
|
|
14
|
+
import { loadCompanion, saveCompanion, writeStatusState } from "../storage/state.ts";
|
|
15
|
+
import { resolveUserId } from "../storage/identity.ts";
|
|
16
|
+
import { generateFallbackName } from "../../../core/reactions.ts";
|
|
16
17
|
|
|
17
18
|
const CYAN = "\x1b[36m";
|
|
18
19
|
const GREEN = "\x1b[32m";
|
|
@@ -25,7 +26,7 @@ const NC = "\x1b[0m";
|
|
|
25
26
|
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
26
27
|
const SETTINGS_FILE = join(CLAUDE_DIR, "settings.json");
|
|
27
28
|
const BUDDY_DIR = join(CLAUDE_DIR, "skills", "buddy");
|
|
28
|
-
const PROJECT_ROOT = resolve(
|
|
29
|
+
const PROJECT_ROOT = resolve(import.meta.dir, "../../..");
|
|
29
30
|
|
|
30
31
|
function banner() {
|
|
31
32
|
console.log(`
|
|
@@ -92,15 +93,50 @@ function preflight(): boolean {
|
|
|
92
93
|
|
|
93
94
|
// ─── Load / update settings.json ────────────────────────────────────────────
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
interface HookCommand {
|
|
97
|
+
type: "command";
|
|
98
|
+
command: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface HookMatcherEntry {
|
|
102
|
+
matcher?: string;
|
|
103
|
+
hooks?: HookCommand[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface ClaudeStatusLineConfig {
|
|
107
|
+
type: "command";
|
|
108
|
+
command: string;
|
|
109
|
+
padding: number;
|
|
110
|
+
refreshInterval: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface ClaudeSettings {
|
|
114
|
+
statusLine?: ClaudeStatusLineConfig;
|
|
115
|
+
hooks?: Partial<Record<"SessionStart" | "SessionEnd" | "PostToolUse" | "Stop" | "UserPromptSubmit", HookMatcherEntry[]>>;
|
|
116
|
+
permissions?: {
|
|
117
|
+
allow?: string[];
|
|
118
|
+
};
|
|
119
|
+
[key: string]: unknown;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface ClaudeJsonConfig {
|
|
123
|
+
mcpServers?: Record<string, { command: string; args: string[]; cwd: string }>;
|
|
124
|
+
[key: string]: unknown;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function hasBuddyHook(entry: HookMatcherEntry): boolean {
|
|
128
|
+
return (entry.hooks ?? []).some((hook) => hook.command.includes("claude-buddy"));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function loadSettings(): ClaudeSettings {
|
|
96
132
|
try {
|
|
97
|
-
return JSON.parse(readFileSync(SETTINGS_FILE, "utf8"));
|
|
133
|
+
return JSON.parse(readFileSync(SETTINGS_FILE, "utf8")) as ClaudeSettings;
|
|
98
134
|
} catch {
|
|
99
135
|
return {};
|
|
100
136
|
}
|
|
101
137
|
}
|
|
102
138
|
|
|
103
|
-
function saveSettings(settings:
|
|
139
|
+
function saveSettings(settings: ClaudeSettings) {
|
|
104
140
|
mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
105
141
|
writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + "\n");
|
|
106
142
|
}
|
|
@@ -108,12 +144,12 @@ function saveSettings(settings: Record<string, any>) {
|
|
|
108
144
|
// ─── Step 1: Register MCP server (in ~/.claude.json) ────────────────────────
|
|
109
145
|
|
|
110
146
|
function installMcp() {
|
|
111
|
-
const serverPath = join(PROJECT_ROOT, "server", "index.ts");
|
|
147
|
+
const serverPath = join(PROJECT_ROOT, "adapters", "claude", "server", "index.ts");
|
|
112
148
|
const claudeJsonPath = join(homedir(), ".claude.json");
|
|
113
149
|
|
|
114
|
-
let claudeJson:
|
|
150
|
+
let claudeJson: ClaudeJsonConfig = {};
|
|
115
151
|
try {
|
|
116
|
-
claudeJson = JSON.parse(readFileSync(claudeJsonPath, "utf8"));
|
|
152
|
+
claudeJson = JSON.parse(readFileSync(claudeJsonPath, "utf8")) as ClaudeJsonConfig;
|
|
117
153
|
} catch { /* fresh config */ }
|
|
118
154
|
|
|
119
155
|
if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
|
|
@@ -131,7 +167,7 @@ function installMcp() {
|
|
|
131
167
|
// ─── Step 2: Install skill ──────────────────────────────────────────────────
|
|
132
168
|
|
|
133
169
|
function installSkill() {
|
|
134
|
-
const srcSkill = join(PROJECT_ROOT, "skills", "buddy", "SKILL.md");
|
|
170
|
+
const srcSkill = join(PROJECT_ROOT, "adapters", "claude", "skills", "buddy", "SKILL.md");
|
|
135
171
|
mkdirSync(BUDDY_DIR, { recursive: true });
|
|
136
172
|
cpSync(srcSkill, join(BUDDY_DIR, "SKILL.md"), { force: true });
|
|
137
173
|
ok("Skill installed: ~/.claude/skills/buddy/SKILL.md");
|
|
@@ -139,8 +175,8 @@ function installSkill() {
|
|
|
139
175
|
|
|
140
176
|
// ─── Step 3: Configure status line (with animation refresh) ─────────────────
|
|
141
177
|
|
|
142
|
-
function installStatusLine(settings:
|
|
143
|
-
const statusScript = join(PROJECT_ROOT, "statusline", "buddy-status.sh");
|
|
178
|
+
function installStatusLine(settings: ClaudeSettings) {
|
|
179
|
+
const statusScript = join(PROJECT_ROOT, "adapters", "claude", "statusline", "buddy-status.sh");
|
|
144
180
|
|
|
145
181
|
settings.statusLine = {
|
|
146
182
|
type: "command",
|
|
@@ -167,15 +203,15 @@ function detectTmux(): boolean {
|
|
|
167
203
|
}
|
|
168
204
|
}
|
|
169
205
|
|
|
170
|
-
function installPopupHooks(settings:
|
|
171
|
-
const popupManager = join(PROJECT_ROOT, "popup", "popup-manager.sh");
|
|
206
|
+
function installPopupHooks(settings: ClaudeSettings) {
|
|
207
|
+
const popupManager = join(PROJECT_ROOT, "adapters", "claude", "popup", "popup-manager.sh");
|
|
172
208
|
|
|
173
209
|
if (!settings.hooks) settings.hooks = {};
|
|
174
210
|
|
|
175
211
|
// SessionStart: open popup
|
|
176
212
|
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
177
213
|
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(
|
|
178
|
-
(h
|
|
214
|
+
(h) => !hasBuddyHook(h),
|
|
179
215
|
);
|
|
180
216
|
settings.hooks.SessionStart.push({
|
|
181
217
|
hooks: [{ type: "command", command: `${popupManager} start` }],
|
|
@@ -184,7 +220,7 @@ function installPopupHooks(settings: Record<string, any>) {
|
|
|
184
220
|
// SessionEnd: close popup
|
|
185
221
|
if (!settings.hooks.SessionEnd) settings.hooks.SessionEnd = [];
|
|
186
222
|
settings.hooks.SessionEnd = settings.hooks.SessionEnd.filter(
|
|
187
|
-
(h
|
|
223
|
+
(h) => !hasBuddyHook(h),
|
|
188
224
|
);
|
|
189
225
|
settings.hooks.SessionEnd.push({
|
|
190
226
|
hooks: [{ type: "command", command: `${popupManager} stop` }],
|
|
@@ -195,17 +231,17 @@ function installPopupHooks(settings: Record<string, any>) {
|
|
|
195
231
|
|
|
196
232
|
// ─── Step 4: Register hooks ─────────────────────────────────────────────────
|
|
197
233
|
|
|
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");
|
|
234
|
+
function installHooks(settings: ClaudeSettings) {
|
|
235
|
+
const reactHook = join(PROJECT_ROOT, "adapters", "claude", "hooks", "react.sh");
|
|
236
|
+
const commentHook = join(PROJECT_ROOT, "adapters", "claude", "hooks", "buddy-comment.sh");
|
|
237
|
+
const nameHook = join(PROJECT_ROOT, "adapters", "claude", "hooks", "name-react.sh");
|
|
202
238
|
|
|
203
239
|
if (!settings.hooks) settings.hooks = {};
|
|
204
240
|
|
|
205
241
|
// PostToolUse: detect errors/test failures/successes in Bash output
|
|
206
242
|
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
207
243
|
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(
|
|
208
|
-
(h
|
|
244
|
+
(h) => !hasBuddyHook(h),
|
|
209
245
|
);
|
|
210
246
|
settings.hooks.PostToolUse.push({
|
|
211
247
|
matcher: "Bash",
|
|
@@ -215,7 +251,7 @@ function installHooks(settings: Record<string, any>) {
|
|
|
215
251
|
// Stop: extract <!-- buddy: --> comment from Claude's response
|
|
216
252
|
if (!settings.hooks.Stop) settings.hooks.Stop = [];
|
|
217
253
|
settings.hooks.Stop = settings.hooks.Stop.filter(
|
|
218
|
-
(h
|
|
254
|
+
(h) => !hasBuddyHook(h),
|
|
219
255
|
);
|
|
220
256
|
settings.hooks.Stop.push({
|
|
221
257
|
hooks: [{ type: "command", command: commentHook }],
|
|
@@ -224,7 +260,7 @@ function installHooks(settings: Record<string, any>) {
|
|
|
224
260
|
// UserPromptSubmit: detect buddy's name in user message → instant status line reaction
|
|
225
261
|
if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
|
|
226
262
|
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
|
|
227
|
-
(h
|
|
263
|
+
(h) => !hasBuddyHook(h),
|
|
228
264
|
);
|
|
229
265
|
settings.hooks.UserPromptSubmit.push({
|
|
230
266
|
hooks: [{ type: "command", command: nameHook }],
|
|
@@ -235,7 +271,7 @@ function installHooks(settings: Record<string, any>) {
|
|
|
235
271
|
|
|
236
272
|
// ─── Step 5: Ensure MCP tools are allowed ───────────────────────────────────
|
|
237
273
|
|
|
238
|
-
function ensurePermissions(settings:
|
|
274
|
+
function ensurePermissions(settings: ClaudeSettings) {
|
|
239
275
|
if (!settings.permissions) settings.permissions = {};
|
|
240
276
|
if (!settings.permissions.allow) settings.permissions.allow = [];
|
|
241
277
|
|
|
@@ -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";
|
|
@@ -6,6 +6,26 @@ import { readFileSync, writeFileSync, existsSync, rmSync, readdirSync } from "fs
|
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
import { homedir } from "os";
|
|
8
8
|
|
|
9
|
+
interface HookCommand {
|
|
10
|
+
type: "command";
|
|
11
|
+
command: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface HookMatcherEntry {
|
|
15
|
+
matcher?: string;
|
|
16
|
+
hooks?: HookCommand[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ClaudeSettings {
|
|
20
|
+
statusLine?: { command?: string };
|
|
21
|
+
hooks?: Record<string, HookMatcherEntry[]>;
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hasBuddyHook(entry: HookMatcherEntry): boolean {
|
|
26
|
+
return (entry.hooks ?? []).some((hook) => hook.command.includes("claude-buddy"));
|
|
27
|
+
}
|
|
28
|
+
|
|
9
29
|
const GREEN = "\x1b[32m";
|
|
10
30
|
const YELLOW = "\x1b[33m";
|
|
11
31
|
const NC = "\x1b[0m";
|
|
@@ -63,7 +83,7 @@ try {
|
|
|
63
83
|
|
|
64
84
|
// Remove hooks and statusline from settings.json
|
|
65
85
|
try {
|
|
66
|
-
const settings = JSON.parse(readFileSync(SETTINGS_FILE, "utf8"));
|
|
86
|
+
const settings = JSON.parse(readFileSync(SETTINGS_FILE, "utf8")) as ClaudeSettings;
|
|
67
87
|
let changed = false;
|
|
68
88
|
|
|
69
89
|
if (settings.statusLine?.command?.includes("buddy")) {
|
|
@@ -76,7 +96,7 @@ try {
|
|
|
76
96
|
if (settings.hooks?.[hookType]) {
|
|
77
97
|
const before = settings.hooks[hookType].length;
|
|
78
98
|
settings.hooks[hookType] = settings.hooks[hookType].filter(
|
|
79
|
-
(h
|
|
99
|
+
(h) => !hasBuddyHook(h),
|
|
80
100
|
);
|
|
81
101
|
if (settings.hooks[hookType].length < before) {
|
|
82
102
|
ok(`${hookType} hooks removed`);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# claude-buddy popup entry point -- runs INSIDE the tmux popup
|
|
3
|
+
#
|
|
4
|
+
# Architecture:
|
|
5
|
+
# - Render loop runs in BACKGROUND (only writes to stdout, never reads stdin)
|
|
6
|
+
# - Input forwarder runs in FOREGROUND (owns stdin exclusively)
|
|
7
|
+
#
|
|
8
|
+
# The reopen loop in popup-manager.sh handles:
|
|
9
|
+
# - ESC forwarding (when tmux closes popup on ESC)
|
|
10
|
+
# - CC pane death detection
|
|
11
|
+
# - Dynamic resizing on reopen
|
|
12
|
+
#
|
|
13
|
+
# Env vars (set by popup-manager.sh via -e):
|
|
14
|
+
# CC_PANE -- tmux pane ID for Claude Code (e.g. %0)
|
|
15
|
+
# BUDDY_DIR -- ~/.claude-buddy
|
|
16
|
+
# BUDDY_SID -- session ID (sanitized pane number, e.g. "0")
|
|
17
|
+
# Args: $1 = SID (fallback for tmux < 3.4 without -e support)
|
|
18
|
+
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
|
|
21
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
22
|
+
|
|
23
|
+
# Session ID: from env (tmux 3.4+), $1 arg, or "default"
|
|
24
|
+
BUDDY_SID="${BUDDY_SID:-${1:-default}}"
|
|
25
|
+
|
|
26
|
+
# On tmux 3.2-3.3, env vars are passed via file (no -e flag support)
|
|
27
|
+
ENV_FILE="${HOME}/.claude-buddy/popup-env.$BUDDY_SID"
|
|
28
|
+
if [ -z "${CC_PANE:-}" ] && [ -f "$ENV_FILE" ]; then
|
|
29
|
+
. "$ENV_FILE"
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
if [ -z "${CC_PANE:-}" ]; then
|
|
33
|
+
echo "Error: CC_PANE not set" >&2
|
|
34
|
+
sleep 2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# ─── Cleanup on exit ─────────────────────────────────────────────────────────
|
|
39
|
+
cleanup() {
|
|
40
|
+
[ -n "${RENDER_PID:-}" ] && kill "$RENDER_PID" 2>/dev/null
|
|
41
|
+
tput cnorm 2>/dev/null
|
|
42
|
+
stty sane 2>/dev/null
|
|
43
|
+
}
|
|
44
|
+
trap cleanup EXIT INT TERM HUP
|
|
45
|
+
|
|
46
|
+
# Hide cursor
|
|
47
|
+
tput civis 2>/dev/null
|
|
48
|
+
|
|
49
|
+
# ─── Render loop in BACKGROUND (stdout only, no stdin) ───────────────────────
|
|
50
|
+
"$SCRIPT_DIR/buddy-render.sh" </dev/null &
|
|
51
|
+
RENDER_PID=$!
|
|
52
|
+
|
|
53
|
+
# ─── Input forwarder in FOREGROUND ───────────────────────────────────────────
|
|
54
|
+
# Raw mode: all bytes pass through without terminal interpretation.
|
|
55
|
+
# No SIGINT on Ctrl-C, no EOF on Ctrl-D, no CR-to-NL on Enter.
|
|
56
|
+
stty raw -echo 2>/dev/null
|
|
57
|
+
|
|
58
|
+
# Use perl for raw byte forwarding. bash's read -n1 internally overrides
|
|
59
|
+
# terminal settings on each call (saves/restores tty mode), which undoes
|
|
60
|
+
# stty raw and re-enables signal processing. perl's sysread doesn't touch
|
|
61
|
+
# the terminal at all -- it reads raw bytes from the file descriptor.
|
|
62
|
+
#
|
|
63
|
+
# Batching: first byte is a blocking read, then non-blocking drain of any
|
|
64
|
+
# remaining bytes (paste arrives as a burst). The batch is sent to CC in
|
|
65
|
+
# a single tmux send-keys call for efficiency.
|
|
66
|
+
exec perl -e '
|
|
67
|
+
use Fcntl qw(F_GETFL F_SETFL O_NONBLOCK);
|
|
68
|
+
my $pane = $ENV{CC_PANE};
|
|
69
|
+
while (1) {
|
|
70
|
+
my $buf;
|
|
71
|
+
my $n = sysread(STDIN, $buf, 1);
|
|
72
|
+
last unless $n;
|
|
73
|
+
# Non-blocking drain for paste batching
|
|
74
|
+
my $flags = fcntl(STDIN, F_GETFL, 0);
|
|
75
|
+
fcntl(STDIN, F_SETFL, $flags | O_NONBLOCK);
|
|
76
|
+
while (sysread(STDIN, my $more, 4096)) {
|
|
77
|
+
$buf .= $more;
|
|
78
|
+
}
|
|
79
|
+
fcntl(STDIN, F_SETFL, $flags);
|
|
80
|
+
|
|
81
|
+
# F12 (\e[24~) = close popup and enter scroll mode
|
|
82
|
+
if ($buf =~ /\e\[24~/) {
|
|
83
|
+
my $buddy_dir = $ENV{BUDDY_DIR} || "$ENV{HOME}/.claude-buddy";
|
|
84
|
+
my $sid = $ENV{BUDDY_SID} // "default";
|
|
85
|
+
open(my $fh, ">", "$buddy_dir/popup-scroll.$sid");
|
|
86
|
+
close($fh) if $fh;
|
|
87
|
+
exit 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
system("tmux", "send-keys", "-t", $pane, "-l", "--", $buf);
|
|
91
|
+
}
|
|
92
|
+
'
|