opencode-buddy 0.2.0 → 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.
@@ -0,0 +1,156 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { TuiPlugin, TuiPluginApi, TuiPluginModule, TuiSlotPlugin } from "@opencode-ai/plugin/tui"
3
+ import { createSignal, createMemo, onCleanup, Show, For } from "solid-js"
4
+ import * as persistence from "./persistence.js"
5
+ import {
6
+ tick as tickState,
7
+ celebrate as celebrateState,
8
+ scared as scaredState,
9
+ deriveState,
10
+ } from "./state.js"
11
+ import { renderFrame, frameCount, frameIntervalMs } from "./species.js"
12
+
13
+ const ANIMATION_TICK_MS = frameIntervalMs()
14
+ const REFRESH_TICK_MS = 1500
15
+
16
+ function View(props: { api: TuiPluginApi; session_id: string }) {
17
+ const theme = () => props.api.theme.current
18
+ const [state, setState] = createSignal<any | null>(null)
19
+ const [frame, setFrame] = createSignal(0)
20
+ const [lastMtime, setLastMtime] = createSignal(0)
21
+
22
+ const refresh = async () => {
23
+ const mt = await persistence.mtime()
24
+ if (mt === 0) {
25
+ const fresh = {
26
+ species: "duck",
27
+ name: "Quack",
28
+ hunger: 80,
29
+ happiness: 80,
30
+ energy: 100,
31
+ xp: 0,
32
+ level: 1,
33
+ hatchedAt: Date.now(),
34
+ lastFed: Date.now(),
35
+ lastPlayed: Date.now(),
36
+ lastTick: Date.now(),
37
+ state: "idle",
38
+ stateUntil: 0,
39
+ stateReason: "",
40
+ }
41
+ await persistence.save(fresh)
42
+ setLastMtime(Date.now())
43
+ setState(fresh)
44
+ return
45
+ }
46
+ if (mt !== lastMtime()) {
47
+ setLastMtime(mt)
48
+ const onDisk = await persistence.load()
49
+ if (onDisk) {
50
+ const ticked = tickState(onDisk)
51
+ const derived = deriveState(ticked)
52
+ setState(derived)
53
+ // Persist derived state (so transient state changes survive)
54
+ persistence.save(derived).catch(() => {})
55
+ }
56
+ }
57
+ }
58
+
59
+ refresh()
60
+ const refreshTimer = setInterval(refresh, REFRESH_TICK_MS)
61
+ onCleanup(() => clearInterval(refreshTimer))
62
+
63
+ const animTimer = setInterval(() => {
64
+ setFrame((f) => f + 1)
65
+ }, ANIMATION_TICK_MS)
66
+ onCleanup(() => clearInterval(animTimer))
67
+
68
+ const s = () => state()
69
+ const species = () => s()?.species ?? "duck"
70
+ const buddyState = () => s()?.state ?? "idle"
71
+ const fc = createMemo(() => frameCount(species(), buddyState()))
72
+ const lines = createMemo(() => renderFrame(species(), buddyState(), frame() % Math.max(1, fc())))
73
+
74
+ const bar = (v: number, color: any) => {
75
+ const filled = Math.round((v / 100) * 10)
76
+ return (
77
+ <text>
78
+ <span style={{ fg: theme().success }}>{"█".repeat(filled)}</span>
79
+ <span style={{ fg: theme().borderSubtle }}>{"░".repeat(10 - filled)}</span>
80
+ </text>
81
+ )
82
+ }
83
+
84
+ return (
85
+ <Show when={s()}>
86
+ <box flexDirection="column" paddingTop={1} paddingBottom={1}>
87
+ <text fg={theme().accent}>
88
+ <b> {s()!.name} the {species()}</b>
89
+ </text>
90
+ <For each={lines()}>
91
+ {(line) => (
92
+ <text>
93
+ {" "}
94
+ {line}
95
+ </text>
96
+ )}
97
+ </For>
98
+ <text fg={theme().textMuted}> {"─".repeat(20)}</text>
99
+ <text>
100
+ {" hunger "}
101
+ {bar(s()!.hunger, theme().success)}
102
+ {" "}
103
+ {Math.floor(s()!.hunger)}
104
+ </text>
105
+ <text>
106
+ {" happy "}
107
+ {bar(s()!.happiness, theme().accent)}
108
+ {" "}
109
+ {Math.floor(s()!.happiness)}
110
+ </text>
111
+ <text>
112
+ {" energy "}
113
+ {bar(s()!.energy, theme().info)}
114
+ {" "}
115
+ {Math.floor(s()!.energy)}
116
+ </text>
117
+ <text fg={theme().textMuted}>
118
+ {" "}
119
+ {buddyState()} · Lv {s()!.level} · xp {s()!.xp}/{s()!.level * 50}
120
+ </text>
121
+ </box>
122
+ </Show>
123
+ )
124
+ }
125
+
126
+ const tui: TuiPlugin = async (api) => {
127
+ // Register the slot. `api` is captured in the closure so the slot
128
+ // function can pass it to the Solid component.
129
+ api.slots.register({
130
+ order: 500,
131
+ slots: {
132
+ sidebar_content(_ctx, props) {
133
+ return <View api={api} session_id={props.session_id} />
134
+ },
135
+ },
136
+ } as TuiSlotPlugin)
137
+
138
+ // React to opencode session events
139
+ api.event.on("session.idle", async () => {
140
+ const s = await persistence.load()
141
+ if (!s) return
142
+ await persistence.save(celebrateState(s))
143
+ })
144
+ api.event.on("session.error", async () => {
145
+ const s = await persistence.load()
146
+ if (!s) return
147
+ await persistence.save(scaredState(s))
148
+ })
149
+ }
150
+
151
+ const plugin: TuiPluginModule & { id: string } = {
152
+ id: "opencode-buddy",
153
+ tui,
154
+ }
155
+
156
+ export default plugin
@@ -1,45 +0,0 @@
1
- #!/usr/bin/env node
2
- // CLI entry point. Routes to either the headless command runner or the
3
- // interactive TUI renderer.
4
-
5
- import { runInteractive, runCommand } from "../src/index.js";
6
- import { install, uninstall } from "../src/install.js";
7
-
8
- const argv = process.argv.slice(2);
9
- const [sub, ...rest] = argv;
10
-
11
- async function main() {
12
- if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
13
- await runCommand([]);
14
- return;
15
- }
16
-
17
- if (sub === "start") {
18
- await runInteractive({ autoSplit: true });
19
- return;
20
- }
21
-
22
- if (sub === "render") {
23
- // Re-entered inside the tmux side pane by `start`. Just render.
24
- await runInteractive({ autoSplit: false });
25
- return;
26
- }
27
-
28
- if (sub === "install") {
29
- await install();
30
- return;
31
- }
32
-
33
- if (sub === "uninstall") {
34
- await uninstall();
35
- return;
36
- }
37
-
38
- // Everything else: headless command
39
- await runCommand(argv);
40
- }
41
-
42
- main().catch((err) => {
43
- console.error(`opencode-buddy: ${err.message || err}`);
44
- process.exit(1);
45
- });
@@ -1,399 +0,0 @@
1
- // Species library: 6 ASCII animals × multiple states × animation frames.
2
- //
3
- // Frame format: array of strings, each string is one row. All frames in a
4
- // state are the same width/height, so the renderer can swap them at a fixed
5
- // cadence without resizing the canvas.
6
-
7
- const RESET = "\x1b[0m";
8
-
9
- const C = {
10
- reset: RESET,
11
- dim: "\x1b[2m",
12
- bold: "\x1b[1m",
13
- red: "\x1b[31m",
14
- green: "\x1b[32m",
15
- yellow: "\x1b[33m",
16
- blue: "\x1b[34m",
17
- magenta: "\x1b[35m",
18
- cyan: "\x1b[36m",
19
- gray: "\x1b[90m",
20
- brightRed: "\x1b[91m",
21
- brightGreen: "\x1b[92m",
22
- brightYellow: "\x1b[93m",
23
- brightBlue: "\x1b[94m",
24
- brightMagenta: "\x1b[95m",
25
- brightCyan: "\x1b[96m",
26
- bgYellow: "\x1b[43m",
27
- bgRed: "\x1b[41m",
28
- };
29
-
30
- // Each frame is a multiline string. Keep them all the same dimensions
31
- // across states of the same species so the canvas doesn't jitter.
32
- const RAW = {
33
- duck: {
34
- idle: [
35
- ` __ `,
36
- ` <(o )___ `,
37
- ` ( ._> / `,
38
- ` \`--' `,
39
- ` `,
40
- ` ~ idle ~ `,
41
- ],
42
- working: [
43
- ` __ `,
44
- ` <(o )___ `,
45
- ` ( ._> / `,
46
- ` *\`--'* .. .. `,
47
- ` / / `,
48
- ` coding... `,
49
- ],
50
- celebrating: [
51
- ` \\o/ `,
52
- ` <(o )___ ! `,
53
- ` ( ._> / `,
54
- ` \`--' \\o/ `,
55
- ` `,
56
- ` \\o/ nice! `,
57
- ],
58
- scared: [
59
- ` __ `,
60
- ` <(O )___ `,
61
- ` ( ._> / `,
62
- ` \`--' !! `,
63
- ` `,
64
- ` !! oh no `,
65
- ],
66
- sleeping: [
67
- ` __ `,
68
- ` <(o )___ `,
69
- ` ( -_> z `,
70
- ` \`--' z `,
71
- ` `,
72
- ` z z z z z `,
73
- ],
74
- },
75
-
76
- cat: {
77
- idle: [
78
- ` /\\_/\\ `,
79
- ` ( o.o ) `,
80
- ` > ^ < `,
81
- ` /| |\\ `,
82
- ` (_| |_) `,
83
- ` meow `,
84
- ],
85
- working: [
86
- ` /\\_/\\ ... `,
87
- ` ( o.o ) `,
88
- ` > ^ < `,
89
- ` /| |\\ .. `,
90
- ` (_| |_) `,
91
- ` typing... `,
92
- ],
93
- celebrating: [
94
- ` /\\_/\\ `,
95
- ` ( ^.^ ) \\o/ `,
96
- ` > ~ < `,
97
- ` /| |\\ \\o/ `,
98
- ` (_| |_) `,
99
- ` purrrfect! `,
100
- ],
101
- scared: [
102
- ` /\\_/\\ !! `,
103
- ` ( O.O ) `,
104
- ` > ^ < `,
105
- ` /| |\\ `,
106
- ` (_| |_) /\\ `,
107
- ` !! hiss `,
108
- ],
109
- sleeping: [
110
- ` /\\_/\\ `,
111
- ` ( -.- ) z `,
112
- ` > ^ < `,
113
- ` /| |\\ z `,
114
- ` (_| |_) `,
115
- ` z z z z z `,
116
- ],
117
- },
118
-
119
- dragon: {
120
- idle: [
121
- ` /\\_/\\ `,
122
- ` ( o o ) ~~ `,
123
- ` > ^ < / `,
124
- ` /| |\\ `,
125
- ` (_| |_) `,
126
- ` rawr `,
127
- ],
128
- working: [
129
- ` /\\_/\\ ~~~ `,
130
- ` ( o o ) ~~ `,
131
- ` > ^ < `,
132
- ` /| |\\ ~~ `,
133
- ` (_| |_) `,
134
- ` forging... `,
135
- ],
136
- celebrating: [
137
- ` /\\^/\\ \\o/ `,
138
- ` ( ^.^ ) `,
139
- ` > ~ < \\o/ `,
140
- ` /| |\\ `,
141
- ` (_| |_) `,
142
- ` +50 xp!! \\o/ `,
143
- ],
144
- scared: [
145
- ` /\\_/\\ !! `,
146
- ` ( O O ) `,
147
- ` > ^ < `,
148
- ` /| |\\ !! `,
149
- ` (_| |_) `,
150
- ` !! yikes `,
151
- ],
152
- sleeping: [
153
- ` /\\_/\\ `,
154
- ` ( - - ) z `,
155
- ` > ^ < `,
156
- ` /| |\\ z `,
157
- ` (_| |_) `,
158
- ` z z z z z `,
159
- ],
160
- },
161
-
162
- axolotl: {
163
- idle: [
164
- ` ^___^ `,
165
- ` (o . o) `,
166
- ` \\|_|_|/ `,
167
- ` \\| |/ `,
168
- ` ) ( `,
169
- ` ~ ambien `,
170
- ],
171
- working: [
172
- ` ^___^ .. `,
173
- ` (o . o) `,
174
- ` \\|_|_|/ .. `,
175
- ` \\| |/ `,
176
- ` ) ( `,
177
- ` swimming... `,
178
- ],
179
- celebrating: [
180
- ` ^___^ \\o/ `,
181
- ` (o ^ o) `,
182
- ` \\|_|_|/ \\o/ `,
183
- ` \\| |/ `,
184
- ` ) ( `,
185
- ` \\o/ splish! `,
186
- ],
187
- scared: [
188
- ` ^___^ !! `,
189
- ` (O . O) `,
190
- ` \\|_|_|/ `,
191
- ` \\| |/ !! `,
192
- ` ) ( `,
193
- ` !! gulp `,
194
- ],
195
- sleeping: [
196
- ` ^___^ `,
197
- ` (o - o) z `,
198
- ` \\|_|_|/ `,
199
- ` \\| |/ z `,
200
- ` ) ( `,
201
- ` z z z z z `,
202
- ],
203
- },
204
-
205
- robot: {
206
- idle: [
207
- ` [ O . O ] `,
208
- ` /|#####|\\ `,
209
- ` / |#####| \\ `,
210
- ` | | `,
211
- ` /| | | |\\ `,
212
- ` beep `,
213
- ],
214
- working: [
215
- ` [ O . O ] ... `,
216
- ` /|#####|\\ `,
217
- ` / |#####| \\ .. `,
218
- ` | | `,
219
- ` /| | | |\\ `,
220
- ` compiling... `,
221
- ],
222
- celebrating: [
223
- ` [ ^ . ^ ] \\o/ `,
224
- ` /|#####|\\ `,
225
- ` / |#####| \\ `,
226
- ` | | `,
227
- ` /| | | |\\ \\o/ `,
228
- ` pass tests! `,
229
- ],
230
- scared: [
231
- ` [ O . O ] !! `,
232
- ` /|#####|\\ `,
233
- ` / |#####| \\ `,
234
- ` | | !! `,
235
- ` /| | | |\\ `,
236
- ` !! segfault `,
237
- ],
238
- sleeping: [
239
- ` [ - . - ] z `,
240
- ` /|#####|\\ `,
241
- ` / |#####| \\ z `,
242
- ` | | `,
243
- ` /| | | |\\ `,
244
- ` z z z z z `,
245
- ],
246
- },
247
-
248
- ghost: {
249
- idle: [
250
- ` .-\"\"-. `,
251
- ` ( o . o ) `,
252
- ` | ~ ~ | `,
253
- ` | | `,
254
- ` \\uuuuu/ `,
255
- ` boo `,
256
- ],
257
- working: [
258
- ` .-\"\"-. ... `,
259
- ` ( o . o ) `,
260
- ` | ~ ~ | `,
261
- ` | | `,
262
- ` \\uuuuu/ `,
263
- ` haunting... `,
264
- ],
265
- celebrating: [
266
- ` .-\"\"-. \\o/ `,
267
- ` ( ^ . ^ ) `,
268
- ` | ~ ~ | \\o/ `,
269
- ` | | `,
270
- ` \\uuuuu/ `,
271
- ` \\o/ spooky! `,
272
- ],
273
- scared: [
274
- ` .-\"\"-. !! `,
275
- ` ( O . O ) `,
276
- ` | ~ ~ | `,
277
- ` | | !! `,
278
- ` \\uuuuu/ `,
279
- ` !! yeek `,
280
- ],
281
- sleeping: [
282
- ` .-\"\"-. `,
283
- ` ( - . - ) z `,
284
- ` | ~ ~ | `,
285
- ` | | `,
286
- ` \\uuuuu/ z `,
287
- ` z z z z z `,
288
- ],
289
- },
290
- };
291
-
292
- // Per-character color map. Each species has a function that takes a char
293
- // from the raw frame and returns an ANSI-colored version. This way we
294
- // can give ducks a yellow bill, axolotls pink gills, ghosts a translucent
295
- // hue, etc.
296
- const PALETTES = {
297
- duck: (ch) => {
298
- if (ch === "o" || ch === "O") return C.brightYellow + ch + RESET;
299
- if (ch === "(" || ch === ")" || ch === "<" || ch === ">")
300
- return C.yellow + ch + RESET;
301
- if (ch === "_" || ch === "-") return C.yellow + ch + RESET;
302
- if (ch === "." || ch === "*") return C.cyan + ch + RESET;
303
- if (ch === "z") return C.gray + ch + RESET;
304
- if (ch === "!") return C.brightYellow + ch + RESET;
305
- if (ch === "~") return C.cyan + ch + RESET;
306
- if (ch === " ") return ch;
307
- return C.yellow + ch + RESET;
308
- },
309
-
310
- cat: (ch) => {
311
- if (ch === "o" || ch === "O" || ch === "^") return C.brightGreen + ch + RESET;
312
- if (ch === "!" || ch === "~") return C.brightMagenta + ch + RESET;
313
- if (ch === "z") return C.gray + ch + RESET;
314
- if (ch === ".") return C.gray + ch + RESET;
315
- if (ch === " ") return ch;
316
- return C.green + ch + RESET;
317
- },
318
-
319
- dragon: (ch) => {
320
- if (ch === "o" || ch === "O" || ch === "^") return C.brightRed + ch + RESET;
321
- if (ch === "~") return C.red + ch + RESET;
322
- if (ch === "!" || ch === "o" || ch === "O") return C.brightYellow + ch + RESET;
323
- if (ch === "z") return C.gray + ch + RESET;
324
- if (ch === " ") return ch;
325
- return C.green + ch + RESET;
326
- },
327
-
328
- axolotl: (ch) => {
329
- if (ch === "o" || ch === "O") return C.brightMagenta + ch + RESET;
330
- if (ch === "^" || ch === "_") return C.magenta + ch + RESET;
331
- if (ch === "!" || ch === "o") return C.brightYellow + ch + RESET;
332
- if (ch === "z") return C.gray + ch + RESET;
333
- if (ch === ".") return C.cyan + ch + RESET;
334
- if (ch === " ") return ch;
335
- return C.magenta + ch + RESET;
336
- },
337
-
338
- robot: (ch) => {
339
- if (ch === "O" || ch === "#") return C.brightCyan + ch + RESET;
340
- if (ch === "o") return C.cyan + ch + RESET;
341
- if (ch === ".") return C.gray + ch + RESET;
342
- if (ch === "!" || ch === "o") return C.brightYellow + ch + RESET;
343
- if (ch === "z") return C.gray + ch + RESET;
344
- if (ch === " ") return ch;
345
- return C.gray + ch + RESET;
346
- },
347
-
348
- ghost: (ch) => {
349
- if (ch === "o" || ch === "O") return C.brightCyan + ch + RESET;
350
- if (ch === "~") return C.cyan + ch + RESET;
351
- if (ch === "!" || ch === "o") return C.brightMagenta + ch + RESET;
352
- if (ch === "z") return C.gray + ch + RESET;
353
- if (ch === ".") return C.gray + ch + RESET;
354
- if (ch === " ") return ch;
355
- return C.brightCyan + ch + RESET;
356
- },
357
- };
358
-
359
- export const SPECIES = ["duck", "cat", "dragon", "axolotl", "robot", "ghost"];
360
-
361
- export const STATES = ["idle", "working", "celebrating", "scared", "sleeping"];
362
-
363
- // Random flavor line shown under the buddy in idle / sleeping.
364
- export const FLAVOR = {
365
- duck: ["quack.", "need code.", "swimming in semicolons"],
366
- cat: ["purr.", "knock something off desk?", "yarn > threads"],
367
- dragon: ["rawr.", "hoarding stack traces", "breathing fire on bugs"],
368
- axolotl: ["ambien?", "regenerating tail...", "neotenic forever"],
369
- robot: ["beep boop", "01010110", "needs oil"],
370
- ghost: ["boo.", "haunting the stack", "passed on... to prod"],
371
- };
372
-
373
- function targetWidth() {
374
- // All species share this width so the rendered canvas never resizes when
375
- // a state changes. Each row is padded (or trimmed) to this length.
376
- return 20;
377
- }
378
-
379
- export function paint(species, state) {
380
- const frame = RAW[species][state];
381
- const palette = PALETTES[species];
382
- const w = targetWidth();
383
- return frame.map((row) => {
384
- let out = "";
385
- // Use [...row] to count grapheme-ish units. ASCII art is pure ASCII
386
- // so spread over code units is fine and matches visual columns.
387
- for (const ch of row) out += palette(ch);
388
- if (out.length < w) out += " ".repeat(w - out.length);
389
- return out;
390
- });
391
- }
392
-
393
- export function frameHeight(species) {
394
- return RAW[species].idle.length;
395
- }
396
-
397
- export function frameWidth() {
398
- return targetWidth();
399
- }