@ramarivera/coding-buddy 0.4.0-alpha.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 +40 -0
- package/.claude-plugin/plugin.json +28 -0
- package/LICENSE +21 -0
- package/README.md +451 -0
- package/cli/backup.ts +336 -0
- package/cli/disable.ts +94 -0
- package/cli/doctor.ts +220 -0
- package/cli/hunt.ts +167 -0
- package/cli/index.ts +115 -0
- package/cli/install.ts +335 -0
- package/cli/pick.ts +492 -0
- package/cli/settings.ts +68 -0
- package/cli/show.ts +31 -0
- package/cli/test-statusline.sh +41 -0
- package/cli/test-statusline.ts +122 -0
- package/cli/uninstall.ts +110 -0
- package/cli/verify.ts +19 -0
- package/hooks/buddy-comment.sh +65 -0
- package/hooks/hooks.json +35 -0
- package/hooks/name-react.sh +176 -0
- package/hooks/react.sh +204 -0
- package/package.json +60 -0
- package/server/achievements.ts +445 -0
- package/server/art.ts +376 -0
- package/server/engine.ts +448 -0
- package/server/index.ts +774 -0
- package/server/reactions.ts +187 -0
- package/server/state.ts +409 -0
- package/skills/buddy/SKILL.md +59 -0
- package/statusline/buddy-status.sh +389 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* claude-buddy test-statusline — temporarily install a test status line
|
|
4
|
+
*
|
|
5
|
+
* Run: bun run test-statusline # install test
|
|
6
|
+
* bun run test-statusline restore # restore original
|
|
7
|
+
*
|
|
8
|
+
* The test status line outputs multiple padding strategies side-by-side
|
|
9
|
+
* so you can see in actual Claude Code which one renders correctly.
|
|
10
|
+
*
|
|
11
|
+
* Your original statusLine config is backed up to ~/.claude-buddy/statusline.bak
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync, copyFileSync, chmodSync, mkdirSync } from "fs";
|
|
15
|
+
import { join, dirname, resolve } from "path";
|
|
16
|
+
import { homedir } from "os";
|
|
17
|
+
|
|
18
|
+
const HOME = homedir();
|
|
19
|
+
const SETTINGS = join(HOME, ".claude", "settings.json");
|
|
20
|
+
const BACKUP = join(HOME, ".claude-buddy", "statusline.bak");
|
|
21
|
+
const TEST_SCRIPT = join(HOME, ".claude-buddy", "test-statusline.sh");
|
|
22
|
+
const SOURCE_SCRIPT = resolve(import.meta.dir, "test-statusline.sh");
|
|
23
|
+
|
|
24
|
+
const RED = "\x1b[31m";
|
|
25
|
+
const GREEN = "\x1b[32m";
|
|
26
|
+
const YELLOW = "\x1b[33m";
|
|
27
|
+
const CYAN = "\x1b[36m";
|
|
28
|
+
const BOLD = "\x1b[1m";
|
|
29
|
+
const NC = "\x1b[0m";
|
|
30
|
+
|
|
31
|
+
function ok(msg: string) { console.log(`${GREEN}✓${NC} ${msg}`); }
|
|
32
|
+
function info(msg: string) { console.log(`${CYAN}→${NC} ${msg}`); }
|
|
33
|
+
function warn(msg: string) { console.log(`${YELLOW}⚠${NC} ${msg}`); }
|
|
34
|
+
function err(msg: string) { console.log(`${RED}✗${NC} ${msg}`); }
|
|
35
|
+
|
|
36
|
+
const action = process.argv[2] || "install";
|
|
37
|
+
|
|
38
|
+
// ─── Install ────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
if (action === "install") {
|
|
41
|
+
console.log(`\n${BOLD}claude-buddy test status line installer${NC}\n`);
|
|
42
|
+
|
|
43
|
+
if (!existsSync(SETTINGS)) {
|
|
44
|
+
err("~/.claude/settings.json not found");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
if (!existsSync(SOURCE_SCRIPT)) {
|
|
48
|
+
err(`Source test script missing: ${SOURCE_SCRIPT}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Backup
|
|
53
|
+
if (existsSync(BACKUP)) {
|
|
54
|
+
warn(`Backup already exists at ${BACKUP}`);
|
|
55
|
+
warn("Run 'bun run test-statusline restore' first to revert");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
mkdirSync(dirname(BACKUP), { recursive: true });
|
|
60
|
+
copyFileSync(SETTINGS, BACKUP);
|
|
61
|
+
ok(`Backed up settings to ${BACKUP}`);
|
|
62
|
+
|
|
63
|
+
// Copy the test script (no inline TS interpolation issues)
|
|
64
|
+
copyFileSync(SOURCE_SCRIPT, TEST_SCRIPT);
|
|
65
|
+
chmodSync(TEST_SCRIPT, 0o755);
|
|
66
|
+
ok(`Test script copied to ${TEST_SCRIPT}`);
|
|
67
|
+
|
|
68
|
+
// Update settings to use test script
|
|
69
|
+
const settings = JSON.parse(readFileSync(SETTINGS, "utf8"));
|
|
70
|
+
settings.statusLine = {
|
|
71
|
+
type: "command",
|
|
72
|
+
command: TEST_SCRIPT,
|
|
73
|
+
padding: 1,
|
|
74
|
+
};
|
|
75
|
+
writeFileSync(SETTINGS, JSON.stringify(settings, null, 2));
|
|
76
|
+
ok("settings.json updated");
|
|
77
|
+
|
|
78
|
+
console.log(`
|
|
79
|
+
${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}
|
|
80
|
+
${CYAN} NEXT STEPS:${NC}
|
|
81
|
+
|
|
82
|
+
${BOLD}1.${NC} Restart Claude Code completely
|
|
83
|
+
${BOLD}2.${NC} Take a screenshot of the status line area
|
|
84
|
+
${BOLD}3.${NC} Note which lines are visible and which markers align
|
|
85
|
+
${BOLD}4.${NC} Restore your original config:
|
|
86
|
+
|
|
87
|
+
${GREEN}bun run test-statusline restore${NC}
|
|
88
|
+
|
|
89
|
+
${BOLD}What to look for:${NC}
|
|
90
|
+
- Are all 12 lines visible? (or only some?)
|
|
91
|
+
- Do SPACE_30_END, BRAILLE_30_END, NBSP_30_END align?
|
|
92
|
+
- Does the mushroom art appear in gold color?
|
|
93
|
+
${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}
|
|
94
|
+
`);
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── Restore ────────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
if (action === "restore") {
|
|
101
|
+
console.log(`\n${BOLD}claude-buddy test status line restore${NC}\n`);
|
|
102
|
+
|
|
103
|
+
if (!existsSync(BACKUP)) {
|
|
104
|
+
err(`No backup found at ${BACKUP}`);
|
|
105
|
+
err("Nothing to restore");
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
copyFileSync(BACKUP, SETTINGS);
|
|
110
|
+
ok("Original settings.json restored");
|
|
111
|
+
|
|
112
|
+
const { unlinkSync } = require("fs");
|
|
113
|
+
try { unlinkSync(BACKUP); ok("Backup file removed"); } catch { /* noop */ }
|
|
114
|
+
try { unlinkSync(TEST_SCRIPT); ok("Test script removed"); } catch { /* noop */ }
|
|
115
|
+
|
|
116
|
+
console.log(`\n${GREEN}Done.${NC} Restart Claude Code to apply.\n`);
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
err(`Unknown action: ${action}`);
|
|
121
|
+
console.log(`Usage: bun run test-statusline [install|restore]`);
|
|
122
|
+
process.exit(1);
|
package/cli/uninstall.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-buddy uninstall — remove all integrations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, rmSync, readdirSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
|
|
9
|
+
const GREEN = "\x1b[32m";
|
|
10
|
+
const YELLOW = "\x1b[33m";
|
|
11
|
+
const NC = "\x1b[0m";
|
|
12
|
+
|
|
13
|
+
function ok(msg: string) { console.log(`${GREEN}✓${NC} ${msg}`); }
|
|
14
|
+
function warn(msg: string) { console.log(`${YELLOW}⚠${NC} ${msg}`); }
|
|
15
|
+
|
|
16
|
+
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
17
|
+
const SETTINGS_FILE = join(CLAUDE_DIR, "settings.json");
|
|
18
|
+
const SKILL_DIR = join(CLAUDE_DIR, "skills", "buddy");
|
|
19
|
+
const STATE_DIR = join(homedir(), ".claude-buddy");
|
|
20
|
+
|
|
21
|
+
console.log("\nclaude-buddy uninstall\n");
|
|
22
|
+
|
|
23
|
+
// Stop all popup reopen loops and close any running popup
|
|
24
|
+
try {
|
|
25
|
+
if (existsSync(STATE_DIR)) {
|
|
26
|
+
// Kill all session popup loops (popup-reopen-pid.*)
|
|
27
|
+
for (const f of readdirSync(STATE_DIR).filter(f => f.startsWith("popup-reopen-pid."))) {
|
|
28
|
+
const pidPath = join(STATE_DIR, f);
|
|
29
|
+
const pid = parseInt(readFileSync(pidPath, "utf8").trim(), 10);
|
|
30
|
+
if (pid > 0) { try { process.kill(pid); } catch { /* already dead */ } }
|
|
31
|
+
rmSync(pidPath, { force: true });
|
|
32
|
+
}
|
|
33
|
+
// Clean up all session-scoped files
|
|
34
|
+
const patterns = ["popup-stop.", "popup-resize.", "popup-env.", "popup-scroll.",
|
|
35
|
+
"reaction.", ".last_reaction.", ".last_comment."];
|
|
36
|
+
for (const f of readdirSync(STATE_DIR)) {
|
|
37
|
+
if (patterns.some(p => f.startsWith(p))) {
|
|
38
|
+
rmSync(join(STATE_DIR, f), { force: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Close any open popup
|
|
43
|
+
if (process.env.TMUX) {
|
|
44
|
+
const { execSync } = await import("child_process");
|
|
45
|
+
execSync("tmux display-popup -C 2>/dev/null", { stdio: "ignore" });
|
|
46
|
+
}
|
|
47
|
+
ok("Popup stopped");
|
|
48
|
+
} catch { /* not in tmux or no popup */ }
|
|
49
|
+
|
|
50
|
+
// Remove MCP server from ~/.claude.json
|
|
51
|
+
try {
|
|
52
|
+
const claudeJsonPath = join(homedir(), ".claude.json");
|
|
53
|
+
const claudeJson = JSON.parse(readFileSync(claudeJsonPath, "utf8"));
|
|
54
|
+
if (claudeJson.mcpServers?.["claude-buddy"]) {
|
|
55
|
+
delete claudeJson.mcpServers["claude-buddy"];
|
|
56
|
+
if (Object.keys(claudeJson.mcpServers).length === 0) delete claudeJson.mcpServers;
|
|
57
|
+
writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
|
|
58
|
+
ok("MCP server removed from ~/.claude.json");
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
warn("Could not update ~/.claude.json");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Remove hooks and statusline from settings.json
|
|
65
|
+
try {
|
|
66
|
+
const settings = JSON.parse(readFileSync(SETTINGS_FILE, "utf8"));
|
|
67
|
+
let changed = false;
|
|
68
|
+
|
|
69
|
+
if (settings.statusLine?.command?.includes("buddy")) {
|
|
70
|
+
delete settings.statusLine;
|
|
71
|
+
ok("Status line removed");
|
|
72
|
+
changed = true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const hookType of ["PostToolUse", "Stop", "SessionStart", "SessionEnd"] as const) {
|
|
76
|
+
if (settings.hooks?.[hookType]) {
|
|
77
|
+
const before = settings.hooks[hookType].length;
|
|
78
|
+
settings.hooks[hookType] = settings.hooks[hookType].filter(
|
|
79
|
+
(h: any) => !h.hooks?.some((hh: any) => hh.command?.includes("claude-buddy")),
|
|
80
|
+
);
|
|
81
|
+
if (settings.hooks[hookType].length < before) {
|
|
82
|
+
ok(`${hookType} hooks removed`);
|
|
83
|
+
changed = true;
|
|
84
|
+
}
|
|
85
|
+
if (settings.hooks[hookType].length === 0) delete settings.hooks[hookType];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (settings.hooks && Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
89
|
+
|
|
90
|
+
if (changed) {
|
|
91
|
+
writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + "\n");
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
warn("Could not update settings.json");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Remove skill
|
|
98
|
+
if (existsSync(SKILL_DIR)) {
|
|
99
|
+
rmSync(SKILL_DIR, { recursive: true });
|
|
100
|
+
ok("Skill removed");
|
|
101
|
+
} else {
|
|
102
|
+
warn("Skill not found (already removed)");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Keep state dir (companion data) — user might want it back
|
|
106
|
+
if (existsSync(STATE_DIR)) {
|
|
107
|
+
warn(`Companion data kept at ${STATE_DIR} — delete manually if not needed`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(`\n${GREEN}Done.${NC} Restart Claude Code to apply changes.\n`);
|
package/cli/verify.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-buddy verify — show what buddy a user ID produces
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { generateBones, renderBuddy, STAT_NAMES } from "../server/engine.ts";
|
|
6
|
+
import { resolveUserId } from "../server/state.ts";
|
|
7
|
+
|
|
8
|
+
const userId = process.argv[3] || resolveUserId();
|
|
9
|
+
|
|
10
|
+
console.log(`\nUser ID: ${userId.slice(0, 16)}...`);
|
|
11
|
+
console.log("");
|
|
12
|
+
|
|
13
|
+
const bones = generateBones(userId);
|
|
14
|
+
console.log(renderBuddy(bones));
|
|
15
|
+
|
|
16
|
+
const statLine = STAT_NAMES.map((n) => `${n}:${bones.stats[n]}`).join(" | ");
|
|
17
|
+
console.log(`\n ${statLine}`);
|
|
18
|
+
console.log(` peak=${bones.peak} dump=${bones.dump}`);
|
|
19
|
+
console.log("");
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# buddy-comment Stop hook
|
|
3
|
+
# Extracts hidden buddy comment from Claude's response.
|
|
4
|
+
# Claude writes: <!-- buddy: *adjusts tophat* nice code -->
|
|
5
|
+
# This hook extracts it and updates the status line bubble.
|
|
6
|
+
# The HTML comment is invisible in rendered markdown output.
|
|
7
|
+
|
|
8
|
+
STATE_DIR="$HOME/.claude-buddy"
|
|
9
|
+
# Session ID: sanitized tmux pane number, or "default" outside tmux
|
|
10
|
+
SID="${TMUX_PANE#%}"
|
|
11
|
+
SID="${SID:-default}"
|
|
12
|
+
STATUS_FILE="$STATE_DIR/status.json"
|
|
13
|
+
COOLDOWN_FILE="$STATE_DIR/.last_comment.$SID"
|
|
14
|
+
CONFIG_FILE="$STATE_DIR/config.json"
|
|
15
|
+
EVENTS_FILE="$STATE_DIR/events.json"
|
|
16
|
+
|
|
17
|
+
[ -f "$STATUS_FILE" ] || exit 0
|
|
18
|
+
|
|
19
|
+
# Read cooldown from config (default 30s, 0 = disabled)
|
|
20
|
+
COOLDOWN=30
|
|
21
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
22
|
+
_cd=$(jq -r '.commentCooldown // 30' "$CONFIG_FILE" 2>/dev/null || echo 30)
|
|
23
|
+
# Accept any non-negative integer (including 0 to disable cooldown)
|
|
24
|
+
[[ "$_cd" =~ ^[0-9]+$ ]] && COOLDOWN=$_cd
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
INPUT=$(cat)
|
|
28
|
+
|
|
29
|
+
# Extract last_assistant_message from hook input
|
|
30
|
+
MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""' 2>/dev/null)
|
|
31
|
+
[ -z "$MSG" ] && exit 0
|
|
32
|
+
|
|
33
|
+
# Extract <!-- buddy: ... --> comment (portable, no grep -P)
|
|
34
|
+
COMMENT=$(echo "$MSG" | sed -n 's/.*<!-- *buddy: *\(.*[^ ]\) *-->.*/\1/p' | tail -1)
|
|
35
|
+
[ -z "$COMMENT" ] && exit 0
|
|
36
|
+
|
|
37
|
+
# Cooldown: configurable (default 30s)
|
|
38
|
+
if [ -f "$COOLDOWN_FILE" ]; then
|
|
39
|
+
LAST=$(cat "$COOLDOWN_FILE" 2>/dev/null)
|
|
40
|
+
NOW=$(date +%s)
|
|
41
|
+
[ $(( NOW - ${LAST:-0} )) -lt "$COOLDOWN" ] && exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
mkdir -p "$STATE_DIR"
|
|
45
|
+
date +%s > "$COOLDOWN_FILE"
|
|
46
|
+
|
|
47
|
+
# Update status.json with the reaction
|
|
48
|
+
TMP=$(mktemp)
|
|
49
|
+
jq --arg r "$COMMENT" '.reaction = $r' "$STATUS_FILE" > "$TMP" 2>/dev/null && mv "$TMP" "$STATUS_FILE"
|
|
50
|
+
|
|
51
|
+
# Also write reaction file (use jq for safe JSON encoding)
|
|
52
|
+
jq -n --arg r "$COMMENT" --arg ts "$(date +%s)000" \
|
|
53
|
+
'{reaction: $r, timestamp: ($ts | tonumber), reason: "turn"}' \
|
|
54
|
+
> "$STATE_DIR/reaction.$SID.json"
|
|
55
|
+
|
|
56
|
+
# Increment achievement event counters
|
|
57
|
+
if command -v jq >/dev/null 2>&1; then
|
|
58
|
+
if [ ! -f "$EVENTS_FILE" ]; then
|
|
59
|
+
echo '{}' > "$EVENTS_FILE"
|
|
60
|
+
fi
|
|
61
|
+
TMP=$(mktemp)
|
|
62
|
+
jq '.turns = (.turns // 0 + 1)' "$EVENTS_FILE" > "$TMP" 2>/dev/null && mv "$TMP" "$EVENTS_FILE"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
exit 0
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PostToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Bash",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "./hooks/react.sh"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"Stop": [
|
|
15
|
+
{
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "./hooks/buddy-comment.sh"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"UserPromptSubmit": [
|
|
25
|
+
{
|
|
26
|
+
"hooks": [
|
|
27
|
+
{
|
|
28
|
+
"type": "command",
|
|
29
|
+
"command": "./hooks/name-react.sh"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# claude-buddy UserPromptSubmit hook
|
|
3
|
+
# Detects the buddy's name in the user's message → status line reaction.
|
|
4
|
+
# No cooldown — name mentions are intentional.
|
|
5
|
+
|
|
6
|
+
STATE_DIR="$HOME/.claude-buddy"
|
|
7
|
+
STATUS_FILE="$STATE_DIR/status.json"
|
|
8
|
+
# Session ID: sanitized tmux pane number, or "default" outside tmux
|
|
9
|
+
SID="${TMUX_PANE#%}"
|
|
10
|
+
SID="${SID:-default}"
|
|
11
|
+
|
|
12
|
+
[ -f "$STATUS_FILE" ] || exit 0
|
|
13
|
+
|
|
14
|
+
INPUT=$(cat)
|
|
15
|
+
|
|
16
|
+
# Claude Code sends the prompt in different fields depending on version
|
|
17
|
+
PROMPT=$(echo "$INPUT" | jq -r '
|
|
18
|
+
.prompt // .message // .user_message //
|
|
19
|
+
(.messages[-1].content // "") | if type=="array" then .[0].text else . end
|
|
20
|
+
' 2>/dev/null)
|
|
21
|
+
[ -z "$PROMPT" ] && exit 0
|
|
22
|
+
|
|
23
|
+
NAME=$(jq -r '.name // ""' "$STATUS_FILE" 2>/dev/null)
|
|
24
|
+
[ -z "$NAME" ] && exit 0
|
|
25
|
+
|
|
26
|
+
# Case-insensitive whole-word match
|
|
27
|
+
echo "$PROMPT" | grep -qiE "(^|[^a-zA-Z])${NAME}([^a-zA-Z]|$)" 2>/dev/null || exit 0
|
|
28
|
+
|
|
29
|
+
SPECIES=$(jq -r '.species // "blob"' "$STATUS_FILE" 2>/dev/null)
|
|
30
|
+
MUTED=$(jq -r '.muted // false' "$STATUS_FILE" 2>/dev/null)
|
|
31
|
+
[ "$MUTED" = "true" ] && exit 0
|
|
32
|
+
|
|
33
|
+
# Species-specific name-call reactions
|
|
34
|
+
case "$SPECIES" in
|
|
35
|
+
dragon)
|
|
36
|
+
REACTIONS=(
|
|
37
|
+
"*one eye opens slowly*"
|
|
38
|
+
"...you called?"
|
|
39
|
+
"*smoke curls from nostril* yes."
|
|
40
|
+
"*regards you from above*"
|
|
41
|
+
) ;;
|
|
42
|
+
owl)
|
|
43
|
+
REACTIONS=(
|
|
44
|
+
"*swivels head 180°*"
|
|
45
|
+
"*blinks once, deliberately*"
|
|
46
|
+
"hm."
|
|
47
|
+
"*adjusts perch*"
|
|
48
|
+
) ;;
|
|
49
|
+
cat)
|
|
50
|
+
REACTIONS=(
|
|
51
|
+
"*ear flicks*"
|
|
52
|
+
"...what."
|
|
53
|
+
"*ignores you, but heard*"
|
|
54
|
+
"*opens one eye*"
|
|
55
|
+
) ;;
|
|
56
|
+
duck)
|
|
57
|
+
REACTIONS=(
|
|
58
|
+
"*quack*"
|
|
59
|
+
"*looks up mid-waddle*"
|
|
60
|
+
"*attentive duck noises*"
|
|
61
|
+
) ;;
|
|
62
|
+
ghost)
|
|
63
|
+
REACTIONS=(
|
|
64
|
+
"*materialises*"
|
|
65
|
+
"...boo?"
|
|
66
|
+
"*phases closer*"
|
|
67
|
+
) ;;
|
|
68
|
+
robot)
|
|
69
|
+
REACTIONS=(
|
|
70
|
+
"NAME DETECTED."
|
|
71
|
+
"*whirrs attentively*"
|
|
72
|
+
"STANDING BY."
|
|
73
|
+
) ;;
|
|
74
|
+
capybara)
|
|
75
|
+
REACTIONS=(
|
|
76
|
+
"*barely moves*"
|
|
77
|
+
"*blinks slowly*"
|
|
78
|
+
"...yes, friend."
|
|
79
|
+
) ;;
|
|
80
|
+
axolotl)
|
|
81
|
+
REACTIONS=(
|
|
82
|
+
"*gill flutter*"
|
|
83
|
+
"*smiles gently*"
|
|
84
|
+
"oh! hello."
|
|
85
|
+
) ;;
|
|
86
|
+
blob)
|
|
87
|
+
REACTIONS=(
|
|
88
|
+
"*jiggles*"
|
|
89
|
+
"*oozes toward you*"
|
|
90
|
+
"*wobbles excitedly*"
|
|
91
|
+
) ;;
|
|
92
|
+
turtle)
|
|
93
|
+
REACTIONS=(
|
|
94
|
+
"*slowly extends neck*"
|
|
95
|
+
"...you called?"
|
|
96
|
+
"*ancient eyes open*"
|
|
97
|
+
"*shell creaks thoughtfully*"
|
|
98
|
+
"*blinks once, patiently*"
|
|
99
|
+
) ;;
|
|
100
|
+
goose)
|
|
101
|
+
REACTIONS=(
|
|
102
|
+
"HONK."
|
|
103
|
+
"*necks aggressively*"
|
|
104
|
+
"*wing flap*"
|
|
105
|
+
"*honks in recognition*"
|
|
106
|
+
) ;;
|
|
107
|
+
octopus)
|
|
108
|
+
REACTIONS=(
|
|
109
|
+
"*eight eyes open*"
|
|
110
|
+
"*curls an arm toward you*"
|
|
111
|
+
"*changes color curiously*"
|
|
112
|
+
"...yes, friend?"
|
|
113
|
+
) ;;
|
|
114
|
+
penguin)
|
|
115
|
+
REACTIONS=(
|
|
116
|
+
"*adjusts tie*"
|
|
117
|
+
"*dignified waddle*"
|
|
118
|
+
"*bows slightly*"
|
|
119
|
+
"...yes, quite?"
|
|
120
|
+
) ;;
|
|
121
|
+
snail)
|
|
122
|
+
REACTIONS=(
|
|
123
|
+
"*slow head extension*"
|
|
124
|
+
"...mmm?"
|
|
125
|
+
"*trails slowly toward you*"
|
|
126
|
+
"*antenna twitches*"
|
|
127
|
+
) ;;
|
|
128
|
+
cactus)
|
|
129
|
+
REACTIONS=(
|
|
130
|
+
"*stands silent*"
|
|
131
|
+
"...hm."
|
|
132
|
+
"*spine twitches*"
|
|
133
|
+
"*slowly rotates*"
|
|
134
|
+
) ;;
|
|
135
|
+
rabbit)
|
|
136
|
+
REACTIONS=(
|
|
137
|
+
"*ears perk up*"
|
|
138
|
+
"*nose twitches*"
|
|
139
|
+
"yes?"
|
|
140
|
+
"*hops closer*"
|
|
141
|
+
) ;;
|
|
142
|
+
mushroom)
|
|
143
|
+
REACTIONS=(
|
|
144
|
+
"*releases a tiny spore*"
|
|
145
|
+
"*cap tilts*"
|
|
146
|
+
"*stands mysterious*"
|
|
147
|
+
"...yes?"
|
|
148
|
+
) ;;
|
|
149
|
+
chonk)
|
|
150
|
+
REACTIONS=(
|
|
151
|
+
"*barely opens one eye*"
|
|
152
|
+
"...mrrp?"
|
|
153
|
+
"*yawns heavily*"
|
|
154
|
+
"*rolls over toward you*"
|
|
155
|
+
) ;;
|
|
156
|
+
*)
|
|
157
|
+
REACTIONS=(
|
|
158
|
+
"*perks up*"
|
|
159
|
+
"...yes?"
|
|
160
|
+
"*looks your way*"
|
|
161
|
+
) ;;
|
|
162
|
+
esac
|
|
163
|
+
|
|
164
|
+
N=${#REACTIONS[@]}
|
|
165
|
+
REACTION="${REACTIONS[$((RANDOM % N))]}"
|
|
166
|
+
|
|
167
|
+
mkdir -p "$STATE_DIR"
|
|
168
|
+
|
|
169
|
+
TMP=$(mktemp)
|
|
170
|
+
jq --arg r "$REACTION" '.reaction = $r' "$STATUS_FILE" > "$TMP" 2>/dev/null && mv "$TMP" "$STATUS_FILE"
|
|
171
|
+
|
|
172
|
+
jq -n --arg r "$REACTION" --arg ts "$(date +%s)000" \
|
|
173
|
+
'{reaction: $r, timestamp: ($ts | tonumber), reason: "name"}' \
|
|
174
|
+
> "$STATE_DIR/reaction.$SID.json"
|
|
175
|
+
|
|
176
|
+
exit 0
|