vibemon 1.5.0
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 +40 -0
- package/assets/characters/apto.png +0 -0
- package/assets/characters/claw.png +0 -0
- package/assets/characters/clawd.png +0 -0
- package/assets/characters/kiro.png +0 -0
- package/assets/generators/generate-icons.js +86 -0
- package/assets/generators/icon-128.png +0 -0
- package/assets/generators/icon-16.png +0 -0
- package/assets/generators/icon-256.png +0 -0
- package/assets/generators/icon-32.png +0 -0
- package/assets/generators/icon-64.png +0 -0
- package/assets/generators/icon-generator.html +221 -0
- package/assets/icon.icns +0 -0
- package/assets/icon.ico +0 -0
- package/assets/icon.png +0 -0
- package/bin/cli.js +16 -0
- package/index.html +26 -0
- package/main.js +358 -0
- package/modules/http-server.cjs +584 -0
- package/modules/http-utils.cjs +110 -0
- package/modules/multi-window-manager.cjs +927 -0
- package/modules/state-manager.cjs +168 -0
- package/modules/tray-manager.cjs +660 -0
- package/modules/validators.cjs +180 -0
- package/modules/ws-client.cjs +313 -0
- package/package.json +112 -0
- package/preload.js +22 -0
- package/renderer.js +84 -0
- package/shared/config.cjs +64 -0
- package/shared/constants.cjs +8 -0
- package/shared/data/constants.json +86 -0
- package/stats.html +521 -0
- package/styles.css +90 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input validation functions for the Vibe Monitor
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { VALID_STATES, CHARACTER_NAMES } = require('../shared/config.cjs');
|
|
6
|
+
|
|
7
|
+
// Validation limits
|
|
8
|
+
const PROJECT_MAX_LENGTH = 100;
|
|
9
|
+
const TOOL_MAX_LENGTH = 50;
|
|
10
|
+
const MODEL_MAX_LENGTH = 50;
|
|
11
|
+
const TERMINAL_ID_MAX_LENGTH = 100;
|
|
12
|
+
// Memory is now a number (0-100), not a string
|
|
13
|
+
// iTerm2: w0t0p0:UUID format, Ghostty: numeric PID
|
|
14
|
+
const ITERM2_SESSION_PATTERN = /^w\d+t\d+p\d+:[0-9A-Fa-f-]{36}$/;
|
|
15
|
+
const GHOSTTY_PID_PATTERN = /^\d{1,10}$/;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validate state value
|
|
19
|
+
* @param {string} state
|
|
20
|
+
* @returns {{valid: boolean, error: string|null}}
|
|
21
|
+
*/
|
|
22
|
+
function validateState(state) {
|
|
23
|
+
if (state === undefined) {
|
|
24
|
+
return { valid: true, error: null };
|
|
25
|
+
}
|
|
26
|
+
if (!VALID_STATES.includes(state)) {
|
|
27
|
+
return { valid: false, error: `Invalid state: ${state}. Valid states: ${VALID_STATES.join(', ')}` };
|
|
28
|
+
}
|
|
29
|
+
return { valid: true, error: null };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate character value
|
|
34
|
+
* @param {string} character
|
|
35
|
+
* @returns {{valid: boolean, error: string|null}}
|
|
36
|
+
*/
|
|
37
|
+
function validateCharacter(character) {
|
|
38
|
+
if (character === undefined) {
|
|
39
|
+
return { valid: true, error: null };
|
|
40
|
+
}
|
|
41
|
+
if (!CHARACTER_NAMES.includes(character)) {
|
|
42
|
+
return { valid: false, error: `Invalid character: ${character}. Valid characters: ${CHARACTER_NAMES.join(', ')}` };
|
|
43
|
+
}
|
|
44
|
+
return { valid: true, error: null };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validate project name
|
|
49
|
+
* @param {string} project
|
|
50
|
+
* @returns {{valid: boolean, error: string|null}}
|
|
51
|
+
*/
|
|
52
|
+
function validateProject(project) {
|
|
53
|
+
if (project === undefined) {
|
|
54
|
+
return { valid: true, error: null };
|
|
55
|
+
}
|
|
56
|
+
if (typeof project !== 'string') {
|
|
57
|
+
return { valid: false, error: 'Project must be a string' };
|
|
58
|
+
}
|
|
59
|
+
if (project.length > PROJECT_MAX_LENGTH) {
|
|
60
|
+
return { valid: false, error: `Project name exceeds ${PROJECT_MAX_LENGTH} characters` };
|
|
61
|
+
}
|
|
62
|
+
return { valid: true, error: null };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validate memory value (number 0-100)
|
|
67
|
+
* @param {number} memory
|
|
68
|
+
* @returns {{valid: boolean, error: string|null}}
|
|
69
|
+
*/
|
|
70
|
+
function validateMemory(memory) {
|
|
71
|
+
if (memory === undefined || memory === null || memory === '') {
|
|
72
|
+
return { valid: true, error: null };
|
|
73
|
+
}
|
|
74
|
+
if (typeof memory !== 'number') {
|
|
75
|
+
return { valid: false, error: 'Memory must be a number' };
|
|
76
|
+
}
|
|
77
|
+
if (!Number.isInteger(memory) || memory < 0 || memory > 100) {
|
|
78
|
+
return { valid: false, error: 'Memory must be an integer between 0 and 100' };
|
|
79
|
+
}
|
|
80
|
+
return { valid: true, error: null };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validate tool name
|
|
85
|
+
* @param {string} tool
|
|
86
|
+
* @returns {{valid: boolean, error: string|null}}
|
|
87
|
+
*/
|
|
88
|
+
function validateTool(tool) {
|
|
89
|
+
if (tool === undefined || tool === '') {
|
|
90
|
+
return { valid: true, error: null };
|
|
91
|
+
}
|
|
92
|
+
if (typeof tool !== 'string') {
|
|
93
|
+
return { valid: false, error: 'Tool must be a string' };
|
|
94
|
+
}
|
|
95
|
+
if (tool.length > TOOL_MAX_LENGTH) {
|
|
96
|
+
return { valid: false, error: `Tool name exceeds ${TOOL_MAX_LENGTH} characters` };
|
|
97
|
+
}
|
|
98
|
+
return { valid: true, error: null };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validate model name
|
|
103
|
+
* @param {string} model
|
|
104
|
+
* @returns {{valid: boolean, error: string|null}}
|
|
105
|
+
*/
|
|
106
|
+
function validateModel(model) {
|
|
107
|
+
if (model === undefined || model === '') {
|
|
108
|
+
return { valid: true, error: null };
|
|
109
|
+
}
|
|
110
|
+
if (typeof model !== 'string') {
|
|
111
|
+
return { valid: false, error: 'Model must be a string' };
|
|
112
|
+
}
|
|
113
|
+
if (model.length > MODEL_MAX_LENGTH) {
|
|
114
|
+
return { valid: false, error: `Model name exceeds ${MODEL_MAX_LENGTH} characters` };
|
|
115
|
+
}
|
|
116
|
+
return { valid: true, error: null };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validate terminal ID (iTerm2 session or Ghostty PID)
|
|
121
|
+
* @param {string} terminalId
|
|
122
|
+
* @returns {{valid: boolean, error: string|null}}
|
|
123
|
+
*/
|
|
124
|
+
function validateTerminalId(terminalId) {
|
|
125
|
+
if (terminalId === undefined || terminalId === null || terminalId === '') {
|
|
126
|
+
return { valid: true, error: null };
|
|
127
|
+
}
|
|
128
|
+
if (typeof terminalId !== 'string') {
|
|
129
|
+
return { valid: false, error: 'terminalId must be a string' };
|
|
130
|
+
}
|
|
131
|
+
if (terminalId.length > TERMINAL_ID_MAX_LENGTH) {
|
|
132
|
+
return { valid: false, error: `terminalId exceeds ${TERMINAL_ID_MAX_LENGTH} characters` };
|
|
133
|
+
}
|
|
134
|
+
// Accept iTerm2 session format or Ghostty PID format
|
|
135
|
+
if (!ITERM2_SESSION_PATTERN.test(terminalId) && !GHOSTTY_PID_PATTERN.test(terminalId)) {
|
|
136
|
+
return { valid: false, error: 'terminalId must be a valid iTerm2 session ID or Ghostty PID' };
|
|
137
|
+
}
|
|
138
|
+
return { valid: true, error: null };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Validate status payload
|
|
143
|
+
* @param {object} data
|
|
144
|
+
* @returns {{valid: boolean, error: string|null}}
|
|
145
|
+
*/
|
|
146
|
+
function validateStatusPayload(data) {
|
|
147
|
+
const stateResult = validateState(data.state);
|
|
148
|
+
if (!stateResult.valid) return stateResult;
|
|
149
|
+
|
|
150
|
+
const characterResult = validateCharacter(data.character);
|
|
151
|
+
if (!characterResult.valid) return characterResult;
|
|
152
|
+
|
|
153
|
+
const projectResult = validateProject(data.project);
|
|
154
|
+
if (!projectResult.valid) return projectResult;
|
|
155
|
+
|
|
156
|
+
const memoryResult = validateMemory(data.memory);
|
|
157
|
+
if (!memoryResult.valid) return memoryResult;
|
|
158
|
+
|
|
159
|
+
const toolResult = validateTool(data.tool);
|
|
160
|
+
if (!toolResult.valid) return toolResult;
|
|
161
|
+
|
|
162
|
+
const modelResult = validateModel(data.model);
|
|
163
|
+
if (!modelResult.valid) return modelResult;
|
|
164
|
+
|
|
165
|
+
const terminalIdResult = validateTerminalId(data.terminalId);
|
|
166
|
+
if (!terminalIdResult.valid) return terminalIdResult;
|
|
167
|
+
|
|
168
|
+
return { valid: true, error: null };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
validateState,
|
|
173
|
+
validateCharacter,
|
|
174
|
+
validateProject,
|
|
175
|
+
validateMemory,
|
|
176
|
+
validateTool,
|
|
177
|
+
validateModel,
|
|
178
|
+
validateTerminalId,
|
|
179
|
+
validateStatusPayload
|
|
180
|
+
};
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket client for Vibe Monitor
|
|
3
|
+
* Connects to a central server to receive real-time status updates
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const WebSocket = require('ws');
|
|
7
|
+
const Store = require('electron-store');
|
|
8
|
+
const { WS_URL, WS_TOKEN } = require('../shared/config.cjs');
|
|
9
|
+
|
|
10
|
+
// Reconnection configuration
|
|
11
|
+
const RECONNECT_INITIAL_DELAY = 5000; // 5 seconds
|
|
12
|
+
const RECONNECT_MAX_DELAY = 30000; // 30 seconds
|
|
13
|
+
const RECONNECT_MULTIPLIER = 1.5;
|
|
14
|
+
|
|
15
|
+
class WsClient {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.ws = null;
|
|
18
|
+
this.url = WS_URL;
|
|
19
|
+
this.reconnectDelay = RECONNECT_INITIAL_DELAY;
|
|
20
|
+
this.reconnectTimer = null;
|
|
21
|
+
this.isConnecting = false;
|
|
22
|
+
this.isConnected = false;
|
|
23
|
+
this.shouldReconnect = true;
|
|
24
|
+
|
|
25
|
+
// Persistent storage for token
|
|
26
|
+
this.store = new Store({
|
|
27
|
+
name: 'ws-settings',
|
|
28
|
+
defaults: {
|
|
29
|
+
token: null
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Load token: stored value > environment variable
|
|
34
|
+
const storedToken = this.store.get('token');
|
|
35
|
+
this.token = storedToken || WS_TOKEN || null;
|
|
36
|
+
|
|
37
|
+
// Callbacks
|
|
38
|
+
this.onStatusUpdate = null; // Called when status message received
|
|
39
|
+
this.onConnectionChange = null; // Called when connection state changes
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get current token
|
|
44
|
+
* @returns {string|null}
|
|
45
|
+
*/
|
|
46
|
+
getToken() {
|
|
47
|
+
return this.token;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Set token and save to store
|
|
52
|
+
* @param {string|null} token
|
|
53
|
+
*/
|
|
54
|
+
setToken(token) {
|
|
55
|
+
this.token = token || null;
|
|
56
|
+
this.store.set('token', this.token);
|
|
57
|
+
|
|
58
|
+
// Reconnect with new token (token is passed via URL query parameter)
|
|
59
|
+
if (this.isConnected || this.isConnecting) {
|
|
60
|
+
this.reconnect();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reconnect to WebSocket server
|
|
66
|
+
*/
|
|
67
|
+
reconnect() {
|
|
68
|
+
// Close current connection
|
|
69
|
+
if (this.ws) {
|
|
70
|
+
this.ws.close();
|
|
71
|
+
this.ws = null;
|
|
72
|
+
}
|
|
73
|
+
this.isConnecting = false;
|
|
74
|
+
this.isConnected = false;
|
|
75
|
+
|
|
76
|
+
// Clear any pending reconnect timer
|
|
77
|
+
if (this.reconnectTimer) {
|
|
78
|
+
clearTimeout(this.reconnectTimer);
|
|
79
|
+
this.reconnectTimer = null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Reconnect immediately
|
|
83
|
+
this.reconnectDelay = RECONNECT_INITIAL_DELAY;
|
|
84
|
+
this.connect();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Clear token from store
|
|
89
|
+
*/
|
|
90
|
+
clearToken() {
|
|
91
|
+
this.token = null;
|
|
92
|
+
this.store.delete('token');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if WebSocket is configured
|
|
97
|
+
* @returns {boolean}
|
|
98
|
+
*/
|
|
99
|
+
isConfigured() {
|
|
100
|
+
return Boolean(this.url);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get connection status
|
|
105
|
+
* @returns {string} 'connected', 'connecting', 'disconnected', or 'not-configured'
|
|
106
|
+
*/
|
|
107
|
+
getStatus() {
|
|
108
|
+
if (!this.isConfigured()) {
|
|
109
|
+
return 'not-configured';
|
|
110
|
+
}
|
|
111
|
+
if (this.isConnected) {
|
|
112
|
+
return 'connected';
|
|
113
|
+
}
|
|
114
|
+
if (this.isConnecting) {
|
|
115
|
+
return 'connecting';
|
|
116
|
+
}
|
|
117
|
+
return 'disconnected';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Build connection URL with token as query parameter (like ESP32)
|
|
122
|
+
* @returns {string}
|
|
123
|
+
*/
|
|
124
|
+
buildConnectionUrl() {
|
|
125
|
+
if (!this.token) {
|
|
126
|
+
return this.url;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Add token as query parameter (same as ESP32: /?token=xxx)
|
|
130
|
+
const separator = this.url.includes('?') ? '&' : '?';
|
|
131
|
+
return `${this.url}${separator}token=${encodeURIComponent(this.token)}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Start WebSocket connection
|
|
136
|
+
*/
|
|
137
|
+
connect() {
|
|
138
|
+
if (!this.isConfigured()) {
|
|
139
|
+
console.log('WebSocket not configured (VIBEMON_WS_URL not set)');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (this.isConnecting || this.isConnected) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.isConnecting = true;
|
|
148
|
+
this.notifyConnectionChange();
|
|
149
|
+
|
|
150
|
+
const connectionUrl = this.buildConnectionUrl();
|
|
151
|
+
console.log(`WebSocket connecting to ${this.url}...`);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
this.ws = new WebSocket(connectionUrl);
|
|
155
|
+
|
|
156
|
+
this.ws.on('open', () => {
|
|
157
|
+
console.log('WebSocket connected');
|
|
158
|
+
this.isConnecting = false;
|
|
159
|
+
this.isConnected = true;
|
|
160
|
+
this.reconnectDelay = RECONNECT_INITIAL_DELAY;
|
|
161
|
+
this.notifyConnectionChange();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
this.ws.on('message', (data) => {
|
|
165
|
+
this.handleMessage(data);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
this.ws.on('close', (code, reason) => {
|
|
169
|
+
console.log(`WebSocket closed: ${code} ${reason}`);
|
|
170
|
+
this.handleDisconnect();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
this.ws.on('error', (error) => {
|
|
174
|
+
console.error('WebSocket error:', error.message);
|
|
175
|
+
// Error will be followed by close event
|
|
176
|
+
});
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error('WebSocket connection error:', error.message);
|
|
179
|
+
this.handleDisconnect();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Send authentication message
|
|
185
|
+
*/
|
|
186
|
+
sendAuth() {
|
|
187
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const authMessage = JSON.stringify({
|
|
192
|
+
type: 'auth',
|
|
193
|
+
token: this.token
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
this.ws.send(authMessage);
|
|
197
|
+
console.log('WebSocket auth sent');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Handle incoming message
|
|
202
|
+
* @param {Buffer|string} data
|
|
203
|
+
*/
|
|
204
|
+
handleMessage(data) {
|
|
205
|
+
try {
|
|
206
|
+
const message = JSON.parse(data.toString());
|
|
207
|
+
|
|
208
|
+
// Handle error messages from server
|
|
209
|
+
if (message.type === 'error') {
|
|
210
|
+
console.error('WebSocket server error:', message.message);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Handle auth success
|
|
215
|
+
if (message.type === 'authenticated') {
|
|
216
|
+
console.log('WebSocket authenticated, userId:', message.userId);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Handle status update (server sends {type: "status", data: {...}})
|
|
221
|
+
if (message.type === 'status' && message.data) {
|
|
222
|
+
if (this.onStatusUpdate) {
|
|
223
|
+
this.onStatusUpdate(message.data);
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Handle status update (direct format: {state: "..."})
|
|
229
|
+
if (message.state) {
|
|
230
|
+
if (this.onStatusUpdate) {
|
|
231
|
+
this.onStatusUpdate(message);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error('WebSocket message parse error:', error.message);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Handle disconnection and schedule reconnect
|
|
241
|
+
*/
|
|
242
|
+
handleDisconnect() {
|
|
243
|
+
this.isConnecting = false;
|
|
244
|
+
this.isConnected = false;
|
|
245
|
+
this.ws = null;
|
|
246
|
+
this.notifyConnectionChange();
|
|
247
|
+
|
|
248
|
+
if (this.shouldReconnect) {
|
|
249
|
+
this.scheduleReconnect();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Schedule reconnection with exponential backoff
|
|
255
|
+
*/
|
|
256
|
+
scheduleReconnect() {
|
|
257
|
+
if (this.reconnectTimer) {
|
|
258
|
+
clearTimeout(this.reconnectTimer);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log(`WebSocket reconnecting in ${this.reconnectDelay / 1000}s...`);
|
|
262
|
+
|
|
263
|
+
this.reconnectTimer = setTimeout(() => {
|
|
264
|
+
this.reconnectTimer = null;
|
|
265
|
+
this.connect();
|
|
266
|
+
}, this.reconnectDelay);
|
|
267
|
+
|
|
268
|
+
// Increase delay for next attempt (exponential backoff)
|
|
269
|
+
this.reconnectDelay = Math.min(
|
|
270
|
+
this.reconnectDelay * RECONNECT_MULTIPLIER,
|
|
271
|
+
RECONNECT_MAX_DELAY
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Notify connection state change
|
|
277
|
+
*/
|
|
278
|
+
notifyConnectionChange() {
|
|
279
|
+
if (this.onConnectionChange) {
|
|
280
|
+
this.onConnectionChange(this.getStatus());
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Disconnect and stop reconnection
|
|
286
|
+
*/
|
|
287
|
+
disconnect() {
|
|
288
|
+
this.shouldReconnect = false;
|
|
289
|
+
|
|
290
|
+
if (this.reconnectTimer) {
|
|
291
|
+
clearTimeout(this.reconnectTimer);
|
|
292
|
+
this.reconnectTimer = null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (this.ws) {
|
|
296
|
+
this.ws.close();
|
|
297
|
+
this.ws = null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.isConnecting = false;
|
|
301
|
+
this.isConnected = false;
|
|
302
|
+
this.notifyConnectionChange();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Cleanup resources
|
|
307
|
+
*/
|
|
308
|
+
cleanup() {
|
|
309
|
+
this.disconnect();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
module.exports = { WsClient };
|
package/package.json
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vibemon",
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "AI assistant status monitor",
|
|
5
|
+
"main": "main.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vibemon": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "electron .",
|
|
11
|
+
"build": "electron-builder",
|
|
12
|
+
"build:mac": "electron-builder --mac",
|
|
13
|
+
"build:dmg": "electron-builder --mac dmg",
|
|
14
|
+
"build:win": "electron-builder --win",
|
|
15
|
+
"build:linux": "electron-builder --linux",
|
|
16
|
+
"build:all": "electron-builder --mac --win --linux",
|
|
17
|
+
"lint": "eslint .",
|
|
18
|
+
"lint:fix": "eslint . --fix",
|
|
19
|
+
"test": "jest",
|
|
20
|
+
"test:watch": "jest --watch",
|
|
21
|
+
"test:coverage": "jest --coverage"
|
|
22
|
+
},
|
|
23
|
+
"author": "nalbam",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"keywords": [
|
|
26
|
+
"vibemon",
|
|
27
|
+
"claude-code",
|
|
28
|
+
"kiro",
|
|
29
|
+
"ai-assistant",
|
|
30
|
+
"status-monitor",
|
|
31
|
+
"electron",
|
|
32
|
+
"pixel-art"
|
|
33
|
+
],
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/nalbam/vibemon-app.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://nalbam.github.io/vibemon-app/",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/nalbam/vibemon-app/issues"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"canvas": "^3.2.1",
|
|
44
|
+
"dotenv": "^17.2.3",
|
|
45
|
+
"electron": "^33.0.0",
|
|
46
|
+
"electron-store": "^8.2.0",
|
|
47
|
+
"ws": "^8.19.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"electron-builder": "^25.0.0",
|
|
51
|
+
"eslint": "^9.0.0",
|
|
52
|
+
"jest": "^29.7.0"
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
"README.md",
|
|
56
|
+
"bin/",
|
|
57
|
+
"main.js",
|
|
58
|
+
"preload.js",
|
|
59
|
+
"index.html",
|
|
60
|
+
"stats.html",
|
|
61
|
+
"renderer.js",
|
|
62
|
+
"styles.css",
|
|
63
|
+
"assets/",
|
|
64
|
+
"modules/",
|
|
65
|
+
"shared/"
|
|
66
|
+
],
|
|
67
|
+
"build": {
|
|
68
|
+
"appId": "com.nalbam.vibemon",
|
|
69
|
+
"productName": "VibeMon",
|
|
70
|
+
"mac": {
|
|
71
|
+
"category": "public.app-category.developer-tools",
|
|
72
|
+
"icon": "assets/icon.icns",
|
|
73
|
+
"target": [
|
|
74
|
+
"dmg",
|
|
75
|
+
"zip"
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
"win": {
|
|
79
|
+
"icon": "assets/icon.ico",
|
|
80
|
+
"target": [
|
|
81
|
+
{
|
|
82
|
+
"target": "nsis",
|
|
83
|
+
"arch": [
|
|
84
|
+
"x64",
|
|
85
|
+
"arm64"
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"target": "portable",
|
|
90
|
+
"arch": [
|
|
91
|
+
"x64"
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
"nsis": {
|
|
97
|
+
"oneClick": false,
|
|
98
|
+
"perMachine": false,
|
|
99
|
+
"allowToChangeInstallationDirectory": true,
|
|
100
|
+
"createDesktopShortcut": true,
|
|
101
|
+
"createStartMenuShortcut": true
|
|
102
|
+
},
|
|
103
|
+
"linux": {
|
|
104
|
+
"icon": "assets/icon.png",
|
|
105
|
+
"target": [
|
|
106
|
+
"AppImage",
|
|
107
|
+
"deb"
|
|
108
|
+
],
|
|
109
|
+
"category": "Development"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
package/preload.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
2
|
+
|
|
3
|
+
contextBridge.exposeInMainWorld('electronAPI', {
|
|
4
|
+
closeWindow: () => ipcRenderer.send('close-window'),
|
|
5
|
+
minimizeWindow: () => ipcRenderer.send('minimize-window'),
|
|
6
|
+
showContextMenu: () => ipcRenderer.send('show-context-menu'),
|
|
7
|
+
focusTerminal: () => ipcRenderer.invoke('focus-terminal'),
|
|
8
|
+
onStateUpdate: (callback) => {
|
|
9
|
+
const handler = (_event, data) => {
|
|
10
|
+
try {
|
|
11
|
+
callback(data);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error('State update callback error:', error);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
ipcRenderer.on('state-update', handler);
|
|
17
|
+
// Return cleanup function to prevent memory leaks
|
|
18
|
+
return () => ipcRenderer.removeListener('state-update', handler);
|
|
19
|
+
},
|
|
20
|
+
getVersion: () => ipcRenderer.invoke('get-version'),
|
|
21
|
+
getPlatform: () => process.platform
|
|
22
|
+
});
|
package/renderer.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createVibeMonEngine } from 'https://static.vibemon.io/js/vibemon-engine-standalone.js';
|
|
2
|
+
|
|
3
|
+
// Static server base URL
|
|
4
|
+
const STATIC_BASE = 'https://static.vibemon.io';
|
|
5
|
+
|
|
6
|
+
// VibeMon engine instance
|
|
7
|
+
let vibeMonEngine = null;
|
|
8
|
+
|
|
9
|
+
// IPC cleanup function
|
|
10
|
+
let cleanupStateListener = null;
|
|
11
|
+
|
|
12
|
+
// Initialize
|
|
13
|
+
async function init() {
|
|
14
|
+
const container = document.getElementById('vibemon-display');
|
|
15
|
+
|
|
16
|
+
// Get platform info for emoji detection
|
|
17
|
+
let useEmoji = false;
|
|
18
|
+
if (window.electronAPI?.getPlatform) {
|
|
19
|
+
const platform = window.electronAPI.getPlatform();
|
|
20
|
+
useEmoji = platform === 'darwin';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Create and initialize VibeMon engine with static server images
|
|
24
|
+
vibeMonEngine = createVibeMonEngine(container, {
|
|
25
|
+
useEmoji,
|
|
26
|
+
characterImageUrls: {
|
|
27
|
+
apto: `${STATIC_BASE}/characters/apto.png`,
|
|
28
|
+
clawd: `${STATIC_BASE}/characters/clawd.png`,
|
|
29
|
+
kiro: `${STATIC_BASE}/characters/kiro.png`,
|
|
30
|
+
claw: `${STATIC_BASE}/characters/claw.png`
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
await vibeMonEngine.init();
|
|
34
|
+
|
|
35
|
+
// Initial render and start animation
|
|
36
|
+
vibeMonEngine.render();
|
|
37
|
+
vibeMonEngine.startAnimation();
|
|
38
|
+
|
|
39
|
+
// Listen for state updates from main process
|
|
40
|
+
if (window.electronAPI) {
|
|
41
|
+
cleanupStateListener = window.electronAPI.onStateUpdate((data) => {
|
|
42
|
+
// Validate incoming data
|
|
43
|
+
if (!data || typeof data !== 'object') return;
|
|
44
|
+
|
|
45
|
+
// Update state in VibeMon engine
|
|
46
|
+
vibeMonEngine.setState(data);
|
|
47
|
+
vibeMonEngine.render();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Right-click context menu (works on all platforms)
|
|
52
|
+
document.addEventListener('contextmenu', (e) => {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
if (window.electronAPI?.showContextMenu) {
|
|
55
|
+
window.electronAPI.showContextMenu();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Click to focus terminal (iTerm2/Ghostty on macOS)
|
|
60
|
+
document.addEventListener('click', (e) => {
|
|
61
|
+
// Ignore right-click
|
|
62
|
+
if (e.button !== 0) return;
|
|
63
|
+
if (window.electronAPI?.focusTerminal) {
|
|
64
|
+
window.electronAPI.focusTerminal();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Cleanup on unload
|
|
70
|
+
function cleanup() {
|
|
71
|
+
if (vibeMonEngine) {
|
|
72
|
+
vibeMonEngine.cleanup();
|
|
73
|
+
vibeMonEngine = null;
|
|
74
|
+
}
|
|
75
|
+
if (cleanupStateListener) {
|
|
76
|
+
cleanupStateListener();
|
|
77
|
+
cleanupStateListener = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Initialize on load
|
|
82
|
+
window.onload = init;
|
|
83
|
+
window.onbeforeunload = cleanup;
|
|
84
|
+
window.onunload = cleanup;
|