opencode-buddy 0.2.1 → 0.3.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.
package/README.md CHANGED
@@ -1,204 +1,125 @@
1
1
  # opencode-buddy
2
2
 
3
- A virtual ASCII pet companion for [opencode](https://opencode.ai). Use it as a `/buddy` slash command inside opencode, or as a tmux side pane next to it.
4
-
5
- ```
6
- opencode TUI buddy pane
7
- ┌──────────────────────┐ ┌──────────────────────┐
8
- │ ╭─ Quack the duck ─╮ │
9
- your opencode │ __ │ │
10
- TUI here │ <(o )___ │ │
11
- ( ._> / │ │
12
- │ │ `--' │
13
-
14
- hunger ████████░░ 79
15
- │ │ happy ████████░░ 79
16
- energy ██████████ 100 │ │
17
- keys: f feed · ...
18
- └──────────────────────┘ └──────────────────────┘
19
- ```
20
-
21
- The buddy reacts to what you're doing — idles when you idle, cheers when opencode finishes a turn, gets scared when a session errors, falls asleep when energy runs low.
22
-
23
- ## Why?
24
-
25
- Claude Code v2.1.88 had a fun `/buddy` easter egg (an ASCII pet companion). When Anthropic removed it in v2.1.97 the community revolted. This is the opencode equivalent: your own buddy, open source, never going to be taken away.
3
+ A virtual ASCII pet companion that lives in the opencode TUI sidebar. Hatches, feeds, plays, and reacts to what you're coding all without leaving opencode.
4
+
5
+ ```
6
+ ┌──────────────────────────┐
7
+ │ opencode TUI │
8
+
9
+ > your prompt here
10
+
11
+ sidebar
12
+ ┌──────────────────────┐
13
+ Quack the duck │ │
14
+ __ │ │
15
+ <(o )___ │ │
16
+ ( ._> / │ │
17
+ `--' │ │
18
+ │ │ ──────────────────── │ │
19
+ │ │ hunger ████████░░ 79 │ │
20
+ │ │ happy ████████░░ 79 │ │
21
+ energy ██████████ 100│
22
+ │ │ idle · Lv 1 · xp 0 │ │
23
+ └──────────────────────┘ │
24
+ └──────────────────────────┘
25
+ ```
26
+
27
+ The buddy blinks every 600ms. Switch species, watch it cheer when opencode finishes a turn, and get scared when a session errors.
26
28
 
27
29
  ## Install
28
30
 
29
- Requires Node.js ≥ 18. tmux is only needed for the side-pane mode (Mode B).
30
-
31
- ### Mode A: `/buddy` inside opencode (recommended)
31
+ Requires Node.js ≥ 18 and opencode 1.15.
32
32
 
33
33
  ```bash
34
34
  npm install -g opencode-buddy
35
- opencode-buddy install
36
35
  ```
37
36
 
38
- This writes `~/.config/opencode/commands/buddy.md` and adds
39
- `opencode-buddy` to your `opencode.json` `plugin` list. **Restart
40
- opencode.** Then in the TUI:
37
+ Add to your `~/.config/opencode/opencode.json`:
41
38
 
42
- ```
43
- /buddy show current status
44
- /buddy feed feed the buddy
45
- /buddy play play with the buddy (+xp)
46
- /buddy rest tuck the buddy in
47
- /buddy switch cat become a cat
48
- /buddy switch dragon become a dragon
49
- /buddy rename Mia rename
50
- /buddy hatch dragon Sparky start fresh
51
- /buddy ascii render ASCII art inline
52
- /buddy help list all actions
39
+ ```json
40
+ {
41
+ "plugin": ["opencode-buddy"]
42
+ }
53
43
  ```
54
44
 
55
- The buddy's mood is updated automatically: `session.idle` makes it
56
- celebrate, `session.error` makes it scared, low energy sends it to sleep.
45
+ Restart opencode. The buddy appears in the sidebar.
57
46
 
58
- ### Mode B: tmux side pane (optional, for a permanent companion)
47
+ ## How to interact
59
48
 
60
- ```bash
61
- npm install -g opencode-buddy
62
- ```
49
+ The buddy has a `buddy` tool the LLM can call. Just talk to it:
63
50
 
64
- In any tmux pane (next to your opencode pane):
65
-
66
- ```bash
67
- opencode-buddy start
68
51
  ```
69
-
70
- A 28-column side pane splits off to the right and the buddy appears
71
- inside it, rendered in full ASCII with color. Press keys in the buddy
72
- pane to interact:
73
-
74
- | Key | Action |
75
- | --- | --- |
76
- | `f` | Feed (+25 hunger) |
77
- | `p` | Play (+20 happiness, +5 xp) |
78
- | `z` | Rest (+30 energy) |
79
- | `r` | Rename (prompts in the pane) |
80
- | `s` | Switch species (cycles through 6) |
81
- | `n` | Hatch a new buddy (resets state) |
82
- | `q` | Quit (kills the side pane) |
83
-
84
- **Modes A and B share the same persisted state** at
85
- `~/.config/opencode-buddy/state.json`, so feeding the buddy via
86
- `/buddy feed` in the TUI will also be reflected in the side pane and
87
- vice versa.
88
-
89
- ### Install from source
90
-
91
- ```bash
92
- git clone https://github.com/AMark-CS/opencode-buddy
93
- cd opencode-buddy
94
- npm install # only for plugin dev deps
95
- npm link # adds `opencode-buddy` to PATH
96
- opencode-buddy install
52
+ /buddy
53
+ /feed the buddy
54
+ /switch to a dragon
55
+ /rename it to Sparky
56
+ /can you show me the buddy's ascii art
97
57
  ```
98
58
 
99
- ### Uninstall
59
+ The LLM will call the `buddy` tool with the right action. Available actions:
100
60
 
101
- ```bash
102
- opencode-buddy uninstall
103
- npm uninstall -g opencode-buddy
104
- ```
105
-
106
- ## Headless commands
107
-
108
- ```bash
109
- opencode-buddy stats # one-line status printout
110
- opencode-buddy feed # feed from any terminal
111
- opencode-buddy hatch dragon Sparky # start fresh as a dragon named Sparky
112
- opencode-buddy switch cat # become a cat
113
- opencode-buddy rename CaptainQuack # rename
114
- opencode-buddy notify done # nudge the buddy from a hook
115
- opencode-buddy path # print path to the state file
116
- ```
117
-
118
- These work whether or not the TUI or tmux side pane is running.
119
- | `n` | Hatch a new buddy (resets state) |
120
- | `q` | Quit (kills the side pane) |
121
-
122
- ### Headless commands
123
-
124
- ```bash
125
- opencode-buddy stats # one-line status printout
126
- opencode-buddy feed # feed from any terminal
127
- opencode-buddy hatch dragon Sparky # start fresh as a dragon named Sparky
128
- opencode-buddy switch cat # become a cat
129
- opencode-buddy rename CaptainQuack # rename
130
- opencode-buddy notify done # nudge the buddy from a hook / hookup
131
- opencode-buddy path # print path to the state file
132
- ```
61
+ | Action | Effect |
62
+ | --- | --- |
63
+ | `status` | Print current stats |
64
+ | `feed` | +25 hunger, -5 energy |
65
+ | `play` | +20 happiness, -5 hunger, +5 xp |
66
+ | `rest` | +30 energy |
67
+ | `switch <species>` | Change to duck, cat, dragon, axolotl, robot, or ghost |
68
+ | `rename <name>` | Rename (max 20 chars) |
69
+ | `hatch [species] [name]` | Start a brand new buddy |
70
+ | `ascii [frame]` | Return rendered ASCII art |
71
+ | `path` | Path to the state file |
72
+ | `help` | List actions |
133
73
 
134
- ## The six species
74
+ ## Six species
135
75
 
136
76
  ```
137
77
  duck cat dragon
138
- __ /\\_/\ /\\_/\
78
+ __ /\_/\ /\_/\
139
79
  <(o )___ ( o.o ) ( o o ) ~~
140
80
  ( ._> / > ^ < > ^ < /
141
- `--' /| |\\ /| |\\
142
- ~ idle ~ (_| |_) (_| |_)
81
+ `--' /| |\ /| |\
82
+ ~ idle ~ (_| |_) (_| |_)
143
83
  meow rawr
144
84
 
145
85
  axolotl robot ghost
146
86
  ^___^ [ O . O ] .-"\"-.
147
- (o . o) /|#####|\\ ( o . o )
148
- \\|_|_|/ / |#####| \\ | ~ ~ |
149
- \\| |/ | | | |
150
- ) ( /| | | |\\ \\uuuuu/
87
+ (o . o) /|#####|\ ( o . o )
88
+ \|_|_|/ / |#####| \ | ~ ~ |
89
+ \| |/ | | | |
90
+ ) ( /| | | |\ \uuuuu/
151
91
  ~ ambien beep boo
152
92
  ```
153
93
 
154
- Each has frames for `idle / working / celebrating / scared / sleeping`.
94
+ Each species has a per-character color palette. Idle state has 3 frames (blink loop) at 600ms per frame.
155
95
 
156
96
  ## How it works
157
97
 
158
- * **As an opencode plugin** (`src/plugin.js`): registers a `buddy` tool the LLM can call, and listens to `session.idle` / `session.error` events to update the buddy's mood automatically. The `/buddy` slash command in `~/.config/opencode/commands/buddy.md` is a thin prompt that tells the LLM to use that tool.
159
- * **As a tmux sidecar** (`bin/opencode-buddy.js start`): runs a separate process in a right-side tmux pane. Uses `tmux capture-pane` to peek at the opencode pane text and infer whether opencode is generating, errored, or idle.
160
- * **Shared state** at `~/.config/opencode-buddy/state.json`. Both the plugin and the sidecar read and write the same file, so a `/buddy feed` in the TUI is immediately visible in the side pane and vice versa.
161
- * **Zero runtime dependencies.** Pure Node.js + ANSI escapes for the renderer, `spawnSync("tmux", ...)` for pane control. The only dev-time dependency is `@opencode-ai/plugin` (used to define the opencode tool).
98
+ - **TUI plugin** (`src/tui-plugin.jsx`): registers `sidebar_content` slot, renders a Solid component that polls `~/.config/opencode-buddy/state.json` every 1.5s and animates at 6fps. Pure opencode-native no tmux, no separate process.
99
+ - **Server plugin** (`src/server-plugin.js`): registers a `buddy` tool the LLM can call. The same package exports both entrypoints (`main` for server, `exports["./tui"]` for TUI).
100
+ - **State** at `~/.config/opencode-buddy/state.json` (~/.config is cross-platform via os.homedir()). Attributes decay over time. Sessions auto-celebrate on `session.idle` and auto-scare on `session.error`.
101
+
102
+ ## Uninstall
162
103
 
163
- ## Limitations
104
+ ```bash
105
+ npm uninstall -g opencode-buddy
106
+ ```
164
107
 
165
- * **Activity inference is best-effort.** If opencode's prompt text changes, the heuristics may misclassify. The `notify` subcommand is the explicit way to push events.
166
- * **The /buddy command is LLM-mediated.** The slash command is a prompt that tells opencode's LLM to call the `buddy` tool. It works only inside an active opencode session. For a direct terminal-friendly interface use `opencode-buddy <subcommand>` or the tmux side pane.
167
- * **No animations yet.** Frames are static; the renderer just swaps the frame when the state changes. Adding 2-3 frame loops per state is straightforward (PRs welcome).
108
+ Then remove `"opencode-buddy"` from your `opencode.json` `plugin` array.
168
109
 
169
110
  ## Project layout
170
111
 
171
112
  ```
172
113
  opencode-buddy/
173
- ├── bin/
174
- │ └── opencode-buddy.js # CLI entry
175
- ├── src/
176
- │ ├── plugin.js # opencode plugin (exports BuddyPlugin)
177
- │ ├── install.js # one-shot installer for /buddy + opencode.json
178
- │ ├── index.js # CLI runtime + headless commands
179
- │ ├── tui.js # ANSI renderer for the side pane
180
- │ ├── tmux.js # tmux capture / split helpers
181
- │ ├── persistence.js # state file at ~/.config/opencode-buddy/
182
- │ └── buddy/
183
- │ ├── species.js # ASCII art + color palettes
184
- │ └── state.js # state machine + attribute math
185
- ├── test/
186
- │ ├── smoke.js # state / species unit tests
187
- │ ├── plugin.smoke.js # plugin tool unit tests
188
- │ └── e2e.js # full tmux roundtrip
189
114
  ├── package.json
190
- ├── LICENSE # MIT
191
115
  ├── README.md
192
- ├── PUSH.md # GitHub push guide
193
- └── PUBLISH.md # npm publish guide
194
- ```
195
-
196
- ## Development
197
-
198
- ```bash
199
- npm test # runs all unit tests (smoke + plugin)
200
- npm run test:e2e # runs end-to-end tmux test (creates & kills a session)
201
- npm run install-buddy # runs the opencode install command
116
+ ├── LICENSE
117
+ └── src/
118
+ ├── tui-plugin.jsx # v2 TUI plugin, mounts to sidebar_content
119
+ ├── server-plugin.js # v1 server plugin, exposes `buddy` tool
120
+ ├── species.js # ASCII art + per-species color palettes
121
+ ├── state.js # state machine (hunger/happiness/energy decay)
122
+ └── persistence.js # state file reader/writer
202
123
  ```
203
124
 
204
125
  ## License
package/package.json CHANGED
@@ -1,42 +1,30 @@
1
1
  {
2
2
  "name": "opencode-buddy",
3
- "version": "0.2.1",
4
- "description": "A virtual ASCII pet companion for opencode. Works as a tmux sidecar or an in-TUI /buddy slash command.",
3
+ "version": "0.3.0",
4
+ "description": "A virtual ASCII pet companion that lives in the opencode TUI sidebar. Hatches, feeds, plays, and reacts to what you're coding.",
5
5
  "type": "module",
6
- "main": "src/plugin.js",
6
+ "main": "src/server-plugin.js",
7
7
  "exports": {
8
- ".": "./src/plugin.js",
9
- "./cli": "./bin/opencode-buddy.js",
8
+ ".": "./src/server-plugin.js",
9
+ "./tui": "./src/tui-plugin.jsx",
10
10
  "./package.json": "./package.json"
11
11
  },
12
- "bin": {
13
- "opencode-buddy": "bin/opencode-buddy.js"
14
- },
15
12
  "files": [
16
- "bin",
17
13
  "src",
18
14
  "README.md",
19
15
  "LICENSE"
20
16
  ],
21
- "scripts": {
22
- "start": "node bin/opencode-buddy.js start",
23
- "hatch": "node bin/opencode-buddy.js hatch",
24
- "stats": "node bin/opencode-buddy.js stats",
25
- "install-buddy": "node bin/opencode-buddy.js install",
26
- "test": "node --test test/smoke.js test/plugin.smoke.js",
27
- "test:e2e": "node test/e2e.js"
28
- },
29
17
  "keywords": [
30
18
  "opencode",
31
19
  "opencode-plugin",
32
- "tmux",
20
+ "tui",
33
21
  "pet",
34
22
  "tamagotchi",
35
23
  "ascii",
36
- "tui",
37
24
  "companion",
38
25
  "buddy",
39
- "virtual-pet"
26
+ "virtual-pet",
27
+ "sidebar"
40
28
  ],
41
29
  "engines": {
42
30
  "node": ">=18"
@@ -16,6 +16,13 @@ export async function load() {
16
16
  }
17
17
  }
18
18
 
19
+ export async function save(state) {
20
+ await fs.mkdir(DIR, { recursive: true });
21
+ const tmp = FILE + ".tmp";
22
+ await fs.writeFile(tmp, JSON.stringify(state, null, 2));
23
+ await fs.rename(tmp, FILE);
24
+ }
25
+
19
26
  export async function mtime() {
20
27
  try {
21
28
  const st = await fs.stat(FILE);
@@ -26,13 +33,6 @@ export async function mtime() {
26
33
  }
27
34
  }
28
35
 
29
- export async function save(state) {
30
- await fs.mkdir(DIR, { recursive: true });
31
- const tmp = FILE + ".tmp";
32
- await fs.writeFile(tmp, JSON.stringify(state, null, 2));
33
- await fs.rename(tmp, FILE);
34
- }
35
-
36
36
  export function pathForDisplay() {
37
37
  return FILE;
38
38
  }
@@ -0,0 +1,133 @@
1
+ // v1 server plugin. Provides the `buddy` tool the LLM can call.
2
+ // Loaded by opencode via package.json `main` (server-kind entrypoint).
3
+
4
+ import { tool } from "@opencode-ai/plugin";
5
+ import * as state from "./state.js";
6
+ import * as persistence from "./persistence.js";
7
+ import { SPECIES, renderFrame, maxFrameHeight } from "./species.js";
8
+
9
+ const SPECIES_LIST = SPECIES.join(", ");
10
+
11
+ const DESCRIPTION = `Interact with your virtual buddy companion. Pass an "action" argument.
12
+
13
+ Actions:
14
+ status - print current stats as a one-liner
15
+ feed - feed the buddy (+25 hunger, -5 energy)
16
+ play - play with the buddy (+20 happiness, +5 xp)
17
+ rest - let the buddy rest (+30 energy)
18
+ switch <species> - change species (${SPECIES_LIST})
19
+ rename <name> - rename the buddy (max 20 chars)
20
+ hatch [species] [name] - start a brand new buddy
21
+ ascii [frame] - render the current buddy as ASCII art
22
+ help - list available actions
23
+
24
+ The buddy lives in the opencode sidebar. Use this tool whenever the
25
+ user wants to interact with their buddy.`;
26
+
27
+ function summary(s) {
28
+ return `${s.name} the ${s.species} | Lv ${s.level} | ${s.state} | hunger ${Math.floor(s.hunger)} happiness ${Math.floor(s.happiness)} energy ${Math.floor(s.energy)} | xp ${s.xp}/${s.level * 50}`;
29
+ }
30
+
31
+ async function loadOrInit() {
32
+ const s = await persistence.load();
33
+ if (s) return s;
34
+ const fresh = state.hatch();
35
+ await persistence.save(fresh);
36
+ return fresh;
37
+ }
38
+
39
+ async function BuddyPlugin() {
40
+ return {
41
+ tool: {
42
+ buddy: tool({
43
+ description: DESCRIPTION,
44
+ args: {
45
+ action: tool.schema
46
+ .string()
47
+ .describe(
48
+ `One of: status, feed, play, rest, switch, rename, hatch, ascii, help. For switch/rename/hatch, also pass the "species" or "name" argument.`,
49
+ ),
50
+ species: tool.schema
51
+ .string()
52
+ .optional()
53
+ .describe(`Species for switch/hatch: ${SPECIES_LIST}`),
54
+ name: tool.schema
55
+ .string()
56
+ .optional()
57
+ .describe(`Name for rename/hatch (max 20 chars).`),
58
+ frame: tool.schema
59
+ .number()
60
+ .int()
61
+ .min(0)
62
+ .max(10)
63
+ .optional()
64
+ .describe(`Frame index for ascii action.`),
65
+ },
66
+ async execute(args) {
67
+ let s = await loadOrInit();
68
+ const action = (args.action || "").toLowerCase();
69
+ const species = (args.species || "").toLowerCase();
70
+ const name = args.name;
71
+ const frame = args.frame ?? 0;
72
+
73
+ switch (action) {
74
+ case "":
75
+ case "status":
76
+ return summary(s);
77
+ case "feed":
78
+ s = state.feed(s);
79
+ await persistence.save(s);
80
+ return `${s.name} munches happily. ${summary(s)}`;
81
+ case "play":
82
+ s = state.play(s);
83
+ await persistence.save(s);
84
+ return `${s.name} plays! ${summary(s)}`;
85
+ case "rest":
86
+ s = state.rest(s);
87
+ await persistence.save(s);
88
+ return `${s.name} curls up. ${summary(s)}`;
89
+ case "switch": {
90
+ if (!species) return `Pick a species: ${SPECIES_LIST}`;
91
+ if (!SPECIES.includes(species))
92
+ return `Unknown species "${species}". Valid: ${SPECIES_LIST}`;
93
+ s = state.switchSpecies(s, species);
94
+ await persistence.save(s);
95
+ return `${s.name} transformed into a ${species}. ${summary(s)}`;
96
+ }
97
+ case "rename": {
98
+ if (!name) return `Provide a name with the "name" argument.`;
99
+ s = state.rename(s, name);
100
+ await persistence.save(s);
101
+ return `Renamed to ${s.name}. ${summary(s)}`;
102
+ }
103
+ case "hatch": {
104
+ const sp = species && SPECIES.includes(species) ? species : "duck";
105
+ s = state.hatch({ species: sp, name: name || "Buddy" });
106
+ await persistence.save(s);
107
+ return `Hatched a new ${sp} named ${s.name}. ${summary(s)}`;
108
+ }
109
+ case "ascii": {
110
+ const lines = renderFrame(s.species, s.state, frame);
111
+ return [
112
+ "```",
113
+ ...lines,
114
+ "```",
115
+ `${s.name} the ${s.species} · ${s.state}`,
116
+ `hunger ${Math.floor(s.hunger)} happy ${Math.floor(s.happiness)} energy ${Math.floor(s.energy)} xp ${s.xp}/${s.level * 50}`,
117
+ ].join("\n");
118
+ }
119
+ case "help":
120
+ return DESCRIPTION;
121
+ default:
122
+ return `Unknown action "${args.action}". Try: action=help`;
123
+ }
124
+ },
125
+ }),
126
+ },
127
+ };
128
+ }
129
+
130
+ export default {
131
+ id: "opencode-buddy",
132
+ server: BuddyPlugin,
133
+ };