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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Peter Steinberger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,217 @@
1
+ <p align="center">
2
+ <img src="assets/icon.png" width="80" alt="flawed-avatar icon" />
3
+ </p>
4
+
5
+ <h1 align="center">flawed-avatar</h1>
6
+
7
+ <p align="center">
8
+ A 3D avatar overlay that gives your <a href="https://github.com/nichochar/open-claw">OpenClaw</a> agent a face.<br/>
9
+ Real-time expressions, lip-sync, text-to-speech, and eye tracking — all running locally.
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://www.npmjs.com/package/flawed-avatar"><img src="https://img.shields.io/npm/v/flawed-avatar?color=cb3837&label=npm" alt="npm version" /></a>
14
+ <a href="LICENSE"><img src="https://img.shields.io/github/license/RyuuTheChosen/flawed-openclaw?color=blue" alt="MIT License" /></a>
15
+ <a href="https://github.com/RyuuTheChosen/flawed-openclaw"><img src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey" alt="Platforms" /></a>
16
+ </p>
17
+
18
+ ---
19
+
20
+ ## What it does
21
+
22
+ The avatar sits in a transparent, always-on-top overlay and reacts to your agent in real time:
23
+
24
+ | Agent state | Avatar behavior |
25
+ |---|---|
26
+ | **Idle** | Breathing animation, relaxed posture, ambient eye saccades |
27
+ | **Thinking** | Surprised expression, amplified head sway |
28
+ | **Speaking** | Happy expression, lip-sync driven by TTS audio or text |
29
+ | **Working** | Relaxed expression, subtle working tilt, head nod |
30
+
31
+ It connects to the OpenClaw gateway over WebSocket and listens for agent lifecycle events — no polling, no config wiring.
32
+
33
+ ## Features
34
+
35
+ **Avatar & Animation**
36
+ - VRM model rendering via Three.js and [@pixiv/three-vrm](https://github.com/pixiv/three-vrm)
37
+ - Compound facial expressions with cubic-eased blend shape transitions
38
+ - Procedural breathing, head sway, speaking nod, and working tilt
39
+ - FBX animation clips per phase (idle, thinking, speaking, working) with Mixamo retargeting
40
+ - Spring bone physics for hair and accessories
41
+ - Image-based lighting (IBL) with spherical harmonics
42
+
43
+ **Lip-sync & TTS**
44
+ - Audio-driven viseme blending via [wLipSync](https://github.com/hecomi/uLipSync)
45
+ - Local neural TTS via [Kokoro](https://github.com/hexgrad/kokoro) (11 voices, offline ONNX)
46
+ - Browser Web Speech API as a lightweight alternative
47
+ - Text-based lip-sync fallback when TTS is off
48
+
49
+ **Eye & Gaze**
50
+ - Eyes track your cursor across the entire screen
51
+ - Micro-saccades with configurable yaw/pitch range and hold durations
52
+ - Hover awareness — avatar reacts when you mouse over it
53
+
54
+ **Desktop Integration**
55
+ - Transparent, click-through Electron overlay (mouse passes through empty pixels)
56
+ - Native drag to reposition
57
+ - Scroll-wheel zoom (0.5x to 6.0x)
58
+ - System tray with show/hide, model picker, and settings
59
+ - Chat window to message the active agent directly
60
+ - Settings panel for scale, lighting, TTS engine, and voice selection
61
+ - All preferences persisted between sessions
62
+
63
+ **Multi-agent**
64
+ - Per-agent VRM model assignment (different agents get different avatars)
65
+ - Automatic model switching when the active agent changes
66
+
67
+ ## Install
68
+
69
+ ### macOS / Linux
70
+
71
+ ```bash
72
+ openclaw plugins install flawed-avatar
73
+ openclaw plugins enable flawed-avatar
74
+ ```
75
+
76
+ ### Windows
77
+
78
+ > `openclaw plugins install <npm-package>` is currently broken on Windows + Node.js v22+ due to an upstream OpenClaw bug. Use the tarball workaround:
79
+
80
+ ```bash
81
+ npm pack flawed-avatar
82
+ openclaw plugins install ./flawed-avatar-0.2.1.tgz
83
+ cd %USERPROFILE%\.openclaw\extensions\flawed-avatar
84
+ npm install --omit=dev
85
+ openclaw plugins enable flawed-avatar
86
+ ```
87
+
88
+ Then restart the gateway:
89
+
90
+ ```bash
91
+ openclaw gateway restart
92
+ ```
93
+
94
+ The avatar overlay will appear automatically.
95
+
96
+ ## Configuration
97
+
98
+ Add plugin settings to `~/.openclaw/openclaw.json`:
99
+
100
+ ```json
101
+ {
102
+ "plugins": {
103
+ "entries": {
104
+ "flawed-avatar": {
105
+ "enabled": true,
106
+ "config": {
107
+ "autoStart": true,
108
+ "vrmPath": "/path/to/custom-model.vrm",
109
+ "gatewayUrl": "ws://127.0.0.1:18789",
110
+ "agents": {
111
+ "agent:researcher:main": { "vrmPath": "/models/researcher.vrm" },
112
+ "agent:coder:main": { "vrmPath": "/models/coder.vrm" }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ | Option | Type | Default | Description |
122
+ |---|---|---|---|
123
+ | `autoStart` | `boolean` | `true` | Launch overlay when OpenClaw starts |
124
+ | `vrmPath` | `string` | bundled model | Path to a default VRM model |
125
+ | `gatewayUrl` | `string` | `ws://127.0.0.1:18789` | OpenClaw gateway WebSocket URL |
126
+ | `agents` | `object` | — | Per-agent VRM overrides keyed by session key |
127
+
128
+ ## Controls
129
+
130
+ | Input | Action |
131
+ |---|---|
132
+ | **Scroll wheel** | Zoom in/out |
133
+ | **Drag handle** | Reposition the overlay |
134
+ | Chat icon | Toggle chat window |
135
+ | Speaker icon | Toggle TTS |
136
+ | Gear icon | Open settings panel |
137
+ | Tray icon | Show/hide, change model, quit |
138
+
139
+ ## Settings panel
140
+
141
+ - **Scale** — 0.5x to 2.0x avatar size
142
+ - **Lighting** — Studio, Warm, Cool, Neutral, or Custom profiles
143
+ - **TTS Engine** — Web Speech (browser) or Kokoro (local neural)
144
+ - **TTS Voice** — 11 Kokoro voices (American/British, male/female) or system voices
145
+
146
+ ## Architecture
147
+
148
+ ```
149
+ Plugin Service (Node.js)
150
+
151
+ ├── Spawns Electron child process
152
+ │ │
153
+ │ ├── Main Process
154
+ │ │ ├── Gateway WebSocket client (agent events)
155
+ │ │ ├── Window manager (avatar + chat + settings)
156
+ │ │ ├── System tray
157
+ │ │ └── Persistence (settings, chat history)
158
+ │ │
159
+ │ └── Renderer (Three.js)
160
+ │ ├── VRM loader + spring bones + IBL
161
+ │ ├── Animator (expressions, breathing, gaze, lip-sync)
162
+ │ └── Audio pipeline (Kokoro TTS → wLipSync → visemes)
163
+
164
+ └── Stdin IPC (show/hide/shutdown/model-switch)
165
+ ```
166
+
167
+ ## Development
168
+
169
+ ```bash
170
+ git clone https://github.com/RyuuTheChosen/flawed-openclaw.git
171
+ cd flawed-openclaw
172
+ npm install
173
+ npm run dev # build + launch Electron
174
+ ```
175
+
176
+ | Script | Description |
177
+ |---|---|
178
+ | `npm run build` | TypeScript + Rolldown bundle + copy renderer assets |
179
+ | `npm run dev` | Build and launch in one step |
180
+ | `npm run start` | Launch the last build without recompiling |
181
+
182
+ ### Project structure
183
+
184
+ ```
185
+ index.ts Plugin registration (OpenClaw SDK)
186
+ src/service.ts Electron process lifecycle manager
187
+ src/main/
188
+ main.ts Electron entry point
189
+ gateway-client.ts WebSocket client (protocol v3)
190
+ window-manager.ts Multi-window coordination
191
+ tray.ts System tray menu
192
+ persistence/ JSON file store with migrations
193
+ src/renderer/
194
+ renderer.ts Boot sequence and render loop
195
+ avatar/ VRM, animator, expressions, eye gaze, spring bones
196
+ audio/ TTS engines, lip-sync, phoneme mapping
197
+ ui/ Chat bubble, typing indicator
198
+ chat-window/ Chat panel renderer
199
+ settings-window/ Settings panel renderer
200
+ src/shared/
201
+ config.ts All tunable constants
202
+ types.ts AgentPhase, AgentState
203
+ ipc-channels.ts Electron IPC channel definitions
204
+ assets/
205
+ models/ Bundled VRM avatars
206
+ animations/{idle,thinking,speaking,working}/ FBX motion clips
207
+ ```
208
+
209
+ ## Requirements
210
+
211
+ - Node.js >= 18
212
+ - OpenClaw (peer dependency)
213
+ - Desktop environment with display server (auto-skips on headless Linux)
214
+
215
+ ## License
216
+
217
+ [MIT](LICENSE)
package/assets/icon.png CHANGED
Binary file
@@ -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>
@@ -0,0 +1,19 @@
1
+ export type DeviceIdentity = {
2
+ deviceId: string;
3
+ publicKeyPem: string;
4
+ privateKeyPem: string;
5
+ };
6
+ export declare function loadDeviceIdentity(): DeviceIdentity | null;
7
+ export declare function loadStoredAuthToken(deviceId: string, role: string): string | null;
8
+ export declare function signPayload(privateKeyPem: string, payload: string): string;
9
+ export declare function publicKeyToBase64Url(publicKeyPem: string): string;
10
+ export declare function buildAuthPayload(params: {
11
+ deviceId: string;
12
+ clientId: string;
13
+ clientMode: string;
14
+ role: string;
15
+ scopes: string[];
16
+ signedAtMs: number;
17
+ token: string | null;
18
+ nonce: string | undefined;
19
+ }): string;
@@ -0,0 +1,83 @@
1
+ import * as crypto from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import * as os from "node:os";
5
+ const IDENTITY_DIR = path.join(os.homedir(), ".openclaw", "identity");
6
+ const DEVICE_FILE = path.join(IDENTITY_DIR, "device.json");
7
+ const DEVICE_AUTH_FILE = path.join(IDENTITY_DIR, "device-auth.json");
8
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
9
+ function base64UrlEncode(buf) {
10
+ return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
11
+ }
12
+ function derivePublicKeyRaw(publicKeyPem) {
13
+ const spki = crypto.createPublicKey(publicKeyPem).export({ type: "spki", format: "der" });
14
+ if (spki.length === ED25519_SPKI_PREFIX.length + 32 &&
15
+ spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
16
+ return spki.subarray(ED25519_SPKI_PREFIX.length);
17
+ }
18
+ return spki;
19
+ }
20
+ export function loadDeviceIdentity() {
21
+ try {
22
+ if (!fs.existsSync(DEVICE_FILE))
23
+ return null;
24
+ const raw = fs.readFileSync(DEVICE_FILE, "utf8");
25
+ const parsed = JSON.parse(raw);
26
+ if (parsed?.version === 1 &&
27
+ typeof parsed.deviceId === "string" &&
28
+ typeof parsed.publicKeyPem === "string" &&
29
+ typeof parsed.privateKeyPem === "string") {
30
+ return {
31
+ deviceId: parsed.deviceId,
32
+ publicKeyPem: parsed.publicKeyPem,
33
+ privateKeyPem: parsed.privateKeyPem,
34
+ };
35
+ }
36
+ }
37
+ catch {
38
+ // Corrupt or unreadable
39
+ }
40
+ return null;
41
+ }
42
+ export function loadStoredAuthToken(deviceId, role) {
43
+ try {
44
+ if (!fs.existsSync(DEVICE_AUTH_FILE))
45
+ return null;
46
+ const raw = fs.readFileSync(DEVICE_AUTH_FILE, "utf8");
47
+ const parsed = JSON.parse(raw);
48
+ if (parsed?.version !== 1 || parsed.deviceId !== deviceId)
49
+ return null;
50
+ if (!parsed.tokens || typeof parsed.tokens !== "object")
51
+ return null;
52
+ const entry = parsed.tokens[role.trim()];
53
+ if (!entry || typeof entry.token !== "string")
54
+ return null;
55
+ return entry.token;
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ export function signPayload(privateKeyPem, payload) {
62
+ const key = crypto.createPrivateKey(privateKeyPem);
63
+ return base64UrlEncode(crypto.sign(null, Buffer.from(payload, "utf8"), key));
64
+ }
65
+ export function publicKeyToBase64Url(publicKeyPem) {
66
+ return base64UrlEncode(derivePublicKeyRaw(publicKeyPem));
67
+ }
68
+ export function buildAuthPayload(params) {
69
+ const version = params.nonce ? "v2" : "v1";
70
+ const base = [
71
+ version,
72
+ params.deviceId,
73
+ params.clientId,
74
+ params.clientMode,
75
+ params.role,
76
+ params.scopes.join(","),
77
+ String(params.signedAtMs),
78
+ params.token ?? "",
79
+ ];
80
+ if (version === "v2")
81
+ base.push(params.nonce ?? "");
82
+ return base.join("|");
83
+ }
@@ -1,4 +1,5 @@
1
1
  import type { AgentState } from "../shared/types.js";
2
+ import type { DeviceIdentity } from "./device-identity.js";
2
3
  /**
3
4
  * Lightweight gateway WebSocket client for the Electron main process.
4
5
  * Implements the minimal protocol v3 handshake (without device auth)
@@ -6,7 +7,7 @@ import type { AgentState } from "../shared/types.js";
6
7
  */
7
8
  export declare function createGatewayClient(gatewayUrl: string, onStateChange: (state: AgentState) => void, onModelSwitch: (vrmPath: string) => void, agentConfigs?: Record<string, {
8
9
  vrmPath?: string;
9
- }>, authToken?: string): {
10
+ }>, authToken?: string, deviceIdentity?: DeviceIdentity | null): {
10
11
  destroy: () => void;
11
12
  sendChat: (text: string, sessionKey: string | null) => void;
12
13
  getCurrentAgentId: () => string | null;
@@ -1,13 +1,28 @@
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";
7
+ import { loadStoredAuthToken, buildAuthPayload, signPayload, publicKeyToBase64Url } from "./device-identity.js";
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ const PKG_VERSION = (() => {
11
+ try {
12
+ const raw = fs.readFileSync(path.resolve(__dirname, "..", "..", "..", "package.json"), "utf-8");
13
+ return JSON.parse(raw).version;
14
+ }
15
+ catch {
16
+ return "0.0.0";
17
+ }
18
+ })();
4
19
  const PROTOCOL_VERSION = 3;
5
20
  /**
6
21
  * Lightweight gateway WebSocket client for the Electron main process.
7
22
  * Implements the minimal protocol v3 handshake (without device auth)
8
23
  * and listens for "agent" event frames to drive avatar animations.
9
24
  */
10
- export function createGatewayClient(gatewayUrl, onStateChange, onModelSwitch, agentConfigs, authToken) {
25
+ export function createGatewayClient(gatewayUrl, onStateChange, onModelSwitch, agentConfigs, authToken, deviceIdentity) {
11
26
  let ws = null;
12
27
  let destroyed = false;
13
28
  let backoffMs = GATEWAY_RECONNECT_BASE_MS;
@@ -25,7 +40,6 @@ export function createGatewayClient(gatewayUrl, onStateChange, onModelSwitch, ag
25
40
  const { stream, data, sessionKey } = evt;
26
41
  // Track session changes - agent events contain the actual sessionKey
27
42
  if (sessionKey && sessionKey !== currentSessionKey) {
28
- console.log("flawed-avatar: setting currentSessionKey from agent event:", sessionKey);
29
43
  currentSessionKey = sessionKey;
30
44
  if (agentConfigs?.[sessionKey]?.vrmPath) {
31
45
  onModelSwitch(agentConfigs[sessionKey].vrmPath);
@@ -86,7 +100,6 @@ export function createGatewayClient(gatewayUrl, onStateChange, onModelSwitch, ag
86
100
  const firstSession = sessions[0];
87
101
  if (firstSession.key) {
88
102
  currentSessionKey = firstSession.key;
89
- console.log("flawed-avatar: auto-detected session from sessions.list:", currentSessionKey, "displayName:", firstSession.displayName);
90
103
  }
91
104
  }
92
105
  }
@@ -100,13 +113,11 @@ export function createGatewayClient(gatewayUrl, onStateChange, onModelSwitch, ag
100
113
  const agentId = firstAgent.id ?? "main";
101
114
  const sessionKey = `agent:${agentId}:main`;
102
115
  currentSessionKey = sessionKey;
103
- console.log("flawed-avatar: fallback to agent main session:", currentSessionKey);
104
116
  }
105
117
  }
106
118
  // Connect success - request active sessions once
107
119
  if (connectSent && !connectionSetupDone && !sessionsListRequestId && !agentsListRequestId) {
108
120
  connectionSetupDone = true;
109
- console.log("flawed-avatar: gateway connected successfully");
110
121
  backoffMs = GATEWAY_RECONNECT_BASE_MS;
111
122
  // Request recently active sessions
112
123
  requestSessionsList();
@@ -136,6 +147,35 @@ export function createGatewayClient(gatewayUrl, onStateChange, onModelSwitch, ag
136
147
  clearTimeout(connectTimer);
137
148
  connectTimer = null;
138
149
  }
150
+ const role = "operator";
151
+ const scopes = ["operator.admin"];
152
+ const storedToken = deviceIdentity ? loadStoredAuthToken(deviceIdentity.deviceId, role) : null;
153
+ const effectiveToken = storedToken ?? authToken ?? undefined;
154
+ const auth = effectiveToken ? { token: effectiveToken } : undefined;
155
+ const nonce = connectNonce ?? undefined;
156
+ const signedAtMs = Date.now();
157
+ const device = (() => {
158
+ if (!deviceIdentity)
159
+ return undefined;
160
+ const payload = buildAuthPayload({
161
+ deviceId: deviceIdentity.deviceId,
162
+ clientId: "gateway-client",
163
+ clientMode: "backend",
164
+ role,
165
+ scopes,
166
+ signedAtMs,
167
+ token: effectiveToken ?? null,
168
+ nonce,
169
+ });
170
+ const signature = signPayload(deviceIdentity.privateKeyPem, payload);
171
+ return {
172
+ id: deviceIdentity.deviceId,
173
+ publicKey: publicKeyToBase64Url(deviceIdentity.publicKeyPem),
174
+ signature,
175
+ signedAt: signedAtMs,
176
+ nonce,
177
+ };
178
+ })();
139
179
  const frame = {
140
180
  type: "req",
141
181
  id: randomUUID(),
@@ -146,14 +186,15 @@ export function createGatewayClient(gatewayUrl, onStateChange, onModelSwitch, ag
146
186
  client: {
147
187
  id: "gateway-client",
148
188
  displayName: "Flawed Avatar",
149
- version: "0.1.0",
189
+ version: PKG_VERSION,
150
190
  platform: process.platform,
151
191
  mode: "backend",
152
192
  },
153
193
  caps: [],
154
- role: "operator",
155
- scopes: ["operator.admin"],
156
- auth: authToken ? { token: authToken } : {},
194
+ role,
195
+ scopes,
196
+ auth,
197
+ device,
157
198
  },
158
199
  };
159
200
  ws.send(JSON.stringify(frame));
@@ -182,7 +223,6 @@ export function createGatewayClient(gatewayUrl, onStateChange, onModelSwitch, ag
182
223
  limit: 10,
183
224
  },
184
225
  };
185
- console.log("flawed-avatar: requesting sessions list");
186
226
  ws.send(JSON.stringify(frame));
187
227
  }
188
228
  function requestAgentsList() {
@@ -195,7 +235,6 @@ export function createGatewayClient(gatewayUrl, onStateChange, onModelSwitch, ag
195
235
  method: "agents.list",
196
236
  params: {},
197
237
  };
198
- console.log("flawed-avatar: requesting agents list (fallback)");
199
238
  ws.send(JSON.stringify(frame));
200
239
  }
201
240
  function connect() {
@@ -203,7 +242,6 @@ export function createGatewayClient(gatewayUrl, onStateChange, onModelSwitch, ag
203
242
  return;
204
243
  ws = new WebSocket(gatewayUrl, { maxPayload: 25 * 1024 * 1024 });
205
244
  ws.on("open", () => {
206
- console.log("flawed-avatar: ws open, sending connect frame");
207
245
  queueConnect();
208
246
  });
209
247
  ws.on("message", (data) => {
@@ -7,6 +7,7 @@ import { createWindowManager } from "./window-manager.js";
7
7
  import { createTray } from "./tray.js";
8
8
  import { createStdinListener } 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 } from "./persistence/index.js";
@@ -75,7 +76,7 @@ app.whenReady().then(() => {
75
76
  const wm = createWindowManager();
76
77
  createTray(wm);
77
78
  // Return VRM model path (CLI override > persisted > default)
78
- const defaultVrmPath = path.join(__dirname, "..", "..", "..", "assets", "models", "default-avatar.vrm");
79
+ const defaultVrmPath = path.join(__dirname, "..", "..", "..", "assets", "models", "CaptainLobster.vrm");
79
80
  ipcMain.handle(IPC.GET_VRM_PATH, () => {
80
81
  if (cliVrmPath)
81
82
  return cliVrmPath;
@@ -130,18 +131,15 @@ app.whenReady().then(() => {
130
131
  // Connect to gateway WebSocket for agent event streaming
131
132
  const gatewayUrl = cliGatewayUrl ?? GATEWAY_URL_DEFAULT;
132
133
  const authToken = resolveAuthToken();
133
- console.log(`flawed-avatar: connecting to ${gatewayUrl} (auth=${authToken ? "token" : "none"})`);
134
- const gw = createGatewayClient(gatewayUrl, (state) => wm.sendAgentState(state), (vrmPath) => wm.sendToAvatar(IPC.VRM_MODEL_CHANGED, vrmPath), agentConfigs, authToken);
134
+ const deviceIdentity = loadDeviceIdentity();
135
+ console.log(`flawed-avatar: connecting to ${gatewayUrl} (auth=${authToken ? "token" : "none"}, device=${deviceIdentity ? deviceIdentity.deviceId.slice(0, 8) + "…" : "none"})`);
136
+ const gw = createGatewayClient(gatewayUrl, (state) => wm.sendAgentState(state), (vrmPath) => wm.sendToAvatar(IPC.VRM_MODEL_CHANGED, vrmPath), agentConfigs, authToken, deviceIdentity);
135
137
  // IPC: send chat message to active agent
136
138
  ipcMain.on(IPC.SEND_CHAT, (_event, text) => {
137
- console.log("flawed-avatar: SEND_CHAT received:", text);
138
139
  if (typeof text !== "string" || text.trim().length === 0 || text.length > CHAT_INPUT_MAX_LENGTH) {
139
- console.log("flawed-avatar: SEND_CHAT rejected (validation failed)");
140
140
  return;
141
141
  }
142
142
  const agentId = gw.getCurrentAgentId();
143
- console.log("flawed-avatar: current agentId:", agentId);
144
- console.log("flawed-avatar: sending chat to gateway");
145
143
  gw.sendChat(text.trim(), agentId);
146
144
  });
147
145
  // Clean up resources on quit
@@ -38,8 +38,8 @@ export declare const ChatMessageSchema: z.ZodObject<{
38
38
  id: z.ZodString;
39
39
  timestamp: z.ZodNumber;
40
40
  role: z.ZodEnum<{
41
- user: "user";
42
41
  assistant: "assistant";
42
+ user: "user";
43
43
  }>;
44
44
  text: z.ZodString;
45
45
  agentId: z.ZodOptional<z.ZodString>;
@@ -50,8 +50,8 @@ export declare const ChatHistorySchema: z.ZodObject<{
50
50
  id: z.ZodString;
51
51
  timestamp: z.ZodNumber;
52
52
  role: z.ZodEnum<{
53
- user: "user";
54
53
  assistant: "assistant";
54
+ user: "user";
55
55
  }>;
56
56
  text: z.ZodString;
57
57
  agentId: z.ZodOptional<z.ZodString>;