opencode-avatar 0.3.4 → 0.3.6
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/dist/electron.js +41 -32
- package/dist/index.js +11 -17
- package/package.json +1 -1
package/dist/electron.js
CHANGED
|
@@ -405,17 +405,20 @@ import * as fs from "fs";
|
|
|
405
405
|
import * as os from "os";
|
|
406
406
|
import { fileURLToPath } from "url";
|
|
407
407
|
require_main().config();
|
|
408
|
+
var logFile = path.join(os.homedir(), "avatar.log");
|
|
409
|
+
function log(msg) {
|
|
410
|
+
fs.appendFileSync(logFile, new Date().toISOString() + ": " + msg + `
|
|
411
|
+
`);
|
|
412
|
+
}
|
|
408
413
|
function getConfig() {
|
|
409
414
|
try {
|
|
410
415
|
const configPath = path.join(os.homedir(), ".config", "opencode", "opencode-avatar.json");
|
|
411
416
|
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
412
417
|
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
418
|
return { falKey: null, prompt: null };
|
|
415
419
|
}
|
|
416
420
|
return { falKey: config.falKey, prompt: config.prompt || null };
|
|
417
421
|
} 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
422
|
return { falKey: null, prompt: null };
|
|
420
423
|
}
|
|
421
424
|
}
|
|
@@ -490,42 +493,39 @@ var HTML_CONTENT = `<!DOCTYPE html>
|
|
|
490
493
|
|
|
491
494
|
ctx.drawImage(srcImg, 0, 0);
|
|
492
495
|
|
|
493
|
-
|
|
494
|
-
const chromaR = topLeftPixel[0];
|
|
495
|
-
const chromaG = topLeftPixel[1];
|
|
496
|
-
const chromaB = topLeftPixel[2];
|
|
497
|
-
|
|
498
|
-
|
|
496
|
+
// Chroma keying: make background transparent
|
|
499
497
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
500
498
|
const data = imageData.data;
|
|
499
|
+
const chromaR = data[0]; // first pixel R
|
|
500
|
+
const chromaG = data[1]; // first pixel G
|
|
501
|
+
const chromaB = data[2]; // first pixel B
|
|
501
502
|
const tolerance = 30;
|
|
502
503
|
|
|
503
504
|
for (let i = 0; i < data.length; i += 4) {
|
|
504
505
|
const r = data[i];
|
|
505
506
|
const g = data[i + 1];
|
|
506
507
|
const b = data[i + 2];
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
Math.abs(b - chromaB) <= tolerance
|
|
512
|
-
) {
|
|
513
|
-
data[i + 3] = 0;
|
|
508
|
+
if (Math.abs(r - chromaR) <= tolerance &&
|
|
509
|
+
Math.abs(g - chromaG) <= tolerance &&
|
|
510
|
+
Math.abs(b - chromaB) <= tolerance) {
|
|
511
|
+
data[i + 3] = 0; // set alpha to 0
|
|
514
512
|
}
|
|
515
513
|
}
|
|
516
514
|
|
|
517
515
|
ctx.putImageData(imageData, 0, 0);
|
|
516
|
+
|
|
518
517
|
img.src = canvas.toDataURL('image/png');
|
|
519
518
|
};
|
|
520
519
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
520
|
+
srcImg.onerror = function(e) {
|
|
521
|
+
console.error('Failed to load image:', e);
|
|
522
|
+
log('Renderer: Avatar load failed, using fallback transparent image');
|
|
523
|
+
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
|
|
524
|
+
};
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
-
ipcRenderer.on('set-avatar', (event,
|
|
528
|
-
loadAvatar(
|
|
527
|
+
ipcRenderer.on('set-avatar', (event, avatarDataUrl) => {
|
|
528
|
+
loadAvatar(avatarDataUrl);
|
|
529
529
|
});
|
|
530
530
|
|
|
531
531
|
// Fallback: load default avatar if no IPC message received
|
|
@@ -690,16 +690,17 @@ function startAvatarServer() {
|
|
|
690
690
|
req.on("end", () => {
|
|
691
691
|
try {
|
|
692
692
|
const { avatarPath } = JSON.parse(body);
|
|
693
|
+
log("Set-avatar request with path: " + avatarPath);
|
|
693
694
|
if (mainWindow && avatarPath) {
|
|
694
|
-
|
|
695
|
+
const imageBuffer = fs.readFileSync(avatarPath);
|
|
696
|
+
const base64 = imageBuffer.toString("base64");
|
|
697
|
+
const dataUrl = `data:image/png;base64,${base64}`;
|
|
698
|
+
mainWindow.webContents.send("set-avatar", dataUrl);
|
|
695
699
|
updateTrayIcon(avatarPath);
|
|
696
700
|
}
|
|
697
701
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
698
702
|
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
|
-
}
|
|
703
|
+
} catch (e) {}
|
|
703
704
|
});
|
|
704
705
|
} else if (req.method === "POST" && req.url === "/generate-avatar") {
|
|
705
706
|
let body = "";
|
|
@@ -716,7 +717,10 @@ function startAvatarServer() {
|
|
|
716
717
|
}
|
|
717
718
|
const avatarPath = await generateAvatarForPrompt(prompt);
|
|
718
719
|
if (mainWindow) {
|
|
719
|
-
|
|
720
|
+
const imageBuffer = fs.readFileSync(avatarPath);
|
|
721
|
+
const base64 = imageBuffer.toString("base64");
|
|
722
|
+
const dataUrl = `data:image/png;base64,${base64}`;
|
|
723
|
+
mainWindow.webContents.send("set-avatar", dataUrl);
|
|
720
724
|
updateTrayIcon(avatarPath);
|
|
721
725
|
}
|
|
722
726
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -776,9 +780,13 @@ function createWindow() {
|
|
|
776
780
|
mainWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(HTML_CONTENT)}`);
|
|
777
781
|
mainWindow.webContents.on("did-finish-load", () => {
|
|
778
782
|
if (mainWindow) {
|
|
783
|
+
log("Window finished loading");
|
|
779
784
|
const avatarPath = getAvatarPath();
|
|
780
785
|
try {
|
|
781
|
-
|
|
786
|
+
const imageBuffer = fs.readFileSync(avatarPath);
|
|
787
|
+
const base64 = imageBuffer.toString("base64");
|
|
788
|
+
const dataUrl = `data:image/png;base64,${base64}`;
|
|
789
|
+
mainWindow.webContents.send("set-avatar", dataUrl);
|
|
782
790
|
updateTrayIcon(avatarPath);
|
|
783
791
|
setTimeout(() => {
|
|
784
792
|
if (mainWindow && !mainWindow.isVisible()) {
|
|
@@ -786,9 +794,7 @@ function createWindow() {
|
|
|
786
794
|
mainWindow.setAlwaysOnTop(true, "screen-saver");
|
|
787
795
|
}
|
|
788
796
|
}, 100);
|
|
789
|
-
} catch (
|
|
790
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
791
|
-
}
|
|
797
|
+
} catch (error) {}
|
|
792
798
|
}
|
|
793
799
|
});
|
|
794
800
|
mainWindow.webContents.on("did-fail-load", (event, errorCode, errorDescription) => {});
|
|
@@ -828,6 +834,7 @@ function createTray() {
|
|
|
828
834
|
tray.on("click", () => mainWindow?.isVisible() ? mainWindow.hide() : mainWindow?.show());
|
|
829
835
|
}
|
|
830
836
|
function processTrayIcon(pngPath) {
|
|
837
|
+
log("Processing tray icon from: " + pngPath);
|
|
831
838
|
let trayIcon = nativeImage.createFromPath(pngPath);
|
|
832
839
|
if (!trayIcon.isEmpty()) {
|
|
833
840
|
const size = trayIcon.getSize();
|
|
@@ -845,8 +852,10 @@ function processTrayIcon(pngPath) {
|
|
|
845
852
|
}
|
|
846
853
|
}
|
|
847
854
|
trayIcon = nativeImage.createFromBitmap(bitmap, size);
|
|
855
|
+
log("Tray icon processed successfully");
|
|
848
856
|
} else {
|
|
849
|
-
|
|
857
|
+
log("Tray icon is empty, using fallback");
|
|
858
|
+
trayIcon = nativeImage.createFromDataURL("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==");
|
|
850
859
|
}
|
|
851
860
|
return trayIcon;
|
|
852
861
|
}
|
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
|
};
|