flawed-avatar 0.2.1 → 0.2.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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +217 -0
  3. package/assets/icon.png +0 -0
  4. package/dist/chat-renderer-bundle/chat-index.html +1 -1
  5. package/dist/main/main/device-identity.d.ts +19 -0
  6. package/dist/main/main/device-identity.js +83 -0
  7. package/dist/main/main/gateway-client.d.ts +2 -1
  8. package/dist/main/main/gateway-client.js +50 -12
  9. package/dist/main/main/main.js +5 -7
  10. package/dist/main/main/persistence/types.d.ts +2 -2
  11. package/dist/renderer-bundle/renderer.js +35 -46
  12. package/dist/settings-preload.cjs +153 -0
  13. package/dist/settings-renderer-bundle/settings-index.html +16 -0
  14. package/dist/settings-renderer-bundle/settings-renderer.js +502 -0
  15. package/dist/settings-renderer-bundle/styles/base.css +106 -0
  16. package/dist/settings-renderer-bundle/styles/chat.css +516 -0
  17. package/dist/settings-renderer-bundle/styles/components/button.css +221 -0
  18. package/dist/settings-renderer-bundle/styles/components/indicator.css +216 -0
  19. package/dist/settings-renderer-bundle/styles/components/input.css +139 -0
  20. package/dist/settings-renderer-bundle/styles/components/toast.css +204 -0
  21. package/dist/settings-renderer-bundle/styles/controls.css +279 -0
  22. package/dist/settings-renderer-bundle/styles/settings.css +310 -0
  23. package/dist/settings-renderer-bundle/styles/tokens.css +220 -0
  24. package/dist/settings-renderer-bundle/styles/utilities.css +349 -0
  25. package/index.ts +2 -2
  26. package/package.json +6 -1
  27. package/src/main/device-identity.ts +103 -0
  28. package/src/main/gateway-client.ts +52 -11
  29. package/src/main/main.ts +5 -6
  30. package/src/renderer/audio/index.ts +0 -3
  31. package/src/renderer/audio/kokoro-model-loader.ts +0 -2
  32. package/src/renderer/audio/kokoro-tts-service.ts +0 -2
  33. package/src/renderer/audio/tts-controller.ts +0 -3
  34. package/src/renderer/avatar/ibl-enhancer.ts +1 -1
  35. package/src/renderer/chat-window/chat-index.html +1 -1
  36. package/src/renderer/renderer.ts +0 -1
  37. package/src/renderer/settings-window/settings-index.html +1 -1
  38. package/src/renderer/ui/chat-bubble.ts +0 -39
  39. package/src/service.ts +1 -1
  40. package/src/renderer/audio/tts-service.ts +0 -16
@@ -1,7 +1,23 @@
1
1
  import WebSocket from "ws";
2
2
  import { randomUUID } from "node:crypto";
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { fileURLToPath } from "node:url";
3
6
  import { GATEWAY_RECONNECT_BASE_MS, GATEWAY_RECONNECT_MAX_MS } from "../shared/config.js";
4
7
  import type { AgentState } from "../shared/types.js";
8
+ import type { DeviceIdentity } from "./device-identity.js";
9
+ import { loadStoredAuthToken, buildAuthPayload, signPayload, publicKeyToBase64Url } from "./device-identity.js";
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ const PKG_VERSION: string = (() => {
14
+ try {
15
+ const raw = fs.readFileSync(path.resolve(__dirname, "..", "..", "..", "package.json"), "utf-8");
16
+ return (JSON.parse(raw) as { version: string }).version;
17
+ } catch {
18
+ return "0.0.0";
19
+ }
20
+ })();
5
21
 
6
22
  const PROTOCOL_VERSION = 3;
7
23
 
