opencode-avatar 0.3.0 → 0.3.2
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 +16 -31
- package/dist/electron.js +12 -9
- package/dist/index.js +17 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A dynamic desktop avatar plugin for OpenCode that displays animated character reactions based on your coding activities.
|
|
4
4
|
|
|
5
|
-
<div align="center"><img src="
|
|
5
|
+
<div align="center"><img src="https://github.com/user-attachments/assets/504043c9-954d-4cfd-93ff-0904d2d4eb95" alt="Avatar" width="300" /></div>
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
@@ -13,15 +13,10 @@ A dynamic desktop avatar plugin for OpenCode that displays animated character re
|
|
|
13
13
|
- **Non-Intrusive**: Appears without stealing focus, stays on top
|
|
14
14
|
- **Auto-Shutdown**: Automatically closes when OpenCode exits
|
|
15
15
|
- **Toast Notifications**: Shows progress for avatar generation
|
|
16
|
+
- **Customizable Prompts**: Optional prompt configuration for personalized avatar styles
|
|
16
17
|
|
|
17
18
|
## Installation
|
|
18
19
|
|
|
19
|
-
### Option 1: From NPM (Recommended)
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npm install -g opencode-avatar
|
|
23
|
-
```
|
|
24
|
-
|
|
25
20
|
Then add to your OpenCode config:
|
|
26
21
|
|
|
27
22
|
```json
|
|
@@ -31,15 +26,6 @@ Then add to your OpenCode config:
|
|
|
31
26
|
}
|
|
32
27
|
```
|
|
33
28
|
|
|
34
|
-
### Option 2: Local Development
|
|
35
|
-
|
|
36
|
-
1. Clone this repository
|
|
37
|
-
2. Run `npm install`
|
|
38
|
-
3. Run `npm run build`
|
|
39
|
-
4. Copy to your OpenCode plugins directory:
|
|
40
|
-
- Project: `.opencode/plugins/`
|
|
41
|
-
- Global: `~/.config/opencode/plugins/`
|
|
42
|
-
|
|
43
29
|
## Configuration
|
|
44
30
|
|
|
45
31
|
### API Key Configuration
|
|
@@ -54,6 +40,19 @@ Create a config file at `~/.config/opencode/opencode-avatar.json`:
|
|
|
54
40
|
|
|
55
41
|
Get your FAL.ai API key from [fal.ai](https://fal.ai).
|
|
56
42
|
|
|
43
|
+
### Custom Prompt Configuration (Optional)
|
|
44
|
+
|
|
45
|
+
You can optionally add a `"prompt"` field to customize how avatars are generated. This text will be appended to all avatar generation prompts:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"falKey": "your_fal_ai_api_key_here",
|
|
50
|
+
"prompt": "in a futuristic cyberpunk style with neon lights"
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The prompt will be added to the end of the AI generation request, allowing you to customize the avatar style, theme, or appearance consistently across all avatar variants.
|
|
55
|
+
|
|
57
56
|
### Avatar Images
|
|
58
57
|
|
|
59
58
|
The plugin automatically downloads a default avatar (`avatar.png`) to `~/.config/opencode/avatar.png` if it doesn't exist. This serves as the source image for generating animated variants.
|
|
@@ -87,20 +86,6 @@ All avatars are stored in `~/.config/opencode/` for persistence across updates.
|
|
|
87
86
|
| **Thinking** | User message | "Thinking hard" animation while processing |
|
|
88
87
|
| **Tool Active** | Tool execution | Pose based on current tool (write, read, etc.) |
|
|
89
88
|
|
|
90
|
-
### Tool Mappings
|
|
91
|
-
|
|
92
|
-
The avatar automatically detects which tools you're using and shows appropriate reactions:
|
|
93
|
-
|
|
94
|
-
| Tool | Avatar Pose |
|
|
95
|
-
|------|-------------|
|
|
96
|
-
| `write` | Writing with pencil |
|
|
97
|
-
| `read` | Reading a book |
|
|
98
|
-
| `edit` | Editing with scissors |
|
|
99
|
-
| `glob` | Searching with magnifying glass |
|
|
100
|
-
| `grep` | Detective searching |
|
|
101
|
-
| `bash` | Hacker typing |
|
|
102
|
-
| `webfetch` | Surfing the web |
|
|
103
|
-
|
|
104
89
|
### File Naming
|
|
105
90
|
|
|
106
91
|
Avatar images are cached with predictable filenames:
|
|
@@ -254,4 +239,4 @@ MIT License - see LICENSE file for details.
|
|
|
254
239
|
- Tool-based reactions
|
|
255
240
|
- Caching system
|
|
256
241
|
- Auto-shutdown
|
|
257
|
-
- Focus protection
|
|
242
|
+
- Focus protection
|
package/dist/electron.js
CHANGED
|
@@ -405,18 +405,18 @@ import * as fs from "fs";
|
|
|
405
405
|
import * as os from "os";
|
|
406
406
|
import { fileURLToPath } from "url";
|
|
407
407
|
require_main().config();
|
|
408
|
-
function
|
|
408
|
+
function getConfig() {
|
|
409
409
|
try {
|
|
410
410
|
const configPath = path.join(os.homedir(), ".config", "opencode", "opencode-avatar.json");
|
|
411
411
|
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
412
412
|
if (!config.falKey) {
|
|
413
413
|
console.warn("Warning: falKey not found in config file. Avatar generation will not work. Please set falKey in ~/.config/opencode/opencode-avatar.json");
|
|
414
|
-
return null;
|
|
414
|
+
return { falKey: null, prompt: null };
|
|
415
415
|
}
|
|
416
|
-
return config.falKey;
|
|
416
|
+
return { falKey: config.falKey, prompt: config.prompt || null };
|
|
417
417
|
} catch (error) {
|
|
418
418
|
console.warn(`Warning: Failed to read config file: ${error.message}. Avatar generation will not work. Please ensure ~/.config/opencode/opencode-avatar.json exists and contains falKey.`);
|
|
419
|
-
return null;
|
|
419
|
+
return { falKey: null, prompt: null };
|
|
420
420
|
}
|
|
421
421
|
}
|
|
422
422
|
var FAL_CDN_URL = "https://v3.fal.media";
|
|
@@ -627,8 +627,8 @@ async function downloadImage(url, outputPath) {
|
|
|
627
627
|
fs.writeFileSync(outputPath, buffer);
|
|
628
628
|
}
|
|
629
629
|
async function generateAvatarForPrompt(prompt) {
|
|
630
|
-
const
|
|
631
|
-
if (!falKey) {
|
|
630
|
+
const config = getConfig();
|
|
631
|
+
if (!config.falKey) {
|
|
632
632
|
console.warn("falKey is not set. Cannot generate avatar. Using default avatar.");
|
|
633
633
|
return path.join(AVATAR_DIR, "avatar.png");
|
|
634
634
|
}
|
|
@@ -647,9 +647,12 @@ async function generateAvatarForPrompt(prompt) {
|
|
|
647
647
|
return;
|
|
648
648
|
}
|
|
649
649
|
const sourceAvatar = path.join(AVATAR_DIR, "avatar.png");
|
|
650
|
-
const uploadedUrl = await uploadFile(sourceAvatar, falKey);
|
|
651
|
-
|
|
652
|
-
|
|
650
|
+
const uploadedUrl = await uploadFile(sourceAvatar, config.falKey);
|
|
651
|
+
let fullPrompt = `make a character variant: ${prompt}. Keep the background as a solid green screen color. Do not let the green screen color appear in reflections or on the subject.`;
|
|
652
|
+
if (config.prompt) {
|
|
653
|
+
fullPrompt += ` ${config.prompt}`;
|
|
654
|
+
}
|
|
655
|
+
const result = await generateAvatarImage(uploadedUrl, fullPrompt, config.falKey);
|
|
653
656
|
const outputUrl = result.images?.[0]?.url || result.image?.url || result.url;
|
|
654
657
|
if (!outputUrl) {
|
|
655
658
|
throw new Error("No output image URL in response: " + JSON.stringify(result, null, 2));
|
package/dist/index.js
CHANGED
|
@@ -4,9 +4,11 @@ import { spawn } from "child_process";
|
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import * as http from "http";
|
|
6
6
|
import * as fs from "fs";
|
|
7
|
+
import * as os from "os";
|
|
7
8
|
var __dirname = "/var/home/wizard/av";
|
|
8
9
|
var AVATAR_DIR = __dirname;
|
|
9
10
|
var DEFAULT_AVATAR = "avatar.png";
|
|
11
|
+
var USER_AVATAR = path.join(os.homedir(), ".config", "opencode", "avatar.png");
|
|
10
12
|
var THINKING_PROMPT = "thinking hard";
|
|
11
13
|
var AVATAR_PORT = 47291;
|
|
12
14
|
function getToolPrompt(toolName, toolDescription) {
|
|
@@ -17,10 +19,11 @@ function getToolPrompt(toolName, toolDescription) {
|
|
|
17
19
|
return toolName;
|
|
18
20
|
}
|
|
19
21
|
var electronProcess = null;
|
|
20
|
-
var currentAvatar =
|
|
22
|
+
var currentAvatar = getAvatarPath();
|
|
21
23
|
var isThinking = false;
|
|
22
24
|
var isToolActive = false;
|
|
23
25
|
var isShuttingDown = false;
|
|
26
|
+
var idleTriggered = false;
|
|
24
27
|
var heartbeatInterval = null;
|
|
25
28
|
function sendHeartbeat() {
|
|
26
29
|
const req = http.request({
|
|
@@ -90,6 +93,9 @@ function promptToFilename(prompt, toolName) {
|
|
|
90
93
|
}
|
|
91
94
|
function getAvatarPath(prompt, toolName) {
|
|
92
95
|
if (!prompt) {
|
|
96
|
+
if (fs.existsSync(USER_AVATAR)) {
|
|
97
|
+
return USER_AVATAR;
|
|
98
|
+
}
|
|
93
99
|
return path.join(AVATAR_DIR, DEFAULT_AVATAR);
|
|
94
100
|
}
|
|
95
101
|
const filename = promptToFilename(prompt, toolName);
|
|
@@ -229,12 +235,15 @@ var AvatarPlugin = async ({ client }) => {
|
|
|
229
235
|
res.on("end", () => {
|
|
230
236
|
if (!showToasts) {
|
|
231
237
|
isToolActive = false;
|
|
238
|
+
isThinking = false;
|
|
232
239
|
}
|
|
233
240
|
if (res.statusCode === 200) {
|
|
234
241
|
if (showToasts) {
|
|
235
242
|
showInfoToast(`Avatar ready: ${prompt}`);
|
|
236
243
|
}
|
|
237
|
-
|
|
244
|
+
if (!idleTriggered || showToasts) {
|
|
245
|
+
setAvatarViaHttp(prompt, toolName);
|
|
246
|
+
}
|
|
238
247
|
resolve();
|
|
239
248
|
} else {
|
|
240
249
|
if (showToasts) {
|
|
@@ -268,14 +277,18 @@ var AvatarPlugin = async ({ client }) => {
|
|
|
268
277
|
const userMessage = output.parts.find((part) => part.type === "text" && part.messageID === input.messageID);
|
|
269
278
|
if (userMessage?.text) {}
|
|
270
279
|
if (userMessage?.text && !isThinking) {
|
|
280
|
+
idleTriggered = false;
|
|
271
281
|
isThinking = true;
|
|
272
|
-
|
|
282
|
+
requestAvatarGeneration(THINKING_PROMPT, false).catch(() => {
|
|
283
|
+
isThinking = false;
|
|
284
|
+
});
|
|
273
285
|
}
|
|
274
286
|
},
|
|
275
287
|
"tool.execute.before": async (input) => {
|
|
276
288
|
const toolName = input.tool;
|
|
277
289
|
const toolDescription = getToolDescription(toolName);
|
|
278
290
|
const prompt = getToolPrompt(toolName, toolDescription);
|
|
291
|
+
idleTriggered = false;
|
|
279
292
|
isToolActive = true;
|
|
280
293
|
requestAvatarGeneration(prompt, false, toolName).catch((err) => {
|
|
281
294
|
isToolActive = false;
|
|
@@ -283,6 +296,7 @@ var AvatarPlugin = async ({ client }) => {
|
|
|
283
296
|
},
|
|
284
297
|
event: async ({ event }) => {
|
|
285
298
|
if (event.type === "session.idle" && (isThinking || isToolActive)) {
|
|
299
|
+
idleTriggered = true;
|
|
286
300
|
isThinking = false;
|
|
287
301
|
isToolActive = false;
|
|
288
302
|
await setAvatarViaHttp();
|