opencode-avatar 0.2.0 → 0.3.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/README.md +33 -3
- package/dist/electron.js +24 -11
- package/dist/index.js +12 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ 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
|
|
|
@@ -54,12 +55,41 @@ Create a config file at `~/.config/opencode/opencode-avatar.json`:
|
|
|
54
55
|
|
|
55
56
|
Get your FAL.ai API key from [fal.ai](https://fal.ai).
|
|
56
57
|
|
|
58
|
+
### Custom Prompt Configuration (Optional)
|
|
59
|
+
|
|
60
|
+
You can optionally add a `"prompt"` field to customize how avatars are generated. This text will be appended to all avatar generation prompts:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"falKey": "your_fal_ai_api_key_here",
|
|
65
|
+
"prompt": "in a futuristic cyberpunk style with neon lights"
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
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.
|
|
70
|
+
|
|
57
71
|
### Avatar Images
|
|
58
72
|
|
|
59
|
-
The plugin
|
|
73
|
+
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.
|
|
74
|
+
|
|
75
|
+
#### Using a Custom Avatar
|
|
76
|
+
|
|
77
|
+
To use your own custom avatar:
|
|
78
|
+
|
|
79
|
+
1. Place your custom `avatar.png` image in `~/.config/opencode/`
|
|
80
|
+
2. The plugin will use this as the base image for all avatar variants
|
|
81
|
+
3. Ensure your image has a solid green background (RGB: 0, 255, 0) for best results with the chroma key processing
|
|
82
|
+
|
|
83
|
+
#### Generated Variants
|
|
84
|
+
|
|
85
|
+
The plugin generates and caches avatar variants in the same directory:
|
|
86
|
+
|
|
87
|
+
- `avatar_write.png` - Writing pose
|
|
88
|
+
- `avatar_read.png` - Reading pose
|
|
89
|
+
- `avatar_thinking_hard.png` - Thinking animation
|
|
90
|
+
- And more based on tool usage
|
|
60
91
|
|
|
61
|
-
|
|
62
|
-
- `avatar.svg` - Fallback avatar (optional)
|
|
92
|
+
All avatars are stored in `~/.config/opencode/` for persistence across updates.
|
|
63
93
|
|
|
64
94
|
## How It Works
|
|
65
95
|
|
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";
|
|
@@ -424,7 +424,7 @@ var FAL_REST_URL = "https://rest.alpha.fal.ai";
|
|
|
424
424
|
var FAL_NANO_BANANA_URL = "https://fal.run/fal-ai/nano-banana-pro/edit";
|
|
425
425
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
426
426
|
var __dirnameResolved = path.dirname(__filename2);
|
|
427
|
-
var AVATAR_DIR = path.join(
|
|
427
|
+
var AVATAR_DIR = path.join(os.homedir(), ".config", "opencode");
|
|
428
428
|
var HTML_CONTENT = `<!DOCTYPE html>
|
|
429
429
|
<html>
|
|
430
430
|
<head>
|
|
@@ -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));
|
|
@@ -834,7 +837,17 @@ function createTray() {
|
|
|
834
837
|
tray.on("click", () => mainWindow?.isVisible() ? mainWindow.hide() : mainWindow?.show());
|
|
835
838
|
}
|
|
836
839
|
app.commandLine.appendSwitch("enable-transparent-visuals");
|
|
837
|
-
app.whenReady().then(() => {
|
|
840
|
+
app.whenReady().then(async () => {
|
|
841
|
+
fs.mkdirSync(AVATAR_DIR, { recursive: true });
|
|
842
|
+
const avatarPath = path.join(AVATAR_DIR, "avatar.png");
|
|
843
|
+
if (!fs.existsSync(avatarPath)) {
|
|
844
|
+
try {
|
|
845
|
+
await downloadImage("https://richardanaya.github.io/opencode-avatar/avatar.png", avatarPath);
|
|
846
|
+
console.log("Downloaded default avatar to", avatarPath);
|
|
847
|
+
} catch (error) {
|
|
848
|
+
console.warn("Failed to download default avatar:", error);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
838
851
|
setTimeout(() => {
|
|
839
852
|
createWindow();
|
|
840
853
|
createTray();
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ 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
|
-
var __dirname = "/var/home/wizard/
|
|
7
|
+
var __dirname = "/var/home/wizard/av";
|
|
8
8
|
var AVATAR_DIR = __dirname;
|
|
9
9
|
var DEFAULT_AVATAR = "avatar.png";
|
|
10
10
|
var THINKING_PROMPT = "thinking hard";
|
|
@@ -21,6 +21,7 @@ var currentAvatar = DEFAULT_AVATAR;
|
|
|
21
21
|
var isThinking = false;
|
|
22
22
|
var isToolActive = false;
|
|
23
23
|
var isShuttingDown = false;
|
|
24
|
+
var idleTriggered = false;
|
|
24
25
|
var heartbeatInterval = null;
|
|
25
26
|
function sendHeartbeat() {
|
|
26
27
|
const req = http.request({
|
|
@@ -229,12 +230,15 @@ var AvatarPlugin = async ({ client }) => {
|
|
|
229
230
|
res.on("end", () => {
|
|
230
231
|
if (!showToasts) {
|
|
231
232
|
isToolActive = false;
|
|
233
|
+
isThinking = false;
|
|
232
234
|
}
|
|
233
235
|
if (res.statusCode === 200) {
|
|
234
236
|
if (showToasts) {
|
|
235
237
|
showInfoToast(`Avatar ready: ${prompt}`);
|
|
236
238
|
}
|
|
237
|
-
|
|
239
|
+
if (!idleTriggered || showToasts) {
|
|
240
|
+
setAvatarViaHttp(prompt, toolName);
|
|
241
|
+
}
|
|
238
242
|
resolve();
|
|
239
243
|
} else {
|
|
240
244
|
if (showToasts) {
|
|
@@ -268,14 +272,18 @@ var AvatarPlugin = async ({ client }) => {
|
|
|
268
272
|
const userMessage = output.parts.find((part) => part.type === "text" && part.messageID === input.messageID);
|
|
269
273
|
if (userMessage?.text) {}
|
|
270
274
|
if (userMessage?.text && !isThinking) {
|
|
275
|
+
idleTriggered = false;
|
|
271
276
|
isThinking = true;
|
|
272
|
-
|
|
277
|
+
requestAvatarGeneration(THINKING_PROMPT, false).catch(() => {
|
|
278
|
+
isThinking = false;
|
|
279
|
+
});
|
|
273
280
|
}
|
|
274
281
|
},
|
|
275
282
|
"tool.execute.before": async (input) => {
|
|
276
283
|
const toolName = input.tool;
|
|
277
284
|
const toolDescription = getToolDescription(toolName);
|
|
278
285
|
const prompt = getToolPrompt(toolName, toolDescription);
|
|
286
|
+
idleTriggered = false;
|
|
279
287
|
isToolActive = true;
|
|
280
288
|
requestAvatarGeneration(prompt, false, toolName).catch((err) => {
|
|
281
289
|
isToolActive = false;
|
|
@@ -283,6 +291,7 @@ var AvatarPlugin = async ({ client }) => {
|
|
|
283
291
|
},
|
|
284
292
|
event: async ({ event }) => {
|
|
285
293
|
if (event.type === "session.idle" && (isThinking || isToolActive)) {
|
|
294
|
+
idleTriggered = true;
|
|
286
295
|
isThinking = false;
|
|
287
296
|
isToolActive = false;
|
|
288
297
|
await setAvatarViaHttp();
|