flawed-avatar 0.2.1

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 (152) hide show
  1. package/assets/animations/idle/Breathing Idle.fbx +0 -0
  2. package/assets/animations/idle/look away gesture.fbx +0 -0
  3. package/assets/animations/idle/weight shift.fbx +0 -0
  4. package/assets/animations/speaking/Agreeing.fbx +0 -0
  5. package/assets/animations/speaking/Talking (1).fbx +0 -0
  6. package/assets/animations/speaking/Talking (2).fbx +0 -0
  7. package/assets/animations/speaking/Talking (3).fbx +0 -0
  8. package/assets/animations/speaking/Talking.fbx +0 -0
  9. package/assets/animations/speaking/head nod yes.fbx +0 -0
  10. package/assets/animations/thinking/Thinking.fbx +0 -0
  11. package/assets/animations/thinking/thoughtful head shake.fbx +0 -0
  12. package/assets/animations/working/acknowledging.fbx +0 -0
  13. package/assets/animations/working/lengthy head nod.fbx +0 -0
  14. package/assets/icon.png +0 -0
  15. package/assets/models/CaptainLobster.vrm +0 -0
  16. package/assets/models/default-avatar.vrm +0 -0
  17. package/dist/chat-preload.cjs +87 -0
  18. package/dist/chat-renderer-bundle/chat-index.html +16 -0
  19. package/dist/chat-renderer-bundle/chat-renderer.js +355 -0
  20. package/dist/chat-renderer-bundle/styles/base.css +106 -0
  21. package/dist/chat-renderer-bundle/styles/chat.css +516 -0
  22. package/dist/chat-renderer-bundle/styles/components/button.css +221 -0
  23. package/dist/chat-renderer-bundle/styles/components/indicator.css +216 -0
  24. package/dist/chat-renderer-bundle/styles/components/input.css +139 -0
  25. package/dist/chat-renderer-bundle/styles/components/toast.css +204 -0
  26. package/dist/chat-renderer-bundle/styles/controls.css +279 -0
  27. package/dist/chat-renderer-bundle/styles/settings.css +310 -0
  28. package/dist/chat-renderer-bundle/styles/tokens.css +220 -0
  29. package/dist/chat-renderer-bundle/styles/utilities.css +349 -0
  30. package/dist/main/main/display-utils.d.ts +12 -0
  31. package/dist/main/main/display-utils.js +29 -0
  32. package/dist/main/main/gateway-client.d.ts +13 -0
  33. package/dist/main/main/gateway-client.js +265 -0
  34. package/dist/main/main/main.d.ts +1 -0
  35. package/dist/main/main/main.js +157 -0
  36. package/dist/main/main/persistence/chat-store.d.ts +8 -0
  37. package/dist/main/main/persistence/chat-store.js +110 -0
  38. package/dist/main/main/persistence/file-store.d.ts +17 -0
  39. package/dist/main/main/persistence/file-store.js +183 -0
  40. package/dist/main/main/persistence/index.d.ts +8 -0
  41. package/dist/main/main/persistence/index.js +8 -0
  42. package/dist/main/main/persistence/migrations.d.ts +23 -0
  43. package/dist/main/main/persistence/migrations.js +191 -0
  44. package/dist/main/main/persistence/settings-store.d.ts +32 -0
  45. package/dist/main/main/persistence/settings-store.js +174 -0
  46. package/dist/main/main/persistence/types.d.ts +72 -0
  47. package/dist/main/main/persistence/types.js +69 -0
  48. package/dist/main/main/settings-broadcast.d.ts +3 -0
  49. package/dist/main/main/settings-broadcast.js +9 -0
  50. package/dist/main/main/stdin-listener.d.ts +15 -0
  51. package/dist/main/main/stdin-listener.js +27 -0
  52. package/dist/main/main/tray.d.ts +3 -0
  53. package/dist/main/main/tray.js +59 -0
  54. package/dist/main/main/window-manager.d.ts +23 -0
  55. package/dist/main/main/window-manager.js +232 -0
  56. package/dist/main/main/window.d.ts +3 -0
  57. package/dist/main/main/window.js +528 -0
  58. package/dist/main/shared/config.d.ts +91 -0
  59. package/dist/main/shared/config.js +111 -0
  60. package/dist/main/shared/ipc-channels.d.ts +54 -0
  61. package/dist/main/shared/ipc-channels.js +68 -0
  62. package/dist/main/shared/types.d.ts +6 -0
  63. package/dist/main/shared/types.js +1 -0
  64. package/dist/preload.cjs +256 -0
  65. package/dist/renderer-bundle/index.html +63 -0
  66. package/dist/renderer-bundle/renderer.js +100734 -0
  67. package/dist/renderer-bundle/styles/base.css +106 -0
  68. package/dist/renderer-bundle/styles/chat.css +516 -0
  69. package/dist/renderer-bundle/styles/components/button.css +221 -0
  70. package/dist/renderer-bundle/styles/components/indicator.css +216 -0
  71. package/dist/renderer-bundle/styles/components/input.css +139 -0
  72. package/dist/renderer-bundle/styles/components/toast.css +204 -0
  73. package/dist/renderer-bundle/styles/controls.css +279 -0
  74. package/dist/renderer-bundle/styles/settings.css +310 -0
  75. package/dist/renderer-bundle/styles/tokens.css +220 -0
  76. package/dist/renderer-bundle/styles/utilities.css +349 -0
  77. package/index.ts +32 -0
  78. package/openclaw.plugin.json +22 -0
  79. package/package.json +45 -0
  80. package/src/electron-launcher.ts +63 -0
  81. package/src/main/chat-preload.cjs +87 -0
  82. package/src/main/display-utils.ts +39 -0
  83. package/src/main/gateway-client.ts +312 -0
  84. package/src/main/main.ts +169 -0
  85. package/src/main/persistence/chat-store.ts +143 -0
  86. package/src/main/persistence/file-store.ts +221 -0
  87. package/src/main/persistence/index.ts +69 -0
  88. package/src/main/persistence/migrations.ts +232 -0
  89. package/src/main/persistence/settings-store.ts +219 -0
  90. package/src/main/persistence/types.ts +107 -0
  91. package/src/main/preload.cjs +256 -0
  92. package/src/main/settings-broadcast.ts +13 -0
  93. package/src/main/settings-preload.cjs +153 -0
  94. package/src/main/stdin-listener.ts +34 -0
  95. package/src/main/tray.ts +65 -0
  96. package/src/main/window-manager.ts +298 -0
  97. package/src/main/window.ts +614 -0
  98. package/src/renderer/audio/audio-player.ts +161 -0
  99. package/src/renderer/audio/frequency-analyzer.ts +104 -0
  100. package/src/renderer/audio/index.ts +36 -0
  101. package/src/renderer/audio/kokoro-model-loader.ts +128 -0
  102. package/src/renderer/audio/kokoro-tts-service.ts +370 -0
  103. package/src/renderer/audio/lip-sync-profile.json +1 -0
  104. package/src/renderer/audio/phoneme-mapper.ts +120 -0
  105. package/src/renderer/audio/tts-controller.ts +344 -0
  106. package/src/renderer/audio/tts-service-factory.ts +75 -0
  107. package/src/renderer/audio/tts-service.ts +16 -0
  108. package/src/renderer/audio/types.ts +120 -0
  109. package/src/renderer/audio/web-speech-tts.ts +177 -0
  110. package/src/renderer/audio/wlipsync-analyzer.ts +145 -0
  111. package/src/renderer/avatar/animation-loader.ts +114 -0
  112. package/src/renderer/avatar/animator.ts +322 -0
  113. package/src/renderer/avatar/expressions.ts +165 -0
  114. package/src/renderer/avatar/eye-gaze.ts +255 -0
  115. package/src/renderer/avatar/eye-saccades.ts +133 -0
  116. package/src/renderer/avatar/hover-awareness.ts +125 -0
  117. package/src/renderer/avatar/ibl-enhancer.ts +163 -0
  118. package/src/renderer/avatar/lip-sync.ts +258 -0
  119. package/src/renderer/avatar/mixamo-retarget.ts +169 -0
  120. package/src/renderer/avatar/pixel-transparency.ts +65 -0
  121. package/src/renderer/avatar/scene.ts +70 -0
  122. package/src/renderer/avatar/spring-bones.ts +27 -0
  123. package/src/renderer/avatar/state-machine.ts +117 -0
  124. package/src/renderer/avatar/vrm-loader.ts +71 -0
  125. package/src/renderer/chat-window/chat-index.html +16 -0
  126. package/src/renderer/chat-window/chat-renderer.ts +28 -0
  127. package/src/renderer/index.html +63 -0
  128. package/src/renderer/renderer.ts +329 -0
  129. package/src/renderer/settings-window/settings-controls.ts +223 -0
  130. package/src/renderer/settings-window/settings-index.html +16 -0
  131. package/src/renderer/settings-window/settings-panel.ts +346 -0
  132. package/src/renderer/settings-window/settings-renderer.ts +5 -0
  133. package/src/renderer/styles/base.css +106 -0
  134. package/src/renderer/styles/chat.css +516 -0
  135. package/src/renderer/styles/components/button.css +221 -0
  136. package/src/renderer/styles/components/indicator.css +216 -0
  137. package/src/renderer/styles/components/input.css +139 -0
  138. package/src/renderer/styles/components/toast.css +204 -0
  139. package/src/renderer/styles/controls.css +279 -0
  140. package/src/renderer/styles/settings.css +310 -0
  141. package/src/renderer/styles/tokens.css +220 -0
  142. package/src/renderer/styles/utilities.css +349 -0
  143. package/src/renderer/types/avatar-bridge.d.ts +86 -0
  144. package/src/renderer/types/chat-bridge.d.ts +37 -0
  145. package/src/renderer/types/settings-bridge.d.ts +54 -0
  146. package/src/renderer/ui/chat-bubble.ts +435 -0
  147. package/src/renderer/ui/icons.ts +47 -0
  148. package/src/renderer/ui/typing-indicator.ts +41 -0
  149. package/src/service.ts +163 -0
  150. package/src/shared/config.ts +135 -0
  151. package/src/shared/ipc-channels.ts +81 -0
  152. package/src/shared/types.ts +7 -0
