watercooler 0.0.8 → 0.0.10

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 CHANGED
@@ -1,6 +1,9 @@
1
- # Watercooler Village
2
1
 
3
- A beautiful 3D visualization of your mailbox messages as a village of coworkers.
2
+ <div align="center"><img width="500" alt="image" src="https://github.com/user-attachments/assets/a4ab0748-cd1b-43d6-9d8b-35e64eb954a2" /></div>
3
+
4
+ # WaterCooler
5
+
6
+ A beautiful 3D visualization of your office of autonomous AI coworkers and their communication.
4
7
 
5
8
  ## Installation
6
9
 
@@ -45,7 +48,7 @@ npm start -- --user richard --mailbox ~/.config/opencode/mailbox.db
45
48
 
46
49
  ## Features
47
50
 
48
- - **3D Village**: Each coworker appears as a colorful house in a circle
51
+ - **3D Office**: Each coworker appears as a colorful house in a circle
49
52
  - **Message Flow**: Animated particles show messages traveling between houses
50
53
  - **Visual Status**:
51
54
  - Green lines = read messages
@@ -59,7 +62,7 @@ npm start -- --user richard --mailbox ~/.config/opencode/mailbox.db
59
62
 
60
63
  - **Backend**: Express server with SQLite
61
64
  - **Frontend**: Vanilla JavaScript + Three.js (from CDN)
62
- - **TypeScript**: Runs directly with tsx (included as a dependency)
65
+ - **TypeScript**: Compiled to JavaScript for production
63
66
 
64
67
  ## Database Integration
65
68
 
@@ -69,4 +72,4 @@ Contains messages table with: id, recipient, sender, message, timestamp, read
69
72
  ### Coworker DB (optional)
70
73
  Contains coworkers table with: name, session_id, agent_type, created_at, parent_id
71
74
 
72
- When provided, watercooler shows ALL coworkers from the database, regardless of whether they have sent/received messages yet.
75
+ When provided, watercooler shows ALL coworkers from the database, regardless of whether they have sent/received messages yet.
@@ -6,9 +6,9 @@ import { dirname, join } from 'path';
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = dirname(__filename);
8
8
 
9
- const serverPath = join(__dirname, '..', 'server.ts');
9
+ const serverPath = join(__dirname, '..', 'dist', 'server.js');
10
10
 
