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 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="avatar.png" alt="Avatar" width="200" /></div>
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 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";
@@ -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));
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 = DEFAULT_AVATAR;
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
- setAvatarViaHttp(prompt, toolName);
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
- await requestAvatarGeneration(THINKING_PROMPT);
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-avatar",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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",