@@ -40,6 +56,7 @@ export function createGatewayClient(
40
56
  onModelSwitch: (vrmPath: string) => void,
41
57
  agentConfigs?: Record<string, { vrmPath?: string }>,
42
58
  authToken?: string,
59
+ deviceIdentity?: DeviceIdentity | null,
43
60
  ): { destroy: () => void; sendChat: (text: string, sessionKey: string | null) => void; getCurrentAgentId: () => string | null } {
44
61
  let ws: WebSocket | null = null;
45
62
  let destroyed = false;
@@ -60,7 +77,6 @@ export function createGatewayClient(
60
77
 
61
78
  // Track session changes - agent events contain the actual sessionKey
62
79
  if (sessionKey && sessionKey !== currentSessionKey) {
63
- console.log("flawed-avatar: setting currentSessionKey from agent event:", sessionKey);
64
80
  currentSessionKey = sessionKey;
65
81
  if (agentConfigs?.[sessionKey]?.vrmPath) {
66
82
  onModelSwitch(agentConfigs[sessionKey].vrmPath!);
@@ -124,7 +140,6 @@ export function createGatewayClient(
124
140
  const firstSession = sessions[0];
125
141
  if (firstSession.key) {
126
142
  currentSessionKey = firstSession.key;
127
- console.log("flawed-avatar: auto-detected session from sessions.list:", currentSessionKey, "displayName:", firstSession.displayName);
128
143
  }
129
144
  }
130
145
  }
@@ -139,14 +154,12 @@ export function createGatewayClient(
139
154
  const agentId = firstAgent.id ?? "main";
140
155
  const sessionKey = `agent:${agentId}:main`;
141
156
  currentSessionKey = sessionKey;
142
- console.log("flawed-avatar: fallback to agent main session:", currentSessionKey);
143
157
  }
144
158
  }
145
159
 
146
160
  // Connect success - request active sessions once
147
161
  if (connectSent && !connectionSetupDone && !sessionsListRequestId && !agentsListRequestId) {
148
162
  connectionSetupDone = true;
149
- console.log("flawed-avatar: gateway connected successfully");
150
163
  backoffMs = GATEWAY_RECONNECT_BASE_MS;
151
164
  // Request recently active sessions
152
165
  requestSessionsList();
@@ -176,6 +189,36 @@ export function createGatewayClient(
176
189
  connectTimer = null;
177
190
  }
178
191
 
192
+ const role = "operator";
193
+ const scopes = ["operator.admin"];
194
+ const storedToken = deviceIdentity ? loadStoredAuthToken(deviceIdentity.deviceId, role) : null;
195
+ const effectiveToken = storedToken ?? authToken ?? undefined;
196
+ const auth = effectiveToken ? { token: effectiveToken } : undefined;
197
+
198
+ const nonce = connectNonce ?? undefined;
199
+ const signedAtMs = Date.now();
200
+ const device = (() => {
201
+ if (!deviceIdentity) return undefined;
202
+ const payload = buildAuthPayload({
203
+ deviceId: deviceIdentity.deviceId,
204
+ clientId: "gateway-client",
205
+ clientMode: "backend",
206
+ role,
207
+ scopes,
208
+ signedAtMs,
209
+ token: effectiveToken ?? null,
210
+ nonce,
211
+ });
212
+ const signature = signPayload(deviceIdentity.privateKeyPem, payload);
213
+ return {
214
+ id: deviceIdentity.deviceId,
215
+ publicKey: publicKeyToBase64Url(deviceIdentity.publicKeyPem),
216
+ signature,
217
+ signedAt: signedAtMs,
218
+ nonce,
219
+ };
220
+ })();
221
+
179
222
  const frame = {
180
223
  type: "req",
181
224
  id: randomUUID(),
@@ -186,14 +229,15 @@ export function createGatewayClient(
186
229
  client: {
187
230
  id: "gateway-client",
188
231
  displayName: "Flawed Avatar",
189
- version: "0.1.0",
232
+ version: PKG_VERSION,
190
233
  platform: process.platform,
191
234
  mode: "backend",
192
235
  },
193
236
  caps: [],
194
- role: "operator",
195
- scopes: ["operator.admin"],
196
- auth: authToken ? { token: authToken } : {},
237
+ role,
238
+ scopes,
239
+ auth,
240
+ device,
197
241
  },
198
242
  };
199
243
 
@@ -223,7 +267,6 @@ export function createGatewayClient(
223
267
  limit: 10,
224
268
  },
225
269
  };
226
- console.log("flawed-avatar: requesting sessions list");
227
270
  ws.send(JSON.stringify(frame));
228
271
  }
229
272
 
@@ -236,7 +279,6 @@ export function createGatewayClient(
236
279
  method: "agents.list",
237
280
  params: {},
238
281
  };
239
- console.log("flawed-avatar: requesting agents list (fallback)");
240
282
  ws.send(JSON.stringify(frame));
241
283
  }
