opencode-avatar 0.3.4 → 0.3.7
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 +2 -0
- package/dist/electron.js +30 -32
- package/dist/index.js +11 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -194,6 +194,8 @@ The output shows:
|
|
|
194
194
|
- Subsequent loads are instant (cached)
|
|
195
195
|
- Reduce avatar size for faster generation
|
|
196
196
|
|
|
197
|
+
> **Warning:** If you have a lot of tools (like from an MCP), it will generate a lot of images per tool, which may impact performance.
|
|
198
|
+
|
|
197
199
|
## Development
|
|
198
200
|
|
|
199
201
|
### Building
|
package/dist/electron.js
CHANGED
|
@@ -410,12 +410,10 @@ function getConfig() {
|
|
|
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
|
-
console.warn("Warning: falKey not found in config file. Avatar generation will not work. Please set falKey in ~/.config/opencode/opencode-avatar.json");
|
|
414
413
|
return { falKey: null, prompt: null };
|
|
415
414
|
}
|
|
416
415
|
return { falKey: config.falKey, prompt: config.prompt || null };
|
|
417
416
|
} 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
417
|
return { falKey: null, prompt: null };
|
|
420
418
|
}
|
|
421
419
|
}
|
|
@@ -490,42 +488,38 @@ var HTML_CONTENT = `<!DOCTYPE html>
|
|
|
490
488
|
|
|
491
489
|
ctx.drawImage(srcImg, 0, 0);
|
|
492
490
|
|
|
493
|
-
|
|
494
|
-
const chromaR = topLeftPixel[0];
|
|
495
|
-
const chromaG = topLeftPixel[1];
|
|
496
|
-
const chromaB = topLeftPixel[2];
|
|
497
|
-
|
|
498
|
-
|
|
491
|
+
// Chroma keying: make background transparent
|
|
499
492
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
500
493
|
const data = imageData.data;
|
|
494
|
+
const chromaR = data[0]; // first pixel R
|
|
495
|
+
const chromaG = data[1]; // first pixel G
|
|
496
|
+
const chromaB = data[2]; // first pixel B
|
|
501
497
|
const tolerance = 30;
|
|
502
498
|
|
|
503
499
|
for (let i = 0; i < data.length; i += 4) {
|
|
504
500
|
const r = data[i];
|
|
505
501
|
const g = data[i + 1];
|
|
506
502
|
const b = data[i + 2];
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
Math.abs(b - chromaB) <= tolerance
|
|
512
|
-
) {
|
|
513
|
-
data[i + 3] = 0;
|
|
503
|
+
if (Math.abs(r - chromaR) <= tolerance &&
|
|
504
|
+
Math.abs(g - chromaG) <= tolerance &&
|
|
505
|
+
Math.abs(b - chromaB) <= tolerance) {
|
|
506
|
+
data[i + 3] = 0; // set alpha to 0
|
|
514
507
|
}
|
|
515
508
|
}
|
|
516
509
|
|
|
517
510
|
ctx.putImageData(imageData, 0, 0);
|
|
511
|
+
|
|
518
512
|
img.src = canvas.toDataURL('image/png');
|
|
519
513
|
};
|
|
520
514
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
515
|
+
srcImg.onerror = function(e) {
|
|
516
|
+
console.error('Failed to load image:', e);
|
|
517
|
+
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
|
|
518
|
+
};
|
|
525
519
|
}
|
|
526
520
|
|
|
527
|
-
ipcRenderer.on('set-avatar', (event,
|
|
528
|
-
loadAvatar(
|
|
521
|
+
ipcRenderer.on('set-avatar', (event, avatarDataUrl) => {
|
|
522
|
+
loadAvatar(avatarDataUrl);
|
|
529
523
|
});
|
|
530
524
|
|
|
531
525
|
// Fallback: load default avatar if no IPC message received
|
|
@@ -691,15 +685,15 @@ function startAvatarServer() {
|
|
|
691
685
|
try {
|
|
692
686
|
const { avatarPath } = JSON.parse(body);
|
|
693
687
|
if (mainWindow && avatarPath) {
|
|
694
|
-
|
|
688
|
+
const imageBuffer = fs.readFileSync(avatarPath);
|
|
689
|
+
const base64 = imageBuffer.toString("base64");
|
|
690
|
+
const dataUrl = `data:image/png;base64,${base64}`;
|
|
691
|
+
mainWindow.webContents.send("set-avatar", dataUrl);
|
|
695
692
|
updateTrayIcon(avatarPath);
|
|
696
693
|
}
|
|
697
694
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
698
695
|
res.end(JSON.stringify({ success: true }));
|
|
699
|
-
} catch (e) {
|
|
700
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
701
|
-
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
702
|
-
}
|
|
696
|
+
} catch (e) {}
|
|
703
697
|
});
|
|
704
698
|
} else if (req.method === "POST" && req.url === "/generate-avatar") {
|
|
705
699
|
let body = "";
|
|
@@ -716,7 +710,10 @@ function startAvatarServer() {
|
|
|
716
710
|
}
|
|
717
711
|
const avatarPath = await generateAvatarForPrompt(prompt);
|
|
718
712
|
if (mainWindow) {
|
|
719
|
-
|
|
713
|
+
const imageBuffer = fs.readFileSync(avatarPath);
|
|
714
|
+
const base64 = imageBuffer.toString("base64");
|
|
715
|
+
const dataUrl = `data:image/png;base64,${base64}`;
|
|
716
|
+
mainWindow.webContents.send("set-avatar", dataUrl);
|
|
720
717
|
updateTrayIcon(avatarPath);
|
|
721
718
|
}
|
|
722
719
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -778,7 +775,10 @@ function createWindow() {
|
|
|
778
775
|
if (mainWindow) {
|
|
779
776
|
const avatarPath = getAvatarPath();
|
|
780
777
|
try {
|
|
781
|
-
|
|
778
|
+
const imageBuffer = fs.readFileSync(avatarPath);
|
|
779
|
+
const base64 = imageBuffer.toString("base64");
|
|
780
|
+
const dataUrl = `data:image/png;base64,${base64}`;
|
|
781
|
+
mainWindow.webContents.send("set-avatar", dataUrl);
|
|
782
782
|
updateTrayIcon(avatarPath);
|
|
783
783
|
setTimeout(() => {
|
|
784
784
|
if (mainWindow && !mainWindow.isVisible()) {
|
|
@@ -786,9 +786,7 @@ function createWindow() {
|
|
|
786
786
|
mainWindow.setAlwaysOnTop(true, "screen-saver");
|
|
787
787
|
}
|
|
788
788
|
}, 100);
|
|
789
|
-
} catch (
|
|
790
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
791
|
-
}
|
|
789
|
+
} catch (error) {}
|
|
792
790
|
}
|
|
793
791
|
});
|
|
794
792
|
mainWindow.webContents.on("did-fail-load", (event, errorCode, errorDescription) => {});
|
|
@@ -846,7 +844,7 @@ function processTrayIcon(pngPath) {
|
|
|
846
844
|
}
|
|
847
845
|
trayIcon = nativeImage.createFromBitmap(bitmap, size);
|
|
848
846
|
} else {
|
|
849
|
-
trayIcon = nativeImage.
|
|
847
|
+
trayIcon = nativeImage.createFromDataURL("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==");
|
|
850
848
|
}
|
|
851
849
|
return trayIcon;
|
|
852
850
|
}
|
package/dist/index.js
CHANGED
|
@@ -6,9 +6,9 @@ import * as http from "http";
|
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import * as os from "os";
|
|
8
8
|
var __dirname = "/var/home/wizard/av";
|
|
9
|
-
var
|
|
9
|
+
var PLUGIN_DIR = __dirname;
|
|
10
|
+
var AVATAR_DIR = path.join(os.homedir(), ".config", "opencode");
|
|
10
11
|
var DEFAULT_AVATAR = "avatar.png";
|
|
11
|
-
var USER_AVATAR = path.join(os.homedir(), ".config", "opencode", "avatar.png");
|
|
12
12
|
var THINKING_PROMPT = "thinking hard";
|
|
13
13
|
var AVATAR_PORT = 47291;
|
|
14
14
|
function getToolPrompt(toolName, toolDescription) {
|
|
@@ -93,18 +93,16 @@ function promptToFilename(prompt, toolName) {
|
|
|
93
93
|
return "avatar_" + baseName.toLowerCase().replace(/[^a-z0-9\s]/g, "").replace(/\s+/g, "_").substring(0, 50) + ".png";
|
|
94
94
|
}
|
|
95
95
|
function getAvatarPath(prompt, toolName) {
|
|
96
|
+
const defaultAvatar = path.join(AVATAR_DIR, DEFAULT_AVATAR);
|
|
96
97
|
if (!prompt) {
|
|
97
|
-
|
|
98
|
-
return USER_AVATAR;
|
|
99
|
-
}
|
|
100
|
-
return path.join(AVATAR_DIR, DEFAULT_AVATAR);
|
|
98
|
+
return defaultAvatar;
|
|
101
99
|
}
|
|
102
100
|
const filename = promptToFilename(prompt, toolName);
|
|
103
101
|
const avatarPath = path.join(AVATAR_DIR, filename);
|
|
104
102
|
if (fs.existsSync(avatarPath)) {
|
|
105
103
|
return avatarPath;
|
|
106
104
|
}
|
|
107
|
-
return
|
|
105
|
+
return defaultAvatar;
|
|
108
106
|
}
|
|
109
107
|
async function startElectron(avatarPath) {
|
|
110
108
|
if (isShuttingDown) {
|
|
@@ -120,10 +118,10 @@ async function startElectron(avatarPath) {
|
|
|
120
118
|
} catch (e) {}
|
|
121
119
|
electronProcess = null;
|
|
122
120
|
}
|
|
123
|
-
const electronPath = path.join(
|
|
124
|
-
const electronEntry = path.join(
|
|
121
|
+
const electronPath = path.join(PLUGIN_DIR, "node_modules", ".bin", "electron");
|
|
122
|
+
const electronEntry = path.join(PLUGIN_DIR, "dist", "electron.js");
|
|
125
123
|
const child = spawn(electronPath, [electronEntry, "--avatar", avatarPath, "--avatar-port", String(AVATAR_PORT)], {
|
|
126
|
-
cwd:
|
|
124
|
+
cwd: PLUGIN_DIR,
|
|
127
125
|
stdio: ["ignore", "pipe", "pipe"],
|
|
128
126
|
detached: false
|
|
129
127
|
});
|
|
@@ -166,9 +164,9 @@ process.on("SIGTERM", () => {
|
|
|
166
164
|
process.on("uncaughtException", (err) => {
|
|
167
165
|
shutdownElectron();
|
|
168
166
|
});
|
|
169
|
-
async function setAvatarViaHttp(prompt, toolName) {
|
|
167
|
+
async function setAvatarViaHttp(prompt, toolName, force) {
|
|
170
168
|
const avatarPath = getAvatarPath(prompt, toolName);
|
|
171
|
-
if (avatarPath === currentAvatar) {
|
|
169
|
+
if (!force && avatarPath === currentAvatar) {
|
|
172
170
|
return;
|
|
173
171
|
}
|
|
174
172
|
currentAvatar = avatarPath;
|
|
@@ -236,10 +234,6 @@ var AvatarPlugin = async ({ client }) => {
|
|
|
236
234
|
let data = "";
|
|
237
235
|
res.on("data", (chunk) => data += chunk);
|
|
238
236
|
res.on("end", () => {
|
|
239
|
-
if (!showToasts) {
|
|
240
|
-
isToolActive = false;
|
|
241
|
-
isThinking = false;
|
|
242
|
-
}
|
|
243
237
|
if (res.statusCode === 200) {
|
|
244
238
|
if (showToasts) {
|
|
245
239
|
showInfoToast(`Avatar ready: ${prompt}`);
|
|
@@ -303,7 +297,7 @@ var AvatarPlugin = async ({ client }) => {
|
|
|
303
297
|
isThinking = false;
|
|
304
298
|
isToolActive = false;
|
|
305
299
|
currentRequestId = null;
|
|
306
|
-
await setAvatarViaHttp();
|
|
300
|
+
await setAvatarViaHttp(undefined, undefined, true);
|
|
307
301
|
}
|
|
308
302
|
}
|
|
309
303
|
};
|