@ramarivera/coding-buddy 0.4.0-alpha.7 → 0.4.0-alpha.9

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 (43) hide show
  1. package/README.md +18 -39
  2. package/adapters/claude/hooks/buddy-comment.sh +4 -1
  3. package/adapters/claude/hooks/name-react.sh +4 -1
  4. package/adapters/claude/hooks/react.sh +4 -1
  5. package/adapters/claude/install/backup.ts +36 -118
  6. package/adapters/claude/install/disable.ts +9 -14
  7. package/adapters/claude/install/doctor.ts +26 -87
  8. package/adapters/claude/install/install.ts +39 -66
  9. package/adapters/claude/install/test-statusline.ts +8 -18
  10. package/adapters/claude/install/uninstall.ts +18 -26
  11. package/adapters/claude/plugin/marketplace.json +4 -4
  12. package/adapters/claude/plugin/plugin.json +3 -5
  13. package/adapters/claude/server/index.ts +132 -5
  14. package/adapters/claude/server/path.ts +12 -0
  15. package/adapters/claude/skills/buddy/SKILL.md +16 -1
  16. package/adapters/claude/statusline/buddy-status.sh +22 -3
  17. package/adapters/claude/storage/paths.ts +9 -0
  18. package/adapters/claude/storage/settings.ts +53 -3
  19. package/adapters/claude/storage/state.ts +22 -4
  20. package/adapters/pi/README.md +19 -0
  21. package/adapters/pi/events.ts +176 -19
  22. package/adapters/pi/index.ts +3 -1
  23. package/adapters/pi/logger.ts +52 -0
  24. package/adapters/pi/prompt.ts +18 -0
  25. package/adapters/pi/storage.ts +1 -0
  26. package/cli/biomes.ts +309 -0
  27. package/cli/buddy-shell.ts +818 -0
  28. package/cli/index.ts +7 -0
  29. package/cli/tui.tsx +2244 -0
  30. package/cli/upgrade.ts +213 -0
  31. package/core/model.ts +6 -0
  32. package/package.json +78 -62
  33. package/scripts/paths.sh +40 -0
  34. package/server/achievements.ts +15 -0
  35. package/server/art.ts +1 -0
  36. package/server/engine.ts +1 -0
  37. package/server/mcp-launcher.sh +16 -0
  38. package/server/path.ts +30 -0
  39. package/server/reactions.ts +1 -0
  40. package/server/state.ts +3 -0
  41. package/adapters/claude/popup/buddy-popup.sh +0 -92
  42. package/adapters/claude/popup/buddy-render.sh +0 -540
  43. package/adapters/claude/popup/popup-manager.sh +0 -355
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}claude-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 claude-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);
package/core/model.ts CHANGED
@@ -26,6 +26,11 @@ export interface ReactionState {
26
26
  reason: string;
27
27
  }
28
28
 
