opencode-avatar 0.1.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.
package/README.md CHANGED
@@ -42,22 +42,40 @@ Then add to your OpenCode config:
42
42
 
43
43
  ## Configuration
44
44
 
45
- ### Environment Variables
45
+ ### API Key Configuration
46
46
 
47
- Create a `.env` file in the plugin directory:
47
+ Create a config file at `~/.config/opencode/opencode-avatar.json`:
48
48
 
49
- ```env
50
- FAL_KEY=your_fal_ai_api_key_here
49
+ ```json
50
+ {
51
+ "falKey": "your_fal_ai_api_key_here"
52
+ }
51
53
  ```
52
54
 
53
55
  Get your FAL.ai API key from [fal.ai](https://fal.ai).
54
56
 
55
57
  ### Avatar Images
56
58
 
57
- The plugin comes with a default avatar (`avatar.png`). Place custom avatars in the plugin directory:
59
+ 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.
60
+
61
+ #### Using a Custom Avatar
62
+
63
+ To use your own custom avatar:
64
+
65
+ 1. Place your custom `avatar.png` image in `~/.config/opencode/`
66
+ 2. The plugin will use this as the base image for all avatar variants
67
+ 3. Ensure your image has a solid green background (RGB: 0, 255, 0) for best results with the chroma key processing
68
+
69
+ #### Generated Variants
70
+
71
+ The plugin generates and caches avatar variants in the same directory:
72
+
73
+ - `avatar_write.png` - Writing pose
74
+ - `avatar_read.png` - Reading pose
75
+ - `avatar_thinking_hard.png` - Thinking animation
76
+ - And more based on tool usage
58
77
 
59
- - `avatar.png` - Default avatar (required)
60
- - `avatar.svg` - Fallback avatar (optional)
78
+ All avatars are stored in `~/.config/opencode/` for persistence across updates.
61
79
 
62
80
  ## How It Works
63
81
 
@@ -168,7 +186,7 @@ The output shows:
168
186
  ### Avatar Not Showing
169
187
 
170
188
  1. Check console output for errors
171
- 2. Verify FAL.ai API key in `.env`
189
+ 2. Verify FAL.ai API key in `~/.config/opencode/opencode-avatar.json`
172
190
  3. Ensure port 47291 is available
173
191
  4. Try restarting OpenCode
174
192
 
package/dist/electron.js CHANGED
@@ -402,14 +402,29 @@ import { app, BrowserWindow, screen, Tray, Menu, nativeImage } from "electron";
402
402
  import * as path from "path";
403
403
  import * as http from "http";
404
404
  import * as fs from "fs";
405
+ import * as os from "os";
405
406
  import { fileURLToPath } from "url";
406
407
  require_main().config();
408
+ function getFalKey() {
409
+ try {
410
+ const configPath = path.join(os.homedir(), ".config", "opencode", "opencode-avatar.json");
411
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
412
+ if (!config.falKey) {
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;
415
+ }
416
+ return config.falKey;
417
+ } catch (error) {
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;
420
+ }
421
+ }
407
422
  var FAL_CDN_URL = "https://v3.fal.media";
408
423
  var FAL_REST_URL = "https://rest.alpha.fal.ai";
409
424
  var FAL_NANO_BANANA_URL = "https://fal.run/fal-ai/nano-banana-pro/edit";
410
425
  var __filename2 = fileURLToPath(import.meta.url);
411
426
  var __dirnameResolved = path.dirname(__filename2);
412
- var AVATAR_DIR = path.join(__dirnameResolved, "..");
427
+ var AVATAR_DIR = path.join(os.homedir(), ".config", "opencode");
413
428
  var HTML_CONTENT = `<!DOCTYPE html>
414
429
  <html>
415
430
  <head>
@@ -551,13 +566,13 @@ function getAvatarPath() {
551
566
  }
552
567
  return path.join(AVATAR_DIR, "avatar.png");
553
568
  }
554
- async function uploadFile(filePath) {
569
+ async function uploadFile(filePath, falKey) {
555
570
  const fileBuffer = fs.readFileSync(filePath);
556
571
  const fileName = path.basename(filePath);
557
572
  const tokenResponse = await fetch(`${FAL_REST_URL}/storage/auth/token?storage_type=fal-cdn-v3`, {
558
573
  method: "POST",
559
574
  headers: {
560
- Authorization: `Key ${process.env.FAL_KEY}`,
575
+ Authorization: `Key ${falKey}`,
561
576
  Accept: "application/json",
562
577
  "Content-Type": "application/json"
563
578
  },
@@ -585,11 +600,11 @@ async function uploadFile(filePath) {
585
600
  const result = await response.json();
586
601
  return result.access_url || result.url || "";
587
602
  }
588
- async function generateAvatarImage(imageUrl, prompt) {
603
+ async function generateAvatarImage(imageUrl, prompt, falKey) {
589
604
  const response = await fetch(FAL_NANO_BANANA_URL, {
590
605
  method: "POST",
591
606
  headers: {
592
- Authorization: `Key ${process.env.FAL_KEY}`,
607
+ Authorization: `Key ${falKey}`,
593
608
  "Content-Type": "application/json"
594
609
  },
595
610
  body: JSON.stringify({
@@ -612,6 +627,11 @@ async function downloadImage(url, outputPath) {
612
627
  fs.writeFileSync(outputPath, buffer);
613
628
  }
614
629
  async function generateAvatarForPrompt(prompt) {
630
+ const falKey = getFalKey();
631
+ if (!falKey) {
632
+ console.warn("falKey is not set. Cannot generate avatar. Using default avatar.");
633
+ return path.join(AVATAR_DIR, "avatar.png");
634
+ }
615
635
  const cachedFilename = promptToFilename(prompt);
616
636
  const cachedPath = path.join(AVATAR_DIR, cachedFilename);
617
637
  if (fs.existsSync(cachedPath)) {
@@ -627,9 +647,9 @@ async function generateAvatarForPrompt(prompt) {
627
647
  return;
628
648
  }
629
649
  const sourceAvatar = path.join(AVATAR_DIR, "avatar.png");
630
- const uploadedUrl = await uploadFile(sourceAvatar);
650
+ const uploadedUrl = await uploadFile(sourceAvatar, falKey);
631
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.`;
632
- const result = await generateAvatarImage(uploadedUrl, fullPrompt);
652
+ const result = await generateAvatarImage(uploadedUrl, fullPrompt, falKey);
633
653
  const outputUrl = result.images?.[0]?.url || result.image?.url || result.url;
634
654
  if (!outputUrl) {
635
655
  throw new Error("No output image URL in response: " + JSON.stringify(result, null, 2));
@@ -814,7 +834,17 @@ function createTray() {
814
834
  tray.on("click", () => mainWindow?.isVisible() ? mainWindow.hide() : mainWindow?.show());
815
835
  }
816
836
  app.commandLine.appendSwitch("enable-transparent-visuals");
817
- app.whenReady().then(() => {
837
+ app.whenReady().then(async () => {
838
+ fs.mkdirSync(AVATAR_DIR, { recursive: true });
839
+ const avatarPath = path.join(AVATAR_DIR, "avatar.png");
840
+ if (!fs.existsSync(avatarPath)) {
841
+ try {
842
+ await downloadImage("https://richardanaya.github.io/opencode-avatar/avatar.png", avatarPath);
843
+ console.log("Downloaded default avatar to", avatarPath);
844
+ } catch (error) {
845
+ console.warn("Failed to download default avatar:", error);
846
+ }
847
+ }
818
848
  setTimeout(() => {
819
849
  createWindow();
820
850
  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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-avatar",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
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",