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 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 comes with a default avatar (`avatar.png`). Place custom avatars in the plugin directory:
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
- - `avatar.png` - Default avatar (required)
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 getFalKey() {
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(__dirnameResolved, "..");
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 falKey = getFalKey();
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
- const 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
- const result = await generateAvatarImage(uploadedUrl, fullPrompt, falKey);
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/opencode-avatar";
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
- setAvatarViaHttp(prompt, toolName);
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
- await requestAvatarGeneration(THINKING_PROMPT);
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-avatar",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Dynamic desktop avatar plugin for OpenCode that reacts to your coding activities",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",