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 +8 -5
- package/bin/watercooler.js +2 -2
- package/dist/server.js +296 -0
- package/package.json +7 -7
- package/public/app.js +12 -17
- package/server.ts +0 -303
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
# Watercooler Village
|
|
2
1
|
|
|
3
|
-
|
|
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
|
|
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**:
|
|
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.
|
package/bin/watercooler.js
CHANGED
|
@@ -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.
|
|
9
|
+
const serverPath = join(__dirname, '..', 'dist', 'server.js');
|
|
10
10
|
|
|
11
|
-
const child = spawn('
|
|
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.
|
|
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.
|
|
6
|
+
"main": "dist/server.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"watercooler": "
|
|
8
|
+
"watercooler": "bin/watercooler.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"bin/",
|
|
12
12
|
"public/",
|
|
13
|
-
"
|
|
13
|
+
"dist/",
|
|
14
14
|
"README.md",
|
|
15
15
|
"LICENSE"
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
|
-
"start": "
|
|
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
|
|
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
|
|
1002
|
-
const toolName =
|
|
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
|
|
1167
|
-
if (config.
|
|
1161
|
+
// Load status states if status DB is configured
|
|
1162
|
+
if (config.status) {
|
|
1168
1163
|
try {
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1164
|
+
const statusRes = await fetch('/api/status');
|
|
1165
|
+
statusStates = await statusRes.json();
|
|
1171
1166
|
} catch (err) {
|
|
1172
|
-
console.error('Error loading
|
|
1173
|
-
|
|
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
|
|
1541
|
-
const toolName =
|
|
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
|
-
});
|