@ramarivera/coding-buddy 0.6.0 → 0.7.1
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/.claude-plugin/marketplace.json +6 -6
- package/.claude-plugin/plugin.json +4 -4
- package/README.md +12 -11
- package/adapters/omp/renderers.ts +61 -4
- package/adapters/omp/ui.ts +1 -1
- package/cli/install.ts +12 -6
- package/cli/pick.ts +26 -8
- package/cli/upgrade.ts +44 -11
- package/hooks/buddy-comment.sh +10 -85
- package/hooks/file-type-react.sh +10 -400
- package/hooks/hooks.json +19 -5
- package/hooks/legacy/file-type-react.sh +403 -0
- package/hooks/legacy/mood-react.sh +210 -0
- package/hooks/legacy/name-react.sh +178 -0
- package/hooks/legacy/react.sh +1364 -0
- package/hooks/mood-react.sh +10 -207
- package/hooks/name-react.sh +10 -178
- package/hooks/react.sh +10 -1367
- package/hooks/suggest.sh +10 -54
- package/package.json +3 -3
- package/scripts/paths.sh +16 -1
- package/server/art.ts +1 -1
- package/server/hooks/buddy-comment.ts +93 -0
- package/server/hooks/common.ts +95 -0
- package/server/hooks/legacy-bash.ts +34 -0
- package/server/hooks/suggest.ts +60 -0
- package/server/state.ts +14 -2
- package/statusline/buddy-status.sh +20 -12
- package/statusline/combined-status.sh +9 -8
package/hooks/suggest.sh
CHANGED
|
@@ -1,55 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
LAST_SUGGESTION_FILE="$STATE_DIR/.last_suggestion"
|
|
13
|
-
SUGGESTIONS_FILE="$STATE_DIR/suggestions.json"
|
|
14
|
-
|
|
15
|
-
[ -f "$CONFIG_FILE" ] || exit 0
|
|
16
|
-
|
|
17
|
-
# Check if suggestions are enabled (default: true)
|
|
18
|
-
SUGGESTIONS_ENABLED="true"
|
|
19
|
-
if [ -f "$CONFIG_FILE" ]; then
|
|
20
|
-
_enabled=$(jq -r '.suggestionsEnabled // true' "$CONFIG_FILE" 2>/dev/null)
|
|
21
|
-
[ "$_enabled" = "false" ] && exit 0
|
|
22
|
-
fi
|
|
23
|
-
|
|
24
|
-
# Check cooldown (default: 180s = 3 min)
|
|
25
|
-
SUGGESTION_COOLDOWN=180
|
|
26
|
-
if [ -f "$CONFIG_FILE" ]; then
|
|
27
|
-
_cd=$(jq -r '.suggestionCooldown // 180' "$CONFIG_FILE" 2>/dev/null || echo 180)
|
|
28
|
-
[[ "$_cd" =~ ^[0-9]+$ ]] && SUGGESTION_COOLDOWN=$_cd
|
|
29
|
-
fi
|
|
30
|
-
|
|
31
|
-
if [ -f "$LAST_SUGGESTION_FILE" ]; then
|
|
32
|
-
LAST=$(cat "$LAST_SUGGESTION_FILE" 2>/dev/null)
|
|
33
|
-
NOW=$(date +%s)
|
|
34
|
-
DIFF=$(( NOW - ${LAST:-0} ))
|
|
35
|
-
[ "$DIFF" -lt "$SUGGESTION_COOLDOWN" ] && exit 0
|
|
36
|
-
fi
|
|
37
|
-
|
|
38
|
-
INPUT=$(cat)
|
|
39
|
-
|
|
40
|
-
# Extract messages
|
|
41
|
-
ASSISTANT_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""' 2>/dev/null)
|
|
42
|
-
[ -z "$ASSISTANT_MSG" ] && exit 0
|
|
43
|
-
|
|
44
|
-
# Record turn and check patterns
|
|
45
|
-
PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
46
|
-
|
|
47
|
-
# Check for patterns via the suggestions script
|
|
48
|
-
if [ -x "$(command -v bun)" ]; then
|
|
49
|
-
# Run the suggestion check (async, non-blocking)
|
|
50
|
-
bun run "$PLUGIN_ROOT/server/check-suggestions.ts" \
|
|
51
|
-
"$(echo "$ASSISTANT_MSG" | jq -Rs .)" \
|
|
52
|
-
>/dev/null 2>&1 &
|
|
53
|
-
fi
|
|
54
|
-
|
|
55
|
-
exit 0
|
|
2
|
+
set +e
|
|
3
|
+
SCRIPT_DIR="${BASH_SOURCE[0]%/*}"
|
|
4
|
+
ROOT="$(cd "$SCRIPT_DIR/.." 2>/dev/null && pwd)" || exit 0
|
|
5
|
+
[ -n "$ROOT" ] || exit 0
|
|
6
|
+
BUN="$(command -v bun 2>/dev/null || true)"
|
|
7
|
+
[ -x "$BUN" ] || BUN="$HOME/.bun/bin/bun"
|
|
8
|
+
[ -x "$BUN" ] || BUN="/opt/homebrew/bin/bun"
|
|
9
|
+
[ -x "$BUN" ] || BUN="/usr/local/bin/bun"
|
|
10
|
+
[ -x "$BUN" ] || exit 0
|
|
11
|
+
exec "$BUN" run "$ROOT/server/hooks/suggest.ts" || exit 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ramarivera/coding-buddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Persistent coding companion for Claude Code, Pi, and Oh My Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"omp": {
|
|
@@ -66,9 +66,9 @@
|
|
|
66
66
|
],
|
|
67
67
|
"repository": {
|
|
68
68
|
"type": "git",
|
|
69
|
-
"url": "git+https://github.com/
|
|
69
|
+
"url": "git+https://github.com/ramarivera/coding-buddy.git"
|
|
70
70
|
},
|
|
71
|
-
"homepage": "https://github.com/
|
|
71
|
+
"homepage": "https://github.com/ramarivera/coding-buddy#readme",
|
|
72
72
|
"license": "MIT",
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
|
package/scripts/paths.sh
CHANGED
|
@@ -37,7 +37,22 @@ else
|
|
|
37
37
|
BUDDY_STATE_DIR="$HOME/.claude-buddy"
|
|
38
38
|
fi
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
# Per-session ID for namespacing transient buddy state files
|
|
41
|
+
# (reaction.$SID.json, .last_reaction.$SID, etc.). Precedence:
|
|
42
|
+
# 1. $CLAUDE_CODE_SESSION_ID — Claude Code sets a UUID per session and
|
|
43
|
+
# propagates it to every child process (hooks, MCP server, statusline).
|
|
44
|
+
# Shortened to first 8 chars for filename hygiene.
|
|
45
|
+
# 2. $TMUX_PANE — legacy tmux-pane isolation, kept as a fallback.
|
|
46
|
+
# 3. "default" — single-session world.
|
|
47
|
+
if [[ -n "${CLAUDE_CODE_SESSION_ID:-}" ]]; then
|
|
48
|
+
BUDDY_SID="${CLAUDE_CODE_SESSION_ID:0:8}"
|
|
49
|
+
elif [[ -n "${TMUX_PANE:-}" ]]; then
|
|
50
|
+
BUDDY_SID="${TMUX_PANE#%}"
|
|
51
|
+
else
|
|
52
|
+
BUDDY_SID="default"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
export CLAUDE_CFG_DIR CLAUDE_SETTINGS_FILE CLAUDE_USER_CONFIG BUDDY_STATE_DIR BUDDY_SID
|
|
41
56
|
|
|
42
57
|
# Windows: Chocolatey installs a PowerShell shim for jq that doesn't work in
|
|
43
58
|
# Git Bash. If jq is not found on PATH, check the real Chocolatey binary path
|
package/server/art.ts
CHANGED
|
@@ -116,7 +116,7 @@ export const SPECIES_ART: Record<Species, string[][]> = {
|
|
|
116
116
|
"\\ {E}' '{E} /",
|
|
117
117
|
" \\ } { /",
|
|
118
118
|
" ≈(° °)≈",
|
|
119
|
-
" \x1b[38;2;255;120;0m
|
|
119
|
+
" \x1b[38;2;255;120;0m/\x1b[0m'\x1b[38;2;255;120;0m|\x1b[0m'\x1b[38;2;255;120;0m\\\x1b[0m"],
|
|
120
120
|
["} {",
|
|
121
121
|
"|\\^```^/|",
|
|
122
122
|
"\\ {E}' '{E} /",
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import {
|
|
6
|
+
defaultSpawnDetached,
|
|
7
|
+
fileExists,
|
|
8
|
+
isOnCooldown,
|
|
9
|
+
nonNegativeInteger,
|
|
10
|
+
parseHookInput,
|
|
11
|
+
readJsonFile,
|
|
12
|
+
readStdin,
|
|
13
|
+
resolveHookSessionId,
|
|
14
|
+
resolveHookStateDir,
|
|
15
|
+
stringField,
|
|
16
|
+
type HookRuntime,
|
|
17
|
+
} from "./common.ts";
|
|
18
|
+
|
|
19
|
+
interface BuddyConfig {
|
|
20
|
+
commentCooldown?: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Events {
|
|
24
|
+
turns?: number;
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const BUDDY_COMMENT_PATTERN = /<!--\s*buddy:\s*([\s\S]*?)\s*-->/g;
|
|
29
|
+
|
|
30
|
+
export interface BuddyCommentResult {
|
|
31
|
+
comment?: string;
|
|
32
|
+
updated: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function extractBuddyComment(message: string): string {
|
|
36
|
+
let comment = "";
|
|
37
|
+
for (const match of message.matchAll(BUDDY_COMMENT_PATTERN)) {
|
|
38
|
+
const candidate = match[1]?.trim();
|
|
39
|
+
if (candidate) comment = candidate;
|
|
40
|
+
}
|
|
41
|
+
return comment;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function handleBuddyComment(rawInput: string, runtime: HookRuntime = {}): BuddyCommentResult {
|
|
45
|
+
const stateDir = resolveHookStateDir(runtime);
|
|
46
|
+
if (!fileExists(join(stateDir, "status.json"))) return { updated: false };
|
|
47
|
+
|
|
48
|
+
const input = parseHookInput(rawInput);
|
|
49
|
+
if (!input) return { updated: false };
|
|
50
|
+
|
|
51
|
+
const assistantMessage = stringField(input, "last_assistant_message");
|
|
52
|
+
if (!assistantMessage) return { updated: false };
|
|
53
|
+
|
|
54
|
+
const comment = extractBuddyComment(assistantMessage);
|
|
55
|
+
if (!comment) return { updated: false };
|
|
56
|
+
|
|
57
|
+
const now = runtime.now?.() ?? Date.now();
|
|
58
|
+
const sid = resolveHookSessionId(runtime);
|
|
59
|
+
const config = readJsonFile<BuddyConfig>(join(stateDir, "config.json")) ?? {};
|
|
60
|
+
const cooldown = nonNegativeInteger(config.commentCooldown, 30);
|
|
61
|
+
const cooldownFile = join(stateDir, `.last_comment.${sid}`);
|
|
62
|
+
if (isOnCooldown(cooldownFile, cooldown, now)) return { updated: false };
|
|
63
|
+
|
|
64
|
+
mkdirSync(stateDir, { recursive: true });
|
|
65
|
+
writeFileSync(cooldownFile, String(Math.floor(now / 1000)));
|
|
66
|
+
writeFileSync(
|
|
67
|
+
join(stateDir, `reaction.${sid}.json`),
|
|
68
|
+
JSON.stringify({ reaction: comment, timestamp: now, reason: "turn" }),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const eventsFile = join(stateDir, "events.json");
|
|
72
|
+
const events = readJsonFile<Events>(eventsFile) ?? {};
|
|
73
|
+
events.turns = (typeof events.turns === "number" ? events.turns : 0) + 1;
|
|
74
|
+
writeFileSync(eventsFile, JSON.stringify(events, null, 2));
|
|
75
|
+
|
|
76
|
+
const spawnDetached = runtime.spawnDetached ?? defaultSpawnDetached(runtime);
|
|
77
|
+
spawnDetached("server/award-xp.ts", ["turn"]);
|
|
78
|
+
spawnDetached("server/consolidate.ts", [
|
|
79
|
+
assistantMessage,
|
|
80
|
+
stringField(input, "last_user_message"),
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
return { comment, updated: true };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (import.meta.main) {
|
|
87
|
+
try {
|
|
88
|
+
handleBuddyComment(await readStdin());
|
|
89
|
+
} catch {
|
|
90
|
+
// Claude Code hooks must never fail the turn.
|
|
91
|
+
}
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { join, resolve } from "path";
|
|
4
|
+
import { buddyStateDir } from "../path.ts";
|
|
5
|
+
|
|
6
|
+
export type JsonObject = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
export interface HookRuntime {
|
|
9
|
+
now?: () => number;
|
|
10
|
+
packageRoot?: string;
|
|
11
|
+
sessionId?: string;
|
|
12
|
+
spawnDetached?: (script: string, args: string[]) => void;
|
|
13
|
+
stateDir?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseHookInput(raw: string): JsonObject | null {
|
|
17
|
+
try {
|
|
18
|
+
const value = JSON.parse(raw) as unknown;
|
|
19
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
20
|
+
? value as JsonObject
|
|
21
|
+
: null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function stringField(input: JsonObject, field: string): string {
|
|
28
|
+
const value = input[field];
|
|
29
|
+
return typeof value === "string" ? value : "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveHookStateDir(runtime: HookRuntime = {}): string {
|
|
33
|
+
return runtime.stateDir ?? buddyStateDir();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resolveHookSessionId(runtime: HookRuntime = {}): string {
|
|
37
|
+
if (runtime.sessionId) return runtime.sessionId;
|
|
38
|
+
const ccSid = process.env.CLAUDE_CODE_SESSION_ID;
|
|
39
|
+
if (ccSid) return ccSid.slice(0, 8);
|
|
40
|
+
const pane = process.env.TMUX_PANE;
|
|
41
|
+
if (pane) return pane.replace(/^%/, "");
|
|
42
|
+
return "default";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function readJsonFile<T>(path: string): T | null {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(readFileSync(path, "utf8")) as T;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function nonNegativeInteger(value: unknown, fallback: number): number {
|
|
54
|
+
if (Number.isInteger(value) && Number(value) >= 0) return Number(value);
|
|
55
|
+
if (typeof value === "string" && /^[0-9]+$/.test(value)) return Number(value);
|
|
56
|
+
return fallback;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isOnCooldown(path: string, cooldownSeconds: number, nowMs: number): boolean {
|
|
60
|
+
try {
|
|
61
|
+
const last = Number.parseInt(readFileSync(path, "utf8").trim(), 10);
|
|
62
|
+
if (!Number.isFinite(last)) return false;
|
|
63
|
+
return Math.floor(nowMs / 1000) - last < cooldownSeconds;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function packageRootFromHooks(): string {
|
|
70
|
+
return resolve(import.meta.dir, "..", "..");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function defaultSpawnDetached(runtime: HookRuntime = {}): (script: string, args: string[]) => void {
|
|
74
|
+
return (script, args) => {
|
|
75
|
+
const root = runtime.packageRoot ?? packageRootFromHooks();
|
|
76
|
+
const child = spawn(process.execPath, ["run", join(root, script), ...args], {
|
|
77
|
+
cwd: root,
|
|
78
|
+
detached: true,
|
|
79
|
+
stdio: "ignore",
|
|
80
|
+
});
|
|
81
|
+
child.unref();
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function fileExists(path: string): boolean {
|
|
86
|
+
try {
|
|
87
|
+
return existsSync(path);
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function readStdin(): Promise<string> {
|
|
94
|
+
return await Bun.stdin.text();
|
|
95
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "child_process";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { packageRootFromHooks, readStdin } from "./common.ts";
|
|
7
|
+
|
|
8
|
+
const ALLOWED = new Set([
|
|
9
|
+
"react.sh",
|
|
10
|
+
"file-type-react.sh",
|
|
11
|
+
"name-react.sh",
|
|
12
|
+
"mood-react.sh",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
if (import.meta.main) {
|
|
16
|
+
try {
|
|
17
|
+
const script = process.argv[2] ?? "";
|
|
18
|
+
if (ALLOWED.has(script)) {
|
|
19
|
+
const root = packageRootFromHooks();
|
|
20
|
+
const hook = join(root, "hooks", "legacy", script);
|
|
21
|
+
if (existsSync(hook)) {
|
|
22
|
+
spawnSync("bash", [hook], {
|
|
23
|
+
cwd: root,
|
|
24
|
+
input: await readStdin(),
|
|
25
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
26
|
+
timeout: 14_000,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// Claude Code hooks must never fail the turn.
|
|
32
|
+
}
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import {
|
|
5
|
+
defaultSpawnDetached,
|
|
6
|
+
fileExists,
|
|
7
|
+
isOnCooldown,
|
|
8
|
+
nonNegativeInteger,
|
|
9
|
+
parseHookInput,
|
|
10
|
+
readJsonFile,
|
|
11
|
+
readStdin,
|
|
12
|
+
resolveHookStateDir,
|
|
13
|
+
stringField,
|
|
14
|
+
type HookRuntime,
|
|
15
|
+
} from "./common.ts";
|
|
16
|
+
|
|
17
|
+
interface SuggestConfig {
|
|
18
|
+
suggestionCooldown?: unknown;
|
|
19
|
+
suggestionsEnabled?: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SuggestHookResult {
|
|
23
|
+
checked: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function handleSuggest(rawInput: string, runtime: HookRuntime = {}): SuggestHookResult {
|
|
27
|
+
const stateDir = resolveHookStateDir(runtime);
|
|
28
|
+
const configFile = join(stateDir, "config.json");
|
|
29
|
+
if (!fileExists(configFile)) return { checked: false };
|
|
30
|
+
|
|
31
|
+
const config = readJsonFile<SuggestConfig>(configFile) ?? {};
|
|
32
|
+
if (config.suggestionsEnabled === false || config.suggestionsEnabled === "false") {
|
|
33
|
+
return { checked: false };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const now = runtime.now?.() ?? Date.now();
|
|
37
|
+
const cooldown = nonNegativeInteger(config.suggestionCooldown, 180);
|
|
38
|
+
if (isOnCooldown(join(stateDir, ".last_suggestion"), cooldown, now)) {
|
|
39
|
+
return { checked: false };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const input = parseHookInput(rawInput);
|
|
43
|
+
if (!input) return { checked: false };
|
|
44
|
+
|
|
45
|
+
const assistantMessage = stringField(input, "last_assistant_message");
|
|
46
|
+
if (!assistantMessage) return { checked: false };
|
|
47
|
+
|
|
48
|
+
const spawnDetached = runtime.spawnDetached ?? defaultSpawnDetached(runtime);
|
|
49
|
+
spawnDetached("server/check-suggestions.ts", [JSON.stringify(assistantMessage)]);
|
|
50
|
+
return { checked: true };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (import.meta.main) {
|
|
54
|
+
try {
|
|
55
|
+
handleSuggest(await readStdin());
|
|
56
|
+
} catch {
|
|
57
|
+
// Claude Code hooks must never fail the turn.
|
|
58
|
+
}
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
package/server/state.ts
CHANGED
|
@@ -42,9 +42,20 @@ export const STATE_DIR = buddyStateDir();
|
|
|
42
42
|
const MANIFEST_FILE = join(STATE_DIR, "menagerie.json");
|
|
43
43
|
const CONFIG_FILE = join(STATE_DIR, "config.json");
|
|
44
44
|
|
|
45
|
-
// ─── Session ID (
|
|
45
|
+
// ─── Session ID (Claude Code session isolation, tmux pane fallback) ─────────
|
|
46
|
+
//
|
|
47
|
+
// Precedence:
|
|
48
|
+
// 1. $CLAUDE_CODE_SESSION_ID — Claude Code sets a UUID per session and
|
|
49
|
+
// propagates it to the MCP server, hooks, and statusline. Shortened
|
|
50
|
+
// to first 8 chars to keep filenames readable.
|
|
51
|
+
// 2. $TMUX_PANE — legacy tmux-pane isolation kept as a fallback.
|
|
52
|
+
// 3. "default" — single-session world.
|
|
53
|
+
//
|
|
54
|
+
// Must match the bash equivalent in scripts/paths.sh.
|
|
46
55
|
|
|
47
56
|
function sessionId(): string {
|
|
57
|
+
const ccSid = process.env.CLAUDE_CODE_SESSION_ID;
|
|
58
|
+
if (ccSid && ccSid.length > 0) return ccSid.slice(0, 8);
|
|
48
59
|
const pane = process.env.TMUX_PANE;
|
|
49
60
|
if (!pane) return "default";
|
|
50
61
|
return pane.replace(/^%/, "");
|
|
@@ -416,7 +427,7 @@ export function writeStatusState(
|
|
|
416
427
|
eye: companion.bones.eye,
|
|
417
428
|
shiny: companion.bones.shiny,
|
|
418
429
|
hat: companion.bones.hat,
|
|
419
|
-
reaction:
|
|
430
|
+
reaction: "",
|
|
420
431
|
muted: muted ?? false,
|
|
421
432
|
achievement: achievement ?? "",
|
|
422
433
|
frames,
|
|
@@ -426,6 +437,7 @@ export function writeStatusState(
|
|
|
426
437
|
mood: moodStr,
|
|
427
438
|
};
|
|
428
439
|
writeFileSync(join(STATE_DIR, "status.json"), JSON.stringify(state));
|
|
440
|
+
if (reaction) saveReaction(reaction, "mcp");
|
|
429
441
|
}
|
|
430
442
|
|
|
431
443
|
// ─── Claude Code settings.json patching (for buddy_statusline tool) ──────────
|
|
@@ -19,9 +19,8 @@ source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh"
|
|
|
19
19
|
|
|
20
20
|
STATE="$BUDDY_STATE_DIR/status.json"
|
|
21
21
|
CONFIG_FILE="$BUDDY_STATE_DIR/config.json"
|
|
22
|
-
#
|
|
23
|
-
SID="$
|
|
24
|
-
SID="${SID:-default}"
|
|
22
|
+
# Per-session ID resolved by paths.sh (CLAUDE_CODE_SESSION_ID > TMUX_PANE > default)
|
|
23
|
+
SID="$BUDDY_SID"
|
|
25
24
|
|
|
26
25
|
[ -f "$STATE" ] || exit 0
|
|
27
26
|
|
|
@@ -32,8 +31,10 @@ NAME=$(jq -r '.name // ""' "$STATE" 2>/dev/null)
|
|
|
32
31
|
[ -z "$NAME" ] && exit 0
|
|
33
32
|
|
|
34
33
|
RARITY=$(jq -r '.rarity // "common"' "$STATE" 2>/dev/null)
|
|
34
|
+
STARS=$(jq -r '.stars // ""' "$STATE" 2>/dev/null)
|
|
35
35
|
SHINY=$(jq -r '.shiny // false' "$STATE" 2>/dev/null)
|
|
36
|
-
|
|
36
|
+
REACTION_FILE="$BUDDY_STATE_DIR/reaction.$SID.json"
|
|
37
|
+
REACTION=$(jq -r '.reaction // ""' "$REACTION_FILE" 2>/dev/null)
|
|
37
38
|
ACHIEVEMENT=$(jq -r '.achievement // ""' "$STATE" 2>/dev/null)
|
|
38
39
|
LEVEL=$(jq -r '.level // 1' "$STATE" 2>/dev/null)
|
|
39
40
|
MOOD=$(jq -r '.mood // "focused"' "$STATE" 2>/dev/null)
|
|
@@ -67,6 +68,7 @@ if [ -f "$CONFIG_FILE" ]; then
|
|
|
67
68
|
fi
|
|
68
69
|
|
|
69
70
|
NC=$'\033[0m'
|
|
71
|
+
NEUTRAL=$'\033[39m'
|
|
70
72
|
case "$RARITY" in
|
|
71
73
|
common)
|
|
72
74
|
[ "$_THEME" = "light" ] && C=$'\033[38;2;90;90;90m' || C=$'\033[38;2;153;153;153m' ;;
|
|
@@ -150,7 +152,6 @@ BUBBLE=""
|
|
|
150
152
|
if [ -n "$ACHIEVEMENT" ] && [ "$ACHIEVEMENT" != "null" ] && [ "$ACHIEVEMENT" != "" ]; then
|
|
151
153
|
BUBBLE=$'\xf0\x9f\x8f\x86'" $ACHIEVEMENT"
|
|
152
154
|
fi
|
|
153
|
-
REACTION_FILE="$BUDDY_STATE_DIR/reaction.$SID.json"
|
|
154
155
|
REACTION_TTL=0
|
|
155
156
|
INNER_W=44
|
|
156
157
|
MARGIN=8
|
|
@@ -189,6 +190,7 @@ fi
|
|
|
189
190
|
# so the geometric center sits at col 6.
|
|
190
191
|
NAME_WITH_LEVEL="$NAME"
|
|
191
192
|
[ "$LEVEL" -gt 1 ] 2>/dev/null && NAME_WITH_LEVEL="${NAME} [L${LEVEL}]"
|
|
193
|
+
[ -n "$STARS" ] && NAME_WITH_LEVEL="${NAME_WITH_LEVEL} ${STARS}"
|
|
192
194
|
case "$MOOD" in
|
|
193
195
|
happy) MOOD_EMOJI="" ;;
|
|
194
196
|
focused) MOOD_EMOJI="" ;;
|
|
@@ -215,13 +217,19 @@ for line in "${ART_LINES[@]}"; do
|
|
|
215
217
|
if [ "$SHINY" = "true" ]; then
|
|
216
218
|
ALL_COLORS+=("${RAINBOW[$(( (_arc + RAINBOW_OFFSET) % RAINBOW_LEN ))]}")
|
|
217
219
|
else
|
|
218
|
-
ALL_COLORS+=("$
|
|
220
|
+
ALL_COLORS+=("$NEUTRAL")
|
|
219
221
|
fi
|
|
220
222
|
_arc=$(( _arc + 1 ))
|
|
221
223
|
done
|
|
222
|
-
ALL_LINES+=("$NAME_LINE"); ALL_COLORS+=("$
|
|
223
|
-
|
|
224
|
-
|
|
224
|
+
ALL_LINES+=("$NAME_LINE"); ALL_COLORS+=("$C")
|
|
225
|
+
|
|
226
|
+
case "$SPECIES" in
|
|
227
|
+
axolotl|capybara|rabbit|mushroom|chonk) ART_W=10 ;;
|
|
228
|
+
duck|blob|cat|dragon|robot|wyvern) ART_W=9 ;;
|
|
229
|
+
octopus|owl|turtle|snail|ghost|cactus) ART_W=8 ;;
|
|
230
|
+
goose|penguin) ART_W=7 ;;
|
|
231
|
+
*) ART_W=10 ;;
|
|
232
|
+
esac
|
|
225
233
|
ART_COUNT=${#ALL_LINES[@]}
|
|
226
234
|
|
|
227
235
|
# ─── Speech bubble (left of art, word-wrapped) ──────────────────────────────
|
|
@@ -372,18 +380,18 @@ for (( i=0; i<MAX_LINES; i++ )); do
|
|
|
372
380
|
|
|
373
381
|
# Connector: "-- " on the middle text line, spaces otherwise
|
|
374
382
|
if [ $bi -eq $CONNECTOR_BI ]; then
|
|
375
|
-
gap="${
|
|
383
|
+
gap="${NEUTRAL}--${NC} "
|
|
376
384
|
else
|
|
377
385
|
gap=" "
|
|
378
386
|
fi
|
|
379
387
|
|
|
380
388
|
if [ "$btype" = "border" ]; then
|
|
381
|
-
echo "${SPACER}${
|
|
389
|
+
echo "${SPACER}${NEUTRAL}${bline}${NC}${gap}${art_part}"
|
|
382
390
|
else
|
|
383
391
|
pipe_l="${bline:0:1}"
|
|
384
392
|
pipe_r="${bline: -1}"
|
|
385
393
|
inner="${bline:1:$(( ${#bline} - 2 ))}"
|
|
386
|
-
echo "${SPACER}${
|
|
394
|
+
echo "${SPACER}${NEUTRAL}${pipe_l}${NC}${DIM}${inner}${NC}${NEUTRAL}${pipe_r}${NC}${gap}${art_part}"
|
|
387
395
|
fi
|
|
388
396
|
else
|
|
389
397
|
empty=$(printf '%*s' "$BOX_W" '')
|
|
@@ -99,9 +99,11 @@ def fmt_stat(label, pct, reset):
|
|
|
99
99
|
c = color_for(pct)
|
|
100
100
|
pct_str = f'{round(pct):3d}%' if pct is not None else ' -%'
|
|
101
101
|
bar = build_bar(pct)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
bar_text = f'{DIM}{label}{NC} {c}{pct_str}{NC} {bar}'
|
|
103
|
+
bar_width = 2 + 1 + 4 + 1 + 10
|
|
104
|
+
timer_text = f' {c}↻{reset}{NC}'
|
|
105
|
+
timer_width = 3 + 1 + len(reset)
|
|
106
|
+
return (bar_text, bar_width), (timer_text, timer_width)
|
|
105
107
|
|
|
106
108
|
try:
|
|
107
109
|
stats = json.loads(os.environ.get('STATS_JSON', '{}'))
|
|
@@ -112,14 +114,13 @@ lines = sys.stdin.read().splitlines()
|
|
|
112
114
|
n = len(lines)
|
|
113
115
|
center = 1
|
|
114
116
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
]
|
|
117
|
+
sess = fmt_stat('5h', stats.get('sess_pct'), stats.get('sess_reset', '--'))
|
|
118
|
+
week = fmt_stat('7d', stats.get('week_pct'), stats.get('week_reset', '--'))
|
|
119
|
+
stat_items = [sess[0], sess[1], week[0], week[1]]
|
|
119
120
|
|
|
120
121
|
for i, line in enumerate(lines):
|
|
121
122
|
si = i - center
|
|
122
|
-
if 0 <= si <
|
|
123
|
+
if 0 <= si < 4 and line.startswith(BRAILLE):
|
|
123
124
|
stat_text, stat_width = stat_items[si]
|
|
124
125
|
after_braille = line[1:]
|
|
125
126
|
num_spaces = len(after_braille) - len(after_braille.lstrip(' '))
|