@ramarivera/coding-buddy 0.4.0-alpha.8 → 0.6.0

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 (92) hide show
  1. package/{adapters/claude/plugin → .claude-plugin}/marketplace.json +11 -6
  2. package/{adapters/claude/plugin → .claude-plugin}/plugin.json +9 -7
  3. package/LICENSE +1 -1
  4. package/README.md +102 -72
  5. package/adapters/omp/commands.ts +201 -0
  6. package/adapters/omp/context.ts +22 -0
  7. package/adapters/omp/events.ts +551 -0
  8. package/adapters/omp/identity.ts +10 -0
  9. package/adapters/omp/index.ts +39 -0
  10. package/adapters/omp/logger.ts +52 -0
  11. package/adapters/omp/prompt.ts +60 -0
  12. package/adapters/omp/renderers.ts +73 -0
  13. package/adapters/omp/storage.ts +25 -0
  14. package/adapters/omp/ui.ts +85 -0
  15. package/adapters/pi/commands.ts +64 -37
  16. package/adapters/pi/events.ts +147 -72
  17. package/adapters/pi/index.ts +9 -4
  18. package/adapters/pi/storage.ts +14 -285
  19. package/adapters/pi/ui.ts +56 -5
  20. package/adapters/shared/file-storage.ts +526 -0
  21. package/{adapters/claude/install → cli}/backup.ts +59 -44
  22. package/cli/biomes.ts +309 -0
  23. package/cli/buddy-shell.ts +818 -0
  24. package/{adapters/claude/install → cli}/disable.ts +14 -32
  25. package/{adapters/claude/install → cli}/doctor.ts +55 -57
  26. package/{adapters/claude/install → cli}/hunt.ts +7 -9
  27. package/cli/index.ts +36 -25
  28. package/{adapters/claude/install → cli}/install.ts +100 -145
  29. package/{adapters/claude/install → cli}/pick.ts +111 -50
  30. package/{adapters/claude/install → cli}/settings.ts +2 -2
  31. package/{adapters/claude/install → cli}/show.ts +4 -4
  32. package/{adapters/claude/install → cli}/test-statusline.sh +2 -2
  33. package/{adapters/claude/install → cli}/test-statusline.ts +11 -11
  34. package/cli/tui.tsx +2333 -0
  35. package/{adapters/claude/install → cli}/uninstall.ts +23 -38
  36. package/cli/upgrade.ts +213 -0
  37. package/cli/validate-species.ts +209 -0
  38. package/cli/verify.ts +3 -3
  39. package/core/achievements.ts +12 -20
  40. package/core/art-data.ts +26 -0
  41. package/core/command-service.ts +99 -97
  42. package/core/engine.ts +83 -1
  43. package/core/ports.ts +10 -0
  44. package/core/reactions.ts +533 -95
  45. package/core/slot-slug.ts +22 -0
  46. package/{adapters/claude/hooks → hooks}/buddy-comment.sh +23 -2
  47. package/hooks/file-type-react.sh +401 -0
  48. package/hooks/hooks.json +52 -0
  49. package/hooks/mood-react.sh +208 -0
  50. package/{adapters/claude/hooks → hooks}/name-react.sh +5 -2
  51. package/hooks/react.sh +1368 -0
  52. package/hooks/suggest.sh +55 -0
  53. package/package.json +113 -59
  54. package/scripts/gen-emoji-widths.ts +34 -0
  55. package/scripts/paths.sh +56 -0
  56. package/scripts/postinstall.cjs +42 -0
  57. package/scripts/snapshot-statusline.sh +54 -0
  58. package/server/achievements.ts +1419 -0
  59. package/server/art.ts +450 -0
  60. package/server/award-xp.ts +48 -0
  61. package/server/check-suggestions.ts +102 -0
  62. package/server/consolidate.ts +33 -0
  63. package/server/index.ts +1340 -0
  64. package/server/mcp-launcher.sh +26 -0
  65. package/server/memory.ts +404 -0
  66. package/server/mood.ts +254 -0
  67. package/server/path.ts +82 -0
  68. package/server/shift-mood.ts +43 -0
  69. package/{adapters/claude/storage → server}/state.ts +200 -7
  70. package/server/suggestions.ts +335 -0
  71. package/server/theme.ts +53 -0
  72. package/server/xp.ts +435 -0
  73. package/{adapters/claude/skills → skills}/buddy/SKILL.md +48 -4
  74. package/statusline/buddy-status.sh +397 -0
  75. package/statusline/combined-status.sh +134 -0
  76. package/statusline/emoji-widths.data +4 -0
  77. package/adapters/claude/hooks/hooks.json +0 -35
  78. package/adapters/claude/hooks/react.sh +0 -204
  79. package/adapters/claude/popup/buddy-popup.sh +0 -92
  80. package/adapters/claude/popup/buddy-render.sh +0 -540
  81. package/adapters/claude/popup/popup-manager.sh +0 -355
  82. package/adapters/claude/rendering/art.ts +0 -264
  83. package/adapters/claude/server/index.ts +0 -752
  84. package/adapters/claude/server/instructions.ts +0 -24
  85. package/adapters/claude/server/resources.ts +0 -38
  86. package/adapters/claude/statusline/buddy-status.sh +0 -389
  87. package/adapters/claude/storage/achievements.ts +0 -253
  88. package/adapters/claude/storage/identity.ts +0 -13
  89. package/adapters/claude/storage/paths.ts +0 -24
  90. package/adapters/claude/storage/settings.ts +0 -41
  91. package/adapters/pi/README.md +0 -83
  92. package/adapters/pi/tools.ts +0 -6
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env bash
2
+ # coding-buddy Stop hook — proactive suggestion detection
3
+ # Detects coding patterns and offers suggestions when relevant.
4
+ #
5
+ # Combined with buddy-comment.sh to avoid running two separate Stop hooks.
6
+
7
+ # shellcheck source=../scripts/paths.sh
8
+ source "$(dirname "${BASH_SOURCE[0]}")/../scripts/paths.sh"
9
+
10
+ STATE_DIR="$BUDDY_STATE_DIR"
11
+ CONFIG_FILE="$STATE_DIR/config.json"
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
package/package.json CHANGED
@@ -1,65 +1,119 @@
1
1
  {
2
- "name": "@ramarivera/coding-buddy",
3
- "version": "0.4.0-alpha.8",
4
- "description": "Persistent coding companion for Claude Code and pi",
5
- "type": "module",
6
- "bin": {
7
- "coding-buddy": "./cli/index.ts"
2
+ "name": "@ramarivera/coding-buddy",
3
+ "version": "0.6.0",
4
+ "description": "Persistent coding companion for Claude Code, Pi, and Oh My Pi",
5
+ "type": "module",
6
+ "omp": {
7
+ "extensions": [
8
+ "./adapters/omp/index.ts"
9
+ ]
10
+ },
11
+ "pi": {
12
+ "extensions": [
13
+ "./adapters/pi/index.ts"
14
+ ]
15
+ },
16
+ "bin": {
17
+ "coding-buddy": "cli/index.ts"
18
+ },
19
+ "scripts": {
20
+ "postinstall": "node scripts/postinstall.cjs",
21
+ "server": "bun run server/index.ts",
22
+ "pick": "bun run cli/pick.ts",
23
+ "hunt": "bun run cli/hunt.ts",
24
+ "install-buddy": "bun run cli/install.ts",
25
+ "show": "bun run cli/show.ts",
26
+ "doctor": "bun run cli/doctor.ts",
27
+ "test-statusline": "bun run cli/test-statusline.ts",
28
+ "backup": "bun run cli/backup.ts",
29
+ "settings": "bun run cli/settings.ts",
30
+ "disable": "bun run cli/disable.ts",
31
+ "enable": "bun run cli/install.ts",
32
+ "upgrade": "bun run cli/upgrade.ts",
33
+ "uninstall": "bun run cli/uninstall.ts",
34
+ "help": "bun run cli/index.ts help",
35
+ "buddy-shell": "npx tsx cli/buddy-shell.ts",
36
+ "tui": "bun run cli/tui.tsx",
37
+ "test": "bun test",
38
+ "typecheck": "tsc --noEmit",
39
+ "gen:emoji-widths": "bun run scripts/gen-emoji-widths.ts"
40
+ },
41
+ "files": [
42
+ "core/",
43
+ "adapters/",
44
+ "server/",
45
+ "cli/",
46
+ "skills/",
47
+ "hooks/",
48
+ "statusline/",
49
+ "scripts/",
50
+ ".claude-plugin/",
51
+ "!**/*.test.ts"
52
+ ],
53
+ "keywords": [
54
+ "coding-buddy",
55
+ "claude-code",
56
+ "pi",
57
+ "oh-my-pi",
58
+ "omp",
59
+ "buddy",
60
+ "companion",
61
+ "mcp",
62
+ "terminal-pet",
63
+ "tamagotchi",
64
+ "pi-package",
65
+ "omp-extension"
66
+ ],
67
+ "repository": {
68
+ "type": "git",
69
+ "url": "git+https://github.com/1270011/claude-buddy.git"
70
+ },
71
+ "homepage": "https://github.com/1270011/claude-buddy#readme",
72
+ "license": "MIT",
73
+ "dependencies": {
74
+ "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
75
+ "@modelcontextprotocol/sdk": "^1.12.1",
76
+ "@xterm/addon-serialize": "^0.14.0",
77
+ "@xterm/headless": "^6.0.0",
78
+ "ink": "^7.0.0",
79
+ "pino": "^10.3.1",
80
+ "react": "^19.2.5"
81
+ },
82
+ "devDependencies": {
83
+ "@mariozechner/pi-coding-agent": "0.66.1",
84
+ "@oh-my-pi/pi-agent-core": "17.0.9",
85
+ "@oh-my-pi/pi-ai": "17.0.9",
86
+ "@oh-my-pi/pi-coding-agent": "17.0.9",
87
+ "@oh-my-pi/pi-catalog": "17.0.9",
88
+ "@types/react": "^19.2.14",
89
+ "bun-types": "^1.3.11",
90
+ "tsx": "^4.21.0",
91
+ "typescript": "^6.0.2"
92
+ },
93
+ "trustedDependencies": [
94
+ "@homebridge/node-pty-prebuilt-multiarch"
95
+ ],
96
+ "publishConfig": {
97
+ "access": "public"
98
+ },
99
+ "peerDependencies": {
100
+ "@mariozechner/pi-coding-agent": "0.66.1",
101
+ "@oh-my-pi/pi-agent-core": "17.0.9",
102
+ "@oh-my-pi/pi-ai": "17.0.9",
103
+ "@oh-my-pi/pi-coding-agent": "17.0.9"
104
+ },
105
+ "peerDependenciesMeta": {
106
+ "@mariozechner/pi-coding-agent": {
107
+ "optional": true
8
108
  },
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"
109
+ "@oh-my-pi/pi-agent-core": {
110
+ "optional": true
25
111
  },
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"
112
+ "@oh-my-pi/pi-ai": {
113
+ "optional": true
44
114
  },
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
- "pino": "^10.3.1"
60
- },
61
- "devDependencies": {
62
- "bun-types": "^1.3.11",
63
- "typescript": "^6.0.2"
115
+ "@oh-my-pi/pi-coding-agent": {
116
+ "optional": true
64
117
  }
118
+ }
65
119
  }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Generate statusline/emoji-widths.data from Unicode's Emoji_Presentation
3
+ * property. The bash statusline renderer loads this file because awk has no
4
+ * Unicode property escapes; server/art.ts uses the same regex directly.
5
+ *
6
+ * Regenerate: bun run gen:emoji-widths
7
+ *
8
+ * The emitted list holds decimal codepoints in U+2600-U+27BF that default to
9
+ * emoji rendering (2 cols). Everything outside that range is handled by
10
+ * simple range checks in the bash/TS code, so we only need to export the
11
+ * ambiguous Miscellaneous Symbols + Dingbats block.
12
+ */
13
+
14
+ import { writeFileSync } from "fs";
15
+ import { join } from "path";
16
+
17
+ const OUT = join(import.meta.dir, "..", "statusline", "emoji-widths.data");
18
+
19
+ const EMOJI_PRES_RE = /\p{Emoji_Presentation}/u;
20
+ const codepoints: number[] = [];
21
+ for (let cp = 0x2600; cp <= 0x27BF; cp++) {
22
+ if (EMOJI_PRES_RE.test(String.fromCodePoint(cp))) codepoints.push(cp);
23
+ }
24
+
25
+ const content = [
26
+ "# Auto-generated by scripts/gen-emoji-widths.ts — do not edit by hand.",
27
+ "# Decimal codepoints in U+2600-U+27BF with Unicode Emoji_Presentation=Yes.",
28
+ "# Regenerate with: bun run gen:emoji-widths",
29
+ codepoints.join(" "),
30
+ "",
31
+ ].join("\n");
32
+
33
+ writeFileSync(OUT, content);
34
+ console.log(`Wrote ${codepoints.length} codepoints to ${OUT}`);
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ # Path resolvers for coding-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
41
+
42
+ # Windows: Chocolatey installs a PowerShell shim for jq that doesn't work in
43
+ # Git Bash. If jq is not found on PATH, check the real Chocolatey binary path
44
+ # and common Windows locations, then prepend to PATH so all scripts find it.
45
+ if ! command -v jq >/dev/null 2>&1; then
46
+ for _jq_candidate in \
47
+ "/c/ProgramData/chocolatey/lib/jq/tools/jq.exe" \
48
+ "/c/tools/jq/jq.exe" \
49
+ "$HOME/bin/jq.exe" \
50
+ "/usr/local/bin/jq.exe"; do
51
+ if [ -x "$_jq_candidate" ]; then
52
+ export PATH="$(dirname "$_jq_candidate"):$PATH"
53
+ break
54
+ fi
55
+ done
56
+ fi
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Post-install check: warn if bun is unavailable.
4
+ * Installation must succeed for Node-only Pi/OMP consumers; Claude-only commands
5
+ * will diagnose bun at runtime.
6
+ */
7
+
8
+ const { spawn } = require("child_process");
9
+
10
+ function checkBun() {
11
+ return new Promise((resolve) => {
12
+ const proc = spawn("bun", ["--version"], { stdio: "pipe" });
13
+ let output = "";
14
+ proc.stdout.on("data", (data) => {
15
+ output += data.toString();
16
+ });
17
+ proc.on("close", (code) => {
18
+ if (code === 0) {
19
+ console.log(`✓ bun ${output.trim()} detected`);
20
+ resolve(true);
21
+ } else {
22
+ console.warn(
23
+ "Bun is required only for Claude-only commands; install it from https://bun.sh or run `npm install -g bun`."
24
+ );
25
+ resolve(false);
26
+ }
27
+ });
28
+ proc.on("error", () => {
29
+ console.warn(
30
+ "Bun is required only for Claude-only commands; install it from https://bun.sh or run `npm install -g bun`."
31
+ );
32
+ resolve(false);
33
+ });
34
+ });
35
+ }
36
+
37
+ checkBun().then((ok) => {
38
+ if (!ok) {
39
+ console.warn("Install completed, but bun is required to run Claude-only commands.");
40
+ }
41
+ process.exit(0);
42
+ });
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ # Render the statusline for every species at every frame index. Used for
3
+ # visual review after changes to art pipeline (server render + bash cycler).
4
+ #
5
+ # Usage:
6
+ # ./scripts/snapshot-statusline.sh # print to stdout
7
+ # ./scripts/snapshot-statusline.sh out/fixtures # write one file per case
8
+ #
9
+ # Uses a temp BUDDY_STATE_DIR so it never touches the user's real state.
10
+ set -euo pipefail
11
+
12
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
13
+ OUT_DIR="${1:-}"
14
+
15
+ TMP_STATE=$(mktemp -d)
16
+ trap 'rm -rf "$TMP_STATE"' EXIT
17
+ export CLAUDE_CONFIG_DIR="$TMP_STATE"
18
+ mkdir -p "$TMP_STATE/buddy-state"
19
+
20
+ SPECIES_LIST=(duck goose blob cat dragon octopus owl penguin turtle snail
21
+ ghost axolotl capybara cactus robot rabbit mushroom chonk)
22
+ HATS=(none crown propeller wizard)
23
+ # Ticks hit all 4 distinct frames: 0→F0, 4→F1, 8→blink, 11→F2.
24
+ TICKS=(0 4 8 11)
25
+
26
+ for species in "${SPECIES_LIST[@]}"; do
27
+ for hat in "${HATS[@]}"; do
28
+ # Generate status.json with pre-rendered frames via a small bun script.
29
+ bun -e "
30
+ import { writeStatusState } from '${ROOT}/server/state.ts';
31
+ writeStatusState({
32
+ bones: {
33
+ species: '${species}', eye: '\\u00b0', hat: '${hat}',
34
+ rarity: 'common', shiny: false,
35
+ stats: { DEBUGGING: 50, PATIENCE: 50, CHAOS: 50, WISDOM: 50, SNARK: 50 },
36
+ peak: 'DEBUGGING', dump: 'PATIENCE',
37
+ },
38
+ name: 'Test', personality: '', hatchedAt: 0, userId: 'x',
39
+ });
40
+ " > /dev/null
41
+
42
+ for tick in "${TICKS[@]}"; do
43
+ label="${species} hat=${hat} tick=${tick}"
44
+ body=$(BUDDY_FAKE_NOW="$tick" COLUMNS=80 \
45
+ "$ROOT/statusline/buddy-status.sh" < /dev/null)
46
+ if [ -n "$OUT_DIR" ]; then
47
+ mkdir -p "$OUT_DIR"
48
+ printf '%s\n' "$body" > "$OUT_DIR/${species}_${hat}_t${tick}.out"
49
+ else
50
+ printf '=== %s ===\n%s\n\n' "$label" "$body"
51
+ fi
52
+ done
53
+ done
54
+ done