11
- const child = spawn('tsx', [serverPath, ...process.argv.slice(2)], {
11
+ const child = spawn('node', [serverPath, ...process.argv.slice(2)], {
12
12
  stdio: 'inherit',
13
13
  shell: false
14
14
  });
package/dist/server.js ADDED
@@ -0,0 +1,296 @@
1
+ import express from 'express';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import Database from 'better-sqlite3';
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ const app = express();
8
+ // Parse CLI args
9
+ const args = process.argv.slice(2);
10
+ let user = null;
11
+ let mailboxPath = null;
12
+ let coworkerPath = null;
13
+ let statusPath = null;
14
+ let port = parseInt(process.env.PORT || '3000', 10);
15
+ let host = process.env.HOST || '0.0.0.0';
16
+ for (let i = 0; i < args.length; i++) {
17
+ if (args[i] === '--user' || args[i] === '-u') {
18
+ user = args[++i];
19
+ }
20
+ else if (args[i] === '--mailbox' || args[i] === '-m') {
21
+ mailboxPath = args[++i];
22
+ }
23
+ else if (args[i] === '--coworkers' || args[i] === '-c') {
24
+ coworkerPath = args[++i];
25
+ }
26
+ else if (args[i] === '--status' || args[i] === '-s') {
27
+ statusPath = args[++i];
28
+ }
29
+ else if (args[i] === '--port' || args[i] === '-p') {
30
+ const p = parseInt(args[++i], 10);
31
+ if (!isNaN(p))
32
+ port = p;
33
+ }
34
+ else if (args[i] === '--host' || args[i] === '-h') {
35
+ host = args[++i];
36
+ }
37
+ }
38
+ if (!user || !mailboxPath) {
39
+ console.error('Usage: watercooler --user <name> --mailbox <path> [--coworkers <path>] [--status <path>] [--port <number>] [--host <address>]');
40
+ process.exit(1);
41
+ }
42
+ console.log(`🚰 Watercooler for ${user}`);
43
+ console.log(` Mailbox: ${mailboxPath}`);
44
+ if (coworkerPath) {
45
+ console.log(` Coworker DB: ${coworkerPath}`);
46
+ }
47
+ if (statusPath) {
48
+ console.log(` Status DB: ${statusPath}`);
49
+ }
50
+ console.log(` URL: http://${host}:${port}`);
51
+ // Databases
52
+ let db = null;
53
+ let coworkerDb = null;
54
+ let statusDb = null;
55
+ try {
56
+ db = new Database(mailboxPath);
57
+ console.log(' Mailbox DB: connected');
58
+ }
59
+ catch (err) {
60
+ console.error(' Mailbox DB error:', err.message);
61
+ process.exit(1);
62
+ }
63
+ if (coworkerPath) {
64
+ try {
65
+ coworkerDb = new Database(coworkerPath);
66
+ console.log(' Coworker DB: connected');
67
+ }
68
+ catch (err) {
69
+ console.warn(' Coworker DB error:', err.message);
70
+ }
71
+ }
72
+ if (statusPath) {
73
+ try {
74
+ statusDb = new Database(statusPath);
75
+ console.log(' Status DB: connected');
76
+ }
77
+ catch (err) {
78
+ console.warn(' Status DB error:', err.message);
79
+ }
80
+ }
81
+ // Helper: Check if table exists
82
+ function tableExists(database, tableName) {
83
+ if (!database)
84
+ return false;
85
+ try {
86
+ const stmt = database.prepare(`
87
+ SELECT name FROM sqlite_master
88
+ WHERE type='table' AND name=?
89
+ `);
90
+ return !!stmt.get(tableName);
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ }
96
+ // Middleware
97
+ app.use(express.json());
98
+ app.use(express.static(path.join(__dirname, 'public')));
99
+ // API: Get inbox (messages TO user)
100
+ app.get('/api/messages', (req, res) => {
101
+ try {
102
+ if (!db)
103
+ throw new Error('Database not connected');
104
+ if (!tableExists(db, 'messages')) {
105
+ res.json([]);
106
+ return;
107
+ }
108
+ const stmt = db.prepare(`
109
+ SELECT * FROM messages
110
+ WHERE recipient = ?
111
+ ORDER BY timestamp DESC
112
+ `);
113
+ res.json(stmt.all(user.toLowerCase()));
114
+ }
115
+ catch (err) {
116
+ res.status(500).json({ error: err.message });
117
+ }
118
+ });
119
+ // API: Get sent messages (messages FROM user)
120
+ app.get('/api/messages/sent', (req, res) => {
121
+ try {
122
+ if (!db)
123
+ throw new Error('Database not connected');
124
+ if (!tableExists(db, 'messages')) {
125
+ res.json([]);
126
+ return;
127
+ }
128
+ const stmt = db.prepare(`
129
+ SELECT * FROM messages
130
+ WHERE sender = ?
131
+ ORDER BY timestamp DESC
132
+ `);
133
+ res.json(stmt.all(user.toLowerCase()));
134
+ }
135
+ catch (err) {
136
+ res.status(500).json({ error: err.message });
137
+ }
138
+ });
139
+ // API: Get ALL messages between ALL agents
140
+ app.get('/api/messages/all', (req, res) => {
141
+ try {
142
+ if (!db)
143
+ throw new Error('Database not connected');
144
+ if (!tableExists(db, 'messages')) {
145
+ res.json([]);
146
+ return;
147
+ }
148
+ const stmt = db.prepare(`
149
+ SELECT * FROM messages
150
+ ORDER BY timestamp DESC
151
+ `);
152
+ res.json(stmt.all());
153
+ }
154
+ catch (err) {
155
+ res.status(500).json({ error: err.message });
156
+ }
157
+ });
158
+ // API: Get all coworkers (from coworker.db + message recipients)
159
+ app.get('/api/coworkers', (req, res) => {
160
+ try {
161
+ if (!db)
162
+ throw new Error('Database not connected');
163
+ const allCoworkers = new Set();
164
+ // Add from coworker.db if available
165
+ if (coworkerDb) {
166
+ try {
167
+ const rows = coworkerDb.prepare('SELECT name FROM coworkers').all();
168
+ rows.forEach(row => allCoworkers.add(row.name.toLowerCase()));
169
+ }
170
+ catch (err) {
171
+ console.error('Error reading coworker.db:', err.message);
172
+ }
173
+ }
174
+ else {
175
+ console.log('No coworkerDb connection available');
176
+ }
177
+ // Note: Messages table is in a different database, not queried here
178
+ // Remove current user
179
+ allCoworkers.delete(user.toLowerCase());
180
+ const result = Array.from(allCoworkers).sort();
181
+ res.json(result);
182
+ }
183
+ catch (err) {
184
+ console.error('Error in /api/coworkers:', err.message);
185
+ res.status(500).json({ error: err.message });
186
+ }
187
+ });
188
+ // Legacy: Get recipients (for backwards compat)
189
+ app.get('/api/recipients', (req, res) => {
190
+ try {
191
+ if (!db)
192
+ throw new Error('Database not connected');
193
+ if (!tableExists(db, 'messages')) {
194
+ res.json([]);
195
+ return;
196
+ }
197
+ const stmt = db.prepare(`SELECT DISTINCT recipient FROM messages`);
198
+ res.json(stmt.all().map((r) => r.recipient));
199
+ }
200
+ catch (err) {
201
+ res.status(500).json({ error: err.message });
202
+ }
203
+ });
204
+ // API: Send message
205
+ app.post('/api/send', (req, res) => {
206
+ try {
207
+ if (!db)
208
+ throw new Error('Database not connected');
209
+ // Auto-create messages table if it doesn't exist
210
+ if (!tableExists(db, 'messages')) {
211
+ db.exec(`
212
+ CREATE TABLE messages (
213
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
214
+ recipient TEXT NOT NULL,
215
+ sender TEXT NOT NULL,
216
+ message TEXT NOT NULL,
217
+ timestamp INTEGER NOT NULL,
218
+ read INTEGER DEFAULT 0
219
+ )
220
+ `);
221
+ }
222
+ const { to, message } = req.body;
223
+ const stmt = db.prepare(`
224
+ INSERT INTO messages (recipient, sender, message, timestamp, read)
225
+ VALUES (?, ?, ?, ?, 0)
226
+ `);
227
+ stmt.run(to.toLowerCase(), user.toLowerCase(), message, Date.now());
228
+ res.json({ success: true });
229
+ }
230
+ catch (err) {
231
+ res.status(500).json({ error: err.message });
232
+ }
233
+ });
234
+ // API: Mark read
235
+ app.post('/api/messages/:id/read', (req, res) => {
236
+ try {
237
+ if (!db)
238
+ throw new Error('Database not connected');
239
+ if (!tableExists(db, 'messages')) {
240
+ res.status(404).json({ error: 'Messages table not found' });
241
+ return;
242
+ }
243
+ db.prepare('UPDATE messages SET read = 1 WHERE id = ?').run(req.params.id);
244
+ res.json({ success: true });
245
+ }
246
+ catch (err) {
247
+ res.status(500).json({ error: err.message });
248
+ }
249
+ });
250
+ // API: Get status states (latest tool usage per coworker)
251
+ app.get('/api/status', (req, res) => {
252
+ try {
253
+ if (!statusDb) {
254
+ res.json({});
255
+ return;
256
+ }
257
+ // Check if latest_tool_usage table exists
258
+ const tableCheck = statusDb.prepare(`
259
+ SELECT name FROM sqlite_master
260
+ WHERE type='table' AND name='latest_tool_usage'
261
+ `).get();
262
+ if (!tableCheck) {
263
+ res.json({});
264
+ return;
265
+ }
266
+ // Get latest tool usage per name
267
+ const stmt = statusDb.prepare(`
268
+ SELECT name, tool_name, timestamp
269
+ FROM latest_tool_usage
270
+ ORDER BY timestamp DESC
271
+ `);
272
+ const rows = stmt.all();
273
+ // Build map of name -> latest tool (first occurrence is latest due to ORDER BY)
274
+ const statusStates = {};
275
+ for (const row of rows) {
276
+ if (!statusStates[row.name]) {
277
+ statusStates[row.name] = {
278
+ tool_name: row.tool_name,
279
+ timestamp: row.timestamp
280
+ };
281
+ }
282
+ }
283
+ res.json(statusStates);
284
+ }
285
+ catch (err) {
286
+ console.error('Error in /api/status:', err.message);
287
+ res.status(500).json({ error: err.message });
288
+ }
289
+ });
290
+ // Config endpoint
291
+ app.get('/api/config', (req, res) => {
292
+ res.json({ user, mailbox: mailboxPath, coworker: coworkerPath, status: statusPath });
293
+ });
294
+ app.listen(port, host, () => {
295
+ console.log('\nāœ… Watercooler running!');
296
+ });
package/package.json CHANGED
@@ -1,21 +1,22 @@
1
1
  {
2
2
  "name": "watercooler",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "A beautiful 3D visualization of your mailbox messages as a village of coworkers",
5
5
  "type": "module",
6
- "main": "server.ts",
6
+ "main": "dist/server.js",
7
7
  "bin": {
8
- "watercooler": "./bin/watercooler.js"
8
+ "watercooler": "bin/watercooler.js"
9
9
  },
10
10
  "files": [
11
11
  "bin/",
12
12
  "public/",
13
- "server.ts",
13
+ "dist/",
14
14
  "README.md",
15
15
  "LICENSE"
16
16
  ],
17
17
  "scripts": {
18
- "start": "tsx --watch server.ts"
18
+ "start": "node ./dist/server.js",
19
+ "build": "tsc"
19
20
  },
20
21
  "keywords": [
21
22
  "watercooler",
@@ -36,8 +37,7 @@
36
37
  },
37
38
  "dependencies": {
38
39
  "better-sqlite3": "^11.8.1",
39
- "express": "^4.21.2",
40
- "tsx": "^4.19.2"
40
+ "express": "^4.21.2"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/better-sqlite3": "^7.6.12",
package/public/app.js CHANGED
@@ -9,7 +9,7 @@ let config = { user: '', mailbox: '', avatar: null };
9
9
  let messages = []; // Messages TO user (for main panel)
10
10
  let allMessages = []; // All messages involving user (for desk dialogs)
11
11
  let recipients = [];
12
- let avatarStates = {}; // Map of name -> {tool_name, timestamp}
12
+ let statusStates = {}; // Map of name -> {tool_name, timestamp}
13
13
  let scene, camera, renderer, controls;
14
14
  let agentMeshes = new Map();
15
15
  let connectionLines = [];
@@ -980,14 +980,9 @@ function updateVillage() {
980
980
  clearConnections();
981
981
 
982
982
  // Use recipients (from coworkers.db) as the authoritative list of agents
983
+ // Only show people in the coworker list, not random message senders
983
984
  const allAgents = new Set([config.user.toLowerCase(), ...recipients.map(r => r.toLowerCase())]);
984
985
 
985
- // Also add message participants
986
- messages.forEach(m => {
987
- allAgents.add(m.sender.toLowerCase());
988
- allAgents.add(m.recipient.toLowerCase());
989
- });
990
-
991
986
  // Arrange agents in a circle on the platform
992
987
  const agents = Array.from(allAgents);
993
988
  const radius = Math.min(20, Math.max(10, agents.length * 3));
@@ -998,8 +993,8 @@ function updateVillage() {
998
993
  const z = Math.sin(angle) * radius;
999
994
  const position = new THREE.Vector3(x, 0, z);
1000
995
 
1001
- const avatarState = avatarStates[agent.toLowerCase()];
1002
- const toolName = avatarState?.tool_name || null;
996
+ const statusState = statusStates[agent.toLowerCase()];
997
+ const toolName = statusState?.tool_name || null;
1003
998
 
1004
999
  if (!agentMeshes.has(agent)) {
1005
1000
  const group = createAgentDesk(agent, position, toolName);
@@ -1163,14 +1158,14 @@ async function loadData() {
1163
1158
  recipients = Array.isArray(recipientsData) ? recipientsData : [];
1164
1159
  allMessages = Array.isArray(allMessagesData) ? allMessagesData : [];
1165
1160
 
1166
- // Load avatar states if avatar DB is configured
1167
- if (config.avatar) {
1161
+ // Load status states if status DB is configured
1162
+ if (config.status) {
1168
1163
  try {
1169
- const avatarsRes = await fetch('/api/avatars');
1170
- avatarStates = await avatarsRes.json();
1164
+ const statusRes = await fetch('/api/status');
1165
+ statusStates = await statusRes.json();
1171
1166
  } catch (err) {
1172
- console.error('Error loading avatar states:', err);
1173
- avatarStates = {};
1167
+ console.error('Error loading status states:', err);
1168
+ statusStates = {};
1174
1169
  }
1175
1170
  }
1176
1171
 
@@ -1537,8 +1532,8 @@ function updateDeskLabels() {
1537
1532
  !m.read
1538
1533
  ).length;
1539
1534
 
1540
- const avatarState = avatarStates[name.toLowerCase()];
1541
- const toolName = avatarState?.tool_name || null;
1535
+ const statusState = statusStates[name.toLowerCase()];
1536
+ const toolName = statusState?.tool_name || null;
1542
1537
 
1543
1538
  const sprite = group.children.find(c => c.isSprite);
1544
1539
  if (sprite) {
package/server.ts DELETED
@@ -1,303 +0,0 @@
1
- import express from 'express';
2
- import path from 'path';
3
- import { fileURLToPath } from 'url';
4
- import Database from 'better-sqlite3';
5
-
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = path.dirname(__filename);
8
-
9
- const app = express();
10
-
11
- // Parse CLI args
12
- const args = process.argv.slice(2);
13
- let user: string | null = null;
14
- let mailboxPath: string | null = null;
15
- let coworkerPath: string | null = null;
16
- let avatarPath: string | null = null;
17
- let port: number = parseInt(process.env.PORT || '3000', 10);
18
- let host: string = process.env.HOST || '0.0.0.0';
19
-
20
- for (let i = 0; i < args.length; i++) {
21
- if (args[i] === '--user' || args[i] === '-u') {
22
- user = args[++i];
23
- } else if (args[i] === '--mailbox' || args[i] === '-m') {
24
- mailboxPath = args[++i];
25
- } else if (args[i] === '--coworkers' || args[i] === '-c') {
26
- coworkerPath = args[++i];
27
- } else if (args[i] === '--avatars' || args[i] === '-a') {
28
- avatarPath = args[++i];
29
- } else if (args[i] === '--port' || args[i] === '-p') {
30
- const p = parseInt(args[++i], 10);
31
- if (!isNaN(p)) port = p;
32
- } else if (args[i] === '--host' || args[i] === '-h') {
33
- host = args[++i];
34
- }
35
- }
36
-
37
- if (!user || !mailboxPath) {
38
- console.error('Usage: watercooler --user <name> --mailbox <path> [--coworkers <path>] [--avatars <path>] [--port <number>] [--host <address>]');
39
- process.exit(1);
40
- }
41
-
42
- console.log(`🚰 Watercooler for ${user}`);
43
- console.log(` Mailbox: ${mailboxPath}`);
44
- if (coworkerPath) {
45
- console.log(` Coworker DB: ${coworkerPath}`);
46
- }
47
- if (avatarPath) {
48
- console.log(` Avatar DB: ${avatarPath}`);
49
- }
50
- console.log(` URL: http://${host}:${port}`);
51
-
52
- // Databases
53
- let db: Database.Database | null = null;
54
- let coworkerDb: Database.Database | null = null;
55
- let avatarDb: Database.Database | null = null;
56
-
57
- try {
58
- db = new Database(mailboxPath);
59
- console.log(' Mailbox DB: connected');
60
- } catch (err: any) {
61
- console.error(' Mailbox DB error:', err.message);
62
- process.exit(1);
63
- }
64
-
65
- if (coworkerPath) {
66
- try {
67
- coworkerDb = new Database(coworkerPath);
68
- console.log(' Coworker DB: connected');
69
- } catch (err: any) {
70
- console.warn(' Coworker DB error:', err.message);
71
- }
72
- }
73
-
74
- if (avatarPath) {
75
- try {
76
- avatarDb = new Database(avatarPath);
77
- console.log(' Avatar DB: connected');
78
- } catch (err: any) {
79
- console.warn(' Avatar DB error:', err.message);
80
- }
81
- }
82
-
83
- // Helper: Check if table exists
84
- function tableExists(database: Database.Database | null, tableName: string): boolean {
85
- if (!database) return false;
86
- try {
87
- const stmt = database.prepare(`
88
- SELECT name FROM sqlite_master
89
- WHERE type='table' AND name=?
90
- `);
91
- return !!stmt.get(tableName);
92
- } catch {
93
- return false;
94
- }
95
- }
96
-
97
- // Middleware
98
- app.use(express.json());
99
- app.use(express.static(path.join(__dirname, 'public')));
100
-
101
- // API: Get inbox (messages TO user)
102
- app.get('/api/messages', (req, res) => {
103
- try {
104
- if (!db) throw new Error('Database not connected');
105
- if (!tableExists(db, 'messages')) {
106
- res.json([]);
107
- return;
108
- }
109
- const stmt = db.prepare(`
110
- SELECT * FROM messages
111
- WHERE recipient = ?
112
- ORDER BY timestamp DESC
113
- `);
114
- res.json(stmt.all(user!.toLowerCase()));
115
- } catch (err: any) {
116
- res.status(500).json({ error: err.message });
117
- }
118
- });
119
-
120
- // API: Get sent messages (messages FROM user)
121
- app.get('/api/messages/sent', (req, res) => {
122
- try {
123
- if (!db) throw new Error('Database not connected');
124
- if (!tableExists(db, 'messages')) {
125
- res.json([]);
126
- return;
127
- }
128
- const stmt = db.prepare(`
129
- SELECT * FROM messages
130
- WHERE sender = ?
131
- ORDER BY timestamp DESC
132
- `);
133
- res.json(stmt.all(user!.toLowerCase()));
134
- } catch (err: any) {
135
- res.status(500).json({ error: err.message });
136
- }
137
- });
138
-
139
- // API: Get ALL messages between ALL agents
140
- app.get('/api/messages/all', (req, res) => {
141
- try {
142
- if (!db) throw new Error('Database not connected');
143
- if (!tableExists(db, 'messages')) {
144
- res.json([]);
145
- return;
146
- }
147
- const stmt = db.prepare(`
148
- SELECT * FROM messages
149
- ORDER BY timestamp DESC
150
- `);
151
- res.json(stmt.all());
152
- } catch (err: any) {
153
- res.status(500).json({ error: err.message });
154
- }
155
- });
156
-
157
- // API: Get all coworkers (from coworker.db + message recipients)
158
- app.get('/api/coworkers', (req, res) => {
159
- try {
160
- if (!db) throw new Error('Database not connected');
161
- const allCoworkers = new Set<string>();
162
-
163
- // Add from coworker.db if available
164
- if (coworkerDb) {
165
- try {
166
- const rows = coworkerDb.prepare('SELECT name FROM coworkers').all() as Array<{name: string}>;
167
-
168
- rows.forEach(row => allCoworkers.add(row.name.toLowerCase()));
169
- } catch (err: any) {
170
- console.error('Error reading coworker.db:', err.message);
171
- }
172
- } else {
173
- console.log('No coworkerDb connection available');
174
- }
175
-
176
- // Note: Messages table is in a different database, not queried here
177
-
178
- // Remove current user
179
- allCoworkers.delete(user!.toLowerCase());
180
-
181
- const result = Array.from(allCoworkers).sort();
182
- res.json(result);
183
- } catch (err: any) {
184
- console.error('Error in /api/coworkers:', err.message);
185
- res.status(500).json({ error: err.message });
186
- }
187
- });
188
-
189
- // Legacy: Get recipients (for backwards compat)
190
- app.get('/api/recipients', (req, res) => {
191
- try {
192
- if (!db) throw new Error('Database not connected');
193
- if (!tableExists(db, 'messages')) {
194
- res.json([]);
195
- return;
196
- }
197
- const stmt = db.prepare(`SELECT DISTINCT recipient FROM messages`);
198
- res.json(stmt.all().map((r: any) => r.recipient));
199
- } catch (err: any) {
200
- res.status(500).json({ error: err.message });
201
- }
202
- });
203
-
204
- // API: Send message
205
- app.post('/api/send', (req, res) => {
206
- try {
207
- if (!db) throw new Error('Database not connected');
208
-
209
- // Auto-create messages table if it doesn't exist
210
- if (!tableExists(db, 'messages')) {
211
- db.exec(`
212
- CREATE TABLE messages (
213
- id INTEGER PRIMARY KEY AUTOINCREMENT,
214
- recipient TEXT NOT NULL,
215
- sender TEXT NOT NULL,
216
- message TEXT NOT NULL,
217
- timestamp INTEGER NOT NULL,
218
- read INTEGER DEFAULT 0
219
- )
220
- `);
221
- }
222
-
223
- const { to, message } = req.body;
224
- const stmt = db.prepare(`
225
- INSERT INTO messages (recipient, sender, message, timestamp, read)
226
- VALUES (?, ?, ?, ?, 0)
227
- `);
228
- stmt.run(to.toLowerCase(), user!.toLowerCase(), message, Date.now());
229
- res.json({ success: true });
230
- } catch (err: any) {
231
- res.status(500).json({ error: err.message });
232
- }
233
- });
234
-
235
- // API: Mark read
236
- app.post('/api/messages/:id/read', (req, res) => {
237
- try {
238
- if (!db) throw new Error('Database not connected');
239
- if (!tableExists(db, 'messages')) {
240
- res.status(404).json({ error: 'Messages table not found' });
241
- return;
242
- }
243
- db.prepare('UPDATE messages SET read = 1 WHERE id = ?').run(req.params.id);
244
- res.json({ success: true });
245
- } catch (err: any) {
246
- res.status(500).json({ error: err.message });
247
- }
248
- });
249
-
250
- // API: Get avatar states (latest tool usage per coworker)
251
- app.get('/api/avatars', (req, res) => {
252
- try {
253
- if (!avatarDb) {
254
- res.json({});
255
- return;
256
- }
257
-
258
- // Check if latest_tool_usage table exists
259
- const tableCheck = avatarDb.prepare(`
260
- SELECT name FROM sqlite_master
261
- WHERE type='table' AND name='latest_tool_usage'
262
- `).get();
263
-
264
- if (!tableCheck) {
265
- res.json({});
266
- return;
267
- }
268
-
269
- // Get latest tool usage per name
270
- const stmt = avatarDb.prepare(`
271
- SELECT name, tool_name, timestamp
272
- FROM latest_tool_usage
273
- ORDER BY timestamp DESC
274
- `);
275
-
276
- const rows = stmt.all() as Array<{name: string; tool_name: string; timestamp: number}>;
277
-
278
- // Build map of name -> latest tool (first occurrence is latest due to ORDER BY)
279
- const avatarStates: Record<string, {tool_name: string; timestamp: number}> = {};
280
- for (const row of rows) {
281
- if (!avatarStates[row.name]) {
282
- avatarStates[row.name] = {
283
- tool_name: row.tool_name,
284
- timestamp: row.timestamp
285
- };
286
- }
287
- }
288
-
289
- res.json(avatarStates);
290
- } catch (err: any) {
291
- console.error('Error in /api/avatars:', err.message);
292
- res.status(500).json({ error: err.message });
293
- }
294
- });
295
-
296
- // Config endpoint
297
- app.get('/api/config', (req, res) => {
298
- res.json({ user, mailbox: mailboxPath, coworker: coworkerPath, avatar: avatarPath });
299
- });
300
-
301
- app.listen(port, host, () => {
302
- console.log('\nāœ… Watercooler running!');
303
- });