watercooler 0.0.9 ā 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/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/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 statusPath: 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] === '--status' || args[i] === '-s') {
|
|
28
|
-
statusPath = 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>] [--status <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 (statusPath) {
|
|
48
|
-
console.log(` Status DB: ${statusPath}`);
|
|
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 statusDb: 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 (statusPath) {
|
|
75
|
-
try {
|
|
76
|
-
statusDb = new Database(statusPath);
|
|
77
|
-
console.log(' Status DB: connected');
|
|
78
|
-
} catch (err: any) {
|
|
79
|
-
console.warn(' Status 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 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
|
-
|
|
258
|
-
// Check if latest_tool_usage table exists
|
|
259
|
-
const tableCheck = statusDb.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 = statusDb.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 statusStates: Record<string, {tool_name: string; timestamp: number}> = {};
|
|
280
|
-
for (const row of rows) {
|
|
281
|
-
if (!statusStates[row.name]) {
|
|
282
|
-
statusStates[row.name] = {
|
|
283
|
-
tool_name: row.tool_name,
|
|
284
|
-
timestamp: row.timestamp
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
res.json(statusStates);
|
|
290
|
-
} catch (err: any) {
|
|
291
|
-
console.error('Error in /api/status:', 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, status: statusPath });
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
app.listen(port, host, () => {
|
|
302
|
-
console.log('\nā
Watercooler running!');
|
|
303
|
-
});
|