242
284
 
@@ -246,7 +288,6 @@ export function createGatewayClient(
246
288
  ws = new WebSocket(gatewayUrl, { maxPayload: 25 * 1024 * 1024 });
247
289
 
248
290
  ws.on("open", () => {
249
- console.log("flawed-avatar: ws open, sending connect frame");
250
291
  queueConnect();
251
292
  });
252
293
 
package/src/main/main.ts CHANGED
@@ -7,6 +7,7 @@ import { createWindowManager } from "./window-manager.js";
7
7
  import { createTray } from "./tray.js";
8
8
  import { createStdinListener, type StdinCommand } from "./stdin-listener.js";
9
9
  import { createGatewayClient } from "./gateway-client.js";
10
+ import { loadDeviceIdentity } from "./device-identity.js";
10
11
  import { IPC } from "../shared/ipc-channels.js";
11
12
  import { GATEWAY_URL_DEFAULT, CHAT_INPUT_MAX_LENGTH } from "../shared/config.js";
12
13
  import { getVrmModelPath, saveVrmModelPath } from "./persistence/index.js";
@@ -77,7 +78,7 @@ app.whenReady().then(() => {
77
78
  createTray(wm);
78
79
 
79
80
  // Return VRM model path (CLI override > persisted > default)
80
- const defaultVrmPath = path.join(__dirname, "..", "..", "..", "assets", "models", "default-avatar.vrm");
81
+ const defaultVrmPath = path.join(__dirname, "..", "..", "..", "assets", "models", "CaptainLobster.vrm");
81
82
  ipcMain.handle(IPC.GET_VRM_PATH, () => {
82
83
  if (cliVrmPath) return cliVrmPath;
83
84
  const persisted = getVrmModelPath();
@@ -133,25 +134,23 @@ app.whenReady().then(() => {
133
134
  // Connect to gateway WebSocket for agent event streaming
134
135
  const gatewayUrl = cliGatewayUrl ?? GATEWAY_URL_DEFAULT;
135
136
  const authToken = resolveAuthToken();
136
- console.log(`flawed-avatar: connecting to ${gatewayUrl} (auth=${authToken ? "token" : "none"})`);
137
+ const deviceIdentity = loadDeviceIdentity();
138
+ console.log(`flawed-avatar: connecting to ${gatewayUrl} (auth=${authToken ? "token" : "none"}, device=${deviceIdentity ? deviceIdentity.deviceId.slice(0, 8) + "…" : "none"})`);
137
139
  const gw = createGatewayClient(
138
140
  gatewayUrl,
139
141
  (state) => wm.sendAgentState(state),
140
142
  (vrmPath) => wm.sendToAvatar(IPC.VRM_MODEL_CHANGED, vrmPath),
141
143
  agentConfigs,
142
144
  authToken,
145
+ deviceIdentity,
143
146
  );
144
147
 
145
148
  // IPC: send chat message to active agent
146
149
  ipcMain.on(IPC.SEND_CHAT, (_event, text: unknown) => {
147
- console.log("flawed-avatar: SEND_CHAT received:", text);
148
150
  if (typeof text !== "string" || text.trim().length === 0 || text.length > CHAT_INPUT_MAX_LENGTH) {
149
- console.log("flawed-avatar: SEND_CHAT rejected (validation failed)");
150
151
  return;
151
152
  }
152
153
  const agentId = gw.getCurrentAgentId();
153
- console.log("flawed-avatar: current agentId:", agentId);
154
- console.log("flawed-avatar: sending chat to gateway");
155
154
  gw.sendChat(text.trim(), agentId);
156
155
  });
157
156
 
@@ -26,9 +26,6 @@ export {
26
26
  export { createWebSpeechTTSService } from "./web-speech-tts.js";
27
27
  export { createKokoroTTSService, disposeKokoroLoader } from "./kokoro-tts-service.js";
28
28
 
29
- // Backwards compatibility alias
30
- export { createWebSpeechTTSService as createTTSService } from "./web-speech-tts.js";
31
-
32
29
  // Factory
33
30
  export { createTTSServiceFactory, createTTSServiceWithFallback, type TTSServiceFactory } from "./tts-service-factory.js";
34
31
 
@@ -44,7 +44,6 @@ function createModelLoader(): KokoroModelLoader {
44
44
  throw new Error("Model loader has been disposed");
45
45
  }
46
46
 
47
- console.log("[Kokoro] Loading model...");
48
47
  loading = true;
49
48
 
50
49
  try {
@@ -57,7 +56,6 @@ function createModelLoader(): KokoroModelLoader {
57
56
  { dtype: "q8" }
58
57
  );
59
58
 
60
- console.log("[Kokoro] Model loaded successfully");
61
59
  model = tts as unknown as KokoroTTS;
62
60
  loading = false;
63
61
  return model;
@@ -166,8 +166,6 @@ export function createKokoroTTSService(events: TTSEvents): TTSService {
166
166
  return;
167
167
  }
168
168
 
169
- console.log(`[Kokoro] Generating: "${segment.text.slice(0, 40)}..." (ready=${readyAudio.length}, pending=${pendingSegments.length})`);
170
-
171
169
  const result = await model.generate(segment.text, { voice: currentVoice });
172
170
 
173
171
  if (disposed || segment.cancelled) {
@@ -132,7 +132,6 @@ export function createTTSController(
132
132
  }
133
133
  wlipSync = analyzer;
134
134
  player.setAnalysisNode(analyzer.node);
135
- console.log("[TTS] wLipSync analyzer initialized");
136
135
  }).catch((err) => {
137
136
  console.warn("[TTS] Failed to initialize wLipSync:", err);
138
137
  }).finally(() => {
@@ -271,8 +270,6 @@ export function createTTSController(
271
270
  setEngine(engine: TTSEngineType): void {
272
271
  if (disposed || engine === currentEngine) return;
273
272
 
274
- console.log(`[TTS] Switching engine from ${currentEngine} to ${engine}`);
275
-
276
273
  // Cancel and dispose current service
277
274
  if (ttsService) {
278
275
  ttsService.cancel();
@@ -68,7 +68,7 @@ export function createIBLEnhancer(): IBLEnhancer {
68
68
  if (!(mat as any).isShaderMaterial && !(mat as any).isMeshStandardMaterial) return;
69
69
  if ((mat as any).__iblEnhanced) return; // Guard: already enhanced
70
70
 
71
- // Chain onBeforeCompile (RISK 3 fix: preserve MToon's original)
71
+ // Chain onBeforeCompile preserve MToon's existing hook
72
72
  const prev = mat.onBeforeCompile;
73
73
  mat.onBeforeCompile = (shader, renderer) => {
74
74
  prev?.call(mat, shader, renderer);
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta http-equiv="Content-Security-Policy" content="default-src 'self' file: data: blob:; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' file: data: blob:; connect-src 'self' file: data: blob:" />
6
- <title>OpenClaw Chat</title>
6
+ <title>Flawed Avatar Chat</title>
7
7
  <link rel="stylesheet" href="./styles/chat.css" />
8
8
  </head>
9
9
  <body>
@@ -122,7 +122,6 @@ async function boot(): Promise<void> {
122
122
  const ttsEnabled = await bridge.getTtsEnabled();
123
123
  const ttsEngine = await bridge.getTtsEngine();
124
124
  const ttsVoice = await bridge.getTtsVoice();
125
- console.log("[TTS] Initializing with:", { ttsEnabled, ttsEngine, ttsVoice });
126
125
  ttsController = createTTSController(animator.getLipSync(), {
127
126
  enabled: ttsEnabled,
128
127
  engine: ttsEngine || "web-speech",
@@ -6,7 +6,7 @@
6
6
  content="default-src 'self' file: data: blob:; script-src 'self';
7
7
  style-src 'self' 'unsafe-inline'; img-src 'self' file: data: blob:;
8
8
  connect-src 'self' file: data: blob:" />
9
- <title>OpenClaw Settings</title>
9
+ <title>Flawed Avatar Settings</title>
10
10
  <link rel="stylesheet" href="./styles/settings.css" />
11
11
  </head>
12
12
  <body>
@@ -182,45 +182,6 @@ export function createChatBubble(
182
182
  pruneHistory();
183
183
  }
184
184
 
185
- // --- Render empty state ---
186
- function renderEmptyState(): HTMLElement {
187
- const el = document.createElement("div");
188
- el.className = "chat-empty";
189
- el.innerHTML = `
190
- <div class="chat-empty__icon" aria-hidden="true">💬</div>
191
- <div class="chat-empty__title">No messages yet</div>
192
- <div class="chat-empty__message">Start a conversation</div>
193
- `;
194
- return el;
195
- }
196
-
197
- // --- Render error state ---
198
- function renderError(message: string): HTMLElement {
199
- const el = document.createElement("div");
200
- el.className = "chat-error";
201
- el.setAttribute("role", "alert");
202
- el.innerHTML = `
203
- <svg class="chat-error__icon" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
204
- <circle cx="8" cy="8" r="7" fill="none" stroke="currentColor" stroke-width="1.5"/>
205
- <path d="M8 4v5M8 11v1"/>
206
- </svg>
207
- <span class="chat-error__message">${message}</span>
208
- `;
209
- return el;
210
- }
211
-
212
- // --- Render loading skeleton ---
213
- function renderLoadingSkeleton(): HTMLElement {
214
- const el = document.createElement("div");
215
- el.className = "chat-loading";
216
- for (let i = 0; i < 3; i++) {
217
- const skeleton = document.createElement("div");
218
- skeleton.className = "skeleton message-skeleton";
219
- el.appendChild(skeleton);
220
- }
221
- return el;
222
- }
223
-
224
185
  // --- Send message ---
225
186
  function sendMessage(text: string): void {
226
187
  bridge.sendChat(text);
package/src/service.ts CHANGED
@@ -13,7 +13,7 @@ const RESTART_BASE_MS = 5_000;
13
13
  const RESTART_MAX_MS = 30_000;
14
14
  const RESTART_RESET_MS = 60_000;
15
15
 
16
- export function createAvatarOverlayService(api: OpenClawPluginApi) {
16
+ export function createFlawedAvatarService(api: OpenClawPluginApi) {
17
17
  let child: ChildProcess | null = null;
18
18
  let stopped = false;
19
19
  let restartBackoffMs = RESTART_BASE_MS;
@@ -1,16 +0,0 @@
1
- /**
2
- * TTS Service - Re-exports for backwards compatibility.
3
- *
4
- * The implementation has been split into:
5
- * - types.ts: Interface definitions
6
- * - web-speech-tts.ts: Web Speech API implementation
7
- * - kokoro-tts-service.ts: Kokoro.js implementation
8
- * - tts-service-factory.ts: Factory for creating services
9
- */
10
-
11
- // Re-export types for backwards compatibility
12
- export type { TTSEvents, TTSService, TTSVoice, TTSEngineType, TTSControllerConfig } from "./types.js";
13
- export { TTS_ENGINES, KOKORO_VOICES, KOKORO_DEFAULT_VOICE } from "./types.js";
14
-
15
- // Re-export Web Speech implementation as default factory for backwards compatibility
16
- export { createWebSpeechTTSService as createTTSService } from "./web-speech-tts.js";