29
+ export interface BuddyTurnCommentModelConfig {
30
+ provider: string;
31
+ model: string;
32
+ }
33
+
29
34
  export interface BuddyConfig {
30
35
  commentCooldown: number;
31
36
  reactionTTL: number;
@@ -33,6 +38,7 @@ export interface BuddyConfig {
33
38
  bubblePosition: "top" | "left";
34
39
  showRarity: boolean;
35
40
  statusLineEnabled: boolean;
41
+ turnCommentModel?: BuddyTurnCommentModelConfig;
36
42
  }
37
43
 
38
44
  export interface GlobalCounters {
package/package.json CHANGED
@@ -1,64 +1,80 @@
1
1
  {
2
- "name": "@ramarivera/coding-buddy",
3
- "version": "0.4.0-alpha.7",
4
- "description": "Persistent coding companion for Claude Code and pi",
5
- "type": "module",
6
- "bin": {
7
- "coding-buddy": "./cli/index.ts"
8
- },
9
- "scripts": {
10
- "server": "bun run adapters/claude/server/index.ts",
11
- "pick": "bun run adapters/claude/install/pick.ts",
12
- "hunt": "bun run adapters/claude/install/hunt.ts",
13
- "install-buddy": "bun run adapters/claude/install/install.ts",
14
- "show": "bun run adapters/claude/install/show.ts",
15
- "doctor": "bun run adapters/claude/install/doctor.ts",
16
- "test-statusline": "bun run adapters/claude/install/test-statusline.ts",
17
- "backup": "bun run adapters/claude/install/backup.ts",
18
- "settings": "bun run adapters/claude/install/settings.ts",
19
- "disable": "bun run adapters/claude/install/disable.ts",
20
- "enable": "bun run adapters/claude/install/install.ts",
21
- "uninstall": "bun run adapters/claude/install/uninstall.ts",
22
- "help": "bun run cli/index.ts help",
23
- "test": "bun test",
24
- "typecheck": "tsc --noEmit"
25
- },
26
- "files": [
27
- "core/",
28
- "adapters/",
29
- "cli/",
30
- "!**/*.test.ts"
31
- ],
32
- "keywords": [
33
- "claude-code",
34
- "buddy",
35
- "companion",
36
- "mcp",
37
- "terminal-pet",
38
- "tamagotchi",
39
- "pi-package"
40
- ],
41
- "repository": {
42
- "type": "git",
43
- "url": "git+https://github.com/ramarivera/claude-buddy.git"
44
- },
45
- "homepage": "https://github.com/ramarivera/claude-buddy#readme",
46
- "license": "MIT",
47
- "publishConfig": {
48
- "access": "public",
49
- "registry": "https://registry.npmjs.org/"
50
- },
51
- "pi": {
52
- "extensions": [
53
- "./adapters/pi/index.ts"
54
- ]
55
- },
56
- "dependencies": {
57
- "@mariozechner/pi-coding-agent": "0.66.1",
58
- "@modelcontextprotocol/sdk": "^1.12.1"
59
- },
60
- "devDependencies": {
61
- "bun-types": "^1.3.11",
62
- "typescript": "^6.0.2"
63
- }
2
+ "name": "@ramarivera/coding-buddy",
3
+ "version": "0.4.0-alpha.9",
4
+ "description": "Persistent coding companion for Claude Code and pi",
5
+ "type": "module",
6
+ "bin": {
7
+ "coding-buddy": "./cli/index.ts"
8
+ },
9
+ "scripts": {
10
+ "server": "bun run adapters/claude/server/index.ts",
11
+ "pick": "bun run adapters/claude/install/pick.ts",
12
+ "hunt": "bun run adapters/claude/install/hunt.ts",
13
+ "install-buddy": "bun run adapters/claude/install/install.ts",
14
+ "show": "bun run adapters/claude/install/show.ts",
15
+ "doctor": "bun run adapters/claude/install/doctor.ts",
16
+ "test-statusline": "bun run adapters/claude/install/test-statusline.ts",
17
+ "backup": "bun run adapters/claude/install/backup.ts",
18
+ "settings": "bun run adapters/claude/install/settings.ts",
19
+ "disable": "bun run adapters/claude/install/disable.ts",
20
+ "enable": "bun run adapters/claude/install/install.ts",
21
+ "upgrade": "bun run cli/upgrade.ts",
22
+ "uninstall": "bun run adapters/claude/install/uninstall.ts",
23
+ "help": "bun run cli/index.ts help",
24
+ "buddy-shell": "npx tsx cli/buddy-shell.ts",
25
+ "tui": "bun run cli/tui.tsx",
26
+ "test": "bun test",
27
+ "typecheck": "tsc --noEmit"
28
+ },
29
+ "files": [
30
+ "core/",
31
+ "adapters/",
32
+ "cli/",
33
+ "scripts/",
34
+ "server/",
35
+ "!**/*.test.ts"
36
+ ],
37
+ "keywords": [
38
+ "claude-code",
39
+ "buddy",
40
+ "companion",
41
+ "mcp",
42
+ "terminal-pet",
43
+ "tamagotchi",
44
+ "pi-package"
45
+ ],
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/ramarivera/claude-buddy.git"
49
+ },
50
+ "homepage": "https://github.com/ramarivera/claude-buddy#readme",
51
+ "license": "MIT",
52
+ "publishConfig": {
53
+ "access": "public",
54
+ "registry": "https://registry.npmjs.org/"
55
+ },
56
+ "pi": {
57
+ "extensions": [
58
+ "./adapters/pi/index.ts"
59
+ ]
60
+ },
61
+ "dependencies": {
62
+ "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
63
+ "@mariozechner/pi-coding-agent": "0.66.1",
64
+ "@modelcontextprotocol/sdk": "^1.12.1",
65
+ "@xterm/addon-serialize": "^0.14.0",
66
+ "@xterm/headless": "^6.0.0",
67
+ "ink": "^7.0.0",
68
+ "pino": "^10.3.1",
69
+ "react": "^19.2.5"
70
+ },
71
+ "devDependencies": {
72
+ "@types/react": "^19.2.14",
73
+ "bun-types": "^1.3.11",
74
+ "tsx": "^4.21.0",
75
+ "typescript": "^6.0.2"
76
+ },
77
+ "trustedDependencies": [
78
+ "@homebridge/node-pty-prebuilt-multiarch"
79
+ ]
64
80
  }
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env bash
2
+ # Path resolvers for claude-buddy shell scripts.
3
+ #
4
+ # Must stay in sync with server/path.ts. Source this file early:
5
+ # source "$(dirname "$0")/../scripts/paths.sh"
6
+ # …and consumers get BUDDY_STATE_DIR, CLAUDE_CFG_DIR, CLAUDE_SETTINGS_FILE,
7
+ # and CLAUDE_USER_CONFIG.
8
+ #
9
+ # Resolution rules (must match server/path.ts):
10
+ # - If CLAUDE_CONFIG_DIR is set → everything lives under it
11
+ # (settings.json, skills/, .claude.json inside the config dir, and
12
+ # buddy state at $CLAUDE_CONFIG_DIR/buddy-state).
13
+ # - Else (single-profile default) → $HOME/.claude, $HOME/.claude.json,
14
+ # $HOME/.claude-buddy.
15
+
16
+ if [[ -n "${CLAUDE_CONFIG_DIR:-}" ]]; then
17
+ CLAUDE_CFG_DIR="$CLAUDE_CONFIG_DIR"
18
+ else
19
+ CLAUDE_CFG_DIR="$HOME/.claude"
20
+ fi
21
+
22
+ CLAUDE_SETTINGS_FILE="$CLAUDE_CFG_DIR/settings.json"
23
+
24
+ # .claude.json: inside CLAUDE_CONFIG_DIR when set, else $HOME. We never
25
+ # fall back to $HOME when CLAUDE_CONFIG_DIR is set — doing so would break
26
+ # profile isolation (enabling buddy in one profile could mutate the
27
+ # home-level file that a different profile reads).
28
+ if [[ -n "${CLAUDE_CONFIG_DIR:-}" ]]; then
29
+ CLAUDE_USER_CONFIG="$CLAUDE_CONFIG_DIR/.claude.json"
30
+ else
31
+ CLAUDE_USER_CONFIG="$HOME/.claude.json"
32
+ fi
33
+
34
+ if [[ -n "${CLAUDE_CONFIG_DIR:-}" ]]; then
35
+ BUDDY_STATE_DIR="$CLAUDE_CONFIG_DIR/buddy-state"
36
+ else
37
+ BUDDY_STATE_DIR="$HOME/.claude-buddy"
38
+ fi
39
+
40
+ export CLAUDE_CFG_DIR CLAUDE_SETTINGS_FILE CLAUDE_USER_CONFIG BUDDY_STATE_DIR
@@ -0,0 +1,15 @@
1
+ export {
2
+ ACHIEVEMENTS,
3
+ type Achievement,
4
+ type EventCounters,
5
+ } from "../core/achievements.ts";
6
+ export type { UnlockedAchievement } from "../core/model.ts";
7
+ export {
8
+ loadUnlocked,
9
+ loadEvents,
10
+ incrementEvent,
11
+ checkAndAward,
12
+ trackActiveDay,
13
+ renderAchievementsCard,
14
+ renderAchievementsCardMarkdown,
15
+ } from "../adapters/claude/storage/achievements.ts";
package/server/art.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "../core/render-model.ts";
@@ -0,0 +1 @@
1
+ export * from "../core/engine.ts";
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env bash
2
+ set -eu
3
+ if ! command -v bun >/dev/null 2>&1; then
4
+ cat >&2 <<'MSG'
5
+ [claude-buddy] ERROR: 'bun' was not found on PATH.
6
+
7
+ claude-buddy's MCP server runs on bun. Install it with:
8
+
9
+ curl -fsSL https://bun.sh/install | bash
10
+
11
+ Then open a new shell and restart Claude Code.
12
+ MSG
13
+ exit 127
14
+ fi
15
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
16
+ exec bun "$SCRIPT_DIR/../adapters/claude/server/index.ts"
package/server/path.ts ADDED
@@ -0,0 +1,30 @@
1
+ import {
2
+ getBuddySkillDir,
3
+ getBuddyStateDir,
4
+ getClaudeConfigDir,
5
+ getClaudeJsonPath,
6
+ getClaudeSettingsPath,
7
+ toUnixPath,
8
+ } from "../adapters/claude/storage/paths.ts";
9
+
10
+ export function claudeConfigDir(): string {
11
+ return getClaudeConfigDir();
12
+ }
13
+
14
+ export function claudeSettingsPath(): string {
15
+ return getClaudeSettingsPath();
16
+ }
17
+
18
+ export function claudeUserConfigPath(): string {
19
+ return getClaudeJsonPath();
20
+ }
21
+
22
+ export function buddyStateDir(): string {
23
+ return getBuddyStateDir();
24
+ }
25
+
26
+ export function claudeSkillDir(_name: string): string {
27
+ return getBuddySkillDir();
28
+ }
29
+
30
+ export { toUnixPath };
@@ -0,0 +1 @@
1
+ export * from "../core/reactions.ts";
@@ -0,0 +1,3 @@
1
+ export * from "../adapters/claude/storage/state.ts";
2
+ export { resolveUserId } from "../adapters/claude/storage/identity.ts";
3
+ export { setBuddyStatusLine, unsetBuddyStatusLine, cleanupPluginState } from "../adapters/claude/storage/settings.ts";
@@ -1,92 +0,0 @@
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
- '