woclaw-hub 0.1.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/Dockerfile +41 -0
- package/README.md +173 -0
- package/package.json +26 -0
- package/src/db.ts +128 -0
- package/src/index.ts +82 -0
- package/src/memory.ts +48 -0
- package/src/rest_server.ts +180 -0
- package/src/topics.ts +105 -0
- package/src/types.ts +65 -0
- package/src/ws_server.ts +376 -0
- package/test-connect.mjs +47 -0
- package/test.ts +166 -0
- package/tsconfig.json +19 -0
package/Dockerfile
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
FROM node:18-alpine
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Install build dependencies for better-sqlite3
|
|
6
|
+
RUN apk add --no-cache \
|
|
7
|
+
python3 \
|
|
8
|
+
make \
|
|
9
|
+
g++ \
|
|
10
|
+
&& rm -rf /var/cache/apk/*
|
|
11
|
+
|
|
12
|
+
# Copy package files
|
|
13
|
+
COPY package*.json ./
|
|
14
|
+
|
|
15
|
+
# Install dependencies
|
|
16
|
+
RUN npm ci --only=production
|
|
17
|
+
|
|
18
|
+
# Copy source (pre-built)
|
|
19
|
+
COPY dist ./dist
|
|
20
|
+
|
|
21
|
+
# Create data directory
|
|
22
|
+
RUN mkdir -p /data && chown -R node:node /data
|
|
23
|
+
|
|
24
|
+
# Expose ports
|
|
25
|
+
EXPOSE 8080 8081
|
|
26
|
+
|
|
27
|
+
# Health check
|
|
28
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
|
29
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:8081/health || exit 1
|
|
30
|
+
|
|
31
|
+
# Run as non-root user
|
|
32
|
+
USER node
|
|
33
|
+
|
|
34
|
+
ENV NODE_ENV=production
|
|
35
|
+
ENV HOST=0.0.0.0
|
|
36
|
+
ENV PORT=8080
|
|
37
|
+
ENV REST_PORT=8081
|
|
38
|
+
ENV DATA_DIR=/data
|
|
39
|
+
ENV AUTH_TOKEN=change-me-in-production
|
|
40
|
+
|
|
41
|
+
CMD ["node", "dist/index.js"]
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# WoClaw Hub
|
|
2
|
+
|
|
3
|
+
WebSocket relay server for OpenClaw multi-agent communication.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### Using Docker
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Build
|
|
11
|
+
docker build -t woclaw/hub .
|
|
12
|
+
|
|
13
|
+
# Run
|
|
14
|
+
docker run -d \
|
|
15
|
+
--name woclaw-hub \
|
|
16
|
+
-p 8080:8080 \
|
|
17
|
+
-p 8081:8081 \
|
|
18
|
+
-v /path/to/data:/data \
|
|
19
|
+
-e AUTH_TOKEN=your-secure-token \
|
|
20
|
+
--restart unless-stopped \
|
|
21
|
+
woclaw/hub
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### From Source
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cd hub
|
|
28
|
+
npm install
|
|
29
|
+
npm run build
|
|
30
|
+
npm start
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Environment Variables
|
|
34
|
+
|
|
35
|
+
| Variable | Default | Description |
|
|
36
|
+
|----------|---------|-------------|
|
|
37
|
+
| `PORT` | 8080 | WebSocket server port |
|
|
38
|
+
| `REST_PORT` | 8081 | REST API port (future) |
|
|
39
|
+
| `HOST` | 0.0.0.0 | Bind address |
|
|
40
|
+
| `DATA_DIR` | /data | SQLite database directory |
|
|
41
|
+
| `AUTH_TOKEN` | change-me | Authentication token |
|
|
42
|
+
| `CONFIG_FILE` | - | JSON config file path |
|
|
43
|
+
|
|
44
|
+
## WebSocket API
|
|
45
|
+
|
|
46
|
+
### Connect
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
const ws = new WebSocket('ws://localhost:8080?agentId=vm151&token=your-token');
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Send Message
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
ws.send(JSON.stringify({
|
|
56
|
+
type: 'message',
|
|
57
|
+
topic: 'openclaw-general',
|
|
58
|
+
content: 'Hello from vm151!'
|
|
59
|
+
}));
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Join Topic
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
ws.send(JSON.stringify({
|
|
66
|
+
type: 'join',
|
|
67
|
+
topic: 'openclaw-general'
|
|
68
|
+
}));
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Leave Topic
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
ws.send(JSON.stringify({
|
|
75
|
+
type: 'leave',
|
|
76
|
+
topic: 'openclaw-general'
|
|
77
|
+
}));
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Write to Shared Memory
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
ws.send(JSON.stringify({
|
|
84
|
+
type: 'memory_write',
|
|
85
|
+
key: 'project-status',
|
|
86
|
+
value: { status: 'in-progress', updated: new Date().toISOString() }
|
|
87
|
+
}));
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Read Memory
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
ws.send(JSON.stringify({
|
|
94
|
+
type: 'memory_read',
|
|
95
|
+
key: 'project-status'
|
|
96
|
+
}));
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### List Topics
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
ws.send(JSON.stringify({
|
|
103
|
+
type: 'topics_list'
|
|
104
|
+
}));
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Get Topic Members
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
ws.send(JSON.stringify({
|
|
111
|
+
type: 'topic_members',
|
|
112
|
+
topic: 'openclaw-general'
|
|
113
|
+
}));
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Server Responses
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
// New message
|
|
120
|
+
{ "type": "message", "topic": "...", "from": "vm151", "content": "...", "timestamp": 1234567890 }
|
|
121
|
+
|
|
122
|
+
// Join confirmation
|
|
123
|
+
{ "type": "join", "topic": "...", "agent": "vm151", "timestamp": 1234567890 }
|
|
124
|
+
|
|
125
|
+
// Leave notification
|
|
126
|
+
{ "type": "leave", "topic": "...", "agent": "vm151", "timestamp": 1234567890 }
|
|
127
|
+
|
|
128
|
+
// Message history (on join)
|
|
129
|
+
{ "type": "history", "topic": "...", "messages": [...], "agents": [...] }
|
|
130
|
+
|
|
131
|
+
// Memory update broadcast
|
|
132
|
+
{ "type": "memory_update", "key": "...", "value": "...", "from": "vm151", "timestamp": 1234567890 }
|
|
133
|
+
|
|
134
|
+
// Memory value response
|
|
135
|
+
{ "type": "memory_value", "key": "...", "value": "...", "exists": true, "updatedAt": ..., "updatedBy": "vm151" }
|
|
136
|
+
|
|
137
|
+
// Topics list
|
|
138
|
+
{ "type": "topics_list", "topics": [{ "name": "...", "agents": 3 }] }
|
|
139
|
+
|
|
140
|
+
// Topic members
|
|
141
|
+
{ "type": "topic_members", "topic": "...", "agents": ["vm151", "vm152"] }
|
|
142
|
+
|
|
143
|
+
// Error
|
|
144
|
+
{ "type": "error", "code": "...", "message": "...", "timestamp": 1234567890 }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Architecture
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
151
|
+
│ WoClaw Hub │
|
|
152
|
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
153
|
+
│ │ Topics Mgr │ │ Memory Pool │ │ SQLite │ │
|
|
154
|
+
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
|
155
|
+
│ │ │ │ │
|
|
156
|
+
│ └────────────────┼────────────────┘ │
|
|
157
|
+
│ │ │
|
|
158
|
+
│ ┌───────┴───────┐ │
|
|
159
|
+
│ │ WSServer │ │
|
|
160
|
+
│ └───────┬───────┘ │
|
|
161
|
+
└──────────────────────────┼───────────────────────────────────┘
|
|
162
|
+
│
|
|
163
|
+
┌─────────────────┼─────────────────┐
|
|
164
|
+
│ │ │
|
|
165
|
+
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
|
|
166
|
+
│ vm151 │ │ vm152 │ │ vm153 │
|
|
167
|
+
│ (p14) │ │ │ │ │
|
|
168
|
+
└─────────┘ └─────────┘ └─────────┘
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "woclaw-hub",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "WoClaw Hub - WebSocket relay server for OpenClaw multi-agent communication",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/index.js",
|
|
10
|
+
"dev": "tsx watch src/index.ts",
|
|
11
|
+
"docker:build": "docker build -t woclaw/hub ."
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"ws": "^8.16.0",
|
|
15
|
+
"uuid": "^9.0.1"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/uuid": "^9.0.7",
|
|
19
|
+
"@types/ws": "^8.5.10",
|
|
20
|
+
"tsx": "^4.7.1",
|
|
21
|
+
"typescript": "^5.3.3"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/db.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// WoClaw Database - Simple JSON File Store
|
|
2
|
+
// No native compilation required!
|
|
3
|
+
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
5
|
+
import { dirname } from 'path';
|
|
6
|
+
import { DBMessage, DBMemory } from './types.js';
|
|
7
|
+
|
|
8
|
+
export class ClawDB {
|
|
9
|
+
private dbPath: string;
|
|
10
|
+
private data: {
|
|
11
|
+
messages: DBMessage[];
|
|
12
|
+
memory: { key: string; value: string; updatedAt: number; updatedBy: string }[];
|
|
13
|
+
topics: { name: string; createdAt: number; messageCount: number }[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
constructor(dataDir: string) {
|
|
17
|
+
if (!existsSync(dataDir)) {
|
|
18
|
+
mkdirSync(dataDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
this.dbPath = `${dataDir}/woclaw.json`;
|
|
21
|
+
this.data = this.load();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private load() {
|
|
25
|
+
if (existsSync(this.dbPath)) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(this.dbPath, 'utf-8'));
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error('[ClawDB] Failed to load DB, starting fresh');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
messages: [],
|
|
34
|
+
memory: [],
|
|
35
|
+
topics: [],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private save() {
|
|
40
|
+
try {
|
|
41
|
+
writeFileSync(this.dbPath, JSON.stringify(this.data, null, 2));
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.error('[ClawDB] Failed to save:', e);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Messages
|
|
48
|
+
saveMessage(msg: DBMessage): void {
|
|
49
|
+
this.data.messages.push(msg);
|
|
50
|
+
// Keep last 1000 messages per topic (simple cleanup)
|
|
51
|
+
if (this.data.messages.length > 10000) {
|
|
52
|
+
this.data.messages = this.data.messages.slice(-5000);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Update topic stats
|
|
56
|
+
const topic = this.data.topics.find(t => t.name === msg.topic);
|
|
57
|
+
if (topic) {
|
|
58
|
+
topic.messageCount++;
|
|
59
|
+
} else {
|
|
60
|
+
this.data.topics.push({ name: msg.topic, createdAt: Date.now(), messageCount: 1 });
|
|
61
|
+
}
|
|
62
|
+
this.save();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getMessages(topic: string, limit: number = 100, before?: number): DBMessage[] {
|
|
66
|
+
let msgs = this.data.messages.filter(m => m.topic === topic);
|
|
67
|
+
if (before) {
|
|
68
|
+
msgs = msgs.filter(m => m.timestamp < before);
|
|
69
|
+
}
|
|
70
|
+
return msgs.slice(-limit).reverse();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Memory
|
|
74
|
+
setMemory(key: string, value: string, updatedBy: string): void {
|
|
75
|
+
const existing = this.data.memory.find(m => m.key === key);
|
|
76
|
+
if (existing) {
|
|
77
|
+
existing.value = value;
|
|
78
|
+
existing.updatedAt = Date.now();
|
|
79
|
+
existing.updatedBy = updatedBy;
|
|
80
|
+
} else {
|
|
81
|
+
this.data.memory.push({ key, value, updatedAt: Date.now(), updatedBy });
|
|
82
|
+
}
|
|
83
|
+
this.save();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getMemory(key: string): DBMemory | undefined {
|
|
87
|
+
const m = this.data.memory.find(mem => mem.key === key);
|
|
88
|
+
if (!m) return undefined;
|
|
89
|
+
return {
|
|
90
|
+
key: m.key,
|
|
91
|
+
value: m.value,
|
|
92
|
+
updatedAt: m.updatedAt,
|
|
93
|
+
updatedBy: m.updatedBy,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
deleteMemory(key: string): boolean {
|
|
98
|
+
const idx = this.data.memory.findIndex(m => m.key === key);
|
|
99
|
+
if (idx >= 0) {
|
|
100
|
+
this.data.memory.splice(idx, 1);
|
|
101
|
+
this.save();
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getAllMemory(): DBMemory[] {
|
|
108
|
+
return this.data.memory.map(m => ({
|
|
109
|
+
key: m.key,
|
|
110
|
+
value: m.value,
|
|
111
|
+
updatedAt: m.updatedAt,
|
|
112
|
+
updatedBy: m.updatedBy,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Topics
|
|
117
|
+
getTopicStats(): { name: string; messageCount: number; createdAt: number }[] {
|
|
118
|
+
return this.data.topics.map(t => ({
|
|
119
|
+
name: t.name,
|
|
120
|
+
messageCount: t.messageCount,
|
|
121
|
+
createdAt: t.createdAt,
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
close(): void {
|
|
126
|
+
this.save();
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { WSServer } from './ws_server.js';
|
|
2
|
+
import { RestServer } from './rest_server.js';
|
|
3
|
+
import { ClawDB } from './db.js';
|
|
4
|
+
import { Config } from './types.js';
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CONFIG: Config = {
|
|
8
|
+
port: parseInt(process.env.PORT || '8080'),
|
|
9
|
+
restPort: parseInt(process.env.REST_PORT || '8081'),
|
|
10
|
+
host: process.env.HOST || '0.0.0.0',
|
|
11
|
+
dataDir: process.env.DATA_DIR || '/data',
|
|
12
|
+
authToken: process.env.AUTH_TOKEN || 'change-me-in-production',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
console.log(`
|
|
17
|
+
██████╗ ███████╗██╗ ██╗ ██╗ ██╗███╗ ██╗██╗ ██╗██╗ ██╗
|
|
18
|
+
██╔══██╗██╔════╝██║ ██║ ██║ ██║████╗ ██║██║ ██║╚██╗██╔╝
|
|
19
|
+
██║ ██║█████╗ ██║ ██║ ██║ ██║██╔██╗ ██║██║ ██║ ╚███╔╝
|
|
20
|
+
██║ ██║██╔══╝ ╚██╗ ██╔╝ ██║ ██║██║╚██╗██║██║ ██║ ██╔██╗
|
|
21
|
+
██████╔╝███████╗ ╚████╔╝ ███████╗██║██║ ╚████║╚██████╔╝██╔╝ ██╗
|
|
22
|
+
╚═════╝ ╚══════╝ ╚═══╝ ╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝
|
|
23
|
+
|
|
24
|
+
OpenClaw Multi-Agent Communication Hub
|
|
25
|
+
`);
|
|
26
|
+
|
|
27
|
+
// Load config from environment or file
|
|
28
|
+
let config = DEFAULT_CONFIG;
|
|
29
|
+
const configPath = process.env.CONFIG_FILE;
|
|
30
|
+
if (configPath) {
|
|
31
|
+
try {
|
|
32
|
+
const fileConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
33
|
+
config = { ...config, ...fileConfig };
|
|
34
|
+
console.log(`[WoClaw] Loaded config from ${configPath}`);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.error(`[WoClaw] Failed to load config: ${e}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(`[WoClaw] Configuration:`);
|
|
42
|
+
console.log(` WebSocket Port: ${config.port}`);
|
|
43
|
+
console.log(` REST Port: ${config.restPort}`);
|
|
44
|
+
console.log(` Host: ${config.host}`);
|
|
45
|
+
console.log(` Data Dir: ${config.dataDir}`);
|
|
46
|
+
console.log(` Auth Token: ${config.authToken.substring(0, 8)}...`);
|
|
47
|
+
console.log('');
|
|
48
|
+
|
|
49
|
+
// Initialize database
|
|
50
|
+
const db = new ClawDB(config.dataDir);
|
|
51
|
+
console.log('[WoClaw] Database initialized');
|
|
52
|
+
|
|
53
|
+
// Initialize WebSocket server (this also creates TopicsManager and MemoryPool internally)
|
|
54
|
+
const wsServer = new WSServer(config, db);
|
|
55
|
+
|
|
56
|
+
// Start REST API server with access to db, topics, memory
|
|
57
|
+
const restServer = new RestServer(config, db, wsServer.getTopicsManager(), wsServer.getMemoryPool());
|
|
58
|
+
restServer.start();
|
|
59
|
+
|
|
60
|
+
console.log('[WoClaw] Server started successfully');
|
|
61
|
+
console.log('');
|
|
62
|
+
console.log('[WoClaw] Endpoints:');
|
|
63
|
+
console.log(` WebSocket: ws://${config.host}:${config.port}`);
|
|
64
|
+
console.log(` REST API: http://${config.host}:${config.restPort}`);
|
|
65
|
+
console.log('');
|
|
66
|
+
|
|
67
|
+
// Graceful shutdown
|
|
68
|
+
const shutdown = () => {
|
|
69
|
+
console.log('[WoClaw] Shutting down...');
|
|
70
|
+
restServer.close();
|
|
71
|
+
wsServer.close();
|
|
72
|
+
process.exit(0);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
process.on('SIGINT', shutdown);
|
|
76
|
+
process.on('SIGTERM', shutdown);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
main().catch((e) => {
|
|
80
|
+
console.error('[WoClaw] Fatal error:', e);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
});
|
package/src/memory.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ClawDB } from './db.js';
|
|
2
|
+
import { DBMemory, OutboundMessage } from './types.js';
|
|
3
|
+
|
|
4
|
+
export class MemoryPool {
|
|
5
|
+
private db: ClawDB;
|
|
6
|
+
private subscribers: Map<string, (msg: OutboundMessage) => void> = new Map();
|
|
7
|
+
|
|
8
|
+
constructor(db: ClawDB) {
|
|
9
|
+
this.db = db;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
write(key: string, value: any, updatedBy: string): DBMemory {
|
|
13
|
+
const serialized = typeof value === 'string' ? value : JSON.stringify(value);
|
|
14
|
+
this.db.setMemory(key, serialized, updatedBy);
|
|
15
|
+
return this.db.getMemory(key)!;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
read(key: string): DBMemory | undefined {
|
|
19
|
+
return this.db.getMemory(key);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
delete(key: string): boolean {
|
|
23
|
+
return this.db.deleteMemory(key);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getAll(): DBMemory[] {
|
|
27
|
+
return this.db.getAllMemory();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// For agents that want to subscribe to memory changes
|
|
31
|
+
subscribe(agentId: string, callback: (msg: OutboundMessage) => void): void {
|
|
32
|
+
this.subscribers.set(agentId, callback);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
unsubscribe(agentId: string): void {
|
|
36
|
+
this.subscribers.delete(agentId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
notifySubscribers(message: OutboundMessage): void {
|
|
40
|
+
for (const callback of this.subscribers.values()) {
|
|
41
|
+
try {
|
|
42
|
+
callback(message);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error('Error notifying subscriber:', e);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { ClawDB } from './db.js';
|
|
3
|
+
import { TopicsManager } from './topics.js';
|
|
4
|
+
import { MemoryPool } from './memory.js';
|
|
5
|
+
import { Config } from './types.js';
|
|
6
|
+
|
|
7
|
+
export class RestServer {
|
|
8
|
+
private server: http.Server | null = null;
|
|
9
|
+
private db: ClawDB;
|
|
10
|
+
private topics: TopicsManager;
|
|
11
|
+
private memory: MemoryPool;
|
|
12
|
+
private config: Config;
|
|
13
|
+
|
|
14
|
+
constructor(config: Config, db: ClawDB, topics: TopicsManager, memory: MemoryPool) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.db = db;
|
|
17
|
+
this.topics = topics;
|
|
18
|
+
this.memory = memory;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
start(): void {
|
|
22
|
+
this.server = http.createServer((req, res) => {
|
|
23
|
+
this.handleRequest(req, res);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
this.server.listen(this.config.restPort, () => {
|
|
27
|
+
console.log(`[WoClaw] REST API running on http://${this.config.host}:${this.config.restPort}`);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
32
|
+
// CORS headers
|
|
33
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
34
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
35
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
36
|
+
|
|
37
|
+
if (req.method === 'OPTIONS') {
|
|
38
|
+
res.writeHead(200);
|
|
39
|
+
res.end();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
44
|
+
const path = url.pathname;
|
|
45
|
+
const method = req.method || 'GET';
|
|
46
|
+
|
|
47
|
+
// Auth check for write operations
|
|
48
|
+
const authHeader = req.headers.authorization;
|
|
49
|
+
if (method !== 'GET' && authHeader !== `Bearer ${this.config.authToken}`) {
|
|
50
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
51
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
if (path === '/health') {
|
|
57
|
+
this.handleHealth(res);
|
|
58
|
+
} else if (path === '/topics') {
|
|
59
|
+
if (method === 'GET') {
|
|
60
|
+
this.handleTopicsList(res);
|
|
61
|
+
} else {
|
|
62
|
+
res.writeHead(405);
|
|
63
|
+
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
64
|
+
}
|
|
65
|
+
} else if (path === '/memory') {
|
|
66
|
+
if (method === 'GET') {
|
|
67
|
+
this.handleMemoryList(res);
|
|
68
|
+
} else {
|
|
69
|
+
res.writeHead(405);
|
|
70
|
+
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
71
|
+
}
|
|
72
|
+
} else if (path.startsWith('/memory/')) {
|
|
73
|
+
const key = decodeURIComponent(path.slice(8));
|
|
74
|
+
if (method === 'GET') {
|
|
75
|
+
this.handleMemoryGet(res, key);
|
|
76
|
+
} else if (method === 'DELETE') {
|
|
77
|
+
this.handleMemoryDelete(res, key);
|
|
78
|
+
} else {
|
|
79
|
+
res.writeHead(405);
|
|
80
|
+
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
81
|
+
}
|
|
82
|
+
} else if (path.startsWith('/topics/')) {
|
|
83
|
+
const topicName = decodeURIComponent(path.slice(8));
|
|
84
|
+
if (method === 'GET') {
|
|
85
|
+
this.handleTopicMessages(res, topicName, url.searchParams.get('limit'));
|
|
86
|
+
} else {
|
|
87
|
+
res.writeHead(405);
|
|
88
|
+
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
res.writeHead(404);
|
|
92
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
93
|
+
}
|
|
94
|
+
} catch (e: any) {
|
|
95
|
+
console.error('[WoClaw] REST error:', e.message);
|
|
96
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
97
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private handleHealth(res: http.ServerResponse): void {
|
|
102
|
+
const stats = this.topics.getStats();
|
|
103
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
104
|
+
res.end(JSON.stringify({
|
|
105
|
+
status: 'ok',
|
|
106
|
+
uptime: process.uptime(),
|
|
107
|
+
timestamp: Date.now(),
|
|
108
|
+
agents: stats.totalAgents,
|
|
109
|
+
topics: stats.totalTopics,
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private handleTopicsList(res: http.ServerResponse): void {
|
|
114
|
+
const stats = this.topics.getStats();
|
|
115
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
116
|
+
res.end(JSON.stringify({
|
|
117
|
+
topics: stats.topicDetails.map(t => ({
|
|
118
|
+
name: t.name,
|
|
119
|
+
agents: t.agents,
|
|
120
|
+
}))
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private handleMemoryList(res: http.ServerResponse): void {
|
|
125
|
+
const allMemory = this.memory.getAll();
|
|
126
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
127
|
+
res.end(JSON.stringify({
|
|
128
|
+
memory: allMemory.map(m => ({
|
|
129
|
+
key: m.key,
|
|
130
|
+
updatedAt: m.updatedAt,
|
|
131
|
+
updatedBy: m.updatedBy,
|
|
132
|
+
}))
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private handleMemoryGet(res: http.ServerResponse, key: string): void {
|
|
137
|
+
const mem = this.memory.read(key);
|
|
138
|
+
if (!mem) {
|
|
139
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
140
|
+
res.end(JSON.stringify({ error: 'Key not found' }));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
144
|
+
res.end(JSON.stringify({
|
|
145
|
+
key: mem.key,
|
|
146
|
+
value: mem.value,
|
|
147
|
+
updatedAt: mem.updatedAt,
|
|
148
|
+
updatedBy: mem.updatedBy,
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private handleMemoryDelete(res: http.ServerResponse, key: string): void {
|
|
153
|
+
const deleted = this.memory.delete(key);
|
|
154
|
+
if (!deleted) {
|
|
155
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
156
|
+
res.end(JSON.stringify({ error: 'Key not found' }));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
160
|
+
res.end(JSON.stringify({ success: true, key }));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private handleTopicMessages(res: http.ServerResponse, topic: string, limit?: string | null): void {
|
|
164
|
+
const limitNum = Math.min(parseInt(limit || '50'), 200);
|
|
165
|
+
const messages = this.db.getMessages(topic, limitNum);
|
|
166
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
167
|
+
res.end(JSON.stringify({
|
|
168
|
+
topic,
|
|
169
|
+
messages: messages.reverse(),
|
|
170
|
+
count: messages.length,
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
close(): void {
|
|
175
|
+
if (this.server) {
|
|
176
|
+
this.server.close();
|
|
177
|
+
console.log('[WoClaw] REST server closed');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|