@@ -0,0 +1,183 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ import { SETTINGS_DEBOUNCE_MS, LOCK_TIMEOUT_MS, LOCK_STALE_MS, } from "../../shared/config.js";
5
+ const openclawDir = path.join(os.homedir(), ".openclaw");
6
+ function ensureDir() {
7
+ try {
8
+ fs.mkdirSync(openclawDir, { recursive: true });
9
+ }
10
+ catch {
11
+ // Directory likely already exists
12
+ }
13
+ }
14
+ function getLockPath(filePath) {
15
+ return `${filePath}.lock`;
16
+ }
17
+ function acquireLock(filePath) {
18
+ const lockPath = getLockPath(filePath);
19
+ try {
20
+ // Check for stale lock
21
+ try {
22
+ const stat = fs.statSync(lockPath);
23
+ const age = Date.now() - stat.mtimeMs;
24
+ if (age > LOCK_STALE_MS) {
25
+ // Lock is stale, remove it
26
+ fs.unlinkSync(lockPath);
27
+ }
28
+ }
29
+ catch {
30
+ // Lock doesn't exist, good
31
+ }
32
+ // Try to create lock file exclusively
33
+ fs.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
34
+ return true;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ function releaseLock(filePath) {
41
+ const lockPath = getLockPath(filePath);
42
+ try {
43
+ fs.unlinkSync(lockPath);
44
+ }
45
+ catch {
46
+ // Lock already released or never acquired
47
+ }
48
+ }
49
+ async function waitForLock(filePath, timeoutMs = LOCK_TIMEOUT_MS) {
50
+ const startTime = Date.now();
51
+ const interval = 50;
52
+ while (Date.now() - startTime < timeoutMs) {
53
+ if (acquireLock(filePath)) {
54
+ return true;
55
+ }
56
+ await new Promise((resolve) => setTimeout(resolve, interval));
57
+ }
58
+ return false;
59
+ }
60
+ export function createFileStore(options) {
61
+ const { filename, schema, defaultValue, debounceMs = SETTINGS_DEBOUNCE_MS } = options;
62
+ const filePath = path.join(openclawDir, filename);
63
+ let cache = null;
64
+ let pendingData = null;
65
+ let saveTimeout = null;
66
+ let isFlushing = false;
67
+ function load() {
68
+ try {
69
+ const data = fs.readFileSync(filePath, "utf-8");
70
+ const parsed = JSON.parse(data);
71
+ const result = schema.safeParse(parsed);
72
+ if (result.success) {
73
+ cache = result.data;
74
+ return { ok: true, data: result.data };
75
+ }
76
+ // Schema validation failed
77
+ console.warn(`[file-store] Schema validation failed for ${filename}:`, result.error.message);
78
+ const fallback = defaultValue();
79
+ cache = fallback;
80
+ return { ok: false, error: result.error.message, fallback };
81
+ }
82
+ catch (err) {
83
+ // File not found or parse error - normal for first run
84
+ const fallback = defaultValue();
85
+ cache = fallback;
86
+ if (err.code !== "ENOENT") {
87
+ console.warn(`[file-store] Error reading ${filename}:`, err);
88
+ return { ok: false, error: String(err), fallback };
89
+ }
90
+ return { ok: true, data: fallback };
91
+ }
92
+ }
93
+ function scheduleWrite(data) {
94
+ pendingData = data;
95
+ cache = data;
96
+ if (saveTimeout) {
97
+ clearTimeout(saveTimeout);
98
+ }
99
+ saveTimeout = setTimeout(() => {
100
+ void performWrite();
101
+ }, debounceMs);
102
+ }
103
+ async function performWrite() {
104
+ if (pendingData === null || isFlushing)
105
+ return;
106
+ isFlushing = true;
107
+ const dataToWrite = pendingData;
108
+ pendingData = null;
109
+ saveTimeout = null;
110
+ const lockAcquired = await waitForLock(filePath);
111
+ if (!lockAcquired) {
112
+ console.warn(`[file-store] Failed to acquire lock for ${filename}, will retry on next save`);
113
+ pendingData = dataToWrite; // Restore pending data for retry
114
+ isFlushing = false;
115
+ return;
116
+ }
117
+ try {
118
+ ensureDir();
119
+ // On Windows, atomic rename doesn't work well across drives,
120
+ // so we write directly
121
+ const content = JSON.stringify(dataToWrite, null, "\t");
122
+ if (process.platform === "win32") {
123
+ fs.writeFileSync(filePath, content);
124
+ }
125
+ else {
126
+ // Atomic write: temp file + rename
127
+ const tempPath = `${filePath}.tmp.${process.pid}`;
128
+ fs.writeFileSync(tempPath, content);
129
+ fs.renameSync(tempPath, filePath);
130
+ }
131
+ }
132
+ catch (err) {
133
+ console.warn(`[file-store] Error writing ${filename}:`, err);
134
+ }
135
+ finally {
136
+ releaseLock(filePath);
137
+ isFlushing = false;
138
+ }
139
+ }
140
+ function save(data) {
141
+ scheduleWrite(data);
142
+ }
143
+ async function flush() {
144
+ if (saveTimeout) {
145
+ clearTimeout(saveTimeout);
146
+ saveTimeout = null;
147
+ }
148
+ if (pendingData !== null) {
149
+ await performWrite();
150
+ }
151
+ }
152
+ function getCache() {
153
+ return cache;
154
+ }
155
+ function cleanup() {
156
+ if (saveTimeout) {
157
+ clearTimeout(saveTimeout);
158
+ saveTimeout = null;
159
+ }
160
+ // Synchronous flush on cleanup
161
+ if (pendingData !== null) {
162
+ try {
163
+ ensureDir();
164
+ const content = JSON.stringify(pendingData, null, "\t");
165
+ fs.writeFileSync(filePath, content);
166
+ }
167
+ catch {
168
+ // Ignore errors during cleanup
169
+ }
170
+ pendingData = null;
171
+ }
172
+ }
173
+ return {
174
+ load,
175
+ save,
176
+ flush,
177
+ getCache,
178
+ cleanup,
179
+ };
180
+ }
181
+ export function getOpenclawDir() {
182
+ return openclawDir;
183
+ }
@@ -0,0 +1,8 @@
1
+ export type { Settings, ChatMessage, ChatHistory, LoadResult, } from "./types.js";
2
+ export type { LightingCustom } from "./types.js";
3
+ export { SETTINGS_SCHEMA_VERSION, CHAT_SCHEMA_VERSION, SettingsSchema, LightingCustomSchema, ChatMessageSchema, ChatHistorySchema, createDefaultSettings, createDefaultChatHistory, } from "./types.js";
4
+ export type { FileStore, StoreOptions } from "./file-store.js";
5
+ export { createFileStore, getOpenclawDir } from "./file-store.js";
6
+ export { loadSettings, saveSettings, savePosition, saveZoom, saveOpacity, saveIdleTimeout, saveTtsEnabled, saveTtsEngine, saveTtsVoice, saveVrmModelPath, getPosition, getZoom, getOpacity, getIdleTimeout, getTtsEnabled, getTtsEngine, getTtsVoice, getVrmModelPath, saveScale, getScale, saveLightingProfile, getLightingProfile, saveLightingCustom, getLightingCustom, flushSettings, cleanupSettings, getSettingsStore, } from "./settings-store.js";
7
+ export { loadChatHistory, appendMessage, clearChatHistory, getRecentMessages, getChatHistory, flushChat, cleanupChat, } from "./chat-store.js";
8
+ export { migrateFileNames, migrateLegacyFiles, migrateV1ToV2, migrateV2ToV3 } from "./migrations.js";
@@ -0,0 +1,8 @@
1
+ export { SETTINGS_SCHEMA_VERSION, CHAT_SCHEMA_VERSION, SettingsSchema, LightingCustomSchema, ChatMessageSchema, ChatHistorySchema, createDefaultSettings, createDefaultChatHistory, } from "./types.js";
2
+ export { createFileStore, getOpenclawDir } from "./file-store.js";
3
+ // Settings store
4
+ export { loadSettings, saveSettings, savePosition, saveZoom, saveOpacity, saveIdleTimeout, saveTtsEnabled, saveTtsEngine, saveTtsVoice, saveVrmModelPath, getPosition, getZoom, getOpacity, getIdleTimeout, getTtsEnabled, getTtsEngine, getTtsVoice, getVrmModelPath, saveScale, getScale, saveLightingProfile, getLightingProfile, saveLightingCustom, getLightingCustom, flushSettings, cleanupSettings, getSettingsStore, } from "./settings-store.js";
5
+ // Chat store
6
+ export { loadChatHistory, appendMessage, clearChatHistory, getRecentMessages, getChatHistory, flushChat, cleanupChat, } from "./chat-store.js";
7
+ // Migrations
8
+ export { migrateFileNames, migrateLegacyFiles, migrateV1ToV2, migrateV2ToV3 } from "./migrations.js";
@@ -0,0 +1,23 @@
1
+ import { type Settings } from "./types.js";
2
+ /**
3
+ * Renames old avatar-overlay-*.json files to flawed-avatar-*.json
4
+ * so existing users' settings survive the package rename.
5
+ */
6
+ export declare function migrateFileNames(): void;
7
+ /**
8
+ * Migrates legacy separate files (position.json, camera.json) to unified settings.
9
+ * Returns the migrated settings if migration occurred, null otherwise.
10
+ * Safe to call multiple times - only migrates if legacy files exist.
11
+ */
12
+ export declare function migrateLegacyFiles(): Settings | null;
13
+ /**
14
+ * Migrates settings from schema v1 to v2.
15
+ * Converts position from `{x,y}` flat object to `Record<displayHash, {x,y}>`.
16
+ * Reads raw JSON to avoid schema validation rejecting the old format.
17
+ */
18
+ export declare function migrateV1ToV2(): void;
19
+ /**
20
+ * Migrates settings from schema v2 to v3.
21
+ * Adds `scale` and `lightingProfile` fields with defaults.
22
+ */
23
+ export declare function migrateV2ToV3(): void;
@@ -0,0 +1,191 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { WINDOW_POSITION_FILE, CAMERA_ZOOM_FILE, CAMERA_ZOOM_MIN, CAMERA_ZOOM_MAX, SCALE_DEFAULT, LIGHTING_PROFILE_DEFAULT, } from "../../shared/config.js";
4
+ import { getOpenclawDir } from "./file-store.js";
5
+ import { createDefaultSettings, SETTINGS_SCHEMA_VERSION, } from "./types.js";
6
+ import { loadSettings, getSettingsStore } from "./settings-store.js";
7
+ import { computeDisplayHash } from "../display-utils.js";
8
+ /**
9
+ * Renames old avatar-overlay-*.json files to flawed-avatar-*.json
10
+ * so existing users' settings survive the package rename.
11
+ */
12
+ export function migrateFileNames() {
13
+ const dir = getOpenclawDir();
14
+ const renames = [
15
+ ["avatar-overlay-settings.json", "flawed-avatar-settings.json"],
16
+ ["avatar-overlay-position.json", "flawed-avatar-position.json"],
17
+ ["avatar-overlay-camera.json", "flawed-avatar-camera.json"],
18
+ ["avatar-overlay-chat.json", "flawed-avatar-chat.json"],
19
+ ];
20
+ for (const [oldName, newName] of renames) {
21
+ const oldPath = path.join(dir, oldName);
22
+ const newPath = path.join(dir, newName);
23
+ try {
24
+ if (fs.existsSync(oldPath) && !fs.existsSync(newPath)) {
25
+ fs.renameSync(oldPath, newPath);
26
+ console.log(`[migrations] Renamed ${oldName} → ${newName}`);
27
+ }
28
+ }
29
+ catch {
30
+ // Best-effort; next run will retry
31
+ }
32
+ }
33
+ }
34
+ function readLegacyPosition() {
35
+ const filePath = path.join(getOpenclawDir(), WINDOW_POSITION_FILE);
36
+ try {
37
+ const data = fs.readFileSync(filePath, "utf-8");
38
+ const parsed = JSON.parse(data);
39
+ if (typeof parsed.x === "number" &&
40
+ typeof parsed.y === "number" &&
41
+ Number.isFinite(parsed.x) &&
42
+ Number.isFinite(parsed.y)) {
43
+ return { x: parsed.x, y: parsed.y };
44
+ }
45
+ }
46
+ catch {
47
+ // File doesn't exist or is invalid
48
+ }
49
+ return null;
50
+ }
51
+ function readLegacyCamera() {
52
+ const filePath = path.join(getOpenclawDir(), CAMERA_ZOOM_FILE);
53
+ try {
54
+ const data = fs.readFileSync(filePath, "utf-8");
55
+ const parsed = JSON.parse(data);
56
+ if (typeof parsed.zoom === "number" &&
57
+ Number.isFinite(parsed.zoom) &&
58
+ parsed.zoom >= CAMERA_ZOOM_MIN &&
59
+ parsed.zoom <= CAMERA_ZOOM_MAX) {
60
+ return { zoom: parsed.zoom };
61
+ }
62
+ }
63
+ catch {
64
+ // File doesn't exist or is invalid
65
+ }
66
+ return null;
67
+ }
68
+ function deleteLegacyFile(filename) {
69
+ const filePath = path.join(getOpenclawDir(), filename);
70
+ try {
71
+ fs.unlinkSync(filePath);
72
+ console.log(`[migrations] Deleted legacy file: ${filename}`);
73
+ }
74
+ catch {
75
+ // File doesn't exist or can't be deleted
76
+ }
77
+ }
78
+ /**
79
+ * Migrates legacy separate files (position.json, camera.json) to unified settings.
80
+ * Returns the migrated settings if migration occurred, null otherwise.
81
+ * Safe to call multiple times - only migrates if legacy files exist.
82
+ */
83
+ export function migrateLegacyFiles() {
84
+ const legacyPosition = readLegacyPosition();
85
+ const legacyCamera = readLegacyCamera();
86
+ // No legacy files to migrate
87
+ if (!legacyPosition && !legacyCamera) {
88
+ return null;
89
+ }
90
+ console.log("[migrations] Found legacy settings files, migrating...");
91
+ // Load current settings (may be defaults if new install)
92
+ const current = loadSettings();
93
+ // Build migrated settings
94
+ const migrated = {
95
+ ...createDefaultSettings(),
96
+ ...current,
97
+ schemaVersion: SETTINGS_SCHEMA_VERSION,
98
+ };
99
+ // Only migrate position if not already set in new format
100
+ if (legacyPosition && (!current.position || Object.keys(current.position).length === 0)) {
101
+ const hash = computeDisplayHash();
102
+ migrated.position = { [hash]: legacyPosition };
103
+ console.log(`[migrations] Migrated position: (${legacyPosition.x}, ${legacyPosition.y}) for display ${hash}`);
104
+ }
105
+ // Only migrate camera if not already set in new format
106
+ if (legacyCamera && !current.camera) {
107
+ migrated.camera = { zoom: legacyCamera.zoom };
108
+ console.log(`[migrations] Migrated camera zoom: ${legacyCamera.zoom}`);
109
+ }
110
+ // Save migrated settings
111
+ getSettingsStore().save(migrated);
112
+ // Delete legacy files after successful migration
113
+ if (legacyPosition) {
114
+ deleteLegacyFile(WINDOW_POSITION_FILE);
115
+ }
116
+ if (legacyCamera) {
117
+ deleteLegacyFile(CAMERA_ZOOM_FILE);
118
+ }
119
+ console.log("[migrations] Migration complete");
120
+ return migrated;
121
+ }
122
+ /**
123
+ * Migrates settings from schema v1 to v2.
124
+ * Converts position from `{x,y}` flat object to `Record<displayHash, {x,y}>`.
125
+ * Reads raw JSON to avoid schema validation rejecting the old format.
126
+ */
127
+ export function migrateV1ToV2() {
128
+ const filePath = path.join(getOpenclawDir(), "flawed-avatar-settings.json");
129
+ let raw;
130
+ try {
131
+ raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
132
+ }
133
+ catch {
134
+ return; // No settings file yet
135
+ }
136
+ // Already at v2 or newer
137
+ if (typeof raw.schemaVersion === "number" && raw.schemaVersion >= 2)
138
+ return;
139
+ // Check if position is old v1 format ({x, y} directly)
140
+ const pos = raw.position;
141
+ if (pos &&
142
+ typeof pos === "object" &&
143
+ !Array.isArray(pos) &&
144
+ "x" in pos &&
145
+ "y" in pos) {
146
+ const oldPos = pos;
147
+ const hash = computeDisplayHash();
148
+ console.log(`[migrations] Converting v1 position to v2 keyed by display hash ${hash}`);
149
+ raw.position = { [hash]: { x: oldPos.x, y: oldPos.y } };
150
+ }
151
+ raw.schemaVersion = 2;
152
+ try {
153
+ fs.writeFileSync(filePath, JSON.stringify(raw, null, "\t"));
154
+ console.log("[migrations] Schema upgraded to v2");
155
+ }
156
+ catch (err) {
157
+ console.warn("[migrations] Failed to write v2 migration:", err);
158
+ }
159
+ }
160
+ /**
161
+ * Migrates settings from schema v2 to v3.
162
+ * Adds `scale` and `lightingProfile` fields with defaults.
163
+ */
164
+ export function migrateV2ToV3() {
165
+ const filePath = path.join(getOpenclawDir(), "flawed-avatar-settings.json");
166
+ let raw;
167
+ try {
168
+ raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
169
+ }
170
+ catch {
171
+ return; // No settings file yet
172
+ }
173
+ // Already at v3 or newer
174
+ if (typeof raw.schemaVersion === "number" && raw.schemaVersion >= 3)
175
+ return;
176
+ // Add defaults for new fields
177
+ if (raw.scale === undefined) {
178
+ raw.scale = SCALE_DEFAULT;
179
+ }
180
+ if (raw.lightingProfile === undefined) {
181
+ raw.lightingProfile = LIGHTING_PROFILE_DEFAULT;
182
+ }
183
+ raw.schemaVersion = SETTINGS_SCHEMA_VERSION;
184
+ try {
185
+ fs.writeFileSync(filePath, JSON.stringify(raw, null, "\t"));
186
+ console.log("[migrations] Schema upgraded to v3");
187
+ }
188
+ catch (err) {
189
+ console.warn("[migrations] Failed to write v3 migration:", err);
190
+ }
191
+ }
@@ -0,0 +1,32 @@
1
+ import { type FileStore } from "./file-store.js";
2
+ import { type Settings, type LightingCustom } from "./types.js";
3
+ export declare function loadSettings(): Settings;
4
+ export declare function saveSettings(settings: Partial<Settings>): void;
5
+ export declare function savePosition(x: number, y: number): void;
6
+ export declare function saveZoom(zoom: number): void;
7
+ export declare function saveOpacity(opacity: number): void;
8
+ export declare function saveIdleTimeout(ms: number): void;
9
+ export declare function getPosition(): {
10
+ x: number;
11
+ y: number;
12
+ } | null;
13
+ export declare function getZoom(): number;
14
+ export declare function getOpacity(): number;
15
+ export declare function getIdleTimeout(): number;
16
+ export declare function saveTtsEnabled(enabled: boolean): void;
17
+ export declare function getTtsEnabled(): boolean;
18
+ export declare function saveTtsEngine(engine: "web-speech" | "kokoro"): void;
19
+ export declare function getTtsEngine(): "web-speech" | "kokoro";
20
+ export declare function saveTtsVoice(voice: string): void;
21
+ export declare function getTtsVoice(): string;
22
+ export declare function saveVrmModelPath(path: string): void;
23
+ export declare function getVrmModelPath(): string | undefined;
24
+ export declare function saveScale(scale: number): void;
25
+ export declare function getScale(): number;
26
+ export declare function saveLightingProfile(profile: string): void;
27
+ export declare function getLightingProfile(): string;
28
+ export declare function saveLightingCustom(custom: LightingCustom): void;
29
+ export declare function getLightingCustom(): LightingCustom | undefined;
30
+ export declare function flushSettings(): Promise<void>;
31
+ export declare function cleanupSettings(): void;
32
+ export declare function getSettingsStore(): FileStore<Settings>;
@@ -0,0 +1,174 @@
1
+ import { SETTINGS_FILE, CAMERA_ZOOM_MIN, CAMERA_ZOOM_MAX, CAMERA_ZOOM_DEFAULT, OPACITY_MIN, OPACITY_MAX, OPACITY_DEFAULT, IDLE_TIMEOUT_DEFAULT, SETTINGS_DEBOUNCE_MS, TTS_ENABLED_DEFAULT, TTS_ENGINE_DEFAULT, TTS_VOICE_DEFAULT, SCALE_MIN, SCALE_MAX, SCALE_DEFAULT, LIGHTING_PROFILE_DEFAULT, } from "../../shared/config.js";
2
+ import { createFileStore } from "./file-store.js";
3
+ import { SettingsSchema, createDefaultSettings, } from "./types.js";
4
+ import { computeDisplayHash } from "../display-utils.js";
5
+ let store = null;
6
+ function getStore() {
7
+ if (!store) {
8
+ store = createFileStore({
9
+ filename: SETTINGS_FILE,
10
+ schema: SettingsSchema,
11
+ defaultValue: createDefaultSettings,
12
+ debounceMs: SETTINGS_DEBOUNCE_MS,
13
+ });
14
+ }
15
+ return store;
16
+ }
17
+ export function loadSettings() {
18
+ const result = getStore().load();
19
+ return result.ok ? result.data : result.fallback;
20
+ }
21
+ export function saveSettings(settings) {
22
+ const current = getStore().getCache() ?? loadSettings();
23
+ const updated = { ...current, ...settings };
24
+ getStore().save(updated);
25
+ }
26
+ export function savePosition(x, y) {
27
+ if (!Number.isFinite(x) || !Number.isFinite(y))
28
+ return;
29
+ const hash = computeDisplayHash();
30
+ const current = getStore().getCache() ?? loadSettings();
31
+ const positions = { ...current.position };
32
+ positions[hash] = { x: Math.round(x), y: Math.round(y) };
33
+ const updated = { ...current, position: positions };
34
+ getStore().save(updated);
35
+ }
36
+ export function saveZoom(zoom) {
37
+ if (!Number.isFinite(zoom))
38
+ return;
39
+ const clamped = Math.max(CAMERA_ZOOM_MIN, Math.min(CAMERA_ZOOM_MAX, zoom));
40
+ const current = getStore().getCache() ?? loadSettings();
41
+ const updated = {
42
+ ...current,
43
+ camera: { zoom: clamped },
44
+ };
45
+ getStore().save(updated);
46
+ }
47
+ export function saveOpacity(opacity) {
48
+ if (!Number.isFinite(opacity))
49
+ return;
50
+ const clamped = Math.max(OPACITY_MIN, Math.min(OPACITY_MAX, opacity));
51
+ const current = getStore().getCache() ?? loadSettings();
52
+ const updated = {
53
+ ...current,
54
+ opacity: clamped,
55
+ };
56
+ getStore().save(updated);
57
+ }
58
+ export function saveIdleTimeout(ms) {
59
+ if (!Number.isInteger(ms) || ms < 0)
60
+ return;
61
+ const current = getStore().getCache() ?? loadSettings();
62
+ const updated = {
63
+ ...current,
64
+ idleTimeoutMs: ms,
65
+ };
66
+ getStore().save(updated);
67
+ }
68
+ export function getPosition() {
69
+ const settings = getStore().getCache() ?? loadSettings();
70
+ if (!settings.position)
71
+ return null;
72
+ const hash = computeDisplayHash();
73
+ return settings.position[hash] ?? null;
74
+ }
75
+ export function getZoom() {
76
+ const settings = getStore().getCache() ?? loadSettings();
77
+ return settings.camera?.zoom ?? CAMERA_ZOOM_DEFAULT;
78
+ }
79
+ export function getOpacity() {
80
+ const settings = getStore().getCache() ?? loadSettings();
81
+ return settings.opacity ?? OPACITY_DEFAULT;
82
+ }
83
+ export function getIdleTimeout() {
84
+ const settings = getStore().getCache() ?? loadSettings();
85
+ return settings.idleTimeoutMs ?? IDLE_TIMEOUT_DEFAULT;
86
+ }
87
+ export function saveTtsEnabled(enabled) {
88
+ const current = getStore().getCache() ?? loadSettings();
89
+ const updated = {
90
+ ...current,
91
+ ttsEnabled: enabled,
92
+ };
93
+ getStore().save(updated);
94
+ }
95
+ export function getTtsEnabled() {
96
+ const settings = getStore().getCache() ?? loadSettings();
97
+ return settings.ttsEnabled ?? TTS_ENABLED_DEFAULT;
98
+ }
99
+ export function saveTtsEngine(engine) {
100
+ const current = getStore().getCache() ?? loadSettings();
101
+ const updated = {
102
+ ...current,
103
+ ttsEngine: engine,
104
+ };
105
+ getStore().save(updated);
106
+ }
107
+ export function getTtsEngine() {
108
+ const settings = getStore().getCache() ?? loadSettings();
109
+ return settings.ttsEngine ?? TTS_ENGINE_DEFAULT;
110
+ }
111
+ export function saveTtsVoice(voice) {
112
+ const current = getStore().getCache() ?? loadSettings();
113
+ const updated = {
114
+ ...current,
115
+ ttsVoice: voice,
116
+ };
117
+ getStore().save(updated);
118
+ }
119
+ export function getTtsVoice() {
120
+ const settings = getStore().getCache() ?? loadSettings();
121
+ return settings.ttsVoice ?? TTS_VOICE_DEFAULT;
122
+ }
123
+ export function saveVrmModelPath(path) {
124
+ const current = getStore().getCache() ?? loadSettings();
125
+ const updated = {
126
+ ...current,
127
+ vrmModelPath: path,
128
+ };
129
+ getStore().save(updated);
130
+ }
131
+ export function getVrmModelPath() {
132
+ const settings = getStore().getCache() ?? loadSettings();
133
+ return settings.vrmModelPath;
134
+ }
135
+ export function saveScale(scale) {
136
+ if (!Number.isFinite(scale))
137
+ return;
138
+ const clamped = Math.max(SCALE_MIN, Math.min(SCALE_MAX, scale));
139
+ const current = getStore().getCache() ?? loadSettings();
140
+ const updated = { ...current, scale: clamped };
141
+ getStore().save(updated);
142
+ }
143
+ export function getScale() {
144
+ const settings = getStore().getCache() ?? loadSettings();
145
+ return settings.scale ?? SCALE_DEFAULT;
146
+ }
147
+ export function saveLightingProfile(profile) {
148
+ const current = getStore().getCache() ?? loadSettings();
149
+ const updated = { ...current, lightingProfile: profile };
150
+ getStore().save(updated);
151
+ }
152
+ export function getLightingProfile() {
153
+ const settings = getStore().getCache() ?? loadSettings();
154
+ return settings.lightingProfile ?? LIGHTING_PROFILE_DEFAULT;
155
+ }
156
+ export function saveLightingCustom(custom) {
157
+ const current = getStore().getCache() ?? loadSettings();
158
+ const updated = { ...current, lightingCustom: custom };
159
+ getStore().save(updated);
160
+ }
161
+ export function getLightingCustom() {
162
+ const settings = getStore().getCache() ?? loadSettings();
163
+ return settings.lightingCustom;
164
+ }
165
+ export async function flushSettings() {
166
+ await getStore().flush();
167
+ }
168
+ export function cleanupSettings() {
169
+ getStore().cleanup();
170
+ }
171
+ // For direct store access (e.g., migrations)
172
+ export function getSettingsStore() {
173
+ return getStore();
174
+ }